Pour valider une adresse e-mail dans Flask, vous effectuez trois contrôles, pas un
seul — et Flask lui-même n'en fournit aucun, car Flask ne dispose pas de sa propre
couche de formulaires. Ce qu'il possède, c'est un écosystème : WTForms
(généralement via Flask-WTF) pour la gestion des formulaires, et le paquet
email-validator sur lequel WTForms s'appuie. Ensemble, ils répondent aux deux
premières des trois questions de la validation — l'adresse est-elle bien formée,
et son domaine peut-il recevoir du courrier — mais jamais à la troisième : la boîte
aux lettres existe-t-elle réellement. La vraie validation se fait par couches :
syntaxe, puis résolution DNS, puis sondage SMTP de la boîte aux lettres. Ce guide
construit chaque couche sous forme d'un validateur WTForms que vous pouvez insérer
dans un formulaire Flask-WTF ou une simple route JSON, et montre où une API de
vérification prend le relais. Il reprend là où s'arrête le
guide Python, si bien que le raisonnement
DNS et SMTP qui s'y trouve reste intégralement valable.
La réponse en bref#
Utilisez le validateur Email() de WTForms pour la syntaxe, son option
check_deliverability=True (ou dnspython) pour la résolution MX, et une API de
vérification pour le contrôle SMTP de la boîte aux lettres — câblés du moins cher au
plus cher pour que chaque couche court-circuite avant que la suivante ne s'exécute.
N'ouvrez pas de connexions SMTP depuis votre processus Flask pour sonder les boîtes
aux lettres : le port 25 sortant est bloqué sur la plupart des hébergeurs, et la
réponse dépend de la réputation de l'IP d'envoi et du greylisting que vous n'avez
pas envie de réimplémenter. Chaque couche écarte des adresses de façon plus
économique que la précédente ; seule l'API peut en valider une. Si ce pipeline vous
est nouveau, ce qu'est la vérification d'e-mails
en présente d'abord les termes.
Couche 1 : validation du format avec le validateur Email de WTForms#
WTForms se charge de la première couche, et il ne la bricole pas à la main.
wtforms.validators.Email délègue au paquet email-validator : déclarer un
validateur Email() sur un champ vous offre donc un vrai contrôle de syntaxe dérivé
de la RFC — pas une expression régulière — gratuitement :
from flask_wtf import FlaskForm
from wtforms import StringField
from wtforms.validators import DataRequired, Email
class SignupForm(FlaskForm):
email = StringField("Email", validators=[DataRequired(), Email()])
Le validateur Email() nécessite l'installation du paquet
(pip install email-validator, ou pip install wtforms[email]) ; sans lui, WTForms
lève une erreur au moment de la validation pour vous le signaler. En interne, il
appelle email_validator.validate_email, que vous pouvez aussi utiliser seul — hors
d'un formulaire, ou dans une couche de service — pour obtenir le même verdict sous
forme de booléen :
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
Sachez exactement ce que cela vous apporte. Avec check_deliverability=False, le
contrôle ne lit que la forme — il ne résout jamais le DNS, n'ouvre jamais de
socket, et ignore totalement si example.com existe. definitely-fake@gmail.com
passe. info@company-that-folded.com passe. typo@gmial.com passe. Toutes trois
sont non délivrables, et aucun validateur qui se contente de lire la chaîne ne vous
le dira jamais, pour la même raison qui fait que
la validation d'e-mail par regex échoue :
syntaxe et délivrabilité sont deux questions différentes. L'une est un fait à propos
de la chaîne ; l'autre est un fait à propos d'Internet.
Couche 2 : le domaine accepte-t-il le courrier ?#
Un domaine sans enregistrement MX ne peut recevoir de courrier pour personne : une
seule résolution DNS élimine donc les domaines morts, les noms d'entreprise mal
orthographiés et les TLD inventés. Ici, la pile de Flask fait quelque chose que
celle de Django ne fait pas d'emblée : email-validator peut effectuer la
résolution pour vous. Activez check_deliverability et il résout les enregistrements
MX du domaine (avec repli sur A/AAAA) grâce à dnspython, en levant
EmailUndeliverableError — une sous-classe de EmailNotValidError — lorsque le
domaine ne peut pas recevoir de courrier. Sur un formulaire, cela tient à une option
sur le validateur que vous avez déjà :
email = StringField("Email", validators=[DataRequired(), Email(check_deliverability=True)])
Cet unique indicateur fond les couches un et deux en un seul validateur. En usage autonome, le même appel renvoie un objet d'adresse normalisée lorsque le domaine est validé :
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
Si vous préférez effectuer le contrôle DNS vous-même — pour mettre les résultats en
cache, pointer vers votre propre résolveur, ou fixer un délai d'expiration serré —
descendez directement vers 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
Couche 3 : la boîte aux lettres existe-t-elle réellement ?#
Les couches un et deux ne peuvent qu'écarter une adresse. Un domaine peut publier
des enregistrements MX parfaits et n'avoir malgré tout aucune boîte aux lettres à
l'adresse que vous détenez — noreply-9f2x@gmail.com est syntaxiquement valide sur
une route de courrier active, et c'est pourtant une boîte aux lettres qui n'a jamais
été créée. Confirmer une boîte aux lettres précise suppose le dialogue de livraison
SMTP : se connecter à l'hôte de messagerie, émettre RCPT TO, lire la réponse, et
se déconnecter avant d'envoyer quoi que ce soit.
En principe, vous pourriez scripter cela depuis Flask avec smtplib. En pratique,
vous ne devriez pas l'exécuter depuis votre serveur applicatif : la plupart des
hébergeurs cloud bloquent le port 25 sortant, la réponse dépend de la réputation de
l'IP depuis laquelle vous vous connectez, et les serveurs de réception appliquent
greylisting et limitation de débit aux expéditeurs inconnus — si bien qu'un sondage
qui fonctionne dans un test local échoue silencieusement, ou vous fait blacklister,
en production. Comment fonctionne la vérification d'e-mails
parcourt le pipeline complet, domaines catch-all inclus. C'est la couche qui vaut la
peine d'être déléguée.
Le point de terminaison verify de Qualisend exécute tout le pipeline — syntaxe, DNS
et sondage SMTP de la boîte aux lettres — depuis une infrastructure à réputation
gérée, et renvoie un verdict. Appelez-le avec requests, en lisant votre clé depuis
l'environnement :
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 réponse est une enveloppe { "result": { ... } }. Lisez result["status"] —
deliverable, risky, undeliverable ou unknown — aux côtés d'un score, d'une
reason et de sub_flags pour des traits comme les adresses de rôle ou jetables.
Consultez la référence de l'API pour la forme exacte des champs ; ici
vous n'avez besoin que du statut pour décider. Rejeter undeliverable est le choix
par défaut sûr ; vous pourriez aussi rejeter risky, ou stocker le score et
laisser passer — c'est une décision de politique, et le
guide d'inscription serverless explique
jusqu'où être strict au moment de la collecte.
Assembler les couches pour valider une adresse e-mail dans Flask#
L'endroit idiomatique pour ajouter un contrôle par champ dans Flask-WTF est une
méthode en ligne validate_<nomduchamp> sur le formulaire. WTForms ajoute cette
méthode à la chaîne de validateurs du champ et l'exécute après les validateurs de
la liste : au moment où elle se déclenche, Email(check_deliverability=True) a donc
déjà réglé la syntaxe et la résolution MX. Protégez-vous avec field.errors pour
que l'aller-retour vers l'API n'ait lieu que pour les adresses ayant franchi les
deux couches moins coûteuses :
from flask_wtf import FlaskForm
from wtforms import StringField
from wtforms.validators import DataRequired, Email, ValidationError
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
if verify(field.data)["status"] == "undeliverable":
raise ValidationError("We couldn't confirm a mailbox at this address.")
Cette protection field.errors est toute l'astuce. Une adresse mal formée ou un
domaine mort enregistre une erreur dans la chaîne avant que la méthode en ligne ne
s'exécute : la protection empêche donc l'appel verify sur les adresses qui n'avaient
aucune chance — une validation dans le mauvais ordre, ou sauter la protection,
dépense un crédit à chaque faute de frappe. Dans une vue, validate_on_submit
exécute tout le pipeline avant que le corps de votre gestionnaire ne s'exécute :
@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
Une chose à décider d'emblée : ce qui se passe quand l'appel API lui-même échoue. Un
délai d'expiration réseau ou une exception requests à l'intérieur de
validate_email ne devrait pas provoquer un 500 sur la requête ni infliger à un
vrai client une erreur qu'il ne peut pas corriger. Interceptez
requests.RequestException séparément de ValidationError, et traitez un service
injoignable comme unknown plutôt que undeliverable — laissez passer
l'inscription et revérifiez l'adresse plus tard, au lieu de bloquer l'enregistrement
sur une panne transitoire. Les couches syntaxe et MX ont déjà tourné : vous
n'assouplissez donc que la couche qui dépend du réseau.
Toutes les applications Flask n'utilisent pas WTForms. Pour une API JSON, les trois
mêmes couches se lisent directement dans une route, renvoyant un 400 sur une
mauvaise adresse au lieu de lever ValidationError :
from flask import Flask, request, jsonify
from email_validator import validate_email, EmailNotValidError
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
if verify(info.normalized)["status"] == "undeliverable": # layer 3
return jsonify(error="Email address appears undeliverable."), 400
# ... create the account
return jsonify(ok=True), 201
info.normalized est l'adresse canonisée que email-validator renvoie, et c'est
elle que vous devriez stocker. C'est la même structure à trois couches que vous
retrouverez dans les versions Node.js et
Python de ce guide — c'est le découpage en
couches, pas le framework, qui fait fonctionner la validation. Au moment de choisir
quel service de vérification alimente la troisième couche, la
comparaison d'API aligne les options.
Foire aux questions#
Le validateur Email de WTForms suffit-il pour valider une adresse e-mail ?#
Pour la syntaxe, oui — Email() délègue au paquet email-validator et constitue un
bien meilleur contrôle de première couche que n'importe quelle expression régulière
artisanale. Mais par défaut, il valide la forme, pas la délivrabilité. Ajoutez
check_deliverability=True et il effectue aussi une résolution MX en direct, qui
écarte les domaines morts ; même dans ce cas, il ne contacte jamais la boîte aux
lettres, si bien qu'un succès signifie « le domaine accepte le courrier », pas
« cette adresse existe ». Associez-le à un contrôle SMTP de la boîte aux lettres
avant de faire confiance à l'adresse.
Comment ajouter un validateur d'e-mail personnalisé dans Flask-WTF ?#
Deux options idiomatiques. Pour un contrôle ponctuel, ajoutez au formulaire une
méthode en ligne nommée validate_<nomduchamp> — par exemple
validate_email(self, field) — et levez wtforms.validators.ValidationError
lorsque la valeur échoue ; WTForms l'exécute après les validateurs listés sur le
champ. Pour une règle réutilisable, écrivez un appelable qui prend (form, field)
et lève la même erreur, puis passez-le dans la liste validators du champ. C'est
dans l'un ou l'autre que doit se placer l'appel verify de Qualisend.
Puis-je vérifier l'existence d'une boîte aux lettres dans Flask sans API ?#
En partie. email-validator avec check_deliverability=True (ou dnspython
directement) confirme que le domaine accepte le courrier, ce qui écarte gratuitement
les domaines morts et ne nécessite rien de plus qu'une petite dépendance. Confirmer
la boîte aux lettres suppose un dialogue SMTP que vous pouvez tenter avec smtplib
mais ne devriez pas exécuter depuis votre serveur applicatif — le port 25 est
largement bloqué et le résultat dépend de la réputation de votre IP. C'est la couche
qu'un service de vérification existe précisément pour prendre en charge.
Faut-il valider les e-mails à l'inscription ou lors du nettoyage d'une liste ?#
Les deux, à des profondeurs différentes. Exécutez les couches syntaxe et MX de façon synchrone dans le formulaire ou la route — elles sont assez rapides pour bloquer la requête et offrir un retour immédiat — et agissez aussi sur le verdict de l'API à cet endroit. Réservez la vérification par lots, plus lourde, au nettoyage de listes et au travail de back-office, où la latence importe peu et où vous pouvez traiter les adresses en masse plutôt qu'un envoi de formulaire à la fois.
Prêt à ajouter la couche SMTP ? Déposez une adresse approuvée par Email() dans le
vérificateur d'e-mails gratuit pour voir une chaîne jugée
valide par WTForms revenir undeliverable, puis câblez le même verdict dans votre
formulaire à l'aide de la référence de l'API — exemples à copier-coller
inclus.