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 +5 -5
- data/.github/workflows/ci.yml +67 -0
- data/.gitignore +2 -0
- data/Appraisals +32 -0
- data/CHANGELOG +10 -0
- data/README.rdoc +3 -1
- data/config/locales/fr.yml +8 -0
- data/config/locales/it.yml +8 -0
- data/config/locales/ja.yml +8 -0
- data/config/locales/pt-BR.yml +8 -0
- data/config/locales/pt.yml +8 -0
- data/gemfiles/rails_4.2.gemfile +8 -0
- data/gemfiles/rails_5.0.gemfile +8 -0
- data/gemfiles/rails_5.1.gemfile +8 -0
- data/gemfiles/rails_5.2.gemfile +8 -0
- data/gemfiles/rails_6.0.gemfile +7 -0
- data/gemfiles/rails_6.1.gemfile +7 -0
- data/gemfiles/rails_7.0.gemfile +7 -0
- data/lib/validates_email_format_of/active_model.rb +4 -4
- data/lib/validates_email_format_of/railtie.rb +2 -2
- data/lib/validates_email_format_of/rspec_matcher.rb +1 -1
- data/lib/validates_email_format_of/version.rb +1 -1
- data/lib/validates_email_format_of.rb +170 -67
- data/rakefile.rb +4 -1
- data/spec/rspec_matcher_spec.rb +9 -11
- data/spec/spec_helper.rb +6 -3
- data/spec/validates_email_format_of_spec.rb +132 -129
- data/validates_email_format_of.gemspec +21 -19
- metadata +76 -15
- data/.travis.yml +0 -18
- data/gemfiles/Gemfile.active_model_2_1 +0 -5
- data/gemfiles/Gemfile.active_model_3_0 +0 -5
- data/gemfiles/Gemfile.active_model_3_1 +0 -5
- data/gemfiles/Gemfile.active_model_3_2 +0 -5
- data/gemfiles/Gemfile.active_model_3_3 +0 -5
- data/gemfiles/Gemfile.active_model_4_0 +0 -5
- data/gemfiles/Gemfile.active_model_4_1 +0 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 42557ade4662b6a601647924b0a35b0ef09ad48077368e2296185b765902f6b8
|
4
|
+
data.tar.gz: ec3bdeed382f2f2af17377a529ef02e9d5271c77b4f810467403265f0b273f3d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
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
|
|
@@ -1,7 +1,7 @@
|
|
1
|
-
require
|
2
|
-
require
|
1
|
+
require "validates_email_format_of"
|
2
|
+
require "active_model"
|
3
3
|
|
4
|
-
if ActiveModel::VERSION::MAJOR < 2 || (
|
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
|
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
|
4
|
-
ValidatesEmailFormatOf
|
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.
|
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,22 +1,97 @@
|
|
1
|
-
|
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
|
7
|
-
I18n.load_path += Dir.glob(File.expand_path(File.join(File.dirname(__FILE__),
|
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
|
9
|
+
require "resolv"
|
11
10
|
|
12
|
-
|
11
|
+
ATEXT_SYMBOLS = /[!\#$%&'*\-\/=?+\^_`{|}~]/
|
13
12
|
|
14
|
-
|
15
|
-
|
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
|
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, :
|
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
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
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
|
-
|
61
|
-
|
138
|
+
# need local and domain parts
|
139
|
+
return [opts[:message]] unless local && !local.empty? && domain && !domain.empty?
|
62
140
|
|
63
|
-
|
64
|
-
|
141
|
+
# check lengths
|
142
|
+
return [opts[:message]] unless domain.length <= opts[:domain_length] && local.length <= opts[:local_length]
|
65
143
|
|
66
|
-
|
67
|
-
|
144
|
+
local.reverse!
|
145
|
+
domain.reverse!
|
68
146
|
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
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
|
-
|
76
|
-
|
77
|
-
|
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
|
-
|
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
|
98
|
-
return false if
|
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
|
-
|
110
|
-
|
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
|
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
|
-
|
222
|
+
true
|
124
223
|
end
|
125
224
|
|
126
225
|
def self.validate_domain_part_syntax(domain)
|
127
|
-
parts = domain.downcase.split(
|
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
|
142
|
-
|
143
|
-
|
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
|
247
|
+
return false unless DOMAIN_PART_TLD.match?(parts[-1])
|
248
|
+
true
|
146
249
|
end
|
147
250
|
end
|
148
251
|
|
149
|
-
require
|
150
|
-
require
|
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