From 511c3ba5575d223dbdb3959d88e5fd15993317b3 Mon Sep 17 00:00:00 2001 From: Scott Goci Date: Wed, 21 Aug 2024 16:28:37 -0400 Subject: [PATCH 1/2] 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 bfcd4716..5b52ef32 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 00000000..92fd614b --- /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 deny_list + @deny_list ||= Set.new + end + + def deny_list=(set) + return unless set.is_a?(Set) + + @deny_list = set + end + + def deny_list_active_record_query + @deny_list_active_record_query ||= default_active_record_query + end + + def deny_list_active_record_query=(query_hash) + return unless valid_query_hash?(query_hash) + + @deny_list_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 d96e772b..11f846d1 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 3a2a1ec5..46761b5e 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 c1c1a3ab..1f3346e4 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 From 62c770be5607407bfcc7b32d237914c67fbbe959 Mon Sep 17 00:00:00 2001 From: Scott Goci Date: Tue, 3 Sep 2024 18:45:52 -0400 Subject: [PATCH 2/2] Small move to support procs and lambdas with existing DynamicOptionValues class --- lib/valid_email2/address.rb | 2 +- lib/valid_email2/dynamic_option_values.rb | 73 ++++------------------- spec/valid_email2_spec.rb | 38 ++++++------ 3 files changed, 31 insertions(+), 82 deletions(-) diff --git a/lib/valid_email2/address.rb b/lib/valid_email2/address.rb index 5b52ef32..ac9180b5 100644 --- a/lib/valid_email2/address.rb +++ b/lib/valid_email2/address.rb @@ -90,7 +90,7 @@ def allow_listed? def deny_listed? valid? && ( - DynamicOptionValues.domain_is_in?(:deny_list, address) || domain_is_in?(ValidEmail2.blacklist) + DynamicOptionValues.domain_is_in?(:deny_list_function, address) || domain_is_in?(ValidEmail2.deny_list) ) end diff --git a/lib/valid_email2/dynamic_option_values.rb b/lib/valid_email2/dynamic_option_values.rb index 92fd614b..11b149dd 100644 --- a/lib/valid_email2/dynamic_option_values.rb +++ b/lib/valid_email2/dynamic_option_values.rb @@ -3,46 +3,29 @@ module ValidEmail2 class DynamicOptionValues class << self - def deny_list - @deny_list ||= Set.new + def deny_list_function + @deny_list_function ||= ->(_domain) { false } end - def deny_list=(set) - return unless set.is_a?(Set) + def deny_list_function=(lambda_function) + return unless lambda_function.is_a?(Proc) + return unless lambda_function.arity == 1 - @deny_list = set - end - - def deny_list_active_record_query - @deny_list_active_record_query ||= default_active_record_query - end - - def deny_list_active_record_query=(query_hash) - return unless valid_query_hash?(query_hash) - - @deny_list_active_record_query = query_hash + @deny_list_function = lambda_function end def parse_option_for_additional_items(type, value) - return false unless respond_to?("#{type}=") + return false unless respond_to?("#{type}_function=") case value when NilClass return false when TrueClass, FalseClass return value - when Set - send("#{type}=", value) - when Array - send("#{type}=", Set.new(value)) + when Set, Array + self.deny_list_function = ->(domain) { value.include?(domain) } 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) + self.deny_list_function = value else return false end @@ -56,42 +39,8 @@ def domain_is_in?(type, address) 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 } + send(type).call(downcase_domain) end end end diff --git a/spec/valid_email2_spec.rb b/spec/valid_email2_spec.rb index 1f3346e4..6f93f544 100644 --- a/spec/valid_email2_spec.rb +++ b/spec/valid_email2_spec.rb @@ -59,20 +59,20 @@ 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"] } +class TestUserDisallowDenyListedWithDynamicArray < TestModel + validates :email, 'valid_email_2/email': { deny_list: proc { |domain| ["test-dynamic-array.com"].include?(domain) } } end -class TestUserDisallowBlacklistedWithDynamicSet < TestModel - validates :email, 'valid_email_2/email': { blacklist: Set.new(["test-dynamic-set.com"]) } +class TestUserDisallowDenyListedWithDynamicSet < TestModel + validates :email, 'valid_email_2/email': { deny_list: proc { |domain| Set.new(["test-dynamic-set.com"]).include?(domain) } } end -class TestUserDisallowBlacklistedWithDynamicProc < TestModel - validates :email, 'valid_email_2/email': { blacklist: proc { ["test-dynamic-proc.com"] } } +class TestUserDisallowDenyListedWithDynamicLambda < TestModel + validates :email, 'valid_email_2/email': { deny_list: ->(domain) { Set.new(["test-dynamic-lambda.com"]).include?(domain) } } end -class TestUserDisallowBlacklistedWithDynamicHash < TestModel - validates :email, 'valid_email_2/email': { blacklist: { active_record_scope: TestDynamicDomainModel, attribute: :domain } } +class TestUserDisallowDenyListedWithDynamicRailsModel < TestModel + validates :email, 'valid_email_2/email': { deny_list: ->(domain) { TestDynamicDomainModel.exists?(domain: domain) } } end class TestUserMessage < TestModel @@ -279,31 +279,31 @@ def set_allow_list 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") + it "is invalid if the domain is deny-listed via a dynamic array option" do + user = TestUserDisallowDenyListedWithDynamicArray.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") + it "is invalid if the domain is deny-listed via a dynamic set option" do + user = TestUserDisallowDenyListedWithDynamicSet.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") + it "is invalid if the domain is deny-listed via a dynamic proc option" do + user = TestUserDisallowDenyListedWithDynamicLambda.new(email: "foo@test-dynamic-lambda.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" + it "is invalid if the domain is deny-listed via a dynamic rails model" do + invalid_dynamic_domain = "test-dynamic-rails-model.com" TestDynamicDomainModel.domain_attribute_values = [invalid_dynamic_domain] - user = TestUserDisallowBlacklistedWithDynamicHash.new(email: "foo@#{invalid_dynamic_domain}") + user = TestUserDisallowDenyListedWithDynamicRailsModel.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") + TestDynamicDomainModel.domain_attribute_values = ["not-deny-listed.com"] + user = TestUserDisallowDenyListedWithDynamicRailsModel.new(email: "foo@test-dynamic-rails-model.com") expect(user.valid?).to be_truthy end end