validates_email_format_of 1.6.2 → 1.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 6b623e89d67bbb88c3d6e9d2618b425b4eb1a35d
4
- data.tar.gz: 79b812468bd56f993fc9b27fede13adf4e7433cc
2
+ SHA256:
3
+ metadata.gz: 2b11027884cda429ba190024f4564c69f88bfdf8dc24ac471fb3ed805050b8f9
4
+ data.tar.gz: c7a676365614b908a6206fcabfb2f9eda65c5995eae2e4f15fdbe18f4e182aa8
5
5
  SHA512:
6
- metadata.gz: 7badf4df9d02ca2171f6aad33c588ee0d9710643799dd258bfe02edcb4abf9a2d57ce46016ed22fa59c003a2299f043ca519a8d2449a587af8d9686ac8b86bca
7
- data.tar.gz: f1df6f726de58e051cb532d7c65a0a0ed77e36ccd02bd83da250cf89a9a6a19e453a3ce6f44bfaaf8c54386ab277a702cc07169e16a6776c49c3b2b484b89e88
6
+ metadata.gz: 9b21600741d0f3e46ed27548db8ab52165e5ee7a284633be31ea1eb127e4156f418f93c6ceea29fe8b9ec305bf52fe4d74166a0de24f1c12b1d0eb0b88e472c8
7
+ data.tar.gz: c867bb8f4e29b7f3ea21c343a2a5668c68f8bf0c02fafdea979c5fcd9a2588cabe84c29cf3c62c5ebd426ad55625897a7a0b156067021d69ae9ae2f2bf8ce63c
@@ -0,0 +1,67 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [master]
6
+ pull_request:
7
+ branches: [master]
8
+
9
+ permissions:
10
+ contents: read
11
+
12
+ jobs:
13
+ test:
14
+ name: "Ruby ${{ matrix.ruby }}, Rails ${{ matrix.gemfile }}"
15
+
16
+ runs-on: ubuntu-latest
17
+
18
+ strategy:
19
+ matrix:
20
+ ruby: ["2.6", "2.7", "3.0", "3.1"]
21
+ gemfile: ["4.2", "5.0", "5.1", "5.2", "6.0", "6.1", "7.0"]
22
+ exclude:
23
+ - gemfile: "4.2"
24
+ ruby: "3.0"
25
+ - gemfile: "4.2"
26
+ ruby: "3.1"
27
+ - gemfile: "5.0"
28
+ ruby: "3.0"
29
+ - gemfile: "5.0"
30
+ ruby: "3.1"
31
+ - gemfile: "5.1"
32
+ ruby: "3.0"
33
+ - gemfile: "5.1"
34
+ ruby: "3.1"
35
+ - gemfile: "5.2"
36
+ ruby: "3.0"
37
+ - gemfile: "5.2"
38
+ ruby: "3.1"
39
+ - gemfile: "7.0"
40
+ ruby: "2.5"
41
+ - gemfile: "7.0"
42
+ ruby: "2.6"
43
+ - gemfile: "7.0"
44
+ ruby: "2.7"
45
+
46
+ env:
47
+ BUNDLE_GEMFILE: gemfiles/rails_${{ matrix.gemfile }}.gemfile
48
+ RAILS_ENV: test
49
+
50
+ steps:
51
+ - uses: actions/checkout@v3
52
+
53
+ - name: "Install Ruby ${{ matrix.ruby }}"
54
+ # To automatically get bug fixes and new Ruby versions for ruby/setup-ruby,
55
+ # change this to (see https://github.com/ruby/setup-ruby#versioning):
56
+ # uses: ruby/setup-ruby@v1
57
+ uses: ruby/setup-ruby@v1
58
+ with:
59
+ bundler: 1
60
+ ruby-version: ${{ matrix.ruby }}
61
+ bundler-cache: true # runs 'bundle install' and caches installed gems automatically
62
+
63
+ - name: Run specs
64
+ run: bundle exec rspec
65
+
66
+ - name: Run standard.rb
67
+ run: bundle exec rake standard
data/.gitignore CHANGED
@@ -6,3 +6,5 @@ Gemfile.lock
6
6
  *.sqlite3
7
7
  *.swp
8
8
  .ruby-version
9
+ gemfiles/.bundle
10
+ gemfiles/*.gemfile.lock
data/Appraisals ADDED
@@ -0,0 +1,32 @@
1
+ # run `bundle exec appraisal install` after making changes here
2
+ appraise "rails-7.0" do
3
+ gem "rails", "~> 7.0"
4
+ end
5
+
6
+ appraise "rails-6.1" do
7
+ gem "rails", "~> 6.1.0"
8
+ end
9
+
10
+ appraise "rails-6.0" do
11
+ gem "rails", "~> 6.0.3", ">= 6.0.3.2"
12
+ end
13
+
14
+ appraise "rails-5.2" do
15
+ gem "rails", "~> 5.2.4", ">= 5.2.4.3"
16
+ gem "i18n", "< 1"
17
+ end
18
+
19
+ appraise "rails-5.1" do
20
+ gem "rails", "~> 5.1.7"
21
+ gem "i18n", "< 1"
22
+ end
23
+
24
+ appraise "rails-5.0" do
25
+ gem "rails", "~> 5.0.7", ">= 5.0.7.2"
26
+ gem "i18n", "< 1"
27
+ end
28
+
29
+ appraise "rails-4.2" do
30
+ gem "rails", "~> 4.2.0"
31
+ gem "i18n", "< 1"
32
+ end
data/CHANGELOG CHANGED
@@ -1,3 +1,18 @@
1
+ == 1.7.1 (3 Aug 2022)
2
+
3
+ * Fix invalid symbols being allowed in the local part - https://github.com/validates-email-format-of/validates_email_format_of/issues/86
4
+ * Fix rspec_matcher when using a custom error message - https://github.com/validates-email-format-of/validates_email_format_of/pull/85 - thanks https://github.com/luuqnhu !
5
+
6
+ == 1.7.0 (29 July 2022)
7
+
8
+ * Use Standard.rb for internal code formatting - https://github.com/validates-email-format-of/validates_email_format_of/commit/db1b0a86af58e478b7f9f2f269bf93bf48dc13c1
9
+ * Add support for comments in the local part and improve quoted character handling - https://github.com/validates-email-format-of/validates_email_format_of/issues/69
10
+ * Improve grammar for parsing domain part and validate domain part lengths - https://github.com/validates-email-format-of/validates_email_format_of/commit/2554b55e547c1fae6599d13b0c99296752888c91
11
+ * Do not strip spaces before validating - https://github.com/validates-email-format-of/validates_email_format_of/issues/61 and https://github.com/validates-email-format-of/validates_email_format_of/issues/72
12
+ * Allow setting check_mx_timeout and reduce the default timeout to 3 seconds - https://github.com/validates-email-format-of/validates_email_format_of/issues/66
13
+ * Fix regex duplicate character warning - https://github.com/validates-email-format-of/validates_email_format_of/pull/71
14
+ * Update CI to include Ruby 2.6 to 3.1 and Rails 4.2 to 7.0
15
+
1
16
  == 1.6.1 (8 Sept 2014)
2
17
 
3
18
  * In a Rails context, this gem now uses ActiveModel's default logic for constructing I18n keys, to make it easier to override them on a model/attribute basis.
data/README.rdoc CHANGED
@@ -40,6 +40,8 @@ Or in your Gemfile:
40
40
  String. A custom error message when the email format is invalid (default is: "does not appear to be a valid e-mail address")
41
41
  :check_mx
42
42
  Boolean. Check domain for a valid MX record (default is false)
43
+ :check_mx_timeout
44
+ Integer. Timeout in seconds for checking MX records before a `ResolvTimeout` is raised (default is 3).
43
45
  :mx_message
44
46
  String. A custom error message when the domain does not match a valid MX record (default is: "is not routable"). Ignored unless :check_mx option is true.
45
47
  :local_length
@@ -56,7 +58,7 @@ Or in your Gemfile:
56
58
 
57
59
  == Testing
58
60
 
59
- To execute the unit tests run <tt>rspec</tt>.
61
+ To execute the unit tests against [all the Rails versions we support run](gemfiles/) <tt>bundle exec appraisal rspec</tt> or run against an individual version with <tt>bundle exec appraisal rails-6.0 rspec</tt>.
60
62
 
61
63
  Tested in Ruby 1.8.7, 1.9.2, 1.9.3, 2.0.0, 2.1.2, JRuby and REE 1.8.7.
62
64
 
@@ -83,7 +85,7 @@ Yes, our Rspec syntax is that simple!
83
85
 
84
86
  == Credits
85
87
 
86
- Written by Alex Dunae (dunae.ca), 2006-11.
88
+ Written by Alex Dunae (dunae.ca), 2006-22.
87
89
 
88
90
  Many thanks to the plugin's recent contributors: https://github.com/alexdunae/validates_email_format_of/contributors
89
91
 
@@ -0,0 +1,8 @@
1
+ fr:
2
+ activemodel: &errors
3
+ errors:
4
+ messages:
5
+ invalid_email_address: "ne semble pas être une adresse email valide"
6
+ email_address_not_routable: "n'est pas routable"
7
+ activerecord:
8
+ <<: *errors
@@ -0,0 +1,8 @@
1
+ it:
2
+ activemodel: &errors
3
+ errors:
4
+ messages:
5
+ invalid_email_address: 'non sembra un indirizzo e-mail valido'
6
+ email_address_not_routable: 'dominio non raggiungibile'
7
+ activerecord:
8
+ <<: *errors
@@ -0,0 +1,8 @@
1
+ ja:
2
+ activemodel: &errors
3
+ errors:
4
+ messages:
5
+ invalid_email_address: 'は妥当なメールアドレスでは無いようです。'
6
+ email_address_not_routable: 'は到達不能です。'
7
+ activerecord:
8
+ <<: *errors
@@ -0,0 +1,8 @@
1
+ pt-BR:
2
+ activemodel: &errors
3
+ errors:
4
+ messages:
5
+ invalid_email_address: 'não parece ser um endereço de e-mail válido'
6
+ email_address_not_routable: 'não é acessível'
7
+ activerecord:
8
+ <<: *errors
@@ -0,0 +1,8 @@
1
+ pt:
2
+ activemodel: &errors
3
+ errors:
4
+ messages:
5
+ invalid_email_address: 'não parece ser um endereço de e-mail válido'
6
+ email_address_not_routable: 'não é acessível'
7
+ activerecord:
8
+ <<: *errors
@@ -0,0 +1,8 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "http://rubygems.org"
4
+
5
+ gem "rails", "~> 4.2.0"
6
+ gem "i18n", "< 1"
7
+
8
+ gemspec path: "../"
@@ -0,0 +1,8 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "http://rubygems.org"
4
+
5
+ gem "rails", "~> 5.0.7", ">= 5.0.7.2"
6
+ gem "i18n", "< 1"
7
+
8
+ gemspec path: "../"
@@ -0,0 +1,8 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "http://rubygems.org"
4
+
5
+ gem "rails", "~> 5.1.7"
6
+ gem "i18n", "< 1"
7
+
8
+ gemspec path: "../"
@@ -0,0 +1,8 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "http://rubygems.org"
4
+
5
+ gem "rails", "~> 5.2.4", ">= 5.2.4.3"
6
+ gem "i18n", "< 1"
7
+
8
+ gemspec path: "../"
@@ -0,0 +1,7 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "http://rubygems.org"
4
+
5
+ gem "rails", "~> 6.0.3", ">= 6.0.3.2"
6
+
7
+ gemspec path: "../"
@@ -0,0 +1,7 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "http://rubygems.org"
4
+
5
+ gem "rails", "~> 6.1.0"
6
+
7
+ gemspec path: "../"
@@ -0,0 +1,7 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "http://rubygems.org"
4
+
5
+ gem "rails", "~> 7.0"
6
+
7
+ gemspec path: "../"
@@ -1,7 +1,7 @@
1
- require 'validates_email_format_of'
2
- require 'active_model'
1
+ require "validates_email_format_of"
2
+ require "active_model"
3
3
 
4
- if ActiveModel::VERSION::MAJOR < 2 || (2 == ActiveModel::VERSION::MAJOR && ActiveModel::VERSION::MINOR < 1)
4
+ if ActiveModel::VERSION::MAJOR < 2 || (ActiveModel::VERSION::MAJOR == 2 && ActiveModel::VERSION::MINOR < 1)
5
5
  puts "WARNING: ActiveModel validation helper methods in validates_email_format_of gem are not compatible with ActiveModel < 2.1.0. Please use ValidatesEmailFormatOf::validate_email_format(email, options) or upgrade ActiveModel"
6
6
  end
7
7
 
@@ -9,7 +9,7 @@ module ActiveModel
9
9
  module Validations
10
10
  class EmailFormatValidator < EachValidator
11
11
  def validate_each(record, attribute, value)
12
- (ValidatesEmailFormatOf::validate_email_format(value, options.merge(:generate_message => true)) || []).each do |error|
12
+ (ValidatesEmailFormatOf.validate_email_format(value, options.merge(generate_message: true)) || []).each do |error|
13
13
  record.errors.add(attribute, error)
14
14
  end
15
15
  end
@@ -1,7 +1,7 @@
1
1
  module ValidatesEmailFormatOf
2
2
  class Railtie < Rails::Railtie
3
- initializer 'validates_email_format_of.load_i18n_locales' do |app|
4
- ValidatesEmailFormatOf::load_i18n_locales
3
+ initializer "validates_email_format_of.load_i18n_locales" do |app|
4
+ ValidatesEmailFormatOf.load_i18n_locales
5
5
  end
6
6
  end
7
7
  end
@@ -6,7 +6,7 @@ RSpec::Matchers.define :validate_email_format_of do |attribute|
6
6
  actual.send(:"#{attribute}=", "invalid@example.")
7
7
  expect(actual).to be_invalid
8
8
  @expected_message ||= ValidatesEmailFormatOf.default_message
9
- expect(actual.errors.messages[attribute.to_sym]).to include(@expected_message)
9
+ expect(actual.errors.added?(attribute, @expected_message)).to be_truthy
10
10
  end
11
11
  chain :with_message do |message|
12
12
  @expected_message = message
@@ -1,3 +1,3 @@
1
1
  module ValidatesEmailFormatOf
2
- VERSION = '1.6.2'
2
+ VERSION = "1.7.1"
3
3
  end
@@ -1,22 +1,99 @@
1
- # encoding: utf-8
2
- require 'validates_email_format_of/version'
1
+ require "validates_email_format_of/version"
3
2
 
4
3
  module ValidatesEmailFormatOf
5
4
  def self.load_i18n_locales
6
- require 'i18n'
7
- I18n.load_path += Dir.glob(File.expand_path(File.join(File.dirname(__FILE__), '..', 'config', 'locales', '*.yml')))
5
+ require "i18n"
6
+ I18n.load_path += Dir.glob(File.expand_path(File.join(File.dirname(__FILE__), "..", "config", "locales", "*.yml")))
8
7
  end
9
8
 
10
- require 'resolv'
9
+ require "resolv"
11
10
 
12
- LocalPartSpecialChars = /[\!\#\$\%\&\'\*\-\/\=\?\+\-\^\_\`\{\|\}\~]/
11
+ # Characters that are allowed in to appear in the local part unquoted
12
+ # https://www.rfc-editor.org/rfc/rfc5322#section-3.2.3
13
+ #
14
+ # An addr-spec is a specific Internet identifier that contains a
15
+ # locally interpreted string followed by the at-sign character ("@",
16
+ # ASCII value 64) followed by an Internet domain. The locally
17
+ # interpreted string is either a quoted-string or a dot-atom. If the
18
+ # string can be represented as a dot-atom (that is, it contains no
19
+ # characters other than atext characters or "." surrounded by atext
20
+ # characters), then the dot-atom form SHOULD be used and the quoted-
21
+ # string form SHOULD NOT be used. Comments and folding white space
22
+ # SHOULD NOT be used around the "@" in the addr-spec.
23
+ #
24
+ # atext = ALPHA / DIGIT /
25
+ # "!" / "#" / "$" / "%" / "&" / "'" / "*" /
26
+ # "+" / "-" / "/" / "=" / "?" / "^" / "_" /
27
+ # "`" / "{" / "|" / "}" / "~"
28
+ # dot-atom-text = 1*atext *("." 1*atext)
29
+ # dot-atom = [CFWS] dot-atom-text [CFWS]
30
+ ATEXT = /\A[A-Z0-9!\#$%&'*\-\/=?+\^_`{|}~]\z/i
31
+
32
+ # Characters that are allowed to appear unquoted in comments
33
+ # https://www.rfc-editor.org/rfc/rfc5322#section-3.2.2
34
+ #
35
+ # ctext = %d33-39 / %d42-91 / %d93-126
36
+ # ccontent = ctext / quoted-pair / comment
37
+ # comment = "(" *([FWS] ccontent) [FWS] ")"
38
+ # CFWS = (1*([FWS] comment) [FWS]) / FWS
39
+ CTEXT = /\A[#{Regexp.escape([33..39, 42..91, 93..126].map { |ascii_range| ascii_range.map(&:chr) }.flatten.join)}\s]/i
40
+
41
+ # https://www.rfc-editor.org/rfc/rfc5322#section-3.2.4
42
+ #
43
+ # Strings of characters that include characters other than those
44
+ # allowed in atoms can be represented in a quoted string format, where
45
+ # the characters are surrounded by quote (DQUOTE, ASCII value 34)
46
+ # characters.
47
+ #
48
+ # qtext = %d33 / ; Printable US-ASCII
49
+ # %d35-91 / ; characters not including
50
+ # %d93-126 / ; "\" or the quote character
51
+ # obs-qtext
52
+ #
53
+ # qcontent = qtext / quoted-pair
54
+ # quoted-string = [CFWS]
55
+ # DQUOTE *([FWS] qcontent) [FWS] DQUOTE
56
+ # [CFWS]
57
+ QTEXT = /\A[#{Regexp.escape([33..33, 35..91, 93..126].map { |ascii_range| ascii_range.map(&:chr) }.flatten.join)}\s]/i
58
+
59
+ IP_OCTET = /\A[0-9]+\Z/
13
60
 
14
- def self.validate_email_domain(email)
15
- domain = email.to_s.downcase.match(/\@(.+)/)[1]
61
+ # From https://datatracker.ietf.org/doc/html/rfc1035#section-2.3.1
62
+ #
63
+ # > The labels must follow the rules for ARPANET host names. They must
64
+ # > start with a letter, end with a letter or digit, and have as interior
65
+ # > characters only letters, digits, and hyphen. There are also some
66
+ # > restrictions on the length. Labels must be 63 characters or less.
67
+ #
68
+ # <label> | <subdomain> "." <label>
69
+ # <label> ::= <letter> [ [ <ldh-str> ] <let-dig> ]
70
+ # <ldh-str> ::= <let-dig-hyp> | <let-dig-hyp> <ldh-str>
71
+ # <let-dig-hyp> ::= <let-dig> | "-"
72
+ # <let-dig> ::= <letter> | <digit>
73
+ DOMAIN_PART_LABEL = /\A[A-Za-z][A-Za-z0-9\-]*[A-Za-z0-9]?\Z/
74
+
75
+ # From https://tools.ietf.org/id/draft-liman-tld-names-00.html#rfc.section.2
76
+ #
77
+ # > A TLD label MUST be at least two characters long and MAY be as long as 63 characters -
78
+ # > not counting any leading or trailing periods (.). It MUST consist of only ASCII characters
79
+ # > from the groups "letters" (A-Z), "digits" (0-9) and "hyphen" (-), and it MUST start with an
80
+ # > ASCII "letter", and it MUST NOT end with a "hyphen". Upper and lower case MAY be mixed at random,
81
+ # > since DNS lookups are case-insensitive.
82
+ #
83
+ # tldlabel = ALPHA *61(ldh) ld
84
+ # ldh = ld / "-"
85
+ # ld = ALPHA / DIGIT
86
+ # ALPHA = %x41-5A / %x61-7A ; A-Z / a-z
87
+ # DIGIT = %x30-39 ; 0-9
88
+ DOMAIN_PART_TLD = /\A[A-Za-z][A-Za-z0-9\-]*[A-Za-z0-9]\Z/
89
+
90
+ def self.validate_email_domain(email, check_mx_timeout: 3)
91
+ domain = email.to_s.downcase.match(/@(.+)/)[1]
16
92
  Resolv::DNS.open do |dns|
93
+ dns.timeouts = check_mx_timeout
17
94
  @mx = dns.getresources(domain, Resolv::DNS::Resource::IN::MX) + dns.getresources(domain, Resolv::DNS::Resource::IN::A)
18
95
  end
19
- @mx.size > 0 ? true : false
96
+ @mx.size > 0
20
97
  end
21
98
 
22
99
  DEFAULT_MESSAGE = "does not appear to be valid"
@@ -25,7 +102,7 @@ module ValidatesEmailFormatOf
25
102
  ERROR_MX_MESSAGE_I18N_KEY = :email_address_not_routable
26
103
 
27
104
  def self.default_message
28
- defined?(I18n) ? I18n.t(ERROR_MESSAGE_I18N_KEY, :scope => [:activemodel, :errors, :messages], :default => DEFAULT_MESSAGE) : DEFAULT_MESSAGE
105
+ defined?(I18n) ? I18n.t(ERROR_MESSAGE_I18N_KEY, scope: [:activemodel, :errors, :messages], default: DEFAULT_MESSAGE) : DEFAULT_MESSAGE
29
106
  end
30
107
 
31
108
  # Validates whether the specified value is a valid email address. Returns nil if the value is valid, otherwise returns an array
@@ -34,57 +111,60 @@ module ValidatesEmailFormatOf
34
111
  # Configuration options:
35
112
  # * <tt>message</tt> - A custom error message (default is: "does not appear to be valid")
36
113
  # * <tt>check_mx</tt> - Check for MX records (default is false)
114
+ # * <tt>check_mx_timeout</tt> - Timeout in seconds for checking MX records before a `ResolvTimeout` is raised (default is 3)
37
115
  # * <tt>mx_message</tt> - A custom error message when an MX record validation fails (default is: "is not routable.")
38
116
  # * <tt>with</tt> The regex to use for validating the format of the email address (deprecated)
39
117
  # * <tt>local_length</tt> Maximum number of characters allowed in the local part (default is 64)
40
118
  # * <tt>domain_length</tt> Maximum number of characters allowed in the domain part (default is 255)
41
119
  # * <tt>generate_message</tt> Return the I18n key of the error message instead of the error message itself (default is false)
42
- def self.validate_email_format(email, options={})
43
- default_options = { :message => options[:generate_message] ? ERROR_MESSAGE_I18N_KEY : default_message,
44
- :check_mx => false,
45
- :mx_message => options[:generate_message] ? ERROR_MX_MESSAGE_I18N_KEY : (defined?(I18n) ? I18n.t(ERROR_MX_MESSAGE_I18N_KEY, :scope => [:activemodel, :errors, :messages], :default => DEFAULT_MX_MESSAGE) : DEFAULT_MX_MESSAGE),
46
- :domain_length => 255,
47
- :local_length => 64,
48
- :generate_message => false
49
- }
50
- opts = options.merge(default_options) {|key, old, new| old} # merge the default options into the specified options, retaining all specified options
51
-
52
- email = email.strip if email
53
-
54
- begin
55
- domain, local = email.reverse.split('@', 2)
56
- rescue
57
- return [ opts[:message] ]
58
- end
120
+ def self.validate_email_format(email, options = {})
121
+ default_options = {message: options[:generate_message] ? ERROR_MESSAGE_I18N_KEY : default_message,
122
+ check_mx: false,
123
+ check_mx_timeout: 3,
124
+ mx_message: if options[:generate_message]
125
+ ERROR_MX_MESSAGE_I18N_KEY
126
+ else
127
+ (defined?(I18n) ? I18n.t(ERROR_MX_MESSAGE_I18N_KEY, scope: [:activemodel, :errors, :messages], default: DEFAULT_MX_MESSAGE) : DEFAULT_MX_MESSAGE)
128
+ end,
129
+ domain_length: 255,
130
+ local_length: 64,
131
+ generate_message: false}
132
+ opts = options.merge(default_options) { |key, old, new| old } # merge the default options into the specified options, retaining all specified options
133
+
134
+ begin
135
+ domain, local = email.reverse.split("@", 2)
136
+ rescue
137
+ return [opts[:message]]
138
+ end
59
139
 
60
- # need local and domain parts
61
- return [ opts[:message] ] unless local and not local.empty? and domain and not domain.empty?
140
+ # need local and domain parts
141
+ return [opts[:message]] unless local && !local.empty? && domain && !domain.empty?
62
142
 
63
- # check lengths
64
- return [ opts[:message] ] unless domain.length <= opts[:domain_length] and local.length <= opts[:local_length]
143
+ # check lengths
144
+ return [opts[:message]] unless domain.length <= opts[:domain_length] && local.length <= opts[:local_length]
65
145
 
66
- local.reverse!
67
- domain.reverse!
146
+ local.reverse!
147
+ domain.reverse!
68
148
 
69
- if opts.has_key?(:with) # holdover from versions <= 1.4.7
70
- return [ opts[:message] ] unless email =~ opts[:with]
71
- else
72
- return [ opts[:message] ] unless self.validate_local_part_syntax(local) and self.validate_domain_part_syntax(domain)
73
- end
149
+ if opts.has_key?(:with) # holdover from versions <= 1.4.7
150
+ return [opts[:message]] unless email&.match?(opts[:with])
151
+ else
152
+ return [opts[:message]] unless validate_local_part_syntax(local) && validate_domain_part_syntax(domain)
153
+ end
74
154
 
75
- if opts[:check_mx] and !self.validate_email_domain(email)
76
- return [ opts[:mx_message] ]
77
- end
155
+ if opts[:check_mx] && !validate_email_domain(email, check_mx_timeout: opts[:check_mx_timeout])
156
+ return [opts[:mx_message]]
157
+ end
78
158
 
79
- return nil # represents no validation errors
159
+ nil # represents no validation errors
80
160
  end
81
161
 
82
-
83
162
  def self.validate_local_part_syntax(local)
84
163
  in_quoted_pair = false
85
164
  in_quoted_string = false
165
+ comment_depth = 0
86
166
 
87
- (0..local.length-1).each do |i|
167
+ (0..local.length - 1).each do |i|
88
168
  ord = local[i].ord
89
169
 
90
170
  # accept anything if it's got a backslash before it
@@ -93,9 +173,26 @@ module ValidatesEmailFormatOf
93
173
  next
94
174
  end
95
175
 
176
+ if in_quoted_string
177
+ next if QTEXT.match?(local[i])
178
+ end
179
+
180
+ # opening paren to show we are going into a comment (CFWS)
181
+ if ord == 40
182
+ comment_depth += 1
183
+ next
184
+ end
185
+
186
+ # closing paren
187
+ if ord == 41
188
+ comment_depth -= 1
189
+ return false if comment_depth < 0
190
+ next
191
+ end
192
+
96
193
  # backslash signifies the start of a quoted pair
97
- if ord == 92 and i < local.length - 1
98
- return false if not in_quoted_string # must be in quoted string per http://www.rfc-editor.org/errata_search.php?rfc=3696
194
+ if ord == 92 && i < local.length - 1
195
+ return false if !in_quoted_string # must be in quoted string per http://www.rfc-editor.org/errata_search.php?rfc=3696
99
196
  in_quoted_pair = true
100
197
  next
101
198
  end
@@ -106,45 +203,53 @@ module ValidatesEmailFormatOf
106
203
  next
107
204
  end
108
205
 
109
- next if local[i,1] =~ /[a-z0-9]/i
110
- next if local[i,1] =~ LocalPartSpecialChars
206
+ if comment_depth > 0
207
+ next if CTEXT.match?(local[i])
208
+ elsif ATEXT.match?(local[i, 1])
209
+ next
210
+ end
111
211
 
112
212
  # period must be followed by something
113
213
  if ord == 46
114
- return false if i == 0 or i == local.length - 1 # can't be first or last char
115
- next unless local[i+1].ord == 46 # can't be followed by a period
214
+ return false if i == 0 || i == local.length - 1 # can't be first or last char
215
+ next unless local[i + 1].ord == 46 # can't be followed by a period
116
216
  end
117
217
 
118
218
  return false
119
219
  end
120
220
 
121
221
  return false if in_quoted_string # unbalanced quotes
222
+ return false unless comment_depth.zero? # unbalanced comment parens
122
223
 
123
- return true
224
+ true
124
225
  end
125
226
 
126
227
  def self.validate_domain_part_syntax(domain)
127
- parts = domain.downcase.split('.', -1)
228
+ parts = domain.downcase.split(".", -1)
128
229
 
129
230
  return false if parts.length <= 1 # Only one domain part
130
231
 
131
- # Empty parts (double period) or invalid chars
132
- return false if parts.any? {
133
- |part|
134
- part.nil? or
135
- part.empty? or
136
- not part =~ /\A[[:alnum:]\-]+\Z/ or
137
- part[0,1] == '-' or part[-1,1] == '-' # hyphen at beginning or end of part
138
- }
139
-
140
232
  # ipv4
141
- return true if parts.length == 4 and parts.all? { |part| part =~ /\A[0-9]+\Z/ and part.to_i.between?(0, 255) }
142
-
143
- return false if parts[-1].length < 2 or not parts[-1] =~ /[a-z\-]/ # TLD is too short or does not contain a char or hyphen
233
+ return true if parts.length == 4 && parts.all? { |part| part =~ IP_OCTET && part.to_i.between?(0, 255) }
234
+
235
+ # From https://datatracker.ietf.org/doc/html/rfc3696#section-2 this is the recommended, pragmatic way to validate a domain name:
236
+ #
237
+ # > It is likely that the better strategy has now become to make the "at least one period" test,
238
+ # > to verify LDH conformance (including verification that the apparent TLD name is not all-numeric),
239
+ # > and then to use the DNS to determine domain name validity, rather than trying to maintain
240
+ # > a local list of valid TLD names.
241
+ #
242
+ # We do a little bit more but not too much and validate the tokens but do not check against a list of valid TLDs.
243
+ parts.each do |part|
244
+ return false if part.nil? || part.empty?
245
+ return false if part.length > 63
246
+ return false unless DOMAIN_PART_LABEL.match?(part)
247
+ end
144
248
 
145
- return true
249
+ return false unless DOMAIN_PART_TLD.match?(parts[-1])
250
+ true
146
251
  end
147
252
  end
148
253
 
149
- require 'validates_email_format_of/active_model' if defined?(::ActiveModel) && !(ActiveModel::VERSION::MAJOR < 2 || (2 == ActiveModel::VERSION::MAJOR && ActiveModel::VERSION::MINOR < 1))
150
- require 'validates_email_format_of/railtie' if defined?(::Rails)
254
+ require "validates_email_format_of/active_model" if defined?(::ActiveModel) && !(ActiveModel::VERSION::MAJOR < 2 || (ActiveModel::VERSION::MAJOR == 2 && ActiveModel::VERSION::MINOR < 1))
255
+ require "validates_email_format_of/railtie" if defined?(::Rails::Railtie)