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
|
require 'mail'
require 'simpleidn'
class ValidateEmail
class << self
SPECIAL_CHARS = %w(( ) , : ; < > @ [ ])
SPECIAL_ESCAPED_CHARS = %w(\ \\ ")
LOCAL_MAX_LEN = 64
DOMAIN_REGEX = /\A(?:[[:alnum:]](?:[[[:alnum:]]-]{0,61}[[:alnum:]])?)(?:\.(?:[[:alnum:]](?:[[[:alnum:]]-]{0,61}[[:alnum:]])?))+\z/
def valid?(value, user_options={})
options = {
:mx => false,
:domain => true,
:message => nil
}.merge(user_options)
m = Mail::Address.new(value)
# We must check that value contains a domain and that value is an email address
return false unless m.domain && m.address == value
# Check that domain consists of dot-atom-text elements > 1 and does not
# contain spaces.
#
# In mail 2.6.1, domains are invalid per rfc2822 are parsed when they shouldn't
# This is to make sure we cover those cases
return false unless m.domain.match(/^\S+$/)
domain_dot_elements = m.domain.split(/\./)
return false unless domain_dot_elements.size > 1 && domain_dot_elements.none?(&:empty?)
# Ensure that the local segment adheres to adheres to RFC-5322
return false unless valid_local?(m.local)
# Check if domain has DNS MX record
if options[:mx]
require 'valid_email/mx_validator'
return mx_valid?(value)
end
if options[:domain]
require 'valid_email/domain_validator'
return domain_valid?(value)
end
true
rescue Mail::Field::ParseError
false
rescue ArgumentError => error
if error.message == 'bad value for range'
false
else
raise error
end
end
def valid_local?(local)
return false unless local.length <= LOCAL_MAX_LEN
# Emails can be validated by segments delineated by '.', referred to as dot atoms.
# See http://tools.ietf.org/html/rfc5322#section-3.2.3
return local.split('.', -1).all? { |da| valid_dot_atom?(da) }
end
def valid_dot_atom?(dot_atom)
# Leading, trailing, and double '.'s aren't allowed
return false if dot_atom.empty?
# A dot atom can be quoted, which allows use of the SPECIAL_CHARS
if dot_atom[0] == '"' || dot_atom[-1] == '"'
# A quoted segment must have leading and trailing '"#"'s
return false if dot_atom[0] != dot_atom[-1]
# Excluding the bounding quotes, all of the SPECIAL_ESCAPED_CHARS must have a leading '\'
index = dot_atom.length - 2
while index > 0
if SPECIAL_ESCAPED_CHARS.include? dot_atom[index]
return false if index == 1 || dot_atom[index - 1] != '\\'
# On an escaped special character, skip an index to ignore the '\' that's doing the escaping
index -= 1
end
index -= 1
end
else
# If we're not in a quoted dot atom then no special characters are allowed.
return false unless ((SPECIAL_CHARS | SPECIAL_ESCAPED_CHARS) & dot_atom.split('')).empty?
end
return true
end
def mx_valid?(value, fallback=false)
m = Mail::Address.new(value)
return false unless m.domain
if m.domain.ascii_only?
ascii_domain = m.domain
else
ascii_domain = SimpleIDN.to_ascii(m.domain)
end
Resolv::DNS.open do |dns|
dns.timeouts = MxValidator.config[:timeouts] unless MxValidator.config[:timeouts].empty?
return dns.getresources(ascii_domain, Resolv::DNS::Resource::IN::MX).size > 0 || (
fallback && dns.getresources(ascii_domain, Resolv::DNS::Resource::IN::A).size > 0
)
end
rescue Mail::Field::ParseError
false
end
def mx_valid_with_fallback?(value)
mx_valid?(value, true)
end
def domain_valid?(value)
m = Mail::Address.new(value)
return false unless m.domain
!(m.domain =~ DOMAIN_REGEX).nil?
rescue Mail::Field::ParseError
false
end
def ban_disposable_email?(value)
m = Mail::Address.new(value)
m.domain && !BanDisposableEmailValidator.config.include?(m.domain)
rescue Mail::Field::ParseError
false
end
end
end
|