Un colega me señaló recientemente una publicación de blog: Sobre la futilidad de la validación de expresiones regulares de correo electrónico . En aras de la brevedad, me referiré a ello como Futilidad en este artículo.
Admito que si bien el desafío de escribir una expresión regular que pueda identificar con éxito si una cadena se ajusta a la definición RFC 5322 de un encabezado de mensaje de Internet es un desafío entretenido, Futility no es una guía útil para el programador práctico.
Esto se debe a que combina encabezados de mensajes RFC 5322 con literales de direcciones RFC 5321; lo que, en un lenguaje sencillo, significa que lo que constituye una dirección de correo electrónico SMTP válida es diferente de lo que constituye un encabezado de mensaje válido en general.
También es porque incita al lector a preocuparse por casos límite que son teóricamente posibles desde el punto de vista de los estándares, pero que demostraré que tienen una probabilidad infinitesimal de ocurrir “en la naturaleza”.
Este artículo ampliará estas dos afirmaciones, discutirá algunos casos de uso posibles para expresiones regulares de correo electrónico y concluirá con ejemplos anotados de "libro de cocina" de expresiones regulares de correo electrónico prácticas.
La universalidad de SMTP para la transmisión de correo electrónico significa que, en la práctica, ningún examen del formato de la dirección de correo electrónico está completo sin una lectura atenta del IETF RFC pertinente, que es 5321.
5322 considera las direcciones de correo electrónico simplemente como un encabezado de mensaje genérico sin reglas de casos especiales que se le apliquen. Esto significa que los comentarios entre paréntesis son válidos, incluso en un nombre de dominio.
El conjunto de pruebas al que se hace referencia en Futility incluye 10 pruebas que contienen comentarios, o caracteres diacríticos o Unicode, e indica que 8 de ellas representan direcciones de correo electrónico válidas.
Esto es incorrecto porque RFC 5321 es explícito al afirmar que las partes del nombre de dominio de las direcciones de correo electrónico " están restringidas para fines de SMTP para consistir en una secuencia de letras, dígitos y guiones extraídos del juego de caracteres ASCII ".
En el contexto de la construcción de una expresión regular, es difícil exagerar el grado en que esta restricción simplifica las cosas, especialmente en lo que respecta a determinar la longitud excesiva de la cadena. La anotación de los ejemplos resaltará esto a continuación.
También implica algunas otras consideraciones prácticas en el contexto de la validación que exploraremos más adelante.
Según ambos RFC, el nombre técnico de la parte de la dirección de correo electrónico a la izquierda del símbolo "@" es "buzón". Ambos RFC permiten una libertad considerable en cuanto a qué caracteres se permiten en la parte del buzón.
La única restricción práctica significativa es que las comillas o los paréntesis deben estar equilibrados, algo que es un verdadero desafío para verificar en la expresión regular estándar.
Sin embargo, las implementaciones de buzones de correo del mundo real son nuevamente la medida que el programador práctico debe emplear.
Como regla general, las personas que nos pagan desaprueban que el 90 % de nuestras horas facturables se dirijan a resolver el 10 % de los casos extremos teóricos que posiblemente ni siquiera existan en la vida real.
Echemos un vistazo a los principales proveedores de buzones de correo electrónico, consumidores y empresas, y consideremos qué tipos de direcciones de correo electrónico permiten.
Para el correo electrónico del consumidor, realicé una investigación primaria utilizando una lista de 5 280 739 direcciones de correo electrónico que se filtraron de las cuentas de Twitter.
Basado en 115 millones de cuentas de Twitter, esto nos da un nivel de confianza del 99 % con un margen de error del 0,055 % para toda la población de Twitter, lo que sería muy representativo de la población general de todas las direcciones de correo electrónico de Internet. Esto es lo que aprendí:
Sin embargo, esto es un 100% redondeado. Para los amantes de las trivias, también encontré:
El efecto neto es que suponiendo que los buzones de las direcciones de correo electrónico contienen solo ASCII alfanumérico, puntos y guiones, obtendrá una precisión superior a la de 5 9 para los correos electrónicos de los consumidores.
Para correos electrónicos comerciales, Datanyze informa que 6,771,269 empresas utilizan 91 soluciones de alojamiento de correo electrónico diferentes. Sin embargo, la distribución de Pareto se mantiene y el 95,19 % de esos buzones están alojados en solo 10 proveedores de servicios.
Google solo permite letras, números y puntos ASCII al crear un buzón. Sin embargo, aceptará el signo más al recibir el correo electrónico .
Solo permite letras ASCII, números y puntos.
Usa Microsoft 365 y solo permite letras, números y puntos ASCII.
No documentado.
Desafortunadamente, solo podemos estar seguros del 82% de las empresas y no sabemos cuántos buzones de correo representa. Sin embargo, sabemos que de las direcciones de correo electrónico de Twitter, solo 400 de 173 467 dominios tenían más de 100 buzones de correo electrónico individuales representados.
Creo que la mayoría del 99% de los dominios restantes eran direcciones de correo electrónico comerciales.
En términos de políticas de nombres de buzones a nivel de servidor o dominio, propongo que es razonable tomar estas 237 592 direcciones de correo electrónico como representativas de una población de 1000 millones de direcciones de correo electrónico comerciales con un nivel de confianza del 99 % y un margen de error del 0,25 %, lo que nos da cerca de 3 9 cuando se supone que un buzón de dirección de correo electrónico contiene solo ASCII alfanumérico, puntos y guiones.
Nuevamente, con la practicidad ante todo en nuestras mentes, consideremos bajo qué circunstancias podríamos necesitar identificar mediante programación una dirección de correo electrónico válida.
En este caso de uso, un nuevo cliente potencial intenta crear una cuenta. Hay dos estrategias de alto nivel que podríamos considerar. En el primer caso, intentamos verificar que la dirección de correo electrónico que proporciona el nuevo usuario es válida y procedemos con la creación de la cuenta de forma sincronizada.
Hay dos razones por las que es posible que no desee adoptar este enfoque. La primera es que, aunque pueda validar que la dirección de correo electrónico tiene un formulario válido, es posible que no exista.
La otra razón es que, en cualquier tipo de escala, sincrónico es una palabra de alerta, lo que debería hacer que el programador pragmático considere en su lugar un modelo de disparar y olvidar donde un front-end web sin estado pasa información de formulario a un microservicio o API que valide de forma asincrónica el correo electrónico enviando un enlace único que activará la finalización del proceso de creación de la cuenta.
En el caso de un formulario de contacto simple, del tipo que se usa a menudo para descargar documentos técnicos, la desventaja potencial de aceptar cadenas que parecen un correo electrónico válido pero no lo son es que está disminuyendo la calidad de su base de datos de marketing al no validar si la dirección de correo electrónico realmente existe.
Entonces, una vez más, el modelo disparar y olvidar es una mejor opción que la validación programática de la cadena ingresada en un formulario.
Esto nos lleva al caso de uso real para la identificación programática de direcciones de correo electrónico en general, y expresiones regulares en particular: anonimizar o extraer grandes fragmentos de texto no estructurado.
Me encontré por primera vez con este caso de uso ayudando a un investigador de seguridad que necesitaba cargar registros de referencia en una base de datos de detección de fraude. Los registros de referencia contenían direcciones de correo electrónico que debían anonimizarse antes de abandonar el jardín amurallado de la empresa.
Eran archivos con cientos de millones de líneas y había cientos de archivos al día. Las "líneas" pueden tener cerca de mil caracteres.
Iterando a través de los caracteres en una línea, aplicando pruebas complejas (p. ej., ¿es esta la primera aparición de @
en la línea y es parte de un nombre de archivo como [email protected]
?) utilizando bucles y funciones de cadena estándar que habrían creado una complejidad de tiempo que era imposiblemente grande.
De hecho, el equipo de desarrollo interno de esta (muy grande) empresa había declarado que era una tarea imposible.
Escribí la siguiente expresión regular compilada:
search_pattern = re.compile("[a-zA-Z0-9\!\#\$\%\'\*\+\-\^\_\`\{\|\}\~\.]+@|\%40(?!(\w+\.)**(jpg|png))(([\w\-]+\.)+([\w\-]+)))")
Y lo dejó caer en la siguiente comprensión de la lista de Python:
results = [(re.sub(search_pattern, "[email protected]", line)) for line in file]
No puedo recordar qué tan rápido fue, pero fue rápido. Mi amigo podría ejecutarlo en una computadora portátil y terminarlo en minutos. Fue preciso. Lo registramos en 5 9 buscando tanto falsos negativos como falsos positivos.
Mi trabajo se hizo algo fácil por el hecho de que los registros de referencia; solo podían contener caracteres "legales" de URL, por lo que pude mapear cualquier colisión que documenté en el archivo Léame del repositorio.
Además, podría haberlo hecho aún más simple (y más rápido) si hubiera realizado el análisis de la dirección de correo electrónico y aprendido con la seguridad de que todo lo que se necesitaba para llegar al objetivo de 5 9 era ASCII alfanumérico, puntos y guiones.
No obstante, este es un buen ejemplo de practicidad y alcance de la solución para que se ajuste al problema real a resolver.
Una de las mejores citas en toda la tradición y la historia de la programación es la advertencia del gran Ward Cunningham de tomarse un segundo para recordar exactamente lo que está tratando de lograr, y luego preguntarse "¿Qué es lo más simple que podría funcionar?"
En el caso práctico de analizar (y opcionalmente transformar) una dirección de correo electrónico a partir de una gran cantidad de texto no estructurado, esta solución fue definitivamente lo más simple que se me ocurrió.
Como dije al principio, encontré divertida la idea de crear una expresión regular compatible con RFC 5322, así que les mostraré fragmentos componibles de expresiones regulares para tratar varios aspectos del estándar y explicar cómo las políticas de expresiones regulares eso. Al final, les mostraré cómo se ve todo ensamblado.
La estructura de una dirección de correo electrónico es:
Ahora para la expresión regular.
^(?<mailbox>(\[a-zA-Z0-9\\+\\!\\#\\$\\%\\&\\'\\\*\\-\\/\\=\\?\\+\\\_\\\{\\}\\|\\\~]|(?<singleDot>(?<!\\.)(?<!^)\\.(?!\\.))|(?<foldedWhiteSpace>\\s?\\&\\#13\\;\\&\\#10\\;.))\{1,64})
Primero, tenemos ^
que "ancla" el primer carácter al comienzo de la cadena. Esto se debe usar si se valida una cadena que se supone que no contiene nada más que un correo electrónico válido. Se asegura de que el primer carácter sea legal.
Si el caso de uso es encontrar un correo electrónico en una cadena más larga, omita el ancla.
A continuación, tenemos (?<mailbox>
. Esto nombra el grupo de captura por conveniencia. Dentro del grupo capturado están los tres fragmentos de expresiones regulares separados por el símbolo de coincidencia alternativo |
lo que significa que un carácter puede coincidir con cualquiera de las tres expresiones.
Parte de escribir una buena expresión regular (rendimiento y predecible) es asegurarse de que las tres expresiones sean mutuamente excluyentes. Es decir, una subcadena que coincida con una, definitivamente no coincidirá con ninguna de las otras dos. Para ello utilizamos clases de caracteres específicas en lugar de los temidos .*
.
[a-zA-Z0-9\+\!\#\$\%\&\'\*\-\/\=\?\+\_\{\}\|\~]
La primera coincidencia alternativa es una clase de carácter encerrada entre corchetes, que captura todos los caracteres ASCII que son legales en un buzón de correo electrónico, excepto el punto, el "espacio en blanco doblado", las comillas dobles y los paréntesis.
La razón por la que los excluimos es que solo son legales condicionalmente , lo que quiere decir que hay reglas sobre cómo puede usarlos que deben validarse. Los manejamos en los próximos 2 partidos alternos.
(?<singleDot>(?<!\.)(?<!^)\.(?!\.))
La primera regla de este tipo se refiere al punto (punto). En un buzón, el punto solo se permite como separador entre dos cadenas de caracteres válidos, por lo que dos puntos consecutivos no son válidos.
Para evitar una coincidencia si hay dos puntos consecutivos, usamos la expresión negativa regex lookbehind (?<!\.)
que especifica que el siguiente carácter (un punto) no coincidirá si hay un punto que lo precede.
Regex look arounds se pueden encadenar. Hay otra mirada negativa detrás antes de que lleguemos al punto (?!^)
que hace cumplir la regla de que el punto no puede ser el primer carácter del buzón.
Después del punto, hay un look_ahead_ _(?!\.)_
negativo , esto evita que un punto coincida si es seguido inmediatamente por un punto.
(?<foldedWhiteSpace>\s?\&\#13\;\&\#10\;.)
Esta es una tontería de RFC 5322 sobre permitir encabezados de varias líneas en los mensajes. Estoy dispuesto a apostar que en la historia de las direcciones de correo electrónico, nunca ha habido nadie que haya creado seriamente una dirección con un buzón de varias líneas (puede que lo hayan hecho como una broma).
Pero estoy jugando el juego 5322, así que aquí está, la cadena de caracteres Unicode que crea el espacio en blanco plegado como una coincidencia alternativa.
Ambos RFC permiten el uso de comillas dobles como una forma de encerrar (o escapar ) caracteres que normalmente serían ilegales.
También permiten incluir comentarios entre paréntesis para que sean legibles por humanos, pero no los tenga en cuenta el agente de transferencia de correo (MTA) al interpretar la dirección.
En ambos casos, los personajes solo son legales si están equilibrados . Esto quiere decir que tiene que haber un par de personajes, uno que abre y otro que cierra .
Estoy tentado a escribir que he descubierto una demostración , sin embargo, esto probablemente solo funcione póstumamente. La verdad es que esto no es trivial en vainilla regex.
Tengo la intuición de que la naturaleza recursiva de la expresión regular "codiciosa" podría aprovecharse, sin embargo, es poco probable que dedique el tiempo necesario para atacar este problema durante los próximos años, por lo que en la mejor tradición, lo dejo. como ejercicio para el lector.
{1,64}
Algo que sí importa es la longitud máxima de un buzón: 64 caracteres.
Entonces, después de cerrar el grupo de captura de buzón con un paréntesis de cierre final, usamos un cuantificador entre llaves para especificar que debemos coincidir con cualquiera de nuestras alternativas al menos una vez y no más de 64 veces.
\s?(?<atSign>(?<!\-)(?<!\.)\@(?!\@))
El fragmento delimitador comienza con el caso especial \s?
porque según Futility, un espacio es legal justo antes del delimitador, y solo estoy tomando su palabra.
El resto del grupo de captura sigue un patrón similar al de singleDot ; no coincidirá si va precedida de un punto o un guión o si va seguida inmediatamente de otra @
.
Aquí, como en el buzón, tenemos 3 coincidencias alternativas. Y el último de estos ha anidado en él otros 4 partidos alternos.
(?<dns>[[:alnum:]]([[:alnum:]\-]{0,63}\.){1,24}[[:alnum:]\-]{1,63}[[:alnum:]])
Esto no pasará varias de las pruebas en Futility, pero como se mencionó anteriormente, cumple estrictamente con RFC 5321, que tiene la última palabra.
(?<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]?)\])
No hay mucho que decir sobre esto. Esta es una expresión regular bien conocida y fácilmente disponible para direcciones 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]?)\]))
No pude encontrar una buena expresión regular para las direcciones IPv6 (e IPv6v4), así que escribí la mía, siguiendo cuidadosamente las reglas anotadas de Backus/Naur de RFC 5321.
No anotaré cada subgrupo de la expresión regular de IPv6, pero he nombrado cada subgrupo para que sea más fácil separarlos y ver qué está pasando.
En realidad, nada demasiado interesante, excepto tal vez la forma en que combiné la coincidencia codiciosa en el lado "izquierdo" y la no codiciosa en el "derecho" en el grupo de captura IUPv6Comp1.
Guardé la expresión regular final, junto con los datos de prueba de Futility, y la mejoré con algunos casos de prueba de IPv6 propios, en Regex101 . Espero que hayan disfrutado este artículo y que resulte útil y un ahorro de tiempo para muchos de ustedes.
AZW