PageRenderTime 118ms CodeModel.GetById 25ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/email_validator.rb

https://github.com/vjt/email_validator
Ruby | 105 lines | 65 code | 16 blank | 24 comment | 8 complexity | 0e19ad620d3e979aafc7eaf2dc61e919 MD5 | raw file
  1. # encoding: utf-8
  2. require 'active_model'
  3. require 'resolv'
  4. class EmailValidator < ActiveModel::EachValidator
  5. local_part_special_chars = Regexp.escape('!#$%&\'*-/=?+-^_`{|}~')
  6. local_part_unquoted = '(?:[[:alnum:]' + local_part_special_chars + ']+[\.\+])*[[:alnum:]' + local_part_special_chars + '+]'
  7. local_part_quoted = '\"(?:[[:alnum:]' + local_part_special_chars + '\.]|\\\\[\x00-\xFF])*\"'
  8. domain_part = '@(?:(?:(?:\w+\-+[^_])|(?:\w+\.[a-z0-9-]*))*(?:[a-z0-9-]{1,63})\.[a-z]{2,6}(?:\.[a-z]{2,6})?)'
  9. email_address_regexp = '(?:' + local_part_unquoted + '+|' + local_part_quoted + '+)' + domain_part
  10. Pattern = Regexp.new('\A' + email_address_regexp + '\Z', Regexp::EXTENDED | Regexp::IGNORECASE, 'n').freeze
  11. Scanner = Regexp.new( email_address_regexp, Regexp::EXTENDED | Regexp::IGNORECASE, 'n').freeze
  12. Separator = /[;,\s]\s*/.freeze # for multiple e-mail addresses
  13. Defaults = {
  14. :message => I18n.t(:invalid_email_address, :scope => [:activerecord, :errors, :messages], :default => 'does not appear to be a valid e-mail address'),
  15. :multiple_message => I18n.t(:invalid_multiple_email, :scope => [:activerecord, :errors, :messages], :default => 'appears to contain an invalid e-mail address'),
  16. :mx_message => I18n.t(:unroutable_email_address, :scope => [:activerecord, :errors, :messages], :default => 'is not routable'),
  17. :check_mx => false,
  18. :with => Pattern,
  19. :local_length => 64,
  20. :domain_length => 255
  21. }.freeze
  22. # Validates whether the specified value is a valid email address,
  23. # and uses record.errors.add() to add the error if the provided
  24. # value is not valid.
  25. #
  26. # Configuration options:
  27. # * <tt>message</tt> - A custom error message
  28. # (default: "does not appear to be a valid e-mail address")
  29. # * <tt>check_mx</tt> - Check for MX records
  30. # (default: false)
  31. # * <tt>mx_message</tt> - A custom error message when an MX record validation fails
  32. # (default: "is not routable.")
  33. # * <tt>with</tt> - The regex to use for validating the format of the email address
  34. # (default: +Pattern+)</tt>
  35. # * <tt>multiple</tt> - Allow multiple e-mail addresses, separated by +Separator+
  36. # (default: false)
  37. # * <tt>multiple_message</tt> - A custom error message shown when there are 2 or more addresses
  38. # to validate and one or more is invalid
  39. # (default: "appears to contain an invalid e-mail address)
  40. # * <tt>local_length</tt> - Maximum number of characters allowed in the local part
  41. # (default: 64)
  42. # * <tt>domain_length</tt> - Maximum number of characters allowed in the domain part
  43. # (default: 255)
  44. def validate_each(record, attribute, value)
  45. return if value.blank? # Use :presence => true
  46. error = self.class.errors_on(value, self.options)
  47. record.errors.add(attribute, :invalid, :message => error, :value => value) if error
  48. end
  49. class << self
  50. def extract(string)
  51. if string.respond_to?(:encode)
  52. string = string.encode('ascii', :undef => :replace)
  53. end
  54. string.scan(Scanner)
  55. end
  56. def valid?(email, options = {})
  57. errors_on(email, options).nil?
  58. end
  59. def errors_on(email, options)
  60. options = Defaults.merge(options)
  61. options[:multiple] ? validate_many(email, options) : validate_one(email, options)
  62. end
  63. private
  64. def validate_many(value, options)
  65. emails = value.split(Separator)
  66. errors = emails.map {|addr| validate_one(addr, options)}
  67. errors.compact!
  68. options[emails.size == 1 ? :message : :multiple_message] unless errors.empty?
  69. end
  70. def validate_one(value, options)
  71. local, domain = value.split('@', 2)
  72. if local.nil? || local.length > options[:local_length] or
  73. domain.nil? || domain.length > options[:domain_length] or
  74. value !~ options[:with]
  75. options[:message]
  76. elsif options[:check_mx] && !validate_email_domain(domain)
  77. options[:mx_message]
  78. end
  79. end
  80. def validate_email_domain(domain)
  81. Resolv::DNS.open do |dns|
  82. dns.getresources(domain, Resolv::DNS::Resource::IN::MX).size > 0
  83. end
  84. rescue Errno::ECONNREFUSED, NoMethodError
  85. # DNS is not available - thus return true
  86. true
  87. end
  88. end
  89. end