Quel est l'état de l'art en matière de validation des e-mails pour Rails?

95

Qu'utilisez-vous pour valider les adresses e-mail des utilisateurs et pourquoi?

J'avais utilisé validates_email_veracity_ofqui interroge en fait les serveurs MX. Mais cela est plein d'échecs pour diverses raisons, principalement liées au trafic et à la fiabilité du réseau.

J'ai regardé autour de moi et je n'ai rien trouvé d'évident que beaucoup de gens utilisent pour effectuer un contrôle de cohérence sur une adresse e-mail. Existe-t-il un plugin ou un bijou maintenu et raisonnablement précis pour cela?

PS: Veuillez ne pas me dire d'envoyer un e-mail avec un lien pour voir si l'e-mail fonctionne. Je développe une fonction "envoyer à un ami", donc ce n'est pas pratique.

Luc Francl
la source
Voici un moyen très simple, sans traiter les regex: detecting-a-valid-email-address
Zabba
Pourriez-vous donner une raison plus détaillée pour laquelle l'interrogation du serveur MX échoue? J'aimerais savoir si je peux voir si cela peut être réparé.
lulalala

Réponses:

67

Avec Rails 3.0, vous pouvez utiliser une validation par e-mail sans expression régulière en utilisant le gem Mail .

Voici mon implémentation ( emballée comme un bijou ).

Alléluia
la source
Bien, j'utilise votre bijou. Merci.
jasoncrawford
ressemble à ###@domain.comvalider?
cwd
1
Les gars j'aimerais faire revivre ce bijou, je n'ai pas eu le temps de le maintenir. Mais il semble que les gens l'utilisent toujours et recherchent des améliorations. Si vous êtes intéressé, écrivez-moi sur le projet github: hallelujah / valid_email
Hallelujah
106

Ne rendez pas cela plus difficile que nécessaire. Votre fonctionnalité n'est pas critique; la validation n'est qu'une étape de base pour détecter les fautes de frappe. Je le ferais avec une simple regex, et ne gaspillerais pas les cycles du processeur sur quelque chose de trop compliqué:

/\A[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]+\z/

Cela a été adapté de http://www.regular-expressions.info/email.html - que vous devriez lire si vous voulez vraiment connaître tous les compromis. Si vous voulez une expression régulière plus correcte et beaucoup plus compliquée entièrement conforme à la RFC822, c'est également sur cette page. Mais la chose est la suivante: vous n'êtes pas obligé de bien faire les choses.

Si l'adresse passe la validation, vous allez envoyer un e-mail. Si l'e-mail échoue, vous recevrez un message d'erreur. À quel moment vous pouvez dire à l'utilisateur "Désolé, votre ami n'a pas reçu cela, souhaitez-vous réessayer?" ou signalez-le pour examen manuel, ou ignorez-le simplement, ou autre chose.

Ce sont les mêmes options que vous auriez à traiter si l'adresse ne passe validation. Car même si votre validation est parfaite et que vous acquérez la preuve absolue que l'adresse existe, l'envoi pourrait encore échouer.

Le coût d'un faux positif à la validation est faible. L'avantage d'une meilleure validation est également faible. Validez généreusement et craignez les erreurs lorsqu'elles se produisent.

SFEley
la source
36
Euh, n'est-ce pas un barf sur .museum et les nouveaux TLD internationaux? Cette regex empêcherait de nombreuses adresses e-mail valides.
Elijah
3
D'accord avec Elijah, c'est une mauvaise recommandation. De plus, je ne sais pas comment vous pensez pouvoir dire à l'utilisateur que son ami n'a pas reçu l'e-mail car il n'y a aucun moyen de savoir si l'e-mail a réussi dès le départ.
Jaryl
8
Bon point sur .museum et autres - quand j'ai publié cette réponse pour la première fois en 2009, ce n'était pas un problème. J'ai modifié le regex. Si vous avez d'autres améliorations, vous pouvez également le modifier ou en faire un article de wiki communautaire.
SFEley
5
FYI, il manquera toujours certaines adresses e-mail valides. Pas beaucoup, mais un peu. Par exemple, techniquement #|@foo.com est une adresse e-mail valide, tout comme "Hey, je peux avoir des espaces s'ils sont cités" @ foo.com. Je trouve plus facile d'ignorer tout ce qui se trouve avant le @ et de valider uniquement la partie domaine.
Nerdmaster
6
Je suis d'accord avec la motivation que vous ne devriez pas vous soucier d'autoriser certaines adresses incorrectes. Malheureusement, cette regex interdira certaines adresses correctes, ce que je considère comme inacceptable. Peut-être que quelque chose comme ça serait mieux? /.+@.+\..+/
ZoFreX
12

J'ai créé un joyau pour la validation des e-mails dans Rails 3. Je suis un peu surpris que Rails n'inclue pas quelque chose comme ça par défaut.

http://github.com/balexand/email_validator

balexand
la source
8
Il s'agit essentiellement d'un wrapper autour de l'expression régulière.
Rob Dawson
Pouvez-vous donner un exemple d'utilisation de ceci avec une instruction ifor unless? La documentation semble rare.
cwd
@cwd Je pense que la documentation est complète. Si vous n'êtes pas familier avec les validations de Rails 3+, consultez ce Railscast ( railscasts.com/episodes/211-validations-in-rails-3 ) ou guides.rubyonrails.org/active_record_validations.html
balex et
7

À partir de la documentation Rails 4 :

class EmailValidator < ActiveModel::EachValidator
  def validate_each(record, attribute, value)
    unless value =~ /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i
      record.errors[attribute] << (options[:message] || "is not an email")
    end
  end
end

class Person < ActiveRecord::Base
  validates :email, presence: true, email: true
end
Mikey
la source
5

Dans Rails 4, ajoutez simplement validates :email, email:true(en supposant que votre champ est appelé email) à votre modèle, puis écrivez un simple (ou complexe †)EmailValidator en fonction de vos besoins.

ex: - votre modèle:

class TestUser
  include Mongoid::Document
  field :email,     type: String
  validates :email, email: true
end

Votre validateur (entre app/validators/email_validator.rb)

class EmailValidator < ActiveModel::EachValidator
  EMAIL_ADDRESS_QTEXT           = Regexp.new '[^\\x0d\\x22\\x5c\\x80-\\xff]', nil, 'n'
  EMAIL_ADDRESS_DTEXT           = Regexp.new '[^\\x0d\\x5b-\\x5d\\x80-\\xff]', nil, 'n'
  EMAIL_ADDRESS_ATOM            = Regexp.new '[^\\x00-\\x20\\x22\\x28\\x29\\x2c\\x2e\\x3a-\\x3c\\x3e\\x40\\x5b-\\x5d\\x7f-\\xff]+', nil, 'n'
  EMAIL_ADDRESS_QUOTED_PAIR     = Regexp.new '\\x5c[\\x00-\\x7f]', nil, 'n'
  EMAIL_ADDRESS_DOMAIN_LITERAL  = Regexp.new "\\x5b(?:#{EMAIL_ADDRESS_DTEXT}|#{EMAIL_ADDRESS_QUOTED_PAIR})*\\x5d", nil, 'n'
  EMAIL_ADDRESS_QUOTED_STRING   = Regexp.new "\\x22(?:#{EMAIL_ADDRESS_QTEXT}|#{EMAIL_ADDRESS_QUOTED_PAIR})*\\x22", nil, 'n'
  EMAIL_ADDRESS_DOMAIN_REF      = EMAIL_ADDRESS_ATOM
  EMAIL_ADDRESS_SUB_DOMAIN      = "(?:#{EMAIL_ADDRESS_DOMAIN_REF}|#{EMAIL_ADDRESS_DOMAIN_LITERAL})"
  EMAIL_ADDRESS_WORD            = "(?:#{EMAIL_ADDRESS_ATOM}|#{EMAIL_ADDRESS_QUOTED_STRING})"
  EMAIL_ADDRESS_DOMAIN          = "#{EMAIL_ADDRESS_SUB_DOMAIN}(?:\\x2e#{EMAIL_ADDRESS_SUB_DOMAIN})*"
  EMAIL_ADDRESS_LOCAL_PART      = "#{EMAIL_ADDRESS_WORD}(?:\\x2e#{EMAIL_ADDRESS_WORD})*"
  EMAIL_ADDRESS_SPEC            = "#{EMAIL_ADDRESS_LOCAL_PART}\\x40#{EMAIL_ADDRESS_DOMAIN}"
  EMAIL_ADDRESS_PATTERN         = Regexp.new "#{EMAIL_ADDRESS_SPEC}", nil, 'n'
  EMAIL_ADDRESS_EXACT_PATTERN   = Regexp.new "\\A#{EMAIL_ADDRESS_SPEC}\\z", nil, 'n'

  def validate_each(record, attribute, value)
    unless value =~ EMAIL_ADDRESS_EXACT_PATTERN
      record.errors[attribute] << (options[:message] || 'is not a valid email')
    end
  end
end

Cela permettra à toutes sortes d'e - mails valides, y compris tagged e-mails comme "[email protected]" et ainsi de suite.

Pour tester cela avec rspecdans votrespec/validators/email_validator_spec.rb

require 'spec_helper'

describe "EmailValidator" do
  let(:validator) { EmailValidator.new({attributes: [:email]}) }
  let(:model) { double('model') }

  before :each do
    model.stub("errors").and_return([])
    model.errors.stub('[]').and_return({})  
    model.errors[].stub('<<')
  end

  context "given an invalid email address" do
    let(:invalid_email) { 'test test tes' }
    it "is rejected as invalid" do
      model.errors[].should_receive('<<')
      validator.validate_each(model, "email", invalid_email)
    end  
  end

  context "given a simple valid address" do
    let(:valid_simple_email) { '[email protected]' }
    it "is accepted as valid" do
      model.errors[].should_not_receive('<<')    
      validator.validate_each(model, "email", valid_simple_email)
    end
  end

  context "given a valid tagged address" do
    let(:valid_tagged_email) { '[email protected]' }
    it "is accepted as valid" do
      model.errors[].should_not_receive('<<')    
      validator.validate_each(model, "email", valid_tagged_email)
    end
  end
end

C'est comme ça que je l'ai fait de toute façon. YMMV

† Les expressions régulières sont comme la violence; s'ils ne fonctionnent pas, vous n'en utilisez pas suffisamment.

Dave Sag
la source
1
Je suis tenté d'utiliser votre validation, mais je n'ai aucune idée d'où vous l'avez obtenue ni comment vous l'avez faite. Pouvez vous nous dire?
Mauricio Moraes
J'ai obtenu l'expression régulière à partir d'une recherche Google, et j'ai écrit le code du wrapper et les tests de spécifications moi-même.
Dave Sag
1
C'est génial que vous ayez également posté les tests! Mais ce qui m'a vraiment attiré, c'est le power-quote là-haut! :)
Mauricio Moraes
4

Comme le suggère Hallelujah, je pense que l'utilisation de la gemme Mail est une bonne approche. Cependant, je n'aime pas certains des cerceaux là-bas.

J'utilise:

def self.is_valid?(email) 

  parser = Mail::RFC2822Parser.new
  parser.root = :addr_spec
  result = parser.parse(email)

  # Don't allow for a TLD by itself list (sam@localhost)
  # The Grammar is: (local_part "@" domain) / local_part ... discard latter
  result && 
     result.respond_to?(:domain) && 
     result.domain.dot_atom_text.elements.size > 1
end

Vous pourriez être plus strict en exigeant que les TLD (domaines de premier niveau) figurent dans cette liste , mais vous seriez obligé de mettre à jour cette liste à mesure que de nouveaux TLD apparaissent (comme l'ajout de 2012 .mobiet.tel )

L'avantage de l'accrochage direct de l'analyseur est que les règles de la grammaire Mail sont assez larges pour les portions user<[email protected]>utilisées par le gem Mail, il est conçu pour lui permettre d'analyser une adresse comme celle qui est courante pour SMTP. En le consommant, Mail::Addressvous êtes obligé de faire un tas de vérifications supplémentaires.

Une autre note concernant le gem Mail, même si la classe est appelée RFC2822, la grammaire contient certains éléments de la RFC5322 , par exemple ce test .

Sam Safran
la source
1
Merci pour cet extrait, Sam. Je suis un peu surpris qu'il n'y ait pas de validation générique "assez bien la plupart du temps" fournie par le gem Mail.
JD.
4

Dans Rails 3, il est possible d'écrire un validateur réutilisable , comme l'explique cet excellent article:

http://archives.ryandaigle.com/articles/2009/8/11/what-s-new-in-edge-rails-independent-model-validators

class EmailValidator < ActiveRecord::Validator   
  def validate()
    record.errors[:email] << "is not valid" unless
    record.email =~ /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i   
  end
end

et utilisez-le avec validates_with:

class User < ActiveRecord::Base   
  validates_with EmailValidator
end
Alessandro De Simone
la source
3

En notant les autres réponses, la question demeure: pourquoi se donner la peine d'être intelligent à ce sujet?

Le volume réel de cas limites que de nombreuses expressions régulières peuvent nier ou manquer semble problématique.

Je pense que la question est «qu'est-ce que j'essaye d'atteindre?», Même si vous «validez» l'adresse e-mail, vous ne validez pas réellement qu'il s'agit d'une adresse e-mail fonctionnelle.

Si vous optez pour une expression régulière, vérifiez simplement la présence de @ côté client.

En ce qui concerne le scénario de courrier électronique incorrect, ayez une branche «message échoué à envoyer» à votre code.

agneau de mouton
la source
1

Il existe essentiellement 3 options les plus courantes:

  1. Regexp (il n'y a pas de regexp d'adresse e-mail qui fonctionne pour tous, alors créez la vôtre)
  2. Requête MX (c'est ce que vous utilisez)
  3. Générer un jeton d'activation et l'envoyer par courrier (manière restful_authentication)

Si vous ne voulez pas utiliser à la fois validates_email_veracity_of et la génération de jetons, j'irais avec la vérification des expressions rationnelles à l'ancienne.

Yaroslav
la source
1

La gemme Mail a un analyseur d'adresses intégré.

begin
  Mail::Address.new(email)
  #valid
rescue Mail::Field::ParseError => e
  #invalid
end
Letronje
la source
Cela ne semble pas fonctionner pour moi dans Rails 3.1. Mail :: Address.new ("john") me renvoie volontiers un nouvel objet Mail :: Address, sans lever d'exception.
jasoncrawford
OK, cela lèvera une exception dans certains cas, mais pas tous. Le lien de @ Hallelujah semble avoir une bonne approche ici.
jasoncrawford
1

Cette solution est basée sur les réponses de @SFEley et @Alessandro DS, avec un refactor et une clarification d'utilisation.

Vous pouvez utiliser cette classe de validation dans votre modèle comme ceci:

class MyModel < ActiveRecord::Base
  # ...
  validates :colum, :email => { :allow_nil => true, :message => 'O hai Mark!' }
  # ...
end

Étant donné que vous avez les éléments suivants dans votre app/validatorsdossier (Rails 3):

class EmailValidator < ActiveModel::EachValidator

  def validate_each(record, attribute, value)
    return options[:allow_nil] == true if value.nil?

    unless matches?(value)
      record.errors[attribute] << (options[:message] || 'must be a valid email address')
    end
  end

  def matches?(value)
    return false unless value

    if /\A[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]+\z/.match(value).nil?
      false
    else
      true
    end

  end
end
le roi de la vérité
la source
1

Pour la validation des listes de diffusion . (J'utilise Rails 4.1.6)

J'ai obtenu mon expression rationnelle d' ici . Il semble être très complet, et il a été testé contre un grand nombre de combinaisons. Vous pouvez voir les résultats sur cette page.

Je l'ai légèrement changé en une expression rationnelle Ruby, et je l'ai mis dans mon lib/validators/email_list_validator.rb

Voici le code:

require 'mail'

class EmailListValidator < ActiveModel::EachValidator

  # Regexp source: https://fightingforalostcause.net/content/misc/2006/compare-email-regex.php
  EMAIL_VALIDATION_REGEXP   = Regexp.new('\A(?!(?:(?:\x22?\x5C[\x00-\x7E]\x22?)|(?:\x22?[^\x5C\x22]\x22?)){255,})(?!(?:(?:\x22?\x5C[\x00-\x7E]\x22?)|(?:\x22?[^\x5C\x22]\x22?)){65,}@)(?:(?:[\x21\x23-\x27\x2A\x2B\x2D\x2F-\x39\x3D\x3F\x5E-\x7E]+)|(?:\x22(?:[\x01-\x08\x0B\x0C\x0E-\x1F\x21\x23-\x5B\x5D-\x7F]|(?:\x5C[\x00-\x7F]))*\x22))(?:\.(?:(?:[\x21\x23-\x27\x2A\x2B\x2D\x2F-\x39\x3D\x3F\x5E-\x7E]+)|(?:\x22(?:[\x01-\x08\x0B\x0C\x0E-\x1F\x21\x23-\x5B\x5D-\x7F]|(?:\x5C[\x00-\x7F]))*\x22)))*@(?:(?:(?!.*[^.]{64,})(?:(?:(?:xn--)?[a-z0-9]+(?:-[a-z0-9]+)*\.){1,126}){1,}(?:(?:[a-z][a-z0-9]*)|(?:(?:xn--)[a-z0-9]+))(?:-[a-z0-9]+)*)|(?:\[(?:(?:IPv6:(?:(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){7})|(?:(?!(?:.*[a-f0-9][:\]]){7,})(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){0,5})?::(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){0,5})?)))|(?:(?:IPv6:(?:(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){5}:)|(?:(?!(?:.*[a-f0-9]:){5,})(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){0,3})?::(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){0,3}:)?)))?(?:(?:25[0-5])|(?:2[0-4][0-9])|(?:1[0-9]{2})|(?:[1-9]?[0-9]))(?:\.(?:(?:25[0-5])|(?:2[0-4][0-9])|(?:1[0-9]{2})|(?:[1-9]?[0-9]))){3}))\]))\z', true)

  def validate_each(record, attribute, value)
    begin
      invalid_emails = Mail::AddressList.new(value).addresses.map do |mail_address|
        # check if domain is present and if it passes validation through the regex
        (mail_address.domain.present? && mail_address.address =~ EMAIL_VALIDATION_REGEXP) ? nil : mail_address.address
      end

      invalid_emails.uniq!
      invalid_emails.compact!
      record.errors.add(attribute, :invalid_emails, :emails => invalid_emails.to_sentence) if invalid_emails.present?
    rescue Mail::Field::ParseError => e

      # Parse error on email field.
      # exception attributes are:
      #   e.element : Kind of element that was wrong (in case of invalid addres it is Mail::AddressListParser)
      #   e.value: mail adresses passed to parser (string)
      #   e.reason: Description of the problem. A message that is not very user friendly
      if e.reason.include?('Expected one of')
        record.errors.add(attribute, :invalid_email_list_characters)
      else
        record.errors.add(attribute, :invalid_emails_generic)
      end
    end
  end

end

Et je l'utilise comme ça dans le modèle:

validates :emails, :presence => true, :email_list => true

Il validera des listes de diffusion comme celle-ci, avec différents séparateurs et synthax:

mail_list = 'John Doe <[email protected]>, [email protected]; David G. <[email protected]>'

Avant d'utiliser cette expression rationnelle, j'utilisais Devise.email_regexp, mais c'est une expression rationnelle très simple et je n'ai pas obtenu tous les cas dont j'avais besoin. Certains e-mails ont été renvoyés.

J'ai essayé d'autres expressions régulières du Web, mais celle-ci a obtenu les meilleurs résultats jusqu'à présent. J'espère que cela aide dans votre cas.

Mauricio Moraes
la source