Jaki jest stan wiedzy na temat sprawdzania poprawności wiadomości e-mail w Railsach?

95

Czego używasz do weryfikacji adresów e-mail użytkowników i dlaczego?

Używałem, validates_email_veracity_ofktóry faktycznie wysyła zapytania do serwerów MX. Ale jest to pełne niepowodzeń z różnych powodów, głównie związanych z ruchem w sieci i niezawodnością.

Rozejrzałem się i nie mogłem znaleźć niczego oczywistego, czego używa wiele osób do sprawdzania poczytalności adresu e-mail. Czy istnieje do tego utrzymana, dość dokładna wtyczka lub klejnot?

PS: Nie mów mi, żebym wysyłał e-mail z linkiem, aby sprawdzić, czy e-mail działa. Rozwijam funkcję „wyślij do znajomego”, więc nie jest to praktyczne.

Luke Francl
źródło
Oto bardzo łatwy sposób, bez zajmowania się wyrażeniem regularnym: wykrywanie-prawidłowego-adresu-e-mail
Zabba
Czy możesz podać bardziej szczegółowy powód niepowodzenia wysyłania zapytań do serwera MX? Chciałbym wiedzieć, żeby zobaczyć, czy można to naprawić.
lulalala

Odpowiedzi:

67

W Railsach 3.0 możesz używać sprawdzania poprawności wiadomości e-mail bez wyrażenia regularnego przy użyciu narzędzia Mail .

Oto moja realizacja ( zapakowana jako klejnot ).

Alleluja
źródło
Świetnie, używam twojego klejnotu. Dzięki.
jasoncrawford,
wygląda na to, ###@domain.comże potwierdzi?
cwd
1
Chłopaki, chciałbym ożywić ten klejnot, nie miałem czasu go konserwować. Ale wydaje się, że ludzie nadal go używają i szukają ulepszeń. Jeśli jesteś zainteresowany, napisz do mnie na projekt github: alleluja / valid_email
Hallelujah
106

Nie rób tego trudniej, niż powinno. Twoja funkcja nie jest krytyczna; walidacja to tylko podstawowy krok w celu wykrycia literówek. Zrobiłbym to za pomocą prostego wyrażenia regularnego i nie marnowałbym cykli procesora na nic zbyt skomplikowanego:

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

Zostało to zaadaptowane z http://www.regular-expressions.info/email.html - z którym powinieneś przeczytać, jeśli naprawdę chcesz poznać wszystkie kompromisy. Jeśli chcesz bardziej poprawnego i znacznie bardziej skomplikowanego wyrażenia regularnego, w pełni zgodnego z RFC822, to też jest na tej stronie. Ale rzecz jest taka: nie musisz robić tego całkowicie dobrze.

Jeśli adres przejdzie weryfikację, wyślesz e-mail. Jeśli wiadomość e-mail się nie powiedzie, otrzymasz komunikat o błędzie. W którym momencie możesz powiedzieć użytkownikowi „Przepraszamy, Twój znajomy tego nie otrzymał. Czy chcesz spróbować ponownie?”lub oflaguj do ręcznego sprawdzenia, albo po prostu zignoruj, czy cokolwiek.

Są to te same opcje, z którymi musiałbyś sobie poradzić, gdyby adres miał przechodzi walidacji. Ponieważ nawet jeśli Twoja walidacja jest doskonała i uzyskasz absolutny dowód, że adres istnieje, wysyłanie może się nie powieść.

Koszt fałszywie pozytywnego wyniku walidacji jest niski. Korzyści z lepszej walidacji są również niewielkie. Weryfikuj hojnie i martw się o błędy, gdy się pojawią.

SFEley
źródło
36
Eee, czy nie będzie to barfować na .museum i nowych międzynarodowych domenach TLD? To wyrażenie regularne uniemożliwiłoby wiele prawidłowych adresów e-mail.
Elijah
3
Zgadzam się z Eliaszem, to zła rekomendacja. Ponadto nie jestem pewien, jak myślisz, jak możesz powiedzieć użytkownikowi, że jego znajomy nie otrzymał e-maila, ponieważ nie ma sposobu, aby stwierdzić, czy e-mail od razu się powiódł.
Jaryl
8
Dobra uwaga na temat .museum i tym podobnych - kiedy po raz pierwszy opublikowałem tę odpowiedź w 2009 roku, nie było to problemem. Zmieniłem wyrażenie regularne. Jeśli masz dalsze ulepszenia, możesz je również edytować lub uczynić z tego posta wiki społeczności.
SFEley
5
FYI, nadal będzie brakować niektórych prawidłowych adresów e-mail. Niewiele, ale kilka. Na przykład technicznie #|@foo.com jest prawidłowym adresem e-mail, podobnie jak „Hej, mogę mieć spacje, jeśli są cytowane” @ foo.com. Najłatwiej jest po prostu zignorować wszystko przed znakiem @ i zweryfikować tylko część domeny.
Nerdmaster
6
Zgadzam się z motywacją, że nie powinieneś martwić się o przepuszczenie niektórych nieprawidłowych adresów. Niestety, to wyrażenie regularne zablokuje niektóre poprawne adresy, które uważam za niedopuszczalne. Może coś takiego byłoby lepsze? /.+@.+\..+/
ZoFreX
12

Stworzyłem klejnot do sprawdzania poprawności wiadomości e-mail w Railsach 3. Jestem trochę zaskoczony, że Railsy domyślnie nie zawierają czegoś takiego.

http://github.com/balexand/email_validator

balexand
źródło
8
Zasadniczo jest to opakowanie wokół wyrażenia regularnego.
Rob Dawson
Czy możesz podać przykład, jak używać tego z instrukcją iflub unless? Dokumentacja wydaje się skromna.
cwd
@cwd Myślę, że dokumentacja jest kompletna. Jeśli nie znasz walidacji Rails 3+, sprawdź ten Railscast ( railscasts.com/episodes/211-validations-in-rails-3 ) lub Guides.rubyonrails.org/active_record_validations.html
balexand
7

Z dokumentacji 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
źródło
5

W Rails 4 po prostu dodaj validates :email, email:true(zakładając, że twoje pole jest wywoływane email) do swojego modelu, a następnie napisz prostą (lub złożoną †) EmailValidatorwedług własnych potrzeb.

np: - Twój model:

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

Twój walidator (wchodzi 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

Pozwoli to na wszelkiego rodzaju prawidłowe e-maile, w tym e-maile z tagami, takie jak „[email protected]” i tak dalej.

Aby to przetestować rspecw swoimspec/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

Tak właśnie zrobiłem. YMMV

† Wyrażenia regularne są jak przemoc; jeśli nie działają, nie używasz ich wystarczająco dużo.

Dave Sag
źródło
1
Kusi mnie, aby skorzystać z twojej weryfikacji, ale nie mam pojęcia, skąd ją masz ani jak to zrobiłeś. Możesz nam powiedzieć?
Mauricio Moraes
Otrzymałem wyrażenie regularne z wyszukiwarki Google i sam napisałem kod opakowania i testy specyfikacji.
Dave Sag
1
To świetnie, że opublikowałeś również testy! Ale to, co naprawdę mnie przyciągnęło, to cytat z siły! :)
Mauricio Moraes
4

Jak sugeruje Hallelujah , uważam, że użycie klejnotu Mail to dobre podejście. Jednak nie podoba mi się niektóre z tamtejszych obręczy.

Używam:

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

Możesz być bardziej rygorystyczny, żądając, aby TLD (domeny najwyższego poziomu) znajdowały się na tej liście , jednak będziesz zmuszony aktualizować tę listę , gdy pojawią się nowe domeny TLD (takie jak dodanie z 2012 r. .mobiI.tel )

Zaletą bezpośredniego przechwycenia parsera jest to, że reguły gramatyki Mail są dość szerokie dla części używanych przez Mail gem. Zostały zaprojektowane tak, aby umożliwić parsowanie adresu podobnego do tego, user<[email protected]>który jest powszechny dla SMTP. Konsumując go od Mail::Addressciebie, jesteś zmuszony wykonać kilka dodatkowych kontroli.

Kolejna uwaga dotycząca klejnotu Mail, mimo że klasa nazywa się RFC2822, gramatyka zawiera pewne elementy RFC5322 , na przykład ten test .

Sam Saffron
źródło
1
Dzięki za ten fragment, Sam. Jestem trochę zaskoczony, że nie ma generycznego „wystarczająco dobrego przez większość czasu” walidacji zapewnianej przez klejnot Mail.
JD.
4

W Railsach 3 można napisać walidator wielokrotnego użytku , jak wyjaśnia ten wspaniały 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

i używaj go z validates_with:

class User < ActiveRecord::Base   
  validates_with EmailValidator
end
Alessandro De Simone
źródło
3

Biorąc pod uwagę inne odpowiedzi, nadal pozostaje pytanie - po co zawracać sobie głowę sprytem?

Faktyczna liczba przypadków skrajnych, które wiele wyrażeń regularnych może zaprzeczyć lub pominąć, wydaje się problematyczna.

Myślę, że pytanie brzmi „co próbuję osiągnąć?”, Nawet jeśli „potwierdzasz” adres e-mail, w rzeczywistości nie potwierdzasz, że jest to działający adres e-mail.

Jeśli wybierzesz regexp, po prostu sprawdź obecność @ po stronie klienta.

Jeśli chodzi o niepoprawny scenariusz dotyczący wiadomości e-mail, otrzymaj gałąź „wiadomość nie została wysłana” na Twój kod.

baranina
źródło
1

Istnieją zasadniczo 3 najbardziej typowe opcje:

  1. Regexp (nie ma wyrażenia regularnego adresu e-mail dla wszystkich, więc wybierz własne)
  2. Zapytanie MX (tego używasz)
  3. Wygenerowanie tokena aktywacyjnego i wysłanie go pocztą (sposób restful_authentication)

Jeśli nie chcesz używać zarówno validates_email_veracity_of, jak i generowania tokenów, skorzystałbym ze sprawdzania regexp starej szkoły.

Jarosław
źródło
1

Klejnot poczty ma wbudowany parser adresów.

begin
  Mail::Address.new(email)
  #valid
rescue Mail::Field::ParseError => e
  #invalid
end
letronje
źródło
Wydaje mi się, że nie działa w Railsach 3.1. Mail :: Address.new ("john") szczęśliwie zwraca mi nowy obiekt Mail :: Address, bez zgłaszania wyjątku.
jasoncrawford,
OK, w niektórych przypadkach zgłosi wyjątek, ale nie we wszystkich. Wydaje się, że link @ Alleluja ma tutaj dobre podejście.
jasoncrawford,
1

To rozwiązanie jest oparte na odpowiedziach @SFEley i @Alessandro DS, z refaktorem i wyjaśnieniem użytkowania.

Możesz użyć tej klasy walidatora w swoim modelu w następujący sposób:

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

Zakładając, że masz w swoim app/validatorsfolderze (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
thekingoftruth
źródło
1

Do walidacji list mailingowych . (Używam Rails 4.1.6)

Mam stąd moje wyrażenie regularne . Wydaje się, że jest bardzo kompletny i został przetestowany w wielu kombinacjach. Możesz zobaczyć wyniki na tej stronie.

Nieznacznie zmieniłem to na wyrażenie regularne Ruby i umieściłem je w pliku lib/validators/email_list_validator.rb

Oto kod:

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

I używam tego w modelu tak:

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

Sprawdza listy mailingowe takie jak ta, z różnymi separatorami i składnią:

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

Przed użyciem tego wyrażenia regularnego użyłem Devise.email_regexp, ale jest to bardzo proste wyrażenie regularne i nie otrzymałem wszystkich potrzebnych przypadków. Niektóre e-maile spadały.

Wypróbowałem inne wyrażenia regularne z sieci, ale ten ma najlepsze wyniki do tej pory. Mam nadzieję, że to pomoże w twoim przypadku.

Mauricio Moraes
źródło