Wie ist der Stand der Technik bei der E-Mail-Validierung für Rails?

95

Womit überprüfen Sie die E-Mail-Adressen der Benutzer und warum?

Ich hatte benutzt validates_email_veracity_of die tatsächlich die MX-Server abfragt. Dies ist jedoch aus verschiedenen Gründen voller Fehler, hauptsächlich im Zusammenhang mit dem Netzwerkverkehr und der Zuverlässigkeit.

Ich habe mich umgesehen und konnte nichts Offensichtliches feststellen, mit dem viele Leute eine Überprüfung der Gesundheit einer E-Mail-Adresse durchführen. Gibt es dafür ein gepflegtes, einigermaßen genaues Plugin oder Juwel?

PS: Bitte sag mir nicht, ich soll eine E-Mail mit einem Link senden, um zu sehen, ob die E-Mail funktioniert. Ich entwickle eine Funktion "An einen Freund senden", daher ist dies nicht praktikabel.

Luke Francl
quelle
Hier ist ein super einfacher Weg, ohne sich mit Regex zu befassen: Erkennen einer gültigen E-Mail-Adresse
Zabba
Können Sie einen detaillierteren Grund angeben, warum die Abfrage des MX-Servers fehlschlägt? Ich würde gerne wissen, damit ich sehen kann, ob diese reparabel sind.
Lulalala

Antworten:

67

Mit Rails 3.0 können Sie eine E-Mail-Validierung ohne Regexp mithilfe des Mail- Gems verwenden .

Hier ist meine Implementierung ( als Juwel verpackt ).

Halleluja
quelle
Schön, ich benutze dein Juwel. Vielen Dank.
Jasoncrawford
sieht aus wie ###@domain.comwird validieren?
CWD
1
Leute, ich möchte dieses Juwel wiederbeleben, ich hatte keine Zeit, es zu pflegen. Aber es scheint, dass die Leute es immer noch benutzen und nach Verbesserungen suchen. Wenn Sie interessiert sind, schreiben Sie mir bitte über das Github-Projekt: Halleluja / valid_email
Halleluja
106

Mach das nicht schwieriger als es sein muss. Ihre Funktion ist nicht kritisch. Die Validierung ist nur ein grundlegender Schritt, um Tippfehler zu erkennen. Ich würde es mit einem einfachen regulären Ausdruck tun und die CPU-Zyklen nicht mit etwas zu Kompliziertem verschwenden:

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

Das wurde von http://www.regular-expressions.info/email.html angepasst - was Sie lesen sollten, wenn Sie wirklich alle Kompromisse kennen wollen. Wenn Sie einen korrekteren und viel komplizierteren, vollständig RFC822-kompatiblen regulären Ausdruck wünschen, finden Sie ihn auch auf dieser Seite. Aber die Sache ist folgende: Sie müssen es nicht ganz richtig machen.

Wenn die Adresse die Validierung besteht, senden Sie eine E-Mail. Wenn die E-Mail fehlschlägt, wird eine Fehlermeldung angezeigt. An welchem ​​Punkt können Sie dem Benutzer mitteilen "Entschuldigung, Ihr Freund hat das nicht erhalten. Möchten Sie es erneut versuchen?" oder markieren Sie es zur manuellen Überprüfung oder ignorieren Sie es einfach oder was auch immer.

Dies sind die gleichen Optionen, mit denen Sie sich befassen müssten, wenn die Adresse dies Validierung bestanden hätte. Denn selbst wenn Ihre Validierung perfekt ist und Sie den absoluten Beweis dafür erhalten, dass die Adresse vorhanden ist, kann das Senden dennoch fehlschlagen.

Die Kosten für ein falsches Positiv bei der Validierung sind gering. Der Nutzen einer besseren Validierung ist ebenfalls gering. Validieren Sie großzügig und sorgen Sie sich um Fehler, wenn diese auftreten.

SFEley
quelle
36
Ähm, wird das nicht im Museum und den neuen internationalen TLDs angezeigt? Diese Regex würde verhindern , dass viele gültige E - Mail - Adressen.
Elijah
3
Mit Elia einverstanden, ist dies eine schlechte Empfehlung. Außerdem bin ich mir nicht sicher, wie Sie dem Benutzer mitteilen können, dass sein Freund die E-Mail nicht erhalten hat, da nicht sofort festgestellt werden kann, ob die E-Mail erfolgreich war.
Jaryl
8
Guter Punkt auf .museum und so - als ich diese Antwort 2009 zum ersten Mal veröffentlichte, war es kein Problem. Ich habe den regulären Ausdruck geändert. Wenn Sie weitere Verbesserungen haben, können Sie diese auch bearbeiten oder einen Community-Wiki-Beitrag erstellen.
SFEley
5
Zu Ihrer Information, hier fehlen noch einige gültige E-Mail-Adressen. Nicht viele, aber einige. Technisch gesehen ist #|@foo.com beispielsweise eine gültige E-Mail-Adresse, ebenso wie "Hey, ich kann Leerzeichen haben, wenn sie in Anführungszeichen stehen" @ foo.com. Ich finde es am einfachsten, alles vor dem @ zu ignorieren und nur den Domain-Teil zu validieren.
Nerdmaster
6
Ich stimme der Motivation zu, dass Sie sich keine Sorgen machen sollten, wenn Sie falsche Adressen eingeben. Leider wird diese Regex einige korrekte Adressen nicht zulassen, was ich als inakzeptabel betrachte. Vielleicht wäre so etwas besser? /.+@.+\..+/
ZoFreX
12

Ich habe in Rails 3 ein Juwel für die E-Mail-Validierung erstellt. Ich bin ein bisschen überrascht, dass Rails so etwas standardmäßig nicht enthält.

http://github.com/balexand/email_validator

Balexand
quelle
8
Dies ist im Wesentlichen ein Wrapper um den regulären Ausdruck.
Rob Dawson
Können Sie ein Beispiel geben, wie dies mit einer ifoder unless-Anweisung verwendet wird? Die Dokumentation scheint spärlich.
CWD
@cwd Ich denke, die Dokumentation ist vollständig. Wenn Sie mit Rails 3+ -Validierungen nicht vertraut sind, lesen Sie diesen Railscast ( railscasts.com/episodes/211-validations-in-rails-3 ) oder guide.rubyonrails.org/active_record_validations.html
balexand
7

Aus den Rails 4-Dokumenten :

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
quelle
5

In Rails 4 fügen validates :email, email:trueSie einfach (vorausgesetzt, Ihr Feld wird aufgerufen email) zu Ihrem Modell hinzu und schreiben dann ein einfaches (oder komplexes †)EmailValidator , das Ihren Anforderungen entspricht.

zB: - Ihr Modell:

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

Ihr Validator (geht rein 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

Dies ermöglicht alle Arten von gültigen E-Mails, einschließlich getaggten E-Mails wie "[email protected]" und so weiter.

Um dies mit rspecin Ihrem zu testenspec/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

So habe ich es trotzdem gemacht. YMMV

† Reguläre Ausdrücke sind wie Gewalt; Wenn sie nicht funktionieren, verwenden Sie nicht genug davon.

Dave Sag
quelle
1
Ich bin versucht, Ihre Validierung zu verwenden, aber ich habe keine Ahnung, woher Sie sie haben oder wie Sie sie erstellt haben. Kannst du uns sagen?
Mauricio Moraes
Ich habe den regulären Ausdruck von einer Google-Suche erhalten und den Wrapper-Code und die Spezifikationstests selbst geschrieben.
Dave Sag
1
Schön, dass Sie auch die Tests gepostet haben! Aber was mich wirklich dazu gebracht hat, war das Power-Zitat dort oben! :)
Mauricio Moraes
4

Wie Halleluja vorschlägt, halte ich die Verwendung des Mail-Edelsteins für einen guten Ansatz. Allerdings mag ich einige der Reifen dort nicht.

Ich benutze:

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

Sie könnten strenger sein, wenn Sie verlangen, dass die TLDs (Top-Level-Domains) in dieser Liste enthalten sind. Sie wären jedoch gezwungen, diese Liste zu aktualisieren, wenn neue TLDs auftauchen (wie der 2012-Zusatz .mobiund.tel ).

Der Vorteil des direkten Einbindens des Parsers besteht darin, dass die Regeln in der Mail-Grammatik für die Teile, die das Mail-Juwel verwendet, ziemlich weit gefasst sind. Es soll ihm ermöglichen, eine Adresse zu analysieren, wie user<[email protected]>sie für SMTP üblich ist. Wenn Sie es aus dem konsumieren, Mail::Addresssind Sie gezwungen, eine Reihe zusätzlicher Überprüfungen durchzuführen.

Ein weiterer Hinweis zum Mail-Juwel, obwohl die Klasse RFC2822 heißt, enthält die Grammatik einige Elemente von RFC5322 , zum Beispiel diesen Test .

Sam Safran
quelle
1
Danke für diesen Ausschnitt, Sam. Ich bin ein wenig überrascht, dass es keine generische "meistens gut genug" -Validierung gibt, die vom Mail-Juwel bereitgestellt wird.
JD.
4

In Rails 3 ist es möglich, ein wiederverwendbares zu schreiben Validator , wie dieser großartige Beitrag erklärt:

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

und benutze es mit validates_with:

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

Bei den anderen Antworten bleibt die Frage offen - warum sollte man sich die Mühe machen, klug zu sein?

Das tatsächliche Volumen der Randfälle, die viele reguläre Ausdrücke möglicherweise ablehnen oder übersehen, scheint problematisch.

Ich denke, die Frage lautet: "Was versuche ich zu erreichen?", Selbst wenn Sie die E-Mail-Adresse "validieren", bestätigen Sie nicht, dass es sich um eine funktionierende E-Mail-Adresse handelt.

Wenn Sie sich für Regexp entscheiden, überprüfen Sie einfach, ob auf der Clientseite @ vorhanden ist.

Lassen Sie für das falsche E-Mail-Szenario einen Zweig "Nachricht konnte nicht gesendet werden" an Ihren Code senden.

Hammelfleisch
quelle
1

Grundsätzlich gibt es 3 häufigste Optionen:

  1. Regexp (es gibt kein Regexp für alle E-Mail-Adressen, also rollen Sie Ihre eigene)
  2. MX-Abfrage (das ist, was Sie verwenden)
  3. Generieren und Aktivieren eines Aktivierungstokens (restful_authentication way)

Wenn Sie nicht sowohl validates_email_veracity_of als auch die Token-Generierung verwenden möchten, würde ich die Regexp-Überprüfung der alten Schule durchführen.

Jaroslaw
quelle
1

Das Mail-Juwel verfügt über einen integrierten Adressparser.

begin
  Mail::Address.new(email)
  #valid
rescue Mail::Field::ParseError => e
  #invalid
end
letronje
quelle
Scheint in Rails 3.1 nicht für mich zu funktionieren. Mail :: Address.new ("john") gibt mir gerne ein neues Mail :: Address-Objekt zurück, ohne eine Ausnahme auszulösen.
Jasoncrawford
OK, in einigen Fällen wird eine Ausnahme ausgelöst, jedoch nicht bei allen. @ Hallelujahs Link scheint hier einen guten Ansatz zu haben.
Jasoncrawford
1

Diese Lösung basiert auf Antworten von @SFEley und @Alessandro DS mit einem Refactor und einer Klarstellung der Verwendung.

Sie können diese Validator-Klasse in Ihrem Modell folgendermaßen verwenden:

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

Vorausgesetzt, Sie haben Folgendes in Ihrem app/validatorsOrdner (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
quelle
1

Zur Validierung von Mailinglisten . (Ich benutze Rails 4.1.6)

Ich habe meinen regulären Ausdruck von hier bekommen . Es scheint sehr vollständig zu sein und wurde gegen eine große Anzahl von Kombinationen getestet. Sie können die Ergebnisse auf dieser Seite sehen.

Ich habe es leicht in einen Ruby-Regexp geändert und in meinen eingefügt lib/validators/email_list_validator.rb

Hier ist der Code:

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

Und ich benutze es so im Modell:

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

Mailinglisten wie diese werden mit unterschiedlichen Trennzeichen und Synthax validiert:

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

Bevor ich diesen regulären Ausdruck verwendet habe, habe ich ihn verwendet Devise.email_regexp, aber das ist ein sehr einfacher regulärer Ausdruck und hat nicht alle Fälle erhalten, die ich brauchte. Einige E-Mails sind gestoßen.

Ich habe andere reguläre Ausdrücke aus dem Internet ausprobiert, aber dieser hat bis jetzt die besten Ergebnisse erzielt. Hoffe es hilft in deinem Fall.

Mauricio Moraes
quelle