Skip to content
Inizia con 100 crediti di verifica gratuiti
Qualisend
Tutti gli articoli
Ingegneria / 6 giugno 2026

Come validare un indirizzo email in Node.js

5 min di lettura

Qualisend team
Come validare un indirizzo email in Node.js

Validare un indirizzo email in Node.js significa fare tre lavori sotto un unico nome. La maggior parte dei tutorial ti mostra una regex e si ferma lì: controlla l'ortografia e la chiama validazione. La vera validazione è a livelli: un controllo della sintassi economico, una ricerca DNS e un test SMTP della casella, e Node ti dà i primi due già pronti. Questa guida costruisce ogni livello con codice funzionante e mostra dove subentra un'API di verifica.

In breve#

Usa una regex permissiva per la sintassi, node:dns/promises per la ricerca MX e un'API di verifica per il controllo SMTP della casella: in quest'ordine, prima il più economico, fermandoti appena uno dà una risposta definitiva. Non provare ad aprire connessioni SMTP dal server della tua app: la porta 25 è bloccata dalla maggior parte degli host e, anche dove non lo è, il controllo dipende dalla reputazione dell'IP di invio e da una gestione del greylisting che non vuoi costruire.

Livello 1: sintassi#

La regex serve qui e da nessun'altra parte. Tienila permissiva: l'obiettivo è intercettare gli errori di battitura al momento dell'inserimento, non reimplementare la RFC 5322 (che comunque non aiuta):

const SYNTAX = /^[^\s@."]+(?:\.[^\s@."]+)*@[^\s@.]+(?:\.[^\s@.]+)+$/;

export function isValidSyntax(email) {
  return typeof email === "string" && email.length <= 320 && SYNTAX.test(email);
}

Superare questo controllo significa "vale la pena controllarlo", non "valido". Ogni livello successivo dà per scontato che la sintassi sia già corretta.

Livello 2: il dominio può ricevere posta?#

Qui Node dimostra quanto vale. Il modulo integrato node:dns/promises risolve i record MX senza dipendenze, e un dominio senza instradamento di posta non può accettare posta per nessuno, quindi questa sola ricerca elimina domini morti, nomi di aziende scritti male e TLD inventati:

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

Alcuni domini accettano posta su un record A senza MX (MX implicito). Se vuoi gestire questo caso limite, ripiega su dns.resolve4 quando resolveMx è vuoto, ma per la stragrande maggioranza degli indirizzi reali il controllo MX è il filtro giusto.

Livello 3: la casella esiste davvero?#

I livelli 1 e 2 possono solo escludere un indirizzo. Confermare una casella significa passare per la conversazione SMTP di recapito: RCPT TO, leggere la risposta, disconnettersi prima di inviare qualsiasi cosa. In teoria puoi farlo con il modulo net. In pratica non dovresti: la maggior parte dei provider cloud blocca la porta 25 in uscita, la risposta dipende dalla reputazione dell'IP da cui ti connetti e i server applicano il greylisting e limitano la frequenza dei mittenti sconosciuti. Questo è il livello che conviene delegare.

POST /verify di Qualisend esegue l'intera pipeline e restituisce un esito. I risultati dei controlli locali arrivano subito, con il test SMTP in coda:

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_mail": false, "catch_all": false },
    "did_you_mean": null,
    "smtp": "pending",
    "reason": null
  }
}

Per la validazione in tempo reale all'iscrizione, di solito basta il result immediato per decidere: rifiuta undeliverable, proponi la correzione did_you_mean, segnala disposable. Quando result.smtp è "pending" e ti serve l'esito confermato via SMTP, interroga il job da un'attività in background, non dentro il request handler, finché il test non termina:

export async function verifyAndWait(email, { tries = 10, delayMs = 1500 } = {}) {
  const { job_id, result } = await verify(email);
  if (result.smtp !== "pending") return result; // cached or skipped: already final
  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, ... }
    if (job.status === "failed" || job.status === "canceled") return null;
    await new Promise((r) => setTimeout(r, delayMs));
  }
  return { ...result, job_id }; // probe still running: poll job_id again later, don't call /verify again
}

Due cose mettono in difficoltà il fetch ingenuo qui sopra quando arriva il traffico reale. Primo, un upstream lento non deve bloccare il tuo processo Node: racchiudi la richiesta in un timeout con AbortController, così fallisce subito invece di bloccare l'event loop. Secondo, un 429 (rate limit) o un 5xx è temporaneo e vale la pena ritentare, mentre un 4xx come 400 significa che il payload è sbagliato e ritentare non serve, quindi separa i due casi:

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);
  }
}

In un modulo di iscrizione, lascia passare l'utente in caso di problemi: se la chiamata va in timeout o restituisce 402, 429 o un 5xx, fallo procedere e verifica l'indirizzo in un secondo momento.

Tieni la chiave API in process.env, mai nel bundle: questa chiamata va fatta sul lato server della tua app Node, non in qualcosa che distribuisci al browser.

Per la pulizia delle liste verifichi migliaia di indirizzi, non uno alla volta. Non usare un ciclo con await (seriale e lento) e non lanciarli tutti insieme (faresti scattare il rate limit). Invia un numero limitato di richieste simultanee con Promise.allSettled, così un indirizzo rifiutato non affonda l'intero batch:

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;
}

Raccogli le voci rejected, aspetta un po' e ritenta solo quelle: gli esiti fulfilled sono già pronti. Il rate limit è per workspace al minuto ed è condiviso da tutte le sue chiavi (60 richieste su Free, 120 su Pro, 300 su Enterprise), quindi una lista lunga finirà comunque per ricevere dei 429 con questo sistema. Per un'intera lista, invia invece un unico job POST /verify/bulk con { text, expected_count } e scorri gli esiti con GET /jobs/{id}?include=results.

Mettere insieme i livelli#

Prima il più economico, e fermati appena hai una risposta:

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
}

I due livelli locali non costano nulla e intercettano subito la maggior parte degli indirizzi senza valore; il livello API entra in gioco solo per quelli che meritano il round-trip di rete. Tutto il trucco sta in quest'ordine: leggi come funziona la verifica email per capire perché ogni fase si trova dove si trova.

Domande frequenti

01Mi basta una libreria come validator.js o email-validator?

Queste librerie gestiscono bene il livello 1, la sintassi, e sostituiscono degnamente una regex scritta a mano. Ma sono controlli di sintassi: non risolvono i record MX e non confermano una casella, quindi superare validator.isEmail() significa comunque "sembra un'email", non "verrà recapitata". Abbinale ai livelli DNS e SMTP descritti sopra.

02Posso controllare se un'email esiste da Node senza un'API?

In parte. node:dns/promises conferma che il dominio accetta posta, e così escludi gratis i domini morti. Confermare la casella richiede un test SMTP, che puoi tentare con il modulo net ma non dovresti eseguire dal server della tua app: la porta 25 è ampiamente bloccata e il risultato dipende dalla reputazione del tuo IP. Un servizio di verifica esiste proprio per gestire questo livello.

03Devo validare le email in modo sincrono all'iscrizione?

Esegui in modo sincrono i livelli immediati: sintassi e MX sono abbastanza veloci da far attendere la richiesta e dare all'utente un feedback istantaneo. Considera il risultato SMTP come la risposta più lenta: all'iscrizione agisci in base all'esito locale immediato e usa il risultato completo confermato via SMTP per la pulizia delle liste. La guida alle iscrizioni serverless mostra il modello dall'inizio alla fine.

04Devo mettere in cache i risultati di verifica per non ricontrollare lo stesso indirizzo?

Sì. Ogni chiamata di verifica è un round-trip di rete e un credito speso, quindi non ripeterla per un indirizzo appena controllato. Usa come chiave di una cache a breve durata l'email normalizzata (prima in minuscolo e senza spazi) e riutilizza l'esito per qualche minuto. Un'avvertenza: non mettere in cache i risultati unknown o ancora pending. Un esito pending non è ancora definitivo: leggi quello finale da GET /jobs/{job_id} quando il test SMTP termina, invece di richiamare la verifica, e ritenta più tardi un unknown. Per un singolo processo Node basta una semplice Map; passa a Redis solo quando esegui più istanze che devono condividere la cache.


Vuoi aggiungere il livello SMTP? Il piano Free include 100 crediti che eseguono l'intera pipeline, e il riferimento API contiene gli endpoint /verify e /jobs completi, con esempi pronti da copiare in sette linguaggi.

La tua reputazione, protetta.

Pulisci la tua prima lista in pochi minuti. 100 crediti gratuiti, senza carta di credito.

Inizia