Skip to content
Empieza con 100 créditos de verificación gratis
Qualisend
Todos los artículos
Ingeniería / 22 de julio de 2026

Cómo validar una dirección de correo en PHP

8 minutes read

Qualisend team
Un diagrama por capas de una dirección de correo en PHP que atraviesa la validación de sintaxis con filter_var, una búsqueda MX con checkdnsrr y un sondeo SMTP del buzón, reduciéndose a un único veredicto de entregabilidad

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.

Your reputation, protected.

Clean your first list in minutes. 100 free credits, no card required.

Get started