Per validare un indirizzo email in Django devi eseguire tre controlli, non uno, e il framework fornisce solo il primo. I validatori di Django confermano che un indirizzo ha la forma corretta; non chiedono mai se il suo dominio può ricevere posta o se la casella esiste. La vera validazione è a livelli: sintassi, poi una ricerca DNS, poi un test SMTP della casella. Questa guida costruisce ogni livello come un validatore Django da inserire in un form o in un serializer di Django REST Framework, e mostra dove subentra un'API di verifica. Riprende da dove si ferma la guida per Python, quindi il ragionamento su DNS e SMTP spiegato lì resta valido così com'è.
In breve#
Usa validate_email (o un qualsiasi EmailField) per la sintassi, dnspython
per la ricerca MX e un'API di verifica per il controllo SMTP della casella,
collegati come tre validatori, partendo dal più economico e interrompendo la catena non
appena uno è decisivo. Non aprire connessioni SMTP dal processo Django per
testare le caselle: la porta 25 in uscita è bloccata dalla maggior parte degli
host, e la risposta dipende dalla reputazione dell'IP di invio e dal
greylisting, cose che non vuoi reimplementare. Ogni livello esclude indirizzi
a un costo minore dell'ultimo; solo l'API può confermarne uno. Se la pipeline
è nuova per te, cos'è la verifica email
spiega prima i termini.
Livello 1: validazione del formato con i validatori di Django#
Django copre già il primo livello. django.core.validators.validate_email è
un'istanza di EmailValidator che solleva ValidationError su un indirizzo
malformato e restituisce None su uno corretto:
from django.core.validators import validate_email
from django.core.exceptions import ValidationError
def is_valid_syntax(email: str) -> bool:
try:
validate_email(email)
return True
except ValidationError:
return False
Raramente lo chiami a mano. Ogni forms.EmailField, models.EmailField e
serializers.EmailField di DRF aggiunge automaticamente EmailValidator,
quindi dichiarare un campo ti dà gratis la validazione della sintassi:
from django import forms
class SignupForm(forms.Form):
email = forms.EmailField() # EmailValidator runs during clean()
Sappi esattamente cosa ottieni. EmailValidator controlla la forma rispetto a
una grammatica derivata dalle RFC: non risolve mai il DNS, non apre mai un
socket e non ha idea se example.com esista. definitely-fake@gmail.com passa.
info@company-that-folded.com passa. typo@gmial.com passa. Sono tutti e tre
non recapitabili, e nessun validatore che legge solo la stringa te lo dirà mai,
per lo stesso motivo per cui la validazione delle email con le regex
fallisce: sintassi e deliverability
sono domande diverse. Una è un fatto che riguarda la stringa; l'altra è un fatto
che riguarda internet.
Livello 2: il dominio accetta posta?#
È il primo livello che Django non ti fornisce, ed è economico da aggiungere. Un
dominio senza record MX non può ricevere posta per nessuno, quindi una sola
ricerca DNS elimina domini morti, nomi di aziende scritti male e TLD inventati.
Django non ha un resolver MX integrato, quindi usa dnspython e racchiudilo in
una semplice funzione:
import dns.resolver # pip install dnspython
def has_mail_route(domain: str) -> bool:
try:
return len(dns.resolver.resolve(domain, "MX")) > 0
except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer, dns.resolver.NoNameservers):
return False
Un validatore Django è semplicemente un callable che solleva ValidationError
su un input non valido, quindi trasforma il controllo in un validatore e potrai
inserirlo direttamente nella lista validators di qualsiasi campo:
from django.core.exceptions import ValidationError
def validate_deliverable_domain(email: str) -> None:
domain = email.rsplit("@", 1)[-1]
if not has_mail_route(domain):
raise ValidationError("This domain can't receive email.")
validate_deliverable_domain("jane@gmail.com") # passes
validate_deliverable_domain("jane@company-that-folded.com") # raises ValidationError
Se vuoi accettare i domini che ricevono posta su un record A senza MX, ripiega
sulla risoluzione di A/AAAA quando l'insieme MX è vuoto, ma un controllo MX
copre la stragrande maggioranza degli indirizzi reali.
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 instradamento di posta attivo, ma resta una casella mai creata. Confermare una casella specifica richiede la conversazione SMTP di consegna: collegarsi all'host di posta, inviare RCPT TO, leggere la risposta e disconnettersi prima di inviare qualsiasi cosa.
In teoria potresti automatizzarlo da Django con smtplib. In pratica non
dovresti eseguirlo dal tuo server applicativo: la maggior parte degli host cloud
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 le
connessioni dei mittenti sconosciuti. Così un test che funziona in locale
fallisce senza avvisi in produzione, o ti fa finire in blacklist. Come funziona
la verifica email illustra l'intera
pipeline, domini catch-all compresi. Questo è il livello che conviene delegare.
L'endpoint di verifica di Qualisend esegue l'intera pipeline (sintassi, DNS e
test SMTP della casella) da un'infrastruttura con reputazione gestita e
restituisce un esito. Chiamalo da un validatore con requests, leggendo la
chiave dalle variabili d'ambiente:
import os
import requests # pip install requests
QUALISEND_VERIFY_URL = "https://api.qualisend.com/v1/verify"
def verify(email: str) -> dict:
res = requests.post(
QUALISEND_VERIFY_URL,
headers={"Authorization": f"Bearer {os.environ['QUALISEND_API_KEY']}"},
json={"email": email},
timeout=10,
)
res.raise_for_status()
return res.json()["result"] # {"status", "score", "reason", "sub_flags"}
La risposta è un involucro { "result": { ... } }. Leggi result["status"]
(deliverable, risky, undeliverable o unknown) insieme a score,
reason e sub_flags per caratteristiche come indirizzi generici o usa e
getta. Consulta il riferimento API per la struttura esatta
dei campi; qui ti serve solo lo stato per sollevare un errore su un indirizzo
che non verrà recapitato:
def validate_mailbox(email: str) -> None:
if verify(email)["status"] == "undeliverable":
raise ValidationError("We couldn't confirm a mailbox at this address.")
Potresti sollevare un errore anche su risky, oppure salvare lo score e
lasciar passare l'indirizzo: è una scelta di policy. Rifiutare undeliverable è
l'impostazione predefinita sicura; la guida alla verifica serverless in fase di registrazione spiega quale livello di
rigore adottare nel punto di raccolta.
Mettere insieme i livelli per validare un indirizzo email in Django#
Il punto idiomatico per comporre i tre livelli in un form è un metodo
clean_<field>. Django chiama clean_email solo dopo che l'EmailValidator
del campo è stato superato, quindi la sintassi è già gestita; aggiungi in ordine
i controlli su dominio e casella, ciascuno pronto a sollevare subito l'errore:
from django import forms
class SignupForm(forms.Form):
email = forms.EmailField() # layer 1 for free
def clean_email(self):
email = self.cleaned_data["email"] # syntax already passed
validate_deliverable_domain(email) # layer 2 — cheap, local
validate_mailbox(email) # layer 3 — the API round-trip
return email
Tutto il trucco sta in quell'ordine: l'EmailValidator del campo blocca la
spazzatura prima che il tuo codice venga eseguito, il controllo DNS locale
esclude i domini morti a costo zero e l'API viene chiamata solo per gli
indirizzi che hanno superato entrambi. Una validazione nell'ordine sbagliato, o
che salta i livelli economici, spende un credito per ogni errore di battitura.
C'è una cosa da decidere in anticipo: cosa succede quando fallisce la chiamata
all'API stessa. Un timeout di rete o un'eccezione di requests dentro
clean_email non dovrebbe restituire a un cliente reale un errore di
validazione che non può correggere. Intercetta l'errore della richiesta
separatamente da ValidationError e tratta un servizio irraggiungibile come
unknown anziché undeliverable: lascia passare la registrazione e riverifica
l'indirizzo più tardi, invece di bloccare la registrazione per un'interruzione
temporanea. I livelli di sintassi e MX sono già stati eseguiti in locale, quindi
stai ammorbidendo solo il livello che dipende dalla rete.
Poiché entrambi i validatori personalizzati sollevano il ValidationError di
Django, le stesse due funzioni si inseriscono senza modifiche in un serializer
di Django REST Framework. L'hook validate_<field> di DRF intercetta
quell'eccezione e la trasforma in un 400 pulito:
from rest_framework import serializers
class SignupSerializer(serializers.Serializer):
email = serializers.EmailField() # layer 1 for free
def validate_email(self, value):
validate_deliverable_domain(value) # layer 2
validate_mailbox(value) # layer 3
return value
È la stessa struttura a tre livelli che trovi nelle versioni di questa guida per Node.js e Python: è la struttura a livelli, non il framework, a far funzionare la validazione. Quando devi scegliere quale servizio di verifica usare per il terzo livello, il confronto tra API passa in rassegna le opzioni.
Domande frequenti#
L'EmailValidator di Django basta per validare un indirizzo email?#
Per la sintassi sì: validate_email (e ogni EmailField che lo usa) è il
controllo giusto per il primo livello ed è 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.
Come scrivo un validatore email personalizzato in Django?#
Un validatore è qualsiasi callable che riceve il valore e solleva
django.core.exceptions.ValidationError in caso di errore. Definisci una
funzione come validate_deliverable_domain(email) che solleva l'eccezione
quando c'è un problema e restituisce None altrimenti, poi passala nella lista
validators=[...] del campo o chiamala dal metodo clean_email di un form. Lo
stesso callable funziona nell'hook validate_email di un serializer DRF, perché
anche DRF intercetta il ValidationError di Django.
Posso controllare se una casella esiste da Django senza un'API?#
In parte. dnspython conferma che il dominio accetta posta, il che esclude
gratis i domini morti e non richiede altro che una piccola dipendenza.
Confermare la casella significa una conversazione SMTP che puoi tentare con
smtplib 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 i livelli di sintassi e MX
in modo sincrono in clean_email (sono abbastanza veloci da bloccare la
richiesta e dare un riscontro immediato) e lì agisci anche in base all'esito
dell'API. Riserva la verifica in blocco, più pesante, alla pulizia delle liste e
al lavoro di back office, dove la latenza non conta e puoi elaborare gli
indirizzi in blocco.
Vuoi aggiungere il livello SMTP? Inserisci un indirizzo sintatticamente perfetto
nella verifica email gratuita per vedere una stringa
approvata da EmailField risultare undeliverable, poi collega lo stesso esito
ai tuoi form con il riferimento API, esempi pronti da
copiare e incollare inclusi.



