Valider une adresse e-mail en Node.js, c'est trois métiers qui portent un seul nom. La plupart des tutoriels vous montrent une regex et s'arrêtent là : ils vérifient l'orthographe et appellent ça de la validation. La vraie validation est en couches : un contrôle de syntaxe peu coûteux, une résolution DNS et un test SMTP de la boîte aux lettres — et Node vous fournit les deux premières dès le départ. Ce guide construit chaque couche avec du code fonctionnel et montre où une API de vérification prend le relais.
La réponse courte#
Utilisez une regex permissive pour la syntaxe, node:dns/promises pour la
résolution MX et une API de vérification pour le contrôle SMTP de la boîte aux
lettres — dans cet ordre, du moins coûteux d'abord, en s'arrêtant dès qu'une
couche est décisive. N'essayez pas d'ouvrir des connexions SMTP depuis le serveur
de votre application : le port 25 est bloqué chez la plupart des hébergeurs, et
même là où il ne l'est pas, le contrôle dépend de la réputation de l'IP d'envoi et
d'une gestion du greylisting que vous n'avez pas envie
de construire.
Couche 1 : la syntaxe#
La regex a sa place ici et nulle part ailleurs. Gardez-la permissive — l'objectif est d'attraper les fautes de frappe dès la saisie, pas de réimplémenter la RFC 5322 (qui n'aide de toute façon en rien) :
const SYNTAX = /^[^\s@"]+(?:\.[^\s@"]+)*@[^\s@.]+(?:\.[^\s@.]+)+$/;
export function isValidSyntax(email) {
return typeof email === "string" && email.length <= 320 && SYNTAX.test(email);
}
Un succès ici signifie « ça vaut la peine de vérifier », pas « valide ». Chaque couche suivante suppose que la syntaxe est déjà correcte.
Couche 2 : le domaine peut-il recevoir du courrier ?#
C'est ici que Node justifie sa présence. Le module intégré node:dns/promises
résout les enregistrements MX sans aucune dépendance, et un domaine sans route de
courrier ne peut accepter de messages pour personne — cette unique résolution
élimine donc les domaines morts, les noms d'entreprise mal orthographiés et les
TLD inventés :
import { resolveMx } from "node:dns/promises";
export async function hasMailRoute(domain) {
try {
const mx = await resolveMx(domain);
return mx.length > 0;
} catch {
// ENOTFOUND / ENODATA → domain doesn't exist or publishes no MX
return false;
}
}
await hasMailRoute("gmail.com"); // true
await hasMailRoute("company-that-folded.com"); // false
Certains domaines acceptent le courrier via un enregistrement A sans MX (MX
implicite). Si vous voulez tenir compte de ce cas particulier, repliez-vous sur
dns.resolve4 lorsque resolveMx est vide — mais pour l'écrasante majorité des
adresses réelles, un contrôle MX est le bon filtre.
Couche 3 : la boîte aux lettres existe-t-elle vraiment ?#
Les couches 1 et 2 ne peuvent qu'écarter une adresse. Confirmer une boîte aux
lettres implique la conversation de livraison SMTP — RCPT TO, lire la réponse, se
déconnecter avant d'envoyer quoi que ce soit. En principe, vous pouvez le faire
avec le module net. En pratique, vous ne devriez pas : la plupart des
fournisseurs 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 appliquent
greylisting et limitation de débit aux expéditeurs inconnus. C'est la couche qu'il
vaut la peine de déléguer.
Le POST /verify de Qualisend exécute le pipeline complet et
renvoie un verdict. Les contrôles locaux reviennent immédiatement, le test SMTP
étant mis en file d'attente :
const BASE = "https://app.qualisend.com/api/v1";
export async function verify(email) {
const res = await fetch(`${BASE}/verify`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.QUALISEND_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ email }),
});
if (!res.ok) throw new Error(`Qualisend responded ${res.status}`);
return res.json();
}
{
"job_id": "6f1c2e0a-9b3d-4a1e-8c77-1a2b3c4d5e6f",
"probe_queued": true,
"result": {
"email": "jane@example.com",
"status": "deliverable",
"score": 95,
"sub_flags": { "role": false, "disposable": false, "free": false, "catch_all": false },
"did_you_mean": null,
"smtp": "pending",
"reason": null
}
}
Pour une validation d'inscription en direct, le result immédiat suffit
généralement pour agir — rejeter undeliverable, proposer la correction
did_you_mean, signaler disposable. Lorsque vous avez besoin du verdict confirmé
par SMTP, interrogez le job jusqu'à ce que le test se termine :
export async function verifyAndWait(email, { tries = 10, delayMs = 1500 } = {}) {
const { job_id } = await verify(email);
for (let i = 0; i < tries; i++) {
const res = await fetch(`${BASE}/jobs/${job_id}?include=results`, {
headers: { Authorization: `Bearer ${process.env.QUALISEND_API_KEY}` },
});
const job = await res.json();
if (job.status === "completed") return job.results[0]; // { status, score, reason, ... }
await new Promise((r) => setTimeout(r, delayMs));
}
return null; // still processing — treat as unknown, retry later
}
Deux détails font trébucher le fetch naïf ci-dessus dès qu'il rencontre du trafic
réel. D'abord, un service en amont lent ne doit pas figer votre processus Node —
enveloppez la requête dans un délai d'expiration AbortController pour qu'elle
échoue vite au lieu de bloquer la boucle d'événements. Ensuite, un 429 (débit
limité) ou un 5xx est transitoire et mérite un nouvel essai, tandis qu'un 4xx
comme 422 signifie que la charge utile est incorrecte et qu'un nouvel essai
n'aidera pas — séparez donc les deux :
export async function verify(email, { timeoutMs = 4000 } = {}) {
const ctrl = new AbortController();
const timer = setTimeout(() => ctrl.abort(), timeoutMs);
try {
const res = await fetch(`${BASE}/verify`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.QUALISEND_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ email }),
signal: ctrl.signal,
});
if (res.status === 429 || res.status >= 500) {
throw Object.assign(new Error(`retryable ${res.status}`), { retryable: true });
}
if (!res.ok) throw new Error(`Qualisend responded ${res.status}`);
return res.json();
} finally {
clearTimeout(timer);
}
}
Gardez la clé d'API dans process.env, jamais dans le bundle — cet appel a sa
place du côté serveur de votre application Node, pas dans quoi que ce soit que vous
livrez au navigateur.
Pour le nettoyage de listes, vous vérifiez des milliers d'adresses, pas une à la
fois. Ne bouclez pas avec await (séquentiel et lent) et ne les lancez pas toutes
d'un coup (vous déclencherez la limite de débit). Envoyez un nombre borné de
requêtes concurrentes avec Promise.allSettled afin qu'une adresse rejetée ne
coule pas tout le lot :
export async function verifyMany(emails, { concurrency = 5 } = {}) {
const out = [];
for (let i = 0; i < emails.length; i += concurrency) {
const slice = emails.slice(i, i + concurrency);
const settled = await Promise.allSettled(slice.map((e) => verify(e)));
out.push(...settled); // each: { status: "fulfilled", value } | { status: "rejected", reason }
}
return out;
}
Rassemblez les entrées rejected, temporisez et ne réessayez que celles-là — les
verdicts fulfilled sont déjà acquis. Ce même schéma à débit maîtrisé vous
maintient sous la limite de débit du forfait sans la moindre dépendance
supplémentaire.
Assembler les couches#
Le moins coûteux d'abord, on s'arrête dès qu'on a une réponse :
export async function validateEmail(email) {
if (!isValidSyntax(email)) return { status: "undeliverable", reason: "invalid_email" };
const domain = email.slice(email.lastIndexOf("@") + 1);
if (!(await hasMailRoute(domain))) return { status: "undeliverable", reason: "invalid_domain" };
const { result } = await verify(email);
return result; // deliverable | risky | undeliverable | unknown, with reason + sub_flags
}
Les deux couches locales ne coûtent rien et attrapent instantanément la plupart des déchets ; la couche API ne s'exécute que sur les adresses qui valent l'aller-retour réseau. Cet ordonnancement est toute l'astuce — voyez comment fonctionne la vérification d'e-mails pour comprendre pourquoi chaque étape se trouve là où elle est.
Foire aux questions#
Une bibliothèque comme validator.js ou email-validator suffit-elle ?#
Ces bibliothèques traitent bien la couche 1 — la syntaxe — et remplacent
avantageusement une regex bricolée à la main. Mais ce sont des contrôleurs de
syntaxe : elles ne résolvent pas les enregistrements MX et ne confirment pas
l'existence d'une boîte aux lettres. Un validator.isEmail() qui passe signifie
donc toujours « on dirait un e-mail », pas « le message sera délivré ». Associez-les
aux couches DNS et SMTP ci-dessus.
Puis-je vérifier depuis Node l'existence d'un e-mail sans API ?#
En partie. node:dns/promises confirme que le domaine accepte le courrier, ce qui
écarte gratuitement les domaines morts. Confirmer la boîte aux lettres suppose un
test SMTP, que vous pouvez tenter avec le module net mais que vous ne devriez pas
exécuter depuis le serveur de votre application — 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 pour prendre en charge.
Faut-il valider les e-mails de façon synchrone à l'inscription ?#
Exécutez les couches instantanées de façon synchrone — la syntaxe et le MX sont assez rapides pour bloquer la requête et donner à l'utilisateur un retour immédiat. Traitez le résultat SMTP comme la réponse plus lente : agissez sur le verdict local immédiat à l'inscription et servez-vous du résultat complet confirmé par SMTP pour le nettoyage de listes. Le guide d'inscription serverless montre le schéma de bout en bout.
Faut-il mettre en cache les résultats de vérification pour éviter de recontrôler la même adresse ?#
Oui. Chaque appel de vérification est un aller-retour réseau et un crédit consommé :
ne le répétez pas pour une adresse que vous venez de contrôler. Indexez un cache à
courte durée de vie sur l'e-mail normalisé — en minuscules et sans espaces
superflus au préalable — et réutilisez le verdict pendant quelques minutes. Une
réserve : ne mettez pas en cache les résultats unknown ou encore en pending,
car ce sont justement ceux qu'il vaut la peine de réessayer une fois le test SMTP
terminé. Une simple Map suffit pour un unique processus Node ; ne recourez à Redis
que lorsque vous exécutez plusieurs instances qui doivent partager le cache.
Prêt à ajouter la couche SMTP ? Le forfait gratuit inclut 100 crédits
qui exécutent le pipeline complet, et la référence de l'API détaille
l'ensemble des endpoints /verify et /jobs avec des exemples prêts à copier-coller
dans sept langages.