Skip to content

Commit

Permalink
Feature/smtp retry if one mx found (#24)
Browse files Browse the repository at this point in the history
* Implement SMTP retry
* Update gem documentation
* Refactor
* Update gem version
  • Loading branch information
bestwebua authored Apr 5, 2019
1 parent 7bf5f37 commit 23a02e6
Show file tree
Hide file tree
Showing 14 changed files with 182 additions and 64 deletions.
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.1.4)
truemail (0.1.5)

GEM
remote: https://rubygems.org/
Expand Down
50 changes: 33 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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="[email protected]"
Expand All @@ -105,15 +107,15 @@ 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
=> #<Truemail::Configuration:0x000055590cb17b40
@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="[email protected]",
Expand Down Expand Up @@ -187,7 +189,13 @@ Truemail.validate('[email protected]', 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:

Expand All @@ -206,7 +214,7 @@ Truemail.validate('[email protected]', with: :mx)
success=true,
email="[email protected]",
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>
Expand All @@ -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```
Expand All @@ -240,7 +250,7 @@ Truemail.validate('[email protected]')
success=true,
email="[email protected]",
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>
Expand All @@ -252,7 +262,7 @@ Truemail.validate('[email protected]')
success=false,
email="[email protected]",
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=
[#<Truemail::Validate::Smtp::Request:0x0000000002d49b10
Expand All @@ -261,20 +271,22 @@ Truemail.validate('[email protected]')
@connection_timeout=2,
@email_pattern=/regex_pattern/,
@response_timeout=2,
@connection_attempts=2,
@smtp_safe_check=false,
@validation_type_by_domain={},
@verifier_domain="example.com",
@verifier_email="[email protected]">,
@email="[email protected]",
@host="mx1.example.com",
@host="127.0.1.1",
@attempts=nil,
@response=
#<struct Truemail::Validate::Smtp::Response
port_opened=true,
connection=true,
helo=
#<Net::SMTP::Response:0x0000000002d5aca8
@status="250",
@string="250 mx1.example.com Hello example.com\n">,
@string="250 127.0.1.1 Hello example.com\n">,
mailfrom=
#<Net::SMTP::Response:0x0000000002d5a618
@status="250",
Expand Down Expand Up @@ -303,7 +315,7 @@ Truemail.validate('[email protected]')
success=true,
email="[email protected]",
domain="example.com",
mail_servers=["mx1.example.com"],
mail_servers=["127.0.1.1", "127.0.1.2"],
errors={},
smtp_debug=
[#<Truemail::Validate::Smtp::Request:0x0000000002c95d40
Expand All @@ -312,20 +324,22 @@ Truemail.validate('[email protected]')
@connection_timeout=2,
@email_pattern=/regex_pattern/,
@response_timeout=2,
@connection_attempts=2,
@smtp_safe_check=true,
@validation_type_by_domain={},
@verifier_domain="example.com",
@verifier_email="[email protected]">,
@email="[email protected]",
@host="mx1.example.com",
@host="127.0.1.1",
@attempts=nil,
@response=
#<struct Truemail::Validate::Smtp::Response
port_opened=true,
connection=false,
helo=
#<Net::SMTP::Response:0x0000000002c934c8
@status="250",
@string="250 mx1.example.com\n">,
@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"}>>,]>,
Expand All @@ -338,7 +352,7 @@ Truemail.validate('[email protected]')
success=false,
email="[email protected]",
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=
[#<Truemail::Validate::Smtp::Request:0x0000000002d49b10
Expand All @@ -347,20 +361,22 @@ Truemail.validate('[email protected]')
@connection_timeout=2,
@email_pattern=/regex_pattern/,
@response_timeout=2,
@connection_attempts=2,
@smtp_safe_check=true,
@validation_type_by_domain={},
@verifier_domain="example.com",
@verifier_email="[email protected]">,
@email="[email protected]",
@host="mx1.example.com",
@host="127.0.1.1",
@attempts=nil,
@response=
#<struct Truemail::Validate::Smtp::Response
port_opened=true,
connection=true,
helo=
#<Net::SMTP::Response:0x0000000002d5aca8
@status="250",
@string="250 mx1.example.com Hello example.com\n">,
@string="250 127.0.1.1 Hello example.com\n">,
mailfrom=#<Net::SMTP::Response:0x0000000002d5a618 @status="250", @string="250 OK\n">,
rcptto=false,
errors={:rcptto=>"550 User not found\n"}>>]>,
Expand Down
10 changes: 6 additions & 4 deletions lib/truemail/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion lib/truemail/validate/resolver_execution_wrapper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
15 changes: 13 additions & 2 deletions lib/truemail/validate/smtp.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 12 additions & 2 deletions lib/truemail/validate/smtp/request.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
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.1.4'
VERSION = '0.1.5'
end
2 changes: 1 addition & 1 deletion spec/support/shared_examples/has_attr_accessor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
21 changes: 21 additions & 0 deletions spec/support/shared_examples/request_retry_behavior.rb
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion spec/support/shared_examples/sets_default_configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
20 changes: 10 additions & 10 deletions spec/truemail/configuration_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
Loading

0 comments on commit 23a02e6

Please sign in to comment.