validates_email_format_of 1.6.2 → 1.7.1

Sign up to get free protection for your applications and to get access to all the features.
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)