Каково современное состояние проверки электронной почты для Rails?


что вы используете для проверки адресов электронной почты пользователей, а почему?

я использовал validates_email_veracity_of, который фактически запрашивает MX-сервера. Но это полно сбоев по разным причинам, в основном связанных с сетевым трафиком и надежностью.

Я посмотрел вокруг, и я не мог найти ничего очевидного, что многие люди используют для выполнения проверки здравомыслия на адрес электронной почты. Есть ли поддерживаемый, достаточно точный плагин или драгоценный камень для это?

P. S.: И пожалуйста, не говори мне отправить электронное письмо со ссылкой, чтобы увидеть, если почта работает. Я разрабатываю функцию "Отправить другу", так что это не практично.

13 95

13 ответов:

с Rails 3.0 вы можете использовать проверку электронной почты без регулярного выражения с помощью Mail gem.

здесь реализация (упакованный в драгоценный камень).

Не делайте это сложнее, чем это должно быть. Ваша функция не критична; проверка-это всего лишь базовый шаг здравомыслия, чтобы поймать опечатки. Я бы сделал это с помощью простого регулярного выражения, а не тратить циклы процессора на что-то слишком сложное:

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

Это было адаптировано из http://www.regular-expressions.info/email.html -- который вы должны прочитать, если вы действительно хотите знать все компромиссы. Если вы хотите более правильный и гораздо более сложный полностью RFC822-совместимый регулярное выражение, это тоже на этой странице. Но дело вот в чем:вы не должны получить его совершенно правы.

Если адрес проходит проверку, вы собираетесь отправить электронное письмо. Если сообщение электронной почты не удается, вы получите сообщение об ошибке. В этот момент Вы можете сказать пользователю "Извините, ваш друг не получил это, вы хотели бы попробовать еще раз?" или пометить его для ручного просмотра, или просто игнорировать его, или что-то еще.

Это те же самые параметры, которые у вас были бы если адрес сделал проверки. Потому что даже если ваша проверка совершенна, и вы получаете абсолютное доказательство того, что адрес существует, отправка все равно может потерпеть неудачу.

стоимость ложного срабатывания при проверке низкая. Преимущество лучшей валидации также невелико. Проверить щедро, и беспокоиться об ошибках, когда они происходят.

Я создал драгоценный камень для проверки электронной почты в Rails 3. Я немного удивлен, что Rails не включает что-то вроде этого по умолчанию.

http://github.com/balexand/email_validator

этот проект, похоже, имеет большинство наблюдателей на github на данный момент (для проверки электронной почты в rails):

https://github.com/alexdunae/validates_email_format_of

С 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

в Rails 4 Просто добавьте validates :email, email:true (предполагая, что поле называется email) к вашей модели и затем написать простой (или сложный†) EmailValidator в соответствии с вашими потребностями.

например: - ваша модель:

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

ваш валидатор (входит 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

это позволит все виды действительных писем, в том числе тегом электронные письма, такие как " test+no_really@test.тес " и так далее.

чтобы проверить это с rspec в вашей 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) { '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

вот как я это сделал в любом случае. YMMV

†регулярные выражения подобны насилию; если они не работают, вы не используете их в достаточном количестве.

как Аллилуйя предлагает мне думать, используя Mail gem хороший подход. Однако мне не нравятся некоторые обручи там.

я использую:

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

вы могли бы быть строже, требуя, чтобы дву (домены верхнего уровня) находились в этот список, однако вы будете вынуждены обновить этот список по мере появления новых дву (например, добавление 2012 года .mobi и .tel)

преимущество прямого подключения парсера заключается в том, что элемент правила в грамматике Почты довольно широки для частей, которые использует Mail gem, он разработан, чтобы позволить ему анализировать адрес, такой как user<user@example.com> что является общим для SMTP. Потребляя его из Mail::Address вы вынуждены делать кучу дополнительных проверок.

еще одно замечание относительно почтового драгоценного камня, хотя класс называется RFC2822, грамматика имеет некоторые элементы RFC5322, например этот тест.

в Rails 3 можно написать многоразовые валидатор, как объясняет этот Великий пост:

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

и использовать его с validates_with:

class User < ActiveRecord::Base   
  validates_with EmailValidator
end

отмечая другие ответы,Вопрос все еще остается-зачем беспокоиться об этом?

фактический объем краевых случаев, которые многие регулярные выражения могут отрицать или пропустить, кажется проблематичным.

Я думаю, что вопрос в том, что я пытаюсь достичь?", даже если вы "проверяете" адрес электронной почты, вы на самом деле не проверяете, что это рабочий адрес электронной почты.

Если вы идете на регулярное выражение, просто проверьте наличие @ на стороне клиента.

как для в неправильном сценарии электронной почты есть ветвь "сообщение не удалось отправить" в ваш код.

есть в основном 3 наиболее распространенных варианта:

  1. обработать (нет работает для всех адресов электронной почты с помощью регулярных выражений, чтобы свернуть свой собственный)
  2. MX запрос (это то, что вы используете)
  3. создание токена активации и отправка его по почте (restful_authentication way)

Если вы не хотите использовать как validates_email_veracity_of, так и генерацию токенов, я бы пошел с проверкой регулярных выражений старой школы.

почтовый камень имеет встроенный анализатор адресов.

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

Это решение основано на ответах @SFEley и @Alessandro DS, с рефактором и разъяснением использования.

вы можете использовать этот класс валидатора в своей модели следующим образом:

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

учитывая, что у вас есть следующее в вашем app/validators папка (рельсы 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

на Проверка Списков Рассылки. (Я использую рельсы 4.1.6)

я получил свое регулярное выражение от здесь. Он кажется очень полным, и он был протестирован против большого количества комбинаций. Вы можете увидеть результаты на этой странице.

Я немного изменил его на рубиновое регулярное выражение и поместил его в мой lib/validators/email_list_validator.rb

вот код:

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

и я использую его так в модели:

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

Он будет проверять списки рассылки, как этот, с различными разделителями и synthax:

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

перед использованием этого регулярного выражения, я использовал Devise.email_regexp, но это очень простое регулярное выражение и не получил все дела, которые мне нужны. Некоторые электронные письма наткнулись.

Я пробовал другие регулярные выражения из интернета, но этот получил лучшие результаты до сих пор. Надеюсь, это поможет в вашем случае.