Dès l'instant où vous devez valider une adresse e-mail dans Rails, Active Record
donne l'impression que c'est trivial — tapez validates :email, regardez le test
passer, et passez à autre chose. Mais cette seule ligne ne répond qu'à la première
des trois questions que pose une véritable validation. L'adresse est-elle
correctement formée ? Son domaine peut-il recevoir du courrier ? La boîte aux
lettres existe-t-elle réellement ? Rails livre une réponse soignée à la première
et rien du tout pour les deux autres. Ce guide construit chaque couche comme une
validation Rails idiomatique — une règle format, puis deux classes
ActiveModel::EachValidator personnalisées — et montre exactement où le framework
s'arrête. Il prolonge le
guide de validation d'e-mail en Ruby : Rails
enveloppe les mêmes briques de la bibliothèque standard dans l'API de validations
déclarative.
La réponse courte#
Utilisez validates :email, format: { with: URI::MailTo::EMAIL_REGEXP } pour la
syntaxe, un validateur personnalisé adossé à la bibliothèque standard resolv
pour la recherche MX, et une API de vérification pour la vérification de boîte aux
lettres SMTP — trois validations sur un même attribut, la moins coûteuse d'abord,
chacune s'abstenant lorsqu'une couche moins coûteuse a déjà échoué. N'ouvrez pas
vous-même une session SMTP depuis votre application pour sonder les boîtes aux
lettres : le port 25 sortant est bloqué sur la plupart des hébergeurs, et la
réponse dépend de la réputation de l'IP d'envoi et du greylisting que vous ne
voulez pas réimplémenter. Une subtilité distingue Rails des frameworks dotés d'un
mot-clé bail — Active Model exécute chaque validateur que vous déclarez et
collecte toutes les erreurs, si bien que le court-circuit est une garde que vous
ajoutez, pas un drapeau que vous passez. Chaque couche écarte les adresses de
façon plus économique que la précédente ; seule la dernière peut valider une
adresse positivement.
Couche 1 : validation de format avec une règle validates#
Rails maîtrise la couche un, et vous n'avez pas à écrire de regex pour la
revendiquer. La bibliothèque uri de Ruby — que Rails charge déjà — fournit un
motif bien testé dans URI::MailTo::EMAIL_REGEXP, alors confiez-le à une
validation format plutôt que de coller quelque chose trouvé sur Stack Overflow :
app/models/user.rb
class User < ApplicationRecord
validates :email,
presence: true,
format: { with: URI::MailTo::EMAIL_REGEXP }
end
Cette constante est la même que celle sur laquelle s'appuie le
guide Ruby. Elle est déjà ancrée avec \A et
\z, elle teste donc la chaîne entière, et elle est délibérément permissive :
elle suit la définition WHATWG/HTML5, plus souple que la RFC 5322, et accepte
volontiers jane@localhost parce qu'elle n'exige pas de point dans le domaine.
C'est un atout. Le rôle de la couche un est d'attraper les fautes de frappe à la
saisie, pas de trancher sur les RFC — ce qui
n'aiderait de toute façon pas.
Ce qui compte, c'est de savoir où Rails s'arrête. Chaque fonctionnalité e-mail
intégrée — la règle format, les scaffolds, le helper email_field — valide la
forme. Aucune d'elles ne résout le DNS ni ne contacte un serveur de messagerie,
si bien que noreply-9f2x@gmail.com, typo@gmial.com et
sales@company-that-folded.com passent tous. Les trois sont indélivrables. Rails
ne livre aucune vérification de délivrabilité.
Couche 2 : le domaine peut-il recevoir du courrier ?#
C'est la première couche que Rails ne vous donne pas, et elle est peu coûteuse à
ajouter. Un domaine sans route de messagerie ne peut accepter du courrier pour
personne, si bien qu'une seule recherche MX élimine les domaines morts, les noms
d'entreprise mal orthographiés et les TLD inventés. Un validateur Rails n'est
qu'une classe qui hérite d'ActiveModel::EachValidator et implémente
validate_each, alors enveloppez-y la recherche MX de la bibliothèque 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 vous remet un résolveur et le ferme à la sortie du bloc, et
getresources renvoie un tableau d'enregistrements MX — vide lorsque le domaine
n'en publie aucun ou n'existe pas, ce qui correspond exactement au cas « aucune
route ». Le rescue couvre l'autre mode d'échec ; notez que Resolv::ResolvTimeout
n'est pas une sous-classe de Resolv::ResolvError, alors nommez les deux si vous
voulez qu'un serveur de noms lent passe proprement à travers. Ajoutez-le au modèle
par sa clé déduite — Rails transforme deliverable_domain en
DeliverableDomainValidator et trouve la classe que Zeitwerk a chargée
automatiquement depuis app/validators :
validates :email,
presence: true,
format: { with: URI::MailTo::EMAIL_REGEXP },
deliverable_domain: true
La garde return if ... record.errors[attribute].present? est toute l'astuce.
Active Model exécute chaque validateur que vous déclarez et agrège les erreurs — il
n'y a pas de bail. Mais les validations s'exécutent dans l'ordre où vous les
déclarez, si bien qu'au moment où celle-ci se déclenche, un échec de format
figure déjà dans record.errors[:email], et la garde saute entièrement la
recherche DNS. Une adresse malformée ne dépense jamais d'aller-retour vers un
serveur de noms.
Couche 3 : la boîte aux lettres existe-t-elle réellement ?#
Les couches un et deux ne peuvent qu'écarter une adresse. Un domaine peut publier
des enregistrements MX parfaits et n'avoir malgré tout aucune boîte aux lettres à
l'adresse que vous détenez — noreply-9f2x@gmail.com est une syntaxe valide sur
une route de messagerie active, et c'est pourtant une boîte qui n'a jamais été
créée. Confirmer une boîte aux lettres spécifique suppose la conversation de
livraison SMTP : se connecter au serveur de messagerie, émettre RCPT TO, lire la
réponse et se déconnecter avant d'envoyer quoi que ce soit. Il y a plus que cela —
comment fonctionne la vérification d'e-mail
parcourt tout le pipeline, y compris les domaines catch-all qui acceptent toutes
les adresses et déjouent une sonde naïve.
Le module net/smtp de Ruby ouvrira volontiers cette conversation. Le problème
n'est pas le code ; c'est le réseau. Exécutez une sonde SMTP depuis votre serveur
d'application et trois choses tournent mal : la plupart des fournisseurs cloud
bloquent entièrement le port 25 sortant, la réponse dépend de la réputation de
l'IP depuis laquelle vous vous connectez, et les serveurs de réception mettent
en greylist et limitent le débit des expéditeurs inconnus — si bien qu'une sonde
qui fonctionne sur votre ordinateur portable échoue discrètement, ou vous fait
inscrire sur une liste de blocage, en production. C'est la couche qu'il vaut la
peine de déléguer.
Appeler Qualisend depuis un validateur personnalisé#
La délégation reste propre parce qu'une API de vérification n'est qu'un validateur
de plus. Le point de terminaison verify de Qualisend exécute tout le pipeline —
syntaxe, DNS et la sonde de boîte aux lettres SMTP — depuis une infrastructure à
la réputation gérée, et renvoie un verdict unique. C'est un simple POST JSON, si
bien que le module net/http de la bibliothèque standard le couvre sans aucune
gem. Lisez la clé depuis l'environnement plutôt que de la coder en dur :
app/validators/deliverable_email_validator.rb
require "net/http"
require "json"
require "uri"
class DeliverableEmailValidator < ActiveModel::EachValidator
VERIFY_URL = URI("https://api.qualisend.com/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
Ce point de terminaison est un espace réservé — consultez la
référence de l'API pour l'URL de base exacte, la forme de la requête
et la façon de générer une clé à portée limitée. La réponse enveloppe un objet
result :
{
"result": {
"status": "deliverable",
"score": 95,
"reason": null,
"sub_flags": { "role": false, "disposable": false, "catch_all": false }
}
}
status vaut l'une des valeurs deliverable, risky, undeliverable ou
unknown. Le validateur ci-dessus n'échoue durement que sur undeliverable et
laisse passer risky et unknown, si bien que vous pouvez décider en aval quoi
en faire — filtrer sur score, ou brancher sur une entrée de sub_flags comme
disposable — au lieu de transformer une adresse limite en erreur de formulaire.
Et lorsque la requête elle-même échoue, verify renvoie nil et le validateur
n'ajoute aucune erreur : les couches de syntaxe et MX ont déjà tourné localement,
si bien qu'adoucir uniquement la couche réseau empêche une panne passagère de
bloquer une véritable inscription. Traitez le JSON ci-dessus comme la forme, non
comme le contrat ; la liste complète des champs se trouve dans la
documentation développeur.
Assembler les couches pour valider une adresse e-mail dans Rails#
Déclarez les trois validations sur l'attribut, la moins coûteuse d'abord. L'ordre de déclaration est l'ordre d'exécution, et chaque garde s'abstient lorsqu'une couche moins coûteuse a déjà échoué, si bien que la règle composée se lit de haut en bas exactement comme elle s'exécute :
app/models/user.rb
class User < ApplicationRecord
validates :email,
presence: true,
format: { with: URI::MailTo::EMAIL_REGEXP },
deliverable_domain: true,
deliverable_email: true
end
Désormais, user.save n'atteint l'API que sur des adresses qui ont déjà passé la
syntaxe et la vérification MX — une faute de frappe échoue à format et ne dépense
jamais d'aller-retour vers un serveur de noms ni de crédit d'API, et un domaine
mort échoue à deliverable_domain avant l'appel réseau. user.valid? exécute
toute la chaîne, et user.errors[:email] porte la couche qui a objecté, quelle
qu'elle soit. C'est cet ordonnancement, et non le framework, qui rend la
validation fiable — la même forme que la
version Ruby, exprimée en validations
déclaratives plutôt qu'en une méthode chaînée à la main.
Parce que ce sont des validations ordinaires, toutes les options standard
fonctionnent. Limitez les deux couches réseau avec if: :email_changed? afin
qu'un user.update(name: ...) sans rapport ne relance pas une recherche DNS ni ne
redépense un crédit sur une adresse que vous avez déjà vérifiée. Réservez le
parcours synchrone aux couches locales rapides plus le verdict immédiat de l'API à
l'inscription ; tout ce qui est plus lourd relève d'un job en arrière-plan. Le
guide d'inscription serverless montre le
motif en temps réel, et
comment nettoyer une liste d'e-mails couvre le
volet par lots.
Foire aux questions#
Rails dispose-t-il d'un moyen intégré de vérifier qu'un e-mail est délivrable ?#
Non. Chaque fonctionnalité e-mail intégrée — la validation format avec
URI::MailTo::EMAIL_REGEXP, les scaffolds, le helper email_field — valide
uniquement la forme. Aucune d'elles ne résout le DNS ni ne contacte un serveur de
messagerie, si bien qu'un domaine inventé ou une boîte aux lettres inexistante
passe. Ajoutez une recherche MX et une vérification de boîte aux lettres SMTP sous
forme de validateurs personnalisés pour couvrir la délivrabilité, puisque Rails
n'en fournit aucune.
Comment écrire un validateur d'e-mail personnalisé dans Rails ?#
Créez une sous-classe d'ActiveModel::EachValidator et implémentez
validate_each(record, attribute, value), en appelant record.errors.add(attribute, message) lorsque la valeur échoue. Enregistrez la classe dans app/validators
pour que Zeitwerk la charge automatiquement, puis attachez-la par sa clé déduite —
un DeliverableDomainValidator se branche avec deliverable_domain: true. Il
accepte les mêmes options que n'importe quelle validation, si bien que if:, on:
et un message: personnalisé fonctionnent tous sans modification.
Puis-je vérifier qu'une boîte aux lettres existe dans Rails sans API ?#
En partie. La bibliothèque standard resolv confirme que le domaine accepte le
courrier, ce qui écarte gratuitement les domaines morts depuis l'intérieur d'un
validateur personnalisé. Confirmer la boîte aux lettres suppose une conversation
SMTP que vous pouvez ouvrir avec net/smtp mais que vous ne devriez pas exécuter
depuis votre serveur d'application — le port 25 sortant est largement bloqué et le
résultat dépend de la réputation de votre IP d'envoi. Cette dernière couche est ce
qu'un service de vérification exécute depuis une infrastructure à la réputation
gérée, conçue pour cela.
Dois-je valider les e-mails à l'inscription ou lors du nettoyage d'une liste ?#
Les deux, à des profondeurs différentes. Exécutez les couches de syntaxe et MX de
façon synchrone à l'inscription — elles sont assez rapides pour bloquer la requête
et donner un retour instantané — et agissez aussi sur le verdict immédiat de l'API
à cet endroit. Réservez une vérification plus poussée et par lots au
nettoyage d'une liste existante, où la latence
importe peu et la minutie compte. Limiter les validateurs réseau à
if: :email_changed? évite que les mises à jour ordinaires re-vérifient une
adresse que vous avez déjà contrôlée.
Prêt à ajouter la couche boîte aux lettres ? La référence de l'API
propose le point de terminaison /verify, les clés à portée limitée et des
extraits à copier-coller, ou collez une adresse dans le
vérificateur d'e-mail gratuit et regardez une chaîne
syntaxiquement parfaite revenir indélivrable.