Hiện đại trong xác thực email cho Rails là gì?


95

Bạn đang sử dụng gì để xác thực địa chỉ email của người dùng và tại sao?

Tôi đã sử dụng validates_email_veracity_ofmà thực sự truy vấn các máy chủ MX. Nhưng điều đó đầy thất bại vì nhiều lý do khác nhau, chủ yếu liên quan đến lưu lượng mạng và độ tin cậy.

Tôi nhìn xung quanh và tôi không thể tìm thấy bất cứ điều gì rõ ràng mà nhiều người đang sử dụng để thực hiện kiểm tra độ tỉnh táo trên một địa chỉ email. Có một plugin hoặc đá quý được duy trì, chính xác hợp lý cho việc này không?

Tái bút: Xin đừng bảo tôi gửi email kèm theo đường dẫn để xem email có hoạt động không. Tôi đang phát triển tính năng "gửi cho bạn bè", vì vậy điều này không thực tế.


Dưới đây là một cách siêu dễ dàng, mà không cần đối phó với regex: phát hiện-một-valid-email-address
Zabba

Bạn có thể cho biết lý do chi tiết hơn tại sao truy vấn máy chủ MX không thành công? Tôi muốn biết để tôi có thể xem liệu những điều này có thể sửa được hay không.
lulalala,

Câu trả lời:


67

Với Rails 3.0, bạn có thể sử dụng xác thực email mà không cần regexp bằng cách sử dụng Mail gem .

Đây là cách thực hiện của tôi ( được đóng gói dưới dạng đá quý ).


Tốt, tôi đang sử dụng đá quý của bạn. Cảm ơn.
jasoncrawford

có vẻ như ###@domain.comsẽ xác thực?
cwd

1
Các bạn ơi, tôi muốn hồi sinh viên ngọc này, tôi không có thời gian để bảo trì nó. Nhưng có vẻ như mọi người vẫn sử dụng nó và tìm kiếm những cải tiến. Nếu bạn quan tâm, vui lòng viết thư cho tôi về dự án github: hallelujah / valid_email
Hallelujah

106

Đừng làm việc này khó hơn mức cần thiết. Tính năng của bạn là không quan trọng; xác thực chỉ là một bước tỉnh táo cơ bản để bắt lỗi chính tả. Tôi sẽ làm điều đó với một regex đơn giản và không lãng phí chu kỳ CPU vào bất kỳ thứ gì quá phức tạp:

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

Điều đó được điều chỉnh từ http://www.regular-expressions.info/email.html - mà bạn nên đọc nếu bạn thực sự muốn biết tất cả các đánh đổi. Nếu bạn muốn có một regex tuân thủ RFC822 hoàn toàn chính xác hơn và phức tạp hơn nhiều, thì đó cũng là trên trang đó. Nhưng vấn đề là thế này: bạn không cần phải hiểu nó hoàn toàn đúng.

Nếu địa chỉ được xác thực, bạn sẽ gửi một email. Nếu email không thành công, bạn sẽ nhận được thông báo lỗi. Tại thời điểm đó, bạn có thể nói với người dùng "Xin lỗi, bạn của bạn không nhận được, bạn có muốn thử lại không?"hoặc gắn cờ nó để xem xét thủ công, hoặc bỏ qua nó, hoặc bất cứ điều gì.

Đây là những tùy chọn tương tự mà bạn phải giải quyết nếu địa chỉ đó vượt qua xác thực. Bởi vì ngay cả khi xác thực của bạn là hoàn hảo và bạn có được bằng chứng tuyệt đối rằng địa chỉ tồn tại, việc gửi vẫn có thể không thành công.

Chi phí của một dương tính giả khi xác thực là thấp. Lợi ích của việc xác nhận tốt hơn cũng thấp. Xác thực một cách hào phóng và lo lắng về các sai sót khi chúng xảy ra.


36
Err, điều đó sẽ không có trên .museum và TLD quốc tế mới phải không? Regex này sẽ ngăn nhiều địa chỉ email hợp lệ.
Elijah

3
Đồng ý với Elijah, đây là một khuyến nghị tồi. Ngoài ra, tôi không chắc làm thế nào bạn nghĩ rằng bạn có thể nói với người dùng rằng bạn của họ không nhận được email vì không có cách nào để biết liệu email có thành công hay không.
Jaryl

8
Điểm tốt trên .museum và những thứ khác - khi tôi đăng câu trả lời đó lần đầu tiên vào năm 2009, nó không phải là một vấn đề. Tôi đã thay đổi regex. Nếu bạn có những cải tiến hơn nữa, bạn cũng có thể chỉnh sửa nó hoặc biến bài này thành một bài đăng trên wiki cộng đồng.
SFEley

5
FYI, điều này sẽ vẫn thiếu một số địa chỉ email hợp lệ. Không nhiều, nhưng một vài. Ví dụ: về mặt kỹ thuật, #|@foo.com là một địa chỉ email hợp lệ, như "Này, tôi có thể có khoảng trắng nếu chúng được trích dẫn" @ foo.com. Tôi thấy dễ nhất là bỏ qua bất kỳ thứ gì trước @ và chỉ xác thực phần miền.
Nerdmaster

6
Tôi đồng ý với động lực rằng bạn không nên lo lắng về việc cho phép thông qua một số địa chỉ không chính xác. Đáng buồn thay, regex này sẽ không cho phép một số địa chỉ chính xác, mà tôi xem là không thể chấp nhận được. Có lẽ một cái gì đó như thế này sẽ tốt hơn? /.+@.+\..+/
ZoFreX

12

Tôi đã tạo một viên ngọc để xác thực email trong Rails 3. Tôi hơi ngạc nhiên rằng Rails không bao gồm một cái gì đó như thế này theo mặc định.

http://github.com/balexand/email_validator


8
Đây thực chất là một trình bao bọc xung quanh regex.
Rob Dawson

Bạn có thể cho một ví dụ về cách sử dụng điều này với một ifhoặc unlesscâu lệnh không? Tài liệu có vẻ thưa thớt.
cwd

@cwd Tôi nghĩ tài liệu đã hoàn tất. Nếu bạn không quen thuộc với việc phê chuẩn Rails 3+, sau đó kiểm tra railscast này ( railscasts.com/episodes/211-validations-in-rails-3 ) hoặc guides.rubyonrails.org/active_record_validations.html
balexand


7

Từ tài liệu 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

Trong Rails 4, chỉ cần thêm validates :email, email:true(giả sử trường của bạn được gọi email) vào mô hình của bạn và sau đó viết một † đơn giản (hoặc phức tạp)EmailValidator để phù hợp với nhu cầu của bạn.

ví dụ: - mô hình của bạn:

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

Trình xác thực của bạn (đi vào 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

Điều này sẽ cho phép tất cả các loại email hợp lệ, bao gồm các email được gắn thẻ như "test+no_really@test.tes", v.v.

Để kiểm tra điều này với rspectrongspec/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

Đây là cách tôi đã làm. YMMV

† Biểu hiện thông thường giống như bạo lực; nếu chúng không hoạt động, bạn không sử dụng đủ chúng.


1
Tôi muốn sử dụng xác thực của bạn, nhưng tôi không biết bạn lấy nó từ đâu hoặc bạn thực hiện nó như thế nào. Bạn có thể cho chúng tôi biết?
Mauricio Moraes

Tôi nhận được biểu thức chính quy từ tìm kiếm trên google và tự viết mã trình bao bọc và kiểm tra thông số kỹ thuật.
Dave Sag

1
Thật tuyệt khi bạn cũng đã đăng các bài kiểm tra! Nhưng điều thực sự khiến tôi chú ý là bảng báo giá điện trên đó! :)
Mauricio Moraes

4

Như Hallelujah gợi ý, tôi nghĩ rằng sử dụng Gem Mail là một cách tiếp cận tốt. Tuy nhiên, tôi không thích một số vòng ở đó.

Tôi sử dụng:

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

Bạn có thể chặt chẽ hơn bằng cách yêu cầu TLD (tên miền cấp cao nhất) phải có trong danh sách này , tuy nhiên, bạn sẽ buộc phải cập nhật danh sách đó khi TLD mới bật lên (như phần bổ sung năm 2012 .mobi.tel )

Ưu điểm của việc nối trình phân tích cú pháp trực tiếp là các quy tắc trong ngữ pháp Thư khá rộng đối với các phần mà Gem Mail sử dụng, nó được thiết kế để cho phép nó phân tích cú pháp một địa chỉ giống như địa chỉ user<user@example.com>phổ biến cho SMTP. Bằng cách tiêu thụ nó từ Mail::Addressbạn, bạn buộc phải thực hiện một loạt các kiểm tra bổ sung.

Một lưu ý khác liên quan đến Mail gem, mặc dù lớp được gọi là RFC2822, ngữ pháp có một số yếu tố của RFC5322 , ví dụ như bài kiểm tra này .


1
Cảm ơn vì đoạn mã này, Sam. Tôi hơi ngạc nhiên khi không có xác thực chung chung "đủ tốt trong hầu hết thời gian" được cung cấp bởi Gem Mail.
JD.

4

Trong Rails 3, bạn có thể viết trình xác nhận có thể sử dụng lại , như bài đăng tuyệt vời này giải thích:

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

và sử dụng nó với validates_with:

class User < ActiveRecord::Base   
  validates_with EmailValidator
end

3

Lưu ý đến các câu trả lời khác, câu hỏi vẫn còn đó - tại sao phải khéo léo về nó?

Khối lượng thực tế của các trường hợp cạnh mà nhiều regex có thể từ chối hoặc bỏ sót dường như có vấn đề.

Tôi nghĩ câu hỏi là 'tôi đang cố gắng làm gì?', Ngay cả khi bạn 'xác thực' địa chỉ email, bạn không thực sự xác thực rằng đó là địa chỉ email đang hoạt động.

Nếu bạn sử dụng regexp, chỉ cần kiểm tra sự hiện diện của @ ở phía máy khách.

Đối với trường hợp email không chính xác, có một nhánh 'thông báo không gửi được' đến mã của bạn.


1

Về cơ bản có 3 tùy chọn phổ biến nhất:

  1. Regexp (không có địa chỉ e-mail nào phù hợp cho tất cả regexp, vì vậy hãy cuộn của riêng bạn)
  2. Truy vấn MX (đó là những gì bạn sử dụng)
  3. Tạo mã thông báo kích hoạt và gửi nó qua thư (cách restful_authentication)

Nếu bạn không muốn sử dụng cả validates_email_veracity_of và tạo mã thông báo, tôi sẽ kiểm tra regexp của trường cũ.


1

Gem Mail có một trình phân tích cú pháp địa chỉ được tích hợp sẵn.

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

Dường như không hiệu quả với tôi trong Rails 3.1. Mail :: Address.new ("john") vui vẻ trả lại cho tôi một đối tượng Mail :: Address mới mà không đưa ra ngoại lệ.
jasoncrawford

OK, nó sẽ ném ra một ngoại lệ trong một số trường hợp, nhưng không phải tất cả. Liên kết của @ Hallelujah dường như có một cách tiếp cận tốt ở đây.
jasoncrawford

1

Giải pháp này dựa trên câu trả lời của @SFEley và @Alessandro DS, với một trình tái cấu trúc và làm rõ cách sử dụng.

Bạn có thể sử dụng lớp trình xác thực này trong mô hình của mình như sau:

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

Giả sử bạn có những thứ sau trong app/validatorsthư mục của mình (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

1

Để xác thực danh sách gửi thư . (Tôi sử dụng Rails 4.1.6)

Tôi nhận được regexp của mình từ đây . Nó có vẻ là một cái rất hoàn chỉnh và nó đã được thử nghiệm với rất nhiều sự kết hợp. Bạn có thể xem kết quả trên trang đó.

Tôi đã thay đổi một chút nó thành một regexp Ruby và đặt nó vào lib/validators/email_list_validator.rb

Đây là mã:

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

Và tôi sử dụng nó như thế này trong mô hình:

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

Nó sẽ xác thực danh sách gửi thư như thế này, với các dấu phân tách và cú pháp khác nhau:

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

Trước khi sử dụng regexp này, tôi đã sử dụng Devise.email_regexp, nhưng đó là một regexp rất đơn giản và không nhận được tất cả các trường hợp tôi cần. Một số email gặp sự cố.

Tôi đã thử các regexps khác từ web, nhưng cái này cho kết quả tốt nhất cho đến bây giờ. Hy vọng nó sẽ giúp trong trường hợp của bạn.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.