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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0e044955522eceb44fb1def3eb4da317dc58625072d0c41f509ce50cc5634d4b
4
- data.tar.gz: bc1af776d07f7a8480441d4bbf3f711d48cc7292947b8e004e1831be71e603bc
3
+ metadata.gz: 18dccc719c18b67299952ce4096d4b1c0184810985015dacfc883d7b7dcb021f
4
+ data.tar.gz: c3a7f37eceb1dde232850accbd1fed04ba73cff1a899d4f12762f54d2d88a533
5
5
  SHA512:
6
- metadata.gz: 1974e08b667ae079117faf1415047fccebcb4a870bd1006a240ee15990bcee39ef75999a55ecc33e812c053a0c218afb0a80df7cf8b8379f3e9343583f7de51f
7
- data.tar.gz: '085e865491902d39d25a49cf0834743cfc0280dee0a1b6f9358e265e8a473a0381b9da929edf8848b782e4e95715f8d58af66836f65e600bb511d06b651b2675'
6
+ metadata.gz: b5276142248e94cc178345e6ea069fb1675003bd2aa011a732287b0cae349f0e43c2fe8a855080550286b373b0fec3359658555efb4b4de0cec0d22a2fff6f2e
7
+ data.tar.gz: '087ae98e571ac2eb9acbb0e1230f19e090f577d44e2d0eba75feb7fa2aeafa391848cb8eb86aca95c38e22a99b099ed702206f9bfa7232e6f4356a66ecceb040'
@@ -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.3.0"}
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/deny_list_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
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
@@ -3,15 +3,13 @@
3
3
  require "valid_email2"
4
4
  require "resolv"
5
5
  require "mail"
6
- require_relative "../helpers/deprecation_helper"
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 ||= address_contain_emoticons? @raw_address
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 address_contain_emoticons?(email)
139
- return false if email.nil?
140
+ def address_contain_multibyte_characters?
141
+ return false if @raw_address.nil?
140
142
 
141
- email.each_char.any? { |char| char.bytesize > 1 }
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
- @mx_servers ||= Resolv::DNS.open(@resolv_config) do |dns|
146
- dns.timeouts = @dns_timeout
147
- dns.getresources(address.domain, Resolv::DNS::Resource::IN::MX)
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
- @mx_or_a_servers ||= Resolv::DNS.open(@resolv_config) do |dns|
157
- dns.timeouts = @dns_timeout
158
- (mx_servers.any? && mx_servers) ||
159
- dns.getresources(address.domain, Resolv::DNS::Resource::IN::A)
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[:disposable_with_whitelist]
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[:disposable_domain_with_whitelist]
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[:blacklist]
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?).map(&:strip)
65
+ email_list.reject(&:empty?)
81
66
  end
82
67
 
83
68
  def error(record, attribute)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal:true
2
2
 
3
3
  module ValidEmail2
4
- VERSION = "5.3.0"
4
+ VERSION = "7.0.0"
5
5
  end
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!
@@ -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"))
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
@@ -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
@@ -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.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,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", ">= 3.2"
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: 5.3.0
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-08-31 00:00:00.000000000 Z
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: '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
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.9.3
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