Um colega recentemente me indicou uma postagem no blog: On the Futility of Email Regex Validation . Por uma questão de brevidade, vou me referir a ele como Futilidade neste artigo.
Admito que, embora o desafio de escrever um regex que possa identificar com sucesso se uma string está em conformidade com a definição RFC 5322 de um cabeçalho de mensagem da Internet seja um desafio divertido, Futility não é um guia útil para o programador prático.
Isso ocorre porque ele combina cabeçalhos de mensagem RFC 5322 com literais de endereço RFC 5321; o que, em linguagem simples, significa que o que constitui um endereço de e-mail SMTP válido é diferente do que constitui um cabeçalho de mensagem válido em geral.
É também porque incita o leitor a se preocupar com casos extremos que são teoricamente possíveis do ponto de vista dos padrões, mas que irei demonstrar, têm uma probabilidade infinitesimal de ocorrer “na natureza”.
Este artigo expandirá essas duas afirmações, discutirá alguns casos de uso possíveis para regex de e-mail e concluirá com exemplos anotados de “livro de receitas” de regex de e-mail prático.
A universalidade do SMTP para a transmissão de e-mail significa que, na prática, nenhum exame de formatação de endereço de e-mail é completo sem uma leitura atenta do IETF RFC relevante, que é 5321.
5322 considera os endereços de e-mail simplesmente como um cabeçalho de mensagem genérico, sem regras de casos especiais aplicáveis a ele. Isso significa que os comentários entre parênteses são válidos, mesmo em um nome de domínio.
O conjunto de testes referenciado no Futility inclui 10 testes que contêm comentários ou caracteres diacríticos ou Unicode e indica que 8 deles representam endereços de e-mail válidos.
Isso é incorreto porque o RFC 5321 é explícito ao afirmar que as partes do nome de domínio dos endereços de e-mail “ são restritas para fins de SMTP para consistir em uma sequência de letras, dígitos e hífens extraídos do conjunto de caracteres ASCII ”.
No contexto da construção de uma expressão regular, é difícil exagerar o grau em que essa restrição simplifica as questões, especialmente no que diz respeito à determinação do comprimento excessivo da string. A anotação dos exemplos irá destacar isso abaixo.
Também implica algumas outras considerações práticas no contexto da validação que exploraremos mais adiante.
De acordo com os dois RFCs, o nome técnico da parte do endereço de e-mail à esquerda do símbolo “@“ é “caixa de correio”. Ambos os RFCs permitem latitude considerável em quais caracteres são permitidos na parte da caixa de correio.
A única restrição prática significativa é que as aspas ou parênteses devem ser balanceados, algo que é um verdadeiro desafio de verificar no regex vanilla.
No entanto, as implementações de caixa de correio do mundo real são novamente a medida que o programador prático deve empregar.
Como regra, as pessoas que nos pagam desaprovam que 90% de nossas horas faturáveis sejam direcionadas para resolver os 10% de casos teóricos que podem nem existir na vida real.
Vamos examinar os provedores de caixa de correio de e-mail dominantes, consumidores e empresas e considerar quais tipos de endereços de e-mail eles permitem.
Para e-mail do consumidor, fiz uma pesquisa primária, usando uma lista de 5.280.739 endereços de e-mail que vazaram de contas do Twitter.
Com base em 115 milhões de contas do Twitter, isso nos dá um nível de confiança de 99% com uma margem de erro de 0,055% para toda a população do Twitter, o que seria muito representativo da população geral de todos os endereços de e-mail da Internet. Aqui está o que eu aprendi:
No entanto, este é um 100% arredondado. Para os amantes de curiosidades, também encontrei:
O efeito líquido é que presumir que as caixas de correio de endereços de e-mail contenham apenas alfanuméricos ASCII, pontos e traços fornecerá uma precisão melhor do que 5 9 para e-mails de consumidores.
Para e-mails comerciais, Datanyze informa que 6.771.269 empresas usam 91 soluções diferentes de hospedagem de e-mail. No entanto, a distribuição de Pareto se mantém e 95,19% dessas caixas de correio são hospedadas por apenas 10 provedores de serviços.
O Google permite apenas letras, números e pontos ASCII ao criar uma caixa de correio. No entanto, aceitará o sinal de mais ao receber e-mail .
Permite apenas letras, números e pontos ASCII.
Usa o Microsoft 365 e permite apenas letras, números e pontos ASCII.
Não documentado.
Infelizmente, só podemos ter certeza de 82% das empresas e não sabemos quantas caixas de correio isso representa. No entanto, sabemos que dos endereços de e-mail do Twitter, apenas 400 de 173.467 domínios tinham mais de 100 caixas de correio de e-mail individuais representadas.
Acredito que a maioria dos 99% dos domínios restantes eram endereços de e-mail comerciais.
Em termos de políticas de nomenclatura de caixas de correio no nível do servidor ou domínio, proponho que seja razoável considerar esses 237.592 endereços de e-mail como representando uma população de 1 bilhão de endereços de e-mail comerciais com um nível de confiança de 99% e margem de erro de 0,25%, o que nos dá perto de 3 9's ao assumir que uma caixa de correio de endereço de e-mail contém apenas alfanuméricos ASCII, pontos e traços.
Novamente, com a praticidade em mente, vamos considerar em que circunstâncias podemos precisar identificar programaticamente um endereço de e-mail válido.
Neste caso de uso, um novo cliente em potencial está tentando criar uma conta. Existem duas estratégias de alto nível que podemos considerar. No primeiro caso, tentamos verificar se o endereço de e-mail fornecido pelo novo usuário é válido e continuamos com a criação da conta de forma síncrona.
Há duas razões pelas quais você pode não querer adotar essa abordagem. A primeira é que, embora você possa validar se o endereço de e-mail tem um formulário válido, ele pode não existir.
A outra razão é que, em qualquer tipo de escala, síncrono é uma palavra de alerta, o que deve fazer com que o programador pragmático considere, em vez disso, um modelo de disparar e esquecer, em que um front-end da Web sem estado passa informações de formulário para um microsserviço ou API que irá valide o e-mail de forma assíncrona enviando um link exclusivo que acionará a conclusão do processo de criação da conta.
No caso de um formulário de contato simples, do tipo frequentemente usado para baixar white papers, a possível desvantagem de aceitar strings que parecem um e-mail válido, mas não são, é que você está diminuindo a qualidade de seu banco de dados de marketing ao não validar se o endereço de e-mail realmente existe.
Então, mais uma vez, o modelo disparar e esquecer é uma opção melhor do que a validação programática da string inserida em um formulário.
Isso nos leva ao caso de uso real para identificação programática de endereço de e-mail em geral e regex em particular: anonimizar ou minerar grandes blocos de texto não estruturado.
Encontrei esse caso de uso pela primeira vez ajudando um pesquisador de segurança que precisava fazer upload de logs de referência para um banco de dados de detecção de fraude. Os logs de referência continham endereços de e-mail que precisavam ser anonimizados antes de deixar o jardim murado da empresa.
Eram arquivos com centenas de milhões de linhas, e havia centenas de arquivos por dia. “Linhas” podem ter quase mil caracteres.
Iterar pelos caracteres em uma linha, aplicar testes complexos (por exemplo, esta é a primeira ocorrência de @
na linha e faz parte de um nome de arquivo como [email protected]
?) uma complexidade de tempo impossivelmente grande.
Na verdade, a equipe interna de desenvolvimento desta (muito grande) empresa havia declarado que era uma tarefa impossível.
Eu escrevi o seguinte regex compilado:
search_pattern = re.compile("[a-zA-Z0-9\!\#\$\%\'\*\+\-\^\_\`\{\|\}\~\.]+@|\%40(?!(\w+\.)**(jpg|png))(([\w\-]+\.)+([\w\-]+)))")
E colocou-o na seguinte compreensão de lista do Python:
results = [(re.sub(search_pattern, "[email protected]", line)) for line in file]
Não consigo lembrar o quão rápido foi, mas foi rápido. Meu amigo poderia executá-lo em um laptop e terminar em minutos. Foi preciso. Cronometramos em 5 9 olhando para falsos negativos e falsos positivos.
Meu trabalho foi facilitado pelo fato de serem logs de referência; eles só podiam conter caracteres "legais" de URL, então consegui mapear todas as colisões que documentei no repo readme .
Além disso, eu poderia ter simplificado ainda mais (e mais rápido) se tivesse realizado a análise do endereço de e-mail e aprendido com a garantia de que tudo o que era necessário para chegar ao alvo 5 9 era alfanumérico ASCII, pontos e traços.
No entanto, este é um bom exemplo de praticidade e escopo da solução para se adequar ao problema real a ser resolvido.
Uma das maiores citações de toda a tradição e história da programação é a admoestação do grande Ward Cunningham para reservar um segundo para lembrar exatamente o que você está tentando realizar e depois se perguntar “Qual é a coisa mais simples que poderia funcionar?”
No caso de uso de análise (e, opcionalmente, transformação) de um endereço de e-mail de uma grande quantidade de texto não estruturado, essa solução foi definitivamente a coisa mais simples em que pude pensar.
Como eu disse no início, achei divertida a ideia de construir um regex compatível com RFC 5322, então mostrarei a você pedaços de regex que podem ser compostos para lidar com vários aspectos do padrão e explicar como as políticas de regex funcionam. No final mostro como fica tudo montado.
A estrutura de um endereço de e-mail é:
Agora, para o regex.
^(?<mailbox>(\[a-zA-Z0-9\\+\\!\\#\\$\\%\\&\\'\\\*\\-\\/\\=\\?\\+\\\_\\\{\\}\\|\\\~]|(?<singleDot>(?<!\\.)(?<!^)\\.(?!\\.))|(?<foldedWhiteSpace>\\s?\\&\\#13\\;\\&\\#10\\;.))\{1,64})
Primeiro, temos ^
que “ancora” o primeiro caractere no início da string. Isso deve ser usado ao validar uma string que deve conter nada além de um e-mail válido. Ele garante que o primeiro caractere seja válido.
Se o caso de uso for encontrar um e-mail em uma string mais longa, omita a âncora.
Em seguida, temos (?<mailbox>
. Isso nomeia o grupo de captura por conveniência. Dentro do grupo capturado estão os três blocos regex separados pelo símbolo de correspondência alternativa |
o que significa que um caractere pode corresponder a qualquer uma das três expressões.
Parte de escrever um bom regex (desempenho e previsível) é garantir que as três expressões sejam mutuamente exclusivas. Isso quer dizer que uma substring que corresponde a uma definitivamente não corresponderá a nenhuma das outras duas. Para fazer isso, usamos classes de caracteres específicas em vez do temido .*
.
[a-zA-Z0-9\+\!\#\$\%\&\'\*\-\/\=\?\+\_\{\}\|\~]
A primeira correspondência alternativa é uma classe de caracteres entre colchetes, que captura todos os caracteres ASCII permitidos em uma caixa de correio de e-mail, exceto o ponto, “espaço em branco dobrado”, as aspas duplas e os parênteses.
A razão pela qual os excluímos é que eles são legais apenas condicionalmente , ou seja, existem regras sobre como você pode usá-los que devem ser validados. Nós lidamos com eles nas próximas 2 partidas alternativas.
(?<singleDot>(?<!\.)(?<!^)\.(?!\.))
A primeira dessas regras diz respeito ao ponto (ponto). Em uma caixa de correio, o ponto só é permitido como separador entre duas strings de caracteres legais, portanto, dois pontos consecutivos não são válidos.
Para evitar uma correspondência se houver dois pontos consecutivos, usamos o lookbehind negativo regex (?<!\.)
que especifica que o próximo caractere (um ponto) não corresponderá se houver um ponto precedendo-o.
Regex look arounds pode ser encadeado. Há outro olhar negativo antes de chegarmos ao ponto (?!^)
que impõe a regra de que o ponto não pode ser o primeiro caractere da caixa de correio.
Após o ponto, há um look_ahead_ negativo _(?!\.)_
, isso impede que um ponto seja correspondido se for imediatamente seguido por um ponto.
(?<foldedWhiteSpace>\s?\&\#13\;\&\#10\;.)
Isso é um absurdo do RFC 5322 sobre permitir cabeçalhos de várias linhas em mensagens. Estou pronto para apostar que, na história dos endereços de e-mail, nunca houve alguém que tenha criado seriamente um endereço com uma caixa de correio de várias linhas (eles podem ter feito isso de brincadeira).
Mas estou jogando o jogo 5322, então aqui está, a sequência de caracteres Unicode que cria o Espaço em Branco Dobrado como uma correspondência alternativa.
Ambos os RFC permitem o uso de aspas duplas como forma de delimitar (ou escapar ) caracteres que normalmente seriam ilegais.
Eles também permitem incluir comentários entre parênteses para que sejam legíveis por humanos, mas não considerados pelo agente de transferência de correspondência (MTA) ao interpretar o endereço.
Em ambos os casos, os personagens só são válidos se equilibrados . Isso significa que deve haver um par de caracteres, um que abre e outro que fecha .
Fico tentado a escrever que descobri um demonstrationem mirabilem , porém, isso provavelmente só funciona postumamente. A verdade é que isso não é trivial no regex de baunilha.
Tenho a intuição de que a natureza recursiva do regex “ganancioso” pode ser explorada com vantagem; no entanto, é improvável que dedique o tempo necessário para atacar esse problema nos próximos anos e, portanto, na melhor tradição, deixo como um exercício para o leitor.
{1,64}
Algo que realmente importa é o tamanho máximo de uma caixa de correio: 64 caracteres.
Então, depois de fecharmos o grupo de captura de caixa de correio com um parêntese de fechamento final, usamos um quantificador entre chaves para especificar que devemos corresponder a qualquer uma de nossas alternativas pelo menos uma vez e não mais que 64 vezes.
\s?(?<atSign>(?<!\-)(?<!\.)\@(?!\@))
O bloco delimitador começa com o caso especial \s?
porque, de acordo com a Futility, um espaço é legal logo antes do delimitador, e estou apenas aceitando a palavra deles.
O restante do grupo de captura segue um padrão semelhante ao singleDot ; não corresponderá se for precedido por um ponto ou traço ou se for seguido imediatamente por outro @
.
Aqui, como na caixa de correio, temos 3 correspondências alternativas. E o último deles aninhados nele outros 4 jogos alternativos.
(?<dns>[[:alnum:]]([[:alnum:]\-]{0,63}\.){1,24}[[:alnum:]\-]{1,63}[[:alnum:]])
Isso não passará em vários dos testes do Futility, mas, conforme mencionado anteriormente, está em conformidade com a RFC 5321, que tem a palavra final.
(?<IPv4>\[((?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\])
Não há muito a dizer sobre isso. Este é um regex conhecido e facilmente disponível para endereços IPv4.
(?<IPv6>(?<IPv6Full>(\[IPv6(\:[0-9a-fA-F]{1,4}){8}\]))|(?<IPv6Comp1>\[IPv6\:((([0-9a-fA-F]{1,4})\:){1,3}(\:([0-9a-fA-F]{1,4})){1,5}?\])|\[IPv6\:((([0-9a-fA-F]{1,4})\:){1,5}(\:([0-9a-fA-F]{1,4})){1,3}?\]))|(?<IPv6Comp2>(\[IPv6\:\:(\:[0-9a-fA-F]{1,4}){1,6}\]))|(?<IPv6Comp3>(\[IPv6\:([0-9a-fA-F]{1,4}\:){1,6}\:\]))|(?<IPv6Comp4>(\[IPv6\:\:\:)\])|(?<IPv6v4Full>(\[IPv6(\:[0-9a-fA-F]{1,4}){6}\:((?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3})(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\])|(?<IPv6v4Comp1>\[IPv6\:((([0-9a-fA-F]{1,4})\:){1,3}(\:([0-9a-fA-F]{1,4})){1,5}?(\:((?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))\])|\[IPv6\:((([0-9a-fA-F]{1,4})\:){1,5}(\:([0-9a-fA-F]{1,4})){1,3}?(\:((?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))\]))|(?<IPv6v4Comp2>(\[IPv6\:\:(\:[0-9a-fA-F]{1,4}){1,5}(\:((?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))\]))|(?<IPv6v4Comp3>(\[IPv6\:([0-9a-fA-F]{1,4}\:){1,5}\:(((?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))\]))|(?<IPv6v4Comp4>(\[IPv6\:\:\:((?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3})(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\]))
Não consegui encontrar uma boa expressão regular para endereços IPv6 (e IPv6v4), então escrevi a minha própria, seguindo cuidadosamente as regras anotadas de Backus/Naur da RFC 5321.
Não anotarei cada subgrupo do regex IPv6, mas nomeei cada subgrupo para facilitar a separação e ver o que está acontecendo.
Nada muito interessante, exceto talvez a maneira como combinei correspondência gulosa no lado “esquerdo” e não gulosa no “direito” no grupo de captura IUPv6Comp1.
Salvei o regex final, junto com os dados de teste do Futility e aprimorado por alguns casos de teste IPv6 meus, para Regex101 . Espero que você tenha gostado deste artigo e que ele seja útil e uma economia de tempo para muitos de vocês.
AZW