PageRenderTime 45ms CodeModel.GetById 17ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/email_validator.rb

https://github.com/balexand/email_validator
Ruby | 164 lines | 113 code | 29 blank | 22 comment | 11 complexity | f4ea2b2ec5fcc5bbd4f5bd5a9192c4db MD5 | raw file
  1. # Based on work from http://thelucid.com/2010/01/08/sexy-validation-in-edge-rails-rails-3/
  2. # EmailValidator class
  3. class EmailValidator < ActiveModel::EachValidator
  4. # rubocop:disable Style/ClassVars
  5. @@default_options = {
  6. :allow_nil => false,
  7. :domain => nil,
  8. :require_fqdn => nil,
  9. :mode => :loose
  10. }
  11. # rubocop:enable Style/ClassVars
  12. # EmailValidator::Error class
  13. class Error < StandardError
  14. def initialize(msg = 'EmailValidator error')
  15. super
  16. end
  17. end
  18. class << self
  19. def default_options
  20. @@default_options
  21. end
  22. def valid?(value, options = {})
  23. options = parse_options(options)
  24. return true if value.nil? && options[:allow_nil] == true
  25. return false if value.nil?
  26. !!(value =~ regexp(options))
  27. end
  28. def invalid?(value, options = {})
  29. !valid?(value, options)
  30. end
  31. # Refs:
  32. # https://tools.ietf.org/html/rfc2822 : 3.2. Lexical Tokens, 3.4.1. Addr-spec specification
  33. # https://tools.ietf.org/html/rfc5321 : 4.1.2. Command Argument Syntax
  34. def regexp(options = {})
  35. options = parse_options(options)
  36. case options[:mode]
  37. when :loose
  38. loose_regexp(options)
  39. when :rfc
  40. rfc_regexp(options)
  41. when :strict
  42. options[:require_fqdn] = true
  43. strict_regexp(options)
  44. else
  45. fail EmailValidator::Error, "Validation mode '#{options[:mode]}' is not supported by EmailValidator"
  46. end
  47. end
  48. protected
  49. def loose_regexp(options = {})
  50. return /\A[^\s]+@[^\s]+\z/ if options[:domain].nil?
  51. /\A[^\s]+@#{domain_part_pattern(options)}\z/
  52. end
  53. def strict_regexp(options = {})
  54. /\A(?>#{local_part_pattern})@#{domain_part_pattern(options)}\z/i
  55. end
  56. def rfc_regexp(options = {})
  57. /\A(?>#{local_part_pattern})(?:@#{domain_part_pattern(options)})?\z/i
  58. end
  59. def alpha
  60. '[[:alpha:]]'
  61. end
  62. def alnum
  63. '[[:alnum:]]'
  64. end
  65. def alnumhy
  66. "(?:#{alnum}|-)"
  67. end
  68. def ipv4
  69. '\d{1,3}(?:\.\d{1,3}){3}'
  70. end
  71. def ipv6
  72. # only supporting full IPv6 addresses right now
  73. 'IPv6:[[:xdigit:]]{1,4}(?::[[:xdigit:]]{1,4}){7}'
  74. end
  75. def address_literal
  76. "\\[(?:#{ipv4}|#{ipv6})\\]"
  77. end
  78. def host_label_pattern
  79. "#{label_is_correct_length}" \
  80. "#{label_contains_no_more_than_one_consecutive_hyphen}" \
  81. "#{alnum}(?:#{alnumhy}{,61}#{alnum})?"
  82. end
  83. # splitting this up into separate regex pattern for performance; let's not
  84. # try the "contains" pattern unless we have to
  85. def domain_label_pattern
  86. "#{host_label_pattern}\\.#{tld_label_pattern}"
  87. end
  88. # While, techincally, TLDs can be numeric-only, this is not allowed by ICANN
  89. # Ref: ICANN Application Guidebook for new TLDs (June 2012)
  90. # says the following starting at page 64:
  91. #
  92. # > The ASCII label must consist entirely of letters (alphabetic characters a-z)
  93. #
  94. # -- https://newgtlds.icann.org/en/applicants/agb/guidebook-full-04jun12-en.pdf
  95. def tld_label_pattern
  96. "#{alpha}{1,64}"
  97. end
  98. def label_is_correct_length
  99. '(?=[^.]{1,63}(?:\.|$))'
  100. end
  101. def domain_part_is_correct_length
  102. '(?=.{1,255}$)'
  103. end
  104. def label_contains_no_more_than_one_consecutive_hyphen
  105. '(?!.*?--.*$)'
  106. end
  107. def atom_char
  108. # The `atext` spec
  109. # We are looking at this without whitespace; no whitespace support here
  110. "[-#{alpha}#{alnum}+_!\"'#$%^&*{}/=?`|~]"
  111. end
  112. def local_part_pattern
  113. # the `dot-atom-text` spec, but with a 64 character limit
  114. "#{atom_char}(?:\\.?#{atom_char}){,63}"
  115. end
  116. def domain_part_pattern(options)
  117. return options[:domain].sub(/\./, '\.') if options[:domain].present?
  118. return fqdn_pattern if options[:require_fqdn]
  119. "#{domain_part_is_correct_length}(?:#{address_literal}|(?:#{host_label_pattern}\\.)*#{tld_label_pattern})"
  120. end
  121. def fqdn_pattern
  122. "(?=.{1,255}$)(?:#{host_label_pattern}\\.)*#{domain_label_pattern}"
  123. end
  124. private
  125. def parse_options(options)
  126. # `:strict` mode enables `:require_fqdn`, unless it is already explicitly disabled
  127. options[:require_fqdn] = true if options[:require_fqdn].nil? && options[:mode] == :strict
  128. default_options.merge(options)
  129. end
  130. end
  131. def validate_each(record, attribute, value)
  132. options = @@default_options.merge(self.options)
  133. record.errors.add(attribute, options[:message] || :invalid) unless self.class.valid?(value, options)
  134. end
  135. end