Validare un indirizzo email in Ruby significa fare tre lavori nascosti dietro il nome di un solo metodo. La maggior parte dei tutorial ti passa una regex, la vede funzionare e dichiara valido l'indirizzo, ma una corrispondenza con un pattern ti dice solo che la stringa ha la forma giusta, non che la posta ci arriverà mai. La validazione vera è a livelli: un controllo economico della sintassi, una ricerca DNS dei server di posta del dominio e un controllo SMTP della casella effettiva. La libreria standard di Ruby ti dà i primi due senza nessuna gem; il terzo è un problema di rete che conviene delegare. Questa guida costruisce ogni livello con codice funzionante e mostra esattamente dove si ferma la libreria standard.
In breve#
Usa URI::MailTo::EMAIL_REGEXP della libreria uri per la sintassi, Resolv::DNS della libreria resolv per la ricerca MX e un'API di verifica per il controllo SMTP della casella: prima il più economico, interrompendo appena un livello è decisivo. Non ricorrere a net/smtp per sondare le caselle dalla tua app: la porta 25 in uscita è bloccata sulla maggior parte degli host e la risposta dipende dalla reputazione del tuo IP di invio e da comportamenti di greylisting che non vuoi reimplementare. Ogni livello esclude indirizzi a un costo minore del successivo; solo l'ultimo può confermarne uno. Quest'ultimo passaggio è proprio il vuoto che la verifica email nasce per colmare.
Livello 1: sintassi#
Il posto della regex è qui e da nessun'altra parte, e in Ruby non devi nemmeno scriverla. La libreria uri, che fa parte della libreria standard, include un pattern ben collaudato in URI::MailTo::EMAIL_REGEXP, quindi usa quello invece di incollare qualcosa da Stack Overflow:
require "uri"
def valid_syntax?(email)
email.is_a?(String) &&
email.length <= 320 &&
email.match?(URI::MailTo::EMAIL_REGEXP)
end
valid_syntax?("jane@example.com") # => true
valid_syntax?("not-an-email") # => false
valid_syntax?("a@@b.com") # => false
Su questa costante vale la pena sapere due cose. È già ancorata con \A e \z, quindi match? controlla l'intera stringa e non una sottostringa: non devi racchiuderla né ancorarla tu. Ed è volutamente permissiva: segue la definizione WHATWG/HTML5 di email valida, meno rigida della RFC 5322, e per esempio accetta senza problemi jane@localhost perché non richiede un punto nel dominio. È una scelta, non un bug. Il compito del primo livello è intercettare gli errori di battitura in fase di inserimento, non discutere delle RFC, cosa che comunque non servirebbe. Il limite di 320 caratteri è una precauzione in più: niente di più lungo può essere un indirizzo reale, e costa meno scartarlo subito che passare a valle una stringa enorme. Superare questo controllo significa "vale la pena verificarlo", mai "valido".
Livello 2: il dominio può ricevere posta?#
Qui la libreria standard di Ruby si guadagna il suo posto. Un dominio senza instradamento della posta non può accettare email per nessuno, quindi una sola ricerca MX elimina domini morti, nomi di aziende scritti male e TLD inventati. La libreria resolv risolve i record senza nessuna gem:
require "resolv"
def mail_route?(domain)
records = Resolv::DNS.open do |dns|
dns.getresources(domain, Resolv::DNS::Resource::IN::MX)
end
!records.empty?
rescue Resolv::ResolvError, Resolv::ResolvTimeout
false
end
mail_route?("gmail.com") # => true
mail_route?("company-that-folded.com") # => false
Resolv::DNS.open ti dà un resolver e lo chiude all'uscita dal blocco, mentre getresources restituisce un array di record MX: un array vuoto se il dominio non ne pubblica o non esiste, ed è esattamente il caso "nessun instradamento" che vuoi intercettare. Il rescue gestisce l'altra modalità di errore: un resolver che va in errore o in timeout. Nota che Resolv::ResolvTimeout non è una sottoclasse di Resolv::ResolvError, quindi indicale entrambe se vuoi che un nameserver lento porti a un esito pulito invece di far saltare la chiamata.
Quando ti servono i veri host di posta invece di un sì o un no, per registrarli nei log o controllare le priorità, ogni record espone exchange e preference, quindi ordina in base a quest'ultimo:
def mail_hosts(domain)
records = Resolv::DNS.open do |dns|
dns.getresources(domain, Resolv::DNS::Resource::IN::MX)
end
records.sort_by(&:preference).map { |mx| mx.exchange.to_s }
# => ["gmail-smtp-in.l.google.com", "alt1.gmail-smtp-in.l.google.com", ...]
rescue Resolv::ResolvError, Resolv::ResolvTimeout
[]
end
Alcuni domini accettano posta su un record A con un MX implicito; se devi gestire questo caso limite, ripiega su getresources(domain, Resolv::DNS::Resource::IN::A) quando l'insieme MX torna vuoto. Per la stragrande maggioranza degli indirizzi reali, un controllo MX è il filtro giusto.
Livello 3: la casella esiste davvero?#
I livelli uno e due possono solo escludere un indirizzo. Un dominio può pubblicare record MX perfetti e non avere comunque nessuna casella all'indirizzo che hai in mano: noreply-9f2x@gmail.com ha una sintassi valida su un instradamento attivo ed è comunque una casella mai creata. Confermare che esista una casella specifica richiede la conversazione SMTP di consegna: connettersi all'host di posta, inviare RCPT TO, leggere la risposta e disconnettersi prima di spedire qualsiasi cosa. C'è dell'altro: come funziona la verifica email ripercorre l'intero processo, compresi i domini catch-all che accettano qualsiasi indirizzo e vanificano un controllo ingenuo.
La libreria standard di Ruby aprirà volentieri quella conversazione per te con net/smtp. Il problema non è il codice, è la rete. Se lanci un controllo SMTP dal tuo server applicativo vanno storte tre cose: la maggior parte dei provider cloud blocca del tutto la porta 25 in uscita, la risposta dipende dalla reputazione dell'IP da cui ti connetti e i server riceventi applicano greylisting e limiti di frequenza ai mittenti sconosciuti. Così un controllo che funziona sul tuo portatile, in produzione fallisce senza dare nell'occhio o ti fa finire in blacklist. Questo è il livello che conviene delegare.
Il controllo completo con un'API#
L'endpoint di verifica di Qualisend esegue l'intero processo (sintassi, DNS e controllo SMTP della casella) da un'infrastruttura con reputazione gestita, costruita apposta. Risponde subito con un esito basato sui controlli rapidi, più un job id per l'esito finale della casella mentre il controllo è ancora in corso. Trattandosi di una semplice POST JSON, basta net/http della libreria standard, senza nessuna gem. Leggi la chiave dall'ambiente invece di scriverla nel codice:
require "net/http"
require "json"
require "uri"
VERIFY_URL = URI("https://app.qualisend.com/api/v1/verify")
# A timeout, 402 (out of credits), 429 (rate limited) or 5xx: no verdict right now.
class VerifyUnavailable < StandardError; end
def verify(email)
request = Net::HTTP::Post.new(VERIFY_URL)
request["Authorization"] = "Bearer #{ENV.fetch('QUALISEND_API_KEY')}"
request["Content-Type"] = "application/json"
request.body = JSON.generate(email: email)
response = Net::HTTP.start(VERIFY_URL.hostname, VERIFY_URL.port, use_ssl: true,
open_timeout: 5, read_timeout: 10) do |http|
http.request(request)
end
if %w[402 429].include?(response.code) || response.is_a?(Net::HTTPServerError)
raise VerifyUnavailable, "Qualisend responded #{response.code}"
end
raise "Qualisend responded #{response.code}" unless response.is_a?(Net::HTTPSuccess)
JSON.parse(response.body, symbolize_names: true)
rescue Net::OpenTimeout, Net::ReadTimeout => e
raise VerifyUnavailable, e.message
end
Il riferimento API spiega la forma della richiesta e come creare una chiave con scope limitati. La risposta contiene un job_id e racchiude un oggetto result:
{
"job_id": "8d1f0c2e-5b7a-4c3e-9f61-2a4b6c8d0e13",
"result": {
"status": "unknown",
"score": 61,
"smtp": "pending",
"reason": null,
"sub_flags": { "role": false, "disposable": false, "catch_all": false }
}
}
Entra nella struttura della risposta e agisci in base a status:
body = verify("jane@example.com")
result = body[:result]
result[:status] # "deliverable" | "risky" | "undeliverable" | "unknown"
result[:score] # confidence, 0–100
result[:reason] # machine-readable reason, or nil
result[:sub_flags] # { role:, disposable:, catch_all:, ... }
result[:smtp] # "pending" while the mailbox probe runs, else "cached" | "skipped"
body[:job_id] # where the final verdict lands when smtp is "pending"
Quando result[:smtp] è "cached" o "skipped", result è già definitivo. Quando è "pending", il controllo della casella è ancora in corso e l'esito immediato viene dai controlli rapidi; quello finale arriva su GET https://app.qualisend.com/api/v1/jobs/{job_id}. Non richiamare /verify per aggiornarlo. Salva il job_id e lascia che un job in background interroghi quell'endpoint ogni pochi secondi finché il job non termina.
net/http ti evita dipendenze, cosa comoda in un job in background o in un piccolo servizio. Se la tua app usa già Faraday o HTTParty, la stessa richiesta è più corta di un paio di righe, e la struttura che rileggi è identica.
Un modo completo per validare un indirizzo email in Ruby#
Concatena i tre livelli partendo dal più economico e fermati appena uno è decisivo:
def validate_email(email)
return { status: "undeliverable", reason: "invalid_email" } unless valid_syntax?(email)
domain = email.rpartition("@").last
return { status: "undeliverable", reason: "invalid_domain" } unless mail_route?(domain)
verify(email)[:result] # deliverable | risky | undeliverable | unknown
rescue VerifyUnavailable
{ status: "unknown", reason: "unknown" } # fail open: let it through, re-verify later
end
rpartition("@") divide sull'ultima @, quindi prendi sempre il dominio reale anche con parti locali virgolettate in modo insolito. I due livelli locali non costano nulla e intercettano all'istante la maggior parte degli indirizzi spazzatura; l'API entra in gioco solo per gli indirizzi che meritano una chiamata di rete. È questo ordine, non il linguaggio, a rendere affidabile la validazione, ed è la stessa struttura che trovi nelle versioni Node.js e Python di questa guida.
Chi usa Rails può inserire il tutto in un modello come validazione personalizzata, così un indirizzo errato non raggiunge mai il database:
class User < ApplicationRecord
validate :email_must_be_deliverable
private
def email_must_be_deliverable
return if email.blank?
verdict = validate_email(email)
errors.add(:email, "doesn't look deliverable") if verdict[:status] == "undeliverable"
end
end
Un controllo all'iscrizione deve fallire in modalità aperta (fail open): validate_email trasforma un timeout, un 402, un 429 o un 5xx in unknown, così un'interruzione dell'API, un limite di frequenza o un saldo crediti esaurito non bloccano mai un cliente reale. Verifica di nuovo quegli indirizzi più tardi.
Limita il percorso sincrono ai due livelli locali rapidi più l'esito immediato dell'API; tutto ciò che è più lento va in un job in background. Il nucleo però è puro Ruby e si comporta allo stesso modo in una route Sinatra, in un task rake o in uno script una tantum: è la stratificazione a fare il lavoro.
Domande frequenti
01Posso validare un indirizzo email in Ruby senza gem?
Per due livelli su tre, sì. URI::MailTo::EMAIL_REGEXP della libreria uri gestisce la sintassi e Resolv::DNS della libreria resolv risolve i record MX: entrambe sono incluse in Ruby, quindi per un controllo di sintassi e dominio non devi installare nulla. Il terzo livello, la conferma della casella, richiede un controllo SMTP che non dovresti lanciare dal tuo server. È la parte di cui si occupa per te un servizio di verifica.
02URI::MailTo::EMAIL_REGEXP basta da solo?
È il controllo giusto per il primo livello e una scelta molto migliore di una regex scritta a mano, ma valida la forma, non la deliverability. Non risolve il DNS, non contatta alcun server di posta ed è permissivo per scelta (accetta jane@localhost), quindi superarlo significa "sembra un'email", non "verrà recapitata". Abbinalo a una ricerca MX e a un controllo della casella prima di fidarti dell'indirizzo.
03Perché non usare net/smtp per controllare se una casella esiste?
net/smtp può aprire la conversazione SMTP, ma lanciarlo dal tuo server applicativo è inaffidabile e rischioso: la maggior parte degli host blocca la porta 25 in uscita, la risposta dipende dalla reputazione del tuo IP di invio e sondare a qualsiasi volume ti fa finire sotto limiti di frequenza o in blacklist. Un'API di verifica esegue il controllo da un'infrastruttura con reputazione gestita, costruita proprio per questo.
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 e lento alla pulizia delle liste e ai processi di back-office; la guida all'iscrizione serverless mostra il pattern in tempo reale dall'inizio alla fine.
Vuoi aggiungere il livello della casella? Il riferimento API contiene l'endpoint /verify, le chiavi con scope limitati e snippet pronti da copiare, oppure incolla un indirizzo nella verifica email gratuita e guarda una stringa sintatticamente perfetta tornare come non recapitabile.



