Valider une adresse e-mail en Ruby, ce sont trois tâches qui se cachent derrière un seul nom de méthode. La plupart des tutoriels vous tendent une regex, la regardent réussir et déclarent l'adresse valide — mais une correspondance de motif indique seulement que la chaîne a la bonne forme, pas que du courrier l'atteindra un jour. La vraie validation se fait en couches : une vérification de syntaxe peu coûteuse, une recherche DNS des serveurs de messagerie du domaine, et une sonde SMTP de la boîte aux lettres réelle. La bibliothèque standard de Ruby vous offre les deux premières sans aucune gem ; la troisième est un problème de réseau qu'il vaut mieux déléguer. Ce guide construit chaque couche avec du code fonctionnel et montre exactement où la bibliothèque standard s'arrête.
La réponse courte#
Utilisez URI::MailTo::EMAIL_REGEXP de la bibliothèque uri pour la syntaxe,
Resolv::DNS de la bibliothèque resolv pour la recherche MX, et une API de
vérification pour la vérification SMTP de la boîte aux lettres — la moins coûteuse
en premier, en court-circuitant dès qu'une couche est décisive. N'allez pas
chercher net/smtp pour sonder les boîtes aux lettres depuis votre propre
application : le port 25 sortant est bloqué sur la plupart des hébergeurs, et la
réponse dépend de la réputation de votre IP d'envoi et d'un comportement de
greylisting que vous n'avez pas envie de réimplémenter. Chaque couche écarte des
adresses de manière moins coûteuse que la précédente ; seule la dernière peut
valider une adresse. Cette ultime étape est précisément la lacune que la
vérification d'e-mails existe pour combler.
Couche 1 : la syntaxe#
La regex a sa place ici et nulle part ailleurs — et en Ruby, vous n'avez même pas
à en écrire une. La bibliothèque uri, qui fait partie de la bibliothèque
standard, fournit un motif bien testé dans URI::MailTo::EMAIL_REGEXP ; allez le
chercher plutôt que de coller quelque chose trouvé sur 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
Deux choses méritent d'être connues à propos de cette constante. Elle est déjà
ancrée avec \A et \z, si bien que match? teste la chaîne entière plutôt
qu'une sous-chaîne — vous n'avez pas besoin de l'entourer ni de l'ancrer
vous-même. Et elle est délibérément permissive : elle suit la définition
WHATWG/HTML5 d'un e-mail valide, qui est plus large que la RFC 5322 et, par
exemple, accepte volontiers jane@localhost parce qu'elle n'exige pas de point
dans le domaine. C'est une fonctionnalité, pas un bug. Le rôle de la première
couche est d'attraper les fautes de frappe à la saisie, pas de plaider les
RFC — ce qui n'aiderait de toute façon pas.
Le garde-fou à 320 caractères est une double sécurité : rien de plus long ne peut
être une vraie adresse, et il est moins coûteux de rejeter tôt que de transmettre
une chaîne géante en aval. Un succès ici signifie « vaut la peine d'être
vérifiée », jamais « valide ».
Couche 2 : le domaine peut-il recevoir du courrier ?#
C'est ici que la bibliothèque standard de Ruby gagne son pain. Un domaine sans
route de messagerie ne peut accepter de courrier pour personne, donc une seule
recherche MX élimine les domaines morts, les noms d'entreprise mal orthographiés
et les TLD inventés. La bibliothèque resolv résout les enregistrements sans
aucune 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 vous remet un résolveur et le ferme à la sortie du bloc, et
getresources renvoie un tableau d'enregistrements MX — un tableau vide lorsque
le domaine n'en publie aucun ou n'existe pas, ce qui est exactement le cas
« pas de route » que vous voulez attraper. Le rescue gère l'autre mode de
défaillance : un résolveur qui échoue ou expire. Notez que
Resolv::ResolvTimeout n'est pas une sous-classe de Resolv::ResolvError, donc
nommez les deux si vous voulez qu'un serveur de noms lent passe proprement plutôt
que de faire exploser l'appel.
Quand vous voulez les serveurs de messagerie réels plutôt qu'un oui/non — pour
les journaliser ou en inspecter les priorités — chaque enregistrement porte
exchange et preference, alors triez par cette dernière :
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
Certains domaines acceptent le courrier sur un enregistrement A avec un MX
implicite ; si vous avez besoin d'honorer ce cas particulier, repliez-vous sur
getresources(domain, Resolv::DNS::Resource::IN::A) lorsque l'ensemble MX revient
vide. Pour l'écrasante majorité des adresses réelles, une vérification MX est le
bon filtre.
Couche 3 : la boîte aux lettres existe-t-elle vraiment ?#
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 a une syntaxe valide sur une
route de messagerie active, et c'est pourtant une boîte qui n'a jamais été créée.
Confirmer qu'une boîte aux lettres précise existe implique 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-mails
parcourt tout le pipeline, y compris les domaines catch-all qui acceptent toutes
les adresses et déjouent une sonde naïve.
La bibliothèque standard de Ruby ouvrira volontiers cette conversation pour vous
avec net/smtp. Le problème n'est pas le code ; c'est le réseau. Exécutez une
sonde SMTP depuis votre serveur applicatif 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 appliquent du greylisting 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 mettre sur liste de blocage, en
production. C'est la couche qu'il vaut la peine de déléguer.
Effectuer la vérification complète avec une API#
Le point de terminaison de vérification de Qualisend exécute tout le
pipeline — syntaxe, DNS et la sonde SMTP de la boîte aux lettres — depuis une
infrastructure à réputation gérée conçue pour cela, et renvoie un verdict unique.
Comme il s'agit d'un simple POST JSON, le net/http de la bibliothèque standard
le couvre sans aucune gem. Lisez votre clé depuis l'environnement plutôt que de la
coder en dur :
require "net/http"
require "json"
require "uri"
VERIFY_URL = URI("https://api.qualisend.com/v1/verify")
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) do |http|
http.request(request)
end
raise "Qualisend responded #{response.code}" unless response.is_a?(Net::HTTPSuccess)
JSON.parse(response.body, symbolize_names: true)
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 créer une clé restreinte. La réponse enveloppe un objet
result :
{
"result": {
"status": "deliverable",
"score": 95,
"reason": null,
"sub_flags": { "role": false, "disposable": false, "catch_all": false }
}
}
Plongez dans l'enveloppe et agissez sur 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:, ... }
net/http garde tout cela sans dépendance, ce qui est pratique dans une tâche
d'arrière-plan ou un petit service. Si votre application s'appuie déjà sur Faraday
ou HTTParty, la même requête tient en quelques lignes de moins — l'enveloppe que
vous relisez est identique.
Une façon complète de valider une adresse e-mail en Ruby#
Enchaînez les trois couches, la moins coûteuse en premier, et arrêtez-vous dès que l'une est décisive :
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
end
rpartition("@") découpe sur le dernier @, de sorte que vous prenez toujours
le vrai domaine, même pour des parties locales curieusement mises entre
guillemets. Les deux couches locales ne coûtent rien et attrapent instantanément
l'essentiel des déchets ; l'API ne s'exécute que sur les adresses qui valent
l'aller-retour réseau. C'est cet ordre — et non le langage — qui rend la
validation fiable, et c'est la même structure que vous trouverez dans les versions
Node.js et
Python de ce guide.
Les utilisateurs de Rails peuvent glisser l'ensemble dans un modèle sous forme de validation personnalisée, afin qu'une mauvaise adresse n'atteigne jamais la base de données :
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
Gardez le chemin synchrone limité aux deux couches locales rapides plus le verdict immédiat de l'API ; tout ce qui est plus lent relève d'une tâche d'arrière-plan. Mais le cœur est du Ruby pur, et il se comporte de la même façon dans une route Sinatra, une tâche rake ou un script ponctuel — c'est le découpage en couches qui fait le travail.
Foire aux questions#
Puis-je valider une adresse e-mail en Ruby sans aucune gem ?#
Pour deux des trois couches, oui. URI::MailTo::EMAIL_REGEXP, issu de la
bibliothèque uri, gère la syntaxe, et Resolv::DNS, issu de la bibliothèque
resolv, résout les enregistrements MX — les deux sont livrés avec Ruby, donc une
vérification syntaxe-plus-domaine ne nécessite aucune installation. La troisième
couche, la confirmation de la boîte aux lettres elle-même, implique une sonde SMTP
que vous ne devriez pas exécuter depuis votre propre serveur. C'est justement la
partie qu'un service de vérification prend en charge à votre place.
URI::MailTo::EMAIL_REGEXP suffit-il à lui seul ?#
C'est la bonne vérification de première couche et un choix bien plus sûr qu'une
regex écrite à la main — mais elle valide la forme, pas la délivrabilité. Elle
ne résout jamais le DNS ni ne contacte de serveur de messagerie, et elle est
permissive par conception (elle accepte jane@localhost), si bien qu'un succès
signifie « ressemble à un e-mail », pas « sera délivré ». Associez-la à une
recherche MX et à une vérification de la boîte aux lettres avant de faire confiance
à l'adresse.
Pourquoi ne pas utiliser net/smtp pour vérifier si une boîte aux lettres existe ?#
net/smtp peut ouvrir la conversation SMTP, mais l'exécuter depuis votre serveur
applicatif est peu fiable et risqué : la plupart des hébergeurs bloquent le port
25 sortant, la réponse dépend de la réputation de votre IP d'envoi, et sonder au
moindre volume vous fait limiter le débit ou mettre sur liste de blocage. Une API
de vérification exécute la sonde depuis une infrastructure à réputation gérée,
conçue précisément 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 la syntaxe et la recherche 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 le travail plus lent et plus poussé au nettoyage de liste et aux tâches d'arrière-guichet ; le guide d'inscription serverless montre le schéma en direct de bout en bout.
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 restreintes et des extraits à
copier-coller, ou collez une adresse dans le
vérificateur d'e-mails gratuit et regardez une chaîne
syntaxiquement parfaite revenir comme non délivrable.