Skip to content

Commit 88300ec

Browse files
committed
Add in ability to dynamically load in blacklisted domains
1 parent 93bb40f commit 88300ec

5 files changed

Lines changed: 180 additions & 1 deletion

File tree

lib/valid_email2/address.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,9 @@ def whitelisted?
8989
end
9090

9191
def blacklisted?
92-
valid? && domain_is_in?(ValidEmail2.blacklist)
92+
valid? && (
93+
DynamicOptionValues.domain_is_in?(:blacklist, address) || domain_is_in?(ValidEmail2.blacklist)
94+
)
9395
end
9496

9597
def valid_mx?
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
# frozen_string_literal:true
2+
3+
module ValidEmail2
4+
class DynamicOptionValues
5+
class << self
6+
def blacklist
7+
@blacklist ||= Set.new
8+
end
9+
10+
def blacklist=(set)
11+
return unless set.is_a?(Set)
12+
13+
@blacklist = set
14+
end
15+
16+
def blacklist_active_record_query
17+
@blacklist_active_record_query ||= default_active_record_query
18+
end
19+
20+
def blacklist_active_record_query=(query_hash)
21+
return unless valid_query_hash?(query_hash)
22+
23+
@blacklist_active_record_query = query_hash
24+
end
25+
26+
def parse_option_for_additional_items(type, value)
27+
return false unless respond_to?("#{type}=")
28+
29+
case value
30+
when NilClass
31+
return false
32+
when TrueClass, FalseClass
33+
return value
34+
when Set
35+
send("#{type}=", value)
36+
when Array
37+
send("#{type}=", Set.new(value))
38+
when Proc
39+
result_value = value.call
40+
return parse_option_for_additional_items(type, result_value)
41+
when Hash, HashWithIndifferentAccess
42+
return false unless valid_query_hash?(value)
43+
return false unless respond_to?("#{type}_active_record_query=")
44+
45+
send("#{type}_active_record_query=", value)
46+
else
47+
return false
48+
end
49+
50+
true
51+
end
52+
53+
def domain_is_in?(type, address)
54+
return false unless type.is_a?(Symbol)
55+
return false unless respond_to?(type)
56+
return false unless address.is_a?(Mail::Address)
57+
58+
downcase_domain = address.domain.downcase
59+
type_result = send(type).include?(downcase_domain)
60+
return type_result if type_result
61+
62+
return false unless respond_to?("#{type}_active_record_query")
63+
64+
option_hash = send("#{type}_active_record_query")
65+
return false unless valid_query_hash?(option_hash)
66+
67+
scope = option_hash[:active_record_scope]
68+
attribute = option_hash[:attribute]
69+
scope.exists?(attribute => downcase_domain)
70+
end
71+
72+
private
73+
74+
def valid_query_hash?(query_hash)
75+
valid_class_array = [Hash]
76+
valid_class_array << HashWithIndifferentAccess if defined?(HashWithIndifferentAccess)
77+
return false unless valid_class_array.include?(query_hash.class)
78+
79+
scope = query_hash[:active_record_scope]
80+
unless scope.is_a?(Class) &&
81+
scope.respond_to?(:where) &&
82+
scope.respond_to?(:exists?) &&
83+
scope.respond_to?(:column_names)
84+
return false
85+
end
86+
87+
attribute = query_hash[:attribute]
88+
return false unless attribute.is_a?(Symbol) && scope.column_names.include?(attribute.to_s)
89+
90+
true
91+
end
92+
93+
def default_active_record_query
94+
@default_active_record_query ||= { active_record_scope: nil, attribute: nil }
95+
end
96+
end
97+
end
98+
end

lib/valid_email2/email_validator.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
require "valid_email2/address"
2+
require "valid_email2/dynamic_option_values"
23
require "active_model"
34
require "active_model/validations"
45

@@ -41,6 +42,7 @@ def validate_each(record, attribute, value)
4142
end
4243

4344
if options[:blacklist]
45+
ValidEmail2::DynamicOptionValues.parse_option_for_additional_items(:blacklist, options[:blacklist])
4446
error(record, attribute) && return if addresses.any?(&:blacklisted?)
4547
end
4648

spec/spec_helper.rb

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,36 @@ def read_attribute_for_validation(key)
2121
@attributes[key]
2222
end
2323
end
24+
25+
class TestDynamicDomainModel
26+
def self.where(*); end
27+
28+
def self.column_names
29+
[domain_attribute].compact
30+
end
31+
32+
def self.exists?(hash)
33+
value = hash[self.domain_attribute.to_sym]
34+
return false if value.nil?
35+
36+
existng_array = self.domain_attribute_values
37+
existng_array.include?(value)
38+
end
39+
40+
def self.domain_attribute
41+
@domain_attribute ||= "domain"
42+
end
43+
44+
def self.domain_attribute_values
45+
@domain_attribute_values ||= []
46+
end
47+
48+
def self.domain_attribute=(new_domain_attribute)
49+
@domain_attribute = new_domain_attribute
50+
@domain_attribute_values = domain_attribute_values
51+
end
52+
53+
def self.domain_attribute_values=(new_domain_attribute_values)
54+
@domain_attribute_values = new_domain_attribute_values
55+
end
56+
end

spec/valid_email2_spec.rb

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,22 @@ class TestUserDisallowBlacklisted < TestModel
5959
validates :email, 'valid_email_2/email': { blacklist: true }
6060
end
6161

62+
class TestUserDisallowBlacklistedWithDynamicArray < TestModel
63+
validates :email, 'valid_email_2/email': { blacklist: ["test-dynamic-array.com"] }
64+
end
65+
66+
class TestUserDisallowBlacklistedWithDynamicSet < TestModel
67+
validates :email, 'valid_email_2/email': { blacklist: Set.new(["test-dynamic-set.com"]) }
68+
end
69+
70+
class TestUserDisallowBlacklistedWithDynamicProc < TestModel
71+
validates :email, 'valid_email_2/email': { blacklist: proc { ["test-dynamic-proc.com"] } }
72+
end
73+
74+
class TestUserDisallowBlacklistedWithDynamicHash < TestModel
75+
validates :email, 'valid_email_2/email': { blacklist: { active_record_scope: TestDynamicDomainModel, attribute: :domain } }
76+
end
77+
6278
class TestUserMessage < TestModel
6379
validates :email, 'valid_email_2/email': { message: "custom message" }
6480
end
@@ -262,6 +278,34 @@ def set_whitelist
262278
user = TestUserDisallowBlacklisted.new(email: "foo@blacklisted-test.com")
263279
expect(user.valid?).to be_falsey
264280
end
281+
282+
it "is invalid if the domain is blacklisted via a dynamic array option" do
283+
user = TestUserDisallowBlacklistedWithDynamicArray.new(email: "foo@test-dynamic-array.com")
284+
expect(user.valid?).to be_falsy
285+
end
286+
287+
it "is invalid if the domain is blacklisted via a dynamic set option" do
288+
user = TestUserDisallowBlacklistedWithDynamicSet.new(email: "foo@test-dynamic-set.com")
289+
expect(user.valid?).to be_falsy
290+
end
291+
292+
it "is invalid if the domain is blacklisted via a dynamic proc option" do
293+
user = TestUserDisallowBlacklistedWithDynamicProc.new(email: "foo@test-dynamic-proc.com")
294+
expect(user.valid?).to be_falsy
295+
end
296+
297+
it "is invalid if the domain is blacklisted via a dynamic hash option" do
298+
invalid_dynamic_domain = "test-dynamic-hash.com"
299+
TestDynamicDomainModel.domain_attribute_values = [invalid_dynamic_domain]
300+
user = TestUserDisallowBlacklistedWithDynamicHash.new(email: "foo@#{invalid_dynamic_domain}")
301+
expect(user.valid?).to be_falsy
302+
end
303+
304+
it "is valid if the dynamic domain list does not include the email domain" do
305+
TestDynamicDomainModel.domain_attribute_values = ["not-blacklisted.com"]
306+
user = TestUserDisallowBlacklistedWithDynamicHash.new(email: "foo@test-dynamic-hash.com")
307+
expect(user.valid?).to be_truthy
308+
end
265309
end
266310

267311
describe "with mx validation" do

0 commit comments

Comments
 (0)