Regex pour valider une adresse e-mail : un motif pratique et ses limites
Aucune regex ne peut dire si une adresse fonctionne. Un motif court repère les fautes de frappe ; un e-mail de confirmation vérifie le reste.
Cherchez une regex pour les adresses e-mail et vous trouverez de tout, de huit caractères à plusieurs milliers. Les plus longues essaient de suivre les normes à la lettre et restent pourtant incapables de dire si une boîte aux lettres existe. Les plus courtes se lisent facilement et, utilisées pour la bonne tâche, suffisent largement. Cette tâche consiste à repérer les erreurs évidentes, comme un @ manquant ou une espace, avant d'envoyer l'e-mail de confirmation qui fait la vraie vérification.
Pourquoi viser toute la grammaire de la RFC 5322 est une erreur
La RFC 5322 définit la syntaxe des adresses dans les messages électroniques, et elle autorise bien plus que ce qu'un formulaire d'inscription souhaite accepter. Toutes ces adresses sont syntaxiquement valides :
"john doe"@example.com: une partie locale entre guillemets qui contient une espace.john(comment)@example.com: un commentaire à l'intérieur de l'adresse.user@[192.168.0.1]: une adresse IP à la place d'un nom de domaine.admin@localhost: un domaine sans point.
Une regex qui implémente toute la grammaire, commentaires et espaces de repli compris, est énorme, difficile à relire, et accepte des adresses que beaucoup de systèmes de messagerie refusent. Elle laisse même passer jane@gmial.com. SMTP (RFC 5321) ajoute des limites de longueur, 64 octets pour la partie avant le @ et 255 pour le domaine, qui, combinées à la syntaxe du chemin, donnent un maximum pratique de 254 caractères. Une vérification de longueur s'écrit plus simplement en code qu'en regex.
Un motif pragmatique
Voici le motif dont la plupart des applications ont réellement besoin : quelque chose sans espace ni @, un seul @, puis un domaine qui contient au moins un point.
^[^\s@]+@[^\s@]+\.[^\s@]+$
Il a une faiblesse qui mérite d'être corrigée. Comme [^\s@] accepte aussi les points, il laisse passer des labels de domaine vides, comme dans user@example..com. Exclure le point de chaque label comble ce trou :
^[^\s@]+@[^\s@.]+(?:\.[^\s@.]+)+$
Voici ce que chaque motif a fait d'une série d'adresses de test dans Node 24, à côté du motif de la spécification HTML présenté dans la section suivante :
| Adresse | Motif court | Motif par labels | HTML type=email |
|---|---|---|---|
first.last+news@example.co.uk | Acceptée | Acceptée | Acceptée |
o'brien@example.ie | Acceptée | Acceptée | Acceptée |
user@bücher.de | Acceptée | Acceptée | Refusée |
δοκιμή@παράδειγμα.δοκιμή | Acceptée | Acceptée | Refusée |
"john doe"@example.com | Refusée | Refusée | Refusée |
admin@localhost | Refusée | Refusée | Acceptée |
user @example.com | Refusée | Refusée | Refusée |
user@@example.com | Refusée | Refusée | Refusée |
user@example..com | Acceptée | Refusée | Refusée |
user@example.com. | Acceptée | Refusée | Refusée |
us..er@example.com | Acceptée | Acceptée | Acceptée |
user@-example.com | Acceptée | Acceptée | Refusée |
user@example.c | Acceptée | Acceptée | Acceptée |
Aucun des trois n'est parfait, et aucun n'a besoin de l'être. Les points consécutifs dans la partie locale et les domaines de premier niveau d'une seule lettre passent partout ; l'e-mail de confirmation s'en charge. L'essentiel est de ne refuser aucune adresse réelle et courante.
Il existe aussi une raison de performance de préférer la version par labels. Sur une entrée fabriquée de 40 000 caractères terminée par une espace, le motif court a mis environ 2,9 secondes dans Node à cause du retour arrière (backtracking), alors que le motif par labels a terminé en moins d'une milliseconde. Vérifier la limite de 254 caractères avant d'exécuter le moindre motif règle le problème dans les deux cas.
Le motif HTML5 de input type=email
Un champ <input type="email"> est validé par le navigateur selon une définition du standard HTML du WHATWG. La spécification la qualifie elle-même de violation délibérée (willful violation) de la RFC 5322 et donne cette expression régulière JavaScript comme implémentation :
/^[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])?)*$/
Il autorise la ponctuation habituelle avant le @, exige des labels de domaine d'au plus 63 lettres, chiffres et tirets qui ne commencent ni ne se terminent par un tiret, et n'exige pas de point : admin@localhost passe donc. Il refuse tout caractère non ASCII. Vous pouvez réutiliser la vérification du navigateur dans un script avec input.checkValidity() ou input.validity.typeMismatch, mais tout ce que vérifie un navigateur peut être contourné : validez de nouveau côté serveur.
Les adresses internationales
Les domaines peuvent contenir des caractères non ASCII. bücher.de circule dans le DNS sous la forme xn--bcher-kva.de, et new URL('http://bücher.de').hostname en JavaScript, idn_to_ascii() en PHP et 'bücher.de'.encode('idna') en Python produisent tous cette forme. La partie avant le @ peut elle aussi contenir des caractères non ASCII, mais seuls les serveurs de messagerie qui prennent en charge SMTPUTF8 (RFC 6531) savent livrer le courrier à ces adresses.
Décidez en fonction de votre fournisseur de messagerie, pas d'une regex. Les motifs construits sur [^\s@] acceptent les adresses internationales, alors que le motif HTML les refuse. filter_var($email, FILTER_VALIDATE_EMAIL) en PHP les refuse aussi : lors de nos tests, il a rejeté user@bücher.de comme δοκιμή@example.com, et avec l'option FILTER_FLAG_EMAIL_UNICODE il a accepté la seconde adresse mais toujours refusé le domaine Unicode.
La même vérification en JavaScript, Python et PHP
Chaque fonction retire les espaces autour de la valeur, applique la limite de longueur et exige que la chaîne entière corresponde. Les trois ont donné des résultats identiques pour les adresses ci-dessus et pour quelques cas limites : des espaces autour de la valeur, un saut de ligne au milieu, une espace insécable dans la partie locale et une adresse de 256 caractères.
// JavaScript
function isValidEmail(value) {
const email = value.trim();
return email.length <= 254 && /^[^\s@]+@[^\s@.]+(?:\.[^\s@.]+)+$/.test(email);
}
# Python
import re
EMAIL_RE = re.compile(r"[^\s@]+@[^\s@.]+(?:\.[^\s@.]+)+")
def is_valid_email(value: str) -> bool:
email = value.strip()
return len(email) <= 254 and EMAIL_RE.fullmatch(email) is not None
// PHP
function isValidEmail(string $value): bool
{
$email = trim($value);
return strlen($email) <= 254
&& preg_match('/^[^\s@]+@[^\s@.]+(?:\.[^\s@.]+)+$/Du', $email) === 1;
}
Deux détails de ces extraits évitent de vrais bugs. En Python, re.match() avec $ a accepté "user@example.com\n", car $ correspond aussi juste avant un saut de ligne final ; re.fullmatch() l'a refusée. PHP se comporte de la même façon tant que vous n'ajoutez pas le modificateur D. Le modificateur u compte aussi en PHP : sans lui, \s ne reconnaissait pas l'espace insécable, et user\u{00A0}x@example.com était acceptée. En JavaScript, n'ajoutez pas le drapeau g si vous vous contentez d'appeler test().
La vraie validation, c'est l'e-mail de confirmation
Un motif peut seulement dire qu'une adresse semble plausible. Il ne sait pas si la boîte existe, si elle accepte du courrier, ni si elle appartient à la personne qui la saisit. Consulter les enregistrements MX montre qu'un domaine est configuré pour recevoir du courrier, pas qu'une boîte précise existe. Le seul test fiable consiste à envoyer un message :
- Enregistrez l'adresse comme non vérifiée.
- Envoyez par e-mail un lien contenant un jeton aléatoire, à usage unique et qui expire, par exemple au bout de 24 heures.
- Ne marquez l'adresse comme vérifiée qu'au moment où le lien est ouvert.
- D'ici là, n'envoyez rien d'autre à cette adresse.
Limitez la normalisation au strict minimum : retirez les espaces et mettez le domaine en minuscules. La RFC 5321 considère la partie avant le @ comme sensible à la casse, même si la plupart des fournisseurs l'ignorent : enregistrez-la donc telle qu'elle a été saisie.
Essayez les motifs sur votre propre liste
Collez l'un des motifs dans le testeur d'expressions régulières sans les barres obliques qui l'encadrent, réglez les drapeaux sur gm et saisissez vos adresses d'exemple, une par ligne. Avec le drapeau m, ^ et $ s'appliquent à chaque ligne : chaque adresse acceptée est donc surlignée séparément. Nous avons vérifié, pour les trois motifs ci-dessus, que cette vue ligne par ligne donne les mêmes résultats qu'un test de chaque adresse isolée. Dans le code d'une application, testez en revanche une adresse à la fois et sans m, sinon une valeur de plusieurs lignes pourrait passer simplement parce que l'une de ses lignes correspond.