Validare un indirizzo email in PHP di solito inizia e finisce con una riga:
filter_var($email, FILTER_VALIDATE_EMAIL). È una funzione nativa, è veloce ed è davvero utile,
ma risponde solo alla prima delle tre domande che una verifica seria si pone.
L'indirizzo ha la forma corretta? Il suo dominio può ricevere posta? La casella esiste
davvero? La libreria standard di PHP risponde alle prime due senza bisogno di altro; la
terza è un problema di rete che conviene delegare. Questa guida costruisce la
validazione email in PHP a livelli, con codice funzionante per ciascuno, e mostra
esattamente dove si ferma filter_var.
In breve#
Usa filter_var() con FILTER_VALIDATE_EMAIL per la sintassi, checkdnsrr() o
getmxrr() per la ricerca MX e un'API di verifica per il controllo SMTP della casella,
partendo dal più economico e fermandoti appena uno dà una risposta definitiva. Non
provare ad aprire da PHP una sessione SMTP per testare le caselle per conto tuo: la porta
25 in uscita è bloccata dalla maggior parte dei provider di hosting, e la risposta dipende dalla
reputazione dell'IP di invio e dal greylisting, che non
vuoi certo reimplementare. Ogni livello esclude indirizzi a un costo minore dell'ultimo, e solo l'ultimo può confermarne uno.
Livello 1: la sintassi con filter_var#
In PHP, filter_var è lo strumento giusto per il primo livello: ti evita di scrivere a
mano una regex ed è più collaudato di qualunque cosa potresti copiare da Stack Overflow.
FILTER_VALIDATE_EMAIL restituisce l'indirizzo in caso di successo e false in caso di
errore, quindi usa un confronto stretto:
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
Il !== false stretto è importante. filter_var restituisce la stringa filtrata in
caso di successo, non true, quindi un if (filter_var(...)) non stretto funziona solo
perché una stringa non vuota è truthy: confrontare con false dice esattamente cosa
intendi e resiste ai refactoring. Il controllo sulla lunghezza è una doppia sicurezza: un
indirizzo più lungo di 320 caratteri non può essere reale, ed è più economico scartarlo
subito che passare una stringa enorme a tutto ciò che viene dopo.
Tieni presente cosa fa e cosa non fa questo filtro. Per impostazione predefinita valida secondo
una grammatica derivata dalle RFC e rifiuta le parti locali internazionalizzate
(Unicode): quindi 用户@example.com non passa, a meno che tu non aggiunga il flag
FILTER_FLAG_EMAIL_UNICODE da PHP 7.1 in su. È un filtro di sintassi, punto: non tocca
mai il DNS, non apre mai un socket e non ha idea se example.com esista. Nota anche che
FILTER_SANITIZE_EMAIL è tutt'altra cosa: rimuove i caratteri non consentiti e
restituisce una stringa alterata invece di una risposta sì/no, quindi non usarlo quando
vuoi validare.
Perché superare filter_var non è un via libera#
Superare filter_var significa che la stringa ha la forma di un indirizzo email, ma
non dice nulla sul fatto che la posta arriverà. definitely-fake@gmail.com passa.
info@company-that-folded.com passa. typo@gmial.com passa. Tutti e tre sono non
recapitabili, e nessun pattern matching (né filter_var né la regex più elaborata che
riesci a trovare) potrà mai dirtelo, perché sintassi e deliverability sono domande
diverse. Una riguarda la stringa; l'altra riguarda internet. È la stessa trappola che fa
fallire la validazione email con le regex,
e filter_var sta esattamente dalla stessa parte della linea.
Se vuoi vedere la differenza con i tuoi occhi, incolla un indirizzo sintatticamente
perfetto nella verifica email gratuita e guarda una stringa
approvata da filter_var tornare come non recapitabile (undeliverable).
Livello 2: il dominio può ricevere posta?#
Qui PHP si fa valere senza alcuna dipendenza. Un dominio senza un instradamento della
posta non può ricevere messaggi per nessuno, quindi questa sola ricerca elimina i domini
inesistenti, i nomi di aziende scritti male e i TLD inventati. Il controllo più rapido è
checkdnsrr(), che restituisce un booleano che indica se esiste un dato tipo di record:
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 il dominio dall'indirizzo con strrpos, così tagli sempre in corrispondenza
dell'ultima @:
$domain = substr($email, strrpos($email, '@') + 1);
Quando ti servono gli host di posta effettivi invece di un sì/no (ad esempio per
registrarli nei log o esaminarne le priorità), usa getmxrr(). Riempie per riferimento
un array di host e un array corrispondente di pesi, e restituisce false quando non c'è
alcun record 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', ...]
}
Livello 3: la casella esiste davvero?#
I primi due livelli possono solo escludere un indirizzo. Un dominio può pubblicare
record MX perfetti e non avere comunque alcuna casella all'indirizzo che hai in mano:
noreply-9f2x@gmail.com ha una sintassi valida su un dominio con un instradamento di posta attivo, ma resta una casella mai creata. Confermare che una casella specifica esista richiede la conversazione SMTP di consegna: collegarsi all'host di posta, inviare RCPT TO, leggere la risposta e disconnettersi prima di inviare qualsiasi cosa. C'è dell'altro:
come funziona la verifica email ripercorre
l'intera pipeline, compresi i domini catch-all che
accettano qualsiasi indirizzo e mandano a vuoto un test ingenuo.
In teoria puoi scriverlo in PHP con fsockopen() e comandi SMTP grezzi. In pratica non
dovresti eseguirlo dal tuo server applicativo: la maggior parte dei cloud provider
blocca la porta 25 in uscita, la risposta dipende dalla reputazione dell'IP da cui ti
colleghi, e i server riceventi applicano il greylisting e limitano la frequenza dei
mittenti sconosciuti. Così un test che funziona in locale fallisce in silenzio in
produzione, o ti fa finire in blacklist. È il livello che conviene delegare.
Il controllo completo con un'API#
POST /verify di Qualisend esegue l'intera pipeline (sintassi, DNS
e test SMTP della casella) da un'infrastruttura con reputazione gestita, costruita
apposta, e restituisce un esito. Senza framework, un piccolo helper cURL copre ogni
chiamata che farai:
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 usi già Guzzle, la stessa richiesta è qualche riga più corta e gestisce per te il JSON e i codici di stato di errore:
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);
I controlli locali tornano subito, con il test SMTP in coda:
{
"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
}
}
Per la verifica in tempo reale durante la registrazione, il result immediato di
solito basta per decidere: rifiuta undeliverable, proponi la correzione
did_you_mean, segnala disposable. Quando ti serve l'esito confermato via SMTP,
interroga periodicamente l'elaborazione finché il test non è terminato:
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
}
Mettere insieme i livelli#
Parti dal più economico e fermati appena hai una risposta:
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
}
I due livelli locali non costano nulla e intercettano subito la maggior parte degli indirizzi spazzatura; il livello API viene eseguito solo sugli indirizzi che meritano la chiamata di rete. Tutto il trucco sta in quell'ordine: è la stessa struttura che trovi nelle versioni Node.js e Python di questa guida, perché a far funzionare la validazione è la struttura a livelli, non il linguaggio.
Domande frequenti#
filter_var basta per validare un indirizzo email in PHP?#
Per la sintassi sì: filter_var($email, FILTER_VALIDATE_EMAIL) è il controllo giusto
per il primo livello e una scelta migliore di una regex scritta a mano. Ma valida la
forma, non la deliverability: non risolve mai il DNS né contatta un server di posta,
quindi superarlo significa "sembra un'email", non "verrà recapitata". Abbinalo
a una ricerca MX e a un controllo SMTP della casella prima di fidarti dell'indirizzo.
Qual è la differenza tra checkdnsrr() e getmxrr()?#
checkdnsrr() risponde a una domanda sì/no (il dominio pubblica un record di un certo
tipo?) e restituisce un booleano, che è tutto ciò che serve per confermare che esiste un
instradamento della posta. getmxrr() va oltre: riempie per riferimento degli array con
i nomi host MX effettivi e i relativi pesi, quindi usalo quando vuoi esaminare o
ordinare i server di posta invece di confermare soltanto che ci sono. Entrambe
restituiscono false quando non c'è alcun record corrispondente.
Posso verificare una casella in PHP senza un servizio esterno?#
In parte. checkdnsrr() e getmxrr() confermano che il dominio accetta posta, il che
esclude gratis i domini inesistenti e non richiede nulla oltre alla libreria standard.
Confermare la casella richiede una conversazione SMTP, che puoi tentare con
fsockopen() ma che non dovresti eseguire dal tuo server applicativo: la porta 25 è
bloccata quasi ovunque e il risultato dipende dalla reputazione del tuo IP. È proprio il
livello di cui si occupa un servizio di verifica.
Conviene eseguire questi controlli alla registrazione o durante la pulizia di una lista?#
Entrambe le cose, con una profondità diversa. Esegui sintassi e ricerca MX in modo sincrono alla registrazione (sono abbastanza veloci da bloccare la richiesta e dare un riscontro immediato) e lì agisci anche sull'esito immediato dell'API. Riserva il risultato completo confermato via SMTP alla pulizia delle liste e al lavoro di back-office più lento; la guida alla verifica serverless in fase di registrazione mostra lo schema dall'inizio alla fine.
Vuoi aggiungere il livello SMTP? Il piano Free include 100 crediti che
eseguono l'intera pipeline, e il riferimento API contiene gli
endpoint completi /verify e /jobs con esempi da copiare e incollare in sette
linguaggi.



