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

snippets/validate-an-email-address-without-sending-a-message/n0.pyTesté tel quel — Cet extrait s’exécute avec ses vraies dépendances, et son test tourne à chaque construction du site.
"""
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

snippets/validate-an-email-address-without-sending-a-message/n0.jsTesté tel quel — Cet extrait s’exécute avec ses vraies dépendances, et son test tourne à chaque construction du site.
/**
 * 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

interactif

Le 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.

à vous d’écrire

Recalculé à chaque frappe.

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 que le code en fait

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.

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

  1. RecommandéN0La définition du standard HTML, plus les longueurs de la RFC 5321Gratuit · Instantané
  2. N1Niveau sans objet ici
  3. N2Niveau sans objet ici
  4. 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

Source des chiffres

Métadonnées