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

95

你使用什么来验证用户的电子邮件地址,为什么要这样做?

我之前一直使用 validates_email_veracity_of,它实际上查询 MX 服务器。但由于与网络流量和可靠性有关的各种原因,这个方法存在许多失败情况。

我搜索了一下,没有找到明显的、被很多人使用的插件或 Gem 来对电子邮件地址进行合理的检查。是否有一个维护良好且相当准确的插件或 Gem 可以用于此目的?

附言:请不要告诉我发送带有链接的电子邮件以查看电子邮件是否有效。我正在开发一个“发送给朋友”的功能,所以这并不实用。


这里有一个超级简单的方法,不需要使用正则表达式:检测有效电子邮件地址 - Zabba
你能否提供更详细的原因,解释为什么查询MX服务器失败?我想知道这些问题是否可以修复。 - lulalala
14个回答

107

不要让这比必要的更难。您的功能并不关键;验证只是一个基本的健全步骤,可以捕捉拼写错误。我会用简单的正则表达式来完成它,不会浪费太多CPU周期:

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

那段代码是从http://www.regular-expressions.info/email.html改编而来的,如果你真的想了解所有权衡,你应该阅读该网页。如果您想要更正确、更复杂且完全符合RFC822的正则表达式也在该网页上。但问题在于:你不需要完全正确。

如果地址经过验证,您将发送电子邮件。如果电子邮件未通过验证,您将收到错误消息。此时,您可以告诉用户“抱歉,您的朋友没有接收到,请再试一次?”或标记为手动审核,或仅忽略它等等。

如果地址已通过验证,则您需要使用与未通过验证相同的选项进行处理。因为即使您的验证是完美的并且获得了绝对证明,地址是存在的,发送也可能失败。

验证的误报成本很低。更好的验证效果的益处也很低。慷慨地进行验证,并在发生错误时予以关注。


36
这个正则表达式是否会在.museum和新的国际顶级域名上出现问题呢?这个正则表达式将阻止许多有效的电子邮件地址。 - Elijah
3
同意Elijah的观点,这是一个糟糕的建议。此外,我不确定您如何告诉用户他的朋友没有收到电子邮件,因为一开始无法确定电子邮件是否发送成功。 - Jaryl
8
关于 .museum 和类似的问题,你说得很有道理。当我在2009年发布那个答案时,并没有这个问题。我已经修改了正则表达式。如果你有更多的改进意见,你也可以进行编辑,或者将其变为社区维护的贴子。 - SFEley
5
FYI,这仍然会错过一些有效的电子邮件地址。虽然不多,但包括技术上有效的#|@foo.com以及在引号中使用空格的"Hey I can have spaces if they're quoted"@foo.com。我发现最简单的方法是忽略@前的任何内容,只验证域部分。 - Nerdmaster
6
我同意这种想法,即不必过于担心允许一些错误的地址通过。不幸的是,这个正则表达式将不允许一些正确的地址通过,我认为这是不能接受的。也许像这样的表达式会更好一些?/.+@.+..+/ - ZoFreX
显示剩余7条评论

67

很好,我正在使用你的宝石。谢谢。 - jasoncrawford
似乎 ###@domain.com 可以通过验证? - cwd
1
大家好,我想要重新振兴这个宝石,我没有时间去维护它。但是似乎人们仍在使用它并寻求改进。如果你有兴趣,请在 Github 项目上给我写信:hallelujah/valid_email。 - Hallelujah

12

8
这基本上是对正则表达式的封装。 - Rob Dawson
你能否举个例子,展示如何将它与 ifunless 语句一起使用?文档似乎很简略。 - cwd
@cwd 我认为文档已经很完整了。如果你对 Rails 3+ 的验证不熟悉,可以看看这个 Railscast (http://railscasts.com/episodes/211-validations-in-rails-3) 或者 http://guides.rubyonrails.org/active_record_validations.html - balexand

10

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”等。

要在spec/validators/email_validator_spec.rb中使用rspec测试此功能。

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
我很想使用你的验证,但是我不知道你从哪里得到它,也不知道你是如何制作它的。你能告诉我们吗? - Mauricio Moraes
我从谷歌搜索中得到了正则表达式,并自己编写了包装器代码和规范测试。 - Dave Sag
1
很棒,你也发布了测试!但是真正让我感动的是上面的那句话! :) - Mauricio Moraes

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

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

你可以更加严格,要求TLD(顶级域名)在此列表中,但是随着新的TLD出现(例如2012年添加的.mobi.tel),你将不得不更新该列表。直接连接解析器的优点在于Mail语法中的规则相当宽松,适用于Mail gem使用的部分,它被设计为允许解析像user<user@example.com>这样的地址,这在SMTP中很常见。通过从Mail::Address消耗它,你被迫进行大量的额外检查。

关于 Mail gem 的另一个注意事项,即使该类被称为 RFC2822,语法中仍有一些RFC5322的元素,例如this test


1
谢谢你的代码片段,Sam。我有点惊讶 Mail gem 没有提供一个通用的“大多数情况下足够好”的验证。 - JD.

3

注意其他答案,问题仍然存在-为什么要费心地做到这一点?

许多正则表达式可能会拒绝或错过的实际边缘情况的数量似乎有问题。

我认为问题是“我想达到什么目的?”即使您“验证”电子邮件地址,您也没有实际验证它是否是有效的电子邮件地址。

如果您选择正则表达式,请在客户端检查@的存在。

至于不正确的电子邮件场景,请将“消息发送失败”的分支添加到您的代码中。


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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接