Skip to content
Commencez avec 100 crédits de vérification gratuits
Qualisend
Tous les articles
Ingénierie / 12 mai 2026

Comment valider une adresse e-mail dans Rails

9 minutes read

Qualisend team
Une fenêtre d'éditeur de code intitulée user.rb avec une trace de voie Rails rouge et une pastille de résultat verte deliverable, au-dessus de la légende validates vers MX vers boîte aux lettres.

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.

Your reputation, protected.

Clean your first list in minutes. 100 free credits, no card required.

Get started