diff --git a/Gemfile.lock b/Gemfile.lock index 17ddce2..f0f2eb2 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - truemail (0.1.4) + truemail (0.1.5) GEM remote: https://rubygems.org/ diff --git a/README.md b/README.md index d7e1f3d..26b5d77 100644 --- a/README.md +++ b/README.md @@ -64,8 +64,10 @@ Truemail.configure do |config| # Optional parameter. A SMTP server response timeout is equal to 2 ms by default. config.response_timeout = 1 - # Optional parameter. Total of timeout retry. It is equal to 1 by default. - config.retry_count = 2 + # Optional parameter. Total of connection attempts. It is equal to 2 by default. + # This parameter uses in mx lookup timeout error and smtp request (for cases when + # there is one mx server). + config.connection_attempts = 3 # Optional parameter. You can predefine which type of validation will be used for domains. # Available validation types: :regex, :mx, :smtp @@ -91,7 +93,7 @@ Truemail.configuration @connection_timeout=1, @email_pattern=/regex_pattern/, @response_timeout=1, - @retry_count=2, + @connection_attempts=3, @validation_type_by_domain={}, @verifier_domain="somedomain.com", @verifier_email="verifier@example.com" @@ -105,7 +107,7 @@ Truemail.configuration.connection_timeout = 3 => 3 Truemail.configuration.response_timeout = 4 => 4 -Truemail.configuration.retry_count = 1 +Truemail.configuration.connection_attempts = 1 => 1 Truemail.configuration @@ -113,7 +115,7 @@ Truemail.configuration @connection_timeout=3, @email_pattern=/regex_pattern/, @response_timeout=4, - @retry_count=1, + @connection_attempts=1, @validation_type_by_domain={}, @verifier_domain="somedomain.com", @verifier_email="verifier@example.com", @@ -187,7 +189,13 @@ Truemail.validate('email@example.com', with: :regex) #### MX validation -Validation by MX records is the second validation level. It uses Regex validation before running itself. When regex validation has completed successfully then runs itself. Truemail MX validation performs strictly following the [RFC 5321](https://tools.ietf.org/html/rfc5321#section-5) standard. +Validation by MX records is the second validation level. It uses Regex validation before running itself. When regex validation has completed successfully then runs itself. + +```code +[Regex validation] -> [MX validation] +``` + +Truemail MX validation performs strictly following the [RFC 5321](https://tools.ietf.org/html/rfc5321#section-5) standard. Example of usage: @@ -206,7 +214,7 @@ Truemail.validate('email@example.com', with: :mx) success=true, email="email@example.com", domain="example.com", - mail_servers=["mx1.example.com", "mx2.example.com"], + mail_servers=["127.0.1.1", "127.0.1.2"], errors={}, smtp_debug=nil>, @validation_type=:mx> @@ -220,6 +228,8 @@ SMTP validation is a final, third validation level. This type of validation trie [Regex validation] -> [MX 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. + By default, you don't need pass with-parameter to use it. Example of usage is specified below: With ```smtp_safe_check = false``` @@ -240,7 +250,7 @@ Truemail.validate('email@example.com') success=true, email="email@example.com", domain="example.com", - mail_servers=["mx1.example.com", "mx2.example.com"], + mail_servers=["127.0.1.1", "127.0.1.2"], errors={}, smtp_debug=nil>, @validation_type=:smtp> @@ -252,7 +262,7 @@ Truemail.validate('email@example.com') success=false, email="email@example.com", domain="example.com", - mail_servers=["mx1.example.com", "mx2.example.com", "mx3.example.com"], + mail_servers=["127.0.1.1", "127.0.1.2"], errors={:smtp=>"smtp error"}, smtp_debug= [#, @email="email@example.com", - @host="mx1.example.com", + @host="127.0.1.1", + @attempts=nil, @response= #, + @string="250 127.0.1.1 Hello example.com\n">, mailfrom= #, @email="email@example.com", - @host="mx1.example.com", + @host="127.0.1.1", + @attempts=nil, @response= #, + @string="250 127.0.1.1\n">, mailfrom=false, rcptto=nil, errors={:mailfrom=>"554 5.7.1 Client host blocked\n", :connection=>"server dropped connection after response"}>>,]>, @@ -338,7 +352,7 @@ Truemail.validate('email@example.com') success=false, email="email@example.com", domain="example.com", - mail_servers=["mx1.example.com", "mx2.example.com", "mx3.example.com"], + mail_servers=["127.0.1.1", "127.0.1.2"], errors={:smtp=>"smtp error"}, smtp_debug= [#, @email="email@example.com", - @host="mx1.example.com", + @host="127.0.1.1", + @attempts=nil, @response= #, + @string="250 127.0.1.1 Hello example.com\n">, mailfrom=#, rcptto=false, errors={:rcptto=>"550 User not found\n"}>>]>, diff --git a/lib/truemail/configuration.rb b/lib/truemail/configuration.rb index 6ac38c9..107b1e3 100644 --- a/lib/truemail/configuration.rb +++ b/lib/truemail/configuration.rb @@ -4,23 +4,25 @@ module Truemail class Configuration DEFAULT_CONNECTION_TIMEOUT = 2 DEFAULT_RESPONSE_TIMEOUT = 2 - DEFAULT_RETRY_COUNT = 1 + DEFAULT_CONNECTION_ATTEMPTS = 2 attr_reader :email_pattern, :verifier_email, :verifier_domain, :connection_timeout, :response_timeout, - :retry_count, + :connection_attempts, :validation_type_by_domain attr_accessor :smtp_safe_check + alias retry_count connection_attempts + def initialize @email_pattern = Truemail::RegexConstant::REGEX_EMAIL_PATTERN @connection_timeout = Truemail::Configuration::DEFAULT_CONNECTION_TIMEOUT @response_timeout = Truemail::Configuration::DEFAULT_RESPONSE_TIMEOUT - @retry_count = Truemail::Configuration::DEFAULT_RETRY_COUNT + @connection_attempts = Truemail::Configuration::DEFAULT_CONNECTION_ATTEMPTS @validation_type_by_domain = {} @smtp_safe_check = false end @@ -41,7 +43,7 @@ def verifier_domain=(domain) @verifier_domain = domain.downcase end - %i[connection_timeout response_timeout retry_count].each do |method| + %i[connection_timeout response_timeout connection_attempts].each do |method| define_method("#{method}=") do |argument| raise ArgumentError.new(argument, __method__) unless argument.is_a?(Integer) && argument.positive? instance_variable_set(:"@#{method}", argument) diff --git a/lib/truemail/validate/resolver_execution_wrapper.rb b/lib/truemail/validate/resolver_execution_wrapper.rb index 2376454..16b8d89 100644 --- a/lib/truemail/validate/resolver_execution_wrapper.rb +++ b/lib/truemail/validate/resolver_execution_wrapper.rb @@ -10,7 +10,7 @@ def self.call(&block) end def initialize - @attempts = Truemail.configuration.retry_count + @attempts = Truemail.configuration.connection_attempts end def call(&block) diff --git a/lib/truemail/validate/smtp.rb b/lib/truemail/validate/smtp.rb index 6cc59db..c4ade20 100644 --- a/lib/truemail/validate/smtp.rb +++ b/lib/truemail/validate/smtp.rb @@ -29,13 +29,24 @@ def request smtp_results.last end + def mail_servers + result.mail_servers + end + + def attempts + @attempts ||= + mail_servers.one? ? { attempts: Truemail.configuration.connection_attempts } : {} + end + def rcptto_error request.response.errors[:rcptto] end def establish_smtp_connection - result.mail_servers.each do |mail_server| - smtp_results << Truemail::Validate::Smtp::Request.new(host: mail_server, email: result.email) + mail_servers.each do |mail_server| + smtp_results << Truemail::Validate::Smtp::Request.new( + host: mail_server, email: result.email, **attempts + ) next unless request.check_port request.run || rcptto_error ? break : next end diff --git a/lib/truemail/validate/smtp/request.rb b/lib/truemail/validate/smtp/request.rb index 47506d3..54c13e6 100644 --- a/lib/truemail/validate/smtp/request.rb +++ b/lib/truemail/validate/smtp/request.rb @@ -13,17 +13,19 @@ class Request attr_reader :host, :email, :response - def initialize(host:, email:) + def initialize(host:, email:, attempts: nil) @host = host @email = email @response = Truemail::Validate::Smtp::Response.new + @attempts = attempts end def check_port Timeout.timeout(configuration.connection_timeout) do return response.port_opened = !TCPSocket.new(host, Truemail::Validate::Smtp::Request::SMTP_PORT).close end - rescue Timeout::Error + rescue => error + retry if attempts_exist? && error.is_a?(Timeout::Error) response.port_opened = false end @@ -33,11 +35,19 @@ def run smtp_handshakes(smtp_request, response) end rescue => error + retry if attempts_exist? assign_error(attribute: :connection, message: compose_from(error)) end private + attr_reader :attempts + + def attempts_exist? + return false unless attempts + (@attempts -= 1).positive? + end + def configuration @configuration ||= Truemail.configuration.dup.freeze end diff --git a/lib/truemail/version.rb b/lib/truemail/version.rb index a25de62..2535d83 100644 --- a/lib/truemail/version.rb +++ b/lib/truemail/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module Truemail - VERSION = '0.1.4' + VERSION = '0.1.5' end diff --git a/spec/support/shared_examples/has_attr_accessor.rb b/spec/support/shared_examples/has_attr_accessor.rb index 4180679..d6aadb5 100644 --- a/spec/support/shared_examples/has_attr_accessor.rb +++ b/spec/support/shared_examples/has_attr_accessor.rb @@ -8,7 +8,7 @@ module Truemail verifier_domain connection_timeout response_timeout - retry_count + connection_attempts smtp_safe_check ].each do |attribute| it "has attr_accessor :#{attribute}" do diff --git a/spec/support/shared_examples/request_retry_behavior.rb b/spec/support/shared_examples/request_retry_behavior.rb new file mode 100644 index 0000000..1418582 --- /dev/null +++ b/spec/support/shared_examples/request_retry_behavior.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module Truemail + RSpec.shared_examples 'request retry behavior' do + before { error_stubs } + + context 'when attempts not exists' do + specify do + expect { response_instance_target_method }.to not_change(request_instance, :attempts) + end + end + + context 'when attempts exists' do + let(:attempts) { { attempts: 5 } } + + specify do + expect { response_instance_target_method }.to change(request_instance, :attempts).from(5).to(0) + end + end + end +end diff --git a/spec/support/shared_examples/sets_default_configuration.rb b/spec/support/shared_examples/sets_default_configuration.rb index a8d618d..99038e5 100644 --- a/spec/support/shared_examples/sets_default_configuration.rb +++ b/spec/support/shared_examples/sets_default_configuration.rb @@ -8,7 +8,7 @@ module Truemail expect(configuration_instance.verifier_domain).to be_nil expect(configuration_instance.connection_timeout).to eq(Truemail::Configuration::DEFAULT_CONNECTION_TIMEOUT) expect(configuration_instance.response_timeout).to eq(Truemail::Configuration::DEFAULT_RESPONSE_TIMEOUT) - expect(configuration_instance.retry_count).to eq(Truemail::Configuration::DEFAULT_RETRY_COUNT) + expect(configuration_instance.connection_attempts).to eq(Truemail::Configuration::DEFAULT_CONNECTION_ATTEMPTS) expect(configuration_instance.validation_type_by_domain).to eq({}) expect(configuration_instance.smtp_safe_check).to be(false) end diff --git a/spec/truemail/configuration_spec.rb b/spec/truemail/configuration_spec.rb index 87b97c3..7f8a20f 100644 --- a/spec/truemail/configuration_spec.rb +++ b/spec/truemail/configuration_spec.rb @@ -6,7 +6,7 @@ describe 'defined constants' do specify { expect(described_class).to be_const_defined(:DEFAULT_CONNECTION_TIMEOUT) } specify { expect(described_class).to be_const_defined(:DEFAULT_RESPONSE_TIMEOUT) } - specify { expect(described_class).to be_const_defined(:DEFAULT_RETRY_COUNT) } + specify { expect(described_class).to be_const_defined(:DEFAULT_CONNECTION_ATTEMPTS) } end describe '.new' do @@ -32,7 +32,7 @@ expect(configuration_instance.email_pattern).to eq(Truemail::RegexConstant::REGEX_EMAIL_PATTERN) expect(configuration_instance.connection_timeout).to eq(2) expect(configuration_instance.response_timeout).to eq(2) - expect(configuration_instance.retry_count).to eq(1) + expect(configuration_instance.connection_attempts).to eq(2) expect(configuration_instance.validation_type_by_domain).to eq({}) expect(configuration_instance.smtp_safe_check).to be(false) end @@ -190,17 +190,17 @@ end end - describe '#retry_count=' do - context 'with valid retry count' do - it 'sets custom retry count' do - expect { configuration_instance.retry_count = 2 } - .to change(configuration_instance, :retry_count) - .from(1).to(2) + describe '#connection_attempts=' do + context 'with valid connection attempts' do + it 'sets custom connection attempts' do + expect { configuration_instance.connection_attempts = 3 } + .to change(configuration_instance, :connection_attempts) + .from(2).to(3) end end - context 'with invalid response timeout' do - let(:setter) { :response_timeout= } + context 'with invalid connection attempts' do + let(:setter) { :connection_attempts= } include_examples 'raises argument error' end diff --git a/spec/truemail/validate/resolver_execution_wrapper_spec.rb b/spec/truemail/validate/resolver_execution_wrapper_spec.rb index ffcd20c..3fac12d 100644 --- a/spec/truemail/validate/resolver_execution_wrapper_spec.rb +++ b/spec/truemail/validate/resolver_execution_wrapper_spec.rb @@ -40,7 +40,7 @@ allow(mx_instance).to receive(method).and_raise(Timeout::Error) expect { resolver_execution_wrapper } - .to change(resolver_execution_wrapper_instance, :attempts).from(1).to(0) + .to change(resolver_execution_wrapper_instance, :attempts).from(2).to(0) expect(resolver_execution_wrapper).to be(false) end diff --git a/spec/truemail/validate/smtp/request_spec.rb b/spec/truemail/validate/smtp/request_spec.rb index aefc442..96feaa4 100644 --- a/spec/truemail/validate/smtp/request_spec.rb +++ b/spec/truemail/validate/smtp/request_spec.rb @@ -1,7 +1,9 @@ # frozen_string_literal: true RSpec.describe Truemail::Validate::Smtp::Request do - subject(:request_instance) { described_class.new(host: mail_server, email: target_email) } + subject(:request_instance) do + described_class.new(host: mail_server, email: target_email, **attempts) + end let(:mail_server) { FFaker::Internet.domain_name } let(:target_email) { FFaker::Internet.email } @@ -10,6 +12,7 @@ let(:configuration) { request_instance.send(:configuration) } let(:connection_timout) { configuration.connection_timeout } let(:response_timeout) { configuration.response_timeout } + let(:attempts) { {} } before { Truemail.configure { |config| config.verifier_email = FFaker::Internet.email } } @@ -20,7 +23,7 @@ specify { expect(described_class).to be_const_defined(:CONNECTION_DROPPED) } end - describe 'attribute accessors' do + describe 'attribute readers' do specify { expect(request_instance.public_methods).to include(:host, :email, :response) } end @@ -32,20 +35,33 @@ describe '#check_port' do let(:connection_timeout) { configuration.connection_timeout } + let(:response_instance_target_method) { request_instance.check_port } context 'when port opened' do specify do allow(Timeout).to receive(:timeout).with(connection_timeout).and_call_original allow(TCPSocket).to receive_message_chain(:new, :close) - expect { request_instance.check_port }.to change(response_instance, :port_opened).from(nil).to(true) + expect { request_instance.check_port } + .to change(response_instance, :port_opened).from(nil).to(true) end end context 'when port closed' do - specify do + let(:error_stubs) do allow(Timeout).to receive(:timeout).with(connection_timeout).and_raise(Timeout::Error) - expect { request_instance.check_port }.to change(response_instance, :port_opened).from(nil).to(false) end + + specify do + error_stubs + expect { response_instance_target_method }.to change(response_instance, :port_opened).from(nil).to(false) + end + + specify do + allow(TCPSocket).to receive(:new).and_raise(SocketError) + expect { response_instance_target_method }.to change(response_instance, :port_opened).from(nil).to(false) + end + + include_examples 'request retry behavior' end end @@ -71,6 +87,8 @@ end describe '#run' do + let(:response_instance_target_method) { request_instance.run } + before do allow(session).to receive(:open_timeout=).with(connection_timout) allow(session).to receive(:read_timeout=).with(response_timeout) @@ -95,14 +113,14 @@ specify do allow(session).to receive(:start).and_yield(session) - expect { request_instance.run } + expect { response_instance_target_method } .to change(response_instance, :connection).from(nil).to(true) .and change(response_instance, :helo).from(nil).to(true) .and change(response_instance, :mailfrom).from(nil).to(true) .and change(response_instance, :rcptto).from(nil).to(true) .and not_change(response_instance, :errors) - expect(request_instance.run).to be(true) + expect(response_instance_target_method).to be(true) end end @@ -117,10 +135,14 @@ end context 'when connection timeout error' do - specify do + let(:error_stubs) do allow(session).to receive(:start).and_raise(Net::OpenTimeout) + end - expect { request_instance.run } + specify do + error_stubs + + expect { response_instance_target_method } .to change(response_instance, :connection).from(nil).to(false) .and change(response_instance, :errors) .from({}).to({ connection: Truemail::Validate::Smtp::Request::CONNECTION_TIMEOUT_ERROR }) @@ -128,16 +150,22 @@ .and not_change(response_instance, :mailfrom) .and not_change(response_instance, :rcptto) - expect(request_instance.run).to be(false) + expect(response_instance_target_method).to be(false) end + + include_examples 'request retry behavior' end context 'when remote server has dropped connection during session' do - specify do + let(:error_stubs) do allow(session).to receive(:start).and_yield(session).and_raise(EOFError) allow(session).to receive(:helo).and_raise(StandardError) + end + + specify do + error_stubs - expect { request_instance.run } + expect { response_instance_target_method } .to change(response_instance, :connection).from(nil).to(false) .and change(response_instance, :errors) .from({}).to({ connection: Truemail::Validate::Smtp::Request::CONNECTION_DROPPED, helo: 'StandardError' }) @@ -145,15 +173,21 @@ .and not_change(response_instance, :mailfrom) .and not_change(response_instance, :rcptto) - expect(request_instance.run).to be(false) + expect(response_instance_target_method).to be(false) end + + include_examples 'request retry behavior' end context 'when connection other errors' do - specify do + let(:error_stubs) do allow(session).to receive(:start).and_raise(StandardError, error_message) + end - expect { request_instance.run } + specify do + error_stubs + + expect { response_instance_target_method } .to change(response_instance, :connection).from(nil).to(false) .and change(response_instance, :errors) .from({}).to({ connection: 'error message' }) @@ -161,8 +195,10 @@ .and not_change(response_instance, :mailfrom) .and not_change(response_instance, :rcptto) - expect(request_instance.run).to be(false) + expect(response_instance_target_method).to be(false) end + + include_examples 'request retry behavior' end context 'when smtp response errors' do @@ -172,7 +208,7 @@ allow(session).to receive(:mailfrom) allow(session).to receive(:rcptto) - expect { request_instance.run } + expect { response_instance_target_method } .to change(response_instance, :connection).from(nil).to(true) .and change(response_instance, :helo).from(nil).to(false) .and change(response_instance, :errors) @@ -183,7 +219,7 @@ expect(session).not_to have_received(:mailfrom) expect(session).not_to have_received(:rcptto) - expect(request_instance.run).to be(false) + expect(response_instance_target_method).to be(false) end it 'mailfrom smtp server error' do @@ -192,7 +228,7 @@ allow(session).to receive(:mailfrom).and_raise(StandardError, error_message) allow(session).to receive(:rcptto) - expect { request_instance.run } + expect { response_instance_target_method } .to change(response_instance, :connection).from(nil).to(true) .and change(response_instance, :helo).from(nil).to(true) .and change(response_instance, :mailfrom).from(nil).to(false) @@ -201,7 +237,7 @@ expect(session).not_to have_received(:rcptto) - expect(request_instance.run).to be(false) + expect(response_instance_target_method).to be(false) end it 'rcptto smtp server error' do @@ -210,14 +246,14 @@ allow(session).to receive(:mailfrom).and_return(true) allow(session).to receive(:rcptto).and_raise(StandardError, error_message) - expect { request_instance.run } + expect { response_instance_target_method } .to change(response_instance, :connection).from(nil).to(true) .and change(response_instance, :helo).from(nil).to(true) .and change(response_instance, :mailfrom).from(nil).to(true) .and change(response_instance, :rcptto).from(nil).to(false) .and change(response_instance, :errors).from({}).to({ rcptto: error_message }) - expect(request_instance.run).to be(false) + expect(response_instance_target_method).to be(false) end end end diff --git a/spec/truemail/validate/smtp_spec.rb b/spec/truemail/validate/smtp_spec.rb index 2836203..ad42608 100644 --- a/spec/truemail/validate/smtp_spec.rb +++ b/spec/truemail/validate/smtp_spec.rb @@ -18,7 +18,7 @@ described_class.new( Truemail::Validator::Result.new( email: email, - mail_servers: Array.new(3) { FFaker::Internet.domain_name } + mail_servers: Array.new(3) { FFaker::Internet.ip_v4_address } ) ) end @@ -34,6 +34,28 @@ end end + describe '#mail_servers' do + it 'returns mail servers from result instance' do + expect(smtp_validator_instance.send(:mail_servers)).to eq(result_instance.mail_servers) + end + end + + describe '#attempts' do + context 'when more then one mail server' do + it 'returns empty hash' do + expect(smtp_validator_instance.send(:attempts)).to eq({}) + end + end + + context 'when one mail server' do + before { allow(result_instance.mail_servers).to receive(:one?).and_return(true) } + + it 'returns hash with attempts from configuration' do + expect(smtp_validator_instance.send(:attempts)).to eq({ attempts: Truemail.configuration.connection_attempts }) + end + end + end + describe '#establish_smtp_connection' do before { allow(result_instance.mail_servers).to receive(:each).and_call_original }