Per validare un indirizzo email in Flask esegui tre controlli, non uno, e Flask di
suo non ne offre nessuno, perché non ha un proprio livello per i moduli. Quello che
ha è un ecosistema: WTForms (di solito tramite Flask-WTF) per la gestione dei moduli
e il pacchetto email-validator su cui WTForms si appoggia. Insieme rispondono alle
prime due delle tre domande della validazione (l'indirizzo ha la forma corretta?
Il suo dominio può ricevere posta?), ma mai alla terza: la casella esiste davvero?
Una vera validazione è a livelli: sintassi, poi una ricerca DNS, poi un test SMTP
della casella. Questa guida costruisce ogni livello come validatore WTForms da inserire
in un modulo Flask-WTF o in una semplice route JSON e mostra dove subentra un'API di
verifica. Riparte da dove si ferma la guida per Python,
quindi il ragionamento su DNS e SMTP resta valido tale e quale.
In breve#
Usa il validatore Email() di WTForms per la sintassi, la sua opzione
check_deliverability=True (o dnspython) per la ricerca MX e un'API di verifica per
il controllo SMTP della casella, collegati partendo dal meno costoso, così ogni livello
interrompe la catena prima che parta il successivo. Non aprire connessioni SMTP dal tuo
processo Flask per testare le caselle: la porta 25 in uscita è bloccata sulla 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 gli indirizzi a un
costo minore del successivo; solo l'API può confermarne uno. Se la pipeline è nuova per
te, cos'è la verifica email
spiega prima i termini.
Livello 1: la validazione del formato con il validatore Email di WTForms#
Il primo livello spetta a WTForms, che non lo scrive a mano. wtforms.validators.Email
delega al pacchetto email-validator, quindi dichiarare un validatore Email() su un
campo ti dà gratis un vero controllo di sintassi derivato dagli RFC, non una regex:
from flask_wtf import FlaskForm
from wtforms import StringField
from wtforms.validators import DataRequired, Email
class SignupForm(FlaskForm):
email = StringField("Email", validators=[DataRequired(), Email()])
Il validatore Email() richiede l'installazione del pacchetto (pip install email-validator
o pip install wtforms[email]); senza, WTForms solleva un errore in fase di validazione
che te lo segnala. Dietro le quinte chiama email_validator.validate_email, che puoi
usare anche da solo (fuori da un modulo o in un livello di servizio) per ottenere lo
stesso esito sotto forma di booleano:
from email_validator import validate_email, EmailNotValidError
def is_valid_syntax(email: str) -> bool:
try:
validate_email(email, check_deliverability=False)
return True
except EmailNotValidError:
return False
Sappi esattamente cosa ottieni. Con check_deliverability=False il controllo legge solo
la forma: 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. Tutti e tre sono non recapitabili e nessun validatore che
si limita a leggere la stringa te lo dirà mai, per lo stesso motivo per cui
la validazione 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?#
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. Qui lo stack di
Flask fa una cosa che Django non fa di serie: email-validator può eseguire la ricerca
per te. Attiva check_deliverability e risolverà i record MX del dominio (ripiegando su
A/AAAA) con dnspython, sollevando EmailUndeliverableError, una sottoclasse di
EmailNotValidError, quando il dominio non può ricevere posta. In un modulo, è una sola
opzione sul validatore che hai già:
email = StringField("Email", validators=[DataRequired(), Email(check_deliverability=True)])
Quell'unico flag riunisce i livelli uno e due in un solo validatore. Da sola, la stessa chiamata restituisce un oggetto con l'indirizzo normalizzato quando il dominio risulta valido:
from email_validator import validate_email, EmailNotValidError
def has_mail_route(email: str) -> bool:
try:
validate_email(email, check_deliverability=True) # syntax + live MX lookup
return True
except EmailNotValidError:
return False
Se preferisci eseguire tu il controllo DNS, per mettere in cache i risultati, usare un
tuo resolver o impostare un timeout stretto, passa direttamente a dnspython:
import dns.resolver # pip install dnspython
def domain_has_mx(domain: str) -> bool:
try:
return len(dns.resolver.resolve(domain, "MX")) > 0
except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer, dns.resolver.NoNameservers):
return False
Livello 3: la casella esiste davvero?#
I livelli uno e due possono solo escludere un indirizzo. Un dominio può pubblicare
record MX perfetti e non avere comunque una casella all'indirizzo che hai in mano:
noreply-9f2x@gmail.com ha una sintassi valida e un instradamento della posta attivo, ma resta
una casella mai creata. Confermare una casella specifica significa affrontare la
conversazione SMTP di recapito: connettersi all'host di posta, inviare RCPT TO, leggere
la risposta e disconnettersi prima di inviare qualsiasi cosa.
In teoria potresti implementarla da Flask con smtplib. In pratica non dovresti eseguirla 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 connetti e i server riceventi
applicano greylisting e limiti di frequenza ai mittenti sconosciuti, quindi un test che
funziona in locale fallisce silenziosamente 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 con requests, leggendo la tua chiave dall'ambiente:
import os
import requests # pip install requests
QUALISEND_VERIFY_URL = "https://app.qualisend.com/api/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", "smtp"}
La risposta è un envelope { "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 forma esatta dei campi; qui per decidere ti basta lo
status. Rifiutare undeliverable è l'impostazione predefinita sicura; potresti rifiutare
anche risky, oppure salvare lo score e lasciar passare l'indirizzo: è una scelta di policy, e la
guida alle iscrizioni serverless spiega quanto
essere severi al punto di acquisizione.
Quanto sia definitivo quello status dipende da result["smtp"]. cached o skipped
significa che l'esito è già definitivo. pending significa che il test SMTP della casella
è ancora in corso: lo status riflette i controlli locali, reason resta null e l'esito
confermato arriva su GET /jobs/{job_id}, usando il job_id al livello superiore della
stessa risposta (accanto a result; verify() qui sopra restituisce solo result, quindi
conserva anche res.json()["job_id"] quando smtp è pending). Interrogalo da un task in
background, mai dalla richiesta, e non chiamare di nuovo /verify per lo stesso indirizzo.
All'iscrizione, agisci sullo status immediato e lascia che quel task in background
contrassegni l'account se il risultato del job completato è undeliverable (leggi
results[0].status con ?include=results; lo status del job dice solo
completed).
Mettere insieme i livelli per validare un indirizzo email in Flask#
Il punto idiomatico per aggiungere un controllo su un campo in Flask-WTF è un metodo inline
validate_<fieldname> sul modulo. WTForms aggiunge quel metodo alla catena di validatori del
campo e lo esegue dopo i validatori in elenco, quindi quando scatta,
Email(check_deliverability=True) ha già risolto sintassi e ricerca MX. Controlla
field.errors, così la chiamata all'API avviene solo per gli indirizzi che hanno superato
entrambi i livelli meno costosi:
from flask_wtf import FlaskForm
from wtforms import StringField
from wtforms.validators import DataRequired, Email, ValidationError
import requests
class SignupForm(FlaskForm):
email = StringField("Email", validators=[
DataRequired(),
Email(check_deliverability=True), # layers 1 + 2
])
def validate_email(self, field): # layer 3 — runs after the list above
if field.errors: # syntax or MX already failed
return # don't spend an API credit
try:
result = verify(field.data)
except requests.RequestException: # timeout, 402, 429, 5xx: fail open
return # let the signup through, re-verify later
if result["status"] == "undeliverable":
raise ValidationError("We couldn't confirm a mailbox at this address.")
Tutto il segreto sta in quel controllo su field.errors. Un indirizzo malformato o un
dominio morto registra un errore nella catena prima che parta il metodo inline, quindi il
controllo ferma la chiamata di verifica sugli indirizzi che non avevano speranze: una
validazione nell'ordine sbagliato, o senza questo controllo, spende un credito per ogni
errore di battitura. In una view, validate_on_submit esegue l'intera pipeline prima del
corpo del tuo handler:
@app.route("/signup", methods=["POST"])
def signup():
form = SignupForm()
if form.validate_on_submit():
# every layer passed — safe to persist
create_account(form.email.data)
return redirect(url_for("welcome"))
return render_template("signup.html", form=form), 400
C'è una cosa da decidere in anticipo: cosa succede quando fallisce la chiamata all'API stessa. Un
timeout di rete dentro validate_email, o l'HTTPError che raise_for_status() solleva su
un 402 (crediti esauriti), un 429 (limite di frequenza) o un 5xx, non deve mandare la
richiesta in errore 500 né lasciare a un vero cliente un errore che non può risolvere.
Intercetta requests.RequestException separatamente da ValidationError e tratta un
servizio irraggiungibile come unknown anziché undeliverable: lascia passare l'iscrizione
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, quindi stai ammorbidendo solo
il livello che dipende dalla rete.
Non tutte le app Flask usano WTForms. Per un'API JSON gli stessi tre livelli si leggono
direttamente in una route, restituendo un 400 su un indirizzo errato invece di sollevare
ValidationError:
from flask import Flask, request, jsonify
from email_validator import validate_email, EmailNotValidError
import requests
app = Flask(__name__)
@app.post("/api/signup")
def api_signup():
email = (request.get_json(silent=True) or {}).get("email", "")
try:
info = validate_email(email, check_deliverability=True) # layers 1 + 2
except EmailNotValidError as exc:
return jsonify(error=str(exc)), 400
try:
status = verify(info.normalized)["status"] # layer 3
except requests.RequestException: # timeout, 402, 429, 5xx
status = "unknown" # fail open, re-verify later
if status == "undeliverable":
return jsonify(error="Email address appears undeliverable."), 400
# ... create the account
return jsonify(ok=True), 201
info.normalized è l'indirizzo in forma canonica restituito da email-validator, ed è
quello che dovresti salvare. È la stessa forma a tre livelli che trovi nelle versioni per
Node.js e
Python di questa guida: è la struttura a livelli, non
il framework, a far funzionare la validazione. Quando devi scegliere quale servizio di
verifica usare per il livello tre, il
confronto tra API passa in rassegna le opzioni.
Domande frequenti
01Il validatore Email di WTForms basta per validare un indirizzo email?
Per la sintassi, sì: Email() delega al pacchetto email-validator ed è un controllo di primo livello migliore di qualsiasi regex scritta a mano. Ma di default valida la forma, non la deliverability. Aggiungi check_deliverability=True ed eseguirà anche una ricerca MX in tempo reale, che esclude i domini morti; anche così non contatta mai la casella, quindi un esito positivo significa "il dominio accetta posta", non "questo indirizzo esiste". Abbinalo a un controllo SMTP della casella prima di fidarti dell'indirizzo.
02Come aggiungo un validatore email personalizzato in Flask-WTF?
Ci sono due opzioni idiomatiche. Per un controllo una tantum, aggiungi al modulo un metodo inline chiamato validate_<fieldname> (es. validate_email(self, field)) e solleva wtforms.validators.ValidationError quando il valore non è valido; WTForms lo esegue dopo i validatori elencati nel campo. Per una regola riutilizzabile, scrivi un callable che riceve (form, field) e solleva lo stesso errore, poi passalo nell'elenco validators del campo. In entrambi i casi è lì che va la chiamata di verifica a Qualisend.
03Posso controllare se una casella esiste in Flask senza un'API?
In parte. email-validator con check_deliverability=True (o direttamente dnspython) conferma che il dominio accetta posta, il che esclude gratis i domini morti e richiede solo una piccola dipendenza. Confermare la casella significa affrontare una conversazione SMTP che puoi tentare con smtplib, ma che non dovresti eseguire dal tuo server applicativo: la porta 25 è ampiamente bloccata e il risultato dipende dalla reputazione del tuo IP. Un servizio di verifica esiste proprio per gestire questo livello.
04Devo validare le email all'iscrizione o quando pulisco una lista?
Entrambe le cose, con una profondità diversa. Esegui i livelli di sintassi e MX in modo sincrono nel modulo o nella route (sono abbastanza veloci da bloccare la richiesta e dare un riscontro immediato) e agisci lì anche sull'esito dell'API. Riserva la verifica in batch, più pesante, alla pulizia delle liste e al lavoro di back-office, dove la latenza non conta e puoi elaborare gli indirizzi in blocco anziché un invio del modulo alla volta.
Vuoi aggiungere il livello SMTP? Inserisci un indirizzo approvato da Email() nella
verifica email gratuita per vedere una stringa valida per WTForms
tornare undeliverable, poi collega lo stesso esito al tuo modulo con il
riferimento API, esempi da copiare e incollare inclusi.



