validates_email_format_of 1.6.3 → 1.7.0

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: 473a6ecf75f1a602abc00714b86b075545d9e2a7
4
- data.tar.gz: 2988b649b8aaa152f177757bd38e4dc2588dc2aa
2
+ SHA256:
3
+ metadata.gz: 42557ade4662b6a601647924b0a35b0ef09ad48077368e2296185b765902f6b8
4
+ data.tar.gz: ec3bdeed382f2f2af17377a529ef02e9d5271c77b4f810467403265f0b273f3d
5
5
  SHA512:
6
- metadata.gz: 645502e19a3a3abb8c3d5eef6bc6d2c864bb7e9d5fd2d6467219222806e6604b7b48aa199141d03c104de2ac9a0cb3476e508274b2068cc8be8e3a3b63739bf7
7
- data.tar.gz: b361147b9c4f5220cf4b50922f710ee0e604041e78301320f5d668d5cf61b6fa646a26e20e9f36d6a4d38b70010dcc4aff9c62a6113bf27139dd045a52b8cf47
6
+ metadata.gz: dfea65637b8d575356d3309e63c9947e0e434d53a261f520a918f889f2849f1d44249dfeb74f81dec47dd78e1b5d2d8093f1dcd97652c9d3d7507b2c51d9b5e4
7
+ data.tar.gz: 0e1420665dd0b6ebe42bb6555552ea31bfedac10c62076f65b43112f62c52246bae37d8c857a3d0816c5ddd54f06d20ac124977b02d151bfd81a78ffc065d7af
@@ -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,13 @@
1
+ == 1.7.0 (29 July 2022)
2
+
3
+ * Use Standard.rb for internal code formatting - https://github.com/validates-email-format-of/validates_email_format_of/commit/db1b0a86af58e478b7f9f2f269bf93bf48dc13c1
4
+ * 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
5
+ * Improve grammar for parsing domain part and validate domain part lengths - https://github.com/validates-email-format-of/validates_email_format_of/commit/2554b55e547c1fae6599d13b0c99296752888c91
6
+ * 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
7
+ * 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
8
+ * Fix regex duplicate character warning - https://github.com/validates-email-format-of/validates_email_format_of/pull/71
9
+ * Update CI to include Ruby 2.6 to 3.1 and Rails 4.2 to 7.0
10
+
1
11
  == 1.6.1 (8 Sept 2014)
2
12
 
3
13
  * 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
 
@@ -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, :invalid_email_address)).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.3'
2
+ VERSION = "1.7.0"
3
3
  end
@@ -1,22 +1,97 @@
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
+ ATEXT_SYMBOLS = /[!\#$%&'*\-\/=?+\^_`{|}~]/
13
12
 
14
- def self.validate_email_domain(email)
15
- domain = email.to_s.downcase.match(/\@(.+)/)[1]
13
+ # Characters that are allowed in to appear in the local part unquoted
14
+ # https://www.rfc-editor.org/rfc/rfc5322#section-3.4.1
15
+ #
16
+ # An addr-spec is a specific Internet identifier that contains a
17
+ # locally interpreted string followed by the at-sign character ("@",
18
+ # ASCII value 64) followed by an Internet domain. The locally
19
+ # interpreted string is either a quoted-string or a dot-atom. If the
20
+ # string can be represented as a dot-atom (that is, it contains no
21
+ # characters other than atext characters or "." surrounded by atext
22
+ # characters), then the dot-atom form SHOULD be used and the quoted-
23
+ # string form SHOULD NOT be used. Comments and folding white space
24
+ # SHOULD NOT be used around the "@" in the addr-spec.
25
+ #
26
+ # dot-atom-text = 1*atext *("." 1*atext)
27
+ # dot-atom = [CFWS] dot-atom-text [CFWS]
28
+ ATEXT = /\A[A-Z0-9#{ATEXT_SYMBOLS}]\z/i
29
+
30
+ # Characters that are allowed to appear unquoted in comments
31
+ # https://www.rfc-editor.org/rfc/rfc5322#section-3.2.2
32
+ #
33
+ # ctext = %d33-39 / %d42-91 / %d93-126
34
+ # ccontent = ctext / quoted-pair / comment
35
+ # comment = "(" *([FWS] ccontent) [FWS] ")"
36
+ # CFWS = (1*([FWS] comment) [FWS]) / FWS
37
+ CTEXT = /\A[#{Regexp.escape([33..39, 42..91, 93..126].map { |ascii_range| ascii_range.map(&:chr) }.flatten.join)}\s]/i
38
+
39
+ # https://www.rfc-editor.org/rfc/rfc5322#section-3.2.4
40
+ #
41
+ # Strings of characters that include characters other than those
42
+ # allowed in atoms can be represented in a quoted string format, where
43
+ # the characters are surrounded by quote (DQUOTE, ASCII value 34)
44
+ # characters.
45
+ #
46
+ # qtext = %d33 / ; Printable US-ASCII
47
+ # %d35-91 / ; characters not including
48
+ # %d93-126 / ; "\" or the quote character
49
+ # obs-qtext
50
+ #
51
+ # qcontent = qtext / quoted-pair
52
+ # quoted-string = [CFWS]
53
+ # DQUOTE *([FWS] qcontent) [FWS] DQUOTE
54
+ # [CFWS]
55
+ QTEXT = /\A[#{Regexp.escape([33..33, 35..91, 93..126].map { |ascii_range| ascii_range.map(&:chr) }.flatten.join)}\s]/i
56
+
57
+ IP_OCTET = /\A[0-9]+\Z/
58
+
59
+ # From https://datatracker.ietf.org/doc/html/rfc1035#section-2.3.1
60
+ #
61
+ # > The labels must follow the rules for ARPANET host names. They must
62
+ # > start with a letter, end with a letter or digit, and have as interior
63
+ # > characters only letters, digits, and hyphen. There are also some
64
+ # > restrictions on the length. Labels must be 63 characters or less.
65
+ #
66
+ # <label> | <subdomain> "." <label>
67
+ # <label> ::= <letter> [ [ <ldh-str> ] <let-dig> ]
68
+ # <ldh-str> ::= <let-dig-hyp> | <let-dig-hyp> <ldh-str>
69
+ # <let-dig-hyp> ::= <let-dig> | "-"
70
+ # <let-dig> ::= <letter> | <digit>
71
+ DOMAIN_PART_LABEL = /\A[A-Za-z][A-Za-z0-9\-]*[A-Za-z0-9]?\Z/
72
+
73
+ # From https://tools.ietf.org/id/draft-liman-tld-names-00.html#rfc.section.2
74
+ #
75
+ # > A TLD label MUST be at least two characters long and MAY be as long as 63 characters -
76
+ # > not counting any leading or trailing periods (.). It MUST consist of only ASCII characters
77
+ # > from the groups "letters" (A-Z), "digits" (0-9) and "hyphen" (-), and it MUST start with an
78
+ # > ASCII "letter", and it MUST NOT end with a "hyphen". Upper and lower case MAY be mixed at random,
79
+ # > since DNS lookups are case-insensitive.
80
+ #
81
+ # tldlabel = ALPHA *61(ldh) ld
82
+ # ldh = ld / "-"
83
+ # ld = ALPHA / DIGIT
84
+ # ALPHA = %x41-5A / %x61-7A ; A-Z / a-z
85
+ # DIGIT = %x30-39 ; 0-9
86
+ DOMAIN_PART_TLD = /\A[A-Za-z][A-Za-z0-9\-]*[A-Za-z0-9]\Z/
87
+
88
+ def self.validate_email_domain(email, check_mx_timeout: 3)
89
+ domain = email.to_s.downcase.match(/@(.+)/)[1]
16
90
  Resolv::DNS.open do |dns|
91
+ dns.timeouts = check_mx_timeout
17
92
  @mx = dns.getresources(domain, Resolv::DNS::Resource::IN::MX) + dns.getresources(domain, Resolv::DNS::Resource::IN::A)
18
93
  end
19
- @mx.size > 0 ? true : false
94
+ @mx.size > 0
20
95
  end
21
96
 
22
97
  DEFAULT_MESSAGE = "does not appear to be valid"
@@ -25,7 +100,7 @@ module ValidatesEmailFormatOf
25
100
  ERROR_MX_MESSAGE_I18N_KEY = :email_address_not_routable
26
101
 
27
102
  def self.default_message
28
- defined?(I18n) ? I18n.t(ERROR_MESSAGE_I18N_KEY, :scope => [:activemodel, :errors, :messages], :default => DEFAULT_MESSAGE) : DEFAULT_MESSAGE
103
+ defined?(I18n) ? I18n.t(ERROR_MESSAGE_I18N_KEY, scope: [:activemodel, :errors, :messages], default: DEFAULT_MESSAGE) : DEFAULT_MESSAGE
29
104
  end
30
105
 
31
106
  # Validates whether the specified value is a valid email address. Returns nil if the value is valid, otherwise returns an array
@@ -34,57 +109,60 @@ module ValidatesEmailFormatOf
34
109
  # Configuration options:
35
110
  # * <tt>message</tt> - A custom error message (default is: "does not appear to be valid")
36
111
  # * <tt>check_mx</tt> - Check for MX records (default is false)
112
+ # * <tt>check_mx_timeout</tt> - Timeout in seconds for checking MX records before a `ResolvTimeout` is raised (default is 3)
37
113
  # * <tt>mx_message</tt> - A custom error message when an MX record validation fails (default is: "is not routable.")
38
114
  # * <tt>with</tt> The regex to use for validating the format of the email address (deprecated)
39
115
  # * <tt>local_length</tt> Maximum number of characters allowed in the local part (default is 64)
40
116
  # * <tt>domain_length</tt> Maximum number of characters allowed in the domain part (default is 255)
41
117
  # * <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
118
+ def self.validate_email_format(email, options = {})
119
+ default_options = {message: options[:generate_message] ? ERROR_MESSAGE_I18N_KEY : default_message,
120
+ check_mx: false,
121
+ check_mx_timeout: 3,
122
+ mx_message: if options[:generate_message]
123
+ ERROR_MX_MESSAGE_I18N_KEY
124
+ else
125
+ (defined?(I18n) ? I18n.t(ERROR_MX_MESSAGE_I18N_KEY, scope: [:activemodel, :errors, :messages], default: DEFAULT_MX_MESSAGE) : DEFAULT_MX_MESSAGE)
126
+ end,
127
+ domain_length: 255,
128
+ local_length: 64,
129
+ generate_message: false}
130
+ opts = options.merge(default_options) { |key, old, new| old } # merge the default options into the specified options, retaining all specified options
131
+
132
+ begin
133
+ domain, local = email.reverse.split("@", 2)
134
+ rescue
135
+ return [opts[:message]]
136
+ end
59
137
 
60
- # need local and domain parts
61
- return [ opts[:message] ] unless local and not local.empty? and domain and not domain.empty?
138
+ # need local and domain parts
139
+ return [opts[:message]] unless local && !local.empty? && domain && !domain.empty?
62
140
 
63
- # check lengths
64
- return [ opts[:message] ] unless domain.length <= opts[:domain_length] and local.length <= opts[:local_length]
141
+ # check lengths
142
+ return [opts[:message]] unless domain.length <= opts[:domain_length] && local.length <= opts[:local_length]
65
143
 
66
- local.reverse!
67
- domain.reverse!
144
+ local.reverse!
145
+ domain.reverse!
68
146
 
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
147
+ if opts.has_key?(:with) # holdover from versions <= 1.4.7
148
+ return [opts[:message]] unless email&.match?(opts[:with])
149
+ else
150
+ return [opts[:message]] unless validate_local_part_syntax(local) && validate_domain_part_syntax(domain)
151
+ end
74
152
 
75
- if opts[:check_mx] and !self.validate_email_domain(email)
76
- return [ opts[:mx_message] ]
77
- end
153
+ if opts[:check_mx] && !validate_email_domain(email, check_mx_timeout: opts[:check_mx_timeout])
154
+ return [opts[:mx_message]]
155
+ end
78
156
 
79
- return nil # represents no validation errors
157
+ nil # represents no validation errors
80
158
  end
81
159
 
82
-
83
160
  def self.validate_local_part_syntax(local)
84
161
  in_quoted_pair = false
85
162
  in_quoted_string = false
163
+ comment_depth = 0
86
164
 
87
- (0..local.length-1).each do |i|
165
+ (0..local.length - 1).each do |i|
88
166
  ord = local[i].ord
89
167
 
90
168
  # accept anything if it's got a backslash before it
@@ -93,9 +171,26 @@ module ValidatesEmailFormatOf
93
171
  next
94
172
  end
95
173
 
174
+ if in_quoted_string
175
+ next if QTEXT.match?(local[i])
176
+ end
177
+
178
+ # opening paren to show we are going into a comment (CFWS)
179
+ if ord == 40
180
+ comment_depth += 1
181
+ next
182
+ end
183
+
184
+ # closing paren
185
+ if ord == 41
186
+ comment_depth -= 1
187
+ return false if comment_depth < 0
188
+ next
189
+ end
190
+
96
191
  # 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
192
+ if ord == 92 && i < local.length - 1
193
+ return false if !in_quoted_string # must be in quoted string per http://www.rfc-editor.org/errata_search.php?rfc=3696
99
194
  in_quoted_pair = true
100
195
  next
101
196
  end
@@ -106,45 +201,53 @@ module ValidatesEmailFormatOf
106
201
  next
107
202
  end
108
203
 
109
- next if local[i,1] =~ /[a-z0-9]/i
110
- next if local[i,1] =~ LocalPartSpecialChars
204
+ if comment_depth > 0
205
+ next if CTEXT.match?(local[i])
206
+ elsif ATEXT.match?(local[i, 1])
207
+ next
208
+ end
111
209
 
112
210
  # period must be followed by something
113
211
  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
212
+ return false if i == 0 || i == local.length - 1 # can't be first or last char
213
+ next unless local[i + 1].ord == 46 # can't be followed by a period
116
214
  end
117
215
 
118
216
  return false
119
217
  end
120
218
 
121
219
  return false if in_quoted_string # unbalanced quotes
220
+ return false unless comment_depth.zero? # unbalanced comment parens
122
221
 
123
- return true
222
+ true
124
223
  end
125
224
 
126
225
  def self.validate_domain_part_syntax(domain)
127
- parts = domain.downcase.split('.', -1)
226
+ parts = domain.downcase.split(".", -1)
128
227
 
129
228
  return false if parts.length <= 1 # Only one domain part
130
229
 
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
230
  # 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
231
+ return true if parts.length == 4 && parts.all? { |part| part =~ IP_OCTET && part.to_i.between?(0, 255) }
232
+
233
+ # From https://datatracker.ietf.org/doc/html/rfc3696#section-2 this is the recommended, pragmatic way to validate a domain name:
234
+ #
235
+ # > It is likely that the better strategy has now become to make the "at least one period" test,
236
+ # > to verify LDH conformance (including verification that the apparent TLD name is not all-numeric),
237
+ # > and then to use the DNS to determine domain name validity, rather than trying to maintain
238
+ # > a local list of valid TLD names.
239
+ #
240
+ # We do a little bit more but not too much and validate the tokens but do not check against a list of valid TLDs.
241
+ parts.each do |part|
242
+ return false if part.nil? || part.empty?
243
+ return false if part.length > 63
244
+ return false unless DOMAIN_PART_LABEL.match?(part)
245
+ end
144
246
 
145
- return true
247
+ return false unless DOMAIN_PART_TLD.match?(parts[-1])
248
+ true
146
249
  end
147
250
  end
148
251
 
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::Railtie)
252
+ require "validates_email_format_of/active_model" if defined?(::ActiveModel) && !(ActiveModel::VERSION::MAJOR < 2 || (ActiveModel::VERSION::MAJOR == 2 && ActiveModel::VERSION::MINOR < 1))
253
+ require "validates_email_format_of/railtie" if defined?(::Rails::Railtie)
data/rakefile.rb CHANGED
@@ -1 +1,4 @@
1
- require 'bundler/gem_tasks'
1
+ require "bundler/gem_tasks"
2
+ require "standard/rake"
3
+
4
+ task default: [:spec, "standard:fix"]