Skip to content
Inizia con 100 crediti di verifica gratuiti
Qualisend
Tutti gli articoli
Ingegneria / 10 maggio 2026

Come validare un indirizzo email in Flask

9 min di lettura

Qualisend team
Come validare un indirizzo email in Flask

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.

La tua reputazione, protetta.

Pulisci la tua prima lista in pochi minuti. 100 crediti gratuiti, senza carta di credito.

Inizia