Validare un indirizzo email in Go significa fare tre lavori sotto un solo nome. I
tutorial che ti mostrano una regex e si fermano lì ne hanno risolto esattamente uno, e
per giunta il meno utile. La vera validazione è a livelli: un controllo economico della
sintassi, una ricerca DNS e un test SMTP della casella, partendo dal più economico. La
libreria standard di Go copre i primi due in modo pulito con net/mail e net, senza
alcuna dipendenza; il terzo è un problema di rete che conviene delegare. Questa guida
costruisce ogni livello con codice Go funzionante e idiomatico e mostra esattamente dove
subentra un'API di verifica.
In breve#
Usa net/mail.ParseAddress per la sintassi, net.LookupMX per la ricerca MX e un'API di
verifica per il controllo SMTP della casella, in quest'ordine: si parte dal più economico
e ci si ferma non appena un livello è risolutivo. Non ricorrere a net/smtp per testare
le caselle dal tuo server: la porta 25 in uscita è bloccata dalla maggior parte degli
host e, anche dove non lo è, il risultato dipende dalla reputazione dell'IP di invio e
dal comportamento del greylisting, cose che non vuoi reimplementare. Ogni livello esclude indirizzi a un costo minore del successivo; solo l'ultimo può confermarne uno.
Livello 1: sintassi con net/mail#
In Go non serve una regex scritta a mano. Il pacchetto net/mail della libreria standard
analizza gli indirizzi RFC 5322 e ParseAddress restituisce un errore non nil su qualsiasi
input malformato, ed è proprio il segnale sì/no che serve a un filtro di sintassi:
package main
import "net/mail"
func isValidSyntax(email string) bool {
if len(email) > 320 {
return false
}
addr, err := mail.ParseAddress(email)
return err == nil && addr.Address == email
}
Il confronto addr.Address == email è la parte che manca alla maggior parte degli esempi.
ParseAddress è fatto per leggere una riga di indirizzo completa, quindi accetta senza problemi
un nome visualizzato: mail.ParseAddress("Jane Doe <jane@example.com>") non restituisce
errori e imposta addr.Address su jane@example.com. Per un campo di iscrizione vuoi
solo l'indirizzo e nient'altro, quindi confronta l'Address analizzato con l'input e
scarta tutto ciò che contiene un nome, parentesi angolari o spazi finali:
isValidSyntax("jane@example.com") // true
isValidSyntax("Jane Doe <jane@example.com>") // false — not a bare address
isValidSyntax("not-an-email") // false — ParseAddress errors
isValidSyntax("a@@b.com") // false
Un esito positivo qui significa "merita un controllo", non "valido". Ti dice che la stringa
ha la forma di un indirizzo, ma nulla sul fatto che la posta arrivi. È lo stesso muro
contro cui sbatte ogni controllo email basato su
regex, e net/mail sta dalla stessa parte: è un
parser, non un oracolo della deliverability. Ogni livello successivo dà per scontato che la
sintassi sia già a posto.
Livello 2: il dominio può ricevere posta?#
Qui la libreria standard dà il meglio di sé, senza alcuna dipendenza. Un dominio che non
pubblica record MX non ha dove far arrivare la posta, quindi una sola ricerca elimina domini
morti, nomi di aziende scritti male e TLD inventati. net.LookupMX risolve i record MX:
package main
import "net"
func hasMailRoute(domain string) bool {
mxs, err := net.LookupMX(domain)
if err != nil {
return false
}
return len(mxs) > 0
}
hasMailRoute("gmail.com") // true
hasMailRoute("company-that-folded.com") // false
LookupMX restituisce una slice di *net.MX ordinata per preferenza, oppure un
*net.DNSError quando il dominio non si risolve. Per un validatore, la lettura sicura è
trattare qualsiasi errore (o una slice vuota) come "nessun instradamento della posta". Se vuoi
gestire i domini che accettano posta su un record A senza un MX esplicito (il cosiddetto MX
implicito), ripiega su net.LookupHost(domain) quando la slice degli MX torna vuota. Per la
stragrande maggioranza degli indirizzi reali, però, un controllo MX è il filtro giusto.
Una nota operativa: LookupMX resta in attesa del tuo resolver DNS, quindi un nameserver
lento o irraggiungibile blocca la goroutine. In un percorso di richiesta usa il resolver che
accetta un context, (&net.Resolver{}).LookupMX(ctx, domain), con una scadenza, e tratta un
timeout come "ricontrolla più tardi", non come un rifiuto definitivo, così un problema DNS
passeggero non manda mai via un cliente reale.
Livello 3: la casella esiste davvero?#
I livelli 1 e 2 possono solo escludere un indirizzo. Un dominio può pubblicare record MX
perfetti e non avere comunque alcuna casella all'indirizzo che hai in mano:
noreply-9f2x@gmail.com ha una sintassi valida su un instradamento della posta attivo e non ha mai
corrisposto a una casella reale. Confermare una casella specifica richiede la conversazione di
recapito SMTP: connettersi all'host di posta, inviare RCPT TO, leggere la risposta e
disconnettersi prima di spedire qualsiasi cosa.
Go include gli strumenti per provarci. net/smtp ti dà smtp.Dial e un Client con i
metodi Mail, Rcpt e Close, e in linea di principio potresti gestire tu quello scambio.
In pratica non dovresti eseguirlo dal tuo server applicativo. La maggior parte dei provider
cloud blocca del tutto la porta 25 in uscita, quindi in produzione la connessione va
semplicemente in timeout anche se sul tuo portatile funzionava. Dove la porta è aperta, il
risultato dipende dalla reputazione dell'IP da cui ti connetti e i server riceventi
applicano greylisting e limiti di frequenza ai mittenti sconosciuti, così un test ingenuo
viene rinviato, rallentato o finisce in blacklist. E non basta un solo round-trip: i domini
catch-all accettano ogni indirizzo e vanificano un singolo RCPT TO.
Come funziona la verifica email ripercorre l'intero
processo. Questo è il livello che conviene delegare.
Il controllo completo con un'API#
L'API in tempo reale di Qualisend esegue l'intero processo (sintassi, DNS e test SMTP della
casella) da un'infrastruttura con reputazione gestita costruita apposta e restituisce un
esito. È una semplice POST JSON, quindi ti bastano net/http e encoding/json della
libreria standard. Decodifica l'involucro result direttamente in una struct:
package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"os"
)
type VerifyResult struct {
Status string `json:"status"` // deliverable | risky | undeliverable | unknown
Score int `json:"score"`
Reason string `json:"reason"`
SubFlags map[string]bool `json:"sub_flags"`
SMTP string `json:"smtp"` // pending | cached | skipped
JobID string `json:"-"` // from the top-level job_id
}
type verifyResponse struct {
JobID string `json:"job_id"`
Result VerifyResult `json:"result"`
}
func verify(ctx context.Context, email string) (VerifyResult, error) {
body, err := json.Marshal(map[string]string{"email": email})
if err != nil {
return VerifyResult{}, err
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost,
"https://app.qualisend.com/api/v1/verify", bytes.NewReader(body))
if err != nil {
return VerifyResult{}, err
}
req.Header.Set("Authorization", "Bearer "+os.Getenv("QUALISEND_API_KEY"))
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
return VerifyResult{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return VerifyResult{}, fmt.Errorf("qualisend: unexpected status %s", res.Status)
}
var payload verifyResponse
if err := json.NewDecoder(res.Body).Decode(&payload); err != nil {
return VerifyResult{}, fmt.Errorf("qualisend: decode response: %w", err)
}
payload.Result.JobID = payload.JobID
return payload.Result, nil
}
Leggi la tua chiave con scope limitati dalla variabile d'ambiente QUALISEND_API_KEY invece
di scriverla nel codice e controlla il
riferimento API nella pagina per sviluppatori per la forma esatta di richiesta
e risposta. L'oggetto result contiene uno status tra deliverable, risky,
undeliverable e unknown, uno score numerico, un reason e una mappa sub_flags
(indirizzo generico, usa e getta, provider di posta gratuita, catch-all e così via): sono i campi in
base a cui deciderai davvero.
La risposta non aspetta il test della casella e SMTP ti dice che tipo di risposta hai.
cached o skipped significano che il risultato è già definitivo. pending significa che
status riflette i controlli locali, reason è ancora vuoto e l'esito SMTP arriverà sul job:
GET https://app.qualisend.com/api/v1/jobs/{job_id}?include=results, con l'id in JobID.
Interrogalo a intervalli di pochi secondi da un worker in background, mai dentro la richiesta
di iscrizione, finché il job non è completed, canceled o failed, e non richiamare
/verify per aggiornare lo stesso indirizzo.
Poiché verify accetta un context.Context, timeout e annullamento sono in mano al chiamante.
Passa una scadenza, così una verifica lenta non blocca mai una richiesta di iscrizione:
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
result, err := verify(ctx, "jane@example.com")
if err != nil {
log.Printf("verify failed: %v", err)
}
Mettere tutto insieme per validare un indirizzo email in Go#
Parti dal più economico e fermati appena hai una risposta. I due livelli locali non costano nulla e scartano all'istante la maggior parte degli indirizzi spazzatura; la chiamata API viene eseguita solo sugli indirizzi che li hanno superati e che meritano il round-trip di rete:
package main
import (
"context"
"strings"
)
func validateEmail(ctx context.Context, email string) (VerifyResult, error) {
if !isValidSyntax(email) {
return VerifyResult{Status: "undeliverable", Reason: "invalid_email"}, nil
}
domain := email[strings.LastIndex(email, "@")+1:]
if !hasMailRoute(domain) {
return VerifyResult{Status: "undeliverable", Reason: "invalid_domain"}, nil
}
return verify(ctx, email)
}
Poi distingui i casi in base allo status, come serve al tuo prodotto: rifiuta gli indirizzi non recapitabili, lascia passare quelli recapitabili e decidi funzionalità per funzionalità cosa fare con gli esiti più incerti:
result, err := validateEmail(ctx, "jane@example.com")
if err != nil {
// network/API failure — fail open or retry, don't block a real user
}
switch result.Status {
case "deliverable":
// accept
case "undeliverable":
// reject at the form
default:
// risky | unknown — flag for review, or allow with a soft warning
}
Tutto il trucco sta in quest'ordine, ed è la stessa forma che trovi nelle versioni Node.js e Python di questa guida: è la struttura a livelli, non il linguaggio, a rendere affidabile la validazione. Leggi come funziona la verifica email per capire perché ogni fase sta dove sta.
Domande frequenti
01net/mail.ParseAddress basta per validare un indirizzo email in Go?
Per la sintassi, sì: net/mail.ParseAddress è il controllo giusto per il primo livello e una scelta molto migliore di una regex scritta a mano, perché analizza secondo la RFC 5322 e restituisce un errore sull'input malformato. Ma valida la forma, non la deliverability: non interroga mai il DNS né contatta un server di posta, quindi un errore nil significa "sembra un indirizzo", non "verrà recapitato". Confronta addr.Address con il tuo input per scartare i nomi visualizzati, poi abbina al controllo una ricerca MX e un test SMTP della casella.
02Come cerco i record MX in Go?
Usa net.LookupMX(domain) della libreria standard. Restituisce una slice di record *net.MX ordinata per preferenza, oppure un *net.DNSError quando il dominio non si risolve. Tratta un errore o una slice vuota come "nessun instradamento della posta" e, in un percorso di richiesta, usa (*net.Resolver).LookupMX(ctx, domain), che accetta un context, così un nameserver lento non può bloccare la goroutine oltre la tua scadenza.
03Posso verificare una casella in Go senza un servizio esterno?
In parte. net.LookupMX conferma che il dominio accetta posta, il che esclude gratis i domini morti e non richiede altro che la libreria standard. Confermare la casella vera e propria richiede una conversazione SMTP, che puoi scrivere con net/smtp ma che non dovresti eseguire dal tuo server applicativo: la porta 25 è bloccata quasi ovunque e il risultato dipende dalla reputazione del tuo IP di invio. È il livello per cui esiste un servizio di verifica; puoi vedere la differenza su qualsiasi indirizzo con la verifica email gratuita.
04Devo validare le email all'iscrizione o quando pulisco una lista?
Entrambe le cose, con una profondità diversa. Esegui sintassi e ricerca MX in modo sincrono all'iscrizione (sono abbastanza veloci da bloccare la richiesta e dare un riscontro immediato) e usa lì anche l'esito immediato dell'API. Riserva il lavoro più approfondito, confermato via SMTP, alla pulizia delle liste e ai job di back-office; la guida all'iscrizione serverless mostra lo schema in tempo reale dall'inizio alla fine.
Vuoi aggiungere il livello SMTP al tuo servizio Go? Il
riferimento API di Qualisend contiene l'endpoint /verify, le chiavi con
scope limitati ed esempi pronti da copiare, e la verifica email gratuita
ti permette di veder tornare come undeliverable un indirizzo sintatticamente perfetto prima
ancora di scrivere una riga di codice.



