Expresión regular para validar un email: un patrón práctico y sus límites

Ninguna regex puede decirle si una dirección funciona. Un patrón corto detecta errores de tecleo; un correo de confirmación demuestra el resto.

Busque una regex de email y encontrará de todo, desde ocho caracteres hasta varios miles. Las largas intentan seguir los estándares al pie de la letra y aun así no pueden decirle si un buzón existe. Las cortas se leen con facilidad y, usadas para la tarea adecuada, son suficientes. Esa tarea es detectar errores evidentes, como una @ que falta o un espacio, antes de enviar el correo de confirmación que hace la comprobación de verdad.

Por qué la gramática completa de la RFC 5322 no es el objetivo

La RFC 5322 define la sintaxis de las direcciones en los mensajes de correo y permite mucho más de lo que necesita cualquier formulario de registro. Todas estas direcciones son sintácticamente válidas:

  • "john doe"@example.com: una parte local entre comillas que contiene un espacio.
  • john(comment)@example.com: un comentario dentro de la dirección.
  • user@[192.168.0.1]: una dirección IP en lugar de un nombre de dominio.
  • admin@localhost: un dominio sin punto.

Una regex que implemente la gramática entera, con comentarios y espacios plegados incluidos, es enorme, difícil de revisar y acepta direcciones que muchos sistemas de correo rechazan. Y sigue dejando pasar jane@gmial.com. SMTP (RFC 5321) añade límites de longitud, 64 octetos para la parte anterior a la @ y 255 para el dominio, que junto con la sintaxis de la ruta dan un máximo práctico de 254 caracteres. Comprobar la longitud es más sencillo en código que dentro de la regex.

Un patrón pragmático

Este es el patrón que la mayoría de las aplicaciones necesita de verdad: algo sin espacios ni @, una sola @ y después un dominio con al menos un punto.

^[^\s@]+@[^\s@]+\.[^\s@]+$

Tiene un punto débil que merece la pena corregir. Como [^\s@] también coincide con los puntos, acepta etiquetas de dominio vacías como user@example..com. Excluir el punto de cada etiqueta cierra ese hueco:

^[^\s@]+@[^\s@.]+(?:\.[^\s@.]+)+$

Esto es lo que hizo cada patrón con una serie de direcciones de prueba en Node 24, junto al patrón de la especificación HTML que se describe en la sección siguiente:

DirecciónPatrón cortoPatrón por etiquetasHTML type=email
first.last+news@example.co.ukAceptadaAceptadaAceptada
o'brien@example.ieAceptadaAceptadaAceptada
user@bücher.deAceptadaAceptadaRechazada
δοκιμή@παράδειγμα.δοκιμήAceptadaAceptadaRechazada
"john doe"@example.comRechazadaRechazadaRechazada
admin@localhostRechazadaRechazadaAceptada
user @example.comRechazadaRechazadaRechazada
user@@example.comRechazadaRechazadaRechazada
user@example..comAceptadaRechazadaRechazada
user@example.com.AceptadaRechazadaRechazada
us..er@example.comAceptadaAceptadaAceptada
user@-example.comAceptadaAceptadaRechazada
user@example.cAceptadaAceptadaAceptada

Ninguno de los tres es perfecto, y no hace falta que lo sea. Los puntos consecutivos en la parte local y los dominios de primer nivel de una sola letra pasan en los tres; de eso se encarga el correo de confirmación. Lo que importa es no rechazar ninguna dirección real y de uso común.

También hay un motivo de rendimiento para preferir la versión por etiquetas. Con una entrada manipulada de 40 000 caracteres que termina en un espacio, el patrón corto tardó unos 2,9 segundos en Node por culpa del retroceso (backtracking), mientras que el patrón por etiquetas terminó en menos de un milisegundo. Comprobar el límite de 254 caracteres antes de ejecutar cualquier patrón elimina el problema en los dos casos.

El patrón de input type=email en HTML5

Un <input type="email"> lo valida el navegador según una definición del estándar HTML de WHATWG. La especificación lo describe como una violación deliberada de la RFC 5322 (willful violation) y ofrece esta expresión regular de JavaScript como implementación:

/^[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])?)*$/

Admite la puntuación habitual antes de la @, exige etiquetas de dominio de 63 letras, dígitos y guiones como máximo que no empiecen ni terminen por guion, y no exige un punto, así que admin@localhost pasa. Rechaza cualquier carácter no ASCII. Puede reutilizar la comprobación del navegador desde un script con input.checkValidity() o input.validity.typeMismatch, pero todo lo que comprueba el navegador se puede saltar, así que vuelva a validar en el servidor.

Direcciones internacionales

Los dominios pueden contener caracteres no ASCII. bücher.de viaja por el DNS como xn--bcher-kva.de, y tanto new URL('http://bücher.de').hostname en JavaScript como idn_to_ascii() en PHP y 'bücher.de'.encode('idna') en Python producen esa forma. La parte anterior a la @ también puede llevar caracteres no ASCII, pero solo los servidores de correo compatibles con SMTPUTF8 (RFC 6531) pueden entregar mensajes a esas direcciones.

Decida en función de su proveedor de correo, no de una regex. Los patrones basados en [^\s@] aceptan direcciones internacionales, mientras que el patrón de HTML las rechaza. También las rechaza filter_var($email, FILTER_VALIDATE_EMAIL) de PHP: en nuestras pruebas rechazó tanto user@bücher.de como δοκιμή@example.com, y con la opción FILTER_FLAG_EMAIL_UNICODE aceptó la segunda pero siguió rechazando el dominio Unicode.

La misma comprobación en JavaScript, Python y PHP

Cada función quita los espacios de los extremos, aplica el límite de longitud y exige que coincida la cadena completa. Las tres devolvieron resultados idénticos con las direcciones anteriores y con algunos casos límite: espacios alrededor del valor, un salto de línea en medio, un espacio de no separación en la parte local y una dirección 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;
}

Dos detalles de esos fragmentos evitan errores reales. En Python, re.match() con $ aceptó "user@example.com\n", porque $ también coincide antes de un salto de línea final; re.fullmatch() la rechazó. PHP se comporta igual salvo que añada el modificador D. El modificador u también importa en PHP: sin él, \s no coincidió con un espacio de no separación y user\u{00A0}x@example.com se aceptó. En JavaScript, no ponga la bandera g cuando solo vaya a llamar a test().

La validación real es un correo de confirmación

Un patrón solo puede decir que una dirección parece plausible. No puede saber si el buzón existe, si acepta correo o si pertenece a quien la está escribiendo. Consultar los registros MX demuestra que un dominio está configurado para recibir correo, pero no que exista un buzón concreto. La única prueba fiable es enviar un mensaje:

  1. Guarde la dirección como no verificada.
  2. Envíe por correo un enlace con un token aleatorio, de un solo uso y con caducidad, por ejemplo de 24 horas.
  3. Marque la dirección como verificada solo cuando se abra el enlace.
  4. Hasta entonces, no envíe nada más a esa dirección.

Normalice lo justo: quite los espacios de los extremos y pase el dominio a minúsculas. La RFC 5321 considera que la parte anterior a la @ distingue mayúsculas de minúsculas, aunque la mayoría de los proveedores no lo haga, así que guárdela tal como se escribió.

Pruebe los patrones con su propia lista

Pegue uno de los patrones en el probador de expresiones regulares sin las barras que lo rodean, ponga las banderas gm y escriba sus direcciones de ejemplo, una por línea. Con la bandera m, ^ y $ se aplican a cada línea, así que cada dirección aceptada se resalta por separado. Comprobamos que esta vista por líneas da los mismos resultados que probar cada dirección de forma individual con los tres patrones anteriores. En el código de su aplicación, en cambio, pruebe una dirección cada vez y sin m, o un valor de varias líneas podría pasar solo porque una de sus líneas coincide.

Preguntas Frecuentes

¿Cuál es la mejor regex para validar un email?

Para la mayoría de los formularios, ^[^\s@]+@[^\s@.]+(?:\.[^\s@.]+)+$ junto con una comprobación de longitud de 254 caracteres. Rechaza los espacios, las arrobas que faltan o se repiten y las etiquetas de dominio vacías sin rechazar direcciones internacionales. Después, confirme la dirección por correo.

¿Por qué no usar una regex completa de la RFC 5322?

Porque acepta cadenas entre comillas, comentarios y dominios en forma de dirección IP que la mayoría de los sistemas rechaza, es muy difícil de mantener y aun así no detecta errores como gmial.com ni sabe si el buzón existe.

¿Basta con input type=email?

Es una buena primera comprobación en el navegador, pero rechaza las direcciones no ASCII, acepta dominios sin punto y se puede saltar. Valide siempre también en el servidor.

¿Cómo compruebo que una dirección de correo existe de verdad?

Envíe un correo de confirmación con un enlace de un solo uso y considere la dirección verificada solo cuando se abra ese enlace. Ni las comprobaciones de sintaxis ni las consultas MX pueden demostrar que un buzón existe o que pertenece al usuario.