Regex para validação de e-mail: um padrão prático e seus limites
Nenhuma regex consegue dizer se um endereço funciona. Um padrão curto pega erros de digitação; um e-mail de confirmação prova o resto.
Procure uma regex de e-mail e você vai encontrar de tudo, de oito caracteres a vários milhares. As longas tentam seguir as normas à risca e mesmo assim não conseguem dizer se a caixa de correio existe. As curtas são fáceis de ler e, usadas para a tarefa certa, dão conta do recado. Essa tarefa é pegar erros óbvios, como um @ faltando ou um espaço, antes de enviar o e-mail de confirmação que faz a verificação de verdade.
Por que a gramática completa da RFC 5322 é o alvo errado
A RFC 5322 define a sintaxe dos endereços em mensagens de e-mail e permite muito mais do que qualquer formulário de cadastro quer aceitar. Todos estes são sintaticamente válidos:
"john doe"@example.com: uma parte local entre aspas que contém um espaço.john(comment)@example.com: um comentário dentro do endereço.user@[192.168.0.1]: um endereço IP no lugar de um nome de domínio.admin@localhost: um domínio sem ponto.
Uma regex que implementa a gramática inteira, com comentários e espaços dobrados (folding whitespace), é enorme, difícil de revisar e aceita endereços que muitos sistemas de e-mail recusam. E ainda deixa passar jane@gmial.com. O SMTP (RFC 5321) acrescenta limites de tamanho, 64 octetos para a parte antes do @ e 255 para o domínio, que junto com a sintaxe de caminho dão um máximo prático de 254 caracteres. Esse limite é mais simples de checar no código do que na regex.
Um padrão pragmático
Este é o padrão de que a maioria das aplicações realmente precisa: algo sem espaços nem @, um único @ e depois um domínio com pelo menos um ponto.
^[^\s@]+@[^\s@]+\.[^\s@]+$
Ele tem uma fraqueza que vale corrigir. Como [^\s@] também casa com pontos, o padrão aceita rótulos de domínio vazios, como em user@example..com. Excluir o ponto de cada rótulo fecha essa brecha:
^[^\s@]+@[^\s@.]+(?:\.[^\s@.]+)+$
Veja o que cada padrão fez com um conjunto de endereços de teste no Node 24, ao lado do padrão da especificação HTML descrito na próxima seção:
| Endereço | Padrão curto | Padrão por rótulos | HTML type=email |
|---|---|---|---|
first.last+news@example.co.uk | Aceito | Aceito | Aceito |
o'brien@example.ie | Aceito | Aceito | Aceito |
user@bücher.de | Aceito | Aceito | Rejeitado |
δοκιμή@παράδειγμα.δοκιμή | Aceito | Aceito | Rejeitado |
"john doe"@example.com | Rejeitado | Rejeitado | Rejeitado |
admin@localhost | Rejeitado | Rejeitado | Aceito |
user @example.com | Rejeitado | Rejeitado | Rejeitado |
user@@example.com | Rejeitado | Rejeitado | Rejeitado |
user@example..com | Aceito | Rejeitado | Rejeitado |
user@example.com. | Aceito | Rejeitado | Rejeitado |
us..er@example.com | Aceito | Aceito | Aceito |
user@-example.com | Aceito | Aceito | Rejeitado |
user@example.c | Aceito | Aceito | Aceito |
Nenhum dos três é perfeito, e nenhum precisa ser. Pontos consecutivos na parte local e domínios de topo com uma só letra passam por todos eles; o e-mail de confirmação pega esses casos. O que importa é não rejeitar nenhum endereço real e de uso comum.
Há também um motivo de desempenho para preferir a versão por rótulos. Com uma entrada fabricada de 40.000 caracteres terminada em espaço, o padrão curto levou cerca de 2,9 segundos no Node por causa do retrocesso (backtracking), enquanto o padrão por rótulos terminou em menos de um milissegundo. Checar o limite de 254 caracteres antes de rodar qualquer padrão elimina o problema nos dois casos.
O padrão do input type=email do HTML5
Um <input type="email"> é validado pelo navegador com base em uma definição do padrão HTML do WHATWG. A especificação chama essa definição de violação intencional (willful violation) da RFC 5322 e traz esta expressão regular JavaScript como implementação:
/^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/
Ela permite a pontuação habitual antes do @, exige rótulos de domínio com no máximo 63 letras, dígitos e hifens que não comecem nem terminem com hífen e não exige ponto, então admin@localhost passa. Qualquer caractere fora do ASCII é rejeitado. Dá para reaproveitar a checagem do navegador em script com input.checkValidity() ou input.validity.typeMismatch, mas tudo o que o navegador verifica pode ser contornado, então valide de novo no servidor.
Endereços internacionais
Domínios podem conter caracteres fora do ASCII. bücher.de trafega pelo DNS como xn--bcher-kva.de, e new URL('http://bücher.de').hostname em JavaScript, idn_to_ascii() em PHP e 'bücher.de'.encode('idna') em Python produzem essa forma. A parte antes do @ também pode ter caracteres fora do ASCII, mas só servidores de e-mail com suporte a SMTPUTF8 (RFC 6531) conseguem entregar mensagens para esses endereços.
Decida com base no seu provedor de e-mail, não em uma regex. Os padrões construídos sobre [^\s@] aceitam endereços internacionais, enquanto o padrão do HTML os rejeita. O mesmo vale para o filter_var($email, FILTER_VALIDATE_EMAIL) do PHP: nos nossos testes ele rejeitou tanto user@bücher.de quanto δοκιμή@example.com e, com a flag FILTER_FLAG_EMAIL_UNICODE, aceitou o segundo, mas continuou rejeitando o domínio Unicode.
A mesma checagem em JavaScript, Python e PHP
Cada função remove os espaços das pontas, aplica o limite de tamanho e exige que a string inteira corresponda ao padrão. As três deram resultados idênticos para os endereços acima e para alguns casos-limite extras: espaços nas pontas, uma quebra de linha no meio do valor, um espaço não separável (non-breaking space) na parte local e um endereço de 256 caracteres.
// JavaScript
function isValidEmail(value) {
const email = value.trim();
return email.length <= 254 && /^[^\s@]+@[^\s@.]+(?:\.[^\s@.]+)+$/.test(email);
}
# Python
import re
EMAIL_RE = re.compile(r"[^\s@]+@[^\s@.]+(?:\.[^\s@.]+)+")
def is_valid_email(value: str) -> bool:
email = value.strip()
return len(email) <= 254 and EMAIL_RE.fullmatch(email) is not None
// PHP
function isValidEmail(string $value): bool
{
$email = trim($value);
return strlen($email) <= 254
&& preg_match('/^[^\s@]+@[^\s@.]+(?:\.[^\s@.]+)+$/Du', $email) === 1;
}
Dois detalhes nesses trechos evitam bugs reais. Em Python, re.match() com $ aceitou "user@example.com\n", porque $ também casa antes de uma quebra de linha final; re.fullmatch() rejeitou. O PHP se comporta do mesmo jeito, a menos que você acrescente o modificador D. O modificador u também faz diferença no PHP: sem ele, \s não reconheceu um espaço não separável, e user\u{00A0}x@example.com foi aceito. Em JavaScript, deixe a flag g de fora quando for apenas chamar test().
A validação de verdade é o e-mail de confirmação
Um padrão só consegue dizer que um endereço parece plausível. Ele não sabe se a caixa de correio existe, se aceita mensagens ou se pertence a quem está digitando. Consultar os registros MX mostra que um domínio está configurado para receber e-mail, mas não que uma caixa específica exista. O único teste confiável é enviar uma mensagem:
- Guarde o endereço como não verificado.
- Envie por e-mail um link com um token aleatório, de uso único e com validade, por exemplo de 24 horas.
- Marque o endereço como verificado só quando o link for aberto.
- Até lá, evite mandar qualquer outra coisa para esse endereço.
Mantenha a normalização leve: remova espaços das pontas e passe o domínio para minúsculas. A RFC 5321 trata a parte antes do @ como sensível a maiúsculas e minúsculas, mesmo que a maioria dos provedores ignore essa diferença, então guarde essa parte exatamente como foi digitada.
Teste os padrões com a sua própria lista
Cole um dos padrões no Testador de Expressões Regulares sem as barras em volta, defina as flags como gm e coloque os endereços de exemplo um por linha. Com a flag m, ^ e $ valem para cada linha, então cada endereço aceito aparece destacado individualmente. Conferimos que essa visão linha a linha dá os mesmos resultados que testar cada endereço separadamente, para os três padrões acima. No código da aplicação, teste um endereço por vez e sem m; caso contrário, um valor com várias linhas pode passar só porque uma delas corresponde.