From 909c3ec05c08af753e53533546b0f6f34cb22173 Mon Sep 17 00:00:00 2001 From: Scott Goci Date: Wed, 21 Aug 2024 16:28:37 -0400 Subject: [PATCH] Add in ability to dynamically load in blacklisted domains --- lib/valid_email2/address.rb | 4 +- lib/valid_email2/dynamic_option_values.rb | 98 +++++++++++++++++++++++ lib/valid_email2/email_validator.rb | 2 + spec/spec_helper.rb | 33 ++++++++ spec/valid_email2_spec.rb | 44 ++++++++++ 5 files changed, 180 insertions(+), 1 deletion(-) create mode 100644 lib/valid_email2/dynamic_option_values.rb diff --git a/lib/valid_email2/address.rb b/lib/valid_email2/address.rb index bfcd471..5b52ef3 100644 --- a/lib/valid_email2/address.rb +++ b/lib/valid_email2/address.rb @@ -89,7 +89,9 @@ def allow_listed? end def deny_listed? - valid? && domain_is_in?(ValidEmail2.deny_list) + valid? && ( + DynamicOptionValues.domain_is_in?(:deny_list, address) || domain_is_in?(ValidEmail2.blacklist) + ) end def valid_mx? diff --git a/lib/valid_email2/dynamic_option_values.rb b/lib/valid_email2/dynamic_option_values.rb new file mode 100644 index 0000000..2bdb00c --- /dev/null +++ b/lib/valid_email2/dynamic_option_values.rb @@ -0,0 +1,98 @@ +# frozen_string_literal:true + +module ValidEmail2 + class DynamicOptionValues + class << self + def blacklist + @blacklist ||= Set.new + end + + def blacklist=(set) + return unless set.is_a?(Set) + + @blacklist = set + end + + def blacklist_active_record_query + @blacklist_active_record_query ||= default_active_record_query + end + + def blacklist_active_record_query=(query_hash) + return unless valid_query_hash?(query_hash) + + @blacklist_active_record_query = query_hash + end + + def parse_option_for_additional_items(type, value) + return false unless respond_to?("#{type}=") + + case value + when NilClass + return false + when TrueClass, FalseClass + return value + when Set + send("#{type}=", value) + when Array + send("#{type}=", Set.new(value)) + when Proc + result_value = value.call + return parse_option_for_additional_items(type, result_value) + when Hash, HashWithIndifferentAccess + return false unless valid_query_hash?(value) + return false unless respond_to?("#{type}_active_record_query=") + + send("#{type}_active_record_query=", value) + else + return false + end + + true + end + + def domain_is_in?(type, address) + return false unless type.is_a?(Symbol) + return false unless respond_to?(type) + return false unless address.is_a?(Mail::Address) + + downcase_domain = address.domain.downcase + type_result = send(type).include?(downcase_domain) + return type_result if type_result + + return false unless respond_to?("#{type}_active_record_query") + + option_hash = send("#{type}_active_record_query") + return false unless valid_query_hash?(option_hash) + + scope = option_hash[:active_record_scope] + attribute = option_hash[:attribute] + scope.exists?(attribute => downcase_domain) + end + + private + + def valid_query_hash?(query_hash) + valid_class_array = [Hash] + valid_class_array << HashWithIndifferentAccess if defined?(HashWithIndifferentAccess) + return false unless valid_class_array.include?(query_hash.class) + + scope = query_hash[:active_record_scope] + unless scope.is_a?(Class) && + scope.respond_to?(:where) && + scope.respond_to?(:exists?) && + scope.respond_to?(:column_names) + return false + end + + attribute = query_hash[:attribute] + return false unless attribute.is_a?(Symbol) && scope.column_names.include?(attribute.to_s) + + true + end + + def default_active_record_query + @default_active_record_query ||= { active_record_scope: nil, attribute: nil } + end + end + end +end diff --git a/lib/valid_email2/email_validator.rb b/lib/valid_email2/email_validator.rb index d96e772..11f846d 100644 --- a/lib/valid_email2/email_validator.rb +++ b/lib/valid_email2/email_validator.rb @@ -1,4 +1,5 @@ require "valid_email2/address" +require "valid_email2/dynamic_option_values" require "active_model" require "active_model/validations" @@ -41,6 +42,7 @@ def validate_each(record, attribute, value) end if options[:deny_list] + ValidEmail2::DynamicOptionValues.parse_option_for_additional_items(:deny_list, options[:deny_list]) error(record, attribute) && return if addresses.any?(&:deny_listed?) end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 3a2a1ec..46761b5 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -21,3 +21,36 @@ def read_attribute_for_validation(key) @attributes[key] end end + +class TestDynamicDomainModel + def self.where(*); end + + def self.column_names + [domain_attribute].compact + end + + def self.exists?(hash) + value = hash[self.domain_attribute.to_sym] + return false if value.nil? + + existng_array = self.domain_attribute_values + existng_array.include?(value) + end + + def self.domain_attribute + @domain_attribute ||= "domain" + end + + def self.domain_attribute_values + @domain_attribute_values ||= [] + end + + def self.domain_attribute=(new_domain_attribute) + @domain_attribute = new_domain_attribute + @domain_attribute_values = domain_attribute_values + end + + def self.domain_attribute_values=(new_domain_attribute_values) + @domain_attribute_values = new_domain_attribute_values + end +end diff --git a/spec/valid_email2_spec.rb b/spec/valid_email2_spec.rb index c1c1a3a..1f3346e 100644 --- a/spec/valid_email2_spec.rb +++ b/spec/valid_email2_spec.rb @@ -59,6 +59,22 @@ class TestUserDisallowDenyListed < TestModel validates :email, 'valid_email_2/email': { deny_list: true } end +class TestUserDisallowBlacklistedWithDynamicArray < TestModel + validates :email, 'valid_email_2/email': { blacklist: ["test-dynamic-array.com"] } +end + +class TestUserDisallowBlacklistedWithDynamicSet < TestModel + validates :email, 'valid_email_2/email': { blacklist: Set.new(["test-dynamic-set.com"]) } +end + +class TestUserDisallowBlacklistedWithDynamicProc < TestModel + validates :email, 'valid_email_2/email': { blacklist: proc { ["test-dynamic-proc.com"] } } +end + +class TestUserDisallowBlacklistedWithDynamicHash < TestModel + validates :email, 'valid_email_2/email': { blacklist: { active_record_scope: TestDynamicDomainModel, attribute: :domain } } +end + class TestUserMessage < TestModel validates :email, 'valid_email_2/email': { message: "custom message" } end @@ -262,6 +278,34 @@ def set_allow_list user = TestUserDisallowDenyListed.new(email: "foo@deny-listed-test.com") expect(user.valid?).to be_falsey end + + it "is invalid if the domain is blacklisted via a dynamic array option" do + user = TestUserDisallowBlacklistedWithDynamicArray.new(email: "foo@test-dynamic-array.com") + expect(user.valid?).to be_falsy + end + + it "is invalid if the domain is blacklisted via a dynamic set option" do + user = TestUserDisallowBlacklistedWithDynamicSet.new(email: "foo@test-dynamic-set.com") + expect(user.valid?).to be_falsy + end + + it "is invalid if the domain is blacklisted via a dynamic proc option" do + user = TestUserDisallowBlacklistedWithDynamicProc.new(email: "foo@test-dynamic-proc.com") + expect(user.valid?).to be_falsy + end + + it "is invalid if the domain is blacklisted via a dynamic hash option" do + invalid_dynamic_domain = "test-dynamic-hash.com" + TestDynamicDomainModel.domain_attribute_values = [invalid_dynamic_domain] + user = TestUserDisallowBlacklistedWithDynamicHash.new(email: "foo@#{invalid_dynamic_domain}") + expect(user.valid?).to be_falsy + end + + it "is valid if the dynamic domain list does not include the email domain" do + TestDynamicDomainModel.domain_attribute_values = ["not-blacklisted.com"] + user = TestUserDisallowBlacklistedWithDynamicHash.new(email: "foo@test-dynamic-hash.com") + expect(user.valid?).to be_truthy + end end describe "with mx validation" do