web-dev-qa-db-fra.com

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

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

J'utilisais validates_email_veracity_of qui interroge réellement les serveurs MX. Mais cela est plein d'échecs pour diverses raisons, principalement liées au trafic réseau et à la fiabilité.

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

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

95
Luke Francl

Avec Rails 3.0, vous pouvez utiliser une validation par e-mail sans regex en utilisant Mail gem .

Voici mon implémentation ( emballé comme un joya ).

67
Hallelujah

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 regex simple, et ne gaspillerais pas les cycles CPU 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 à RFC822, c'est également sur cette page. Mais la chose est la suivante: vous n'avez pas besoin de bien faire les choses.

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

Ce sont les mêmes options que vous auriez à traiter si l'adresse avait passé la validation. Car même si votre validation est parfaite et que vous acquérez une preuve absolue que l'adresse existe, l'envoi peut toujours é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.

106
SFEley

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

http://github.com/balexand/email_validator

12
balexand

Ce projet semble avoir le plus de téléspectateurs sur github en ce moment (pour la validation des e-mails dans Rails):

https://github.com/alexdunae/validates_email_format_of

10
Brian Armstrong

De la Rails 4 docs :

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
7
Mikey

Dans Rails 4 ajoutez simplement validates :email, email:true (en supposant que votre champ s'appelle email) dans votre modèle, puis écrivez un simple (ou complexe †) EmailValidator pour répondre à vos besoins.

ex: - votre modèle:

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

Votre validateur (va dans 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 de courriels valides, y compris marqués courriels comme "[email protected]" et ainsi de suite.

Pour tester cela avec rspec dans votre spec/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.

5
Dave Sag

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 l'utiliser avec validates_with:

class User < ActiveRecord::Base   
  validates_with EmailValidator
end
4
Alessandro DS

Comme Alléluia le suggère, je pense que l'utilisation du Mail gem 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) soient dans cette liste , mais vous seriez obligé de mettre à jour cette liste à mesure que de nouveaux TLD apparaîtront (comme l'ajout de 2012 .mobi et .tel)

L'avantage de raccorder directement l'analyseur est que les règles de la grammaire du courrier sont assez larges pour les parties utilisées par le gem Mail, il est conçu pour lui permettre d'analyser une adresse comme user<[email protected]> ce qui est courant pour SMTP. En le consommant à partir du Mail::Address vous êtes obligé de faire un tas de vérifications supplémentaires.

Une autre note concernant la gemme Mail, même si la classe s'appelle RFC2822, la grammaire a certains éléments de RFC5322 , par exemple ce test .

4
Sam Saffron

Compte tenu des autres réponses, la question demeure - pourquoi se soucier d'être intelligent à ce sujet?

Le volume réel de cas Edge 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'essaie de réaliser?", 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 l'expression régulière, vérifiez simplement la présence de @ du côté client.

En ce qui concerne le scénario de courrier électronique incorrect, une branche "message n'a pas pu être envoyé" à votre code.

3
muttonlamb

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

begin
  Mail::Address.new(email)
  #valid
rescue Mail::Field::ParseError => e
  #invalid
end
1
letronje

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

Vous pouvez utiliser cette classe de validateur 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 disposez des éléments suivants dans votre app/validators dossier (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
1
thekingoftruth

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

J'ai obtenu mon expression rationnelle de ici . Il semble que ce soit 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 Ruby regexp, et 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 les 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'ai utilisé 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 courriels se sont heurtés.

J'ai essayé d'autres expressions rationnelles sur le Web, mais celle-ci a obtenu les meilleurs résultats jusqu'à présent. J'espère que cela vous aidera dans votre cas.

1
Mauricio Moraes

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 lancez la vôtre)
  2. Requête MX (c'est ce que vous utilisez)
  3. Générer un jeton d'activation et l'envoyer (façon restful_authentication)

Si vous ne souhaitez pas utiliser à la fois validates_email_veracity_of et la génération de jetons, je choisirais la vérification des expressions rationnelles à l'ancienne.

1
Yaroslav