Qual é o estado da arte em validação de email para Rails?

95

O que você está usando para validar os endereços de e-mail dos usuários e por quê?

Eu estava usando o validates_email_veracity_ofque realmente consulta os servidores MX. Mas isso está cheio de falhas por vários motivos, principalmente relacionados ao tráfego de rede e à confiabilidade.

Olhei em volta e não consegui encontrar nada óbvio que muitas pessoas estão usando para realizar uma verificação de sanidade em um endereço de e-mail. Existe um plugin ou gem mantido e razoavelmente preciso para isso?

PS: Por favor, não me diga para enviar um e-mail com um link para ver se o e-mail funciona. Estou desenvolvendo um recurso "enviar para um amigo", então isso não é prático.

Luke Francl
fonte
Esta é uma maneira super fácil, sem lidar com regex: detecting-a-valid-email-address
Zabba
Você poderia dar um motivo mais detalhado do porque a consulta ao servidor MX falhou? Gostaria de saber se isso pode ser corrigido.
lulalala

Respostas:

67

Com Rails 3.0 você pode usar uma validação de email sem regexp usando a gem Mail .

Aqui está minha implementação ( empacotada como uma gema ).

Aleluia
fonte
Legal, estou usando sua gema. Obrigado.
jasoncrawford de
parece que ###@domain.comvai validar?
cwd
1
Pessoal, gostaria de reviver esta joia, não tive tempo de mantê-la. Mas parece que as pessoas ainda usam e procuram melhorias. Se você estiver interessado, escreva-me no projeto do github: hallelujah / valid_email
Hallelujah
106

Não torne isso mais difícil do que o necessário. Seu recurso não é crítico; a validação é apenas uma etapa de sanidade básica para detectar erros de digitação. Eu faria isso com um regex simples e não desperdiçaria os ciclos da CPU com nada muito complicado:

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

Isso foi adaptado de http://www.regular-expressions.info/email.html - que você deve ler se realmente quiser saber todas as vantagens e desvantagens. Se você deseja um regex totalmente compatível com RFC822 mais correto e muito mais complicado, isso também está nessa página. Mas a questão é a seguinte: você não precisa acertar totalmente.

Se o endereço passar na validação, você enviará um e-mail. Se o e-mail falhar, você receberá uma mensagem de erro. Nesse ponto, você pode dizer ao usuário "Desculpe, seu amigo não recebeu isso, gostaria de tentar novamente?" ou sinalize-o para revisão manual, ou simplesmente ignore-o, ou o que for.

Essas são as mesmas opções com as quais você teria que lidar se o endereço fosse aprovado na validação. Porque mesmo que sua validação seja perfeita e você adquira a prova absoluta de que o endereço existe, o envio pode falhar

O custo de um falso positivo na validação é baixo. O benefício de uma melhor validação também é baixo. Valide generosamente e se preocupe com os erros quando eles acontecerem.

SFEley
fonte
36
Err, isso não vai vomitar no .museum e nos novos TLDs internacionais? Este regex impediria muitos endereços de e-mail válidos.
Elijah
3
Concordo com Elijah, esta é uma recomendação ruim. Além disso, não tenho certeza de como você acha que pode dizer ao usuário que seu amigo não recebeu o e-mail porque não há como saber se o e-mail foi bem-sucedido imediatamente.
Jaryl
8
Bom ponto sobre .museum e tal - quando postei essa resposta pela primeira vez em 2009, não foi um problema. Eu alterei o regex. Se você tiver mais melhorias, também pode editá-lo ou torná-lo uma postagem na wiki da comunidade.
SFEley de
5
Para sua informação, ainda faltarão alguns endereços de e-mail válidos. Não muitos, mas alguns. Por exemplo, tecnicamente #|@foo.com é um endereço de e-mail válido, como "Ei, posso colocar espaços se estiverem entre aspas" @ foo.com. Acho mais fácil simplesmente ignorar qualquer coisa antes de @ e validar apenas a parte do domínio.
Nerdmaster
6
Concordo com a motivação de que você não deve se preocupar em permitir a passagem de alguns endereços incorretos. Infelizmente, esta regex não permitirá alguns endereços corretos, que considero inaceitáveis. Talvez algo assim fosse melhor? /.+@.+\..+/
ZoFreX
12

Eu criei uma gem para validação de email no Rails 3. Estou meio surpreso que o Rails não inclua algo assim por padrão.

http://github.com/balexand/email_validator

Balexand
fonte
8
Este é essencialmente um invólucro em torno da regex.
Rob Dawson
Você pode dar um exemplo de como usar isso com uma instrução ifou unless? A documentação parece esparsa.
cwd
@cwd Acho que a documentação está completa. Se você não está familiarizado com as validações Rails 3+, verifique este Railscasts ( railscasts.com/episodes/211-validations-in-rails-3 ) ou guias.rubyonrails.org/active_record_validations.html
balexand
7

Dos documentos do 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
fonte
5

No Rails 4, simplesmente adicione validates :email, email:true(assumindo que seu campo seja chamado email) ao seu modelo e então escreva um † simples (ou complexo) EmailValidatorpara atender às suas necessidades.

por exemplo: - seu modelo:

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

Seu 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

Isso permitirá todos os tipos de e-mails válidos, incluindo e-mails marcados como "[email protected]" e assim por diante.

Para testar isso rspecem seuspec/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

É assim que eu fiz de qualquer maneira. YMMV

† Expressões regulares são como violência; se eles não funcionarem, você não está usando o suficiente.

Dave Sag
fonte
1
Estou tentado a usar sua validação, mas não tenho ideia de onde você a tirou ou como a fez. Você pode nos dizer?
Mauricio Moraes
Peguei a expressão regular de uma pesquisa no Google e escrevi o código do wrapper e os testes de especificação sozinho.
Dave Sag
1
É ótimo que você tenha postado os testes também! Mas o que realmente me pegou foi a citação de poder lá em cima! :)
Mauricio Moraes
4

Como sugere Hallelujah , acho que usar a gema Mail é uma boa abordagem. No entanto, não gosto de alguns dos aros lá.

Eu uso:

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

Você poderia ser mais estrito exigindo que os TLDs (domínios de nível superior) estejam nesta lista ; no entanto, você seria forçado a atualizar essa lista à medida que novos TLDs surgissem (como a adição de 2012 .mobie .tel)

A vantagem de conectar o analisador direto é que as regras na gramática do Mail são bastante amplas para as partes que a gem do Mail usa, e é projetado para permitir que analise um endereço como o user<[email protected]>que é comum para SMTP. Ao consumi-lo do, Mail::Addressvocê é forçado a fazer várias verificações extras.

Outra observação a respeito da gema Mail, embora a classe se chame RFC2822, a gramática possui alguns elementos da RFC5322 , por exemplo este teste .

Sam Saffron
fonte
1
Obrigado por este trecho, Sam. Estou um pouco surpreso que não haja uma validação genérica "boa o suficiente na maioria das vezes" fornecida pela gem Mail.
JD.
4

No Rails 3 é possível escrever um validador reutilizável , como explica este ótimo post:

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

e usá-lo com validates_with:

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

Observando as outras respostas, a pergunta ainda permanece - por que se preocupar em ser inteligente sobre isso?

O volume real de casos extremos que muitos regex podem negar ou ignorar parece problemático.

Acho que a questão é 'o que estou tentando fazer?', Mesmo que você 'valide' o endereço de e-mail, na verdade não está validando se é um endereço de e-mail válido.

Se você for para regexp, apenas verifique a presença de @ no lado do cliente.

Quanto ao cenário de e-mail incorreto, tenha um branch 'mensagem falhou ao enviar' para o seu código.

carneiro
fonte
1

Existem basicamente 3 opções mais comuns:

  1. Regexp (não existe regexp de endereços de e-mail que funciona para todos, então crie a sua própria)
  2. Consulta MX (é o que você usa)
  3. Gerar um token de ativação e enviá-lo (forma restful_authentication)

Se você não quiser usar validates_email_veracity_of e geração de token, eu escolheria a verificação de expressão regular da velha escola.

Yaroslav
fonte
1

A gem Mail tem um analisador de endereços embutido.

begin
  Mail::Address.new(email)
  #valid
rescue Mail::Field::ParseError => e
  #invalid
end
Letronje
fonte
Não parece funcionar para mim no Rails 3.1. Mail :: Address.new ("john") felizmente me retorna um novo objeto Mail :: Address, sem levantar uma exceção.
jasoncrawford de
OK, ele lançará uma exceção em alguns casos, mas não em todos. O link de @Aleluia parece ter uma boa abordagem aqui.
jasoncrawford de
1

Esta solução é baseada nas respostas de @SFEley e @Alessandro DS, com refatoração e esclarecimento de uso.

Você pode usar esta classe validadora em seu modelo da seguinte maneira:

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

Dado que você tem o seguinte em sua app/validatorspasta (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
o reinado da verdade
fonte
1

Para validação de listas de discussão . (Eu uso Rails 4.1.6)

Peguei meu regexp daqui . Parece ser muito completo e foi testado em um grande número de combinações. Você pode ver os resultados nessa página.

Eu mudei ligeiramente para uma regexp Ruby e coloquei no meu lib/validators/email_list_validator.rb

Aqui está o 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

E eu uso assim no modelo:

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

Ele validará listas de e-mail como esta, com diferentes separadores e sintaxe:

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

Antes de usar esta regexp, eu usei Devise.email_regexp, mas é uma regexp muito simples e não obtive todos os casos que precisava. Alguns e-mails falharam.

Tentei outras expressões regulares da web, mas esta obteve os melhores resultados até agora. Espero que ajude no seu caso.

Mauricio Moraes
fonte