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çoPadrão curtoPadrão por rótulosHTML type=email
first.last+news@example.co.ukAceitoAceitoAceito
o'brien@example.ieAceitoAceitoAceito
user@bücher.deAceitoAceitoRejeitado
δοκιμή@παράδειγμα.δοκιμήAceitoAceitoRejeitado
"john doe"@example.comRejeitadoRejeitadoRejeitado
admin@localhostRejeitadoRejeitadoAceito
user @example.comRejeitadoRejeitadoRejeitado
user@@example.comRejeitadoRejeitadoRejeitado
user@example..comAceitoRejeitadoRejeitado
user@example.com.AceitoRejeitadoRejeitado
us..er@example.comAceitoAceitoAceito
user@-example.comAceitoAceitoRejeitado
user@example.cAceitoAceitoAceito

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:

  1. Guarde o endereço como não verificado.
  2. Envie por e-mail um link com um token aleatório, de uso único e com validade, por exemplo de 24 horas.
  3. Marque o endereço como verificado só quando o link for aberto.
  4. 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.

Perguntas Frequentes

Qual é a melhor regex para validar e-mail?

Para a maioria dos formulários, ^[^\s@]+@[^\s@.]+(?:\.[^\s@.]+)+$ combinada com uma checagem de tamanho de 254 caracteres. Ela rejeita espaços, @ ausente ou duplicado e rótulos de domínio vazios sem rejeitar endereços internacionais. Depois, confirme o endereço por e-mail.

Por que não usar uma regex completa da RFC 5322?

Porque ela aceita strings entre aspas, comentários e domínios em forma de endereço IP que a maioria dos sistemas recusa, é muito difícil de manter e ainda assim não detecta erros de digitação como gmial.com nem diz se a caixa de correio existe.

O input type=email é suficiente?

É uma boa primeira checagem no navegador, mas rejeita endereços fora do ASCII, aceita domínios sem ponto e pode ser contornado. Valide sempre também no servidor.

Como verificar se um endereço de e-mail realmente existe?

Envie um e-mail de confirmação com um link de uso único e só considere o endereço verificado depois que o link for aberto. Checagens de sintaxe e consultas MX não provam que a caixa de correio existe nem que pertence ao usuário.