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 min de leitura

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

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_mail": 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. Isso só é necessário enquanto result.smtp for pending; cached ou skipped significa que o primeiro result já é definitivo. Rode o loop em um worker ou fila em segundo plano, nunca dentro da requisição de cadastro, e não chame /verify de novo para atualizar um endereço:

function verify_and_wait(string $email, int $tries = 10, float $delay = 1.5): ?array
{
    $response = verify($email);
    if ($response['result']['smtp'] !== 'pending') {
        return $response['result']; // cached or skipped: already final
    }
    $jobId = $response['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' => ...]
        }
        if ($job['status'] === 'canceled' || $job['status'] === 'failed') {
            return null; // the job ended without a verdict
        }
        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'];
    }

    try {
        return verify($email)['result']; // deliverable | risky | undeliverable | unknown
    } catch (RuntimeException $e) {
        // Timeout or API error (e.g. 402, 429, 5xx): fail open, re-verify later.
        return ['status' => 'unknown', 'reason' => '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

01O 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.

02Qual é 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.

03Posso 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.

04Devo 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.

Sua reputação, protegida.

Limpe sua primeira lista em minutos. 100 créditos grátis, sem cartão.

Começar