valid_email2 5.3.0 → 7.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/release.yaml +1 -1
- data/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +31 -0
- data/README.md +8 -2
- data/config/disposable_email_domains.txt +2 -2
- data/lib/valid_email2/address.rb +42 -20
- data/lib/valid_email2/dns_records_cache.rb +37 -0
- data/lib/valid_email2/email_validator.rb +5 -20
- data/lib/valid_email2/version.rb +1 -1
- data/lib/valid_email2.rb +2 -24
- data/pull_mailchecker_emails.rb +3 -3
- data/spec/address_spec.rb +300 -0
- data/spec/spec_helper.rb +1 -0
- data/spec/valid_email2_spec.rb +10 -0
- data/valid_email2.gemspec +2 -2
- metadata +6 -6
- data/lib/helpers/deprecation_helper.rb +0 -14
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 18dccc719c18b67299952ce4096d4b1c0184810985015dacfc883d7b7dcb021f
|
4
|
+
data.tar.gz: c3a7f37eceb1dde232850accbd1fed04ba73cff1a899d4f12762f54d2d88a533
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz: '
|
6
|
+
metadata.gz: b5276142248e94cc178345e6ea069fb1675003bd2aa011a732287b0cae349f0e43c2fe8a855080550286b373b0fec3359658555efb4b4de0cec0d22a2fff6f2e
|
7
|
+
data.tar.gz: '087ae98e571ac2eb9acbb0e1230f19e090f577d44e2d0eba75feb7fa2aeafa391848cb8eb86aca95c38e22a99b099ed702206f9bfa7232e6f4356a66ecceb040'
|
@@ -1 +1 @@
|
|
1
|
-
{".":"
|
1
|
+
{".":"7.0.0"}
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,36 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
+
## [7.0.0](https://github.com/micke/valid_email2/compare/v6.0.0...v7.0.0) (2024-11-19)
|
4
|
+
|
5
|
+
|
6
|
+
### ⚠ BREAKING CHANGES
|
7
|
+
|
8
|
+
* revert allowing scandinavian characters by making allowed multibyte characters configurable ([#261](https://github.com/micke/valid_email2/issues/261))
|
9
|
+
|
10
|
+
### Bug Fixes
|
11
|
+
|
12
|
+
* revert allowing scandinavian characters by making allowed multibyte characters configurable ([#261](https://github.com/micke/valid_email2/issues/261)) ([cf6a1e9](https://github.com/micke/valid_email2/commit/cf6a1e9e28f78e0c6f3e3ea7e9160bf9194b45e6))
|
13
|
+
|
14
|
+
## [6.0.0](https://github.com/micke/valid_email2/compare/v5.3.0...v6.0.0) (2024-11-03)
|
15
|
+
|
16
|
+
|
17
|
+
### ⚠ BREAKING CHANGES
|
18
|
+
|
19
|
+
* Remove deprecated methods and options
|
20
|
+
|
21
|
+
### Features
|
22
|
+
|
23
|
+
* Cache DNS lookups ([#256](https://github.com/micke/valid_email2/issues/256)) ([72115ec](https://github.com/micke/valid_email2/commit/72115ec1b866e54b5a4d530d7eaeb7e52a3c8e98))
|
24
|
+
* Remove deprecated methods and options ([1a29d27](https://github.com/micke/valid_email2/commit/1a29d27d587a39d181dfe2b6f39028bc317aff52))
|
25
|
+
|
26
|
+
|
27
|
+
### Bug Fixes
|
28
|
+
|
29
|
+
* disallow # in domain ([#259](https://github.com/micke/valid_email2/issues/259)) ([1643323](https://github.com/micke/valid_email2/commit/1643323fa3da8973cb63a727410aa9696706e3c8))
|
30
|
+
* **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))
|
31
|
+
* Falsely detecting Scandinavian characters as emojis ([#257](https://github.com/micke/valid_email2/issues/257)) ([e64de5c](https://github.com/micke/valid_email2/commit/e64de5c675a015c09c7e6b89bb4f65a39137f48f))
|
32
|
+
* 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))
|
33
|
+
|
3
34
|
## [5.3.0](https://github.com/micke/valid_email2/compare/v5.2.6...v5.3.0) (2024-08-31)
|
4
35
|
|
5
36
|
|
data/README.md
CHANGED
@@ -84,7 +84,7 @@ To validate that the domain is not a disposable email or a disposable email (che
|
|
84
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 on the deny list (under config/
|
87
|
+
To validate that the domain is not on the deny list (under config/deny_listed_email_domains.yml):
|
88
88
|
```ruby
|
89
89
|
validates :email, 'valid_email_2/email': { deny_list: true }
|
90
90
|
```
|
@@ -128,6 +128,12 @@ address.valid_strict_mx? => true
|
|
128
128
|
address.subaddressed? => false
|
129
129
|
```
|
130
130
|
|
131
|
+
If you want to allow multibyte characters, set it explicitly.
|
132
|
+
|
133
|
+
```ruby
|
134
|
+
ValidEmail2::Address.permitted_multibyte_characters_regex = /[ÆæØøÅåÄäÖöÞþÐð]/
|
135
|
+
```
|
136
|
+
|
131
137
|
### Test environment
|
132
138
|
|
133
139
|
If you are validating `mx` then your specs will fail without an internet connection.
|
@@ -151,7 +157,7 @@ This gem is tested against currently supported Ruby and Rails versions. For an u
|
|
151
157
|
|
152
158
|
In version v5.3.0 the config directory files were renamed as follows:
|
153
159
|
|
154
|
-
`config/blacklisted_email_domains.yml` -> `config/deny_listed_email_domains.yml`
|
160
|
+
`config/blacklisted_email_domains.yml` -> `config/deny_listed_email_domains.yml`
|
155
161
|
`config/whitelisted_email_domains.yml` -> `config/allow_listed_email_domains.yml`
|
156
162
|
|
157
163
|
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.
|
@@ -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
|
@@ -85655,6 +85654,7 @@ kwa-law.com
|
|
85655
85654
|
kwa.xyz
|
85656
85655
|
kwacollections.com
|
85657
85656
|
kwadratowamaskar.pl
|
85657
|
+
kwalah.com
|
85658
85658
|
kwalidd.cf
|
85659
85659
|
kwangjinmold.com
|
85660
85660
|
kwanj.ml
|
@@ -168875,4 +168875,4 @@ zzzmail.pl
|
|
168875
168875
|
zzzpush.icu
|
168876
168876
|
zzzz1717.com
|
168877
168877
|
zzzzzzzzzzzzz.com
|
168878
|
-
zzzzzzzzzzzzz.com0-00.usa.cc
|
168878
|
+
zzzzzzzzzzzzz.com0-00.usa.cc
|
data/lib/valid_email2/address.rb
CHANGED
@@ -3,15 +3,13 @@
|
|
3
3
|
require "valid_email2"
|
4
4
|
require "resolv"
|
5
5
|
require "mail"
|
6
|
-
|
6
|
+
require "valid_email2/dns_records_cache"
|
7
7
|
|
8
8
|
module ValidEmail2
|
9
9
|
class Address
|
10
|
-
extend DeprecationHelper
|
11
|
-
|
12
10
|
attr_accessor :address
|
13
11
|
|
14
|
-
PROHIBITED_DOMAIN_CHARACTERS_REGEX = /[+!_\/\s'
|
12
|
+
PROHIBITED_DOMAIN_CHARACTERS_REGEX = /[+!_\/\s'#`]/
|
15
13
|
DEFAULT_RECIPIENT_DELIMITER = '+'
|
16
14
|
DOT_DELIMITER = '.'
|
17
15
|
|
@@ -23,13 +21,19 @@ module ValidEmail2
|
|
23
21
|
@prohibited_domain_characters_regex = val
|
24
22
|
end
|
25
23
|
|
24
|
+
def self.permitted_multibyte_characters_regex
|
25
|
+
@permitted_multibyte_characters_regex
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.permitted_multibyte_characters_regex=(val)
|
29
|
+
@permitted_multibyte_characters_regex = val
|
30
|
+
end
|
31
|
+
|
26
32
|
def initialize(address, dns_timeout = 5, dns_nameserver = nil)
|
27
33
|
@parse_error = false
|
28
34
|
@raw_address = address
|
29
35
|
@dns_timeout = dns_timeout
|
30
|
-
|
31
|
-
@resolv_config = Resolv::DNS::Config.default_config_hash
|
32
|
-
@resolv_config[:nameserver] = dns_nameserver if dns_nameserver
|
36
|
+
@dns_nameserver = dns_nameserver
|
33
37
|
|
34
38
|
begin
|
35
39
|
@address = Mail::Address.new(address)
|
@@ -37,7 +41,7 @@ module ValidEmail2
|
|
37
41
|
@parse_error = true
|
38
42
|
end
|
39
43
|
|
40
|
-
@parse_error ||=
|
44
|
+
@parse_error ||= address_contain_multibyte_characters?
|
41
45
|
end
|
42
46
|
|
43
47
|
def valid?
|
@@ -90,12 +94,10 @@ module ValidEmail2
|
|
90
94
|
def allow_listed?
|
91
95
|
domain_is_in?(ValidEmail2.allow_list)
|
92
96
|
end
|
93
|
-
deprecate_method :whitelisted?, :allow_listed?
|
94
97
|
|
95
98
|
def deny_listed?
|
96
99
|
valid? && domain_is_in?(ValidEmail2.deny_list)
|
97
100
|
end
|
98
|
-
deprecate_method :blacklisted?, :deny_listed?
|
99
101
|
|
100
102
|
def valid_mx?
|
101
103
|
return false unless valid?
|
@@ -135,16 +137,32 @@ module ValidEmail2
|
|
135
137
|
}
|
136
138
|
end
|
137
139
|
|
138
|
-
def
|
139
|
-
return false if
|
140
|
+
def address_contain_multibyte_characters?
|
141
|
+
return false if @raw_address.nil?
|
140
142
|
|
141
|
-
|
143
|
+
return false if @raw_address.ascii_only?
|
144
|
+
|
145
|
+
@raw_address.each_char.any? { |char| char.bytesize > 1 && char !~ self.class.permitted_multibyte_characters_regex }
|
146
|
+
end
|
147
|
+
|
148
|
+
def resolv_config
|
149
|
+
@resolv_config ||= begin
|
150
|
+
config = Resolv::DNS::Config.default_config_hash
|
151
|
+
config[:nameserver] = @dns_nameserver if @dns_nameserver
|
152
|
+
config
|
153
|
+
end
|
154
|
+
|
155
|
+
@resolv_config
|
142
156
|
end
|
143
157
|
|
144
158
|
def mx_servers
|
145
|
-
@
|
146
|
-
|
147
|
-
|
159
|
+
@mx_servers_cache ||= ValidEmail2::DnsRecordsCache.new
|
160
|
+
|
161
|
+
@mx_servers_cache.fetch(address.domain.downcase) do
|
162
|
+
Resolv::DNS.open(resolv_config) do |dns|
|
163
|
+
dns.timeouts = @dns_timeout
|
164
|
+
dns.getresources(address.domain, Resolv::DNS::Resource::IN::MX)
|
165
|
+
end
|
148
166
|
end
|
149
167
|
end
|
150
168
|
|
@@ -153,10 +171,14 @@ module ValidEmail2
|
|
153
171
|
end
|
154
172
|
|
155
173
|
def mx_or_a_servers
|
156
|
-
@
|
157
|
-
|
158
|
-
|
159
|
-
|
174
|
+
@mx_or_a_servers_cache ||= ValidEmail2::DnsRecordsCache.new
|
175
|
+
|
176
|
+
@mx_or_a_servers_cache.fetch(address.domain.downcase) do
|
177
|
+
Resolv::DNS.open(resolv_config) do |dns|
|
178
|
+
dns.timeouts = @dns_timeout
|
179
|
+
(mx_servers.any? && mx_servers) ||
|
180
|
+
dns.getresources(address.domain, Resolv::DNS::Resource::IN::A)
|
181
|
+
end
|
160
182
|
end
|
161
183
|
end
|
162
184
|
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
|
@@ -1,12 +1,9 @@
|
|
1
1
|
require "valid_email2/address"
|
2
2
|
require "active_model"
|
3
3
|
require "active_model/validations"
|
4
|
-
require_relative "../helpers/deprecation_helper"
|
5
4
|
|
6
5
|
module ValidEmail2
|
7
6
|
class EmailValidator < ActiveModel::EachValidator
|
8
|
-
include DeprecationHelper
|
9
|
-
|
10
7
|
def default_options
|
11
8
|
{ disposable: false, mx: false, strict_mx: false, disallow_subaddressing: false, multiple: false, dns_timeout: 5, dns_nameserver: nil }
|
12
9
|
end
|
@@ -35,27 +32,15 @@ module ValidEmail2
|
|
35
32
|
error(record, attribute) && return if addresses.any?(&:disposable_domain?)
|
36
33
|
end
|
37
34
|
|
38
|
-
if options[:
|
39
|
-
deprecation_message(:disposable_with_whitelist, :disposable_with_allow_list)
|
40
|
-
end
|
41
|
-
|
42
|
-
if options[:disposable_with_allow_list] || options[:disposable_with_whitelist]
|
35
|
+
if options[:disposable_with_allow_list]
|
43
36
|
error(record, attribute) && return if addresses.any? { |address| address.disposable? && !address.allow_listed? }
|
44
37
|
end
|
45
38
|
|
46
|
-
if options[:
|
47
|
-
deprecation_message(:disposable_domain_with_whitelist, :disposable_domain_with_allow_list)
|
48
|
-
end
|
49
|
-
|
50
|
-
if options[:disposable_domain_with_allow_list] || options[:disposable_domain_with_whitelist]
|
39
|
+
if options[:disposable_domain_with_allow_list]
|
51
40
|
error(record, attribute) && return if addresses.any? { |address| address.disposable_domain? && !address.allow_listed? }
|
52
41
|
end
|
53
42
|
|
54
|
-
if options[:
|
55
|
-
deprecation_message(:blacklist, :deny_list)
|
56
|
-
end
|
57
|
-
|
58
|
-
if options[:deny_list] || options[:blacklist]
|
43
|
+
if options[:deny_list]
|
59
44
|
error(record, attribute) && return if addresses.any?(&:deny_listed?)
|
60
45
|
end
|
61
46
|
|
@@ -72,12 +57,12 @@ module ValidEmail2
|
|
72
57
|
options = default_options.merge(self.options)
|
73
58
|
|
74
59
|
if options[:multiple]
|
75
|
-
email_list = input.is_a?(Array) ? input : input.split(',')
|
60
|
+
email_list = input.is_a?(Array) ? input : input.split(',').map(&:strip)
|
76
61
|
else
|
77
62
|
email_list = [input]
|
78
63
|
end
|
79
64
|
|
80
|
-
email_list.reject(&:empty?)
|
65
|
+
email_list.reject(&:empty?)
|
81
66
|
end
|
82
67
|
|
83
68
|
def error(record, attribute)
|
data/lib/valid_email2/version.rb
CHANGED
data/lib/valid_email2.rb
CHANGED
@@ -1,36 +1,24 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "valid_email2/email_validator"
|
4
|
-
require_relative "./helpers/deprecation_helper"
|
5
4
|
|
6
5
|
module ValidEmail2
|
7
|
-
|
8
|
-
BLACKLIST_FILE = "config/blacklisted_email_domains.yml"
|
9
6
|
DENY_LIST_FILE = "config/deny_listed_email_domains.yml"
|
10
|
-
WHITELIST_FILE = "config/whitelisted_email_domains.yml"
|
11
7
|
ALLOW_LIST_FILE = "config/allow_listed_email_domains.yml"
|
12
8
|
DISPOSABLE_FILE = File.expand_path('../config/disposable_email_domains.txt', __dir__)
|
13
9
|
|
14
10
|
class << self
|
15
|
-
extend DeprecationHelper
|
16
|
-
|
17
11
|
def disposable_emails
|
18
12
|
@disposable_emails ||= load_file(DISPOSABLE_FILE)
|
19
13
|
end
|
20
14
|
|
21
15
|
def deny_list
|
22
|
-
@deny_list ||= load_if_exists(DENY_LIST_FILE) ||
|
23
|
-
load_deprecated_if_exists(BLACKLIST_FILE) ||
|
24
|
-
Set.new
|
16
|
+
@deny_list ||= load_if_exists(DENY_LIST_FILE) || Set.new
|
25
17
|
end
|
26
|
-
deprecate_method :blacklist, :deny_list
|
27
18
|
|
28
19
|
def allow_list
|
29
|
-
@allow_list ||= load_if_exists(ALLOW_LIST_FILE) ||
|
30
|
-
load_deprecated_if_exists(WHITELIST_FILE) ||
|
31
|
-
Set.new
|
20
|
+
@allow_list ||= load_if_exists(ALLOW_LIST_FILE) || Set.new
|
32
21
|
end
|
33
|
-
deprecate_method :whitelist, :allow_list
|
34
22
|
|
35
23
|
private
|
36
24
|
|
@@ -38,16 +26,6 @@ module ValidEmail2
|
|
38
26
|
load_file(path) if File.exist?(path)
|
39
27
|
end
|
40
28
|
|
41
|
-
def load_deprecated_if_exists(path)
|
42
|
-
if File.exist?(path)
|
43
|
-
warn <<~WARN
|
44
|
-
Warning: The file `#{path}` used by valid_email2 is deprecated and won't be read in version 6 of valid_email2;
|
45
|
-
Rename the file to `#{path.gsub("blacklisted", "deny_listed").gsub("whitelisted", "allow_listed")}` instead."
|
46
|
-
WARN
|
47
|
-
load_file(path)
|
48
|
-
end
|
49
|
-
end
|
50
|
-
|
51
29
|
def load_file(path)
|
52
30
|
# This method MUST return a Set, otherwise the
|
53
31
|
# performance will suffer!
|
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/spec/address_spec.rb
CHANGED
@@ -33,5 +33,305 @@ 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 invalid if it contains Japanese characters" do
|
38
|
+
address = described_class.new("あいうえお@example.com")
|
39
|
+
expect(address.valid?).to be false
|
40
|
+
end
|
41
|
+
|
42
|
+
it "is invalid if it contains special scandinavian characters" do
|
43
|
+
address = described_class.new("jørgen@email.dk")
|
44
|
+
expect(address.valid?).to eq false
|
45
|
+
end
|
46
|
+
|
47
|
+
context "permitted_multibyte_characters_regex is set" do
|
48
|
+
before do
|
49
|
+
described_class.permitted_multibyte_characters_regex = /[ÆæØøÅåÄäÖöÞþÐð]/
|
50
|
+
end
|
51
|
+
|
52
|
+
it "is valid if it contains special scandinavian characters" do
|
53
|
+
address = described_class.new("jørgen@email.dk")
|
54
|
+
expect(address.valid?).to eq true
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
describe "caching" do
|
60
|
+
let(:email_address) { "example@ymail.com" }
|
61
|
+
let(:email_instance) { described_class.new(email_address) }
|
62
|
+
let(:dns_records_cache_instance) { ValidEmail2::DnsRecordsCache.new }
|
63
|
+
let(:ttl) { 1_000 }
|
64
|
+
let(:mock_resolv_dns) { instance_double(Resolv::DNS) }
|
65
|
+
let(:mock_mx_records) { [double("MX", exchange: "mx.ymail.com", preference: 10, ttl: ttl)] }
|
66
|
+
|
67
|
+
before do
|
68
|
+
allow(email_instance).to receive(:null_mx?).and_return(false)
|
69
|
+
allow(Resolv::DNS).to receive(:open).and_yield(mock_resolv_dns)
|
70
|
+
allow(mock_resolv_dns).to receive(:timeouts=)
|
71
|
+
end
|
72
|
+
|
73
|
+
describe "#valid_strict_mx?" do
|
74
|
+
let(:cached_at) { Time.now }
|
75
|
+
let(:mock_cache_data) { { email_instance.address.domain => { records: mock_mx_records, cached_at: cached_at, ttl: ttl } } }
|
76
|
+
|
77
|
+
before do
|
78
|
+
allow(mock_resolv_dns).to receive(:getresources)
|
79
|
+
.with(email_instance.address.domain, Resolv::DNS::Resource::IN::MX)
|
80
|
+
.and_return(mock_mx_records)
|
81
|
+
end
|
82
|
+
|
83
|
+
it "calls the MX servers lookup when the email is not cached" do
|
84
|
+
result = email_instance.valid_strict_mx?
|
85
|
+
|
86
|
+
expect(Resolv::DNS).to have_received(:open).once
|
87
|
+
expect(result).to be true
|
88
|
+
end
|
89
|
+
|
90
|
+
it "does not call the MX servers lookup when the email is cached" do
|
91
|
+
email_instance.valid_strict_mx?
|
92
|
+
email_instance.valid_strict_mx?
|
93
|
+
|
94
|
+
expect(Resolv::DNS).to have_received(:open).once
|
95
|
+
end
|
96
|
+
|
97
|
+
it "returns the cached result for subsequent calls" do
|
98
|
+
first_result = email_instance.valid_strict_mx?
|
99
|
+
expect(first_result).to be true
|
100
|
+
|
101
|
+
allow(mock_resolv_dns).to receive(:getresources)
|
102
|
+
.with(email_instance.address.domain, Resolv::DNS::Resource::IN::MX)
|
103
|
+
.and_return([])
|
104
|
+
|
105
|
+
second_result = email_instance.valid_strict_mx?
|
106
|
+
expect(second_result).to be true
|
107
|
+
end
|
108
|
+
|
109
|
+
describe "ttl" do
|
110
|
+
before do
|
111
|
+
dns_records_cache_instance.instance_variable_set(:@cache, mock_cache_data)
|
112
|
+
allow(ValidEmail2::DnsRecordsCache).to receive(:new).and_return(dns_records_cache_instance)
|
113
|
+
allow(dns_records_cache_instance).to receive(:fetch).with(email_instance.address.domain).and_call_original
|
114
|
+
end
|
115
|
+
|
116
|
+
context "when the time since last lookup is less than the cached ttl entry" do
|
117
|
+
let(:cached_at) { Time.now }
|
118
|
+
|
119
|
+
it "does not call the MX servers lookup" do
|
120
|
+
email_instance.valid_strict_mx?
|
121
|
+
|
122
|
+
expect(Resolv::DNS).not_to have_received(:open)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
context "when the time since last lookup is greater than the cached ttl entry" do
|
127
|
+
let(:cached_at) { Time.now - ttl }
|
128
|
+
|
129
|
+
it "calls the MX servers lookup" do
|
130
|
+
email_instance.valid_strict_mx?
|
131
|
+
|
132
|
+
expect(Resolv::DNS).to have_received(:open).once
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
describe "cache size" do
|
138
|
+
before do
|
139
|
+
dns_records_cache_instance.instance_variable_set(:@cache, mock_cache_data)
|
140
|
+
allow(ValidEmail2::DnsRecordsCache).to receive(:new).and_return(dns_records_cache_instance)
|
141
|
+
allow(dns_records_cache_instance).to receive(:fetch).with(email_instance.address.domain).and_call_original
|
142
|
+
end
|
143
|
+
|
144
|
+
context "when the cache size is less than or equal to the max cache size" do
|
145
|
+
before do
|
146
|
+
stub_const("ValidEmail2::DnsRecordsCache::MAX_CACHE_SIZE", 1)
|
147
|
+
end
|
148
|
+
|
149
|
+
it "does not prune the cache" do
|
150
|
+
expect(dns_records_cache_instance).not_to receive(:prune_cache)
|
151
|
+
|
152
|
+
email_instance.valid_strict_mx?
|
153
|
+
end
|
154
|
+
|
155
|
+
it "does not call the MX servers lookup" do
|
156
|
+
email_instance.valid_strict_mx?
|
157
|
+
|
158
|
+
expect(Resolv::DNS).not_to have_received(:open)
|
159
|
+
end
|
160
|
+
|
161
|
+
context "and there are older cached entries" do
|
162
|
+
let(:mock_cache_data) { { "another_domain.com" => { records: mock_mx_records, cached_at: cached_at - 100, ttl: ttl } } }
|
163
|
+
|
164
|
+
it "does not prune those entries" do
|
165
|
+
email_instance.valid_strict_mx?
|
166
|
+
|
167
|
+
expect(dns_records_cache_instance.instance_variable_get(:@cache).keys.size).to eq 2
|
168
|
+
expect(dns_records_cache_instance.instance_variable_get(:@cache).keys).to match_array([email_instance.address.domain, "another_domain.com"])
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
context "when the cache size is greater than the max cache size" do
|
174
|
+
before do
|
175
|
+
stub_const("ValidEmail2::DnsRecordsCache::MAX_CACHE_SIZE", 0)
|
176
|
+
end
|
177
|
+
|
178
|
+
it "prunes the cache" do
|
179
|
+
expect(dns_records_cache_instance).to receive(:prune_cache).once
|
180
|
+
|
181
|
+
email_instance.valid_strict_mx?
|
182
|
+
end
|
183
|
+
|
184
|
+
it "calls the the MX servers lookup" do
|
185
|
+
email_instance.valid_strict_mx?
|
186
|
+
|
187
|
+
expect(Resolv::DNS).to have_received(:open).once
|
188
|
+
end
|
189
|
+
|
190
|
+
context "and there are older cached entries" do
|
191
|
+
let(:mock_cache_data) { { "another_domain.com" => { records: mock_mx_records, cached_at: cached_at - 100, ttl: ttl } } }
|
192
|
+
|
193
|
+
it "prunes those entries" do
|
194
|
+
email_instance.valid_strict_mx?
|
195
|
+
|
196
|
+
expect(dns_records_cache_instance.instance_variable_get(:@cache).keys.size).to eq 1
|
197
|
+
expect(dns_records_cache_instance.instance_variable_get(:@cache).keys).to match_array([email_instance.address.domain])
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
describe "#valid_mx?" do
|
205
|
+
let(:cached_at) { Time.now }
|
206
|
+
let(:mock_cache_data) { { email_instance.address.domain => { records: mock_a_records, cached_at: cached_at, ttl: ttl } } }
|
207
|
+
let(:mock_a_records) { [double("A", address: "192.168.1.1", ttl: ttl)] }
|
208
|
+
|
209
|
+
before do
|
210
|
+
allow(email_instance).to receive(:mx_servers).and_return(mock_mx_records)
|
211
|
+
allow(mock_resolv_dns).to receive(:getresources)
|
212
|
+
.with(email_instance.address.domain, Resolv::DNS::Resource::IN::A)
|
213
|
+
.and_return(mock_a_records)
|
214
|
+
end
|
215
|
+
|
216
|
+
it "calls the MX or A servers lookup when the email is not cached" do
|
217
|
+
result = email_instance.valid_mx?
|
218
|
+
|
219
|
+
expect(Resolv::DNS).to have_received(:open).once
|
220
|
+
expect(result).to be true
|
221
|
+
end
|
222
|
+
|
223
|
+
it "does not call the MX or A servers lookup when the email is cached" do
|
224
|
+
email_instance.valid_mx?
|
225
|
+
email_instance.valid_mx?
|
226
|
+
|
227
|
+
expect(Resolv::DNS).to have_received(:open).once
|
228
|
+
end
|
229
|
+
|
230
|
+
it "returns the cached result for subsequent calls" do
|
231
|
+
first_result = email_instance.valid_mx?
|
232
|
+
expect(first_result).to be true
|
233
|
+
|
234
|
+
allow(mock_resolv_dns).to receive(:getresources)
|
235
|
+
.with(email_instance.address.domain, Resolv::DNS::Resource::IN::A)
|
236
|
+
.and_return([])
|
237
|
+
|
238
|
+
second_result = email_instance.valid_mx?
|
239
|
+
expect(second_result).to be true
|
240
|
+
end
|
241
|
+
|
242
|
+
describe "ttl" do
|
243
|
+
before do
|
244
|
+
dns_records_cache_instance.instance_variable_set(:@cache, mock_cache_data)
|
245
|
+
allow(ValidEmail2::DnsRecordsCache).to receive(:new).and_return(dns_records_cache_instance)
|
246
|
+
allow(dns_records_cache_instance).to receive(:fetch).with(email_instance.address.domain).and_call_original
|
247
|
+
end
|
248
|
+
|
249
|
+
context "when the time since last lookup is less than the cached ttl entry" do
|
250
|
+
let(:cached_at) { Time.now }
|
251
|
+
|
252
|
+
it "does not call the MX or A servers lookup" do
|
253
|
+
email_instance.valid_mx?
|
254
|
+
|
255
|
+
expect(Resolv::DNS).not_to have_received(:open)
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
context "when the time since last lookup is greater than the cached ttl entry" do
|
260
|
+
let(:cached_at) { Time.now - ttl }
|
261
|
+
|
262
|
+
it "calls the MX or A servers lookup " do
|
263
|
+
email_instance.valid_mx?
|
264
|
+
|
265
|
+
expect(Resolv::DNS).to have_received(:open).once
|
266
|
+
end
|
267
|
+
end
|
268
|
+
end
|
269
|
+
|
270
|
+
describe "cache size" do
|
271
|
+
before do
|
272
|
+
dns_records_cache_instance.instance_variable_set(:@cache, mock_cache_data)
|
273
|
+
allow(ValidEmail2::DnsRecordsCache).to receive(:new).and_return(dns_records_cache_instance)
|
274
|
+
allow(dns_records_cache_instance).to receive(:fetch).with(email_instance.address.domain).and_call_original
|
275
|
+
end
|
276
|
+
|
277
|
+
context "when the cache size is less than or equal to the max cache size" do
|
278
|
+
before do
|
279
|
+
stub_const("ValidEmail2::DnsRecordsCache::MAX_CACHE_SIZE", 1)
|
280
|
+
end
|
281
|
+
|
282
|
+
it "does not prune the cache" do
|
283
|
+
expect(email_instance).not_to receive(:prune_cache)
|
284
|
+
|
285
|
+
email_instance.valid_mx?
|
286
|
+
end
|
287
|
+
|
288
|
+
it "does not call the MX or A servers lookup" do
|
289
|
+
email_instance.valid_mx?
|
290
|
+
|
291
|
+
expect(Resolv::DNS).not_to have_received(:open)
|
292
|
+
end
|
293
|
+
|
294
|
+
context "and there are older cached entries" do
|
295
|
+
let(:mock_cache_data) { { "another_domain.com" => { records: mock_a_records, cached_at: cached_at - 100, ttl: ttl } } }
|
296
|
+
|
297
|
+
it "does not prune those entries" do
|
298
|
+
email_instance.valid_mx?
|
299
|
+
|
300
|
+
expect(dns_records_cache_instance.instance_variable_get(:@cache).keys.size).to eq 2
|
301
|
+
expect(dns_records_cache_instance.instance_variable_get(:@cache).keys).to match_array([email_instance.address.domain, "another_domain.com"])
|
302
|
+
end
|
303
|
+
end
|
304
|
+
end
|
305
|
+
|
306
|
+
context "when the cache size is greater than the max cache size" do
|
307
|
+
before do
|
308
|
+
stub_const("ValidEmail2::DnsRecordsCache::MAX_CACHE_SIZE", 0)
|
309
|
+
end
|
310
|
+
|
311
|
+
it "prunes the cache" do
|
312
|
+
expect(dns_records_cache_instance).to receive(:prune_cache).once
|
313
|
+
|
314
|
+
email_instance.valid_mx?
|
315
|
+
end
|
316
|
+
|
317
|
+
it "calls the MX or A servers lookup" do
|
318
|
+
email_instance.valid_mx?
|
319
|
+
|
320
|
+
expect(Resolv::DNS).to have_received(:open).once
|
321
|
+
end
|
322
|
+
|
323
|
+
context "and there are older cached entries" do
|
324
|
+
let(:mock_cache_data) { { "another_domain.com" => { records: mock_a_records, cached_at: cached_at - 100, ttl: ttl } } }
|
325
|
+
|
326
|
+
it "prunes those entries" do
|
327
|
+
email_instance.valid_mx?
|
328
|
+
|
329
|
+
expect(dns_records_cache_instance.instance_variable_get(:@cache).keys.size).to eq 1
|
330
|
+
expect(dns_records_cache_instance.instance_variable_get(:@cache).keys).to match_array([email_instance.address.domain])
|
331
|
+
end
|
332
|
+
end
|
333
|
+
end
|
334
|
+
end
|
335
|
+
end
|
36
336
|
end
|
37
337
|
end
|
data/spec/spec_helper.rb
CHANGED
data/spec/valid_email2_spec.rb
CHANGED
@@ -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
|
data/valid_email2.gemspec
CHANGED
@@ -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,5 @@ 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
31
|
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.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-19 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -114,14 +114,14 @@ 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
125
|
description: ActiveModel validation for email. Including MX lookup and disposable
|
126
126
|
email deny list
|
127
127
|
email:
|
@@ -144,9 +144,9 @@ files:
|
|
144
144
|
- config/disposable_email_domains.txt
|
145
145
|
- gemfiles/activemodel6.gemfile
|
146
146
|
- gemfiles/activemodel7.gemfile
|
147
|
-
- lib/helpers/deprecation_helper.rb
|
148
147
|
- lib/valid_email2.rb
|
149
148
|
- lib/valid_email2/address.rb
|
149
|
+
- lib/valid_email2/dns_records_cache.rb
|
150
150
|
- lib/valid_email2/email_validator.rb
|
151
151
|
- lib/valid_email2/version.rb
|
152
152
|
- pull_mailchecker_emails.rb
|
@@ -168,7 +168,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
168
168
|
requirements:
|
169
169
|
- - ">="
|
170
170
|
- !ruby/object:Gem::Version
|
171
|
-
version: 1.
|
171
|
+
version: 3.1.0
|
172
172
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
173
173
|
requirements:
|
174
174
|
- - ">="
|
@@ -1,14 +0,0 @@
|
|
1
|
-
module DeprecationHelper
|
2
|
-
def deprecate_method(old_method, new_method)
|
3
|
-
define_method(old_method) do |*args, &block|
|
4
|
-
klass = is_a? Module
|
5
|
-
target = klass ? "#{self}." : "#{self.class}#"
|
6
|
-
warn "Warning: `#{target}#{old_method}` is deprecated and will be removed in version 6 of valid_email2; use `#{new_method}` instead."
|
7
|
-
send(new_method, *args, &block)
|
8
|
-
end
|
9
|
-
end
|
10
|
-
|
11
|
-
def deprecation_message(old_name, new_name)
|
12
|
-
warn "Warning: `#{old_name}` is deprecated and will be removed in version 6 of valid_email2; use `#{new_name}` instead."
|
13
|
-
end
|
14
|
-
end
|