class EmailValidator

EmailValidator class

def address_literal

def address_literal
  "\\[(?:#{ipv4}|#{ipv6})\\]"
end

def alnum

def alnum
  '[[:alnum:]]'
end

def alnumhy

def alnumhy
  "(?:#{alnum}|-)"
end

def alpha

def alpha
  '[[:alpha:]]'
end

def atom_char

def atom_char
  # The `atext` spec
  # We are looking at this without whitespace; no whitespace support here
  "[-#{alpha}#{alnum}+_!\"'#$%^&*{}/=?`|~]"
end

def default_options

def default_options
  @@default_options
end

def domain_label_pattern

try the "contains" pattern unless we have to
splitting this up into separate regex pattern for performance; let's not
def domain_label_pattern
  "#{host_label_pattern}\\.#{tld_label_pattern}"
end

def domain_part_is_correct_length

def domain_part_is_correct_length
  '(?=.{1,255}$)'
end

def domain_part_pattern(options)

def domain_part_pattern(options)
  return regexp_safe_domain(options) unless options[:domain].nil?
  return fqdn_pattern if options[:require_fqdn]
  "#{domain_part_is_correct_length}(?:#{address_literal}|(?:#{host_label_pattern}\\.)*#{tld_label_pattern})"
end

def fqdn_pattern

def fqdn_pattern
  "(?=.{1,255}$)(?:#{host_label_pattern}\\.)*#{domain_label_pattern}"
end

def host_label_pattern

def host_label_pattern
  "#{label_is_correct_length}" \
  "#{alnum}(?:#{alnumhy}{,61}#{alnum})?"
end

def invalid?(value, options = {})

def invalid?(value, options = {})
  !valid?(value, options)
end

def ipv4

def ipv4
  '\d{1,3}(?:\.\d{1,3}){3}'
end

def ipv6

def ipv6
  # only supporting full IPv6 addresses right now
  'IPv6:[[:xdigit:]]{1,4}(?::[[:xdigit:]]{1,4}){7}'
end

def label_is_correct_length

def label_is_correct_length
  '(?=[^.]{1,63}(?:\.|$))'
end

def local_part_pattern

def local_part_pattern
  # the `dot-atom-text` spec, but with a 64 character limit
  "#{atom_char}(?:\\.?#{atom_char}){,63}"
end

def loose_regexp(options = {})

def loose_regexp(options = {})
  return /\A[^\s]+@[^\s]+\z/ if options[:domain].nil?
  /\A[^\s]+@#{domain_part_pattern(options)}\z/
end

def parse_options(options)

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

def regexp(options = {})

https://tools.ietf.org/html/rfc5321 : 4.1.2. Command Argument Syntax
https://tools.ietf.org/html/rfc2822 : 3.2. Lexical Tokens, 3.4.1. Addr-spec specification
Refs:
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

def regexp_safe_domain(options)

def regexp_safe_domain(options)
  options[:domain].sub(/\./, '\.')
end

def rfc_regexp(options = {})

def rfc_regexp(options = {})
  /\A(?>#{local_part_pattern})(?:@#{domain_part_pattern(options)})?\z/i
end

def strict_regexp(options = {})

def strict_regexp(options = {})
  /\A(?>#{local_part_pattern})@#{domain_part_pattern(options)}\z/i
end

def tld_label_pattern

-- https://newgtlds.icann.org/en/applicants/agb/guidebook-full-04jun12-en.pdf

> The ASCII label must consist entirely of letters (alphabetic characters a-z)

says the following starting at page 64:
Ref: ICANN Application Guidebook for new TLDs (June 2012)
While, techincally, TLDs can be numeric-only, this is not allowed by ICANN
def tld_label_pattern
  "#{alpha}{1,64}"
end

def valid?(value, options = {})

def valid?(value, options = {})
  options = parse_options(options)
  return true if value.nil? && options[:allow_nil] == true
  return false if value.nil?
  # quickly fail if domain is required but doesn't match
  return false unless options[:domain].nil? || value[/^.*@#{regexp_safe_domain(options)}$/]
  !!(value =~ regexp(options))
end

def validate_each(record, attribute, value)

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