Vérifier une adresse e-mail sans envoyer de message
Contrôler qu’une adresse saisie dans un formulaire a la forme d’une adresse, avant de l’enregistrer.
Pas besoin d’IA
GratuitInstantanéVos données ne sortent pas
Le verdict
N0, parce que la définition est publiée et qu’elle est celle que votre visiteur vient de subir dans son navigateur : un serveur qui applique autre chose refuse ce que le champ a accepté, ou l’inverse. C’est mesuré dans la fiche : sur trente-deux adresses, `email-validator` et `validator.js` rendent quatre verdicts différents l’un de l’autre, et quatorze qui s’écartent de la définition du navigateur — dont une partie locale de soixante-cinq caractères qu’`email-validator` accepte alors que la RFC 5321 la plafonne à soixante-quatre. Ce que ce niveau ne fait pas, il le dit : le rapport rend `routable` à faux sur un domaine sans point, et la suite — le domaine reçoit-il du courrier — est une résolution DNS, pas un niveau de plus.
La définition du standard HTML, plus les longueurs de la RFC 5321
Pas besoin d’IA
Règle et algorithme classiqueN0GratuitInstantané
Python
"""
Contrôler une adresse électronique avec la définition que le navigateur a
déjà appliquée.
Niveau N0, et il n'y en a pas d'autre : rien ici ne touche au réseau, donc
rien ici ne sait si la boîte existe. Ce que le code sait, c'est la forme.
Cette forme est spécifiée. Le standard HTML définit ce qu'est une adresse
valide pour `input type=email` et publie l'expression régulière
correspondante, en précisant que « cette exigence est une violation
délibérée de la RFC 5322 ». Cette violation est justement l'intérêt : c'est
la définition contre laquelle vos utilisateurs ont déjà été validés dans leur
navigateur, donc celle avec laquelle un serveur devrait être d'accord.
Les bibliothèques mûres, elles, ne sont d'accord ni avec elle ni entre elles.
Sur les trente-deux adresses du test, `email-validator` 2.3.0 en Python et
`validator.js` 13.15.35 en JavaScript rendent quatre verdicts différents, et
la définition du navigateur s'écarte de l'une ou de l'autre quatorze fois.
Parmi les quatre : la forme entre guillemets "jean dupont"@exemple.fr, un
domaine de premier niveau d'une seule lettre, et une partie locale de
soixante-cinq caractères, qu'`email-validator` accepte alors que la RFC 5321
la plafonne à soixante-quatre — d'où la longueur contrôlée ici plutôt que
laissée à une bibliothèque.
Deux choses que le rapport dit au lieu de les trancher. La partie avant
l'arobase garde sa casse, parce que la RFC 5321 la rend sensible à la casse
et que seul le domaine ne l'est pas : mettre l'adresse entière en minuscules
réécrit en silence le nom d'une boîte. Et `routable` vaut faux pour
jean@localhost, que la définition HTML accepte : un nom d'hôte sans point est
une adresse valide qu'aucun serveur de courrier public n'atteindra.
"""
from __future__ import annotations
import re
# The regular expression published by the HTML standard for input type=email,
# transcribed character for character.
WHATWG = re.compile(
r"^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+"
r"@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?"
r"(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$"
)
# RFC 5321, section 4.5.3.1: sixty-four octets for the local part, two hundred
# and fifty-six for the path, which leaves two hundred and fifty-four for the
# address itself once the angle brackets are counted.
MAX_LOCAL, MAX_ADDRESS = 64, 254
# The five characters the HTML standard calls ASCII whitespace. The vertical
# tab is not one of them, and `str.strip()` with no argument would remove it.
ASCII_WHITESPACE = "\t\n\f\r "
def check_email_syntax(raw) -> dict:
"""
Say whether `raw` has the shape of an address, and what it normalises to.
Nothing raises, and nothing is sent: a refusal comes back as `valid: False`
with a `reason`, because a validator that throws in a request path leaves
the form with nothing to show.
"""
if not isinstance(raw, str):
return _report(False, None, f"an address is text, not {type(raw).__name__}")
# The HTML value sanitisation algorithm for an email field, in full: « Strip
# newlines from the value, then strip leading and trailing ASCII whitespace
# from the value. » Both halves matter, and in that order: an address pasted
# from a signature carries a newline in the middle, which the browser
# removes before validating; and the five ASCII whitespace characters do not
# include the vertical tab, which the browser therefore refuses. A server
# that does not do the same refuses what the browser accepted, or the other
# way round.
written = raw.replace("\n", "").replace("\r", "").strip(ASCII_WHITESPACE)
if not WHATWG.match(written):
return _report(False, None, "does not match the HTML definition of an email address")
local, _, domain = written.rpartition("@")
if len(local) > MAX_LOCAL:
return _report(False, None, f"the part before the @ is over {MAX_LOCAL} characters")
# Only the domain is case-insensitive. The local part belongs to the
# receiving server, and RFC 5321 says it may tell Jean from jean.
normalised = f"{local}@{domain.lower()}"
if len(normalised) > MAX_ADDRESS:
return _report(False, None, f"over {MAX_ADDRESS} characters")
return _report(True, normalised, None)
def _report(valid: bool, normalised: str | None, reason: str | None) -> dict:
local, _, domain = (normalised or "").rpartition("@")
return {
"valid": valid,
"normalised": normalised,
"local": local or None,
"domain": domain or None,
# A domain with no dot is a host name on the local network. The HTML
# definition allows it; public mail does not reach it. That is all this
# field says: `jean@1.2.3.4` has three dots and comes back routable,
# because nothing here resolves anything.
"routable": bool(domain) and "." in domain,
"reason": reason,
}JavaScript
/**
* Contrôler une adresse électronique avec la définition que le navigateur a
* déjà appliquée.
*
* Niveau N0, et il n'y en a pas d'autre : rien ici ne touche au réseau, donc
* rien ici ne sait si la boîte existe. Ce que le code sait, c'est la forme.
*
* Cette forme est spécifiée. Le standard HTML définit ce qu'est une adresse
* valide pour `input type=email` et publie l'expression régulière
* correspondante, en précisant que « cette exigence est une violation
* délibérée de la RFC 5322 ». Cette violation est justement l'intérêt : c'est
* la définition contre laquelle vos utilisateurs ont déjà été validés dans leur
* navigateur, donc celle avec laquelle un serveur devrait être d'accord.
*
* Les bibliothèques mûres, elles, ne sont d'accord ni avec elle ni entre elles.
* Sur les trente-deux adresses du test, `email-validator` 2.3.0 en Python et
* `validator.js` 13.15.35 en JavaScript rendent quatre verdicts différents, et
* la définition du navigateur s'écarte de l'une ou de l'autre quatorze fois.
* Parmi les quatre : la forme entre guillemets "jean dupont"@exemple.fr, un
* domaine de premier niveau d'une seule lettre, et une partie locale de
* soixante-cinq caractères, qu'`email-validator` accepte alors que la RFC 5321
* la plafonne à soixante-quatre — d'où la longueur contrôlée ici plutôt que
* laissée à une bibliothèque.
*
* Deux choses que le rapport dit au lieu de les trancher. La partie avant
* l'arobase garde sa casse, parce que la RFC 5321 la rend sensible à la casse
* et que seul le domaine ne l'est pas : mettre l'adresse entière en minuscules
* réécrit en silence le nom d'une boîte. Et `routable` vaut faux pour
* jean@localhost, que la définition HTML accepte : un nom d'hôte sans point est
* une adresse valide qu'aucun serveur de courrier public n'atteindra.
*/
// The regular expression published by the HTML standard for input type=email,
// transcribed character for character.
export const WHATWG =
/^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
// RFC 5321, section 4.5.3.1: sixty-four octets for the local part, two hundred
// and fifty-six for the path, which leaves two hundred and fifty-four for the
// address itself once the angle brackets are counted.
export const MAX_LOCAL = 64;
// The five characters the HTML standard calls ASCII whitespace. The vertical
// tab is not one of them, and `String.prototype.trim` would remove it.
const ASCII_WHITESPACE = /^[\t\n\f\r ]+|[\t\n\f\r ]+$/g;
const NEWLINES = /[\n\r]/g;
export const MAX_ADDRESS = 254;
/**
* Say whether `raw` has the shape of an address, and what it normalises to.
*
* Nothing throws, and nothing is sent: a refusal comes back as `valid: false`
* with a `reason`, because a validator that throws in a request path leaves
* the form with nothing to show.
*
* @param {string} raw
*/
export function checkEmailSyntax(raw) {
if (typeof raw !== 'string') {
return report(false, null, `an address is text, not ${kindOf(raw)}`);
}
// The HTML value sanitisation algorithm for an email field, in full: « Strip
// newlines from the value, then strip leading and trailing ASCII whitespace
// from the value. » Both halves matter, and in that order: an address pasted
// from a signature carries a newline in the middle, which the browser removes
// before validating; and the five ASCII whitespace characters do not include
// the vertical tab, which the browser therefore refuses. A server that does
// not do the same refuses what the browser accepted, or the other way round.
const written = raw.replace(NEWLINES, '').replace(ASCII_WHITESPACE, '');
if (!WHATWG.test(written)) {
return report(false, null, 'does not match the HTML definition of an email address');
}
const at = written.lastIndexOf('@');
const local = written.slice(0, at);
const domain = written.slice(at + 1);
if (local.length > MAX_LOCAL) {
return report(false, null, `the part before the @ is over ${MAX_LOCAL} characters`);
}
// Only the domain is case-insensitive. The local part belongs to the
// receiving server, and RFC 5321 says it may tell Jean from jean.
const normalised = `${local}@${domain.toLowerCase()}`;
if (normalised.length > MAX_ADDRESS) {
return report(false, null, `over ${MAX_ADDRESS} characters`);
}
return report(true, normalised, null);
}
const kindOf = (value) => (value === null ? 'null' : typeof value);
function report(valid, normalised, reason) {
const at = (normalised ?? '').lastIndexOf('@');
const local = at > 0 ? normalised.slice(0, at) : null;
const domain = at >= 0 ? normalised.slice(at + 1) : null;
return {
valid,
normalised,
local,
domain: domain || null,
// A domain with no dot is a host name on the local network. The HTML
// definition allows it; public mail does not reach it. That is all this
// field says: `jean@1.2.3.4` has three dots and comes back routable,
// because nothing here resolves anything.
routable: Boolean(domain) && domain.includes('.'),
reason,
};
}Essayer
interactifLe code ci-dessus tourne dans votre navigateur. Écrivez dans le champ : le résultat se refait sous vos yeux.
Tapez une adresse. Le contrôle est celui du standard HTML, celui que votre navigateur applique déjà au champ : rien ne part sur le réseau, et rien ne dit si la boîte existe.
Ou partez d’un exemple
La saisie libre et les autres exemples ont besoin de JavaScript. L’exemple affiché ci-dessous, lui, a été calculé à la construction du site.
Ce qu’il renvoie
{ "valid": true, "normalised": "Jean.DUPONT@exemple.fr", "local": "Jean.DUPONT", "domain": "exemple.fr", "routable": true, "reason": null }
Ce qu’il en conclut
Forme valide
Normalisée en Jean.DUPONT@exemple.fr. Le domaine porte un point : rien ici ne dit qu’il existe, seulement qu’il n’est pas un nom d’hôte local.
Là, ça casse
Ce que ça implique
- Vos données ne sortent pas
- Résultat toujours identique
- Se teste unitairement, cas par cas
- Aucun fournisseur dans la boucle
- Empreinte négligeable
Ce que cela ajoute à votre périmètre réglementaire
- Aucun périmètre spécifique ajouté : l’adresse est contrôlée dans votre processus et n’est transmise à personne
- Une adresse électronique est une donnée personnelle : la contrôler localement ne la communique pas, la soumettre à un service de vérification en ligne la communique
Point de rupture
La syntaxe ne dit rien de la boîte : « jean@gmial.com » passe, et le domaine est une faute de frappe sur gmail.com — la forme est la même, le courriel part et ne revient jamais. Le témoin est dans le même test : « jean@@exemple.fr », « jean@-exemple.fr » et « jean@exemple..fr » sont bien refusés.
Quand passer au niveau suivant
Vos utilisateurs signalent une adresse refusée à tort, et c’est l’adresse internationalisée — « jéan@exemple.fr » — que la définition HTML écarte comme le font tous les navigateurs. La suite est la RFC 6531, pas un modèle.
Les quatre options
- RecommandéN0La définition du standard HTML, plus les longueurs de la RFC 5321Gratuit · Instantané
- N1Niveau sans objet ici
- N2Niveau sans objet ici
- N3Niveau sans objet ici
Les autres options
N1Niveau sans objet ici
Niveau sans objet ici
Modèle classique légerN1
Il n’y a rien à apprendre : la forme d’une adresse est une grammaire publiée, pas une régularité statistique, et un classifieur entraîné sur des adresses étiquetées rendrait un score là où une expression régulière rend une décision qu’on peut relire. Le seul jugement qu’un modèle classique pourrait apporter — ce domaine ressemble à une faute de frappe sur un grand fournisseur — est une distance d’édition contre une liste de domaines connus, c’est-à-dire encore du code ordinaire.
N2Niveau sans objet ici
Niveau sans objet ici
Petit modèle spécialisé auto-hébergéN2
Même argument, avec un service à tenir en plus. Et l’objection propre à ce besoin : le contrôle est appelé à chaque frappe dans un champ, y compris dans le navigateur. Un petit modèle spécialisé y ajouterait un chargement, une mémoire résidente et une inférence par caractère tapé, pour approcher une grammaire que le navigateur applique déjà gratuitement.
N3Niveau sans objet ici
Niveau sans objet ici
API de LLM généralisteN3
Un appel de modèle par adresse saisie coûte un aller-retour réseau, une latence dans un champ de formulaire, et l’envoi d’une donnée personnelle chez un tiers, pour approcher une expression régulière publiée. Il ne sait pas davantage ce que la syntaxe ignore : l’existence du domaine se résout dans le DNS, l’existence de la boîte se prouve en lui écrivant, et un modèle qui affirmerait l’une ou l’autre inventerait.
Pour aller plus loin
- HTML Standard — la définition d’une adresse valide pour input type=email
- RFC 5321 — les longueurs maximales d’une partie locale et d’une adresse
- RFC 6531 — SMTP pour les adresses internationalisées
- email-validator — la bibliothèque Python, qui sait aussi résoudre les enregistrements MX
- validator.js — la bibliothèque JavaScript, et son isEmail
Source des chiffres
- HTML Standard (consulté) : l’expression régulière publiée pour input type=email, et « This requirement is a willful violation of RFC 5322 »
- RFC 5321, section 4.5.3.1 (consultée) : soixante-quatre octets pour la partie locale, deux cent cinquante-six pour le chemin
- email-validator sur PyPI (consulté) : version 2.3.0, publiée le 26 août 2025, licence Unlicense
- validator.js sur son dépôt (consulté) : version 13.15.35, publiée le 2 avril 2026, licence MIT, aucune dépendance
Métadonnées
Information générale, tenue à jour au mieux. Ce n’est pas un avis juridique. Faites valider votre situation par un professionnel.