Skip to content

Commit

Permalink
Add in ability to dynamically load in blacklisted domains
Browse files Browse the repository at this point in the history
  • Loading branch information
scouttyg committed Sep 3, 2024
1 parent 714c744 commit 909c3ec
Show file tree
Hide file tree
Showing 5 changed files with 180 additions and 1 deletion.
4 changes: 3 additions & 1 deletion lib/valid_email2/address.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand Down
98 changes: 98 additions & 0 deletions lib/valid_email2/dynamic_option_values.rb
Original file line number Diff line number Diff line change
@@ -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
2 changes: 2 additions & 0 deletions lib/valid_email2/email_validator.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
require "valid_email2/address"
require "valid_email2/dynamic_option_values"
require "active_model"
require "active_model/validations"

Expand Down Expand Up @@ -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

Expand Down
33 changes: 33 additions & 0 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
44 changes: 44 additions & 0 deletions spec/valid_email2_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -262,6 +278,34 @@ def set_allow_list
user = TestUserDisallowDenyListed.new(email: "[email protected]")
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: "[email protected]")
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: "[email protected]")
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: "[email protected]")
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: "[email protected]")
expect(user.valid?).to be_truthy
end
end

describe "with mx validation" do
Expand Down

0 comments on commit 909c3ec

Please sign in to comment.