valid_email2 3.4.0 → 4.0.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/.travis.yml +3 -5
- data/CHANGELOG.md +20 -0
- data/README.md +21 -4
- data/config/disposable_email_domains.txt +159 -2
- data/lib/valid_email2/address.rb +43 -21
- data/lib/valid_email2/email_validator.rb +22 -3
- data/lib/valid_email2/version.rb +3 -1
- data/pull_mailchecker_emails.rb +1 -1
- data/spec/valid_email2_spec.rb +63 -0
- metadata +3 -3
data/lib/valid_email2/address.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal:true
|
2
|
+
|
1
3
|
require "valid_email2"
|
2
4
|
require "resolv"
|
3
5
|
require "mail"
|
@@ -7,8 +9,8 @@ module ValidEmail2
|
|
7
9
|
attr_accessor :address
|
8
10
|
|
9
11
|
PROHIBITED_DOMAIN_CHARACTERS_REGEX = /[+!_\/\s'`]/
|
10
|
-
DEFAULT_RECIPIENT_DELIMITER = '+'
|
11
|
-
DOT_DELIMITER = '.'
|
12
|
+
DEFAULT_RECIPIENT_DELIMITER = '+'
|
13
|
+
DOT_DELIMITER = '.'
|
12
14
|
|
13
15
|
def self.prohibited_domain_characters_regex
|
14
16
|
@prohibited_domain_characters_regex ||= PROHIBITED_DOMAIN_CHARACTERS_REGEX
|
@@ -18,9 +20,10 @@ module ValidEmail2
|
|
18
20
|
@prohibited_domain_characters_regex = val
|
19
21
|
end
|
20
22
|
|
21
|
-
def initialize(address)
|
23
|
+
def initialize(address, dns_timeout = 5)
|
22
24
|
@parse_error = false
|
23
25
|
@raw_address = address
|
26
|
+
@dns_timeout = dns_timeout
|
24
27
|
|
25
28
|
begin
|
26
29
|
@address = Mail::Address.new(address)
|
@@ -35,22 +38,27 @@ module ValidEmail2
|
|
35
38
|
return @valid unless @valid.nil?
|
36
39
|
return false if @parse_error
|
37
40
|
|
38
|
-
@valid =
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
41
|
+
@valid = address.domain &&
|
42
|
+
address.address == @raw_address &&
|
43
|
+
valid_domain? &&
|
44
|
+
valid_address?
|
45
|
+
end
|
46
|
+
|
47
|
+
def valid_domain?
|
48
|
+
domain = address.domain
|
49
|
+
|
50
|
+
domain !~ self.class.prohibited_domain_characters_regex &&
|
51
|
+
domain.include?('.') &&
|
52
|
+
!domain.include?('..') &&
|
53
|
+
!domain.start_with?('.') &&
|
54
|
+
!domain.start_with?('-') &&
|
55
|
+
!domain.include?('-.')
|
56
|
+
end
|
57
|
+
|
58
|
+
def valid_address?
|
59
|
+
!address.local.include?('..') &&
|
60
|
+
!address.local.end_with?('.') &&
|
61
|
+
!address.local.start_with?('.')
|
54
62
|
end
|
55
63
|
|
56
64
|
def dotted?
|
@@ -84,6 +92,12 @@ module ValidEmail2
|
|
84
92
|
def valid_mx?
|
85
93
|
return false unless valid?
|
86
94
|
|
95
|
+
mx_or_a_servers.any?
|
96
|
+
end
|
97
|
+
|
98
|
+
def valid_strict_mx?
|
99
|
+
return false unless valid?
|
100
|
+
|
87
101
|
mx_servers.any?
|
88
102
|
end
|
89
103
|
|
@@ -96,12 +110,13 @@ module ValidEmail2
|
|
96
110
|
i = address_domain.index('.')
|
97
111
|
return false unless i
|
98
112
|
|
99
|
-
|
113
|
+
domain_list.include?(address_domain[(i + 1)..-1])
|
100
114
|
end
|
101
115
|
|
102
116
|
def mx_server_is_in?(domain_list)
|
103
117
|
mx_servers.any? { |mx_server|
|
104
118
|
return false unless mx_server.respond_to?(:exchange)
|
119
|
+
|
105
120
|
mx_server = mx_server.exchange.to_s
|
106
121
|
|
107
122
|
domain_list.any? { |domain|
|
@@ -118,7 +133,14 @@ module ValidEmail2
|
|
118
133
|
|
119
134
|
def mx_servers
|
120
135
|
@mx_servers ||= Resolv::DNS.open do |dns|
|
121
|
-
|
136
|
+
dns.timeouts = @dns_timeout
|
137
|
+
dns.getresources(address.domain, Resolv::DNS::Resource::IN::MX)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def mx_or_a_servers
|
142
|
+
@mx_or_a_servers ||= Resolv::DNS.open do |dns|
|
143
|
+
dns.timeouts = @dns_timeout
|
122
144
|
(mx_servers.any? && mx_servers) ||
|
123
145
|
dns.getresources(address.domain, Resolv::DNS::Resource::IN::A)
|
124
146
|
end
|
@@ -5,15 +5,14 @@ require "active_model/validations"
|
|
5
5
|
module ValidEmail2
|
6
6
|
class EmailValidator < ActiveModel::EachValidator
|
7
7
|
def default_options
|
8
|
-
{ regex: true, disposable: false, mx: false, disallow_subaddressing: false, multiple: false }
|
8
|
+
{ regex: true, disposable: false, mx: false, strict_mx: false, disallow_subaddressing: false, multiple: false, dns_timeout: 5 }
|
9
9
|
end
|
10
10
|
|
11
11
|
def validate_each(record, attribute, value)
|
12
12
|
return unless value.present?
|
13
13
|
options = default_options.merge(self.options)
|
14
14
|
|
15
|
-
|
16
|
-
addresses = value_spitted.map { |v| ValidEmail2::Address.new(v) }
|
15
|
+
addresses = sanitized_values(value).map { |v| ValidEmail2::Address.new(v, options[:dns_timeout]) }
|
17
16
|
|
18
17
|
error(record, attribute) && return unless addresses.all?(&:valid?)
|
19
18
|
|
@@ -37,6 +36,10 @@ module ValidEmail2
|
|
37
36
|
error(record, attribute) && return if addresses.any? { |address| address.disposable? && !address.whitelisted? }
|
38
37
|
end
|
39
38
|
|
39
|
+
if options[:disposable_domain_with_whitelist]
|
40
|
+
error(record, attribute) && return if addresses.any? { |address| address.disposable_domain? && !address.whitelisted? }
|
41
|
+
end
|
42
|
+
|
40
43
|
if options[:blacklist]
|
41
44
|
error(record, attribute) && return if addresses.any?(&:blacklisted?)
|
42
45
|
end
|
@@ -44,6 +47,22 @@ module ValidEmail2
|
|
44
47
|
if options[:mx]
|
45
48
|
error(record, attribute) && return unless addresses.all?(&:valid_mx?)
|
46
49
|
end
|
50
|
+
|
51
|
+
if options[:strict_mx]
|
52
|
+
error(record, attribute) && return unless addresses.all?(&:valid_strict_mx?)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def sanitized_values(input)
|
57
|
+
options = default_options.merge(self.options)
|
58
|
+
|
59
|
+
if options[:multiple]
|
60
|
+
email_list = input.is_a?(Array) ? input : input.split(',')
|
61
|
+
else
|
62
|
+
email_list = [input]
|
63
|
+
end
|
64
|
+
|
65
|
+
email_list.reject(&:empty?).map(&:strip)
|
47
66
|
end
|
48
67
|
|
49
68
|
def error(record, attribute)
|
data/lib/valid_email2/version.rb
CHANGED
data/pull_mailchecker_emails.rb
CHANGED
@@ -8,7 +8,7 @@ require "net/http"
|
|
8
8
|
whitelisted_emails = %w(
|
9
9
|
onet.pl poczta.onet.pl fastmail.fm hushmail.com
|
10
10
|
hush.ai hush.com hushmail.me naver.com qq.com example.com
|
11
|
-
yandex.net gmx.com gmx.es
|
11
|
+
yandex.net gmx.com gmx.es webdesignspecialist.com.au vp.com
|
12
12
|
)
|
13
13
|
|
14
14
|
existing_emails = File.open("config/disposable_email_domains.txt") { |f| f.read.split("\n") }
|
data/spec/valid_email2_spec.rb
CHANGED
@@ -19,6 +19,10 @@ class TestUserMX < TestModel
|
|
19
19
|
validates :email, 'valid_email_2/email': { mx: true }
|
20
20
|
end
|
21
21
|
|
22
|
+
class TestUserStrictMX < TestModel
|
23
|
+
validates :email, 'valid_email_2/email': { strict_mx: true }
|
24
|
+
end
|
25
|
+
|
22
26
|
class TestUserDisallowDisposable < TestModel
|
23
27
|
validates :email, 'valid_email_2/email': { disposable: true }
|
24
28
|
end
|
@@ -31,6 +35,10 @@ class TestUserDisallowDisposableWithWhitelist < TestModel
|
|
31
35
|
validates :email, 'valid_email_2/email': { disposable_with_whitelist: true }
|
32
36
|
end
|
33
37
|
|
38
|
+
class TestUserDisallowDisposableDomainWithWhitelist < TestModel
|
39
|
+
validates :email, 'valid_email_2/email': { disposable_domain_with_whitelist: true }
|
40
|
+
end
|
41
|
+
|
34
42
|
class TestUserDisallowBlacklisted < TestModel
|
35
43
|
validates :email, 'valid_email_2/email': { blacklist: true }
|
36
44
|
end
|
@@ -92,6 +100,16 @@ describe ValidEmail2 do
|
|
92
100
|
expect(user.valid?).to be_falsey
|
93
101
|
end
|
94
102
|
|
103
|
+
it "is invalid if the address starts with a dot" do
|
104
|
+
user = TestUser.new(email: ".foo@bar.com")
|
105
|
+
expect(user.valid?).to be_falsey
|
106
|
+
end
|
107
|
+
|
108
|
+
it "is invalid if the local part of the address ends with a dot" do
|
109
|
+
user = TestUser.new(email: "foo.@bar.com")
|
110
|
+
expect(user.valid?).to be_falsey
|
111
|
+
end
|
112
|
+
|
95
113
|
it "is invalid if the email contains emoticons" do
|
96
114
|
user = TestUser.new(email: "foo🙈@gmail.com")
|
97
115
|
expect(user.valid?).to be_falsy
|
@@ -170,8 +188,18 @@ describe ValidEmail2 do
|
|
170
188
|
let(:whitelist_domain) { disposable_domain }
|
171
189
|
let(:whitelist_file_path) { "config/whitelisted_email_domains.yml" }
|
172
190
|
|
191
|
+
# Some of the specs below need to explictly set the whitelist var or it
|
192
|
+
# may be cached to an empty set
|
193
|
+
def set_whitelist
|
194
|
+
ValidEmail2.instance_variable_set(
|
195
|
+
:@whitelist,
|
196
|
+
ValidEmail2.send(:load_if_exists, ValidEmail2::WHITELIST_FILE)
|
197
|
+
)
|
198
|
+
end
|
199
|
+
|
173
200
|
after do
|
174
201
|
FileUtils.rm(whitelist_file_path, force: true)
|
202
|
+
set_whitelist
|
175
203
|
end
|
176
204
|
|
177
205
|
it "is invalid if the domain is disposable and not in the whitelist" do
|
@@ -181,9 +209,22 @@ describe ValidEmail2 do
|
|
181
209
|
|
182
210
|
it "is valid if the domain is disposable but in the whitelist" do
|
183
211
|
File.open(whitelist_file_path, "w") { |f| f.write [whitelist_domain].to_yaml }
|
212
|
+
set_whitelist
|
184
213
|
user = TestUserDisallowDisposableWithWhitelist.new(email: "foo@#{whitelist_domain}")
|
214
|
+
expect(user.valid?).to be_truthy
|
215
|
+
end
|
216
|
+
|
217
|
+
it "is invalid if the domain is a disposable_domain and not in the whitelist" do
|
218
|
+
user = TestUserDisallowDisposableDomainWithWhitelist.new(email: "foo@#{whitelist_domain}")
|
185
219
|
expect(user.valid?).to be_falsey
|
186
220
|
end
|
221
|
+
|
222
|
+
it "is valid if the domain is a disposable_domain but in the whitelist" do
|
223
|
+
File.open(whitelist_file_path, "w") { |f| f.write [whitelist_domain].to_yaml }
|
224
|
+
set_whitelist
|
225
|
+
user = TestUserDisallowDisposableDomainWithWhitelist.new(email: "foo@#{whitelist_domain}")
|
226
|
+
expect(user.valid?).to be_truthy
|
227
|
+
end
|
187
228
|
end
|
188
229
|
end
|
189
230
|
|
@@ -216,6 +257,23 @@ describe ValidEmail2 do
|
|
216
257
|
end
|
217
258
|
end
|
218
259
|
|
260
|
+
describe "with strict mx validation" do
|
261
|
+
it "is valid if mx records are found" do
|
262
|
+
user = TestUserStrictMX.new(email: "foo@gmail.com")
|
263
|
+
expect(user.valid?).to be_truthy
|
264
|
+
end
|
265
|
+
|
266
|
+
it "is invalid if A records are found but no mx records are found" do
|
267
|
+
user = TestUserStrictMX.new(email: "foo@ghs.google.com")
|
268
|
+
expect(user.valid?).to be_falsey
|
269
|
+
end
|
270
|
+
|
271
|
+
it "is invalid if no mx records are found" do
|
272
|
+
user = TestUserStrictMX.new(email: "foo@subdomain.gmail.com")
|
273
|
+
expect(user.valid?).to be_falsey
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
219
277
|
describe "with dotted validation" do
|
220
278
|
it "is valid when address does not contain dots" do
|
221
279
|
user = TestUserDotted.new(email: "johndoe@gmail.com")
|
@@ -254,6 +312,11 @@ describe ValidEmail2 do
|
|
254
312
|
expect(user.valid?).to be_truthy
|
255
313
|
end
|
256
314
|
|
315
|
+
it "tests each address from an array" do
|
316
|
+
user = TestUserMultiple.new(email: %w[foo@gmail.com bar@gmail.com])
|
317
|
+
expect(user.valid?).to be_truthy
|
318
|
+
end
|
319
|
+
|
257
320
|
context 'when one address is invalid' do
|
258
321
|
it "fails for all" do
|
259
322
|
user = TestUserMultiple.new(email: "foo@gmail.com, bar@123")
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: valid_email2
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 4.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Micke Lisinge
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2021-06-29 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -157,7 +157,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
157
157
|
- !ruby/object:Gem::Version
|
158
158
|
version: '0'
|
159
159
|
requirements: []
|
160
|
-
rubygems_version: 3.
|
160
|
+
rubygems_version: 3.2.3
|
161
161
|
signing_key:
|
162
162
|
specification_version: 4
|
163
163
|
summary: ActiveModel validation for email. Including MX lookup and disposable email
|