Onde o código realmente está
Um e-mail de verificação tem até três lugares onde o código pode estar, e qual deles você deve ler decide tudo o que vem depois. O JSON da mensagem que a API retorna dá os três de uma vez: subject, text (a parte em texto puro, ou null) e html (a parte em HTML, ou null).
| Onde | Como se parece | Como ler |
|---|---|---|
A parte em texto puro (text) | Your code is 481920. It expires in 10 minutes. | Analise essa primeiro quando ela existir. Sem marcação, nada para decodificar, e o texto é estável. |
A parte em HTML (html) | A mesma frase dentro de uma tabela, muitas vezes com os dígitos estilizados um por célula, e todo & escrito como uma entidade. | Substitua as tags por espaços, decodifique as entidades, colapse os espaços em branco, depois aplique o padrão. Nunca use regex direto no HTML bruto. |
| A linha de assunto | 481920 is your verification code | Um presente quando quem envia faz isso: não há corpo nenhum para analisar. Confira no assunto e caia para o corpo como alternativa. |
| Uma imagem | O código desenhado como uma imagem, para derrotar exatamente esse tipo de script. | Raro, e um sinal de que quem envia não quer automação. Mude o template de quem envia se ele for seu; não há solução honesta se não for. |
A parte em texto puro é a preferida, e é a que a maioria dos sistemas de template gera automaticamente a partir do HTML — então ela costuma estar presente. Quando ela é null, a parte em HTML é o único corpo, e as próximas duas seções tratam de como lê-la com segurança.
Ancore o padrão no seu próprio texto
O instinto é usar \d{6}. Ele combina com o código, e também combina com o ano no rodapé, o CEP no bloco de endereço, os últimos seis dígitos de um número de telefone e o número do pedido que aparece duas linhas acima do código. O que vier primeiro vence, e o teste o digita no formulário com total confiança.
| Padrão | Também combina com | Veredito |
|---|---|---|
\d{6} | Anos, CEPs, preços sem separador, números de pedido, números de telefone, códigos de rastreio. | Nunca. Isso não é um padrão, é cara ou coroa. |
\b\d{6}\b | Tudo o que foi citado acima que por acaso tenha exatamente seis dígitos com um espaço de cada lado — ainda é a maior parte. | Pouco melhor. Limites de palavra não sabem o que é um código. |
code is\D{0,12}(\d{6}) | Só os seis dígitos que vêm depois das palavras que o seu template coloca antes do código, com espaço para dois-pontos, um espaço ou os restos em branco de uma tag. | Sim. Ele combina com o código e nada mais, e falha no dia em que alguém reformular o e-mail — uma falha da qual você quer ficar sabendo. |
O \D{0,12} é o detalhe prático: depois que as tags foram substituídas por espaços, as palavras e os dígitos podem estar separados por dois-pontos, uma sequência de espaços, ou os restos de uma <strong> que costumava ficar entre eles. Até uma dezena de não-dígitos cobre tudo isso sem deixar o padrão pular para um número diferente.
Templates que separam os dígitos
Um design popular coloca cada dígito do código em sua própria caixa, para que fique legível em um celular. No HTML isso são seis células de tabela, ou seis <span>s, e o número nunca aparece como seis caracteres consecutivos em lugar nenhum do código-fonte:
<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>Um regex sobre o HTML bruto não encontra nada. A correção não é um regex mais esperto; é transformar o HTML em texto primeiro, em uma ordem fixa:
- Substitua toda tag por um espaço. Um espaço, não nada —
<td>4</td><td>8</td>precisa virar4 8, não48colado com o que vier em seguida. - Decodifique as entidades.
&, ,'. Um espaço não separável entre dois dígitos não é um espaço para um regex até ser decodificado. - Colapse os espaços em branco, depois combine permitindo espaço entre os dígitos. Para o design em caixas, use
code is\D{0,12}(\d)\s*(\d)\s*(\d)\s*(\d)\s*(\d)\s*(\d)e junte os grupos; para um template normal, o padrão simples da seção anterior já basta.
Os helpers abaixo fazem os passos um e dois para você e buscam nas duas partes de uma vez, então um teste não precisa saber qual design o template está usando neste mês.
A mensagem mais recente nem sempre é a certa
Toda listagem de caixa de entrada aqui volta com a mais recente primeiro, e messages[0] é o que a maioria dos primeiros rascunhos lê. Três situações fazem dessa a mensagem errada:
- Duas mensagens de uma única ação
- O cadastro envia um e-mail de boas-vindas e um e-mail com código, na ordem em que a fila de quem envia esvaziar. Metade das vezes o de boas-vindas é o mais recente. Filtre pelo assunto, ou pelo remetente, antes de pegar qualquer coisa.
- Um reenvio
- O teste pediu o código duas vezes — uma por engano, uma de propósito — e o servidor só aceita o mais recente. A mensagem mais antiga ainda está na caixa de entrada, ainda combina com o padrão, e ainda resulta em seis dígitos que agora são inválidos.
- Uma execução de teste anterior
- Só se o endereço foi reutilizado, o que nunca deveria acontecer. Um endereço novo por execução torna esse caso impossível; se você não pode ter um, o snapshot abaixo é a alternativa.
A forma robusta é a mesma em qualquer runner: veja o que está na caixa de entrada antes de disparar o e-mail, depois aceite só uma mensagem que ainda não estava lá e que combina com o assunto esperado.
// 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));Códigos que expiram durante a execução
A maioria dos códigos de uso único é válida por cinco a quinze minutos. Isso parece generoso até uma suíte de testes enfileirar vinte specs, cada um pedindo seu código no início e digitando no final. Três regras mantêm o código vivo:
- Peça o código o mais tarde possível. Dispare o envio imediatamente antes da espera, não em um passo de setup que roda enquanto outros testes estão na fila.
- Mantenha o prazo de espera bem abaixo do tempo de vida do código. Um prazo de sessenta segundos para um código de dez minutos deixa nove minutos para digitá-lo. Um prazo de dez minutos não deixa nada.
- Nunca guarde um código para outro teste. Códigos são de uso único além de terem vida curta; uma fixture compartilhada que distribui um código é uma corrida entre dois testes por um único número.
Extratores prontos para usar
Três versões das mesmas quatro linhas: as duas partes unidas, tags viram espaços, entidades decodificadas, espaços em branco colapsados, e então o padrão ancorado. Mude o padrão para combinar com o texto do seu template e nada mais precisa ser tocado.
A partir de um shell, com 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];
}O JSON de mensagem que eles leem vem de GET /api/v1/message/{id}, documentado na referência da API; a espera que te dá o id em primeiro lugar está no guia de testes de ponta a ponta, e como helpers prontos para Playwright, Cypress, Python e Node.js.
Antes de considerar concluído
- O padrão ancorado no texto do seu template, e mantido ao lado do template.
- As duas partes buscadas, como texto: tags viram espaços, entidades decodificadas, espaços em branco colapsados.
- Um filtro por assunto ou remetente, para que um e-mail de boas-vindas nunca vença um e-mail com código.
- Um snapshot antes de qualquer reenvio, e só mensagens novas aceitas depois dele.
- O prazo de espera bem abaixo do tempo de vida do código, e o envio disparado logo antes da espera.
- Uma mensagem de falha que nomeia o id da mensagem e o assunto em que procurou.
Isso cobre todas as formas conhecidas de um extrator de seis dígitos passar com o número errado. Um agente lendo o mesmo e-mail tem os mesmos problemas e uma ferramenta a menos para lidar com eles, e é por isso que o servidor MCP entrega a mensagem inteira em vez de um palpite — uma caixa de entrada que um agente de IA consegue ler aborda isso.
Perguntas
Devo ler a parte em texto ou a parte em HTML?
A parte em texto quando ela existir: é estável e não tem nada para decodificar. Busque nas duas de qualquer forma, como os helpers fazem, para que um template que só manda HTML continue funcionando e um template que só manda texto nunca trave em um HTML vazio.
Meu código tem letras. O padrão muda?
Só a classe de caracteres: ([A-Z0-9]{6}), ou qualquer que seja o alfabeto usado por quem envia, ainda ancorada no texto antes dela. Adicione a flag i se a caixa não for garantida, e tome cuidado para que a classe não combine também com uma palavra que apareça logo depois da âncora.
E os magic links em vez de códigos?
Mesma disciplina, padrão diferente: confira a URL contra um fragmento de caminho que você conhece — /confirm/, /auth/magic/ — em vez de “o primeiro link”, porque um e-mail transacional costuma carregar cinco links, e o que você quer raramente é o primeiro. Decodifique & antes de visitá-lo.
Por quanto tempo uma mensagem fica disponível para leitura?
5 dias depois que chega, tendo sido lida ou não. Isso é bem mais tempo do que qualquer código permanece válido, então um teste nunca precisa se apressar para ler — só para digitar.
Posso obter o código sem fazer polling na caixa de entrada?
Via REST, não: você faz polling uma vez por segundo com um prazo, que é o ritmo documentado e nunca sofre limitação. Via MCP existe uma ferramenta wait_for_message que mantém a chamada aberta até a mensagem chegar, que é o formato de que um agente de IA precisa.
Preciso de uma chave de API?
Não. Os domínios públicos não exigem chave, conta nem cabeçalho. Só o conjunto pago de domínios mantidos fora das listas de bloqueio de e-mail descartável usa um token bearer, e o código de extração é idêntico nos dois casos.


