Skip to content

Commit

Permalink
Feature/domain whitelist blacklist (#43)
Browse files Browse the repository at this point in the history
* Error key in lower_snake_case
* Add whitelist, blacklist check
* Update reek config
* Rename Skip to DomainListMatch
* Add Configuration#validation_type_for= argument type validation
* Add attr_accessors Configuration#whitelisted_domains, Configuration#blacklisted_domains
* Update shared examples
* Update core, Validator#run tests
* Add DomainListMatch tests
* Update Gemfile
* Update gem version
* Update readme
  • Loading branch information
bestwebua authored Jun 4, 2019
1 parent 37387d2 commit d507760
Show file tree
Hide file tree
Showing 17 changed files with 206 additions and 49 deletions.
5 changes: 5 additions & 0 deletions .reek.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ detectors:
- Truemail::Validate::Mx#null_mx?
- Truemail::Validate::Mx#a_record
- Truemail::Audit::Base#verifier_domain
- Truemail::Configuration#domain_matcher

ControlParameter:
exclude:
Expand All @@ -42,3 +43,7 @@ detectors:
exclude:
- Truemail::Validate::Smtp#not_includes_user_not_found_errors
- Truemail::GenerateEmailHelper#prepare_user_name

NilCheck:
exclude:
- Truemail::Validator#result_not_changed?
2 changes: 1 addition & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
PATH
remote: .
specs:
truemail (0.2.0)
truemail (1.0.0)

GEM
remote: https://rubygems.org/
Expand Down
26 changes: 22 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ The Truemail gem helps you validate emails by regex pattern, presence of domain

- Configurable validator, validate only what you need
- Zero runtime dependencies
- Has whitelist/blacklist
- Has simple SMTP debugger
- 100% test coverage

Expand Down Expand Up @@ -74,10 +75,18 @@ Truemail.configure do |config|
config.connection_attempts = 3

# Optional parameter. You can predefine which type of validation will be used for domains.
# Also you can skip validation by domain. Available validation types: :regex, :mx, :smtp, :skip
# Also you can skip validation by domain. Available validation types: :regex, :mx, :smtp
# This configuration will be used over current or default validation type parameter
# All of validations for 'somedomain.com' will be processed with mx validation only
config.validation_type_for = { 'somedomain.com' => :mx, 'otherdomain.com' => :skip }
config.validation_type_for = { 'somedomain.com' => :regex, 'otherdomain.com' => :mx }

# Optional parameter. Validation of email which contains whitelisted domain always will
# return true. Other validations will not processed even if it was defined in validation_type_for
config.whitelisted_domains = ['somedomain1.com', 'somedomain2.com']

# Optional parameter. Validation of email which contains whitelisted domain always will
# return false. Other validations will not processed even if it was defined in validation_type_for
config.blacklisted_domains = ['somedomain1.com', 'somedomain2.com']

# Optional parameter. This option will be parse bodies of SMTP errors. It will be helpful
# if SMTP server does not return an exact answer that the email does not exist
Expand All @@ -100,6 +109,8 @@ Truemail.configuration
@response_timeout=1,
@connection_attempts=3,
@validation_type_by_domain={},
@whitelisted_domains=[],
@blacklisted_domains=[],
@verifier_domain="somedomain.com",
@verifier_email="verifier@example.com"
@smtp_safe_check=true>
Expand All @@ -123,6 +134,8 @@ Truemail.configuration
@response_timeout=4,
@connection_attempts=1,
@validation_type_by_domain={},
@whitelisted_domains=[],
@blacklisted_domains=[],
@verifier_domain="somedomain.com",
@verifier_email="verifier@example.com",
@smtp_safe_check=true>
Expand Down Expand Up @@ -281,6 +294,8 @@ Truemail.validate('email@example.com')
@connection_attempts=2,
@smtp_safe_check=false,
@validation_type_by_domain={},
@whitelisted_domains=[],
@blacklisted_domains=[],
@verifier_domain="example.com",
@verifier_email="verifier@example.com">,
@email="email@example.com",
Expand Down Expand Up @@ -335,6 +350,8 @@ Truemail.validate('email@example.com')
@connection_attempts=2,
@smtp_safe_check=true,
@validation_type_by_domain={},
@whitelisted_domains=[],
@blacklisted_domains=[],
@verifier_domain="example.com",
@verifier_email="verifier@example.com">,
@email="email@example.com",
Expand Down Expand Up @@ -373,6 +390,8 @@ Truemail.validate('email@example.com')
@connection_attempts=2,
@smtp_safe_check=true,
@validation_type_by_domain={},
@whitelisted_domains=[],
@blacklisted_domains=[],
@verifier_domain="example.com",
@verifier_email="verifier@example.com">,
@email="email@example.com",
Expand Down Expand Up @@ -447,8 +466,7 @@ end
---
## ToDo

1. Gem compatibility with Ruby 2.3
2. Fail validations logger
Fail validations logger

## Contributing

Expand Down
27 changes: 23 additions & 4 deletions lib/truemail/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ class Configuration
:connection_timeout,
:response_timeout,
:connection_attempts,
:validation_type_by_domain
:validation_type_by_domain,
:whitelisted_domains,
:blacklisted_domains

attr_accessor :smtp_safe_check

Expand All @@ -26,6 +28,8 @@ def initialize
@response_timeout = Truemail::Configuration::DEFAULT_RESPONSE_TIMEOUT
@connection_attempts = Truemail::Configuration::DEFAULT_CONNECTION_ATTEMPTS
@validation_type_by_domain = {}
@whitelisted_domains = []
@blacklisted_domains = []
@smtp_safe_check = false
end

Expand Down Expand Up @@ -59,6 +63,13 @@ def validation_type_for=(settings)
validation_type_by_domain.merge!(settings)
end

%i[whitelisted_domains blacklisted_domains].each do |method|
define_method("#{method}=") do |argument|
raise ArgumentError.new(argument, __method__) unless argument.is_a?(Array) && check_domain_list(argument)
instance_variable_set(:"@#{method}", argument)
end
end

def complete?
!!verifier_email
end
Expand All @@ -74,17 +85,25 @@ def default_verifier_domain
self.verifier_domain ||= verifier_email[Truemail::RegexConstant::REGEX_EMAIL_PATTERN, 3]
end

def domain_matcher
->(domain) { Truemail::RegexConstant::REGEX_DOMAIN_PATTERN.match?(domain.to_s) }
end

def check_domain(domain)
raise Truemail::ArgumentError.new(domain, 'domain') unless
Truemail::RegexConstant::REGEX_DOMAIN_PATTERN.match?(domain.to_s)
raise Truemail::ArgumentError.new(domain, 'domain') unless domain_matcher.call(domain)
end

def check_domain_list(domains)
domains.all?(&domain_matcher)
end

def check_validation_type(validation_type)
raise Truemail::ArgumentError.new(validation_type, 'validation type') unless
Truemail::Validator::VALIDATION_TYPES.include?(validation_type)
Truemail::Validator::VALIDATION_TYPES.include?(validation_type)
end

def validate_validation_type(settings)
raise Truemail::ArgumentError.new(settings, 'hash with settings') unless settings.is_a?(Hash)
settings.each do |domain, validation_type|
check_domain(domain)
check_validation_type(validation_type)
Expand Down
2 changes: 1 addition & 1 deletion lib/truemail/core.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ module Audit
end

module Validate
require 'truemail/validate/skip'
require 'truemail/validate/base'
require 'truemail/validate/domain_list_match'
require 'truemail/validate/regex'
require 'truemail/validate/mx'
require 'truemail/validate/smtp'
Expand Down
2 changes: 1 addition & 1 deletion lib/truemail/validate/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ class Base < Truemail::Worker
private

def add_error(message)
result.errors[self.class.name.split('::').last.downcase.to_sym] = message
result.errors[self.class.name.split('::').last.gsub(/(?<=.)(?=[A-Z])/, '_').downcase.to_sym] = message
end

def mail_servers
Expand Down
30 changes: 30 additions & 0 deletions lib/truemail/validate/domain_list_match.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# frozen_string_literal: true

module Truemail
module Validate
class DomainListMatch < Truemail::Validate::Base
ERROR = 'blacklisted email'

def run
return success(true) if whitelisted_domain?
return unless blacklisted_domain?
success(false)
add_error(Truemail::Validate::DomainListMatch::ERROR)
end

private

def email_domain
@email_domain ||= result.email[Truemail::RegexConstant::REGEX_DOMAIN_FROM_EMAIL, 1]
end

def whitelisted_domain?
configuration.whitelisted_domains.include?(email_domain)
end

def blacklisted_domain?
configuration.blacklisted_domains.include?(email_domain)
end
end
end
end
11 changes: 0 additions & 11 deletions lib/truemail/validate/skip.rb

This file was deleted.

9 changes: 7 additions & 2 deletions lib/truemail/validator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
module Truemail
class Validator
RESULT_ATTRS = %i[success email domain mail_servers errors smtp_debug].freeze
VALIDATION_TYPES = %i[regex mx smtp skip].freeze
VALIDATION_TYPES = %i[regex mx smtp].freeze

Result = Struct.new(*RESULT_ATTRS, keyword_init: true) do
def initialize(errors: {}, mail_servers: [], **args)
Expand All @@ -21,12 +21,17 @@ def initialize(email, with: :smtp)
end

def run
Truemail::Validate.const_get(validation_type.capitalize).check(result)
Truemail::Validate::DomainListMatch.check(result)
Truemail::Validate.const_get(validation_type.capitalize).check(result) if result_not_changed?
self
end

private

def result_not_changed?
result.success.nil?
end

def select_validation_type(email, current_validation_type)
domain = email[Truemail::RegexConstant::REGEX_EMAIL_PATTERN, 3]
Truemail.configuration.validation_type_by_domain[domain] || current_validation_type
Expand Down
2 changes: 1 addition & 1 deletion lib/truemail/version.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# frozen_string_literal: true

module Truemail
VERSION = '0.2.0'
VERSION = '1.0.0'
end
2 changes: 2 additions & 0 deletions spec/support/shared_examples/has_attr_accessor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ module Truemail
connection_timeout
response_timeout
connection_attempts
whitelisted_domains
blacklisted_domains
smtp_safe_check
].each do |attribute|
it "has attr_accessor :#{attribute}" do
Expand Down
2 changes: 2 additions & 0 deletions spec/support/shared_examples/sets_default_configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ module Truemail
expect(configuration_instance.response_timeout).to eq(Truemail::Configuration::DEFAULT_RESPONSE_TIMEOUT)
expect(configuration_instance.connection_attempts).to eq(Truemail::Configuration::DEFAULT_CONNECTION_ATTEMPTS)
expect(configuration_instance.validation_type_by_domain).to eq({})
expect(configuration_instance.whitelisted_domains).to eq([])
expect(configuration_instance.blacklisted_domains).to eq([])
expect(configuration_instance.smtp_safe_check).to be(false)
end
end
Expand Down
51 changes: 50 additions & 1 deletion spec/truemail/configuration_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@
expect(configuration_instance.response_timeout).to eq(2)
expect(configuration_instance.connection_attempts).to eq(2)
expect(configuration_instance.validation_type_by_domain).to eq({})
expect(configuration_instance.whitelisted_domains).to eq([])
expect(configuration_instance.blacklisted_domains).to eq([])
expect(configuration_instance.smtp_safe_check).to be(false)
end

Expand All @@ -51,6 +53,8 @@
.and not_change(configuration_instance, :connection_timeout)
.and not_change(configuration_instance, :response_timeout)
.and not_change(configuration_instance, :validation_type_by_domain)
.and not_change(configuration_instance, :whitelisted_domains)
.and not_change(configuration_instance, :blacklisted_domains)
.and not_change(configuration_instance, :smtp_safe_check)

configuration_instance_expectaions
Expand All @@ -69,6 +73,8 @@
.and not_change(configuration_instance, :connection_timeout)
.and not_change(configuration_instance, :response_timeout)
.and not_change(configuration_instance, :validation_type_by_domain)
.and not_change(configuration_instance, :whitelisted_domains)
.and not_change(configuration_instance, :blacklisted_domains)
.and not_change(configuration_instance, :smtp_safe_check)

configuration_instance_expectaions
Expand All @@ -87,6 +93,8 @@
.and not_change(configuration_instance, :connection_timeout)
.and not_change(configuration_instance, :response_timeout)
.and not_change(configuration_instance, :validation_type_by_domain)
.and not_change(configuration_instance, :whitelisted_domains)
.and not_change(configuration_instance, :blacklisted_domains)
.and not_change(configuration_instance, :smtp_safe_check)

configuration_instance_expectaions
Expand Down Expand Up @@ -230,7 +238,7 @@
describe '#validation_type_for=' do
context 'with valid validation type attributes' do
let(:domains_config) do
(1..4).map { FFaker::Internet.unique.domain_name }.zip(%i[regex mx smtp skip]).to_h
(1..3).map { FFaker::Internet.unique.domain_name }.zip(%i[regex mx smtp]).to_h
end

it 'sets validation type for domain' do
Expand All @@ -240,6 +248,15 @@
end
end

context 'with invalid settings type' do
let(:invalid_argument) { [] }

specify do
expect { configuration_instance.validation_type_for = invalid_argument }
.to raise_error(Truemail::ArgumentError, "#{invalid_argument} is not a valid hash with settings")
end
end

context 'with invalid domain' do
let(:domain) { 'not_valid_domain' }

Expand All @@ -260,6 +277,38 @@
end
end

%i[whitelisted_domains= blacklisted_domains=].each do |domain_list_type|
describe "##{domain_list_type}" do
let(:domains_list) { (1..3).map { FFaker::Internet.unique.domain_name } }

context "with valid #{domain_list_type} parameter type and context" do
it 'sets whitelisted domains list' do
expect { configuration_instance.public_send(domain_list_type, domains_list) }
.to change(configuration_instance, domain_list_type[0...-1].to_sym)
.from([]).to(domains_list)
end
end

context "with invalid #{domain_list_type} parameter type" do
let(:wrong_parameter_type) { 'not_array' }

specify do
expect { configuration_instance.public_send(domain_list_type, wrong_parameter_type) }
.to raise_error(Truemail::ArgumentError, "#{wrong_parameter_type} is not a valid #{domain_list_type}")
end
end

context 'with invalid whitelisted_domains= parameter context' do
let(:wrong_parameter_context) { ['not_domain', 123] }

specify do
expect { configuration_instance.public_send(domain_list_type, wrong_parameter_context) }
.to raise_error(Truemail::ArgumentError, "#{wrong_parameter_context} is not a valid #{domain_list_type}")
end
end
end
end

describe '#smtp_safe_check=' do
it 'sets smtp safe check' do
expect { configuration_instance.smtp_safe_check = true }
Expand Down
2 changes: 1 addition & 1 deletion spec/truemail/core_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -136,8 +136,8 @@ module Truemail

RSpec.describe Truemail::Validate do
describe 'defined constants' do
specify { expect(described_class).to be_const_defined(:Skip) }
specify { expect(described_class).to be_const_defined(:Base) }
specify { expect(described_class).to be_const_defined(:DomainListMatch) }
specify { expect(described_class).to be_const_defined(:Regex) }
specify { expect(described_class).to be_const_defined(:Mx) }
specify { expect(described_class).to be_const_defined(:Smtp) }
Expand Down
Loading

0 comments on commit d507760

Please sign in to comment.