1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165
|
# 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?
# 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 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}" \
"#{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 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 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
"(?=.{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
def regexp_safe_domain(options)
options[:domain].sub(/\./, '\.')
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
|