Skip to content
Comece com 100 créditos de verificação grátis
Qualisend
Todos os artigos
Engenharia / 22 de julho de 2026

Como validar um endereço de e-mail em PHP

8 minutes read

Qualisend team
Um diagrama em camadas de um endereço de e-mail em PHP passando pela validação de sintaxe do filter_var, por uma consulta de MX com checkdnsrr e por um teste SMTP da caixa postal, afunilando até um único veredito de entregabilidade

Validar um endereço de e-mail em PHP costuma começar e terminar em uma linha: filter_var($email, FILTER_VALIDATE_EMAIL). É nativo, é rápido e é genuinamente útil — mas responde apenas à primeira das três perguntas que a validação de verdade faz. O endereço tem o formato correto? O domínio dele consegue receber e-mail? A caixa postal realmente existe? A biblioteca padrão do PHP responde às duas primeiras de imediato; a terceira é um problema de rede que vale a pena delegar. Este guia constrói a validação de e-mail em PHP em camadas, com código funcional para cada uma, e mostra exatamente onde o filter_var para.

A resposta curta#

Use filter_var() com FILTER_VALIDATE_EMAIL para a sintaxe, checkdnsrr() ou getmxrr() para a consulta de MX e uma API de verificação para a checagem SMTP da caixa postal — as mais baratas primeiro, interrompendo assim que uma delas for decisiva. Não tente abrir uma sessão SMTP a partir do PHP para sondar caixas postais por conta própria: a porta 25 de saída é bloqueada na maioria dos hosts, e a resposta depende da reputação do IP remetente e do greylisting que você não vai querer reimplementar. Cada camada descarta endereços de forma mais barata que a anterior; só a última consegue aprovar um endereço.

Camada 1: sintaxe com filter_var#

Para o PHP, o filter_var é a ferramenta certa para a camada um — ele te poupa de escrever um regex à mão e é mais bem testado do que qualquer coisa que você colaria do Stack Overflow. FILTER_VALIDATE_EMAIL retorna o endereço em caso de sucesso e false em caso de falha, então compare de forma estrita:

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

O !== false estrito importa. O filter_var retorna a string filtrada em caso de sucesso, não true, então um if (filter_var(...)) frouxo só funciona porque uma string não vazia é truthy — comparar contra false diz exatamente o que você quer dizer e sobrevive a refatorações. A checagem de comprimento é uma segurança extra: um endereço com mais de 320 caracteres não pode ser real, e é mais barato rejeitá-lo cedo do que entregar uma string gigante a qualquer coisa lá na frente.

Saiba o que esse filtro faz e o que não faz. Por padrão, ele valida contra uma gramática derivada da RFC e rejeita partes locais internacionalizadas (Unicode) — então 用户@example.com falha, a menos que você adicione a flag FILTER_FLAG_EMAIL_UNICODE no PHP 7.1 ou superior. É um filtro de sintaxe, ponto final: nunca toca no DNS, nunca abre um socket e não faz ideia se example.com existe. Note também que FILTER_SANITIZE_EMAIL é outra história — ele remove caracteres não permitidos e retorna uma string alterada em vez de uma resposta de sim ou não, então não recorra a ele quando o que você quer é validar.

Por que passar no filter_var não é sinal verde#

Passar no filter_var significa que a string tem o formato de um endereço de e-mail. Não diz nada sobre a mensagem chegar ou não. definitely-fake@gmail.com passa. info@company-that-folded.com passa. typo@gmial.com passa. Todos os três são inentregáveis, e nenhuma quantidade de correspondência de padrões — o filter_var ou o regex mais elaborado que você conseguir encontrar — jamais vai te dizer isso, porque sintaxe e entregabilidade são perguntas diferentes. Uma é um fato sobre a string; a outra é um fato sobre a internet. Essa é a mesma armadilha que faz a validação de e-mail por regex falhar, e o filter_var fica exatamente do mesmo lado da linha.

Se você quiser ver a diferença por conta própria, cole um endereço sintaticamente perfeito no verificador de e-mail gratuito e veja uma string aprovada pelo filter_var voltar como undeliverable.

Camada 2: o domínio consegue receber e-mail?#

É aqui que o PHP justifica seu valor sem nenhuma dependência. Um domínio sem rota de e-mail não consegue aceitar mensagens para ninguém, então essa única consulta elimina domínios mortos, nomes de empresas escritos errado e TLDs inventados. A checagem mais rápida é checkdnsrr(), que retorna um booleano para saber se um determinado tipo de registro existe:

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

Separe o domínio do endereço com strrpos, para sempre cortar no último @:

$domain = substr($email, strrpos($email, '@') + 1);

Quando você precisar dos servidores de e-mail de fato, em vez de um sim ou não — digamos, para registrá-los em log ou inspecionar prioridades — recorra ao getmxrr(). Ele preenche um array de hosts e um array de pesos correspondente por referência e retorna false quando não há nenhum registro MX:

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', ...]
}

Camada 3: a caixa postal realmente existe?#

As camadas um e dois só conseguem descartar um endereço. Um domínio pode publicar registros MX perfeitos e ainda assim não ter nenhuma caixa postal no endereço que você tem em mãos — noreply-9f2x@gmail.com tem sintaxe válida em um domínio com uma rota de e-mail ativa, e mesmo assim é uma caixa postal que nunca foi criada. Confirmar que uma caixa postal específica existe significa a conversa de entrega SMTP: conectar ao servidor de e-mail, emitir RCPT TO, ler a resposta e desconectar antes de enviar qualquer coisa. Há mais nisso — como funciona a verificação de e-mail percorre o pipeline completo, incluindo os domínios catch-all que aceitam todo endereço e derrotam um teste ingênuo.

Em princípio, dá para programar isso em PHP com fsockopen() e comandos SMTP crus. Na prática, você não deveria executá-lo a partir do seu servidor de aplicação: a maioria dos provedores de nuvem bloqueia a porta 25 de saída, a resposta depende da reputação do IP a partir do qual você conecta, e os servidores receptores fazem greylisting e limitam a taxa de remetentes desconhecidos — então um teste que funciona num teste local falha silenciosamente, ou te coloca em uma blocklist, em produção. É esta a camada que vale a pena delegar.

Fazendo a checagem completa com uma API#

O POST /verify da Qualisend roda o pipeline inteiro — sintaxe, DNS e o teste SMTP da caixa postal — a partir de uma infraestrutura com reputação gerenciada, construída para isso, e retorna um veredito. Sem nenhum framework, um pequeno helper cURL cobre todas as chamadas que você vai fazer:

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]);
}

Se você já usa o Guzzle, a mesma requisição fica algumas linhas mais curta e cuida do JSON e dos códigos de status de erro para você:

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);

As checagens locais voltam imediatamente, com o teste SMTP na fila:

{
  "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 a validação de e-mail ao vivo no cadastro, o result imediato costuma ser suficiente para agir — rejeite undeliverable, ofereça a correção did_you_mean, sinalize disposable. Quando você precisar do veredito confirmado por SMTP, faça polling no job até o teste terminar:

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 as camadas#

As mais baratas primeiro, pare assim que tiver uma resposta:

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
}

As duas camadas locais não custam nada e pegam a maior parte do lixo na hora; a camada da API roda apenas em endereços que valem a ida e volta pela rede. Essa ordenação é o truque inteiro — o mesmo formato que você vai encontrar nas versões Node.js e Python deste guia, porque é a divisão em camadas, e não a linguagem, o que faz a validação funcionar.

Perguntas frequentes#

O filter_var é suficiente para validar um endereço de e-mail em PHP?#

Para a sintaxe, sim — filter_var($email, FILTER_VALIDATE_EMAIL) é a checagem certa para a camada um e uma aposta melhor do que um regex feito à mão. Mas ele valida o formato, não a entregabilidade: nunca resolve DNS nem contata um servidor de e-mail, então passar significa "parece um e-mail", não "será entregue". Combine-o com uma consulta de MX e uma checagem SMTP da caixa postal antes de confiar no endereço.

Qual é a diferença entre checkdnsrr() e getmxrr()?#

checkdnsrr() responde a uma pergunta de sim ou não — o domínio publica um registro de um determinado tipo? — e retorna um booleano, que é tudo o que você precisa para confirmar que existe uma rota de e-mail. getmxrr() vai além: preenche arrays com os nomes de host MX reais e seus pesos por referência, então use-o quando quiser inspecionar ou ordenar os servidores de e-mail em vez de apenas confirmar que eles existem. Ambos retornam false quando não há registro correspondente.

Posso verificar uma caixa postal em PHP sem um serviço externo?#

Em parte. checkdnsrr() e getmxrr() confirmam que o domínio aceita e-mail, o que descarta domínios mortos de graça e não precisa de nada além da biblioteca padrão. Confirmar a caixa postal significa uma conversa SMTP, que você pode tentar com fsockopen(), mas não deve executar a partir do seu servidor de aplicação — a porta 25 é bloqueada em larga escala e o resultado depende da reputação do seu IP. É essa a camada que um serviço de verificação existe para resolver.

Devo rodar essas checagens no cadastro ou ao limpar uma lista?#

Ambos, em profundidades diferentes. Rode a sintaxe e a consulta de MX de forma síncrona no cadastro — elas são rápidas o suficiente para bloquear a requisição e dar retorno instantâneo — e aja também sobre o veredito imediato da API ali. Reserve o resultado completo confirmado por SMTP para a limpeza de listas e o trabalho de retaguarda mais lento; o guia de cadastro serverless mostra o padrão de ponta a ponta.


Pronto para adicionar a camada SMTP? O plano gratuito inclui 100 créditos que rodam o pipeline completo, e a referência da API traz os endpoints /verify e /jobs completos com exemplos de copiar e colar em sete linguagens.

Your reputation, protected.

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

Get started