diff --git a/lib/valid_email2/address.rb b/lib/valid_email2/address.rb index bfcd471..ac9180b 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_function, address) || domain_is_in?(ValidEmail2.deny_list) + ) 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..11b149d --- /dev/null +++ b/lib/valid_email2/dynamic_option_values.rb @@ -0,0 +1,47 @@ +# frozen_string_literal:true + +module ValidEmail2 + class DynamicOptionValues + class << self + def deny_list_function + @deny_list_function ||= ->(_domain) { false } + end + + def deny_list_function=(lambda_function) + return unless lambda_function.is_a?(Proc) + return unless lambda_function.arity == 1 + + @deny_list_function = lambda_function + end + + def parse_option_for_additional_items(type, value) + return false unless respond_to?("#{type}_function=") + + case value + when NilClass + return false + when TrueClass, FalseClass + return value + when Set, Array + self.deny_list_function = ->(domain) { value.include?(domain) } + when Proc + self.deny_list_function = 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 + + send(type).call(downcase_domain) + 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..6f93f54 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 TestUserDisallowDenyListedWithDynamicArray < TestModel + validates :email, 'valid_email_2/email': { deny_list: proc { |domain| ["test-dynamic-array.com"].include?(domain) } } +end + +class TestUserDisallowDenyListedWithDynamicSet < TestModel + validates :email, 'valid_email_2/email': { deny_list: proc { |domain| Set.new(["test-dynamic-set.com"]).include?(domain) } } +end + +class TestUserDisallowDenyListedWithDynamicLambda < TestModel + validates :email, 'valid_email_2/email': { deny_list: ->(domain) { Set.new(["test-dynamic-lambda.com"]).include?(domain) } } +end + +class TestUserDisallowDenyListedWithDynamicRailsModel < TestModel + validates :email, 'valid_email_2/email': { deny_list: ->(domain) { TestDynamicDomainModel.exists?(domain: 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 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 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 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 deny-listed via a dynamic rails model" do + invalid_dynamic_domain = "test-dynamic-rails-model.com" + TestDynamicDomainModel.domain_attribute_values = [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-deny-listed.com"] + user = TestUserDisallowDenyListedWithDynamicRailsModel.new(email: "foo@test-dynamic-rails-model.com") + expect(user.valid?).to be_truthy + end end describe "with mx validation" do