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.