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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b0a01a2acf82274bd6e459721211ff637ab27f24ca2ec01ae4423ed4ffac25d9
4
- data.tar.gz: cd8506ab6c60327a789e2fb4f1d702a49a2059ff2c9af79c6db10ecb0c7023b0
3
+ metadata.gz: a0296ec313cffb7390cb7ca2b22d48f15854bbb1cc8dc1442b31389b7350f31b
4
+ data.tar.gz: 11da87b6097bdc4df988d16045809dc42336b68b2f4fdf087a1ad38013619061
5
5
  SHA512:
6
- metadata.gz: 91af74f0a0c8fecac0ade9e9320186c0e86c76e48665d07ab79fe9ef0fdde75a3ff37d0d39b2cce8e2fd30a04a4815492ab5f24e7094f55b7852e3a6b41aef95
7
- data.tar.gz: d19d1e8c83b8aa2833ed2d472e41b8722018d08d5d465416f4cefc8c588dd9a9334d07401787acb9cee89cc9b326892097a6d3ad5d5ff015b0bce774bdbfc4ee
6
+ metadata.gz: e6003807039faf5c307d53b2617adcc5c0a0e75055f560e48073ea9cc1d5093f50aba58f2cbe121cc913b99ba8df62789e11a59d236cd0b13b17ad800925e5a5
7
+ data.tar.gz: 981042cfa3c8a72f101ba40a3922a0a3c2c2aa26ea0635ac4beb0ce59917171ec0ff1126dde68c77edb42939e4618372960c88645f720ad588e6735a2c392baf
@@ -1,5 +1,5 @@
1
1
  name: CI
2
- on: [push, pull_request]
2
+ on: [push, pull_request_target]
3
3
  jobs:
4
4
  test:
5
5
  strategy:
@@ -1,7 +1,7 @@
1
1
  on:
2
2
  push:
3
3
  branches:
4
- - master
4
+ - main
5
5
 
6
6
  permissions:
7
7
  contents: write
@@ -1 +1 @@
1
- {".":"5.2.6"}
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 whitelisted (under config/whitelisted_email_domains.yml):
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': { disposable_with_whitelist: true }
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 whitelisted (under config/whitelisted_email_domains.yml):
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': { disposable_domain_with_whitelist: true }
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 blacklisted (under config/blacklisted_email_domains.yml):
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': { blacklist: true }
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
@@ -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? @raw_address
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 whitelisted?
88
- domain_is_in?(ValidEmail2.whitelist)
87
+ def allow_listed?
88
+ domain_is_in?(ValidEmail2.allow_list)
89
89
  end
90
90
 
91
- def blacklisted?
92
- valid? && domain_is_in?(ValidEmail2.blacklist)
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?(email)
134
- return false if email.nil?
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
- email.each_char.any? { |char| char.bytesize > 1 }
146
+ @resolv_config
137
147
  end
138
148
 
139
149
  def mx_servers
140
- @mx_servers ||= Resolv::DNS.open(@resolv_config) do |dns|
141
- dns.timeouts = @dns_timeout
142
- dns.getresources(address.domain, Resolv::DNS::Resource::IN::MX)
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
- @mx_or_a_servers ||= Resolv::DNS.open(@resolv_config) do |dns|
152
- dns.timeouts = @dns_timeout
153
- (mx_servers.any? && mx_servers) ||
154
- dns.getresources(address.domain, Resolv::DNS::Resource::IN::A)
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[:disposable_with_whitelist]
36
- error(record, attribute) && return if addresses.any? { |address| address.disposable? && !address.whitelisted? }
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[:disposable_domain_with_whitelist]
40
- error(record, attribute) && return if addresses.any? { |address| address.disposable_domain? && !address.whitelisted? }
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[:blacklist]
44
- error(record, attribute) && return if addresses.any?(&:blacklisted?)
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?).map(&:strip)
65
+ email_list.reject(&:empty?)
66
66
  end
67
67
 
68
68
  def error(record, attribute)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal:true
2
2
 
3
3
  module ValidEmail2
4
- VERSION = "5.2.6"
4
+ VERSION = "6.0.0"
5
5
  end
data/lib/valid_email2.rb CHANGED
@@ -3,8 +3,8 @@
3
3
  require "valid_email2/email_validator"
4
4
 
5
5
  module ValidEmail2
6
- BLACKLIST_FILE = "config/blacklisted_email_domains.yml"
7
- WHITELIST_FILE = "config/whitelisted_email_domains.yml"
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 blacklist
16
- @blacklist ||= load_if_exists(BLACKLIST_FILE)
15
+ def deny_list
16
+ @deny_list ||= load_if_exists(DENY_LIST_FILE) || Set.new
17
17
  end
18
18
 
19
- def whitelist
20
- @whitelist ||= load_if_exists(WHITELIST_FILE)
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
- File.exist?(path) ? load_file(path) : Set.new
26
+ load_file(path) if File.exist?(path)
27
27
  end
28
28
 
29
29
  def load_file(path)
@@ -5,7 +5,7 @@ require "yaml"
5
5
  require "json"
6
6
  require "net/http"
7
7
 
8
- whitelisted_emails = %w[
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 - whitelisted_emails
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"))
@@ -1,7 +1,6 @@
1
1
  {
2
2
  "release-type": "ruby",
3
3
  "include-component-in-tag": false,
4
- "last-release-sha": "b327e76f71e764f6d76351425e11d1563e04f188",
5
4
  "packages": {
6
5
  ".": {
7
6
  "release-type": "ruby",
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
@@ -5,6 +5,7 @@ require "valid_email2"
5
5
  require 'rspec-benchmark'
6
6
  RSpec.configure do |config|
7
7
  config.include RSpec::Benchmark::Matchers
8
+ config.default_formatter = 'doc'
8
9
  end
9
10
  RSpec::Benchmark.configure do |config|
10
11
  config.disable_gc = true
@@ -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 TestUserDisallowDisposableWithWhitelist < TestModel
51
- validates :email, 'valid_email_2/email': { disposable_with_whitelist: true }
50
+ class TestUserDisallowDisposableWithAllowList < TestModel
51
+ validates :email, 'valid_email_2/email': { disposable_with_allow_list: true }
52
52
  end
53
53
 
54
- class TestUserDisallowDisposableDomainWithWhitelist < TestModel
55
- validates :email, 'valid_email_2/email': { disposable_domain_with_whitelist: true }
54
+ class TestUserDisallowDisposableDomainWithAllowList < TestModel
55
+ validates :email, 'valid_email_2/email': { disposable_domain_with_allow_list: true }
56
56
  end
57
57
 
58
- class TestUserDisallowBlacklisted < TestModel
59
- validates :email, 'valid_email_2/email': { blacklist: true }
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 whitelisted emails" do
212
- let(:whitelist_domain) { disposable_domain }
213
- let(:whitelist_file_path) { "config/whitelisted_email_domains.yml" }
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 whitelist var or it
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 set_whitelist
227
+ def set_allow_list
218
228
  ValidEmail2.instance_variable_set(
219
- :@whitelist,
220
- ValidEmail2.send(:load_if_exists, ValidEmail2::WHITELIST_FILE)
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(whitelist_file_path, force: true)
226
- set_whitelist
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 whitelist" do
230
- user = TestUserDisallowDisposableWithWhitelist.new(email: "foo@#{whitelist_domain}")
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 whitelist" do
235
- File.open(whitelist_file_path, "w") { |f| f.write [whitelist_domain].to_yaml }
236
- set_whitelist
237
- user = TestUserDisallowDisposableWithWhitelist.new(email: "foo@#{whitelist_domain}")
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 whitelist" do
242
- user = TestUserDisallowDisposableDomainWithWhitelist.new(email: "foo@#{whitelist_domain}")
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 whitelist" do
247
- File.open(whitelist_file_path, "w") { |f| f.write [whitelist_domain].to_yaml }
248
- set_whitelist
249
- user = TestUserDisallowDisposableDomainWithWhitelist.new(email: "foo@#{whitelist_domain}")
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 blacklist validation" do
256
- it "is valid if the domain is not blacklisted" do
257
- user = TestUserDisallowBlacklisted.new(email: "foo@gmail.com")
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 blacklisted" do
262
- user = TestUserDisallowBlacklisted.new(email: "foo@blacklisted-test.com")
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 blacklist}
12
- spec.summary = %q{ActiveModel validation for email. Including MX lookup and disposable email blacklist}
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.9.3"
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", ">= 3.2"
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: 5.2.6
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-08-10 00:00:00.000000000 Z
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: '3.2'
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: '3.2'
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 blacklist
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/blacklisted_email_domains.yml
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.9.3
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
- blacklist
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