valid_email2 7.0.15 → 7.1.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 +4 -4
- data/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +12 -0
- data/Gemfile +4 -0
- data/README.md +20 -0
- data/config/disposable_email_domains.txt +4632 -1
- data/lib/valid_email2/address.rb +20 -9
- data/lib/valid_email2/email_validator.rb +22 -12
- data/lib/valid_email2/version.rb +1 -1
- data/pull_mailchecker_emails.rb +1 -0
- data/spec/address_spec.rb +10 -0
- data/spec/spec_helper.rb +2 -0
- data/spec/valid_email2_spec.rb +71 -0
- metadata +2 -2
data/lib/valid_email2/address.rb
CHANGED
|
@@ -8,9 +8,12 @@ module ValidEmail2
|
|
|
8
8
|
class Address
|
|
9
9
|
attr_accessor :address
|
|
10
10
|
|
|
11
|
-
PROHIBITED_DOMAIN_CHARACTERS_REGEX = /[+!_\/\s'
|
|
11
|
+
PROHIBITED_DOMAIN_CHARACTERS_REGEX = /[+!_\/\s'#`|]/
|
|
12
12
|
DEFAULT_RECIPIENT_DELIMITER = '+'
|
|
13
13
|
DOT_DELIMITER = '.'
|
|
14
|
+
HYPHEN_DELIMITER = '-'
|
|
15
|
+
MAX_DOMAIN_LENGTH = 253
|
|
16
|
+
MAX_LABEL_LENGTH = 63
|
|
14
17
|
|
|
15
18
|
def self.prohibited_domain_characters_regex
|
|
16
19
|
@prohibited_domain_characters_regex ||= PROHIBITED_DOMAIN_CHARACTERS_REGEX
|
|
@@ -52,13 +55,13 @@ module ValidEmail2
|
|
|
52
55
|
def valid_domain?
|
|
53
56
|
domain = address.domain
|
|
54
57
|
return false if domain.nil?
|
|
58
|
+
return false if domain =~ self.class.prohibited_domain_characters_regex
|
|
59
|
+
return false if domain.length > MAX_DOMAIN_LENGTH
|
|
55
60
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
!domain.start_with?('-') &&
|
|
61
|
-
!domain.include?('-.')
|
|
61
|
+
labels = domain.split(DOT_DELIMITER, -1)
|
|
62
|
+
return false if labels.length < 2
|
|
63
|
+
|
|
64
|
+
labels.all? { |label| valid_domain_label?(label) }
|
|
62
65
|
end
|
|
63
66
|
|
|
64
67
|
def valid_address?
|
|
@@ -82,11 +85,11 @@ module ValidEmail2
|
|
|
82
85
|
end
|
|
83
86
|
|
|
84
87
|
def disposable_domain?
|
|
85
|
-
domain_is_in?(address.domain, ValidEmail2.disposable_emails)
|
|
88
|
+
valid? && domain_is_in?(address.domain, ValidEmail2.disposable_emails)
|
|
86
89
|
end
|
|
87
90
|
|
|
88
91
|
def allow_listed?
|
|
89
|
-
domain_is_in?(address.domain, ValidEmail2.allow_list)
|
|
92
|
+
valid? && domain_is_in?(address.domain, ValidEmail2.allow_list)
|
|
90
93
|
end
|
|
91
94
|
|
|
92
95
|
def deny_listed?
|
|
@@ -109,6 +112,14 @@ module ValidEmail2
|
|
|
109
112
|
|
|
110
113
|
private
|
|
111
114
|
|
|
115
|
+
# An RFC 1035 label: 1-63 octets, and it may not begin or end with a hyphen.
|
|
116
|
+
def valid_domain_label?(label)
|
|
117
|
+
!label.empty? &&
|
|
118
|
+
label.length <= MAX_LABEL_LENGTH &&
|
|
119
|
+
!label.start_with?(HYPHEN_DELIMITER) &&
|
|
120
|
+
!label.end_with?(HYPHEN_DELIMITER)
|
|
121
|
+
end
|
|
122
|
+
|
|
112
123
|
def disposable_mx_server?
|
|
113
124
|
mx_server_is_in?(ValidEmail2.disposable_emails)
|
|
114
125
|
end
|
|
@@ -6,7 +6,16 @@ require "active_model/validations"
|
|
|
6
6
|
module ValidEmail2
|
|
7
7
|
class EmailValidator < ActiveModel::EachValidator
|
|
8
8
|
def default_options
|
|
9
|
-
{
|
|
9
|
+
{
|
|
10
|
+
disposable: false,
|
|
11
|
+
mx: false,
|
|
12
|
+
strict_mx: false,
|
|
13
|
+
disallow_subaddressing: false,
|
|
14
|
+
multiple: false,
|
|
15
|
+
dns_timeout: 5,
|
|
16
|
+
dns_nameserver: nil,
|
|
17
|
+
specific_error_messages: false
|
|
18
|
+
}
|
|
10
19
|
end
|
|
11
20
|
|
|
12
21
|
def validate_each(record, attribute, value)
|
|
@@ -19,39 +28,39 @@ module ValidEmail2
|
|
|
19
28
|
error(record, attribute) && return unless addresses.all?(&:valid?)
|
|
20
29
|
|
|
21
30
|
if options[:disallow_dotted]
|
|
22
|
-
error(record, attribute) && return if addresses.any?(&:dotted?)
|
|
31
|
+
error(record, attribute, :disallow_dotted) && return if addresses.any?(&:dotted?)
|
|
23
32
|
end
|
|
24
33
|
|
|
25
34
|
if options[:disallow_subaddressing]
|
|
26
|
-
error(record, attribute) && return if addresses.any?(&:subaddressed?)
|
|
35
|
+
error(record, attribute, :disallow_subaddressing) && return if addresses.any?(&:subaddressed?)
|
|
27
36
|
end
|
|
28
37
|
|
|
29
38
|
if options[:disposable]
|
|
30
|
-
error(record, attribute) && return if addresses.any?(&:disposable?)
|
|
39
|
+
error(record, attribute, :disposable) && return if addresses.any?(&:disposable?)
|
|
31
40
|
end
|
|
32
41
|
|
|
33
42
|
if options[:disposable_domain]
|
|
34
|
-
error(record, attribute) && return if addresses.any?(&:disposable_domain?)
|
|
43
|
+
error(record, attribute, :disposable_domain) && return if addresses.any?(&:disposable_domain?)
|
|
35
44
|
end
|
|
36
45
|
|
|
37
46
|
if options[:disposable_with_allow_list]
|
|
38
|
-
error(record, attribute) && return if addresses.any? { |address| address.disposable? && !address.allow_listed? }
|
|
47
|
+
error(record, attribute, :disposable_with_allow_list) && return if addresses.any? { |address| address.disposable? && !address.allow_listed? }
|
|
39
48
|
end
|
|
40
49
|
|
|
41
50
|
if options[:disposable_domain_with_allow_list]
|
|
42
|
-
error(record, attribute) && return if addresses.any? { |address| address.disposable_domain? && !address.allow_listed? }
|
|
51
|
+
error(record, attribute, :disposable_domain_with_allow_list) && return if addresses.any? { |address| address.disposable_domain? && !address.allow_listed? }
|
|
43
52
|
end
|
|
44
53
|
|
|
45
54
|
if options[:deny_list]
|
|
46
|
-
error(record, attribute) && return if addresses.any?(&:deny_listed?)
|
|
55
|
+
error(record, attribute, :deny_list) && return if addresses.any?(&:deny_listed?)
|
|
47
56
|
end
|
|
48
57
|
|
|
49
58
|
if options[:mx]
|
|
50
|
-
error(record, attribute) && return unless addresses.all?(&:valid_mx?)
|
|
59
|
+
error(record, attribute, :mx) && return unless addresses.all?(&:valid_mx?)
|
|
51
60
|
end
|
|
52
61
|
|
|
53
62
|
if options[:strict_mx]
|
|
54
|
-
error(record, attribute) && return unless addresses.all?(&:valid_strict_mx?)
|
|
63
|
+
error(record, attribute, :strict_mx) && return unless addresses.all?(&:valid_strict_mx?)
|
|
55
64
|
end
|
|
56
65
|
end
|
|
57
66
|
|
|
@@ -67,10 +76,11 @@ module ValidEmail2
|
|
|
67
76
|
email_list.reject(&:empty?)
|
|
68
77
|
end
|
|
69
78
|
|
|
70
|
-
def error(record, attribute)
|
|
79
|
+
def error(record, attribute, error_key = :invalid)
|
|
71
80
|
message = options[:message].respond_to?(:call) ? options[:message].call : options[:message]
|
|
81
|
+
message ||= options[:specific_error_messages] ? error_key : :invalid
|
|
72
82
|
|
|
73
|
-
record.errors.add(attribute, message
|
|
83
|
+
record.errors.add(attribute, message)
|
|
74
84
|
end
|
|
75
85
|
end
|
|
76
86
|
end
|
data/lib/valid_email2/version.rb
CHANGED
data/pull_mailchecker_emails.rb
CHANGED
|
@@ -16,6 +16,7 @@ allow_listed_emails = %w[
|
|
|
16
16
|
duck.com mozmail.com dralias.com 8alias.com 8shield.net
|
|
17
17
|
mailinblack.com anonaddy.com anonaddy.me addy.io privaterelay.appleid.com appleid.com
|
|
18
18
|
net.ua kommespaeter.de alpenjodel.de my.id web.id directbox.com embarqmail.com
|
|
19
|
+
unlimit.com reallyfast.info
|
|
19
20
|
]
|
|
20
21
|
|
|
21
22
|
existing_emails = File.open("config/disposable_email_domains.txt") { |f| f.read.split("\n") }
|
data/spec/address_spec.rb
CHANGED
|
@@ -38,6 +38,11 @@ describe ValidEmail2::Address do
|
|
|
38
38
|
expect(address.valid?).to be false
|
|
39
39
|
end
|
|
40
40
|
|
|
41
|
+
it "is invalid if email contains a pipe" do
|
|
42
|
+
address = described_class.new("foo@gmai|.com")
|
|
43
|
+
expect(address.valid?).to be false
|
|
44
|
+
end
|
|
45
|
+
|
|
41
46
|
it "is invalid if it contains Japanese characters" do
|
|
42
47
|
address = described_class.new("あいうえお@example.com")
|
|
43
48
|
expect(address.valid?).to be false
|
|
@@ -83,6 +88,11 @@ describe ValidEmail2::Address do
|
|
|
83
88
|
address = described_class.new("foo@example.com")
|
|
84
89
|
expect(address.disposable_domain?).to eq false
|
|
85
90
|
end
|
|
91
|
+
|
|
92
|
+
it "is false if the email cannot be parsed" do
|
|
93
|
+
address = described_class.new('"><test>')
|
|
94
|
+
expect(address.disposable_domain?).to eq false
|
|
95
|
+
end
|
|
86
96
|
end
|
|
87
97
|
|
|
88
98
|
context "when the disposable domain has subdomains" do
|
data/spec/spec_helper.rb
CHANGED
data/spec/valid_email2_spec.rb
CHANGED
|
@@ -7,6 +7,10 @@ class TestUser < TestModel
|
|
|
7
7
|
validates :email, 'valid_email_2/email': true
|
|
8
8
|
end
|
|
9
9
|
|
|
10
|
+
class TestUserSpecificErrorMessage < TestModel
|
|
11
|
+
validates :email, 'valid_email_2/email': { disallow_dotted: true, specific_error_messages: true }
|
|
12
|
+
end
|
|
13
|
+
|
|
10
14
|
class TestUserDotted < TestModel
|
|
11
15
|
validates :email, 'valid_email_2/email': { disallow_dotted: true }
|
|
12
16
|
end
|
|
@@ -171,6 +175,48 @@ describe ValidEmail2 do
|
|
|
171
175
|
user = TestUser.new(email: "foo@example.com#")
|
|
172
176
|
expect(user.valid?).to be_falsey
|
|
173
177
|
end
|
|
178
|
+
|
|
179
|
+
describe "domain label structure" do
|
|
180
|
+
# A domain label may not begin or end with a hyphen and is limited to 63
|
|
181
|
+
# octets; the whole domain is limited to 253. These rules apply to every
|
|
182
|
+
# label, not only the first/last position of the domain string.
|
|
183
|
+
long_label = "a" * 64
|
|
184
|
+
long_domain = (["aa"] + ["a"] * 126).join(".") # 254 chars, all-valid labels
|
|
185
|
+
|
|
186
|
+
{
|
|
187
|
+
"a leading hyphen on the first label" => "foo@-example.com",
|
|
188
|
+
"a leading hyphen on a later label" => "foo@sub.-example.com",
|
|
189
|
+
"a leading hyphen on a deeply nested label" => "foo@a.b.-c.example.com",
|
|
190
|
+
"a trailing hyphen on a non-final label" => "foo@example-.com",
|
|
191
|
+
"a trailing hyphen on the final label" => "foo@example.com-",
|
|
192
|
+
"a bare hyphen label" => "foo@example.-.com",
|
|
193
|
+
"a label longer than 63 octets" => "foo@#{long_label}.com",
|
|
194
|
+
"a later label longer than 63 octets" => "foo@example.#{long_label}.com",
|
|
195
|
+
"a domain longer than 253 octets" => "foo@#{long_domain}"
|
|
196
|
+
}.each do |description, email|
|
|
197
|
+
it "is invalid with #{description}" do
|
|
198
|
+
expect(TestUser.new(email: email).valid?).to be_falsey
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
full_label = "a" * 63
|
|
203
|
+
max_domain = (["a"] * 127).join(".") # exactly 253 chars
|
|
204
|
+
|
|
205
|
+
{
|
|
206
|
+
"an internal hyphen" => "foo@ex-ample.com",
|
|
207
|
+
"hyphens on several internal labels" => "foo@f-o-o.example.com",
|
|
208
|
+
"a double internal hyphen" => "foo@a--b.example.com",
|
|
209
|
+
"a punycode (IDNA ACE) label" => "foo@xn--bcher-kva.com",
|
|
210
|
+
"a label beginning with a digit" => "foo@3m.com",
|
|
211
|
+
"a 63 octet label at the limit" => "foo@#{full_label}.com",
|
|
212
|
+
"a later 63 octet label" => "foo@example.#{full_label}.com",
|
|
213
|
+
"a domain at the 253 octet limit" => "foo@#{max_domain}"
|
|
214
|
+
}.each do |description, email|
|
|
215
|
+
it "is valid with #{description}" do
|
|
216
|
+
expect(TestUser.new(email: email).valid?).to be_truthy
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
end
|
|
174
220
|
end
|
|
175
221
|
|
|
176
222
|
describe "with disposable validation" do
|
|
@@ -418,6 +464,7 @@ describe ValidEmail2 do
|
|
|
418
464
|
it "is invalid when address cotains dots" do
|
|
419
465
|
user = TestUserDotted.new(email: "john.doe@gmail.com")
|
|
420
466
|
expect(user.valid?).to be_falsey
|
|
467
|
+
expect(user.errors.full_messages).to include("Email is invalid")
|
|
421
468
|
end
|
|
422
469
|
end
|
|
423
470
|
|
|
@@ -433,6 +480,30 @@ describe ValidEmail2 do
|
|
|
433
480
|
end
|
|
434
481
|
end
|
|
435
482
|
|
|
483
|
+
context "when detail error messages are enabled" do
|
|
484
|
+
describe "with custom error message" do
|
|
485
|
+
it "supports settings a custom error message" do
|
|
486
|
+
with_translations(:en, { errors: { messages: { disallow_dotted: "dotted message" } } }) do
|
|
487
|
+
user = TestUserSpecificErrorMessage.new(email: "john.doe@gmail.com")
|
|
488
|
+
user.valid?
|
|
489
|
+
|
|
490
|
+
expect(user.errors.full_messages).to include("Email dotted message")
|
|
491
|
+
end
|
|
492
|
+
end
|
|
493
|
+
end
|
|
494
|
+
end
|
|
495
|
+
|
|
496
|
+
def with_translations(locale, translations)
|
|
497
|
+
original_backend = I18n.backend
|
|
498
|
+
|
|
499
|
+
I18n.backend = I18n::Backend::KeyValue.new({}, true)
|
|
500
|
+
I18n.backend.store_translations locale, translations
|
|
501
|
+
|
|
502
|
+
yield
|
|
503
|
+
ensure
|
|
504
|
+
I18n.backend = original_backend
|
|
505
|
+
end
|
|
506
|
+
|
|
436
507
|
context "when message is present" do
|
|
437
508
|
describe "with custom error message" do
|
|
438
509
|
it "supports settings a custom error message" do
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: valid_email2
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 7.0
|
|
4
|
+
version: 7.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Micke Lisinge
|
|
@@ -205,7 +205,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
205
205
|
- !ruby/object:Gem::Version
|
|
206
206
|
version: '0'
|
|
207
207
|
requirements: []
|
|
208
|
-
rubygems_version: 4.0.
|
|
208
|
+
rubygems_version: 4.0.19
|
|
209
209
|
specification_version: 4
|
|
210
210
|
summary: ActiveModel validation for email. Including MX lookup and disposable email
|
|
211
211
|
deny list
|