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

Come validare un indirizzo email in Rails

9 min di lettura

Qualisend team
Come validare un indirizzo email in Rails

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.

La tua reputazione, protetta.

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

Inizia