valid_email2 5.2.6 → 6.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yaml +1 -1
- data/.github/workflows/release.yaml +1 -1
- data/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +28 -0
- data/README.md +15 -6
- data/config/deny_listed_email_domains.yml +1 -0
- data/config/disposable_email_domains.txt +0 -1
- data/lib/valid_email2/address.rb +37 -19
- data/lib/valid_email2/dns_records_cache.rb +37 -0
- data/lib/valid_email2/email_validator.rb +8 -8
- data/lib/valid_email2/version.rb +1 -1
- data/lib/valid_email2.rb +7 -7
- data/pull_mailchecker_emails.rb +3 -3
- data/release-please-config.json +0 -1
- data/spec/address_spec.rb +284 -0
- data/spec/spec_helper.rb +1 -0
- data/spec/valid_email2_spec.rb +42 -32
- data/valid_email2.gemspec +5 -4
- metadata +23 -8
- data/config/blacklisted_email_domains.yml +0 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a0296ec313cffb7390cb7ca2b22d48f15854bbb1cc8dc1442b31389b7350f31b
|
4
|
+
data.tar.gz: 11da87b6097bdc4df988d16045809dc42336b68b2f4fdf087a1ad38013619061
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e6003807039faf5c307d53b2617adcc5c0a0e75055f560e48073ea9cc1d5093f50aba58f2cbe121cc913b99ba8df62789e11a59d236cd0b13b17ad800925e5a5
|
7
|
+
data.tar.gz: 981042cfa3c8a72f101ba40a3922a0a3c2c2aa26ea0635ac4beb0ce59917171ec0ff1126dde68c77edb42939e4618372960c88645f720ad588e6735a2c392baf
|
data/.github/workflows/ci.yaml
CHANGED
@@ -1 +1 @@
|
|
1
|
-
{".":"
|
1
|
+
{".":"6.0.0"}
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,33 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
+
## [6.0.0](https://github.com/micke/valid_email2/compare/v5.3.0...v6.0.0) (2024-11-03)
|
4
|
+
|
5
|
+
|
6
|
+
### ⚠ BREAKING CHANGES
|
7
|
+
|
8
|
+
* Remove deprecated methods and options
|
9
|
+
|
10
|
+
### Features
|
11
|
+
|
12
|
+
* Cache DNS lookups ([#256](https://github.com/micke/valid_email2/issues/256)) ([72115ec](https://github.com/micke/valid_email2/commit/72115ec1b866e54b5a4d530d7eaeb7e52a3c8e98))
|
13
|
+
* Remove deprecated methods and options ([1a29d27](https://github.com/micke/valid_email2/commit/1a29d27d587a39d181dfe2b6f39028bc317aff52))
|
14
|
+
|
15
|
+
|
16
|
+
### Bug Fixes
|
17
|
+
|
18
|
+
* disallow # in domain ([#259](https://github.com/micke/valid_email2/issues/259)) ([1643323](https://github.com/micke/valid_email2/commit/1643323fa3da8973cb63a727410aa9696706e3c8))
|
19
|
+
* **email_validator:** handle trailing whitespace in emails ([#255](https://github.com/micke/valid_email2/issues/255)) ([a06fdf7](https://github.com/micke/valid_email2/commit/a06fdf72cfc0df51c81057c6094aba332b692741))
|
20
|
+
* Falsely detecting Scandinavian characters as emojis ([#257](https://github.com/micke/valid_email2/issues/257)) ([e64de5c](https://github.com/micke/valid_email2/commit/e64de5c675a015c09c7e6b89bb4f65a39137f48f))
|
21
|
+
* typo in readme about config file to deny email domains ([#254](https://github.com/micke/valid_email2/issues/254)) ([a39ed57](https://github.com/micke/valid_email2/commit/a39ed5792a9d46abd954fceff929770eabf99a73))
|
22
|
+
|
23
|
+
## [5.3.0](https://github.com/micke/valid_email2/compare/v5.2.6...v5.3.0) (2024-08-31)
|
24
|
+
|
25
|
+
|
26
|
+
### Features
|
27
|
+
|
28
|
+
* Deprecate blacklist and whitelist naming ([9df0bf8](https://github.com/micke/valid_email2/commit/9df0bf8c9912721b007e5a6acc67f533ca560c9f))
|
29
|
+
* Warn when loading deprecated files ([46634df](https://github.com/micke/valid_email2/commit/46634dff409f9a47f10684d629dc022b22daf362))
|
30
|
+
|
3
31
|
## [5.2.6](https://github.com/micke/valid_email2/compare/v5.2.5...v5.2.6) (2024-08-10)
|
4
32
|
|
5
33
|
|
data/README.md
CHANGED
@@ -73,20 +73,20 @@ To validate that the domain is not a disposable email (checks domain only, does
|
|
73
73
|
validates :email, 'valid_email_2/email': { disposable_domain: true }
|
74
74
|
```
|
75
75
|
|
76
|
-
To validate that the domain is not a disposable email or a disposable email (checks domain and MX server) but
|
76
|
+
To validate that the domain is not a disposable email or a disposable email (checks domain and MX server) but allow-listed (under config/allow_listed_email_domains.yml):
|
77
77
|
```ruby
|
78
|
-
validates :email, 'valid_email_2/email': {
|
78
|
+
validates :email, 'valid_email_2/email': { disposable_with_allow_list: true }
|
79
79
|
```
|
80
80
|
|
81
|
-
To validate that the domain is not a disposable email or a disposable email (checks domain only, does not check MX server) but
|
81
|
+
To validate that the domain is not a disposable email or a disposable email (checks domain only, does not check MX server) but allow-listed (under config/allow_listed_email_domains.yml):
|
82
82
|
|
83
83
|
```ruby
|
84
|
-
validates :email, 'valid_email_2/email': {
|
84
|
+
validates :email, 'valid_email_2/email': { disposable_domain_with_allow_list: true }
|
85
85
|
```
|
86
86
|
|
87
|
-
To validate that the domain is not
|
87
|
+
To validate that the domain is not on the deny list (under config/deny_listed_email_domains.yml):
|
88
88
|
```ruby
|
89
|
-
validates :email, 'valid_email_2/email': {
|
89
|
+
validates :email, 'valid_email_2/email': { deny_list: true }
|
90
90
|
```
|
91
91
|
|
92
92
|
To validate that email is not subaddressed:
|
@@ -147,6 +147,15 @@ end
|
|
147
147
|
|
148
148
|
This gem is tested against currently supported Ruby and Rails versions. For an up to date list, check the build matrix in the [workflow](.github/workflows/ci.yaml).
|
149
149
|
|
150
|
+
## Upgrading to v5.3.0
|
151
|
+
|
152
|
+
In version v5.3.0 the config directory files were renamed as follows:
|
153
|
+
|
154
|
+
`config/blacklisted_email_domains.yml` -> `config/deny_listed_email_domains.yml`
|
155
|
+
`config/whitelisted_email_domains.yml` -> `config/allow_listed_email_domains.yml`
|
156
|
+
|
157
|
+
You won't need to make any changes yourself if you're installing this version for the first time. For individuals updating from earlier versions, make sure to update the file namings as per the above. In future versions this will be a breaking change.
|
158
|
+
|
150
159
|
## Upgrading to v3.0.0
|
151
160
|
|
152
161
|
In version v3.0.0 I decided to move __and__ rename the config files from the
|
@@ -0,0 +1 @@
|
|
1
|
+
- deny-listed-test.com
|
@@ -51296,7 +51296,6 @@ embaeqmail.com
|
|
51296
51296
|
embalaje.us
|
51297
51297
|
embaramail.com
|
51298
51298
|
embarq.net
|
51299
|
-
embarqmail.com
|
51300
51299
|
embarqumail.com
|
51301
51300
|
embassyofcoffee.de
|
51302
51301
|
embatqmail.com
|
data/lib/valid_email2/address.rb
CHANGED
@@ -3,12 +3,14 @@
|
|
3
3
|
require "valid_email2"
|
4
4
|
require "resolv"
|
5
5
|
require "mail"
|
6
|
+
require "unicode/emoji"
|
7
|
+
require "valid_email2/dns_records_cache"
|
6
8
|
|
7
9
|
module ValidEmail2
|
8
10
|
class Address
|
9
11
|
attr_accessor :address
|
10
12
|
|
11
|
-
PROHIBITED_DOMAIN_CHARACTERS_REGEX = /[+!_\/\s'
|
13
|
+
PROHIBITED_DOMAIN_CHARACTERS_REGEX = /[+!_\/\s'#`]/
|
12
14
|
DEFAULT_RECIPIENT_DELIMITER = '+'
|
13
15
|
DOT_DELIMITER = '.'
|
14
16
|
|
@@ -24,9 +26,7 @@ module ValidEmail2
|
|
24
26
|
@parse_error = false
|
25
27
|
@raw_address = address
|
26
28
|
@dns_timeout = dns_timeout
|
27
|
-
|
28
|
-
@resolv_config = Resolv::DNS::Config.default_config_hash
|
29
|
-
@resolv_config[:nameserver] = dns_nameserver if dns_nameserver
|
29
|
+
@dns_nameserver = dns_nameserver
|
30
30
|
|
31
31
|
begin
|
32
32
|
@address = Mail::Address.new(address)
|
@@ -34,7 +34,7 @@ module ValidEmail2
|
|
34
34
|
@parse_error = true
|
35
35
|
end
|
36
36
|
|
37
|
-
@parse_error ||= address_contain_emoticons?
|
37
|
+
@parse_error ||= address_contain_emoticons?
|
38
38
|
end
|
39
39
|
|
40
40
|
def valid?
|
@@ -84,12 +84,12 @@ module ValidEmail2
|
|
84
84
|
valid? && mx_server_is_in?(ValidEmail2.disposable_emails)
|
85
85
|
end
|
86
86
|
|
87
|
-
def
|
88
|
-
domain_is_in?(ValidEmail2.
|
87
|
+
def allow_listed?
|
88
|
+
domain_is_in?(ValidEmail2.allow_list)
|
89
89
|
end
|
90
90
|
|
91
|
-
def
|
92
|
-
valid? && domain_is_in?(ValidEmail2.
|
91
|
+
def deny_listed?
|
92
|
+
valid? && domain_is_in?(ValidEmail2.deny_list)
|
93
93
|
end
|
94
94
|
|
95
95
|
def valid_mx?
|
@@ -130,16 +130,30 @@ module ValidEmail2
|
|
130
130
|
}
|
131
131
|
end
|
132
132
|
|
133
|
-
def address_contain_emoticons?
|
134
|
-
return false if
|
133
|
+
def address_contain_emoticons?
|
134
|
+
return false if @raw_address.nil?
|
135
|
+
|
136
|
+
@raw_address.scan(Unicode::Emoji::REGEX).length >= 1
|
137
|
+
end
|
138
|
+
|
139
|
+
def resolv_config
|
140
|
+
@resolv_config ||= begin
|
141
|
+
config = Resolv::DNS::Config.default_config_hash
|
142
|
+
config[:nameserver] = @dns_nameserver if @dns_nameserver
|
143
|
+
config
|
144
|
+
end
|
135
145
|
|
136
|
-
|
146
|
+
@resolv_config
|
137
147
|
end
|
138
148
|
|
139
149
|
def mx_servers
|
140
|
-
@
|
141
|
-
|
142
|
-
|
150
|
+
@mx_servers_cache ||= ValidEmail2::DnsRecordsCache.new
|
151
|
+
|
152
|
+
@mx_servers_cache.fetch(address.domain.downcase) do
|
153
|
+
Resolv::DNS.open(resolv_config) do |dns|
|
154
|
+
dns.timeouts = @dns_timeout
|
155
|
+
dns.getresources(address.domain, Resolv::DNS::Resource::IN::MX)
|
156
|
+
end
|
143
157
|
end
|
144
158
|
end
|
145
159
|
|
@@ -148,10 +162,14 @@ module ValidEmail2
|
|
148
162
|
end
|
149
163
|
|
150
164
|
def mx_or_a_servers
|
151
|
-
@
|
152
|
-
|
153
|
-
|
154
|
-
|
165
|
+
@mx_or_a_servers_cache ||= ValidEmail2::DnsRecordsCache.new
|
166
|
+
|
167
|
+
@mx_or_a_servers_cache.fetch(address.domain.downcase) do
|
168
|
+
Resolv::DNS.open(resolv_config) do |dns|
|
169
|
+
dns.timeouts = @dns_timeout
|
170
|
+
(mx_servers.any? && mx_servers) ||
|
171
|
+
dns.getresources(address.domain, Resolv::DNS::Resource::IN::A)
|
172
|
+
end
|
155
173
|
end
|
156
174
|
end
|
157
175
|
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module ValidEmail2
|
2
|
+
class DnsRecordsCache
|
3
|
+
MAX_CACHE_SIZE = 1_000
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
# Cache structure: { domain (String): { records: [], cached_at: Time, ttl: Integer } }
|
7
|
+
@cache = {}
|
8
|
+
end
|
9
|
+
|
10
|
+
def fetch(domain, &block)
|
11
|
+
prune_cache if @cache.size > MAX_CACHE_SIZE
|
12
|
+
|
13
|
+
cache_entry = @cache[domain]
|
14
|
+
|
15
|
+
if cache_entry && (Time.now - cache_entry[:cached_at]) < cache_entry[:ttl]
|
16
|
+
return cache_entry[:records]
|
17
|
+
else
|
18
|
+
@cache.delete(domain)
|
19
|
+
end
|
20
|
+
|
21
|
+
records = block.call
|
22
|
+
|
23
|
+
if records.any?
|
24
|
+
ttl = records.map(&:ttl).min
|
25
|
+
@cache[domain] = { records: records, cached_at: Time.now, ttl: ttl }
|
26
|
+
end
|
27
|
+
|
28
|
+
records
|
29
|
+
end
|
30
|
+
|
31
|
+
def prune_cache
|
32
|
+
entries_sorted_by_cached_at_asc = (@cache.sort_by { |_domain, data| data[:cached_at] }).flatten
|
33
|
+
entries_to_remove = entries_sorted_by_cached_at_asc.first(@cache.size - MAX_CACHE_SIZE)
|
34
|
+
entries_to_remove.each { |domain| @cache.delete(domain) }
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -32,16 +32,16 @@ module ValidEmail2
|
|
32
32
|
error(record, attribute) && return if addresses.any?(&:disposable_domain?)
|
33
33
|
end
|
34
34
|
|
35
|
-
if options[:
|
36
|
-
error(record, attribute) && return if addresses.any? { |address| address.disposable? && !address.
|
35
|
+
if options[:disposable_with_allow_list]
|
36
|
+
error(record, attribute) && return if addresses.any? { |address| address.disposable? && !address.allow_listed? }
|
37
37
|
end
|
38
38
|
|
39
|
-
if options[:
|
40
|
-
error(record, attribute) && return if addresses.any? { |address| address.disposable_domain? && !address.
|
39
|
+
if options[:disposable_domain_with_allow_list]
|
40
|
+
error(record, attribute) && return if addresses.any? { |address| address.disposable_domain? && !address.allow_listed? }
|
41
41
|
end
|
42
42
|
|
43
|
-
if options[:
|
44
|
-
error(record, attribute) && return if addresses.any?(&:
|
43
|
+
if options[:deny_list]
|
44
|
+
error(record, attribute) && return if addresses.any?(&:deny_listed?)
|
45
45
|
end
|
46
46
|
|
47
47
|
if options[:mx]
|
@@ -57,12 +57,12 @@ module ValidEmail2
|
|
57
57
|
options = default_options.merge(self.options)
|
58
58
|
|
59
59
|
if options[:multiple]
|
60
|
-
email_list = input.is_a?(Array) ? input : input.split(',')
|
60
|
+
email_list = input.is_a?(Array) ? input : input.split(',').map(&:strip)
|
61
61
|
else
|
62
62
|
email_list = [input]
|
63
63
|
end
|
64
64
|
|
65
|
-
email_list.reject(&:empty?)
|
65
|
+
email_list.reject(&:empty?)
|
66
66
|
end
|
67
67
|
|
68
68
|
def error(record, attribute)
|
data/lib/valid_email2/version.rb
CHANGED
data/lib/valid_email2.rb
CHANGED
@@ -3,8 +3,8 @@
|
|
3
3
|
require "valid_email2/email_validator"
|
4
4
|
|
5
5
|
module ValidEmail2
|
6
|
-
|
7
|
-
|
6
|
+
DENY_LIST_FILE = "config/deny_listed_email_domains.yml"
|
7
|
+
ALLOW_LIST_FILE = "config/allow_listed_email_domains.yml"
|
8
8
|
DISPOSABLE_FILE = File.expand_path('../config/disposable_email_domains.txt', __dir__)
|
9
9
|
|
10
10
|
class << self
|
@@ -12,18 +12,18 @@ module ValidEmail2
|
|
12
12
|
@disposable_emails ||= load_file(DISPOSABLE_FILE)
|
13
13
|
end
|
14
14
|
|
15
|
-
def
|
16
|
-
@
|
15
|
+
def deny_list
|
16
|
+
@deny_list ||= load_if_exists(DENY_LIST_FILE) || Set.new
|
17
17
|
end
|
18
18
|
|
19
|
-
def
|
20
|
-
@
|
19
|
+
def allow_list
|
20
|
+
@allow_list ||= load_if_exists(ALLOW_LIST_FILE) || Set.new
|
21
21
|
end
|
22
22
|
|
23
23
|
private
|
24
24
|
|
25
25
|
def load_if_exists(path)
|
26
|
-
|
26
|
+
load_file(path) if File.exist?(path)
|
27
27
|
end
|
28
28
|
|
29
29
|
def load_file(path)
|
data/pull_mailchecker_emails.rb
CHANGED
@@ -5,7 +5,7 @@ require "yaml"
|
|
5
5
|
require "json"
|
6
6
|
require "net/http"
|
7
7
|
|
8
|
-
|
8
|
+
allow_listed_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
11
|
yandex.net gmx.com gmx.es webdesignspecialist.com.au vp.com
|
@@ -15,7 +15,7 @@ whitelisted_emails = %w[
|
|
15
15
|
passinbox.com passfwd.com passmail.com passmail.net
|
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
|
-
net.ua kommespaeter.de alpenjodel.de my.id web.id directbox.com
|
18
|
+
net.ua kommespaeter.de alpenjodel.de my.id web.id directbox.com embarqmail.com
|
19
19
|
]
|
20
20
|
|
21
21
|
existing_emails = File.open("config/disposable_email_domains.txt") { |f| f.read.split("\n") }
|
@@ -29,6 +29,6 @@ remote_emails = [
|
|
29
29
|
resp.body.split("\n").flatten
|
30
30
|
end
|
31
31
|
|
32
|
-
result_emails = (existing_emails + remote_emails).map(&:strip).uniq.sort -
|
32
|
+
result_emails = (existing_emails + remote_emails).map(&:strip).uniq.sort - allow_listed_emails
|
33
33
|
|
34
34
|
File.write("config/disposable_email_domains.txt", result_emails.join("\n"))
|
data/release-please-config.json
CHANGED
data/spec/address_spec.rb
CHANGED
@@ -33,5 +33,289 @@ describe ValidEmail2::Address do
|
|
33
33
|
address = described_class.new("foo🙈@gmail.com")
|
34
34
|
expect(address.valid?).to be false
|
35
35
|
end
|
36
|
+
|
37
|
+
it "is valid if it contains special scandinavian characters" do
|
38
|
+
address = described_class.new("jørgen@email.dk")
|
39
|
+
expect(address.valid?).to eq true
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
describe "caching" do
|
44
|
+
let(:email_address) { "example@ymail.com" }
|
45
|
+
let(:email_instance) { described_class.new(email_address) }
|
46
|
+
let(:dns_records_cache_instance) { ValidEmail2::DnsRecordsCache.new }
|
47
|
+
let(:ttl) { 1_000 }
|
48
|
+
let(:mock_resolv_dns) { instance_double(Resolv::DNS) }
|
49
|
+
let(:mock_mx_records) { [double("MX", exchange: "mx.ymail.com", preference: 10, ttl: ttl)] }
|
50
|
+
|
51
|
+
before do
|
52
|
+
allow(email_instance).to receive(:null_mx?).and_return(false)
|
53
|
+
allow(Resolv::DNS).to receive(:open).and_yield(mock_resolv_dns)
|
54
|
+
allow(mock_resolv_dns).to receive(:timeouts=)
|
55
|
+
end
|
56
|
+
|
57
|
+
describe "#valid_strict_mx?" do
|
58
|
+
let(:cached_at) { Time.now }
|
59
|
+
let(:mock_cache_data) { { email_instance.address.domain => { records: mock_mx_records, cached_at: cached_at, ttl: ttl } } }
|
60
|
+
|
61
|
+
before do
|
62
|
+
allow(mock_resolv_dns).to receive(:getresources)
|
63
|
+
.with(email_instance.address.domain, Resolv::DNS::Resource::IN::MX)
|
64
|
+
.and_return(mock_mx_records)
|
65
|
+
end
|
66
|
+
|
67
|
+
it "calls the MX servers lookup when the email is not cached" do
|
68
|
+
result = email_instance.valid_strict_mx?
|
69
|
+
|
70
|
+
expect(Resolv::DNS).to have_received(:open).once
|
71
|
+
expect(result).to be true
|
72
|
+
end
|
73
|
+
|
74
|
+
it "does not call the MX servers lookup when the email is cached" do
|
75
|
+
email_instance.valid_strict_mx?
|
76
|
+
email_instance.valid_strict_mx?
|
77
|
+
|
78
|
+
expect(Resolv::DNS).to have_received(:open).once
|
79
|
+
end
|
80
|
+
|
81
|
+
it "returns the cached result for subsequent calls" do
|
82
|
+
first_result = email_instance.valid_strict_mx?
|
83
|
+
expect(first_result).to be true
|
84
|
+
|
85
|
+
allow(mock_resolv_dns).to receive(:getresources)
|
86
|
+
.with(email_instance.address.domain, Resolv::DNS::Resource::IN::MX)
|
87
|
+
.and_return([])
|
88
|
+
|
89
|
+
second_result = email_instance.valid_strict_mx?
|
90
|
+
expect(second_result).to be true
|
91
|
+
end
|
92
|
+
|
93
|
+
describe "ttl" do
|
94
|
+
before do
|
95
|
+
dns_records_cache_instance.instance_variable_set(:@cache, mock_cache_data)
|
96
|
+
allow(ValidEmail2::DnsRecordsCache).to receive(:new).and_return(dns_records_cache_instance)
|
97
|
+
allow(dns_records_cache_instance).to receive(:fetch).with(email_instance.address.domain).and_call_original
|
98
|
+
end
|
99
|
+
|
100
|
+
context "when the time since last lookup is less than the cached ttl entry" do
|
101
|
+
let(:cached_at) { Time.now }
|
102
|
+
|
103
|
+
it "does not call the MX servers lookup" do
|
104
|
+
email_instance.valid_strict_mx?
|
105
|
+
|
106
|
+
expect(Resolv::DNS).not_to have_received(:open)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
context "when the time since last lookup is greater than the cached ttl entry" do
|
111
|
+
let(:cached_at) { Time.now - ttl }
|
112
|
+
|
113
|
+
it "calls the MX servers lookup" do
|
114
|
+
email_instance.valid_strict_mx?
|
115
|
+
|
116
|
+
expect(Resolv::DNS).to have_received(:open).once
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
describe "cache size" do
|
122
|
+
before do
|
123
|
+
dns_records_cache_instance.instance_variable_set(:@cache, mock_cache_data)
|
124
|
+
allow(ValidEmail2::DnsRecordsCache).to receive(:new).and_return(dns_records_cache_instance)
|
125
|
+
allow(dns_records_cache_instance).to receive(:fetch).with(email_instance.address.domain).and_call_original
|
126
|
+
end
|
127
|
+
|
128
|
+
context "when the cache size is less than or equal to the max cache size" do
|
129
|
+
before do
|
130
|
+
stub_const("ValidEmail2::DnsRecordsCache::MAX_CACHE_SIZE", 1)
|
131
|
+
end
|
132
|
+
|
133
|
+
it "does not prune the cache" do
|
134
|
+
expect(dns_records_cache_instance).not_to receive(:prune_cache)
|
135
|
+
|
136
|
+
email_instance.valid_strict_mx?
|
137
|
+
end
|
138
|
+
|
139
|
+
it "does not call the MX servers lookup" do
|
140
|
+
email_instance.valid_strict_mx?
|
141
|
+
|
142
|
+
expect(Resolv::DNS).not_to have_received(:open)
|
143
|
+
end
|
144
|
+
|
145
|
+
context "and there are older cached entries" do
|
146
|
+
let(:mock_cache_data) { { "another_domain.com" => { records: mock_mx_records, cached_at: cached_at - 100, ttl: ttl } } }
|
147
|
+
|
148
|
+
it "does not prune those entries" do
|
149
|
+
email_instance.valid_strict_mx?
|
150
|
+
|
151
|
+
expect(dns_records_cache_instance.instance_variable_get(:@cache).keys.size).to eq 2
|
152
|
+
expect(dns_records_cache_instance.instance_variable_get(:@cache).keys).to match_array([email_instance.address.domain, "another_domain.com"])
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
context "when the cache size is greater than the max cache size" do
|
158
|
+
before do
|
159
|
+
stub_const("ValidEmail2::DnsRecordsCache::MAX_CACHE_SIZE", 0)
|
160
|
+
end
|
161
|
+
|
162
|
+
it "prunes the cache" do
|
163
|
+
expect(dns_records_cache_instance).to receive(:prune_cache).once
|
164
|
+
|
165
|
+
email_instance.valid_strict_mx?
|
166
|
+
end
|
167
|
+
|
168
|
+
it "calls the the MX servers lookup" do
|
169
|
+
email_instance.valid_strict_mx?
|
170
|
+
|
171
|
+
expect(Resolv::DNS).to have_received(:open).once
|
172
|
+
end
|
173
|
+
|
174
|
+
context "and there are older cached entries" do
|
175
|
+
let(:mock_cache_data) { { "another_domain.com" => { records: mock_mx_records, cached_at: cached_at - 100, ttl: ttl } } }
|
176
|
+
|
177
|
+
it "prunes those entries" do
|
178
|
+
email_instance.valid_strict_mx?
|
179
|
+
|
180
|
+
expect(dns_records_cache_instance.instance_variable_get(:@cache).keys.size).to eq 1
|
181
|
+
expect(dns_records_cache_instance.instance_variable_get(:@cache).keys).to match_array([email_instance.address.domain])
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
describe "#valid_mx?" do
|
189
|
+
let(:cached_at) { Time.now }
|
190
|
+
let(:mock_cache_data) { { email_instance.address.domain => { records: mock_a_records, cached_at: cached_at, ttl: ttl } } }
|
191
|
+
let(:mock_a_records) { [double("A", address: "192.168.1.1", ttl: ttl)] }
|
192
|
+
|
193
|
+
before do
|
194
|
+
allow(email_instance).to receive(:mx_servers).and_return(mock_mx_records)
|
195
|
+
allow(mock_resolv_dns).to receive(:getresources)
|
196
|
+
.with(email_instance.address.domain, Resolv::DNS::Resource::IN::A)
|
197
|
+
.and_return(mock_a_records)
|
198
|
+
end
|
199
|
+
|
200
|
+
it "calls the MX or A servers lookup when the email is not cached" do
|
201
|
+
result = email_instance.valid_mx?
|
202
|
+
|
203
|
+
expect(Resolv::DNS).to have_received(:open).once
|
204
|
+
expect(result).to be true
|
205
|
+
end
|
206
|
+
|
207
|
+
it "does not call the MX or A servers lookup when the email is cached" do
|
208
|
+
email_instance.valid_mx?
|
209
|
+
email_instance.valid_mx?
|
210
|
+
|
211
|
+
expect(Resolv::DNS).to have_received(:open).once
|
212
|
+
end
|
213
|
+
|
214
|
+
it "returns the cached result for subsequent calls" do
|
215
|
+
first_result = email_instance.valid_mx?
|
216
|
+
expect(first_result).to be true
|
217
|
+
|
218
|
+
allow(mock_resolv_dns).to receive(:getresources)
|
219
|
+
.with(email_instance.address.domain, Resolv::DNS::Resource::IN::A)
|
220
|
+
.and_return([])
|
221
|
+
|
222
|
+
second_result = email_instance.valid_mx?
|
223
|
+
expect(second_result).to be true
|
224
|
+
end
|
225
|
+
|
226
|
+
describe "ttl" do
|
227
|
+
before do
|
228
|
+
dns_records_cache_instance.instance_variable_set(:@cache, mock_cache_data)
|
229
|
+
allow(ValidEmail2::DnsRecordsCache).to receive(:new).and_return(dns_records_cache_instance)
|
230
|
+
allow(dns_records_cache_instance).to receive(:fetch).with(email_instance.address.domain).and_call_original
|
231
|
+
end
|
232
|
+
|
233
|
+
context "when the time since last lookup is less than the cached ttl entry" do
|
234
|
+
let(:cached_at) { Time.now }
|
235
|
+
|
236
|
+
it "does not call the MX or A servers lookup" do
|
237
|
+
email_instance.valid_mx?
|
238
|
+
|
239
|
+
expect(Resolv::DNS).not_to have_received(:open)
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
context "when the time since last lookup is greater than the cached ttl entry" do
|
244
|
+
let(:cached_at) { Time.now - ttl }
|
245
|
+
|
246
|
+
it "calls the MX or A servers lookup " do
|
247
|
+
email_instance.valid_mx?
|
248
|
+
|
249
|
+
expect(Resolv::DNS).to have_received(:open).once
|
250
|
+
end
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
describe "cache size" do
|
255
|
+
before do
|
256
|
+
dns_records_cache_instance.instance_variable_set(:@cache, mock_cache_data)
|
257
|
+
allow(ValidEmail2::DnsRecordsCache).to receive(:new).and_return(dns_records_cache_instance)
|
258
|
+
allow(dns_records_cache_instance).to receive(:fetch).with(email_instance.address.domain).and_call_original
|
259
|
+
end
|
260
|
+
|
261
|
+
context "when the cache size is less than or equal to the max cache size" do
|
262
|
+
before do
|
263
|
+
stub_const("ValidEmail2::DnsRecordsCache::MAX_CACHE_SIZE", 1)
|
264
|
+
end
|
265
|
+
|
266
|
+
it "does not prune the cache" do
|
267
|
+
expect(email_instance).not_to receive(:prune_cache)
|
268
|
+
|
269
|
+
email_instance.valid_mx?
|
270
|
+
end
|
271
|
+
|
272
|
+
it "does not call the MX or A servers lookup" do
|
273
|
+
email_instance.valid_mx?
|
274
|
+
|
275
|
+
expect(Resolv::DNS).not_to have_received(:open)
|
276
|
+
end
|
277
|
+
|
278
|
+
context "and there are older cached entries" do
|
279
|
+
let(:mock_cache_data) { { "another_domain.com" => { records: mock_a_records, cached_at: cached_at - 100, ttl: ttl } } }
|
280
|
+
|
281
|
+
it "does not prune those entries" do
|
282
|
+
email_instance.valid_mx?
|
283
|
+
|
284
|
+
expect(dns_records_cache_instance.instance_variable_get(:@cache).keys.size).to eq 2
|
285
|
+
expect(dns_records_cache_instance.instance_variable_get(:@cache).keys).to match_array([email_instance.address.domain, "another_domain.com"])
|
286
|
+
end
|
287
|
+
end
|
288
|
+
end
|
289
|
+
|
290
|
+
context "when the cache size is greater than the max cache size" do
|
291
|
+
before do
|
292
|
+
stub_const("ValidEmail2::DnsRecordsCache::MAX_CACHE_SIZE", 0)
|
293
|
+
end
|
294
|
+
|
295
|
+
it "prunes the cache" do
|
296
|
+
expect(dns_records_cache_instance).to receive(:prune_cache).once
|
297
|
+
|
298
|
+
email_instance.valid_mx?
|
299
|
+
end
|
300
|
+
|
301
|
+
it "calls the MX or A servers lookup" do
|
302
|
+
email_instance.valid_mx?
|
303
|
+
|
304
|
+
expect(Resolv::DNS).to have_received(:open).once
|
305
|
+
end
|
306
|
+
|
307
|
+
context "and there are older cached entries" do
|
308
|
+
let(:mock_cache_data) { { "another_domain.com" => { records: mock_a_records, cached_at: cached_at - 100, ttl: ttl } } }
|
309
|
+
|
310
|
+
it "prunes those entries" do
|
311
|
+
email_instance.valid_mx?
|
312
|
+
|
313
|
+
expect(dns_records_cache_instance.instance_variable_get(:@cache).keys.size).to eq 1
|
314
|
+
expect(dns_records_cache_instance.instance_variable_get(:@cache).keys).to match_array([email_instance.address.domain])
|
315
|
+
end
|
316
|
+
end
|
317
|
+
end
|
318
|
+
end
|
319
|
+
end
|
36
320
|
end
|
37
321
|
end
|
data/spec/spec_helper.rb
CHANGED
data/spec/valid_email2_spec.rb
CHANGED
@@ -47,16 +47,16 @@ class TestUserDisallowDisposableDomain < TestModel
|
|
47
47
|
validates :email, 'valid_email_2/email': { disposable_domain: true }
|
48
48
|
end
|
49
49
|
|
50
|
-
class
|
51
|
-
validates :email, 'valid_email_2/email': {
|
50
|
+
class TestUserDisallowDisposableWithAllowList < TestModel
|
51
|
+
validates :email, 'valid_email_2/email': { disposable_with_allow_list: true }
|
52
52
|
end
|
53
53
|
|
54
|
-
class
|
55
|
-
validates :email, 'valid_email_2/email': {
|
54
|
+
class TestUserDisallowDisposableDomainWithAllowList < TestModel
|
55
|
+
validates :email, 'valid_email_2/email': { disposable_domain_with_allow_list: true }
|
56
56
|
end
|
57
57
|
|
58
|
-
class
|
59
|
-
validates :email, 'valid_email_2/email': {
|
58
|
+
class TestUserDisallowDenyListed < TestModel
|
59
|
+
validates :email, 'valid_email_2/email': { deny_list: true }
|
60
60
|
end
|
61
61
|
|
62
62
|
class TestUserMessage < TestModel
|
@@ -158,6 +158,16 @@ describe ValidEmail2 do
|
|
158
158
|
user = TestUser.new(email: "foo@gmail-.com")
|
159
159
|
expect(user.valid?).to be_falsy
|
160
160
|
end
|
161
|
+
|
162
|
+
it "is invalid with trailing whitespace" do
|
163
|
+
user = TestUser.new(email: "foo@example.com ")
|
164
|
+
expect(user.valid?).to be_falsey
|
165
|
+
end
|
166
|
+
|
167
|
+
it "is invalid if domain contains #" do
|
168
|
+
user = TestUser.new(email: "foo@example.com#")
|
169
|
+
expect(user.valid?).to be_falsey
|
170
|
+
end
|
161
171
|
end
|
162
172
|
|
163
173
|
describe "with disposable validation" do
|
@@ -208,58 +218,58 @@ describe ValidEmail2 do
|
|
208
218
|
end
|
209
219
|
end
|
210
220
|
|
211
|
-
describe "with
|
212
|
-
let(:
|
213
|
-
let(:
|
221
|
+
describe "with allow-listed emails" do
|
222
|
+
let(:allow_list_domain) { disposable_domain }
|
223
|
+
let(:allow_list_file_path) { "config/allow_listed_email_domains.yml" }
|
214
224
|
|
215
|
-
# Some of the specs below need to explictly set the
|
225
|
+
# Some of the specs below need to explictly set the allow list var or it
|
216
226
|
# may be cached to an empty set
|
217
|
-
def
|
227
|
+
def set_allow_list
|
218
228
|
ValidEmail2.instance_variable_set(
|
219
|
-
:@
|
220
|
-
ValidEmail2.send(:load_if_exists, ValidEmail2::
|
229
|
+
:@allow_list,
|
230
|
+
ValidEmail2.send(:load_if_exists, ValidEmail2::ALLOW_LIST_FILE)
|
221
231
|
)
|
222
232
|
end
|
223
233
|
|
224
234
|
after do
|
225
|
-
FileUtils.rm(
|
226
|
-
|
235
|
+
FileUtils.rm(allow_list_file_path, force: true)
|
236
|
+
set_allow_list
|
227
237
|
end
|
228
238
|
|
229
|
-
it "is invalid if the domain is disposable and not in the
|
230
|
-
user =
|
239
|
+
it "is invalid if the domain is disposable and not in the allow list" do
|
240
|
+
user = TestUserDisallowDisposableWithAllowList.new(email: "foo@#{allow_list_domain}")
|
231
241
|
expect(user.valid?).to be_falsey
|
232
242
|
end
|
233
243
|
|
234
|
-
it "is valid if the domain is disposable but in the
|
235
|
-
File.open(
|
236
|
-
|
237
|
-
user =
|
244
|
+
it "is valid if the domain is disposable but in the allow list" do
|
245
|
+
File.open(allow_list_file_path, "w") { |f| f.write [allow_list_domain].to_yaml }
|
246
|
+
set_allow_list
|
247
|
+
user = TestUserDisallowDisposableWithAllowList.new(email: "foo@#{allow_list_domain}")
|
238
248
|
expect(user.valid?).to be_truthy
|
239
249
|
end
|
240
250
|
|
241
|
-
it "is invalid if the domain is a disposable_domain and not in the
|
242
|
-
user =
|
251
|
+
it "is invalid if the domain is a disposable_domain and not in the allow list" do
|
252
|
+
user = TestUserDisallowDisposableDomainWithAllowList.new(email: "foo@#{allow_list_domain}")
|
243
253
|
expect(user.valid?).to be_falsey
|
244
254
|
end
|
245
255
|
|
246
|
-
it "is valid if the domain is a disposable_domain but in the
|
247
|
-
File.open(
|
248
|
-
|
249
|
-
user =
|
256
|
+
it "is valid if the domain is a disposable_domain but in the allow list" do
|
257
|
+
File.open(allow_list_file_path, "w") { |f| f.write [allow_list_domain].to_yaml }
|
258
|
+
set_allow_list
|
259
|
+
user = TestUserDisallowDisposableDomainWithAllowList.new(email: "foo@#{allow_list_domain}")
|
250
260
|
expect(user.valid?).to be_truthy
|
251
261
|
end
|
252
262
|
end
|
253
263
|
end
|
254
264
|
|
255
|
-
describe "with
|
256
|
-
it "is valid if the domain is not
|
257
|
-
user =
|
265
|
+
describe "with deny list validation" do
|
266
|
+
it "is valid if the domain is not deny-listed" do
|
267
|
+
user = TestUserDisallowDenyListed.new(email: "foo@gmail.com")
|
258
268
|
expect(user.valid?).to be_truthy
|
259
269
|
end
|
260
270
|
|
261
|
-
it "is invalid if the domain is
|
262
|
-
user =
|
271
|
+
it "is invalid if the domain is deny-listed" do
|
272
|
+
user = TestUserDisallowDenyListed.new(email: "foo@deny-listed-test.com")
|
263
273
|
expect(user.valid?).to be_falsey
|
264
274
|
end
|
265
275
|
end
|
data/valid_email2.gemspec
CHANGED
@@ -8,8 +8,8 @@ Gem::Specification.new do |spec|
|
|
8
8
|
spec.version = ValidEmail2::VERSION
|
9
9
|
spec.authors = ["Micke Lisinge"]
|
10
10
|
spec.email = ["hi@micke.me"]
|
11
|
-
spec.description = %q{ActiveModel validation for email. Including MX lookup and disposable email
|
12
|
-
spec.summary = %q{ActiveModel validation for email. Including MX lookup and disposable email
|
11
|
+
spec.description = %q{ActiveModel validation for email. Including MX lookup and disposable email deny list}
|
12
|
+
spec.summary = %q{ActiveModel validation for email. Including MX lookup and disposable email deny list}
|
13
13
|
spec.homepage = "https://github.com/micke/valid_email2"
|
14
14
|
spec.license = "MIT"
|
15
15
|
|
@@ -18,7 +18,7 @@ Gem::Specification.new do |spec|
|
|
18
18
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
19
|
spec.require_paths = ["lib"]
|
20
20
|
|
21
|
-
spec.required_ruby_version = ">= 1.
|
21
|
+
spec.required_ruby_version = ">= 3.1.0"
|
22
22
|
|
23
23
|
spec.add_development_dependency "bundler", "~> 2.0"
|
24
24
|
spec.add_development_dependency "rake", "~> 12.3"
|
@@ -27,5 +27,6 @@ Gem::Specification.new do |spec|
|
|
27
27
|
spec.add_development_dependency "net-smtp"
|
28
28
|
spec.add_development_dependency "pry"
|
29
29
|
spec.add_runtime_dependency "mail", "~> 2.5"
|
30
|
-
spec.add_runtime_dependency "activemodel", ">=
|
30
|
+
spec.add_runtime_dependency "activemodel", ">= 6.0"
|
31
|
+
spec.add_runtime_dependency "unicode-emoji", "~> 3.7.0"
|
31
32
|
end
|
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: 6.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: 2024-
|
11
|
+
date: 2024-11-03 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -114,16 +114,30 @@ dependencies:
|
|
114
114
|
requirements:
|
115
115
|
- - ">="
|
116
116
|
- !ruby/object:Gem::Version
|
117
|
-
version: '
|
117
|
+
version: '6.0'
|
118
118
|
type: :runtime
|
119
119
|
prerelease: false
|
120
120
|
version_requirements: !ruby/object:Gem::Requirement
|
121
121
|
requirements:
|
122
122
|
- - ">="
|
123
123
|
- !ruby/object:Gem::Version
|
124
|
-
version: '
|
124
|
+
version: '6.0'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: unicode-emoji
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - "~>"
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: 3.7.0
|
132
|
+
type: :runtime
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - "~>"
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: 3.7.0
|
125
139
|
description: ActiveModel validation for email. Including MX lookup and disposable
|
126
|
-
email
|
140
|
+
email deny list
|
127
141
|
email:
|
128
142
|
- hi@micke.me
|
129
143
|
executables: []
|
@@ -140,12 +154,13 @@ files:
|
|
140
154
|
- LICENSE.txt
|
141
155
|
- README.md
|
142
156
|
- Rakefile
|
143
|
-
- config/
|
157
|
+
- config/deny_listed_email_domains.yml
|
144
158
|
- config/disposable_email_domains.txt
|
145
159
|
- gemfiles/activemodel6.gemfile
|
146
160
|
- gemfiles/activemodel7.gemfile
|
147
161
|
- lib/valid_email2.rb
|
148
162
|
- lib/valid_email2/address.rb
|
163
|
+
- lib/valid_email2/dns_records_cache.rb
|
149
164
|
- lib/valid_email2/email_validator.rb
|
150
165
|
- lib/valid_email2/version.rb
|
151
166
|
- pull_mailchecker_emails.rb
|
@@ -167,7 +182,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
167
182
|
requirements:
|
168
183
|
- - ">="
|
169
184
|
- !ruby/object:Gem::Version
|
170
|
-
version: 1.
|
185
|
+
version: 3.1.0
|
171
186
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
172
187
|
requirements:
|
173
188
|
- - ">="
|
@@ -178,7 +193,7 @@ rubygems_version: 3.5.3
|
|
178
193
|
signing_key:
|
179
194
|
specification_version: 4
|
180
195
|
summary: ActiveModel validation for email. Including MX lookup and disposable email
|
181
|
-
|
196
|
+
deny list
|
182
197
|
test_files:
|
183
198
|
- spec/address_spec.rb
|
184
199
|
- spec/benchmark_spec.rb
|
@@ -1 +0,0 @@
|
|
1
|
-
- blacklisted-test.com
|