module ValidatesEmailFormatOf
Constants
- 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]
- 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
- DEFAULT_MESSAGE
- DEFAULT_MX_MESSAGE
- 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.
- 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
- ERROR_MESSAGE_I18N_KEY
- ERROR_MX_MESSAGE_I18N_KEY
- IP_OCTET
- 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-qtext
qcontent = qtext / quoted-pair quoted-string = [CFWS]
DQUOTE *([FWS] qcontent) [FWS] DQUOTE [CFWS]
- VERSION
Public Class Methods
# File lib/validates_email_format_of.rb, line 111 def self.default_message defined?(I18n) ? I18n.t(ERROR_MESSAGE_I18N_KEY, scope: [:activemodel, :errors, :messages], default: DEFAULT_MESSAGE) : DEFAULT_MESSAGE end
# File lib/validates_email_format_of.rb, line 4 def self.load_i18n_locales require "i18n" I18n.load_path += Dir.glob(File.expand_path(File.join(File.dirname(__FILE__), "..", "config", "locales", "*.yml"))) end
# File lib/validates_email_format_of.rb, line 234 def self.validate_domain_part_syntax(domain) parts = domain.downcase.split(".", -1) 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
# File lib/validates_email_format_of.rb, line 97 def self.validate_email_domain(email, check_mx_timeout: 3) domain = email.to_s.downcase.match(/@(.+)/)[1] 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
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 a ‘ResolvTimeout` is raised (default is 3) -
mx_message
- A custom error message when an MX record validation fails (default is: “is not routable.”) -
with
The regex to use for validating the format of the email address (deprecated) -
local_length
Maximum number of characters allowed in the local part (default is 64) -
domain_length
Maximum number of characters allowed in the domain part (default is 255) -
generate_message
Return the I18n key of the error message instead of the error message itself (default is false)
# File lib/validates_email_format_of.rb, line 127 def self.validate_email_format(email, options = {}) default_options = {message: options[:generate_message] ? ERROR_MESSAGE_I18N_KEY : default_message, check_mx: false, check_mx_timeout: 3, mx_message: if options[: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 = options.merge(default_options) { |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 return [opts[:message]] unless email&.match?(opts[:with]) else return [opts[:message]] unless validate_local_part_syntax(local) && validate_domain_part_syntax(domain) end if opts[:check_mx] && !validate_email_domain(email, check_mx_timeout: opts[:check_mx_timeout]) return [opts[:mx_message]] end nil # represents no validation errors end
# File lib/validates_email_format_of.rb, line 169 def self.validate_local_part_syntax(local) in_quoted_pair = false in_quoted_string = false comment_depth = 0 (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 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 && i < local.length - 1 return false if !in_quoted_string # must be in quoted string per http://www.rfc-editor.org/errata_search.php?rfc=3696 in_quoted_pair = true next end # double quote delimits quoted strings if ord == 34 in_quoted_string = !in_quoted_string next end if comment_depth > 0 next if CTEXT.match?(local[i]) elsif ATEXT.match?(local[i, 1]) next end # period must be followed by something if ord == 46 return false if i == 0 || i == local.length - 1 # can't be first or last char next unless local[i + 1].ord == 46 # can't be followed by a period end return false end return false if in_quoted_string # unbalanced quotes return false unless comment_depth.zero? # unbalanced comment parens true end