Rails电子邮件验证的最新技术是什么?


95

您使用什么来验证用户的电子邮件地址,为什么?

我一直在使用validates_email_veracity_of哪个实际查询MX服务器。但是由于种种原因,这充满了失败,主要与网络流量和可靠性有关。

我环顾四周,找不到很多人用来对电子邮件地址进行完整性检查的明显信息。是否有为此维护的,相当准确的插件或gem?

PS:请不要告诉我发送带有链接的电子邮件,以查看该电子邮件是否有效。我正在开发“发送给朋友”功能,因此这不切实际。


这是一种非常简单的方法,无需处理正则表达式:检测有效的电子邮件地址
Zabba 2011年

您能否详细说明查询MX服务器失败的原因?我想知道,所以我可以看看它们是否可修复。
lulalala

Answers:


67

在Rails 3.0中,您可以使用Mail gem来使用没有regexp的电子邮件验证。

这是我的实现打包为gem)。


很好,我正在使用您的宝石。谢谢。
jasoncrawford 2012年

看起来###@domain.com会验证吗?
cwd

1
伙计们,我想复兴这个宝石,我没有时间去维护它。但是似乎人们仍然在使用它并寻求改进。如果您有兴趣,请在github项目上给我写信:hallelujah / valid_email
Hallelujah

106

不要使它变得比需要的难。您的功能不重要;验证只是捕捉错别字的基本理智步骤。我会用一个简单的正则表达式来做到这一点,并且不会在任何过于复杂的事情上浪费CPU周期:

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

改编自http://www.regular-expressions.info/email.html-如果您确实想知道所有折衷方法,则应阅读该文章。如果您想要更正确,更复杂且完全符合RFC822的正则表达式,则也可以在该页面上找到。但问题是这样的:您不必完全正确。

如果地址通过验证,您将发送一封电子邮件。如果电子邮件失败,您将收到一条错误消息。这时您可以告诉用户“对不起,您的朋友没有收到,您​​想再试一次吗?” 或将其标记为手动审核,或忽略它或其他任何内容。

如果地址确实通过了验证,则这些选项与您必须处理的选项相同。因为即使您的验证是完美的并且您获得地址存在的绝对证明,发送仍然可能失败。

验证误报的成本很低。更好的验证的好处也很低。进行大量验证,并在发生错误时担心它们。


36
不好意思,是不是对.museum和新的国际TLD bar之以鼻?此正则表达式将阻止许多有效的电子邮件地址。
以利亚

3
与以利亚一致同意,这是一个错误的建议。另外,我不确定您如何认为您可以告诉用户他的朋友没有收到电子邮件,因为无法立即判断电子邮件是否成功。
贾里尔

8
.museum之类的好点-当我于2009年首次发布该答案时,这不是问题。我更改了正则表达式。如果您有进一步的改进,您也可以对其进行编辑,或者将其设为社区Wiki。
SFEley 2011年

5
仅供参考,这仍然会丢失一些有效的电子邮件地址。数量不多,但很少。例如,从技术上讲,#|@foo.com是一个有效的电子邮件地址,就像“嘿,如果有引号,我可以有空格” @ foo.com。我发现最简单的方法是忽略@之前的所有内容,然后仅验证域部分。
Nerdmaster 2012年

6
我同意您不要担心允许通过一些错误地址的动机。遗憾的是,此正则表达式将禁止某些正确的地址,我认为这是不可接受的。也许这样的事情会更好?/.+@.+\..+/
ZoFreX 2012年

12

我在Rails 3中创建了一个用于电子邮件验证的gem。我很惊讶Rails默认情况下不包含这样的内容。

http://github.com/balexand/email_validator


8
这实际上是正则表达式的包装。
罗伯·道森

您能否举例说明如何将其与ifor unless语句一起使用?文档似乎很少。
cwd

@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

无论如何,这就是我所做的。青年汽车

†正则表达式就像暴力;如果它们不起作用,则说明您使用的不够。


1
我很想使用您的验证,但是我不知道您从何处获得验证或如何进行验证。你能告诉我们吗?
莫里西奥·莫赖斯

我从Google搜索中获取了正则表达式,然后自己编写了包装器代码和规范测试。
戴夫·萨格2014年

1
您也发布了测试真是太好了!但是真正让我着迷的是那里的电源报价!:)
Mauricio Moraes 2014年

4

正如Hallelujah所建议的那样,我认为使用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所使用的部分,Mail语法规则相当广泛,它旨在允许其解析user<user@example.com>SMTP常用的地址。通过从中消费它,Mail::Address您被迫进行大量额外的检查。

关于Mail gem的另一个说明,即使该类称为RFC2822,该语法也包含RFC5322的某些元素,例如此测试


1
谢谢您的摘录,山姆。令我感到惊讶的是,Mail gem没有提供通用的“大多数时间都足够好”的验证。
京东。

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

基本上有3种最常见的选择:

  1. 正则表达式(没有适用于所有电子邮件地址的正则表达式,因此请自己滚动)
  2. MX查询(即您所使用的)
  3. 生成激活令牌并将其邮寄(restful_authentication方法)

如果您不想同时使用validates_email_veracity_of和令牌生成,那么我将使用老式的正则表达式检查。


1

Mail gem具有内置的地址解析器。

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

在Rails 3.1中似乎不适用于我。Mail :: Address.new(“ john”)愉快地向我返回了一个新的Mail :: Address对象,而没有引发异常。
jasoncrawford 2012年

好的,在某些情况下会引发异常,但不是全部。@Hallelujah的链接在这里似乎是一种很好的方法。
jasoncrawford 2012年

1

该解决方案基于@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

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

它将使用不同的分隔符和synthax验证像这样的邮件列表:

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

在使用此regexp之前,我使用过Devise.email_regexp,但这是一个非常简单的regexp,没有得到我需要的所有情况。一些电子邮件碰到了。

我从网上尝试了其他正则表达式,但是到目前为止,这是最好的结果。希望它对您有帮助。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.