Qual è lo stato dell'arte nella convalida della posta elettronica per Rails?


95

Che cosa utilizzi per convalidare gli indirizzi email degli utenti e perché?

Stavo usando validates_email_veracity_ofche interroga effettivamente i server MX. Ma questo è pieno di errori per vari motivi, principalmente legati al traffico di rete e all'affidabilità.

Mi sono guardato intorno e non sono riuscito a trovare nulla di ovvio che molte persone utilizzano per eseguire un controllo di integrità su un indirizzo e-mail. Esiste un plugin o una gemma mantenuta e ragionevolmente accurata per questo?

PS: per favore non dirmi di inviare un'e-mail con un collegamento per vedere se l'email funziona. Sto sviluppando una funzione "invia ad un amico", quindi non è pratico.


Ecco un modo semplicissimo, senza occuparsi di regex: detecting-a-valid-email-address
Zabba

Potresti fornire un motivo più dettagliato per cui l'interrogazione del server MX non riesce? Vorrei sapere così posso vedere se questi sono risolvibili.
lulalala

Risposte:


67

Con Rails 3.0 puoi usare una validazione email senza regexp usando Mail gem .

Ecco la mia implementazione ( confezionata come una gemma ).


Bene, sto usando la tua gemma. Grazie.
jasoncrawford

sembra ###@domain.comconvalidare?
cwd

1
Ragazzi vorrei far rivivere questo gioiello, non ho avuto il tempo di mantenerlo. Ma sembra che le persone lo usino ancora e cercano miglioramenti. Se sei interessato, scrivimi sul progetto github: hallelujah / valid_email
Hallelujah

106

Non renderlo più difficile del necessario. La tua funzionalità non è critica; la convalida è solo un passaggio di sanità mentale di base per rilevare errori di battitura. Lo farei con una semplice regex e non sprecherò i cicli della CPU su qualcosa di troppo complicato:

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

Questo è stato adattato da http://www.regular-expressions.info/email.html - che dovresti leggere se vuoi davvero conoscere tutti i compromessi. Se vuoi un'espressione regolare più corretta e molto più complicata completamente conforme a RFC822, anche quella è su quella pagina. Ma il fatto è questo: non devi farlo completamente bene.

Se l'indirizzo supera la convalida, invierai un'email. Se l'email non riesce, riceverai un messaggio di errore. A quel punto puoi dire all'utente "Mi spiace, il tuo amico non l'ha ricevuto, vuoi riprovare?" o segnalalo per la revisione manuale, o semplicemente ignoralo, o qualsiasi altra cosa.

Queste sono le stesse opzioni che dovresti gestire se l'indirizzo avesse superato la convalida. Perché anche se la tua convalida è perfetta e acquisisci la prova assoluta che l'indirizzo esiste, l'invio potrebbe comunque non riuscire.

Il costo di un falso positivo alla convalida è basso. Anche il vantaggio di una migliore convalida è basso. Convalida generosamente e preoccupati degli errori quando si verificano.


36
Ehm, quel barf su .museum e sui nuovi TLD internazionali? Questa regex impedirebbe molti indirizzi email validi.
Elijah

3
D'accordo con Elijah, questa è una cattiva raccomandazione. Inoltre, non sono sicuro di come pensi di poter dire all'utente che il suo amico non ha ricevuto l'email perché non c'è modo di sapere se l'email è andata a buon fine.
Jaryl

8
Buon punto su .museum e simili: quando ho pubblicato per la prima volta quella risposta nel 2009 non era un problema. Ho modificato la regex. Se hai ulteriori miglioramenti, puoi anche modificarlo o renderlo un post wiki della comunità.
SFEley

5
Cordiali saluti, mancheranno ancora alcuni indirizzi e-mail validi. Non molti, ma pochi. Ad esempio, tecnicamente #|@foo.com è un indirizzo email valido, così come "Ehi, posso avere spazi se sono citati" @ foo.com. Trovo più semplice ignorare qualsiasi cosa prima di @ e convalidare solo la parte del dominio.
Nerdmaster

6
Sono d'accordo con la motivazione che non dovresti preoccuparti di consentire attraverso alcuni indirizzi errati. Purtroppo questa regex non consentirà alcuni indirizzi corretti, che considero inaccettabili. Forse qualcosa del genere sarebbe meglio? /.+@.+\..+/
ZoFreX

12

Ho creato un gioiello per la convalida della posta elettronica in Rails 3. Sono piuttosto sorpreso che Rails non includa qualcosa di simile per impostazione predefinita.

http://github.com/balexand/email_validator


8
Questo è essenzialmente un involucro attorno alla regex.
Rob Dawson

Puoi fornire un esempio di come usarlo con un'istruzione ifo unless? La documentazione sembra scarsa.
cwd

@cwd Penso che la documentazione sia completa. Se non hai familiarità con le convalide di Rails 3+, dai un'occhiata a questo Railscasts.com/episodes/211-validations-in-rails-3 ) o guides.rubyonrails.org/active_record_validations.html
balexand


7

Dai documenti di 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

5

In Rails 4 aggiungi semplicemente validates :email, email:true(supponendo che il tuo campo sia chiamato email) al tuo modello e poi scrivi un semplice (o complesso †)EmailValidator in base alle tue esigenze.

es: - il tuo modello:

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

Il tuo validatore (entra 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

Ciò consentirà tutti i tipi di email valide, comprese quelle contrassegnate email come "test+no_really@test.tes" e così via.

Per testarlo con rspecnel tuo filespec/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) { 'test@test.tes' }
    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) { 'test+thingo@test.tes' }
    it "is accepted as valid" do
      model.errors[].should_not_receive('<<')    
      validator.validate_each(model, "email", valid_tagged_email)
    end
  end
end

Comunque è così che l'ho fatto. YMMV

† Le espressioni regolari sono come la violenza; se non funzionano, non ne usi abbastanza.


1
Sono tentato di usare la tua convalida, ma non ho idea da dove l'hai presa o come l'hai fatta. Ci puoi dire?
Mauricio Moraes

Ho ottenuto l'espressione regolare da una ricerca su Google e ho scritto io stesso il codice wrapper e le specifiche.
Dave Sag

1
È fantastico che anche tu abbia pubblicato i test! Ma quello che mi ha veramente colpito è stata la citazione di potere lassù! :)
Mauricio Moraes

4

Come suggerisce Hallelujah, penso di usare la gemma di posta sia un buon approccio. Tuttavia, non mi piacciono alcuni dei cerchi lì.

Io uso:

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

Potresti essere più severo chiedendo che i TLD (domini di primo livello) siano in questo elenco , tuttavia saresti costretto ad aggiornare tale elenco quando vengono visualizzati nuovi TLD (come l'aggiunta del 2012 .mobie.tel )

Il vantaggio di agganciare direttamente il parser è che le regole nella grammatica di Mail sono abbastanza ampie per le parti utilizzate da Mail gem, è progettato per consentirgli di analizzare un indirizzo come user<user@example.com>è comune per SMTP. Consumandolo dal Mail::Addresssei costretto a fare un sacco di controlli extra.

Un'altra nota riguarda la gemma di posta, anche se la classe si chiama RFC2822, la grammatica ha alcuni elementi di RFC5322 , ad esempio questo test .


1
Grazie per questo frammento, Sam. Sono un po 'sorpreso che non ci sia una convalida generica "abbastanza buona la maggior parte del tempo" fornita dalla gemma di posta.
JD.

4

In Rails 3 è possibile scrivere un validatore riutilizzabile , come spiega questo fantastico post:

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

e usalo con validates_with:

class User < ActiveRecord::Base   
  validates_with EmailValidator
end

3

Prendendo atto delle altre risposte, la domanda rimane ancora: perché preoccuparsi di essere intelligente al riguardo?

Il volume effettivo di casi limite che molte espressioni regolari possono negare o ignorare sembra problematico.

Penso che la domanda sia "cosa sto cercando di ottenere?", Anche se "convalidi" l'indirizzo email, in realtà non stai convalidando che si tratta di un indirizzo email funzionante.

Se scegli regexp, controlla solo la presenza di @ sul lato client.

Per quanto riguarda lo scenario di posta elettronica errato, avere un ramo "Impossibile inviare il messaggio" al tuo codice.


1

Ci sono fondamentalmente 3 opzioni più comuni:

  1. Regexp (non esiste una regexp di indirizzi e-mail validi per tutti, quindi crea la tua)
  2. Query MX (questo è ciò che usi)
  3. Generare un token di attivazione e inviarlo per posta (modalità restful_authentication)

Se non si desidera utilizzare sia validates_email_veracity_of che la generazione di token, sceglierei il controllo regexp della vecchia scuola.


1

La gemma di posta ha un analizzatore di indirizzi integrato.

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

Non sembra funzionare per me in Rails 3.1. Mail :: Address.new ("john") mi restituisce felicemente un nuovo oggetto Mail :: Address, senza sollevare un'eccezione.
jasoncrawford

OK, in alcuni casi verrà generata un'eccezione, ma non in tutti. Il collegamento di @ Hallelujah sembra avere un buon approccio qui.
jasoncrawford

1

Questa soluzione si basa sulle risposte di @SFEley e @Alessandro DS, con un refactoring e chiarimenti sull'utilizzo.

Puoi usare questa classe di convalida nel tuo modello in questo modo:

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

Dato che hai quanto segue nella tua app/validatorscartella (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

Per la convalida delle liste di distribuzione . (Io uso Rails 4.1.6)

Ho ottenuto la mia regexp da qui . Sembra essere molto completo ed è stato testato con un gran numero di combinazioni. Puoi vedere i risultati in quella pagina.

L'ho leggermente cambiato in una regexp di Ruby e l'ho inserito nel mio file lib/validators/email_list_validator.rb

Ecco il codice:

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

E lo uso in questo modo nel modello:

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

Validerà le mailing list come questa, con diversi separatori e sintassi:

mail_list = 'John Doe <john@doe.com>, chuck@schuld.dea.th; David G. <david@pink.floyd.division.bell>'

Prima di usare questa regexp, ho usato Devise.email_regexp , ma è un'espressione regolare molto semplice e non ho ottenuto tutti i casi di cui avevo bisogno. Alcune e-mail sono state urtate.

Ho provato altre espressioni regolari dal web, ma questa ha ottenuto i migliori risultati fino ad ora. Spero che ti sia d'aiuto nel tuo caso.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.