/lib/email_validator.rb
Ruby | 164 lines | 113 code | 29 blank | 22 comment | 11 complexity | f4ea2b2ec5fcc5bbd4f5bd5a9192c4db MD5 | raw file
- # Based on work from http://thelucid.com/2010/01/08/sexy-validation-in-edge-rails-rails-3/
- # EmailValidator class
- class EmailValidator < ActiveModel::EachValidator
- # rubocop:disable Style/ClassVars
- @@default_options = {
- :allow_nil => false,
- :domain => nil,
- :require_fqdn => nil,
- :mode => :loose
- }
- # rubocop:enable Style/ClassVars
- # EmailValidator::Error class
- class Error < StandardError
- def initialize(msg = 'EmailValidator error')
- super
- end
- end
- class << self
- def default_options
- @@default_options
- end
- def valid?(value, options = {})
- options = parse_options(options)
- return true if value.nil? && options[:allow_nil] == true
- return false if value.nil?
- !!(value =~ regexp(options))
- end
- def invalid?(value, options = {})
- !valid?(value, options)
- end
- # Refs:
- # https://tools.ietf.org/html/rfc2822 : 3.2. Lexical Tokens, 3.4.1. Addr-spec specification
- # https://tools.ietf.org/html/rfc5321 : 4.1.2. Command Argument Syntax
- def regexp(options = {})
- options = parse_options(options)
- case options[:mode]
- when :loose
- loose_regexp(options)
- when :rfc
- rfc_regexp(options)
- when :strict
- options[:require_fqdn] = true
- strict_regexp(options)
- else
- fail EmailValidator::Error, "Validation mode '#{options[:mode]}' is not supported by EmailValidator"
- end
- end
- protected
- def loose_regexp(options = {})
- return /\A[^\s]+@[^\s]+\z/ if options[:domain].nil?
- /\A[^\s]+@#{domain_part_pattern(options)}\z/
- end
- def strict_regexp(options = {})
- /\A(?>#{local_part_pattern})@#{domain_part_pattern(options)}\z/i
- end
- def rfc_regexp(options = {})
- /\A(?>#{local_part_pattern})(?:@#{domain_part_pattern(options)})?\z/i
- end
- def alpha
- '[[:alpha:]]'
- end
- def alnum
- '[[:alnum:]]'
- end
- def alnumhy
- "(?:#{alnum}|-)"
- end
- def ipv4
- '\d{1,3}(?:\.\d{1,3}){3}'
- end
- def ipv6
- # only supporting full IPv6 addresses right now
- 'IPv6:[[:xdigit:]]{1,4}(?::[[:xdigit:]]{1,4}){7}'
- end
- def address_literal
- "\\[(?:#{ipv4}|#{ipv6})\\]"
- end
- def host_label_pattern
- "#{label_is_correct_length}" \
- "#{label_contains_no_more_than_one_consecutive_hyphen}" \
- "#{alnum}(?:#{alnumhy}{,61}#{alnum})?"
- end
- # splitting this up into separate regex pattern for performance; let's not
- # try the "contains" pattern unless we have to
- def domain_label_pattern
- "#{host_label_pattern}\\.#{tld_label_pattern}"
- end
- # While, techincally, TLDs can be numeric-only, this is not allowed by ICANN
- # Ref: ICANN Application Guidebook for new TLDs (June 2012)
- # says the following starting at page 64:
- #
- # > The ASCII label must consist entirely of letters (alphabetic characters a-z)
- #
- # -- https://newgtlds.icann.org/en/applicants/agb/guidebook-full-04jun12-en.pdf
- def tld_label_pattern
- "#{alpha}{1,64}"
- end
- def label_is_correct_length
- '(?=[^.]{1,63}(?:\.|$))'
- end
- def domain_part_is_correct_length
- '(?=.{1,255}$)'
- end
- def label_contains_no_more_than_one_consecutive_hyphen
- '(?!.*?--.*$)'
- end
- def atom_char
- # The `atext` spec
- # We are looking at this without whitespace; no whitespace support here
- "[-#{alpha}#{alnum}+_!\"'#$%^&*{}/=?`|~]"
- end
- def local_part_pattern
- # the `dot-atom-text` spec, but with a 64 character limit
- "#{atom_char}(?:\\.?#{atom_char}){,63}"
- end
- def domain_part_pattern(options)
- return options[:domain].sub(/\./, '\.') if options[:domain].present?
- return fqdn_pattern if options[:require_fqdn]
- "#{domain_part_is_correct_length}(?:#{address_literal}|(?:#{host_label_pattern}\\.)*#{tld_label_pattern})"
- end
- def fqdn_pattern
- "(?=.{1,255}$)(?:#{host_label_pattern}\\.)*#{domain_label_pattern}"
- end
- private
- def parse_options(options)
- # `:strict` mode enables `:require_fqdn`, unless it is already explicitly disabled
- options[:require_fqdn] = true if options[:require_fqdn].nil? && options[:mode] == :strict
- default_options.merge(options)
- end
- end
- def validate_each(record, attribute, value)
- options = @@default_options.merge(self.options)
- record.errors.add(attribute, options[:message] || :invalid) unless self.class.valid?(value, options)
- end
- end