From 76d3e216d0d05484fc793d89c89e64c0226e218f Mon Sep 17 00:00:00 2001 From: Vladislav Trotsenko Date: Wed, 28 Apr 2021 21:02:51 +0300 Subject: [PATCH] Feature/MX blacklist validation layer (#147) * Added Truemail::Validate::MxBlacklist, tests * Updated Truemail::Core, tests * Updated Truemail::Configuration, tests * Updated Truemail::Validator * Updated Truemail::Validate::Smtp, tests * Updated Truemail::Log::Serializer::Base, dependent tests * Updated Truemail::Log::Serializer::ValidatorText, tests * Updated gem development dependencies * Updated gem documentation, changelog, version --- .codeclimate.yml | 2 +- .reek.yml | 17 +-- .rubocop.yml | 3 + CHANGELOG.md | 29 +++++ Gemfile.lock | 20 ++-- README.md | 106 ++++++++++++++++-- lib/truemail/configuration.rb | 43 ++++--- lib/truemail/core.rb | 5 +- lib/truemail/log/serializer/base.rb | 13 ++- lib/truemail/log/serializer/validator_text.rb | 6 +- lib/truemail/validate/mx_blacklist.rb | 22 ++++ lib/truemail/validate/smtp.rb | 2 +- lib/truemail/validator.rb | 8 +- lib/truemail/version.rb | 2 +- spec/support/schemas/auditor.json | 15 +++ spec/support/schemas/validator.json | 15 +++ spec/truemail/configuration_spec.rb | 44 +++++++- spec/truemail/core_spec.rb | 19 ++++ .../log/serializer/validator_text_spec.rb | 33 +++++- .../validate/domain_list_match_spec.rb | 18 +-- spec/truemail/validate/mx_blacklist_spec.rb | 60 ++++++++++ spec/truemail/validate/smtp_spec.rb | 4 +- truemail.gemspec | 10 +- 23 files changed, 413 insertions(+), 83 deletions(-) create mode 100644 lib/truemail/validate/mx_blacklist.rb create mode 100644 spec/truemail/validate/mx_blacklist_spec.rb diff --git a/.codeclimate.yml b/.codeclimate.yml index 3194faa..1b4cacf 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -7,7 +7,7 @@ checks: plugins: rubocop: enabled: true - channel: rubocop-1-12 + channel: rubocop-1-13 reek: enabled: true diff --git a/.reek.yml b/.reek.yml index 675aa8a..5e5e3c8 100644 --- a/.reek.yml +++ b/.reek.yml @@ -34,18 +34,19 @@ detectors: UtilityFunction: exclude: - - Truemail::Validate::Smtp::Request#compose_from - - Truemail::Validator#select_validation_type - - Truemail::Validate::Base#configuration - - Truemail::Validate::Mx#null_mx? - - Truemail::Validate::Mx#a_record - Truemail::Audit::Base#verifier_domain - - Truemail::Configuration#domain_matcher - Truemail::Configuration#logger_options + - Truemail::Configuration#match_regex? + - Truemail::Configuration#regex_by_method + - Truemail::Dns::Worker#nameserver_port - Truemail::Log::Serializer::Base#errors - Truemail::Log::Serializer::ValidatorBase#replace_invalid_chars - - Truemail::Dns::Worker#nameserver_port - - Truemail::Configuration#check_dns_settings + - Truemail::Validator#select_validation_type + - Truemail::Validator#constantize + - Truemail::Validate::Base#configuration + - Truemail::Validate::Mx#null_mx? + - Truemail::Validate::Mx#a_record + - Truemail::Validate::Smtp::Request#compose_from ControlParameter: exclude: diff --git a/.rubocop.yml b/.rubocop.yml index 236f374..d3f0f6d 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -356,6 +356,9 @@ Performance/RedundantEqualityComparisonBlock: Performance/RedundantSplitRegexpArgument: Enabled: true +Performance/MapCompact: + Enabled: true + RSpec/ExampleLength: Enabled: false diff --git a/CHANGELOG.md b/CHANGELOG.md index 5aeef88..81c0747 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,35 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [2.4.0] - 2021.04.28 + +### Added + +- Implemented `MxBlacklist` validation. This layer provides checking mail servers with predefined blacklisted IP addresses list and can be used as a part of DEA ([disposable email address](https://en.wikipedia.org/wiki/Disposable_email_address)) validations. + +```ruby +Truemail.configure do |config| + # Optional parameter. With this option Truemail will filter out unwanted mx servers via + # predefined list of ip addresses. It can be used as a part of DEA (disposable email + # address) validations. It is equal to empty array by default. + config.blacklisted_mx_ip_addresses = ['1.1.1.1', '2.2.2.2'] +end + +``` + +- Added `Truemail::Validate::MxBlacklist`, tests + +### Changed + +- Updated `Truemail::Core`, tests +- Updated `Truemail::Configuration`, tests +- Updated `Truemail::Validator` +- Updated `Truemail::Validate::Smtp`, tests +- Updated `Truemail::Log::Serializer::Base`, dependent tests +- Updated `Truemail::Log::Serializer::ValidatorText`, tests +- Updated gem development dependencies +- Updated gem documentation, changelog, version + ## [2.3.4] - 2021.04.16 ### Fixed diff --git a/Gemfile.lock b/Gemfile.lock index af8e19f..38c05c8 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - truemail (2.3.4) + truemail (2.4.0) simpleidn (~> 0.2.1) GEM @@ -54,7 +54,7 @@ GEM public_suffix (4.0.6) rainbow (3.0.0) rake (13.0.3) - reek (6.0.3) + reek (6.0.4) kwalify (~> 0.7.0) parser (~> 3.0.0) psych (~> 3.1) @@ -74,7 +74,7 @@ GEM diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.10.0) rspec-support (3.10.2) - rubocop (1.12.1) + rubocop (1.13.0) parallel (~> 1.10) parser (>= 3.0.0.0) rainbow (>= 2.2.2, < 4.0) @@ -85,10 +85,10 @@ GEM unicode-display_width (>= 1.4.0, < 3.0) rubocop-ast (1.4.1) parser (>= 2.7.1.5) - rubocop-performance (1.10.2) - rubocop (>= 0.90.0, < 2.0) + rubocop-performance (1.11.0) + rubocop (>= 1.7.0, < 2.0) rubocop-ast (>= 0.4.0) - rubocop-rspec (2.2.0) + rubocop-rspec (2.3.0) rubocop (~> 1.0) rubocop-ast (>= 1.1.0) ruby-progressbar (1.11.0) @@ -129,11 +129,11 @@ DEPENDENCIES overcommit (~> 0.57.0) pry-byebug (~> 3.9) rake (~> 13.0, >= 13.0.3) - reek (~> 6.0, >= 6.0.3) + reek (~> 6.0, >= 6.0.4) rspec (~> 3.10) - rubocop (~> 1.12, >= 1.12.1) - rubocop-performance (~> 1.10, >= 1.10.2) - rubocop-rspec (~> 2.2) + rubocop (~> 1.13) + rubocop-performance (~> 1.11) + rubocop-rspec (~> 2.3) simplecov (~> 0.17.1) truemail! truemail-rspec (~> 0.4) diff --git a/README.md b/README.md index 8ba7b93..fc5c12b 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,7 @@ Configurable framework agnostic plain Ruby email validator. Verify email via Reg - [DNS (MX) validation](#mx-validation) - [RFC MX lookup flow](#rfc-mx-lookup-flow) - [Not RFC MX lookup flow](#not-rfc-mx-lookup-flow) + - [MX blacklist validation](#mx-blacklist-validation) - [SMTP validation](#smtp-validation) - [SMTP fail fast enabled](#smtp-fail-fast-enabled) - [SMTP safe check disabled](#smtp-safe-check-disabled) @@ -132,6 +133,7 @@ You can use global gem configuration or custom independent configuration. Availa - whitelisted domains - whitelist validation - blacklisted domains +- blacklisted mx ip-addresses - custom DNS gateway(s) - RFC MX lookup flow - SMTP fail fast @@ -200,7 +202,12 @@ Truemail.configure do |config| # Optional parameter. Validation of email which contains blacklisted domain always will # return false. Other validations will not processed even if it was defined in validation_type_for # It is equal to empty array by default. - config.blacklisted_domains = ['somedomain1.com', 'somedomain2.com'] + config.blacklisted_domains = ['somedomain3.com', 'somedomain4.com'] + + # Optional parameter. With this option Truemail will filter out unwanted mx servers via + # predefined list of ip addresses. It can be used as a part of DEA (disposable email + # address) validations. It is equal to empty array by default. + config.blacklisted_mx_ip_addresses = ['1.1.1.1', '2.2.2.2'] # Optional parameter. This option will provide to use custom DNS gateway when Truemail # interacts with DNS. Valid port numbers are in the range 1-65535. If you won't specify @@ -245,11 +252,13 @@ Truemail.configuration @smtp_error_body_pattern=/regex_pattern/, @response_timeout=1, @connection_attempts=3, - @validation_type_by_domain={}, - @whitelisted_domains=[], + @default_validation_type=:mx, + @validation_type_by_domain={"somedomain.com" => :regex, "otherdomain.com" => :mx}, + @whitelisted_domains=["somedomain1.com", "somedomain2.com"], @whitelist_validation=true, - @blacklisted_domains=[], - @dns=[], + @blacklisted_domains=["somedomain3.com", "somedomain4.com"], + @blacklisted_mx_ip_addresses=["1.1.1.1", "2.2.2.2"], + @dns=["10.0.0.1", "10.0.0.2:54"], @verifier_domain="somedomain.com", @verifier_email="verifier@example.com", @not_rfc_mx_lookup_flow=true, @@ -276,11 +285,13 @@ Truemail.configuration @smtp_error_body_pattern=/regex_pattern/, @response_timeout=4, @connection_attempts=1, - @validation_type_by_domain={}, - @whitelisted_domains=[], + @default_validation_type=:mx, + @validation_type_by_domain={"somedomain.com" => :regex, "otherdomain.com" => :mx}, + @whitelisted_domains=["somedomain1.com", "somedomain2.com"], @whitelist_validation=true, - @blacklisted_domains=[], - @dns=[], + @blacklisted_domains=["somedomain3.com", "somedomain4.com"], + @blacklisted_mx_ip_addresses=["1.1.1.1", "2.2.2.2"], + @dns=["10.0.0.1", "10.0.0.2:54"], @verifier_domain="somedomain.com", @verifier_email="verifier@example.com", @not_rfc_mx_lookup_flow=true, @@ -361,6 +372,7 @@ Truemail.validate('email@white-domain.com') smtp_debug=nil>, configuration=# ``` +#### MX blacklist validation + +MX blacklist validation is the third validation level. This layer provides checking extracted mail server(s) IP address from MX validation with predefined blacklisted IP addresses list. It can be used as a part of DEA ([disposable email address](https://en.wikipedia.org/wiki/Disposable_email_address)) validations. + +```code +[Whitelist/Blacklist] -> [Regex validation] -> [MX validation] -> [MX blacklist validation] +``` + +Example of usage: + +```ruby +require 'truemail' + +Truemail.configure do |config| + config.verifier_email = 'verifier@example.com' + config.blacklisted_mx_ip_addresses = ['127.0.1.2'] +end + +Truemail.validate('email@example.com', with: :mx_blacklist) + +=> #"blacklisted mx server ip address"}, + smtp_debug=nil, + configuration= + #>, + @validation_type=:mx_blacklist> +``` + #### SMTP validation -SMTP validation is a final, third validation level. This type of validation tries to check real existence of email account on a current email server. This validation runs a chain of previous validations and if they're complete successfully then runs itself. +SMTP validation is a final, fourth validation level. This type of validation tries to check real existence of email account on a current email server. This validation runs a chain of previous validations and if they're complete successfully then runs itself. ```code -[Whitelist/Blacklist] -> [Regex validation] -> [MX validation] -> [SMTP validation] +[Whitelist/Blacklist] -> [Regex validation] -> [MX validation] -> [MX blacklist validation] -> [SMTP validation] ``` If total count of MX servers is equal to one, `Truemail::Smtp` validator will use value from `Truemail.configuration.connection_attempts` as connection attempts. By default it's equal `2`. @@ -787,6 +858,7 @@ Truemail.validate('email@example.com') configuration= #(domain) { Truemail::RegexConstant::REGEX_DOMAIN_PATTERN.match?(domain.to_s) } + def regex_by_method(method) + return Truemail::RegexConstant::REGEX_IP_ADDRESS_PATTERN if method.eql?(:blacklisted_mx_ip_addresses) + return Truemail::RegexConstant::REGEX_DNS_SERVER_ADDRESS_PATTERN if method.eql?(:dns) + Truemail::RegexConstant::REGEX_DOMAIN_PATTERN end - def check_domain(domain) - raise_unless(domain, 'domain', domain_matcher.call(domain)) - end - - def check_domain_list(domains) - domains.all?(&domain_matcher) + def items_match_regex?(items, regex_pattern) + items.all? { |item| match_regex?(regex_pattern, item) } end def check_validation_type(validation_type) @@ -143,15 +142,11 @@ def check_validation_type(validation_type) def validate_validation_type(settings) raise_unless(settings, 'hash with settings', settings.is_a?(::Hash)) settings.each do |domain, validation_type| - check_domain(domain) + raise_unless(domain, 'domain', match_regex?(Truemail::RegexConstant::REGEX_DOMAIN_PATTERN, domain)) check_validation_type(validation_type) end end - def check_dns_settings(dns_servers) - dns_servers.all? { |dns_server| Truemail::RegexConstant::REGEX_DNS_SERVER_ADDRESS_PATTERN.match?(dns_server.to_s) } - end - def logger_options(current_options) Truemail::Configuration::DEFAULT_LOGGER_OPTIONS.merge(current_options).values end diff --git a/lib/truemail/core.rb b/lib/truemail/core.rb index a8725d4..6b1519b 100644 --- a/lib/truemail/core.rb +++ b/lib/truemail/core.rb @@ -24,8 +24,10 @@ module RegexConstant REGEX_DOMAIN_PATTERN = /(?=\A.{4,255}\z)(\A#{REGEX_DOMAIN}\z)/.freeze REGEX_DOMAIN_FROM_EMAIL = /\A.+@(.+)\z/.freeze REGEX_SMTP_ERROR_BODY_PATTERN = /(?=.*550)(?=.*(user|account|customer|mailbox)).*/i.freeze + REGEX_IP_ADDRESS = /((1\d|[1-9]|2[0-4])?\d|25[0-5])(\.\g<1>){3}/.freeze + REGEX_IP_ADDRESS_PATTERN = /\A#{REGEX_IP_ADDRESS}\z/.freeze REGEX_PORT_NUMBER = /6553[0-5]|655[0-2]\d|65[0-4](\d){2}|6[0-4](\d){3}|[1-5](\d){4}|[1-9](\d){0,3}/.freeze - REGEX_DNS_SERVER_ADDRESS_PATTERN = /\A((1\d|[1-9]|2[0-4])?\d|25[0-5])(\.\g<1>){3}(:#{REGEX_PORT_NUMBER})?\z/.freeze + REGEX_DNS_SERVER_ADDRESS_PATTERN = /\A#{REGEX_IP_ADDRESS}(:#{REGEX_PORT_NUMBER})?\z/.freeze end module Dns @@ -46,6 +48,7 @@ module Validate require_relative '../truemail/validate/domain_list_match' require_relative '../truemail/validate/regex' require_relative '../truemail/validate/mx' + require_relative '../truemail/validate/mx_blacklist' require_relative '../truemail/validate/smtp' require_relative '../truemail/validate/smtp/response' require_relative '../truemail/validate/smtp/request' diff --git a/lib/truemail/log/serializer/base.rb b/lib/truemail/log/serializer/base.rb index 998c208..89b1267 100644 --- a/lib/truemail/log/serializer/base.rb +++ b/lib/truemail/log/serializer/base.rb @@ -6,6 +6,14 @@ module Serializer class Base require 'json' + CONFIGURATION_ARRAY_ATTRS = %i[ + validation_type_by_domain + whitelisted_domains + blacklisted_domains + blacklisted_mx_ip_addresses + dns + ].freeze + CONFIGURATION_REGEX_ATTRS = %i[email_pattern smtp_error_body_pattern].freeze DEFAULT_GEM_VALUE = 'default gem value' def self.call(executor_instance) @@ -30,7 +38,7 @@ def errors(executor_result_target) alias warnings errors - %i[validation_type_by_domain whitelisted_domains blacklisted_domains dns].each do |method| + Truemail::Log::Serializer::Base::CONFIGURATION_ARRAY_ATTRS.each do |method| define_method(method) do value = executor_configuration.public_send(method) return if value.empty? @@ -38,7 +46,7 @@ def errors(executor_result_target) end end - %i[email_pattern smtp_error_body_pattern].each do |method| + Truemail::Log::Serializer::Base::CONFIGURATION_REGEX_ATTRS.each do |method| define_method(method) do value = executor_configuration.public_send(method) default_pattern = Truemail::RegexConstant.const_get( @@ -55,6 +63,7 @@ def configuration whitelist_validation: executor_configuration.whitelist_validation, whitelisted_domains: whitelisted_domains, blacklisted_domains: blacklisted_domains, + blacklisted_mx_ip_addresses: blacklisted_mx_ip_addresses, dns: dns, not_rfc_mx_lookup_flow: executor_configuration.not_rfc_mx_lookup_flow, smtp_fail_fast: executor_configuration.smtp_fail_fast, diff --git a/lib/truemail/log/serializer/validator_text.rb b/lib/truemail/log/serializer/validator_text.rb index a366f88..755f548 100644 --- a/lib/truemail/log/serializer/validator_text.rb +++ b/lib/truemail/log/serializer/validator_text.rb @@ -19,9 +19,9 @@ def serialize def data_composer(enumerable_object) enumerable_object.inject([]) do |formatted_data, (key, value)| data = - case - when value.is_a?(::Hash) then "\n#{printer(value)}" - when value.is_a?(::Array) then value.join(', ') + case value + when ::Hash then "\n#{printer(value)}" + when ::Array then value.join(', ') else value end formatted_data << "#{key.to_s.tr('_', ' ')}: #{data}".chomp << "\n" diff --git a/lib/truemail/validate/mx_blacklist.rb b/lib/truemail/validate/mx_blacklist.rb new file mode 100644 index 0000000..9d2da6c --- /dev/null +++ b/lib/truemail/validate/mx_blacklist.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Truemail + module Validate + class MxBlacklist < Truemail::Validate::Base + ERROR = 'blacklisted mx server ip address' + + def run + return false unless Truemail::Validate::Mx.check(result) + return true if success(mail_servers.none?(&blacklisted_ip?)) + add_error(Truemail::Validate::MxBlacklist::ERROR) + false + end + + private + + def blacklisted_ip? + ->(mail_server) { configuration.blacklisted_mx_ip_addresses.include?(mail_server) } + end + end + end +end diff --git a/lib/truemail/validate/smtp.rb b/lib/truemail/validate/smtp.rb index f4f8625..fbb596a 100644 --- a/lib/truemail/validate/smtp.rb +++ b/lib/truemail/validate/smtp.rb @@ -13,7 +13,7 @@ def initialize(result) end def run - return false unless Truemail::Validate::Mx.check(result) + return false unless Truemail::Validate::MxBlacklist.check(result) establish_smtp_connection return true if success(success_response?) result.smtp_debug = smtp_results diff --git a/lib/truemail/validator.rb b/lib/truemail/validator.rb index 7276583..b9bdf59 100644 --- a/lib/truemail/validator.rb +++ b/lib/truemail/validator.rb @@ -3,7 +3,7 @@ module Truemail class Validator < Truemail::Executor RESULT_ATTRS = %i[success email domain mail_servers errors smtp_debug configuration].freeze - VALIDATION_TYPES = %i[regex mx smtp].freeze + VALIDATION_TYPES = %i[regex mx mx_blacklist smtp].freeze Result = ::Struct.new(*RESULT_ATTRS, keyword_init: true) do def initialize(mail_servers: [], errors: {}, **args) @@ -27,7 +27,7 @@ def initialize(email, configuration:, with: nil) def run Truemail::Validate::DomainListMatch.check(result) - result_not_changed? ? Truemail::Validate.const_get(validation_type.capitalize).check(result) : update_validation_type + result_not_changed? ? Truemail::Validate.const_get(constantize(validation_type)).check(result) : update_validation_type logger&.push(self) self end @@ -43,6 +43,10 @@ def select_validation_type(email, current_validation_type) result.configuration.validation_type_by_domain[domain] || current_validation_type end + def constantize(symbol) + symbol.capitalize.to_s.gsub(/_[a-z]/, &:upcase).tr('_', '').to_sym + end + def update_validation_type @validation_type = result.success ? :whitelist : :blacklist end diff --git a/lib/truemail/version.rb b/lib/truemail/version.rb index 65cccdb..acc8dd5 100644 --- a/lib/truemail/version.rb +++ b/lib/truemail/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module Truemail - VERSION = '2.3.4' + VERSION = '2.4.0' end diff --git a/spec/support/schemas/auditor.json b/spec/support/schemas/auditor.json index 8b9c3fa..99904c8 100644 --- a/spec/support/schemas/auditor.json +++ b/spec/support/schemas/auditor.json @@ -17,6 +17,7 @@ "whitelist_validation": false, "whitelisted_domains": null, "blacklisted_domains": null, + "blacklisted_mx_ip_addresses": null, "dns": null, "not_rfc_mx_lookup_flow": false, "smtp_fail_fast": false, @@ -109,6 +110,7 @@ "whitelist_validation", "whitelisted_domains", "blacklisted_domains", + "blacklisted_mx_ip_addresses", "dns", "not_rfc_mx_lookup_flow", "smtp_fail_fast", @@ -157,6 +159,19 @@ null ] }, + "blacklisted_mx_ip_addresses": { + "$id": "#/properties/configuration/properties/blacklisted_mx_ip_addresses", + "type": [ + "array", + "null" + ], + "title": "The blacklisted mx ip addresses schema", + "description": "An explanation about the purpose of this instance.", + "default": null, + "examples": [ + null + ] + }, "dns": { "$id": "#/properties/configuration/properties/dns", "type": "null", diff --git a/spec/support/schemas/validator.json b/spec/support/schemas/validator.json index 124a8ae..7732d87 100644 --- a/spec/support/schemas/validator.json +++ b/spec/support/schemas/validator.json @@ -236,6 +236,7 @@ { "whitelist_validation": false, "blacklisted_domains": null, + "blacklisted_mx_ip_addresses": null, "validation_type_by_domain": null, "smtp_error_body_pattern": "default gem value", "smtp_safe_check": false, @@ -251,6 +252,7 @@ "whitelist_validation", "whitelisted_domains", "blacklisted_domains", + "blacklisted_mx_ip_addresses", "dns", "not_rfc_mx_lookup_flow", "smtp_fail_fast", @@ -309,6 +311,19 @@ null ] }, + "blacklisted_mx_ip_addresses": { + "$id": "#/properties/configuration/properties/blacklisted_mx_ip_addresses", + "type": [ + "array", + "null" + ], + "title": "The blacklisted mx ip addresses schema", + "description": "An explanation about the purpose of this instance.", + "default": null, + "examples": [ + null + ] + }, "dns": { "$id": "#/properties/configuration/properties/dns", "type": [ diff --git a/spec/truemail/configuration_spec.rb b/spec/truemail/configuration_spec.rb index 10e5f57..2ceec69 100644 --- a/spec/truemail/configuration_spec.rb +++ b/spec/truemail/configuration_spec.rb @@ -42,7 +42,9 @@ :response_timeout, :connection_attempts, :whitelisted_domains, - :blacklisted_domains + :blacklisted_domains, + :blacklisted_mx_ip_addresses, + :dns ) end end @@ -61,6 +63,7 @@ whitelisted_domains whitelist_validation blacklisted_domains + blacklisted_mx_ip_addresses dns not_rfc_mx_lookup_flow smtp_fail_fast @@ -98,6 +101,7 @@ expect(configuration_instance.whitelisted_domains).to eq([]) expect(configuration_instance.whitelist_validation).to eq(false) expect(configuration_instance.blacklisted_domains).to eq([]) + expect(configuration_instance.blacklisted_mx_ip_addresses).to eq([]) expect(configuration_instance.dns).to eq([]) expect(configuration_instance.not_rfc_mx_lookup_flow).to be(false) expect(configuration_instance.smtp_fail_fast).to be(false) @@ -121,6 +125,7 @@ expect(configuration_instance.whitelisted_domains).to eq([]) expect(configuration_instance.whitelist_validation).to eq(false) expect(configuration_instance.blacklisted_domains).to eq([]) + expect(configuration_instance.blacklisted_mx_ip_addresses).to eq([]) expect(configuration_instance.dns).to eq([]) expect(configuration_instance.not_rfc_mx_lookup_flow).to be(false) expect(configuration_instance.smtp_fail_fast).to be(false) @@ -145,6 +150,7 @@ .and not_change(configuration_instance, :whitelisted_domains) .and not_change(configuration_instance, :whitelist_validation) .and not_change(configuration_instance, :blacklisted_domains) + .and not_change(configuration_instance, :blacklisted_mx_ip_addresses) .and not_change(configuration_instance, :dns) .and not_change(configuration_instance, :not_rfc_mx_lookup_flow) .and not_change(configuration_instance, :smtp_fail_fast) @@ -171,6 +177,7 @@ .and not_change(configuration_instance, :whitelisted_domains) .and not_change(configuration_instance, :whitelist_validation) .and not_change(configuration_instance, :blacklisted_domains) + .and not_change(configuration_instance, :blacklisted_mx_ip_addresses) .and not_change(configuration_instance, :dns) .and not_change(configuration_instance, :not_rfc_mx_lookup_flow) .and not_change(configuration_instance, :smtp_fail_fast) @@ -197,6 +204,7 @@ .and not_change(configuration_instance, :whitelisted_domains) .and not_change(configuration_instance, :whitelist_validation) .and not_change(configuration_instance, :blacklisted_domains) + .and not_change(configuration_instance, :blacklisted_mx_ip_addresses) .and not_change(configuration_instance, :dns) .and not_change(configuration_instance, :not_rfc_mx_lookup_flow) .and not_change(configuration_instance, :smtp_fail_fast) @@ -445,6 +453,40 @@ end end + describe '#blacklisted_mx_ip_addresses=' do + let(:setter) { :blacklisted_mx_ip_addresses= } + + context 'with valid blacklisted mx ip addresses parameter type and context' do + let(:blacklisted_mx_ip_addresses) { Array.new(2) { random_ip_address } } + + it 'sets blacklisted mx ip addresses list' do + expect { configuration_instance.public_send(setter, blacklisted_mx_ip_addresses) } + .to change(configuration_instance, setter[0...-1].to_sym) + .from([]).to(blacklisted_mx_ip_addresses) + end + end + + context 'with invalid blacklisted mx ip addresses parameter type' do + let(:invalid_argument) { 'not_array' } + + include_examples 'raises extended argument error' + end + + context 'with invalid blacklisted mx ip addresses parameter context' do + context 'when includes not a String' do + let(:invalid_argument) { [42, random_ip_address] } + + include_examples 'raises extended argument error' + end + + context 'when includes wrong ip address' do + let(:invalid_argument) { ['not_ip_address', random_ip_address] } + + include_examples 'raises extended argument error' + end + end + end + describe '#dns=' do let(:setter) { :dns= } diff --git a/spec/truemail/core_spec.rb b/spec/truemail/core_spec.rb index c634314..3d8a7af 100644 --- a/spec/truemail/core_spec.rb +++ b/spec/truemail/core_spec.rb @@ -19,6 +19,10 @@ specify { expect(described_class).to be_const_defined(:REGEX_DOMAIN_PATTERN) } specify { expect(described_class).to be_const_defined(:REGEX_DOMAIN_FROM_EMAIL) } specify { expect(described_class).to be_const_defined(:REGEX_SMTP_ERROR_BODY_PATTERN) } + specify { expect(described_class).to be_const_defined(:REGEX_IP_ADDRESS) } + specify { expect(described_class).to be_const_defined(:REGEX_IP_ADDRESS_PATTERN) } + specify { expect(described_class).to be_const_defined(:REGEX_PORT_NUMBER) } + specify { expect(described_class).to be_const_defined(:REGEX_DNS_SERVER_ADDRESS_PATTERN) } end describe 'Truemail::RegexConstant::REGEX_EMAIL_PATTERN' do @@ -147,6 +151,20 @@ end end + describe 'Truemail::RegexConstant::REGEX_IP_ADDRESS_PATTERN' do + subject(:regex_pattern) { described_class::REGEX_IP_ADDRESS_PATTERN } + + describe 'Success' do + specify { expect(regex_pattern.match?(random_ip_address)).to be(true) } + end + + describe 'Failure' do + %w[10.300.0.256 11.287.0.1 172.1600.0.0 -0.1.1.1 8.08.8.8 192.168.0.255a 0.00.0.42].each do |invalid_ip_address| + specify { expect(regex_pattern.match?(invalid_ip_address)).to be(false) } + end + end + end + describe 'Truemail::RegexConstant::REGEX_DNS_SERVER_ADDRESS_PATTERN' do subject(:regex_pattern) { described_class::REGEX_DNS_SERVER_ADDRESS_PATTERN } @@ -291,6 +309,7 @@ def invalid_port_number 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(:MxBlacklist) } specify { expect(described_class).to be_const_defined(:Smtp) } end end diff --git a/spec/truemail/log/serializer/validator_text_spec.rb b/spec/truemail/log/serializer/validator_text_spec.rb index 8cf034f..84bee6c 100644 --- a/spec/truemail/log/serializer/validator_text_spec.rb +++ b/spec/truemail/log/serializer/validator_text_spec.rb @@ -14,7 +14,10 @@ let(:email) { random_email } let(:mx_servers) { create_servers_list } - let(:validator_instance) { create_validator(validation_type, email, mx_servers, success: success_status) } + let(:configuration_instance) { create_configuration } + let(:validator_instance) do + create_validator(validation_type, email, mx_servers, success: success_status, configuration: configuration_instance) + end shared_examples 'formatted text output' do it 'returns formatted text output' do @@ -70,6 +73,12 @@ include_examples 'formatted text output' end + describe 'ip list match validation' do + let(:validation_type) { :mx_blacklist } + + include_examples 'formatted text output' + end + describe 'smtp validation' do let(:validation_type) { :smtp } @@ -127,6 +136,28 @@ include_examples 'formatted text output' end + describe 'ip list match validation' do + let(:configuration_instance) { create_configuration(blacklisted_mx_ip_addresses: mx_servers) } + let(:validation_type) { :mx_blacklist } + let(:error) { 'mx blacklist: blacklisted mx server ip address' } + let(:expected_output) do + <<~EXPECTED_OUTPUT + Truemail #{validation_type} validation for #{email} failed (#{error}) + + CONFIGURATION SETTINGS: + whitelist validation: false + blacklisted mx ip addresses: #{mx_servers.join(', ')} + not rfc mx lookup flow: false + smtp fail fast: false + smtp safe check: false + email pattern: default gem value + smtp error body pattern: default gem value + EXPECTED_OUTPUT + end + + include_examples 'formatted text output' + end + describe 'smtp validation' do let(:validation_type) { :smtp } let(:error) { 'smtp: smtp error' } diff --git a/spec/truemail/validate/domain_list_match_spec.rb b/spec/truemail/validate/domain_list_match_spec.rb index 0c4fd99..3a8fe07 100644 --- a/spec/truemail/validate/domain_list_match_spec.rb +++ b/spec/truemail/validate/domain_list_match_spec.rb @@ -10,7 +10,7 @@ end describe '.check' do - subject(:list_match_validator) { described_class.check(result_instance) } + subject(:domain_list_match_validator) { described_class.check(result_instance) } let(:email) { random_email } let(:domain) { email[Truemail::RegexConstant::REGEX_DOMAIN_FROM_EMAIL, 1] } @@ -28,7 +28,7 @@ specify do allow(configuration_instance).to receive(:whitelisted_domains).and_return([domain]) allow(configuration_instance).to receive(:blacklisted_domains).and_return([]) - expect { list_match_validator }.to change(result_instance, :success).from(nil).to(true) + expect { domain_list_match_validator }.to change(result_instance, :success).from(nil).to(true) end end @@ -36,7 +36,7 @@ specify do allow(configuration_instance).to receive(:whitelisted_domains).and_return([]) allow(configuration_instance).to receive(:blacklisted_domains).and_return([domain]) - expect { list_match_validator }.to change(result_instance, :success) + expect { domain_list_match_validator }.to change(result_instance, :success) .from(nil).to(false) .and change(result_instance, :errors) .from({}).to(domain_list_match: Truemail::Validate::DomainListMatch::ERROR) @@ -47,7 +47,7 @@ specify do allow(configuration_instance).to receive(:whitelisted_domains).and_return([domain]) allow(configuration_instance).to receive(:blacklisted_domains).and_return([domain]) - expect { list_match_validator }.to change(result_instance, :success).from(nil).to(true) + expect { domain_list_match_validator }.to change(result_instance, :success).from(nil).to(true) end end @@ -55,7 +55,7 @@ specify do allow(configuration_instance).to receive(:whitelisted_domains).and_return([]) allow(configuration_instance).to receive(:blacklisted_domains).and_return([]) - expect { list_match_validator }.not_to change(result_instance, :success) + expect { domain_list_match_validator }.not_to change(result_instance, :success) end end end @@ -69,14 +69,14 @@ context 'when email domain in white list' do specify do allow(configuration_instance).to receive(:blacklisted_domains).and_return([]) - expect { list_match_validator }.not_to change(result_instance, :success) + expect { domain_list_match_validator }.not_to change(result_instance, :success) end end context 'when email domain exists on both lists' do specify do allow(configuration_instance).to receive(:blacklisted_domains).and_return([domain]) - expect { list_match_validator }.to change(result_instance, :success) + expect { domain_list_match_validator }.to change(result_instance, :success) .from(nil).to(false) .and change(result_instance, :errors) .from({}).to(domain_list_match: Truemail::Validate::DomainListMatch::ERROR) @@ -90,7 +90,7 @@ context 'when email domain in black list' do specify do allow(configuration_instance).to receive(:blacklisted_domains).and_return([domain]) - expect { list_match_validator }.to change(result_instance, :success) + expect { domain_list_match_validator }.to change(result_instance, :success) .from(nil).to(false) .and change(result_instance, :errors) .from({}).to(domain_list_match: Truemail::Validate::DomainListMatch::ERROR) @@ -100,7 +100,7 @@ context 'when email domain not exists on both lists' do specify do allow(configuration_instance).to receive(:blacklisted_domains).and_return([]) - expect { list_match_validator }.to change(result_instance, :success) + expect { domain_list_match_validator }.to change(result_instance, :success) .from(nil).to(false) .and change(result_instance, :errors) .from({}).to(domain_list_match: Truemail::Validate::DomainListMatch::ERROR) diff --git a/spec/truemail/validate/mx_blacklist_spec.rb b/spec/truemail/validate/mx_blacklist_spec.rb new file mode 100644 index 0000000..b7d6b9b --- /dev/null +++ b/spec/truemail/validate/mx_blacklist_spec.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +RSpec.describe Truemail::Validate::MxBlacklist do + describe 'defined constants' do + specify { expect(described_class).to be_const_defined(:ERROR) } + end + + describe 'inheritance' do + specify { expect(described_class).to be < Truemail::Validate::Base } + end + + describe '.check' do + subject(:mx_blacklist_validator) { described_class.check(result_instance) } + + let(:mail_servers) { Array.new(2) { random_ip_address } } + let(:configuration_instance) { create_configuration(blacklisted_mx_ip_addresses: blacklisted_mx_ip_addresses) } + let(:validator_instance) do + create_validator( + :mx, + random_email, + mail_servers, + success: true, + configuration: configuration_instance + ) + end + let(:result_instance) { validator_instance.result } + + describe 'Success' do + shared_examples 'not blacklisted mx server ip address' do + specify { expect { mx_blacklist_validator }.not_to change(result_instance, :success) } + end + + context 'when ip list match validation not configured' do + let(:configuration_instance) { create_configuration } + + it_behaves_like 'not blacklisted mx server ip address' + end + + context 'when mx servers ip addresses not included in blacklisted mx ip address list' do + let(:blacklisted_mx_ip_addresses) { [] } + + it_behaves_like 'not blacklisted mx server ip address' + end + end + + describe 'Failure' do + context 'when mx servers ip addresses included in blacklisted mx ip address list' do + let(:blacklisted_mx_ip_addresses) { mail_servers.sample(1) } + + specify do + expect { mx_blacklist_validator } + .to change(result_instance, :success) + .from(true).to(false) + .and change(result_instance, :errors) + .from({}).to(mx_blacklist: Truemail::Validate::MxBlacklist::ERROR) + end + end + end + end +end diff --git a/spec/truemail/validate/smtp_spec.rb b/spec/truemail/validate/smtp_spec.rb index 4604930..d24e44d 100644 --- a/spec/truemail/validate/smtp_spec.rb +++ b/spec/truemail/validate/smtp_spec.rb @@ -20,7 +20,7 @@ described_class.new( Truemail::Validator::Result.new( email: email, - mail_servers: Array.new(3) { random_ip_address }, + mail_servers: Array.new(2) { random_ip_address }, configuration: configuration_instance ) ) @@ -167,7 +167,7 @@ describe '#run' do before do - allow(Truemail::Validate::Mx).to receive(:check).and_return(true) + allow(Truemail::Validate::MxBlacklist).to receive(:check).and_return(true) allow(result_instance.mail_servers).to receive(:each) result_instance.success = true end diff --git a/truemail.gemspec b/truemail.gemspec index af880f7..9fc56e5 100644 --- a/truemail.gemspec +++ b/truemail.gemspec @@ -11,7 +11,7 @@ Gem::Specification.new do |spec| spec.email = ['admin@bestweb.com.ua'] spec.summary = %(truemail) - spec.description = %(Configurable framework agnostic plain Ruby email validator. Verify email via Regex, DNS and SMTP.) + spec.description = %(Configurable framework agnostic plain Ruby email validator. Verify email via Regex, DNS, SMTP and even more.) spec.homepage = 'https://github.com/truemail-rb/truemail' spec.license = 'MIT' @@ -42,11 +42,11 @@ Gem::Specification.new do |spec| spec.add_development_dependency 'overcommit', '~> 0.57.0' spec.add_development_dependency 'pry-byebug', '~> 3.9' spec.add_development_dependency 'rake', '~> 13.0', '>= 13.0.3' - spec.add_development_dependency 'reek', '~> 6.0', '>= 6.0.3' + spec.add_development_dependency 'reek', '~> 6.0', '>= 6.0.4' spec.add_development_dependency 'rspec', '~> 3.10' - spec.add_development_dependency 'rubocop', '~> 1.12', '>= 1.12.1' - spec.add_development_dependency 'rubocop-performance', '~> 1.10', '>= 1.10.2' - spec.add_development_dependency 'rubocop-rspec', '~> 2.2' + spec.add_development_dependency 'rubocop', '~> 1.13' + spec.add_development_dependency 'rubocop-performance', '~> 1.11' + spec.add_development_dependency 'rubocop-rspec', '~> 2.3' spec.add_development_dependency 'simplecov', '~> 0.17.1' spec.add_development_dependency 'truemail-rspec', '~> 0.4' spec.add_development_dependency 'webmock', '~> 3.12', '>= 3.12.2'