Apa yang canggih dalam validasi email untuk Rails?

95

Apa yang Anda gunakan untuk memvalidasi alamat email pengguna, dan mengapa?

Saya telah menggunakan validates_email_veracity_ofyang sebenarnya menanyakan server MX. Tetapi itu penuh dengan kegagalan karena berbagai alasan, kebanyakan terkait dengan lalu lintas jaringan dan keandalan.

Saya melihat sekeliling dan saya tidak dapat menemukan sesuatu yang jelas bahwa banyak orang menggunakan untuk melakukan pemeriksaan kewarasan pada alamat email. Apakah ada plugin atau gem yang dipelihara dan cukup akurat untuk ini?

NB: Tolong jangan suruh saya mengirim email dengan link untuk melihat apakah email itu berfungsi. Saya sedang mengembangkan fitur "kirim ke teman", jadi ini tidak praktis.

Luke Francl
sumber
Berikut adalah cara yang sangat mudah, tanpa berurusan dengan regex: mendeteksi-alamat-email-yang-valid
Zabba
Bisakah Anda memberikan alasan yang lebih detail mengapa kueri server MX gagal? Saya ingin tahu sehingga saya dapat melihat apakah ini dapat diperbaiki.
lulalala

Jawaban:

67

Dengan Rails 3.0 Anda dapat menggunakan validasi email tanpa regexp menggunakan permata Mail .

Berikut adalah implementasi saya ( dikemas sebagai permata ).

Haleluya
sumber
Bagus, saya menggunakan permata Anda. Terima kasih.
jasoncrawford
sepertinya ###@domain.comakan memvalidasi?
cwd
1
Guys Saya ingin menghidupkan kembali permata ini, saya tidak punya waktu untuk merawatnya. Tapi sepertinya orang masih menggunakannya dan mencari perbaikan. Jika Anda tertarik, silakan tulis saya di proyek github: hallelujah / valid_email
Hallelujah
106

Jangan membuat ini lebih sulit dari yang seharusnya. Fitur Anda tidak penting; validasi hanyalah langkah kewarasan dasar untuk menangkap kesalahan ketik. Saya akan melakukannya dengan regex sederhana, dan tidak menyia-nyiakan siklus CPU pada sesuatu yang terlalu rumit:

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

Itu diadaptasi dari http://www.regular-expressions.info/email.html - yang harus Anda baca jika Anda benar-benar ingin mengetahui semua pengorbanan. Jika Anda menginginkan regex yang lebih benar dan jauh lebih rumit yang sepenuhnya sesuai dengan RFC822, itu juga ada di halaman itu. Tapi masalahnya adalah ini: Anda tidak harus melakukannya dengan benar.

Jika alamat tersebut lolos validasi, Anda akan mengirim email. Jika email gagal, Anda akan mendapatkan pesan kesalahan. Pada tahap mana Anda dapat memberi tahu pengguna "Maaf, teman Anda tidak menerimanya, apakah Anda ingin mencoba lagi?" atau tandai untuk peninjauan manual, atau abaikan saja, atau apa pun.

Ini adalah opsi yang sama yang harus Anda tangani jika alamat tersebut lolos validasi. Karena meskipun validasi Anda sempurna dan Anda memperoleh bukti mutlak bahwa alamat tersebut ada, pengiriman masih bisa gagal.

Biaya positif palsu pada validasi rendah. Manfaat validasi yang lebih baik juga rendah. Validasi dengan murah hati, dan khawatirkan kesalahan saat terjadi.

SFEley
sumber
36
Err, bukankah itu akan muntah di .museum dan TLD internasional baru? Regex ini akan mencegah banyak alamat email yang valid.
Elijah
3
Setuju dengan Elia, ini adalah rekomendasi yang buruk. Selain itu, saya tidak yakin bagaimana menurut Anda Anda dapat memberi tahu pengguna bahwa temannya tidak menerima email karena tidak ada cara untuk mengetahui apakah email tersebut berhasil langsung.
Jaryl
8
Poin yang bagus tentang .museum dan semacamnya - ketika saya pertama kali memposting jawaban itu pada tahun 2009, itu bukan masalah. Saya mengubah regex. Jika Anda memiliki perbaikan lebih lanjut, Anda dapat mengeditnya juga, atau menjadikannya posting wiki komunitas.
SFEley
5
FYI, ini masih akan kehilangan beberapa alamat email yang valid. Tidak banyak, tapi sedikit. Misalnya, secara teknis # [email protected] adalah alamat email yang valid, seperti "Hai, saya dapat memiliki spasi jika dikutip" @ foo.com. Saya merasa paling mudah untuk mengabaikan apa pun sebelum @ dan hanya memvalidasi bagian domain.
Nerdmaster
6
Saya setuju dengan motivasi bahwa Anda tidak perlu khawatir tentang membiarkan melalui beberapa alamat yang salah. Sayangnya regex ini akan melarang beberapa alamat yang benar, yang menurut saya tidak dapat diterima. Mungkin sesuatu seperti ini lebih baik? /.+@.+\..+/
ZoFreX
12

Saya membuat permata untuk validasi email di Rails 3. Saya agak terkejut bahwa Rails tidak menyertakan sesuatu seperti ini secara default.

http://github.com/balexand/email_validator

balexand
sumber
8
Ini pada dasarnya adalah pembungkus di sekitar regex.
Rob Dawson
Dapatkah Anda memberikan contoh bagaimana menggunakan ini dengan pernyataan ifatau unless? Dokumentasi tampaknya jarang.
cwd
@cwd Saya rasa dokumentasinya sudah lengkap. Jika Anda tidak terbiasa dengan validasi Rails 3+, lihat Railscast ini ( railscasts.com/episodes/211-validations-in-rails-3 ) atau panduan.rubyonrails.org/active_record_validations.html
balexand
7

Dari dokumen 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
Mikey
sumber
5

Di Rails 4 cukup tambahkan validates :email, email:true(dengan asumsi field Anda dipanggil email) ke model Anda dan kemudian tulis sederhana (atau kompleks †) EmailValidatoruntuk menyesuaikan dengan kebutuhan Anda.

misalnya: - model Anda:

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

Validator Anda (masuk 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

Ini akan mengizinkan semua jenis email yang valid, termasuk email yang diberi tag seperti "[email protected]" dan seterusnya.

Untuk mengujinya dengan rspecdispec/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) { '[email protected]' }
    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) { '[email protected]' }
    it "is accepted as valid" do
      model.errors[].should_not_receive('<<')    
      validator.validate_each(model, "email", valid_tagged_email)
    end
  end
end

Begitulah cara saya melakukannya. YMMV

† Ekspresi reguler seperti kekerasan; jika tidak berfungsi, Anda tidak cukup menggunakannya.

Dave Sag
sumber
1
Saya tergoda untuk menggunakan validasi Anda, tetapi saya tidak tahu dari mana Anda mendapatkannya atau bagaimana Anda membuatnya. Bisakah kamu memberitahu kami?
Mauricio Moraes
Saya mendapatkan ekspresi reguler dari pencarian Google, dan menulis kode pembungkus dan pengujian spesifikasi sendiri.
Dave Sag
1
Senang sekali Anda memposting tes juga! Tapi yang benar-benar membuat saya tertarik adalah kutipan kekuatan di atas sana! :)
Mauricio Moraes
4

Seperti yang disarankan Hallelujah, saya pikir menggunakan permata Mail adalah pendekatan yang baik. Namun, saya tidak menyukai beberapa rintangan di sana.

Saya menggunakan:

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

Anda bisa lebih tegas dengan menuntut agar TLD (domain tingkat atas) ada dalam daftar ini , namun Anda akan dipaksa untuk memperbarui daftar itu sebagai TLD baru yang muncul (seperti penambahan tahun 2012 .mobidan .tel)

Keuntungan dari mengaitkan parser langsung adalah bahwa aturan dalam tata bahasa Mail cukup lebar untuk bagian-bagian yang digunakan permata Mail, ia dirancang untuk memungkinkannya mengurai alamat seperti user<[email protected]>yang umum untuk SMTP. Dengan mengonsumsinya dari Mail::AddressAnda dipaksa untuk melakukan banyak pemeriksaan tambahan.

Catatan lain tentang permata Mail, meskipun kelasnya disebut RFC2822, tata bahasanya memiliki beberapa elemen RFC5322 , misalnya tes ini .

Sam Saffron
sumber
1
Terima kasih untuk cuplikan ini, Sam. Saya sedikit terkejut bahwa tidak ada validasi umum "cukup baik untuk sebagian besar waktu" yang diberikan oleh permata Mail.
JD.
4

Di Rails 3 dimungkinkan untuk menulis validator yang dapat digunakan kembali , karena pos hebat ini menjelaskan:

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

dan gunakan dengan validates_with:

class User < ActiveRecord::Base   
  validates_with EmailValidator
end
Alessandro De Simone
sumber
3

Memperhatikan jawaban lainnya, pertanyaannya masih tersisa - mengapa repot-repot menjadi pintar tentang itu?

Volume kasus edge sebenarnya yang mungkin ditolak atau dilewatkan oleh banyak regex tampaknya bermasalah.

Saya rasa pertanyaannya adalah 'apa yang saya coba capai?', Meskipun Anda 'memvalidasi' alamat email tersebut, Anda sebenarnya tidak memvalidasi bahwa itu adalah alamat email yang berfungsi.

Jika Anda menggunakan regexp, cukup periksa keberadaan @ di sisi klien.

Adapun skenario email yang salah, memiliki cabang 'pesan gagal mengirim' ke kode Anda.

daging kambing
sumber
1

Pada dasarnya ada 3 opsi paling umum:

  1. Regexp (tidak ada regexp alamat email yang berfungsi untuk semua, jadi buat alamat email Anda sendiri)
  2. Kueri MX (itulah yang Anda gunakan)
  3. Menghasilkan token aktivasi dan mengirimkannya (cara restful_authentication)

Jika Anda tidak ingin menggunakan validates_email_veracity_of dan pembuatan token, saya akan menggunakan pemeriksaan regexp sekolah lama.

Yaroslav
sumber
1

Permata Mail memiliki pengurai alamat bawaan.

begin
  Mail::Address.new(email)
  #valid
rescue Mail::Field::ParseError => e
  #invalid
end
letronje
sumber
Sepertinya tidak berhasil untuk saya di Rails 3.1. Mail :: Address.new ("john") dengan senang hati mengembalikan objek Mail :: Address baru, tanpa memunculkan pengecualian.
jasoncrawford
Oke, ini akan memunculkan pengecualian dalam beberapa kasus, tetapi tidak semua. Tautan @ Hallelujah sepertinya memiliki pendekatan yang baik di sini.
jasoncrawford
1

Solusi ini didasarkan pada jawaban oleh @SFEley dan @Alessandro DS, dengan refactor, dan klarifikasi penggunaan.

Anda dapat menggunakan kelas validator ini dalam model Anda seperti:

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

Mengingat Anda memiliki yang berikut di app/validatorsfolder Anda (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
sumber
1

Untuk Validasi Milis . (Saya menggunakan Rails 4.1.6)

Saya mendapatkan regexp saya dari sini . Tampaknya ini sangat lengkap, dan telah diuji terhadap banyak kombinasi. Anda dapat melihat hasilnya di halaman itu.

Saya sedikit mengubahnya menjadi Ruby regexp, dan memasukkannya ke file lib/validators/email_list_validator.rb

Berikut kodenya:

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

Dan saya menggunakannya seperti ini dalam model:

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

Ini akan memvalidasi milis seperti ini, dengan pemisah dan sintaks yang berbeda:

mail_list = 'John Doe <[email protected]>, [email protected]; David G. <[email protected]>'

Sebelum menggunakan regexp ini, saya menggunakan Devise.email_regexp, tapi itu regexp yang sangat sederhana dan tidak mendapatkan semua kasus yang saya butuhkan. Beberapa email terbentur.

Saya mencoba regex lain dari web, tapi yang ini mendapatkan hasil terbaik sampai sekarang. Semoga membantu dalam kasus Anda.

Mauricio Moraes
sumber