¿Cuál es el estado del arte en la validación de correo electrónico para Rails?

95

¿Qué está utilizando para validar las direcciones de correo electrónico de los usuarios y por qué?

Había estado usando validates_email_veracity_ofque en realidad consulta los servidores MX. Pero eso está lleno de fallas por varias razones, principalmente relacionadas con el tráfico y la confiabilidad de la red.

Miré a mi alrededor y no pude encontrar nada obvio que muchas personas estén usando para realizar una verificación de cordura en una dirección de correo electrónico. ¿Existe un complemento o gema mantenido y razonablemente preciso para esto?

PD: Por favor, no me diga que envíe un correo electrónico con un enlace para ver si el correo electrónico funciona. Estoy desarrollando una función "enviar a un amigo", por lo que esto no es práctico.

Luke Francl
fuente
Aquí hay una forma súper fácil, sin tener que lidiar con expresiones regulares: detectar una dirección de correo electrónico válida
Zabba
¿Podría dar una razón más detallada por la que falla la consulta del servidor MX? Me gustaría saberlo para ver si se pueden arreglar.
lulalala

Respuestas:

67

Con Rails 3.0, puede usar una validación de correo electrónico sin expresiones regulares usando la gema Mail .

Aquí está mi implementación ( empaquetada como una joya ).

Aleluya
fuente
Bien, estoy usando tu gema. Gracias.
Jasoncrawford
parece que ###@domain.comva a validar?
cwd
1
Chicos, me gustaría revivir esta joya, no tuve tiempo de mantenerla. Pero parece que la gente todavía lo usa y busca mejoras. Si estás interesado, escríbeme sobre el proyecto github: aleluya / valid_email
Aleluya
106

No hagas esto más difícil de lo necesario. Su función no es crítica; la validación es solo un paso básico de cordura para detectar errores tipográficos. Lo haría con una simple expresión regular y no desperdiciaría los ciclos de la CPU en algo demasiado complicado:

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

Eso fue adaptado de http://www.regular-expressions.info/email.html , que debe leer si realmente desea conocer todas las compensaciones. Si desea una expresión regular más correcta y mucho más complicada totalmente compatible con RFC822, también está en esa página. Pero la cosa es esta: no tienes que hacerlo del todo bien.

Si la dirección pasa la validación, enviará un correo electrónico. Si el correo electrónico falla, recibirá un mensaje de error. En ese momento puede decirle al usuario "Lo siento, su amigo no recibió eso, ¿le gustaría volver a intentarlo?" o marcarlo para revisión manual, o simplemente ignorarlo, o lo que sea.

Estas son las mismas opciones que le tiene que tratar si la dirección hizo pasar la validación. Porque incluso si su validación es perfecta y obtiene una prueba absoluta de que la dirección existe, el envío aún podría fallar.

El costo de un falso positivo en la validación es bajo. El beneficio de una mejor validación también es bajo. Valide generosamente y preocúpese por los errores cuando ocurran.

SFEley
fuente
36
Err, ¿no se burlará de .museum y los nuevos TLD internacionales? Esta expresión regular evitaría muchas direcciones de correo electrónico válidas.
Elijah
3
De acuerdo con Elijah, esta es una mala recomendación. Además, no estoy seguro de cómo cree que puede decirle al usuario que su amigo no recibió el correo electrónico porque no hay forma de saber si el correo electrónico tuvo éxito de inmediato.
Jaryl
8
Buen punto sobre .museum y demás : cuando publiqué esa respuesta por primera vez en 2009 no fue un problema. Alteré la expresión regular. Si tiene más mejoras, también puede editarlo o convertirlo en una publicación wiki de la comunidad.
SFEley
5
Para su información, todavía faltarán algunas direcciones de correo electrónico válidas. No muchos, pero unos pocos. Por ejemplo, técnicamente #|@foo.com es una dirección de correo electrónico válida, al igual que "Oye, puedo tener espacios si se citan" @ foo.com. Me resulta más fácil simplemente ignorar cualquier cosa antes de @ y validar solo la parte del dominio.
Nerdmaster
6
Estoy de acuerdo con la motivación de que no debe preocuparse por permitir el paso de algunas direcciones incorrectas. Lamentablemente, esta expresión regular no permitirá algunas direcciones correctas, que considero inaceptables. ¿Quizás algo como esto sería mejor? /.+@.+\..+/
ZoFreX
12

Creé una joya para la validación de correo electrónico en Rails 3. Me sorprende un poco que Rails no incluya algo como esto por defecto.

http://github.com/balexand/email_validator

balexand
fuente
8
Esto es esencialmente una envoltura alrededor de la expresión regular.
Rob Dawson
¿Puede dar un ejemplo de cómo usar esto con una declaración ifo unless? La documentación parece escasa.
Cwd
@cwd Creo que la documentación está completa. Si no está familiarizado con las validaciones de Rails 3+, consulte este Railscast ( railscasts.com/episodios/211-validations-in-rails-3 ) o guides.rubyonrails.org/active_record_validations.html
balexand
7

De los documentos de 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
fuente
5

En Rails 4 simplemente agregue validates :email, email:true(asumiendo que se llama su campo email) a su modelo y luego escriba un simple (o complejo †)EmailValidator que se adapte a sus necesidades.

por ejemplo: - su modelo:

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

Tu validador (entra 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

Esto permitirá todo tipo de correos electrónicos válidos, incluidos los correos electrónicos etiquetados como "[email protected]" y así sucesivamente.

Para probar esto rspecen tuspec/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

Así es como lo he hecho de todos modos. YMMV

† Las expresiones regulares son como violencia; si no funcionan, no está utilizando suficientes.

Dave Sag
fuente
1
Estoy tentado a usar tu validación, pero no tengo idea de dónde la obtuviste o cómo la hiciste. ¿Puedes decirnos?
Mauricio Moraes
Obtuve la expresión regular de una búsqueda en Google y escribí el código de envoltura y las pruebas de especificaciones yo mismo.
Dave Sag
1
¡Es genial que también hayas publicado las pruebas! ¡Pero lo que realmente me atrapó fue la cita de poder allí arriba! :)
Mauricio Moraes
4

Como sugiere Hallelujah , creo que usar la gema Mail es un buen enfoque. Sin embargo, no me gustan algunos de los aros allí.

Yo suelo:

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

Podría ser más estricto exigiendo que los TLD (dominios de nivel superior) estén en esta lista ; sin embargo, se verá obligado a actualizar esa lista a medida que aparezcan nuevos TLD (como la adición de 2012 .mobiy .tel)

La ventaja de conectar el analizador directamente es que las reglas de la gramática de Mail son bastante amplias para las partes que usa la gema de Mail, está diseñado para permitirle analizar una dirección como la user<[email protected]>que es común para SMTP. Al consumirlo del Mail::Address, se ve obligado a hacer un montón de controles adicionales.

Otra nota con respecto a la gema Mail, aunque la clase se llama RFC2822, la gramática tiene algunos elementos de RFC5322 , por ejemplo esta prueba .

Sam Saffron
fuente
1
Gracias por este fragmento, Sam. Estoy un poco sorprendido de que no haya una validación genérica "suficientemente buena la mayor parte del tiempo" proporcionada por la gema Mail.
JD.
4

En Rails 3 es posible escribir un validador reutilizable , como explica esta gran publicación:

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

y utilícelo con validates_with:

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

Teniendo en cuenta las otras respuestas, la pregunta sigue siendo: ¿por qué molestarse en ser inteligente al respecto?

El volumen real de casos extremos que muchas expresiones regulares pueden negar o pasar por alto parece problemático.

Creo que la pregunta es '¿qué estoy tratando de lograr?', Incluso si 'valida' la dirección de correo electrónico, en realidad no está validando que sea una dirección de correo electrónico que funcione.

Si elige regexp, simplemente verifique la presencia de @ en el lado del cliente.

En cuanto al escenario de correo electrónico incorrecto, tiene una rama de 'error al enviar el mensaje' a su código.

cordero
fuente
1

Básicamente, existen 3 opciones más comunes:

  1. Regexp (no hay regexp de direcciones de correo electrónico que funcione para todos, por lo que debe crear el suyo propio)
  2. Consulta MX (eso es lo que usa)
  3. Generando un token de activación y enviándolo por correo (modo restful_authentication)

Si no desea usar validates_email_veracity_of y token generation, optaría por la verificación de expresiones regulares de la vieja escuela.

Yaroslav
fuente
1

La gema Mail tiene un analizador de direcciones integrado.

begin
  Mail::Address.new(email)
  #valid
rescue Mail::Field::ParseError => e
  #invalid
end
letronje
fuente
No parece funcionar para mí en Rails 3.1. Mail :: Address.new ("john") felizmente me devuelve un nuevo objeto Mail :: Address, sin generar una excepción.
Jasoncrawford
Bien, arrojará una excepción en algunos casos, pero no en todos. El enlace de @ Hallelujah parece tener un buen enfoque aquí.
Jasoncrawford
1

Esta solución se basa en las respuestas de @SFEley y @Alessandro DS, con una refactorización y aclaración de uso.

Puede usar esta clase de validación en su modelo así:

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

Dado que tiene lo siguiente en su app/validatorscarpeta (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
el rey de la verdad
fuente
1

Para validación de listas de correo . (Yo uso Rails 4.1.6)

Obtuve mi expresión regular de aquí . Parece ser muy completo y ha sido probado con un gran número de combinaciones. Puedes ver los resultados en esa página.

Lo cambié ligeramente a una expresión regular de Ruby y lo puse en mi lib/validators/email_list_validator.rb

Aquí está el código:

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

Y lo uso así en el modelo:

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

Validará listas de correo como esta, con diferentes separadores y sintetizadores:

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

Antes de usar esta expresión regular, usé Devise.email_regexp, pero esa es una expresión regular muy simple y no obtuve todos los casos que necesitaba. Algunos correos electrónicos saltaron.

Probé otras expresiones regulares de la web, pero esta obtuvo los mejores resultados hasta ahora. Espero que te ayude en tu caso.

Mauricio Moraes
fuente