validates_email_format_of 1.6.3 → 1.7.0

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: 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"]