diff --git a/lib/valid_email2/address.rb b/lib/valid_email2/address.rb index 63963b4..47ba769 100644 --- a/lib/valid_email2/address.rb +++ b/lib/valid_email2/address.rb @@ -8,13 +8,14 @@ module ValidEmail2 class Address attr_accessor :address - # Cache structure: { domain (String): { records: [], cached_at: Time, ttl: Integer } } - @@mx_servers_cache = {} - @@mx_or_a_servers_cache = {} - PROHIBITED_DOMAIN_CHARACTERS_REGEX = /[+!_\/\s'`]/ DEFAULT_RECIPIENT_DELIMITER = '+' DOT_DELIMITER = '.' + MAX_CACHE_SIZE = 1_000 + + # Cache structure: { domain (String): { records: [], cached_at: Time, ttl: Integer } } + @@mx_servers_cache = {} + @@mx_or_a_servers_cache = {} def self.prohibited_domain_characters_regex @prohibited_domain_characters_regex ||= PROHIBITED_DOMAIN_CHARACTERS_REGEX @@ -141,6 +142,10 @@ def address_contain_emoticons?(email) end def mx_servers + if @@mx_servers_cache.size > MAX_CACHE_SIZE + prune_cache(@@mx_servers_cache) + end + domain = address.domain.downcase if @@mx_servers_cache[domain] @@ -170,6 +175,10 @@ def null_mx? end def mx_or_a_servers + if @@mx_or_a_servers_cache.size > MAX_CACHE_SIZE + prune_cache(@@mx_or_a_servers_cache) + end + domain = address.domain.downcase if @@mx_or_a_servers_cache[domain] @@ -194,5 +203,11 @@ def mx_or_a_servers records end + + def prune_cache(cache) + entries_sorted_by_cached_at_asc = (cache.sort_by { |_domain, data| data[:cached_at] }).flatten + entries_to_remove = entries_sorted_by_cached_at_asc.first(cache.size - MAX_CACHE_SIZE) + entries_to_remove.each { |domain| cache.delete(domain) } + end end end diff --git a/spec/address_spec.rb b/spec/address_spec.rb index a43686c..dbf0aac 100644 --- a/spec/address_spec.rb +++ b/spec/address_spec.rb @@ -97,6 +97,65 @@ expect(Resolv::DNS).to have_received(:open).once end + + it "does not prune the cache when the cache size is less than the max cache size" do + expect(email_instance).not_to receive(:prune_cache) + + email_instance.valid_strict_mx? + end + + it "prunes the cache when the cache size is greater than the max cache size" do + stub_const("#{described_class}::MAX_CACHE_SIZE", 0) + + expect(email_instance).to receive(:prune_cache).with(described_class.class_variable_get(:@@mx_servers_cache)).once + + email_instance.valid_strict_mx? + email_instance.valid_strict_mx? + end + + it "does not call the MX or A servers lookup when there is a cached entry for the domain and the cache size is less than the max cache size" do + stub_const("#{described_class}::MAX_CACHE_SIZE", 1) + described_class.class_variable_set(:@@mx_servers_cache, { email_instance.address.domain => { records: mock_mx_records, cached_at: Time.now, ttl: ttl }}) + + email_instance.valid_strict_mx? + + expect(Resolv::DNS).not_to have_received(:open) + end + + it "calls the MX or A servers lookup when there is a cached entry for the domain but the cache size is greater than the max cache size" do + stub_const("#{described_class}::MAX_CACHE_SIZE", 0) + described_class.class_variable_set(:@@mx_servers_cache, { email_instance.address.domain => { records: mock_mx_records, cached_at: Time.now, ttl: ttl }}) + + email_instance.valid_strict_mx? + + expect(Resolv::DNS).to have_received(:open).once + end + + it "does not prune older entries when the cache size is less than the max size" do + stub_const("#{described_class}::MAX_CACHE_SIZE", 1) + described_class.class_variable_set(:@@mx_servers_cache, { + 'another_domain.com' => { + records: mock_mx_records, cached_at: Time.now - 100, ttl: ttl + } + }) + + email_instance.valid_strict_mx? + + expect(described_class.class_variable_get(:@@mx_servers_cache).keys).to match_array([email_instance.address.domain, 'another_domain.com']) + end + + it "prunes older entries when the cache size is greater than the max size" do + stub_const("#{described_class}::MAX_CACHE_SIZE", 0) + described_class.class_variable_set(:@@mx_servers_cache, { + 'another_domain.com' => { + records: mock_mx_records, cached_at: Time.now - 100, ttl: ttl + } + }) + + email_instance.valid_strict_mx? + + expect(described_class.class_variable_get(:@@mx_servers_cache).keys).to match_array([email_instance.address.domain]) + end end describe "#valid_mx?" do @@ -148,11 +207,70 @@ it "calls the MX or A servers lookup when the time since last lookup is greater than the cached ttl entry" do described_class.class_variable_set(:@@mx_or_a_servers_cache, { email_instance.address.domain => { records: mock_a_records, cached_at: Time.now - ttl, ttl: ttl }}) - + email_instance.valid_mx? expect(Resolv::DNS).to have_received(:open).once end + + it "does not prune the cache when the cache size is less than the max cache size" do + expect(email_instance).not_to receive(:prune_cache) + + email_instance.valid_mx? + end + + it "prunes the cache when the cache size is greater than the max cache size" do + stub_const("#{described_class}::MAX_CACHE_SIZE", 0) + + expect(email_instance).to receive(:prune_cache).with(described_class.class_variable_get(:@@mx_or_a_servers_cache)).once + + email_instance.valid_mx? + email_instance.valid_mx? + end + + it "does not call the MX or A servers lookup when there is a cached entry for the domain and the cache size is less than the max cache size" do + stub_const("#{described_class}::MAX_CACHE_SIZE", 1) + described_class.class_variable_set(:@@mx_or_a_servers_cache, { email_instance.address.domain => { records: mock_a_records, cached_at: Time.now, ttl: ttl }}) + + email_instance.valid_mx? + + expect(Resolv::DNS).not_to have_received(:open) + end + + it "calls the MX or A servers lookup when there is a cached entry for the domain but the cache size is greater than the max cache size" do + stub_const("#{described_class}::MAX_CACHE_SIZE", 0) + described_class.class_variable_set(:@@mx_or_a_servers_cache, { email_instance.address.domain => { records: mock_a_records, cached_at: Time.now, ttl: ttl }}) + + email_instance.valid_mx? + + expect(Resolv::DNS).to have_received(:open).once + end + + it "does not prune older entries when the cache size is less than the max size" do + stub_const("#{described_class}::MAX_CACHE_SIZE", 1) + described_class.class_variable_set(:@@mx_or_a_servers_cache, { + 'another_domain.com' => { + records: mock_a_records, cached_at: Time.now - 100, ttl: ttl + } + }) + + email_instance.valid_mx? + + expect(described_class.class_variable_get(:@@mx_or_a_servers_cache).keys).to match_array([email_instance.address.domain, 'another_domain.com']) + end + + it "prunes older entries when the cache size is greater than the max size" do + stub_const("#{described_class}::MAX_CACHE_SIZE", 0) + described_class.class_variable_set(:@@mx_or_a_servers_cache, { + 'another_domain.com' => { + records: mock_a_records, cached_at: Time.now - 100, ttl: ttl + } + }) + + email_instance.valid_mx? + + expect(described_class.class_variable_get(:@@mx_or_a_servers_cache).keys).to match_array([email_instance.address.domain]) + end end end end