Où se trouve réellement le code
Un e-mail de vérification a jusqu’à trois endroits où le code peut se trouver, et celui que vous devez lire détermine tout ce qui suit. Le JSON du message renvoyé par l’API vous donne les trois à la fois : subject, text (la partie texte brut, ou null) et html (la partie HTML, ou null).
| Où | À quoi ça ressemble | Comment le lire |
|---|---|---|
La partie texte brut (text) | Your code is 481920. It expires in 10 minutes. | Analysez cette partie en premier quand elle existe. Aucun balisage, rien à décoder, et la formulation est stable. |
La partie HTML (html) | La même phrase à l’intérieur d’un tableau, souvent avec les chiffres stylisés un par cellule, et chaque & écrit sous forme d’entité. | Remplacez les balises par des espaces, décodez les entités, normalisez les espaces, puis appliquez le motif. Ne faites jamais de regex sur du HTML brut. |
| La ligne de sujet | 481920 is your verification code | Un cadeau quand l’expéditeur le fait : aucun corps à analyser du tout. Faites correspondre sur le sujet, avec le corps en repli. |
| Une image | Le code dessiné comme une image, pour déjouer exactement ce genre de script. | Rare, et un signe que l’expéditeur ne veut pas d’automatisation. Changez le modèle de l’expéditeur si c’est le vôtre ; il n’y a pas de contournement honnête si ce n’est pas le cas. |
La partie texte brut est celle à privilégier, et c’est celle que la plupart des systèmes de templating génèrent automatiquement à partir du HTML — elle est donc généralement présente. Quand elle vaut null, la partie HTML est le seul corps, et les deux sections suivantes expliquent comment la lire en toute sécurité.
Ancrez le motif sur votre propre formulation
Le réflexe, c’est \d{6}. Ça correspond au code, mais aussi à l’année dans le pied de page, au code postal dans le bloc d’adresse, aux six derniers chiffres d’un numéro de téléphone, et au numéro de commande qui apparaît deux lignes au-dessus du code. Celui qui apparaît en premier gagne, et le test le saisit dans le formulaire avec une confiance totale.
| Motif | Correspond aussi à | Verdict |
|---|---|---|
\d{6} | Années, codes postaux, prix sans séparateur, numéros de commande, numéros de téléphone, numéros de suivi. | Jamais. Ce n’est pas un motif, c’est un coup de dé. |
\b\d{6}\b | Tout ce qui précède, dès lors que c’est exactement six chiffres avec un espace de chaque côté — donc encore la plupart des cas. | À peine mieux. Les limites de mots ne savent pas ce qu’est un code. |
code is\D{0,12}(\d{6}) | Seulement les six chiffres qui suivent les mots que votre modèle place avant le code, avec de la place pour un deux-points, un espace, ou les espaces résiduels laissés par une balise. | Oui. Ça ne correspond qu’au code et à rien d’autre, et ça échoue le jour où quelqu’un reformule l’e-mail — un échec dont vous voulez justement être informé. |
Le \D{0,12} est le détail pratique : une fois les balises remplacées par des espaces, les mots et les chiffres peuvent être séparés par un deux-points, une suite d’espaces, ou les résidus d’un <strong> qui se trouvait autrefois entre eux. Jusqu’à une douzaine de non-chiffres couvre tout ça sans laisser le motif sauter vers un autre nombre.
Les modèles qui séparent les chiffres
Un design populaire place chaque chiffre du code dans sa propre case, pour que ce soit lisible sur un téléphone. En HTML, cela donne six cellules de tableau, ou six <span>, et le nombre n’apparaît jamais comme six caractères consécutifs nulle part dans la source :
<p>Your code is</p>
<table><tr>
<td class="digit">4</td><td class="digit">8</td><td class="digit">1</td>
<td class="digit">9</td><td class="digit">2</td><td class="digit">0</td>
</tr></table>Une regex sur le HTML brut ne trouve rien. La solution n’est pas une regex plus astucieuse ; c’est de transformer d’abord le HTML en texte, dans un ordre fixe :
- Remplacez chaque balise par un espace. Un espace, pas rien —
<td>4</td><td>8</td>doit devenir4 8, pas48collé à ce qui suivait. - Décodez les entités.
&, ,'. Un espace insécable entre deux chiffres n’est pas un espace pour une regex tant qu’il n’est pas décodé. - Normalisez les espaces, puis faites correspondre en autorisant des espaces entre les chiffres. Pour le design en cases,
code is\D{0,12}(\d)\s*(\d)\s*(\d)\s*(\d)\s*(\d)\s*(\d), puis assemblez les groupes ; pour un modèle normal, le motif simple de la section précédente suffit.
Les utilitaires ci-dessous font les étapes un et deux à votre place, et recherchent dans les deux parties à la fois, de sorte qu’un test n’a pas besoin de savoir quel design le modèle utilise ce mois-ci.
Le message le plus récent n’est pas toujours le bon
Ici, chaque listage de boîte revient avec le plus récent en premier, et messages[0] est ce que lit la plupart des premiers jets. Trois situations font de celui-ci le mauvais message :
- Deux messages pour une seule action
- L’inscription envoie un e-mail de bienvenue et un e-mail de code, dans l’ordre où la file de l’expéditeur se vide. La moitié du temps, le message de bienvenue est le plus récent. Filtrez sur le sujet, ou sur l’expéditeur, avant de prendre quoi que ce soit.
- Un renvoi
- Le test a demandé le code deux fois — une fois par erreur, une fois volontairement — et le serveur n’accepte que le plus récent. L’ancien message est toujours dans la boîte, correspond toujours au motif, et se transforme toujours en six chiffres désormais invalides.
- Une exécution de test précédente
- Seulement si l’adresse a été réutilisée, ce qui ne devrait jamais arriver. Une adresse fraîche par exécution rend ce cas impossible ; si vous ne pouvez pas en avoir une, l’instantané ci-dessous sert de solution de repli.
La forme robuste est la même dans chaque runner : regardez ce qu’il y a dans la boîte avant de déclencher le courrier, puis n’acceptez qu’un message qui n’était pas encore là et qui correspond au sujet attendu.
// Remember what is already there, THEN trigger the resend, THEN wait for something new.
const before = new Set((await listMailbox(address)).messages.map(m => m.id));
await page.getByRole('button', { name: 'Resend code' }).click();
const fresh = await waitFor(address, m => !before.has(m.id) && /code/i.test(m.subject));Les codes qui expirent pendant l’exécution
La plupart des codes à usage unique sont valides de cinq à quinze minutes. Cela semble généreux jusqu’à ce qu’une suite de tests mette vingt specs en file d’attente, chacun ayant demandé son code au début et l’ayant saisi à la fin. Trois règles gardent le code valide :
- Demandez le code aussi tard que possible. Déclenchez l’envoi juste avant l’attente, pas dans une étape de préparation qui s’exécute pendant que d’autres tests attendent en file.
- Gardez l’échéance d’attente bien en dessous de la durée de vie du code. Une échéance de soixante secondes pour un code de dix minutes laisse neuf minutes pour le saisir. Une échéance de dix minutes ne laisse rien.
- Ne stockez jamais un code pour un autre test. Les codes sont à usage unique autant qu’à courte durée de vie ; une fixture partagée qui en distribue un crée une course entre deux tests pour un même nombre.
Extracteurs prêts à l’emploi
Trois versions des quatre mêmes lignes : les deux parties jointes, les balises remplacées par des espaces, les entités décodées, les espaces normalisés, puis le motif ancré. Changez le motif pour qu’il corresponde à la formulation de votre modèle, et rien d’autre n’a besoin d’être touché.
Depuis un shell, avec jq
curl -sG "https://grabmail.io/api/v1/message/$ID" --data-urlencode "mailbox=$ADDR" \
| jq -r '[.text, .html] | map(select(. != null)) | join(" ") | gsub("<[^>]*>"; " ")' \
| grep -oiE 'code is[^0-9]{0,12}[0-9]{6}' | grep -oE '[0-9]{6}' | head -1Python
import html
import re
TAGS = re.compile(r"<[^>]+>")
CODE = re.compile(r"code is\D{0,12}(\d{6})", re.I) # anchored on YOUR template's wording
def text_of(message: dict) -> str:
"""Both parts as plain text: tags out, entities decoded, whitespace folded."""
raw = f"{message.get('text') or ''}\n{message.get('html') or ''}"
return re.sub(r"\s+", " ", html.unescape(TAGS.sub(" ", raw)))
def code_from(message: dict, pattern: re.Pattern = CODE) -> str:
hit = pattern.search(text_of(message))
if not hit:
raise AssertionError(f"no code in message {message['id']!r} ({message['subject']!r})")
return hit.group(1)TypeScript
export type Message = { id: string; subject: string; text: string | null; html: string | null };
const TAGS = /<[^>]+>/g;
const ENTITIES: Record<string, string> = { '&': '&', '<': '<', '>': '>', '"': '"', ''': "'", ' ': ' ' };
/** Both parts as plain text: tags out, the common entities decoded, whitespace folded. */
export const textOf = (m: Message): string =>
`${m.text ?? ''}\n${m.html ?? ''}`
.replace(TAGS, ' ')
.replace(/&(amp|lt|gt|quot|#39|nbsp);/g, e => ENTITIES[e])
.replace(/\s+/g, ' ');
/** Anchored on your own wording. A reworded template fails loudly. */
export function codeFrom(m: Message, pattern = /code is\D{0,12}(\d{6})/i): string {
const hit = textOf(m).match(pattern);
if (!hit) throw new Error(`no code in message ${m.id} ("${m.subject}")`);
return hit[1];
}Le JSON de message que ces extracteurs lisent provient de GET /api/v1/message/{id}, documenté dans la référence de l’API ; l’attente qui vous procure d’abord l’id se trouve dans le guide de test de bout en bout, et sous forme d’utilitaires prêts à l’emploi pour Playwright, Cypress, Python et Node.js.
Avant de considérer que c’est terminé
- Le motif ancré sur la formulation de votre modèle, et conservé à côté du modèle.
- Les deux parties recherchées, en tant que texte : balises remplacées par des espaces, entités décodées, espaces normalisés.
- Un filtre sur le sujet ou l’expéditeur, pour qu’un e-mail de bienvenue ne l’emporte jamais sur un e-mail de code.
- Un instantané avant tout renvoi, et seuls les nouveaux messages acceptés après.
- L’échéance d’attente bien en dessous de la durée de vie du code, et l’envoi déclenché juste avant l’attente.
- Un message d’échec qui nomme l’id du message et le sujet dans lequel il a cherché.
Cela couvre toutes les façons dont on a vu un extracteur à six chiffres réussir sur le mauvais nombre. Un agent qui lit le même courrier a les mêmes problèmes et un outil de moins pour les résoudre, c’est pourquoi le serveur MCP lui remet le message entier plutôt qu’une supposition — une boîte qu’un agent IA peut lire détaille tout ça.
Questions
Dois-je lire la partie texte ou la partie HTML ?
La partie texte quand elle existe : elle est stable et n’a rien à décoder. Recherchez quand même dans les deux, comme le font les utilitaires, pour qu’un modèle qui n’envoie que du HTML fonctionne quand même, et qu’un modèle qui n’envoie que du texte ne trébuche jamais sur un HTML vide.
Mon code contient des lettres. Le motif change-t-il ?
Seulement la classe de caractères : ([A-Z0-9]{6}), ou quel que soit l’alphabet utilisé par l’expéditeur, toujours ancrée sur la formulation qui précède. Ajoutez l’indicateur i si la casse n’est pas garantie, et veillez à ce que la classe ne corresponde pas aussi à un mot anglais qui suit l’ancre.
Qu’en est-il des liens magiques à la place des codes ?
Même discipline, motif différent : faites correspondre l’URL sur un fragment de chemin que vous connaissez — /confirm/, /auth/magic/ — plutôt que sur « le premier lien », parce qu’un e-mail transactionnel porte généralement cinq liens et que celui que vous voulez est rarement le premier. Décodez & avant de le visiter.
Combien de temps un message reste-t-il disponible à la lecture ?
5 jours après son arrivée, qu’il ait été lu ou non. C’est bien plus long que la durée de validité de n’importe quel code, donc un test n’a jamais besoin de se dépêcher pour la lecture — seulement pour la saisie.
Puis-je obtenir le code sans interroger la boîte ?
Via REST, non : vous interrogez une fois par seconde avec une échéance, ce qui est le rythme documenté et n’est jamais limité en deçà. Via MCP, il existe un outil wait_for_message qui garde l’appel ouvert jusqu’à ce que le message arrive, ce qui est la forme dont un agent IA a besoin.
Ai-je besoin d’une clé d’API ?
Non. Les domaines publics ne demandent ni clé, ni compte, ni en-tête. Seul le pool payant de domaines tenus à l’écart des listes noires de mail jetable utilise un token bearer, et le code d’extraction est identique dans les deux cas.


