Validar una dirección de correo en PHP suele empezar y terminar con una sola línea:
filter_var($email, FILTER_VALIDATE_EMAIL). Está integrada, es rápida y es
genuinamente útil, pero solo responde a la primera de las tres preguntas que
plantea una validación de verdad. ¿Tiene la dirección la forma correcta? ¿Puede
su dominio recibir correo? ¿Existe realmente el buzón? La biblioteca estándar de
PHP responde a las dos primeras de fábrica; la tercera es un problema de red que
merece la pena delegar. Esta guía construye la validación de correo en PHP por
capas, con código funcional para cada una, y muestra exactamente dónde se detiene
filter_var.
La respuesta corta#
Usa filter_var() con FILTER_VALIDATE_EMAIL para la sintaxis, checkdnsrr() o
getmxrr() para la búsqueda MX y una API de verificación para la comprobación SMTP
del buzón: primero lo más barato, cortocircuitando en cuanto una sea decisiva. No
intentes abrir una sesión SMTP desde PHP para sondear buzones por tu cuenta: el
puerto 25 de salida está bloqueado en la mayoría de los hosts, y la respuesta
depende de la reputación de la IP emisora y del
greylisting que no querrás reimplementar. Cada capa
descarta direcciones fuera de forma más barata que la anterior; solo la última
puede dar una dirección por válida.
Capa 1: sintaxis con filter_var#
En PHP, filter_var es la herramienta adecuada para la primera capa: te ahorra
tener que escribir una expresión regular a mano, y está mejor probada que
cualquier cosa que pegarías de Stack Overflow. FILTER_VALIDATE_EMAIL devuelve la
dirección si tiene éxito y false si falla, así que compara de forma estricta:
function is_valid_syntax(string $email): bool
{
return strlen($email) <= 320
&& filter_var($email, FILTER_VALIDATE_EMAIL) !== false;
}
is_valid_syntax('jane@example.com'); // true
is_valid_syntax('not-an-email'); // false
is_valid_syntax('a@@b.com'); // false
El !== false estricto importa. filter_var devuelve la cadena filtrada si tiene
éxito, no true, de modo que un if (filter_var(...)) laxo funciona solo porque
una cadena no vacía es verdadera (truthy): comparar contra false dice lo que
quieres decir y sobrevive a la refactorización. La comprobación de longitud es un
cinturón y tirantes: una dirección de más de 320 caracteres no puede ser real, y
es más barato rechazarla pronto que pasar una cadena gigante a cualquier cosa río
abajo.
Ten claro qué hace y qué no hace este filtro. Por defecto valida contra una
gramática derivada del RFC y rechaza las partes locales internacionalizadas
(Unicode), así que 用户@example.com falla a menos que añadas el flag
FILTER_FLAG_EMAIL_UNICODE en PHP 7.1 y superiores. Es un filtro de sintaxis,
punto: nunca toca el DNS, nunca abre un socket y no tiene ni idea de si
example.com existe. Ten en cuenta también que FILTER_SANITIZE_EMAIL es otra
bestia distinta: elimina los caracteres no permitidos y devuelve una cadena
mutilada en lugar de una respuesta de sí o no, así que no recurras a él cuando lo
que quieres es validar.
Por qué que filter_var lo apruebe no es luz verde#
Que filter_var lo apruebe significa que la cadena tiene la forma de una
dirección de correo. No dice nada sobre si el correo llegará.
definitely-fake@gmail.com pasa. info@company-that-folded.com pasa.
typo@gmial.com pasa. Las tres son inentregables, y ninguna cantidad de coincidencia
de patrones —filter_var o la expresión regular más elaborada que encuentres— te
lo dirá jamás, porque la sintaxis y la entregabilidad son preguntas distintas. Una
es un hecho sobre la cadena; la otra es un hecho sobre internet. Esta es la misma
trampa que hace que
la validación de correo con regex falle, y
filter_var está exactamente en el mismo lado de la línea.
Si quieres ver la diferencia por ti mismo, pega una dirección sintácticamente
perfecta en el comprobador de correo gratuito y observa cómo
una cadena aprobada por filter_var vuelve como undeliverable.
Capa 2: ¿puede el dominio recibir correo?#
Aquí es donde PHP se gana el sueldo sin ninguna dependencia. Un dominio sin ruta de
correo no puede aceptar correo para nadie, así que esta única búsqueda elimina
dominios muertos, nombres de empresa mal escritos y TLD inventados. La comprobación
más rápida es checkdnsrr(), que devuelve un booleano según si existe un tipo de
registro dado:
function has_mail_route(string $domain): bool
{
// checkdnsrr() returns true if the domain publishes at least one MX record.
// Fall back to A for domains that accept mail on an implicit MX.
return checkdnsrr($domain, 'MX') || checkdnsrr($domain, 'A');
}
has_mail_route('gmail.com'); // true
has_mail_route('company-that-folded.com'); // false
Separa el dominio de la dirección con strrpos para que siempre cortes en la
última @:
$domain = substr($email, strrpos($email, '@') + 1);
Cuando necesites los hosts de correo reales en lugar de un sí o no —por ejemplo,
para registrarlos o inspeccionar prioridades— recurre a getmxrr(). Rellena un
array de hosts y un array de pesos coincidente por referencia y devuelve false
cuando no hay ningún registro MX en absoluto:
function mail_hosts(string $domain): array
{
$hosts = [];
$weights = [];
if (!getmxrr($domain, $hosts, $weights)) {
return [];
}
array_multisort($weights, $hosts); // lowest weight = highest priority
return $hosts; // e.g. ['gmail-smtp-in.l.google.com', 'alt1.gmail-smtp-in.l.google.com', ...]
}
Capa 3: ¿existe realmente el buzón?#
Las capas uno y dos solo pueden descartar una dirección fuera. Un dominio puede
publicar registros MX perfectos y aún no tener ningún buzón en la dirección que
manejas: noreply-9f2x@gmail.com tiene una sintaxis válida en un dominio con una
ruta de correo activa, y sigue siendo un buzón que nunca se creó. Confirmar que un
buzón concreto existe implica la conversación de entrega SMTP: conectarse al host de
correo, emitir RCPT TO, leer la respuesta y desconectarse antes de enviar nada.
Hay más miga: cómo funciona la verificación de correo
recorre todo el proceso, incluidos los
dominios catch-all que aceptan todas las
direcciones y derrotan un sondeo ingenuo.
En principio puedes programar esto en PHP con fsockopen() y comandos SMTP en
crudo. En la práctica no deberías ejecutarlo desde el servidor de tu aplicación: la
mayoría de los proveedores de nube bloquean el puerto 25 de salida, la respuesta
depende de la reputación de la IP desde la que te conectas, y los servidores
receptores aplican greylisting y limitan la tasa de los remitentes desconocidos, de
modo que un sondeo que funciona en una prueba local falla en silencio, o te mete en
una lista de bloqueo, en producción. Esta es la capa que vale la pena delegar.
Hacer la comprobación completa con una API#
El POST /verify de Qualisend ejecuta todo el proceso —sintaxis,
DNS y el sondeo SMTP del buzón— desde una infraestructura con reputación gestionada
construida para ello, y devuelve un veredicto. Sin ningún framework, un pequeño
ayudante con cURL cubre todas las llamadas que harás:
const QUALISEND_BASE = 'https://app.qualisend.com/api/v1';
function qualisend(string $method, string $path, ?array $body = null): array
{
$ch = curl_init(QUALISEND_BASE . $path);
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('QUALISEND_API_KEY'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => $body !== null ? json_encode($body) : null,
CURLOPT_TIMEOUT => 10,
]);
$raw = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($raw === false || $status >= 400) {
throw new RuntimeException("Qualisend responded {$status}");
}
return json_decode($raw, true);
}
function verify(string $email): array
{
return qualisend('POST', '/verify', ['email' => $email]);
}
Si ya usas Guzzle, la misma petición ocupa unas pocas líneas menos y gestiona el JSON y los códigos de estado de error por ti:
use GuzzleHttp\Client;
$http = new Client(['base_uri' => 'https://app.qualisend.com/api/v1/']);
$response = $http->post('verify', [
'headers' => ['Authorization' => 'Bearer ' . getenv('QUALISEND_API_KEY')],
'json' => ['email' => 'jane@example.com'],
'timeout' => 10,
]);
$result = json_decode((string) $response->getBody(), true);
Las comprobaciones locales vuelven de inmediato, con el sondeo SMTP en cola:
{
"job_id": "6f1c2e0a-9b3d-4a1e-8c77-1a2b3c4d5e6f",
"probe_queued": true,
"result": {
"email": "jane@example.com",
"status": "deliverable",
"score": 95,
"sub_flags": { "role": false, "disposable": false, "free": false, "catch_all": false },
"did_you_mean": null,
"smtp": "pending",
"reason": null
}
}
Para la validación de correo en el registro en vivo, el result inmediato suele
bastar para actuar: rechaza undeliverable, ofrece la corrección did_you_mean,
marca disposable. Cuando necesites el veredicto confirmado por SMTP, sondea el
trabajo hasta que el sondeo termine:
function verify_and_wait(string $email, int $tries = 10, float $delay = 1.5): ?array
{
$jobId = verify($email)['job_id'];
for ($i = 0; $i < $tries; $i++) {
$job = qualisend('GET', "/jobs/{$jobId}?include=results");
if ($job['status'] === 'completed') {
return $job['results'][0]; // ['status' => ..., 'score' => ..., 'reason' => ...]
}
usleep((int) ($delay * 1_000_000));
}
return null; // still processing — treat as unknown, retry later
}
Juntando las capas#
Primero lo más barato, para en cuanto tengas una respuesta:
function validate_email(string $email): array
{
if (!is_valid_syntax($email)) {
return ['status' => 'undeliverable', 'reason' => 'invalid_email'];
}
$domain = substr($email, strrpos($email, '@') + 1);
if (!has_mail_route($domain)) {
return ['status' => 'undeliverable', 'reason' => 'invalid_domain'];
}
return verify($email)['result']; // deliverable | risky | undeliverable | unknown
}
Las dos capas locales no cuestan nada y atrapan la mayor parte de la basura al instante; la capa de la API se ejecuta solo sobre direcciones que merecen el viaje de ida y vuelta por la red. Ese orden es todo el truco: la misma estructura que encontrarás en las versiones Node.js y Python de esta guía, porque lo que hace que la validación funcione son las capas, no el lenguaje.
Preguntas frecuentes#
¿Basta con filter_var para validar una dirección de correo en PHP?#
Para la sintaxis, sí: filter_var($email, FILTER_VALIDATE_EMAIL) es la comprobación
adecuada de primera capa y una apuesta mejor que una expresión regular casera. Pero
valida la forma, no la entregabilidad: nunca resuelve el DNS ni contacta con un
servidor de correo, así que pasarla significa "parece un correo", no "se entregará".
Combínala con una búsqueda MX y una comprobación SMTP del buzón antes de fiarte de
la dirección.
¿Cuál es la diferencia entre checkdnsrr() y getmxrr()?#
checkdnsrr() responde a una pregunta de sí o no —¿publica el dominio un registro de
un tipo determinado?— y devuelve un booleano, que es todo lo que necesitas para
confirmar que existe una ruta de correo. getmxrr() va más allá: rellena arrays con
los nombres de host MX reales y sus pesos por referencia, así que úsalo cuando
quieras inspeccionar u ordenar los servidores de correo en lugar de solo confirmar
que están ahí. Ambas devuelven false cuando no hay ningún registro coincidente.
¿Puedo verificar un buzón en PHP sin un servicio externo?#
En parte. checkdnsrr() y getmxrr() confirman que el dominio acepta correo, lo que
descarta dominios muertos gratis y no necesita nada más allá de la biblioteca
estándar. Confirmar el buzón implica una conversación SMTP, que puedes intentar con
fsockopen() pero no deberías ejecutar desde el servidor de tu aplicación: el
puerto 25 está ampliamente bloqueado y el resultado depende de la reputación de tu
IP. Esa es la capa que existe para que la maneje un servicio de verificación.
¿Debo ejecutar estas comprobaciones al registrarse o al limpiar una lista?#
Ambas, con distinta profundidad. Ejecuta la sintaxis y la búsqueda MX de forma síncrona al registrarse —son lo bastante rápidas para bloquear la petición y dar retroalimentación instantánea— y actúa también allí sobre el veredicto inmediato de la API. Reserva el resultado completo confirmado por SMTP para la limpieza de listas y el trabajo de trastienda más lento; la guía de registro serverless muestra el patrón de principio a fin.
¿Listo para añadir la capa SMTP? El plan gratuito incluye 100 créditos
que ejecutan el proceso completo, y la referencia de la API tiene los
endpoints completos /verify y /jobs con ejemplos para copiar y pegar en siete
lenguajes.