2009-07-20 9 views
95

¿Qué está utilizando para validar las direcciones de correo electrónico de los usuarios, y por qué?¿Cuál es el estado del arte en la validación de correo electrónico para Rails?

He estado usando validates_email_veracity_of que realmente consulta los servidores MX. Pero eso está lleno de fallas por varias razones, principalmente relacionadas con el tráfico de red y la confiabilidad.

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

P.S .: No me pidas que envíe un correo electrónico con un enlace para ver si el correo electrónico funciona. Estoy desarrollando una función de "enviar a un amigo", así que esto no es práctico.

+0

Aquí hay una manera súper fácil, sin tener que lidiar con regex: [detectando una dirección de correo electrónico válida] (http: // lindsaar.net/2008/4/14/tip-4-detecting-a-valid-email-address) – Zabba

+0

¿Podría darnos una razón más detallada de por qué falla la consulta del servidor MX? Me gustaría saber para poder ver si son corregibles. – lulalala

Respuesta

67

Con Rails 3.0 puede usar una validación de correo electrónico sin regexp usando el Mail gem.

Aquí está my implementation (packaged as a gem).

+0

Bien, estoy usando tu joya. Gracias. – jasoncrawford

+0

parece que '### @ domain.com' se validará? – cwd

+1

Chicos Me gustaría revivir esta joya, no tuve tiempo para mantenerla. Pero parece que las personas todavía lo usan y buscan mejoras. Si está interesado, escríbame en el proyecto github: hallelujah/valid_email – Hallelujah

1

Hay básicamente 3 opciones más comunes:

  1. Regexp (no hay una dirección expreg obras-de-todo el correo electrónico, así que hágalo usted mismo)
  2. consulta MX (que es lo que se utiliza)
  3. generar un símbolo de activación y enviarlo por correo (modo restful_authentication)

Si no desea utilizar tanto validates_email_veracity_of y generación de tokens, me gustaría ir con el viejo comprobación de expresiones regulares de la escuela.

106

No lo haga 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 perder los ciclos de CPU en algo demasiado complicado:

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

que fue adaptado a partir http://www.regular-expressions.info/email.html - que debería leer si realmente quieres conocer todas las ventajas y desventajas. Si quieres una expresión regular más correcta y mucho más complicada que cumpla con RFC822, también está en esa página. Pero el problema es este: no tiene que hacerlo del todo bien.

Si la dirección pasa la validación, usted va a 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 una revisión manual, o simplemente ignorarlo, o lo que sea.

Estas son las mismas opciones que tendrías que tratar si la dirección hiciera validacion de pase. Porque incluso si su validación es perfecta y adquiere una prueba absoluta de que la dirección existe, el envío 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. Valida generosamente y preocúpate por los errores cuando suceden.

+35

Err, ¿no es eso una barf en .museum y los nuevos TLDs 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 crees que puedes 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 en **. Museum ** y tal, cuando publiqué esa respuesta por primera vez en 2009, no fue un problema. He alterado la expresión regular. Si tiene otras mejoras, puede editarlas también, o hacer que esta sea una publicación wiki de la comunidad. – SFEley

6

actualización: TMail ha sido reemplazado por el Mail gem

Como SFEley dijo que depende de lo profundo que desea ser.En la mayoría de los casos, su expresión regular es suficiente. Solo uso la biblioteca TMail de ruby ​​para validar cualquier dirección de correo electrónico legal, quizás a expensas de algunos ciclos de CPU.

begin 
    TMail::Address.parse(email_address) 
    return true 
rescue 
    return false 
end 
+0

Como nota, la gema TMail ahora está reemplazada por la gema Mail (del mismo autor). – lulalala

12

Creé una joya para la validación de correo electrónico en Rails 3. Estoy un poco sorprendido de que Rails no incluya algo así por defecto.

http://github.com/balexand/email_validator

+8

Esto es esencialmente una envoltura alrededor de la expresión regular. –

+0

¿Puedes dar un ejemplo de cómo usar esto con una instrucción 'if' o' unless'? La documentación parece escasa. – cwd

+0

@cwd Creo que la documentación está completa. Si no está familiarizado con las validaciones de Rails 3+, consulte este Railscast (http://railscasts.com/episodes/211-validations-in-rails-3) o http://guides.rubyonrails.org/active_record_validations .html – balexand

1

La gema Mail ha construido en un analizador de direcciones.

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

Parece que no funciona en Rails 3.1. Mail :: Address.new ("john") felizmente me devuelve un nuevo objeto Mail :: Address, sin generar una excepción. – jasoncrawford

+0

Bien, emitirá una excepción en algunos casos, pero no en todos. El enlace de @ Hallelujah parece tener un buen enfoque aquí. – jasoncrawford

4

En Rails 3 que es posible escribir un reutilizable validador, ya que este gran post explica:

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 utilizarlo con validates_with:

class User < ActiveRecord::Base 
    validates_with EmailValidator 
end 
4

Como sugiere Hallelujah Creo utilizando el Mail gem es un buen enfoque. Sin embargo, no me gustan algunos de los aros de allí.

que utilizo:

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 ([email protected]) 
    # The Grammar is: (local_part "@" domain)/local_part ... discard latter 
    result && 
    result.respond_to?(:domain) && 
    result.domain.dot_atom_text.elements.size > 1 
end 

Usted podría ser más estrictos al exigir que los dominios de nivel superior (dominios de nivel superior) están en this list, sin embargo, se vería obligado a actualizar dicha lista como nuevos TLD pop-up (como el 2012 Además .mobi y .tel)

la ventaja de enganchar el analizador directa es que la rules in Mail grammar son bastante amplia para las porciones utiliza la gema mail, que está diseñado para permitir que se analiza una dirección como user<[email protected]> que es común para SMTP. Al consumirlo desde el Mail::Address, se ve obligado a hacer un montón de controles adicionales.

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

+1

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

1

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

Puede utilizar esta clase de validador en su modelo de este modo:

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

Dado que tiene lo siguiente en su app/validators carpeta (carriles 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._%+-][email protected][A-Za-z0-9.-]+\.[A-Za-z]+\z/.match(value).nil? 
     false 
    else 
     true 
    end 

    end 
end 
3

Observando las otras respuestas, la Todavía queda una pregunta: ¿por qué molestarse en ser inteligente al respecto?

El volumen real de casos extremos que muchos regex pueden negar o perder 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 se trata de una dirección de correo electrónico que funcione.

Si elige la expresión regular, simplemente verifique la presencia de @ en el lado del cliente.

En cuanto al escenario de correo electrónico incorrecto, tenga una rama 'mensaje no enviado' a su código.

7

Desde el Rails 4 docs:

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

En los carriles 4, simplemente añadir validates :email, email:true (asumiendo que su campo se llama email) para su modelo y luego escribir un simple (o complejo †) EmailValidator para satisfacer sus necesidades.

por ejemplo: - su modelo:

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

Su validador (va en 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 mensajes de correo electrónico válidas, incluyendo etiquetados mensajes de correo electrónico como "prueba + no_really @ test.tes "y así sucesivamente.

Para probar esto con rspec en su spec/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ás usando suficientes.

+1

Estoy tentado de utilizar su validación, pero no tengo ni idea de dónde lo obtuvo o cómo lo hizo. ¿Puedes decirnos? –

+0

Obtuve la expresión regular de una búsqueda en Google y escribí el código del contenedor y las pruebas de especificación por mi cuenta. –

+1

¡Es fantástico que hayas publicado las pruebas también! Pero lo que realmente me atrapó fue el poder: ¡citar allá arriba! :) –

1

Para Listas de correo Validación. (Uso Rails 4.1.6)

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

he cambiado ligeramente a una expresión regular 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 como esto en el modelo:

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

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

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 chocaron.

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

Cuestiones relacionadas