Pull to refresh

Ruby: про email вообще и ради валидации без регекспов, в частности

Reading time3 min
Views6.9K
image

Привет Хабр!

Немного наблюдений.



По предложенному вопросу пергамента исписано непозволительно много. Тем не менее, я бы хотел остановится на трех важных, но игнорируемых аттрибутах, свойственных email-у, с точки web-разработки.
Во первых email уникален, в отличии от никнейма, который, в половине случаев, занят кем-то до нас. Однако все еще встречаются сайты с логином по никнейму, который, для всех таких сайтов, ну никак не упомнить. Предлагаю использовать для логина только email.
Во вторых, часть разработчиков игнорирует type='email', когда JS валидаторы натравлены на это поле, и планшетные устройства переключают раскладку, что удобно.
В третьих, ради чего это статья, каждый год пишутся статьи вида «Почему плохо валидировать регекспом», что больше похоже на фетиш. Надеюсь гугл проиндексирует верно.

Собственно вся преамбула ради одного интересного решения, которое должно помочь Ruby-стам, не исключено, что в других языках подобный метод тоже реализуем.



Собственно, когда проблема первый раз сформулировалась в моей голове как проблема, то я воспользовался гуглом, и в два-три клика наткнулся на решение, которым пользуюсь до сих пор. Важно: решение нагло стырено и выдается за свое, и если кто-то найдет ссылку на первоисточник, с радостью вставлю в статью.

Первый показатель валидности



gem 'mail' древняя как мир рубишная библиотека
require 'mail'
mail = Mail::Addres.new('antiqe@gmail.com')
mail.local #antiqe
mail.domain #gmail.com

Это первое что нужно сделать
Если в адресе почты содержатся недопустимые символы, то библиотека вызовет exception, который нужно поймать. Однако, нам ничего не ясно про домен, и домен тут может быть инвалидным.

Второй показатель валидности



mail = Mail::Address.new('antiqe@gmail.com')
 => #<Mail::Address:72490440 Address: |antiqe@gmail.com| > 

tree = mail.__send__(:tree)
 => SyntaxNode+Address1+AddrSpec0 offset=0, "antiqe@gmail.com" (dig_comments,comments,local_part,domain):
  SyntaxNode+LocalDotAtom0 offset=0, "antiqe" (local_dot_atom_text):
    SyntaxNode+CFWS1 offset=0, "":
      SyntaxNode offset=0, ""
      SyntaxNode offset=0, ""
    SyntaxNode offset=0, "antiqe":
      SyntaxNode+LocalDotAtomText0 offset=0, "antiqe" (domain_text):
        SyntaxNode offset=0, ""
        SyntaxNode offset=0, "antiqe":
          SyntaxNode offset=0, "a"
          SyntaxNode offset=1, "n"
          SyntaxNode offset=2, "t"
          SyntaxNode offset=3, "i"
          SyntaxNode offset=4, "q"
          SyntaxNode offset=5, "e"
    SyntaxNode+CFWS1 offset=6, "":
      SyntaxNode offset=6, ""
      SyntaxNode offset=6, ""
  SyntaxNode offset=6, "@"
  SyntaxNode+DotAtom0 offset=7, "gmail.com" (dot_atom_text):
    SyntaxNode+CFWS1 offset=7, "":
      SyntaxNode offset=7, ""
      SyntaxNode offset=7, ""
    SyntaxNode offset=7, "gmail.com":
      SyntaxNode+DotAtomText0 offset=7, "gmail." (domain_text):
        SyntaxNode offset=7, "gmail":
          SyntaxNode offset=7, "g"
          SyntaxNode offset=8, "m"
          SyntaxNode offset=9, "a"
          SyntaxNode offset=10, "i"
          SyntaxNode offset=11, "l"
        SyntaxNode offset=12, "."
      SyntaxNode+DotAtomText0 offset=13, "com" (domain_text):
        SyntaxNode offset=13, "com":
          SyntaxNode offset=13, "c"
          SyntaxNode offset=14, "o"
          SyntaxNode offset=15, "m"
        SyntaxNode offset=16, ""
    SyntaxNode+CFWS1 offset=16, "":
      SyntaxNode offset=16, ""
      SyntaxNode offset=16, "" 


У нас тут синтаксическое дерево, природа и свойства которого, для меня, чуть менее чем полностью непостижимы. Знаю только то, что синтаксическое дерево, в отличии от регекспа не рекурсивно по своей природе.
Можно только предположить, что создатели библиотеки, что-то знали, но сказали не всем.
Это дерево дает нам возможность ответить на один важный вопрос: из скольких элементов состоит домен. И если таких элементов более одного — домен валиден.

Частная реализация



В рельсах достаточно вот это:

require 'mail'
class EmailValidator < ActiveModel::EachValidator
  def validate_each(record,attribute,value)
    begin
      address = Mail::Address.new(value)
      result = address.domain && address.address == value # правда, что то что распарсил Mail в сумме вернет нам обратно наш email
      tree = address.__send__(:tree)
      result &&= (tree.domain.dot_atom_text.elements.size > 1) # правда ли и то, что елементов в домене больше одного
    rescue Exception => e # ловим  исключение, если в адресе русские или китайские символы
      result = false
    end
    record.errors[attribute] << (options[:message] || "is invalid") unless result
  end
end


положить в app/validators/email_validator.rb, чтобы в любой модели использовать:

validates :email, :presence => true, :email => true


Ложноинвалидных или ложноположительных адресов за более чем два года не выявлено.
Больше сказать нечего.
Всем добра.
Tags:
Hubs:
+4
Comments31

Articles