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

95

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

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

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

PS: Пожалуйста, не просите меня отправлять электронное письмо со ссылкой, чтобы проверить, работает ли оно. Я разрабатываю функцию «отправить другу», поэтому это непрактично.

Люк Франкл
источник
Вот супер-простой способ без использования регулярных выражений: определение действительного адреса электронной почты
Забба,
Не могли бы вы более подробно описать причину, по которой запрос сервера MX завершился неудачно? Я хотел бы знать, чтобы увидеть, можно ли это исправить.
lulalala

Ответы:

67

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

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

Аллилуйя
источник
Отлично, я использую твой камень. Спасибо.
jasoncrawford
похоже ###@domain.comбудет подтверждать?
cwd 02
1
Ребят хотелось бы оживить эту жемчужину, не успел ее обслуживать. Но кажется, что люди все еще используют его и ищут улучшения. Если вам интересно, напишите, пожалуйста, мне о проекте на github: hallelujah / valid_email
Hallelujah
106

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

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

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

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

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

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

SFEley
источник
36
Эээээ, а разве этот бардак не появится на .museum и новых международных доменах верхнего уровня? Это регулярное выражение предотвратило бы множество действительных адресов электронной почты.
Элайджа,
3
Согласен с Илией, это плохая рекомендация. Кроме того, я не уверен, как вы думаете, что можете сказать пользователю, что его друг не получил электронное письмо, потому что нет способа сразу определить, удалось ли электронное письмо успешно.
Джарил
8
Хорошее замечание по поводу .museum и тому подобного - когда я впервые опубликовал этот ответ в 2009 году, это не было проблемой. Я изменил регулярное выражение. Если у вас есть дальнейшие улучшения, вы также можете отредактировать его или сделать это сообщение вики сообщества.
SFEley
5
К вашему сведению, здесь все равно будут отсутствовать некоторые действующие адреса электронной почты. Не много, но несколько. Например, технически #|@foo.com является допустимым адресом электронной почты, как и «Эй, у меня могут быть пробелы, если они цитируются» @ foo.com. Я считаю, что проще всего игнорировать что-либо до @ и проверять только часть домена.
Nerdmaster
6
Я согласен с тем, что вам не следует беспокоиться о пропуске некоторых неправильных адресов. К сожалению, это регулярное выражение будет запрещать некоторые правильные адреса, которые я считаю неприемлемыми. Может быть, что-то подобное было бы лучше? /.+@.+\..+/
ZoFreX
12

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

http://github.com/balexand/email_validator

Балександ
источник
8
По сути, это оболочка для регулярного выражения.
Роб Доусон
Вы можете привести пример того, как использовать это с оператором ifили unless? Документация кажется скудной.
cwd 02
@cwd Думаю, документация полная. Если вы не знакомы с валидациями Rails 3+, то посмотрите этот Railscast ( railscasts.com/episodes/211-validations-in-rails-3 ) или guides.rubyonrails.org/active_record_validations.html
balexand
7

Из документов 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

В 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.tes» и так далее.

Чтобы проверить это 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

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

Дэйв Саг
источник
1
У меня есть искушение использовать вашу проверку, но я понятия не имею, откуда вы это взяли и как вы это сделали. Вы можете сказать нам?
Маурисио Мораес,
Я получил регулярное выражение из поиска Google и сам написал код оболочки и тесты спецификации.
Дэйв Саг
1
Замечательно, что вы тоже разместили тесты! Но что меня действительно поразило, так это цитата о власти! :)
Маурисио Мораес
4

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

Я использую:

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

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

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

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

Сэм Шафран
источник
1
Спасибо за этот фрагмент, Сэм. Я немного удивлен, что gem Mail не обеспечивает универсальной проверки «достаточно хорошо в большинстве случаев».
JD.
4

В 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

Есть три основных варианта:

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

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

Ярослав
источник
1

Гем Mail имеет встроенный анализатор адресов.

begin
  Mail::Address.new(email)
  #valid
rescue Mail::Field::ParseError => e
  #invalid
end
Letronje
источник
Похоже, у меня не работает в Rails 3.1. Mail :: Address.new ("john") с радостью возвращает мне новый объект Mail :: Address, не вызывая исключения.
jasoncrawford
Хорошо, в некоторых случаях это вызовет исключение, но не во всех. Ссылка @ Hallelujah, кажется, имеет здесь хороший подход.
jasoncrawford
1

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

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

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

Если у вас в app/validatorsпапке (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
источник
1

Для проверки списков рассылки . (Я использую Rails 4.1.6)

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

Я немного изменил его на регулярное выражение Ruby и поместил в свой 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

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

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

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

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

Маурисио Мораес
источник