Module: ValidatesEmailFormatOf
- Defined in:
- lib/validates_email_format_of.rb,
lib/validates_email_format_of/railtie.rb,
lib/validates_email_format_of/version.rb
Defined Under Namespace
Classes: Railtie
Constant Summary collapse
- ATEXT =
Characters that are allowed in to appear in the local part unquoted www.rfc-editor.org/rfc/rfc5322#section-3.2.3
An addr-spec is a specific Internet identifier that contains a locally interpreted string followed by the at-sign character (“@”, ASCII value 64) followed by an Internet domain. The locally interpreted string is either a quoted-string or a dot-atom. If the string can be represented as a dot-atom (that is, it contains no characters other than atext characters or “.” surrounded by atext characters), then the dot-atom form SHOULD be used and the quoted- string form SHOULD NOT be used. Comments and folding white space SHOULD NOT be used around the “@” in the addr-spec.
atext = ALPHA / DIGIT / "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / "-" / "/" / "=" / "?" / "^" / "_" / "`" / "{" / "|" / "}" / "~" dot-atom-text = 1*atext *("." 1*atext) dot-atom = [CFWS] dot-atom-text [CFWS] /\A[A-Z0-9!\#$%&'*\-\/=?+\^_`{|}~]\z/i- CTEXT =
Characters that are allowed to appear unquoted in comments www.rfc-editor.org/rfc/rfc5322#section-3.2.2
ctext = %d33-39 / %d42-91 / %d93-126 ccontent = ctext / quoted-pair / comment comment = “(” *([FWS] ccontent) [FWS] “)” CFWS = (1*( comment) [FWS]) / FWS
/\A[#{Regexp.escape([33..39, 42..91, 93..126].map { |ascii_range| ascii_range.map(&:chr) }.flatten.join)}\s]/i- QTEXT =
www.rfc-editor.org/rfc/rfc5322#section-3.2.4
Strings of characters that include characters other than those allowed in atoms can be represented in a quoted string format, where the characters are surrounded by quote (DQUOTE, ASCII value 34) characters.
qtext = %d33 / ; Printable US-ASCII
d35-91 / ; characters not including d93-126 / ; "\" or the quote character obs-qtextqcontent = qtext / quoted-pair quoted-string = [CFWS]
DQUOTE *([FWS] qcontent) [FWS] DQUOTE [CFWS] /\A[#{Regexp.escape([33..33, 35..91, 93..126].map { |ascii_range| ascii_range.map(&:chr) }.flatten.join)}\s]/i- IP_OCTET =
/\A[0-9]+\Z/- DOMAIN_PART_LABEL =
From datatracker.ietf.org/doc/html/rfc1035#section-2.3.1
> The labels must follow the rules for ARPANET host names. They must > start with a letter, end with a letter or digit, and have as interior > characters only letters, digits, and hyphen. There are also some > restrictions on the length. Labels must be 63 characters or less.
<label> | <subdomain> “.” <label> <label> ::= <letter> [ [ <ldh-str> ] <let-dig> ] <ldh-str> ::= <let-dig-hyp> | <let-dig-hyp> <ldh-str> <let-dig-hyp> ::= <let-dig> | “-” <let-dig> ::= <letter> | <digit>
Additionally, from datatracker.ietf.org/doc/html/rfc1123#section-2.1
> One aspect of host name syntax is hereby changed: the > restriction on the first character is relaxed to allow either a > letter or a digit. Host software MUST support this more liberal > syntax.
/\A[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9]?\Z/- DOMAIN_PART_TLD =
From tools.ietf.org/id/draft-liman-tld-names-00.html#rfc.section.2
> A TLD label MUST be at least two characters long and MAY be as long as 63 characters - > not counting any leading or trailing periods (.). It MUST consist of only ASCII characters > from the groups “letters” (A-Z), “digits” (0-9) and “hyphen” (-), and it MUST start with an > ASCII “letter”, and it MUST NOT end with a “hyphen”. Upper and lower case MAY be mixed at random, > since DNS lookups are case-insensitive.
tldlabel = ALPHA *61(ldh) ld ldh = ld / “-” ld = ALPHA / DIGIT ALPHA = %x41-5A / %x61-7A ; A-Z / a-z DIGIT = %x30-39 ; 0-9
/\A[A-Za-z][A-Za-z0-9-]*[A-Za-z0-9]\Z/- DEFAULT_MESSAGE =
"does not appear to be valid"- DEFAULT_MX_MESSAGE =
"is not routable"- ERROR_MESSAGE_I18N_KEY =
:invalid_email_address- ERROR_MX_MESSAGE_I18N_KEY =
:email_address_not_routable- VERSION =
"1.8.2"
Class Method Summary collapse
- .default_message ⇒ Object
- .deprecation_warn(msg) ⇒ Object
- .load_i18n_locales ⇒ Object
- .validate_domain_part_syntax(domain, idn: true) ⇒ Object
- .validate_email_domain(email, idn: true, check_mx_timeout: 3) ⇒ Object
-
.validate_email_format(email, options = {}) ⇒ Object
Validates whether the specified value is a valid email address.
- .validate_local_part_syntax(local) ⇒ Object
Class Method Details
.default_message ⇒ Object
114 115 116 |
# File 'lib/validates_email_format_of.rb', line 114 def self. defined?(I18n) ? I18n.t(ERROR_MESSAGE_I18N_KEY, scope: [:activemodel, :errors, :messages], default: DEFAULT_MESSAGE) : DEFAULT_MESSAGE end |
.deprecation_warn(msg) ⇒ Object
298 299 300 301 302 303 304 |
# File 'lib/validates_email_format_of.rb', line 298 def self.deprecation_warn(msg) if defined?(ActiveSupport::Deprecation) ActiveSupport::Deprecation.warn(msg) else warn(msg) end end |
.load_i18n_locales ⇒ Object
5 6 7 8 |
# File 'lib/validates_email_format_of.rb', line 5 def self.load_i18n_locales require "i18n" I18n.load_path += Dir.glob(File.(File.join(File.dirname(__FILE__), "..", "config", "locales", "*.yml"))) end |
.validate_domain_part_syntax(domain, idn: true) ⇒ Object
271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 |
# File 'lib/validates_email_format_of.rb', line 271 def self.validate_domain_part_syntax(domain, idn: true) parts = domain.downcase.split(".", -1) parts.map! { |part| SimpleIDN.to_ascii(part) } if idn return false if parts.length <= 1 # Only one domain part # ipv4 return true if parts.length == 4 && parts.all? { |part| part =~ IP_OCTET && part.to_i.between?(0, 255) } # From https://datatracker.ietf.org/doc/html/rfc3696#section-2 this is the recommended, pragmatic way to validate a domain name: # # > It is likely that the better strategy has now become to make the "at least one period" test, # > to verify LDH conformance (including verification that the apparent TLD name is not all-numeric), # > and then to use the DNS to determine domain name validity, rather than trying to maintain # > a local list of valid TLD names. # # We do a little bit more but not too much and validate the tokens but do not check against a list of valid TLDs. parts.each do |part| return false if part.nil? || part.empty? return false if part.length > 63 return false unless DOMAIN_PART_LABEL.match?(part) end return false unless DOMAIN_PART_TLD.match?(parts[-1]) true end |
.validate_email_domain(email, idn: true, check_mx_timeout: 3) ⇒ Object
98 99 100 101 102 103 104 105 106 107 |
# File 'lib/validates_email_format_of.rb', line 98 def self.validate_email_domain(email, idn: true, check_mx_timeout: 3) domain = email.to_s.downcase.match(/@(.+)/)[1] domain = SimpleIDN.to_ascii(domain) if idn Resolv::DNS.open do |dns| dns.timeouts = check_mx_timeout @mx = dns.getresources(domain, Resolv::DNS::Resource::IN::MX) + dns.getresources(domain, Resolv::DNS::Resource::IN::A) end @mx.size > 0 end |
.validate_email_format(email, options = {}) ⇒ Object
Validates whether the specified value is a valid email address. Returns nil if the value is valid, otherwise returns an array containing one or more validation error messages.
Configuration options:
-
message- A custom error message (default is: “does not appear to be valid”) -
check_mx- Check for MX records (default is false) -
check_mx_timeout- Timeout in seconds for checking MX records before aResolvTimeoutis raised (default is 3) -
idn- Enable or disable Internationalized Domain Names (default is true) -
mx_message- A custom error message when an MX record validation fails (default is: “is not routable.”) -
local_lengthMaximum number of characters allowed in the local part (default is 64) -
domain_lengthMaximum number of characters allowed in the domain part (default is 255) -
generate_messageReturn the I18n key of the error message instead of the error message itself (default is false)
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 166 167 168 169 170 171 172 |
# File 'lib/validates_email_format_of.rb', line 130 def self.validate_email_format(email, = {}) = {message: [:generate_message] ? ERROR_MESSAGE_I18N_KEY : , check_mx: false, check_mx_timeout: 3, idn: true, mx_message: if [:generate_message] ERROR_MX_MESSAGE_I18N_KEY else (defined?(I18n) ? I18n.t(ERROR_MX_MESSAGE_I18N_KEY, scope: [:activemodel, :errors, :messages], default: DEFAULT_MX_MESSAGE) : DEFAULT_MX_MESSAGE) end, domain_length: 255, local_length: 64, generate_message: false} opts = .merge() { |key, old, new| old } # merge the default options into the specified options, retaining all specified options begin domain, local = email.reverse.split("@", 2) rescue return [opts[:message]] end # need local and domain parts return [opts[:message]] unless local && !local.empty? && domain && !domain.empty? # check lengths return [opts[:message]] unless domain.length <= opts[:domain_length] && local.length <= opts[:local_length] local.reverse! domain.reverse! if opts.has_key?(:with) # holdover from versions <= 1.4.7 deprecation_warn(":with option is deprecated and will be removed in the next version") return [opts[:message]] unless email&.match?(opts[:with]) else return [opts[:message]] unless validate_local_part_syntax(local) && validate_domain_part_syntax(domain, idn: opts[:idn]) end if opts[:check_mx] && !validate_email_domain(email, check_mx_timeout: opts[:check_mx_timeout], idn: opts[:idn]) return [opts[:mx_message]] end nil # represents no validation errors end |
.validate_local_part_syntax(local) ⇒ Object
174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 |
# File 'lib/validates_email_format_of.rb', line 174 def self.validate_local_part_syntax(local) in_quoted_pair = false in_quoted_string = false comment_depth = 0 # The local part is made up of dot-atom and quoted-string joined together by "." characters # # https://www.rfc-editor.org/rfc/rfc5322#section-3.4.1 # > local-part = dot-atom / quoted-string / obs-local-part # # https://www.rfc-editor.org/rfc/rfc5322#section-3.2.3 # Both atom and dot-atom are interpreted as a single unit, comprising # > the string of characters that make it up. Semantically, the optional # > comments and FWS surrounding the rest of the characters are not part # > of the atom; the atom is only the run of atext characters in an atom, # > or the atext and "." characters in a dot-atom. joining_atoms = true (0..local.length - 1).each do |i| ord = local[i].ord # accept anything if it's got a backslash before it if in_quoted_pair in_quoted_pair = false next end # double quote delimits quoted strings if ord == 34 if in_quoted_string # leaving the quoted string in_quoted_string = false next elsif joining_atoms # are we allowed to enter a quoted string? in_quoted_string = true joining_atoms = false next else return false end end # period indicates we want to join atoms, e.g. `aaa.bbb."ccc"@example.com if ord == 46 return false if i.zero? return false if joining_atoms joining_atoms = true next end joining_atoms = false # quoted string logic must come before comment processing since a quoted string # may contain parens, e.g. `"name(a)"@example.com` if in_quoted_string next if QTEXT.match?(local[i]) end # opening paren to show we are going into a comment (CFWS) if ord == 40 comment_depth += 1 next end # closing paren if ord == 41 comment_depth -= 1 return false if comment_depth < 0 next end # backslash signifies the start of a quoted pair if ord == 92 # https://www.rfc-editor.org/rfc/rfc5322#section-3.2.1 # > The only places in this specification where quoted-pair currently appears are # > ccontent, qcontent, and in obs-dtext in section 4. return false unless in_quoted_string || comment_depth > 0 in_quoted_pair = true next end if comment_depth > 0 next if CTEXT.match?(local[i]) elsif ATEXT.match?(local[i, 1]) next end return false end return false if in_quoted_pair # unbalanced quoted pair return false if in_quoted_string # unbalanced quotes return false unless comment_depth.zero? # unbalanced comment parens return false if joining_atoms # the last char we encountered was a period true end |