valid_email2 6.0.0 → 7.0.9
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/.github/workflows/ci.yaml +5 -2
- data/.github/workflows/release.yaml +1 -0
- data/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +75 -0
- data/README.md +17 -0
- data/config/disposable_email_domains.txt +228 -1
- data/gemfiles/activemodel8.gemfile +5 -0
- data/lib/valid_email2/address.rb +19 -44
- data/lib/valid_email2/dns.rb +71 -0
- data/lib/valid_email2/email_validator.rb +3 -1
- data/lib/valid_email2/version.rb +1 -1
- data/spec/address_spec.rb +57 -41
- data/spec/dns_spec.rb +21 -0
- data/spec/spec_helper.rb +1 -0
- data/spec/valid_email2_spec.rb +3 -0
- data/valid_email2.gemspec +2 -2
- metadata +24 -21
- data/lib/valid_email2/dns_records_cache.rb +0 -37
data/lib/valid_email2/address.rb
CHANGED
@@ -1,10 +1,8 @@
|
|
1
1
|
# frozen_string_literal:true
|
2
2
|
|
3
3
|
require "valid_email2"
|
4
|
-
require "resolv"
|
5
4
|
require "mail"
|
6
|
-
require "
|
7
|
-
require "valid_email2/dns_records_cache"
|
5
|
+
require "valid_email2/dns"
|
8
6
|
|
9
7
|
module ValidEmail2
|
10
8
|
class Address
|
@@ -22,11 +20,18 @@ module ValidEmail2
|
|
22
20
|
@prohibited_domain_characters_regex = val
|
23
21
|
end
|
24
22
|
|
25
|
-
def
|
23
|
+
def self.permitted_multibyte_characters_regex
|
24
|
+
@permitted_multibyte_characters_regex
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.permitted_multibyte_characters_regex=(val)
|
28
|
+
@permitted_multibyte_characters_regex = val
|
29
|
+
end
|
30
|
+
|
31
|
+
def initialize(address, dns = Dns.new)
|
26
32
|
@parse_error = false
|
27
33
|
@raw_address = address
|
28
|
-
@
|
29
|
-
@dns_nameserver = dns_nameserver
|
34
|
+
@dns = dns
|
30
35
|
|
31
36
|
begin
|
32
37
|
@address = Mail::Address.new(address)
|
@@ -34,7 +39,7 @@ module ValidEmail2
|
|
34
39
|
@parse_error = true
|
35
40
|
end
|
36
41
|
|
37
|
-
@parse_error ||=
|
42
|
+
@parse_error ||= address_contain_multibyte_characters?
|
38
43
|
end
|
39
44
|
|
40
45
|
def valid?
|
@@ -96,14 +101,14 @@ module ValidEmail2
|
|
96
101
|
return false unless valid?
|
97
102
|
return false if null_mx?
|
98
103
|
|
99
|
-
|
104
|
+
@dns.mx_servers(address.domain).any? || @dns.a_servers(address.domain).any?
|
100
105
|
end
|
101
106
|
|
102
107
|
def valid_strict_mx?
|
103
108
|
return false unless valid?
|
104
109
|
return false if null_mx?
|
105
110
|
|
106
|
-
mx_servers.any?
|
111
|
+
@dns.mx_servers(address.domain).any?
|
107
112
|
end
|
108
113
|
|
109
114
|
private
|
@@ -119,7 +124,7 @@ module ValidEmail2
|
|
119
124
|
end
|
120
125
|
|
121
126
|
def mx_server_is_in?(domain_list)
|
122
|
-
mx_servers.any? { |mx_server|
|
127
|
+
@dns.mx_servers(address.domain).any? { |mx_server|
|
123
128
|
return false unless mx_server.respond_to?(:exchange)
|
124
129
|
|
125
130
|
mx_server = mx_server.exchange.to_s
|
@@ -130,47 +135,17 @@ module ValidEmail2
|
|
130
135
|
}
|
131
136
|
end
|
132
137
|
|
133
|
-
def
|
138
|
+
def address_contain_multibyte_characters?
|
134
139
|
return false if @raw_address.nil?
|
135
140
|
|
136
|
-
@raw_address.
|
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
|
141
|
+
return false if @raw_address.ascii_only?
|
145
142
|
|
146
|
-
@
|
147
|
-
end
|
148
|
-
|
149
|
-
def mx_servers
|
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
|
157
|
-
end
|
143
|
+
@raw_address.each_char.any? { |char| char.bytesize > 1 && char !~ self.class.permitted_multibyte_characters_regex }
|
158
144
|
end
|
159
145
|
|
160
146
|
def null_mx?
|
147
|
+
mx_servers = @dns.mx_servers(address.domain)
|
161
148
|
mx_servers.length == 1 && mx_servers.first.preference == 0 && mx_servers.first.exchange.length == 0
|
162
149
|
end
|
163
|
-
|
164
|
-
def mx_or_a_servers
|
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
|
173
|
-
end
|
174
|
-
end
|
175
150
|
end
|
176
151
|
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
require "resolv"
|
2
|
+
|
3
|
+
module ValidEmail2
|
4
|
+
class Dns
|
5
|
+
MAX_CACHE_SIZE = 1_000
|
6
|
+
CACHE = {}
|
7
|
+
|
8
|
+
CacheEntry = Struct.new(:records, :cached_at, :ttl)
|
9
|
+
|
10
|
+
def self.prune_cache
|
11
|
+
entries_sorted_by_cached_at_asc = CACHE.sort_by { |key, data| data.cached_at }
|
12
|
+
entries_to_remove = entries_sorted_by_cached_at_asc.first(CACHE.size - MAX_CACHE_SIZE)
|
13
|
+
entries_to_remove.each { |key, _value| CACHE.delete(key) }
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.clear_cache
|
17
|
+
CACHE.clear
|
18
|
+
end
|
19
|
+
|
20
|
+
def initialize(dns_timeout = 5, dns_nameserver = nil)
|
21
|
+
@dns_timeout = dns_timeout
|
22
|
+
@dns_nameserver = dns_nameserver
|
23
|
+
end
|
24
|
+
|
25
|
+
def mx_servers(domain)
|
26
|
+
fetch(domain, Resolv::DNS::Resource::IN::MX)
|
27
|
+
end
|
28
|
+
|
29
|
+
def a_servers(domain)
|
30
|
+
fetch(domain, Resolv::DNS::Resource::IN::A)
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def prune_cache
|
36
|
+
self.class.prune_cache
|
37
|
+
end
|
38
|
+
|
39
|
+
def fetch(domain, type)
|
40
|
+
prune_cache if CACHE.size > MAX_CACHE_SIZE
|
41
|
+
|
42
|
+
domain = domain.downcase
|
43
|
+
cache_key = [domain, type]
|
44
|
+
cache_entry = CACHE[cache_key]
|
45
|
+
|
46
|
+
if cache_entry && Time.now - cache_entry.cached_at < cache_entry.ttl
|
47
|
+
return cache_entry.records
|
48
|
+
else
|
49
|
+
CACHE.delete(cache_key)
|
50
|
+
end
|
51
|
+
|
52
|
+
records = Resolv::DNS.open(resolv_config) do |dns|
|
53
|
+
dns.timeouts = @dns_timeout
|
54
|
+
dns.getresources(domain, type)
|
55
|
+
end
|
56
|
+
|
57
|
+
if records.any?
|
58
|
+
ttl = records.map(&:ttl).min
|
59
|
+
CACHE[cache_key] = CacheEntry.new(records, Time.now, ttl)
|
60
|
+
end
|
61
|
+
|
62
|
+
records
|
63
|
+
end
|
64
|
+
|
65
|
+
def resolv_config
|
66
|
+
config = Resolv::DNS::Config.default_config_hash
|
67
|
+
config[:nameserver] = @dns_nameserver if @dns_nameserver
|
68
|
+
config
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -1,4 +1,5 @@
|
|
1
1
|
require "valid_email2/address"
|
2
|
+
require "logger" # Fix concurrent-ruby removing logger dependency which Rails itself does not have
|
2
3
|
require "active_model"
|
3
4
|
require "active_model/validations"
|
4
5
|
|
@@ -12,7 +13,8 @@ module ValidEmail2
|
|
12
13
|
return unless value.present?
|
13
14
|
options = default_options.merge(self.options)
|
14
15
|
|
15
|
-
|
16
|
+
dns = ValidEmail2::Dns.new(options[:dns_timeout], options[:dns_nameserver])
|
17
|
+
addresses = sanitized_values(value).map { |v| ValidEmail2::Address.new(v, dns) }
|
16
18
|
|
17
19
|
error(record, attribute) && return unless addresses.all?(&:valid?)
|
18
20
|
|
data/lib/valid_email2/version.rb
CHANGED
data/spec/address_spec.rb
CHANGED
@@ -3,6 +3,10 @@
|
|
3
3
|
require "spec_helper"
|
4
4
|
|
5
5
|
describe ValidEmail2::Address do
|
6
|
+
before do
|
7
|
+
ValidEmail2::Dns.clear_cache
|
8
|
+
end
|
9
|
+
|
6
10
|
describe "#valid?" do
|
7
11
|
it "is valid" do
|
8
12
|
address = described_class.new("foo@bar123.com")
|
@@ -34,16 +38,32 @@ describe ValidEmail2::Address do
|
|
34
38
|
expect(address.valid?).to be false
|
35
39
|
end
|
36
40
|
|
37
|
-
it "is
|
41
|
+
it "is invalid if it contains Japanese characters" do
|
42
|
+
address = described_class.new("あいうえお@example.com")
|
43
|
+
expect(address.valid?).to be false
|
44
|
+
end
|
45
|
+
|
46
|
+
it "is invalid if it contains special scandinavian characters" do
|
38
47
|
address = described_class.new("jørgen@email.dk")
|
39
|
-
expect(address.valid?).to eq
|
48
|
+
expect(address.valid?).to eq false
|
49
|
+
end
|
50
|
+
|
51
|
+
context "permitted_multibyte_characters_regex is set" do
|
52
|
+
before do
|
53
|
+
described_class.permitted_multibyte_characters_regex = /[ÆæØøÅåÄäÖöÞþÐð]/
|
54
|
+
end
|
55
|
+
|
56
|
+
it "is valid if it contains special scandinavian characters" do
|
57
|
+
address = described_class.new("jørgen@email.dk")
|
58
|
+
expect(address.valid?).to eq true
|
59
|
+
end
|
40
60
|
end
|
41
61
|
end
|
42
62
|
|
43
63
|
describe "caching" do
|
44
64
|
let(:email_address) { "example@ymail.com" }
|
45
|
-
let(:
|
46
|
-
let(:
|
65
|
+
let(:dns_instance) { ValidEmail2::Dns.new }
|
66
|
+
let(:email_instance) { described_class.new(email_address, dns_instance) }
|
47
67
|
let(:ttl) { 1_000 }
|
48
68
|
let(:mock_resolv_dns) { instance_double(Resolv::DNS) }
|
49
69
|
let(:mock_mx_records) { [double("MX", exchange: "mx.ymail.com", preference: 10, ttl: ttl)] }
|
@@ -56,7 +76,7 @@ describe ValidEmail2::Address do
|
|
56
76
|
|
57
77
|
describe "#valid_strict_mx?" do
|
58
78
|
let(:cached_at) { Time.now }
|
59
|
-
let(:mock_cache_data) { { email_instance.address.domain =>
|
79
|
+
let(:mock_cache_data) { { [email_instance.address.domain, Resolv::DNS::Resource::IN::MX] => ValidEmail2::Dns::CacheEntry.new(mock_mx_records, cached_at, ttl) } }
|
60
80
|
|
61
81
|
before do
|
62
82
|
allow(mock_resolv_dns).to receive(:getresources)
|
@@ -92,9 +112,9 @@ describe ValidEmail2::Address do
|
|
92
112
|
|
93
113
|
describe "ttl" do
|
94
114
|
before do
|
95
|
-
|
96
|
-
allow(ValidEmail2::
|
97
|
-
allow(
|
115
|
+
stub_const("ValidEmail2::Dns::CACHE", mock_cache_data)
|
116
|
+
allow(ValidEmail2::Dns).to receive(:new).and_return(dns_instance)
|
117
|
+
allow(dns_instance).to receive(:fetch).with(email_instance.address.domain, Resolv::DNS::Resource::IN::MX).and_call_original
|
98
118
|
end
|
99
119
|
|
100
120
|
context "when the time since last lookup is less than the cached ttl entry" do
|
@@ -120,18 +140,18 @@ describe ValidEmail2::Address do
|
|
120
140
|
|
121
141
|
describe "cache size" do
|
122
142
|
before do
|
123
|
-
|
124
|
-
allow(ValidEmail2::
|
125
|
-
allow(
|
143
|
+
stub_const("ValidEmail2::Dns::CACHE", mock_cache_data)
|
144
|
+
allow(ValidEmail2::Dns).to receive(:new).and_return(dns_instance)
|
145
|
+
allow(dns_instance).to receive(:fetch).with(email_instance.address.domain, Resolv::DNS::Resource::IN::MX).and_call_original
|
126
146
|
end
|
127
147
|
|
128
148
|
context "when the cache size is less than or equal to the max cache size" do
|
129
149
|
before do
|
130
|
-
stub_const("ValidEmail2::
|
150
|
+
stub_const("ValidEmail2::Dns::MAX_CACHE_SIZE", 1)
|
131
151
|
end
|
132
152
|
|
133
153
|
it "does not prune the cache" do
|
134
|
-
expect(
|
154
|
+
expect(dns_instance).not_to receive(:prune_cache)
|
135
155
|
|
136
156
|
email_instance.valid_strict_mx?
|
137
157
|
end
|
@@ -143,42 +163,40 @@ describe ValidEmail2::Address do
|
|
143
163
|
end
|
144
164
|
|
145
165
|
context "and there are older cached entries" do
|
146
|
-
let(:mock_cache_data) { { "another_domain.com" =>
|
166
|
+
let(:mock_cache_data) { { ["another_domain.com", Resolv::DNS::Resource::IN::MX] => ValidEmail2::Dns::CacheEntry.new(mock_mx_records, cached_at - 100, ttl) } }
|
147
167
|
|
148
168
|
it "does not prune those entries" do
|
149
169
|
email_instance.valid_strict_mx?
|
150
170
|
|
151
|
-
expect(
|
152
|
-
expect(dns_records_cache_instance.instance_variable_get(:@cache).keys).to match_array([email_instance.address.domain, "another_domain.com"])
|
171
|
+
expect(ValidEmail2::Dns::CACHE.keys).to match_array([[email_instance.address.domain, Resolv::DNS::Resource::IN::MX], ["another_domain.com", Resolv::DNS::Resource::IN::MX]])
|
153
172
|
end
|
154
173
|
end
|
155
174
|
end
|
156
175
|
|
157
176
|
context "when the cache size is greater than the max cache size" do
|
158
177
|
before do
|
159
|
-
stub_const("ValidEmail2::
|
178
|
+
stub_const("ValidEmail2::Dns::MAX_CACHE_SIZE", 0)
|
160
179
|
end
|
161
180
|
|
162
181
|
it "prunes the cache" do
|
163
|
-
expect(
|
182
|
+
expect(dns_instance).to receive(:prune_cache).once
|
164
183
|
|
165
184
|
email_instance.valid_strict_mx?
|
166
185
|
end
|
167
186
|
|
168
|
-
it "calls the the MX servers lookup" do
|
187
|
+
it "calls the the MX servers lookup" do
|
169
188
|
email_instance.valid_strict_mx?
|
170
189
|
|
171
190
|
expect(Resolv::DNS).to have_received(:open).once
|
172
191
|
end
|
173
192
|
|
174
193
|
context "and there are older cached entries" do
|
175
|
-
let(:mock_cache_data) { { "another_domain.com" =>
|
194
|
+
let(:mock_cache_data) { { ["another_domain.com", Resolv::DNS::Resource::IN::MX] => ValidEmail2::Dns::CacheEntry.new(mock_mx_records, cached_at - 100, ttl) } }
|
176
195
|
|
177
196
|
it "prunes those entries" do
|
178
197
|
email_instance.valid_strict_mx?
|
179
198
|
|
180
|
-
expect(
|
181
|
-
expect(dns_records_cache_instance.instance_variable_get(:@cache).keys).to match_array([email_instance.address.domain])
|
199
|
+
expect(ValidEmail2::Dns::CACHE.keys).to match_array([[email_instance.address.domain, Resolv::DNS::Resource::IN::MX]])
|
182
200
|
end
|
183
201
|
end
|
184
202
|
end
|
@@ -187,13 +205,13 @@ describe ValidEmail2::Address do
|
|
187
205
|
|
188
206
|
describe "#valid_mx?" do
|
189
207
|
let(:cached_at) { Time.now }
|
190
|
-
let(:mock_cache_data) { { email_instance.address.domain =>
|
208
|
+
let(:mock_cache_data) { { [email_instance.address.domain, Resolv::DNS::Resource::IN::MX] => ValidEmail2::Dns::CacheEntry.new(mock_a_records, cached_at, ttl) } }
|
191
209
|
let(:mock_a_records) { [double("A", address: "192.168.1.1", ttl: ttl)] }
|
192
210
|
|
193
211
|
before do
|
194
212
|
allow(email_instance).to receive(:mx_servers).and_return(mock_mx_records)
|
195
213
|
allow(mock_resolv_dns).to receive(:getresources)
|
196
|
-
.with(email_instance.address.domain, Resolv::DNS::Resource::IN::
|
214
|
+
.with(email_instance.address.domain, Resolv::DNS::Resource::IN::MX)
|
197
215
|
.and_return(mock_a_records)
|
198
216
|
end
|
199
217
|
|
@@ -225,9 +243,9 @@ describe ValidEmail2::Address do
|
|
225
243
|
|
226
244
|
describe "ttl" do
|
227
245
|
before do
|
228
|
-
|
229
|
-
allow(ValidEmail2::
|
230
|
-
allow(
|
246
|
+
stub_const("ValidEmail2::Dns::CACHE", mock_cache_data)
|
247
|
+
allow(ValidEmail2::Dns).to receive(:new).and_return(dns_instance)
|
248
|
+
allow(dns_instance).to receive(:fetch).with(email_instance.address.domain, Resolv::DNS::Resource::IN::MX).and_call_original
|
231
249
|
end
|
232
250
|
|
233
251
|
context "when the time since last lookup is less than the cached ttl entry" do
|
@@ -253,14 +271,14 @@ describe ValidEmail2::Address do
|
|
253
271
|
|
254
272
|
describe "cache size" do
|
255
273
|
before do
|
256
|
-
|
257
|
-
allow(ValidEmail2::
|
258
|
-
allow(
|
274
|
+
stub_const("ValidEmail2::Dns::CACHE", mock_cache_data)
|
275
|
+
allow(ValidEmail2::Dns).to receive(:new).and_return(dns_instance)
|
276
|
+
allow(dns_instance).to receive(:fetch).with(email_instance.address.domain, Resolv::DNS::Resource::IN::MX).and_call_original
|
259
277
|
end
|
260
278
|
|
261
279
|
context "when the cache size is less than or equal to the max cache size" do
|
262
280
|
before do
|
263
|
-
stub_const("ValidEmail2::
|
281
|
+
stub_const("ValidEmail2::Dns::MAX_CACHE_SIZE", 1)
|
264
282
|
end
|
265
283
|
|
266
284
|
it "does not prune the cache" do
|
@@ -276,24 +294,23 @@ describe ValidEmail2::Address do
|
|
276
294
|
end
|
277
295
|
|
278
296
|
context "and there are older cached entries" do
|
279
|
-
let(:mock_cache_data) { { "another_domain.com" =>
|
297
|
+
let(:mock_cache_data) { { ["another_domain.com", Resolv::DNS::Resource::IN::MX] => ValidEmail2::Dns::CacheEntry.new(mock_a_records, cached_at - 100, ttl) } }
|
280
298
|
|
281
299
|
it "does not prune those entries" do
|
282
300
|
email_instance.valid_mx?
|
283
301
|
|
284
|
-
expect(
|
285
|
-
expect(dns_records_cache_instance.instance_variable_get(:@cache).keys).to match_array([email_instance.address.domain, "another_domain.com"])
|
302
|
+
expect(ValidEmail2::Dns::CACHE.keys).to match_array([[email_instance.address.domain, Resolv::DNS::Resource::IN::MX], ["another_domain.com", Resolv::DNS::Resource::IN::MX]])
|
286
303
|
end
|
287
304
|
end
|
288
305
|
end
|
289
306
|
|
290
307
|
context "when the cache size is greater than the max cache size" do
|
291
308
|
before do
|
292
|
-
stub_const("ValidEmail2::
|
309
|
+
stub_const("ValidEmail2::Dns::MAX_CACHE_SIZE", 0)
|
293
310
|
end
|
294
311
|
|
295
|
-
it "prunes the cache" do
|
296
|
-
expect(
|
312
|
+
it "prunes the cache" do
|
313
|
+
expect(dns_instance).to receive(:prune_cache).once
|
297
314
|
|
298
315
|
email_instance.valid_mx?
|
299
316
|
end
|
@@ -305,17 +322,16 @@ describe ValidEmail2::Address do
|
|
305
322
|
end
|
306
323
|
|
307
324
|
context "and there are older cached entries" do
|
308
|
-
let(:mock_cache_data) { { "another_domain.com" =>
|
325
|
+
let(:mock_cache_data) { { ["another_domain.com", Resolv::DNS::Resource::IN::MX] => ValidEmail2::Dns::CacheEntry.new(mock_a_records, cached_at - 100, ttl) } }
|
309
326
|
|
310
327
|
it "prunes those entries" do
|
311
328
|
email_instance.valid_mx?
|
312
329
|
|
313
|
-
expect(
|
314
|
-
expect(dns_records_cache_instance.instance_variable_get(:@cache).keys).to match_array([email_instance.address.domain])
|
330
|
+
expect(ValidEmail2::Dns::CACHE.keys).to match_array([[email_instance.address.domain, Resolv::DNS::Resource::IN::MX]])
|
315
331
|
end
|
316
332
|
end
|
317
333
|
end
|
318
|
-
end
|
334
|
+
end
|
319
335
|
end
|
320
336
|
end
|
321
337
|
end
|
data/spec/dns_spec.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "spec_helper"
|
4
|
+
|
5
|
+
describe ValidEmail2::Dns do
|
6
|
+
describe "#mx_servers" do
|
7
|
+
it "gets a record" do
|
8
|
+
dns = described_class.new
|
9
|
+
records = dns.mx_servers("gmail.com")
|
10
|
+
expect(records.size).to_not be_zero
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
describe "#a_servers" do
|
15
|
+
it "gets a record" do
|
16
|
+
dns = described_class.new
|
17
|
+
records = dns.a_servers("gmail.com")
|
18
|
+
expect(records.size).to_not be_zero
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
data/spec/spec_helper.rb
CHANGED
data/spec/valid_email2_spec.rb
CHANGED
data/valid_email2.gemspec
CHANGED
@@ -22,11 +22,11 @@ Gem::Specification.new do |spec|
|
|
22
22
|
|
23
23
|
spec.add_development_dependency "bundler", "~> 2.0"
|
24
24
|
spec.add_development_dependency "rake", "~> 12.3"
|
25
|
+
spec.add_development_dependency "securerandom", "0.3.1" # https://github.com/micke/valid_email2/actions/runs/13325070756/job/37216488894
|
25
26
|
spec.add_development_dependency "rspec", "~> 3.5"
|
26
27
|
spec.add_development_dependency "rspec-benchmark", "~> 0.6"
|
27
28
|
spec.add_development_dependency "net-smtp"
|
28
|
-
spec.add_development_dependency "
|
29
|
+
spec.add_development_dependency "debug"
|
29
30
|
spec.add_runtime_dependency "mail", "~> 2.5"
|
30
31
|
spec.add_runtime_dependency "activemodel", ">= 6.0"
|
31
|
-
spec.add_runtime_dependency "unicode-emoji", "~> 3.7.0"
|
32
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: 7.0.9
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Micke Lisinge
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2025-02-14 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -38,6 +38,20 @@ dependencies:
|
|
38
38
|
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '12.3'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: securerandom
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - '='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 0.3.1
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - '='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 0.3.1
|
41
55
|
- !ruby/object:Gem::Dependency
|
42
56
|
name: rspec
|
43
57
|
requirement: !ruby/object:Gem::Requirement
|
@@ -81,7 +95,7 @@ dependencies:
|
|
81
95
|
- !ruby/object:Gem::Version
|
82
96
|
version: '0'
|
83
97
|
- !ruby/object:Gem::Dependency
|
84
|
-
name:
|
98
|
+
name: debug
|
85
99
|
requirement: !ruby/object:Gem::Requirement
|
86
100
|
requirements:
|
87
101
|
- - ">="
|
@@ -122,20 +136,6 @@ dependencies:
|
|
122
136
|
- - ">="
|
123
137
|
- !ruby/object:Gem::Version
|
124
138
|
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
|
139
139
|
description: ActiveModel validation for email. Including MX lookup and disposable
|
140
140
|
email deny list
|
141
141
|
email:
|
@@ -158,15 +158,17 @@ files:
|
|
158
158
|
- config/disposable_email_domains.txt
|
159
159
|
- gemfiles/activemodel6.gemfile
|
160
160
|
- gemfiles/activemodel7.gemfile
|
161
|
+
- gemfiles/activemodel8.gemfile
|
161
162
|
- lib/valid_email2.rb
|
162
163
|
- lib/valid_email2/address.rb
|
163
|
-
- lib/valid_email2/
|
164
|
+
- lib/valid_email2/dns.rb
|
164
165
|
- lib/valid_email2/email_validator.rb
|
165
166
|
- lib/valid_email2/version.rb
|
166
167
|
- pull_mailchecker_emails.rb
|
167
168
|
- release-please-config.json
|
168
169
|
- spec/address_spec.rb
|
169
170
|
- spec/benchmark_spec.rb
|
171
|
+
- spec/dns_spec.rb
|
170
172
|
- spec/spec_helper.rb
|
171
173
|
- spec/valid_email2_spec.rb
|
172
174
|
- valid_email2.gemspec
|
@@ -174,7 +176,7 @@ homepage: https://github.com/micke/valid_email2
|
|
174
176
|
licenses:
|
175
177
|
- MIT
|
176
178
|
metadata: {}
|
177
|
-
post_install_message:
|
179
|
+
post_install_message:
|
178
180
|
rdoc_options: []
|
179
181
|
require_paths:
|
180
182
|
- lib
|
@@ -190,12 +192,13 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
190
192
|
version: '0'
|
191
193
|
requirements: []
|
192
194
|
rubygems_version: 3.5.3
|
193
|
-
signing_key:
|
195
|
+
signing_key:
|
194
196
|
specification_version: 4
|
195
197
|
summary: ActiveModel validation for email. Including MX lookup and disposable email
|
196
198
|
deny list
|
197
199
|
test_files:
|
198
200
|
- spec/address_spec.rb
|
199
201
|
- spec/benchmark_spec.rb
|
202
|
+
- spec/dns_spec.rb
|
200
203
|
- spec/spec_helper.rb
|
201
204
|
- spec/valid_email2_spec.rb
|
@@ -1,37 +0,0 @@
|
|
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
|