Quando devi validare un indirizzo email in Rails, Active Record fa sembrare tutto
banale: scrivi validates :email, vedi il test passare e vai avanti. Ma quella
riga risponde solo alla prima delle tre domande che pone una vera validazione.
L'indirizzo ha la forma corretta? Il suo dominio può ricevere posta? La casella
esiste davvero? Rails dà una risposta pulita alla prima e nessuna alle altre
due. Questa guida costruisce ogni livello come una validazione Rails idiomatica
(una regola format, poi due classi personalizzate ActiveModel::EachValidator)
e mostra esattamente dove si ferma il framework. È un'estensione della
guida alla validazione delle email in Ruby:
Rails avvolge gli stessi componenti della libreria standard nell'API dichiarativa
delle validazioni.
In breve#
Usa validates :email, format: { with: URI::MailTo::EMAIL_REGEXP } per la sintassi,
un validatore personalizzato basato sulla libreria standard resolv per la ricerca
MX e un'API di verifica per il controllo SMTP della casella: tre validazioni su un
solo attributo, partendo dalla più economica, ognuna delle quali viene saltata se un livello
più economico ha già fallito. Non aprire una sessione SMTP dalla tua app per testare direttamente le caselle: la porta 25 in uscita è bloccata dalla maggior parte degli host e la risposta dipende dalla reputazione dell'IP di invio e dal greylisting, cose che non
vuoi reimplementare. Un dettaglio distingue Rails dai framework con una parola chiave
bail: Active Model esegue tutti i validatori che dichiari e raccoglie tutti gli
errori, quindi l'interruzione anticipata è una condizione di guardia che aggiungi tu,
non un flag da passare. Ogni livello esclude indirizzi a un costo minore del successivo; solo l'ultimo può confermarne uno.
Livello 1: validazione del formato con una regola validates#
Il primo livello è territorio di Rails e per coprirlo non devi scrivere una regex.
La libreria uri di Ruby, che Rails carica già, offre un pattern ben collaudato in
URI::MailTo::EMAIL_REGEXP, quindi passalo a una validazione format invece di incollare
qualcosa trovato su Stack Overflow:
app/models/user.rb
class User < ApplicationRecord
validates :email,
presence: true,
format: { with: URI::MailTo::EMAIL_REGEXP }
end
È la stessa costante su cui si appoggia la guida per Ruby.
È già ancorata con \A e \z, quindi controlla l'intera stringa, ed è volutamente
permissiva: segue la definizione WHATWG/HTML5, meno rigida della RFC 5322, e accetta
senza problemi jane@localhost perché non richiede un punto nel dominio. È una scelta
voluta. Il compito del primo livello è intercettare gli errori di battitura in fase
di inserimento, non discutere sulle RFC (cosa che
comunque non servirebbe).
L'importante è sapere dove si ferma Rails. Tutti gli strumenti integrati per le email
(la regola format, gli scaffold, l'helper email_field) validano la forma. Nessuno
di questi interroga il DNS o contatta un server di posta, quindi noreply-9f2x@gmail.com,
typo@gmial.com e sales@company-that-folded.com passano tutti. Tutti e tre sono non recapitabili. Rails non offre alcun controllo di deliverability.
Livello 2: il dominio può ricevere posta?#
È il primo livello che Rails non ti dà, ed è economico da aggiungere. 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. Un
validatore Rails è semplicemente una classe che estende ActiveModel::EachValidator
e implementa validate_each, quindi basta metterci dentro la ricerca MX della libreria
resolv:
app/validators/deliverable_domain_validator.rb
require "resolv"
class DeliverableDomainValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
return if value.blank? || record.errors[attribute].present?
domain = value.rpartition("@").last
return if domain.present? && mail_route?(domain)
record.errors.add(attribute, options[:message] || "can't receive email")
end
private
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
end
Resolv::DNS.open ti passa un resolver e lo chiude all'uscita dal blocco, mentre
getresources restituisce un array di record MX: vuoto quando il dominio non ne
pubblica o non esiste, che è proprio il caso "nessun percorso". Il rescue copre
l'altro tipo di errore; tieni presente che Resolv::ResolvTimeout non è una
sottoclasse di Resolv::ResolvError, quindi indicale entrambe se vuoi che un
nameserver lento finisca nello stesso ramo senza intoppi. Aggiungi il validatore al modello con
la sua chiave dedotta: Rails trasforma deliverable_domain in
DeliverableDomainValidator e trova la classe che Zeitwerk ha caricato
automaticamente da app/validators:
validates :email,
presence: true,
format: { with: URI::MailTo::EMAIL_REGEXP },
deliverable_domain: true
Tutto il trucco sta nella guardia return if ... record.errors[attribute].present?.
Active Model esegue ogni validatore dichiarato e accumula gli errori: non esiste
bail. Le validazioni però girano nell'ordine in cui le dichiari, quindi quando
parte questa, un errore di format si trova già in record.errors[:email] e la
guardia salta del tutto la ricerca DNS. Un indirizzo malformato non consuma mai un
round-trip verso il nameserver.
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 alcuna casella all'indirizzo che hai in mano:
noreply-9f2x@gmail.com ha una sintassi valida e un instradamento della posta attivo, ma è
una casella mai creata. 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. C'è dell'altro:
come funziona la verifica email ripercorre
l'intero processo, compresi i domini catch-all che accettano ogni indirizzo e
vanificano un test ingenuo.
net/smtp di Ruby apre quella conversazione senza problemi. Il problema non è il
codice, ma la rete. Se esegui un test SMTP dal tuo server applicativo vanno storte
tre cose: la maggior parte dei provider cloud blocca del tutto la porta 25 in uscita,
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 che funziona sul tuo portatile, in produzione fallisce in silenzio o ti fa
finire in blacklist. Questo è il livello che conviene delegare.
Chiamare Qualisend da un validatore personalizzato#
La delega resta pulita perché un'API di verifica è solo un altro validatore.
L'endpoint di verifica di Qualisend esegue l'intero processo (sintassi, DNS e test
SMTP della casella) da un'infrastruttura con reputazione gestita. Risponde subito con
un esito basato sui controlli rapidi, più un job id per l'esito finale sulla casella
mentre il test è ancora in corso. È una semplice POST JSON, quindi basta net/http
della libreria standard, senza gem. Leggi la chiave dall'ambiente invece di scriverla
nel codice:
app/validators/deliverable_email_validator.rb
require "net/http"
require "json"
require "uri"
class DeliverableEmailValidator < ActiveModel::EachValidator
VERIFY_URL = URI("https://app.qualisend.com/api/v1/verify")
def validate_each(record, attribute, value)
return if value.blank? || record.errors[attribute].present?
result = verify(value)
return if result.nil? # our outage shouldn't block a real signup
if result["status"] == "undeliverable"
record.errors.add(attribute, options[:message] || "appears to be undeliverable")
end
end
private
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
) { |http| http.request(request) }
return nil unless response.is_a?(Net::HTTPSuccess)
JSON.parse(response.body)["result"]
rescue StandardError
nil # unreachable or slow — treat as unknown and re-verify later
end
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 }
}
}
status è uno tra deliverable, risky, undeliverable e unknown. Il validatore
qui sopra fa fallire solo undeliverable e lascia passare risky e unknown, così
puoi decidere a valle cosa farne (filtrare in base a score o distinguere i casi in base a una voce di
sub_flags come disposable) invece di trasformare un indirizzo dubbio in un errore
del modulo. E quando è la richiesta stessa a fallire, verify restituisce nil e il
validatore non aggiunge alcun errore: i livelli di sintassi e MX sono già stati
eseguiti in locale, quindi ammorbidire solo il livello di rete evita che un'interruzione
passeggera blocchi un'iscrizione reale. Considera il JSON qui sopra come la forma, non come il contratto; l'elenco completo dei campi è nella
documentazione per sviluppatori.
Il campo smtp ti dice quanto è definitivo quell'esito. Se è cached o skipped,
result è già la risposta finale. Se è pending, il test della casella è ancora in
corso: l'esito immediato viene dai controlli rapidi e quello finale arriva su
GET https://app.qualisend.com/api/v1/jobs/{job_id}?include=results. Non aspettarlo
dentro il validatore e non richiamare /verify per aggiornarlo. Salva il job_id e
lascia che un job in background interroghi quell'endpoint a intervalli di pochi secondi
finché lo status del job non è completed, poi agisci in base allo status
dell'indirizzo in results.
Mettere insieme i livelli per validare un indirizzo email in Rails#
Dichiara tutte e tre le validazioni sull'attributo, partendo dalla più economica. L'ordine di dichiarazione è l'ordine di esecuzione e ogni guardia salta il controllo se un livello più economico ha già fallito, quindi la regola composta si legge dall'alto verso il basso esattamente come viene eseguita:
app/models/user.rb
class User < ApplicationRecord
validates :email,
presence: true,
format: { with: URI::MailTo::EMAIL_REGEXP },
deliverable_domain: true,
deliverable_email: true
end
Ora user.save arriva all'API solo con indirizzi che hanno già superato i controlli di sintassi e
MX: un errore di battitura si ferma a format e non consuma mai un
round-trip verso il nameserver né un credito API, e un dominio morto si ferma a
deliverable_domain prima della chiamata di rete. user.valid? esegue l'intera
catena e user.errors[:email] riporta il livello che ha sollevato l'obiezione. È
quest'ordine, non il framework, a rendere affidabile la validazione: la stessa forma
della versione Ruby, espressa con validazioni
dichiarative invece che con un metodo concatenato a mano.
Poiché sono normali validazioni, funzionano tutte le opzioni standard. Limita i due
livelli di rete con if: :email_changed?, così un user.update(name: ...) che non
c'entra nulla non ripete una ricerca DNS e non spende di nuovo un credito su un
indirizzo già verificato. All'iscrizione, limita il percorso sincrono ai livelli locali
rapidi più l'esito immediato dell'API; tutto ciò che è più pesante va in un job in
background. La guida all'iscrizione serverless
mostra lo schema in tempo reale, mentre
come pulire una lista email copre il lato batch.
Domande frequenti
01Rails ha un modo integrato per controllare se un'email è recapitabile?
No. Tutti gli strumenti integrati per le email (la validazione format con URI::MailTo::EMAIL_REGEXP, gli scaffold, l'helper email_field) validano solo la forma. Nessuno di questi interroga il DNS o contatta un server di posta, quindi un dominio inventato o una casella inesistente passano. Per coprire la deliverability aggiungi una ricerca MX e un controllo SMTP della casella come validatori personalizzati, perché Rails non offre né l'una né l'altro.
02Come scrivo un validatore email personalizzato in Rails?
Estendi ActiveModel::EachValidator e implementa validate_each(record, attribute, value), chiamando record.errors.add(attribute, message) quando il valore non è valido. Salva la classe in app/validators perché Zeitwerk la carichi automaticamente, poi collegala con la sua chiave dedotta: un DeliverableDomainValidator si aggiunge con deliverable_domain: true. Accetta le stesse opzioni di qualsiasi validazione, quindi if:, on: e un message: personalizzato funzionano senza modifiche.
03Posso verificare che una casella esista in Rails senza un'API?
In parte. La libreria standard resolv conferma che il dominio accetta posta, il che esclude gratis i domini morti dall'interno di un validatore personalizzato. Confermare la casella richiede una conversazione SMTP che puoi aprire con net/smtp ma che non dovresti eseguire dal tuo server applicativo: la porta 25 in uscita è bloccata quasi ovunque e il risultato dipende dalla reputazione del tuo IP di invio. Quest'ultimo livello è ciò che un servizio di verifica esegue da un'infrastruttura con reputazione gestita, costruita apposta.
04Devo validare le email all'iscrizione o quando pulisco una lista?
Entrambe le cose, con una profondità diversa. Esegui i livelli di sintassi e 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 la verifica più approfondita, in batch, alla pulizia di una lista esistente, dove la latenza non conta e la completezza sì. Limitare i validatori di rete con if: :email_changed? evita che i normali aggiornamenti verifichino di nuovo un indirizzo già controllato.
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.



