truemail 2.1.0 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.codeclimate.yml +1 -1
- data/.reek.yml +10 -0
- data/.rubocop.yml +54 -0
- data/CHANGELOG.md +96 -1
- data/Gemfile.lock +60 -42
- data/LICENSE.txt +1 -1
- data/README.md +124 -1
- data/lib/truemail.rb +2 -2
- data/lib/truemail/audit/base.rb +0 -1
- data/lib/truemail/audit/dns.rb +1 -1
- data/lib/truemail/audit/ip.rb +1 -1
- data/lib/truemail/audit/ptr.rb +4 -3
- data/lib/truemail/auditor.rb +1 -1
- data/lib/truemail/configuration.rb +41 -32
- data/lib/truemail/core.rb +11 -15
- data/lib/truemail/dns/punycode_representer.rb +16 -0
- data/lib/truemail/dns/resolver.rb +17 -0
- data/lib/truemail/dns/worker.rb +52 -0
- data/lib/truemail/log/serializer/auditor_json.rb +1 -1
- data/lib/truemail/log/serializer/base.rb +3 -1
- data/lib/truemail/log/serializer/validator_base.rb +6 -2
- data/lib/truemail/log/serializer/validator_text.rb +2 -2
- data/lib/truemail/logger.rb +1 -1
- data/lib/truemail/validate/mx.rb +7 -9
- data/lib/truemail/validate/smtp.rb +15 -5
- data/lib/truemail/validate/smtp/request.rb +11 -7
- data/lib/truemail/validate/smtp/response.rb +1 -1
- data/lib/truemail/validator.rb +2 -2
- data/lib/truemail/version.rb +1 -1
- data/lib/truemail/wrapper.rb +3 -3
- data/truemail.gemspec +10 -8
- metadata +73 -24
data/lib/truemail.rb
CHANGED
@@ -10,7 +10,7 @@ module Truemail
|
|
10
10
|
class << self
|
11
11
|
def configuration(&block)
|
12
12
|
@configuration ||= begin
|
13
|
-
return unless
|
13
|
+
return unless block
|
14
14
|
configuration = Truemail::Configuration.new(&block)
|
15
15
|
raise_unless(configuration.complete?, Truemail::INCOMPLETE_CONFIG)
|
16
16
|
configuration
|
@@ -46,7 +46,7 @@ module Truemail
|
|
46
46
|
end
|
47
47
|
|
48
48
|
def check_argument_type(argument)
|
49
|
-
raise_unless(argument.is_a?(String), Truemail::INVALID_TYPE, Truemail::TypeError)
|
49
|
+
raise_unless(argument.is_a?(::String), Truemail::INVALID_TYPE, Truemail::TypeError)
|
50
50
|
end
|
51
51
|
|
52
52
|
def determine_configuration(custom_configuration)
|
data/lib/truemail/audit/base.rb
CHANGED
data/lib/truemail/audit/dns.rb
CHANGED
data/lib/truemail/audit/ip.rb
CHANGED
data/lib/truemail/audit/ptr.rb
CHANGED
@@ -15,13 +15,14 @@ module Truemail
|
|
15
15
|
private
|
16
16
|
|
17
17
|
def current_host_reverse_lookup
|
18
|
-
IPAddr.new(current_host_ip).reverse
|
18
|
+
::IPAddr.new(current_host_ip).reverse
|
19
19
|
end
|
20
20
|
|
21
21
|
def ptr_records
|
22
22
|
@ptr_records ||= Truemail::Wrapper.call(configuration: configuration) do
|
23
|
-
|
24
|
-
current_host_reverse_lookup,
|
23
|
+
Truemail::Dns::Resolver.ptr_records(
|
24
|
+
current_host_reverse_lookup,
|
25
|
+
configuration: configuration
|
25
26
|
).map { |ptr_record| ptr_record.name.to_s }
|
26
27
|
end || []
|
27
28
|
end
|
data/lib/truemail/auditor.rb
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
module Truemail
|
4
4
|
class Auditor < Truemail::Executor
|
5
|
-
Result = Struct.new(:current_host_ip, :warnings, :configuration, keyword_init: true) do
|
5
|
+
Result = ::Struct.new(:current_host_ip, :warnings, :configuration, keyword_init: true) do
|
6
6
|
def initialize(warnings: {}, **args)
|
7
7
|
super
|
8
8
|
end
|
@@ -7,34 +7,31 @@ module Truemail
|
|
7
7
|
DEFAULT_CONNECTION_ATTEMPTS = 2
|
8
8
|
DEFAULT_VALIDATION_TYPE = :smtp
|
9
9
|
DEFAULT_LOGGER_OPTIONS = { tracking_event: :error, stdout: false, log_absolute_path: nil }.freeze
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
10
|
+
SETTERS = %i[
|
11
|
+
email_pattern
|
12
|
+
smtp_error_body_pattern
|
13
|
+
connection_timeout
|
14
|
+
response_timeout
|
15
|
+
connection_attempts
|
16
|
+
whitelisted_domains
|
17
|
+
blacklisted_domains
|
18
|
+
].freeze
|
19
|
+
|
20
|
+
attr_reader :verifier_email,
|
14
21
|
:verifier_domain,
|
15
|
-
:connection_timeout,
|
16
|
-
:response_timeout,
|
17
|
-
:connection_attempts,
|
18
22
|
:default_validation_type,
|
19
23
|
:validation_type_by_domain,
|
20
|
-
:
|
21
|
-
:
|
22
|
-
|
24
|
+
:dns,
|
25
|
+
:logger,
|
26
|
+
*Truemail::Configuration::SETTERS
|
23
27
|
|
24
|
-
attr_accessor :whitelist_validation, :not_rfc_mx_lookup_flow, :smtp_safe_check
|
28
|
+
attr_accessor :whitelist_validation, :not_rfc_mx_lookup_flow, :smtp_fail_fast, :smtp_safe_check
|
25
29
|
|
26
30
|
def initialize(&block)
|
27
31
|
instance_initializer.each do |instace_variable, value|
|
28
32
|
instance_variable_set(:"@#{instace_variable}", value)
|
29
33
|
end
|
30
|
-
tap(&block) if
|
31
|
-
end
|
32
|
-
|
33
|
-
%i[email_pattern smtp_error_body_pattern].each do |method|
|
34
|
-
define_method("#{method}=") do |argument|
|
35
|
-
raise_unless(argument, __method__, argument.is_a?(Regexp))
|
36
|
-
instance_variable_set(:"@#{method}", argument)
|
37
|
-
end
|
34
|
+
tap(&block) if block
|
38
35
|
end
|
39
36
|
|
40
37
|
def verifier_email=(email)
|
@@ -48,15 +45,8 @@ module Truemail
|
|
48
45
|
@verifier_domain = domain.downcase
|
49
46
|
end
|
50
47
|
|
51
|
-
%i[connection_timeout response_timeout connection_attempts].each do |method|
|
52
|
-
define_method("#{method}=") do |argument|
|
53
|
-
raise_unless(argument, __method__, argument.is_a?(Integer) && argument.positive?)
|
54
|
-
instance_variable_set(:"@#{method}", argument)
|
55
|
-
end
|
56
|
-
end
|
57
|
-
|
58
48
|
def default_validation_type=(argument)
|
59
|
-
raise_unless(argument, __method__, argument.is_a?(Symbol) && Truemail::Validator::VALIDATION_TYPES.include?(argument))
|
49
|
+
raise_unless(argument, __method__, argument.is_a?(::Symbol) && Truemail::Validator::VALIDATION_TYPES.include?(argument))
|
60
50
|
@default_validation_type = argument
|
61
51
|
end
|
62
52
|
|
@@ -65,18 +55,31 @@ module Truemail
|
|
65
55
|
validation_type_by_domain.merge!(settings)
|
66
56
|
end
|
67
57
|
|
68
|
-
|
58
|
+
def argument_consistent?(argument)
|
59
|
+
case argument
|
60
|
+
when ::Array then check_domain_list(argument)
|
61
|
+
when ::Integer then argument.positive?
|
62
|
+
when ::Regexp then true
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
Truemail::Configuration::SETTERS.each do |method|
|
69
67
|
define_method("#{method}=") do |argument|
|
70
|
-
raise_unless(argument, __method__,
|
68
|
+
raise_unless(argument, __method__, argument_consistent?(argument))
|
71
69
|
instance_variable_set(:"@#{method}", argument)
|
72
70
|
end
|
73
71
|
end
|
74
72
|
|
73
|
+
def dns=(argument)
|
74
|
+
raise_unless(argument, __method__, argument.is_a?(::Array) && check_dns_settings(argument))
|
75
|
+
@dns = argument
|
76
|
+
end
|
77
|
+
|
75
78
|
def logger=(options)
|
76
79
|
tracking_event, stdout, log_absolute_path = logger_options(options)
|
77
80
|
valid_event = Truemail::Log::Event::TRACKING_EVENTS.key?(tracking_event)
|
78
81
|
stdout_only = stdout && log_absolute_path.nil?
|
79
|
-
file_only = log_absolute_path.is_a?(String)
|
82
|
+
file_only = log_absolute_path.is_a?(::String)
|
80
83
|
both_types = stdout && file_only
|
81
84
|
argument_info = valid_event ? log_absolute_path : tracking_event
|
82
85
|
raise_unless(argument_info, __method__, valid_event && (stdout_only || file_only || both_types))
|
@@ -89,7 +92,7 @@ module Truemail
|
|
89
92
|
|
90
93
|
private
|
91
94
|
|
92
|
-
def instance_initializer
|
95
|
+
def instance_initializer # rubocop:disable Metrics/MethodLength
|
93
96
|
{
|
94
97
|
email_pattern: Truemail::RegexConstant::REGEX_EMAIL_PATTERN,
|
95
98
|
smtp_error_body_pattern: Truemail::RegexConstant::REGEX_SMTP_ERROR_BODY_PATTERN,
|
@@ -101,7 +104,9 @@ module Truemail
|
|
101
104
|
whitelisted_domains: [],
|
102
105
|
whitelist_validation: false,
|
103
106
|
blacklisted_domains: [],
|
107
|
+
dns: [],
|
104
108
|
not_rfc_mx_lookup_flow: false,
|
109
|
+
smtp_fail_fast: false,
|
105
110
|
smtp_safe_check: false
|
106
111
|
}
|
107
112
|
end
|
@@ -136,13 +141,17 @@ module Truemail
|
|
136
141
|
end
|
137
142
|
|
138
143
|
def validate_validation_type(settings)
|
139
|
-
raise_unless(settings, 'hash with settings', settings.is_a?(Hash))
|
144
|
+
raise_unless(settings, 'hash with settings', settings.is_a?(::Hash))
|
140
145
|
settings.each do |domain, validation_type|
|
141
146
|
check_domain(domain)
|
142
147
|
check_validation_type(validation_type)
|
143
148
|
end
|
144
149
|
end
|
145
150
|
|
151
|
+
def check_dns_settings(dns_servers)
|
152
|
+
dns_servers.all? { |dns_server| Truemail::RegexConstant::REGEX_DNS_SERVER_ADDRESS_PATTERN.match?(dns_server.to_s) }
|
153
|
+
end
|
154
|
+
|
146
155
|
def logger_options(current_options)
|
147
156
|
Truemail::Configuration::DEFAULT_LOGGER_OPTIONS.merge(current_options).values
|
148
157
|
end
|
data/lib/truemail/core.rb
CHANGED
@@ -10,32 +10,28 @@ module Truemail
|
|
10
10
|
require_relative '../truemail/validator'
|
11
11
|
require_relative '../truemail/logger'
|
12
12
|
|
13
|
-
ConfigurationError = Class.new(StandardError)
|
14
|
-
TypeError = Class.new(StandardError)
|
15
|
-
|
16
|
-
ArgumentError = Class.new(StandardError) do
|
13
|
+
ConfigurationError = ::Class.new(::StandardError)
|
14
|
+
TypeError = ::Class.new(::StandardError)
|
15
|
+
ArgumentError = ::Class.new(::StandardError) do
|
17
16
|
def initialize(arg_value, arg_name)
|
18
17
|
super("#{arg_value} is not a valid #{arg_name}")
|
19
18
|
end
|
20
19
|
end
|
21
20
|
|
22
|
-
PunycodeRepresenter = Class.new do
|
23
|
-
require 'simpleidn'
|
24
|
-
|
25
|
-
def self.call(email)
|
26
|
-
return unless email.is_a?(String)
|
27
|
-
return email if email.ascii_only?
|
28
|
-
user, domain = email.split('@')
|
29
|
-
"#{user}@#{SimpleIDN.to_ascii(domain.downcase)}"
|
30
|
-
end
|
31
|
-
end
|
32
|
-
|
33
21
|
module RegexConstant
|
34
22
|
REGEX_DOMAIN = /[\p{L}0-9]+([\-.]{1}[\p{L}0-9]+)*\.\p{L}{2,63}/i.freeze
|
35
23
|
REGEX_EMAIL_PATTERN = /(?=\A.{6,255}\z)(\A([\p{L}0-9]+[\w|\-.+]*)@(#{REGEX_DOMAIN})\z)/.freeze
|
36
24
|
REGEX_DOMAIN_PATTERN = /(?=\A.{4,255}\z)(\A#{REGEX_DOMAIN}\z)/.freeze
|
37
25
|
REGEX_DOMAIN_FROM_EMAIL = /\A.+@(.+)\z/.freeze
|
38
26
|
REGEX_SMTP_ERROR_BODY_PATTERN = /(?=.*550)(?=.*(user|account|customer|mailbox)).*/i.freeze
|
27
|
+
REGEX_PORT_NUMBER = /(6553[0-5]|655[0-2][0-9]\d|65[0-4](\d){2}|6[0-4](\d){3}|[1-5](\d){4}|[1-9](\d){0,3})/.freeze
|
28
|
+
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
|
29
|
+
end
|
30
|
+
|
31
|
+
module Dns
|
32
|
+
require_relative '../truemail/dns/punycode_representer'
|
33
|
+
require_relative '../truemail/dns/worker'
|
34
|
+
require_relative '../truemail/dns/resolver'
|
39
35
|
end
|
40
36
|
|
41
37
|
module Audit
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Truemail
|
4
|
+
module Dns
|
5
|
+
PunycodeRepresenter = Class.new do
|
6
|
+
require 'simpleidn'
|
7
|
+
|
8
|
+
def self.call(email)
|
9
|
+
return unless email.is_a?(::String)
|
10
|
+
return email if email.ascii_only?
|
11
|
+
user, domain = email.split('@')
|
12
|
+
"#{user}@#{SimpleIDN.to_ascii(domain.downcase)}"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Truemail
|
4
|
+
module Dns
|
5
|
+
class Resolver
|
6
|
+
WORKER_ACTIONS = %i[dns_lookup a_record a_records cname_records mx_records ptr_records].freeze
|
7
|
+
|
8
|
+
class << self
|
9
|
+
Truemail::Dns::Resolver::WORKER_ACTIONS.each do |worker_action|
|
10
|
+
define_method(worker_action) do |argument, configuration:|
|
11
|
+
Truemail::Dns::Worker.new(configuration.dns).public_send(worker_action, argument)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Truemail
|
4
|
+
module Dns
|
5
|
+
require 'resolv'
|
6
|
+
|
7
|
+
class Worker < ::Resolv::DNS
|
8
|
+
DEFAULT_DNS_PORT = 53
|
9
|
+
|
10
|
+
attr_reader :dns_gateway
|
11
|
+
|
12
|
+
def initialize(dns_servers)
|
13
|
+
super(dns_servers.empty? ? nil : config_info(dns_servers))
|
14
|
+
end
|
15
|
+
|
16
|
+
def dns_lookup(host_address)
|
17
|
+
getname(host_address).to_s
|
18
|
+
end
|
19
|
+
|
20
|
+
def a_record(host_name)
|
21
|
+
getaddress(host_name).to_s
|
22
|
+
end
|
23
|
+
|
24
|
+
def a_records(host_name)
|
25
|
+
getaddresses(host_name).map(&:to_s)
|
26
|
+
end
|
27
|
+
|
28
|
+
def cname_records(host_name)
|
29
|
+
getresources(host_name, ::Resolv::DNS::Resource::IN::CNAME)
|
30
|
+
end
|
31
|
+
|
32
|
+
def mx_records(host_name)
|
33
|
+
getresources(host_name, ::Resolv::DNS::Resource::IN::MX)
|
34
|
+
end
|
35
|
+
|
36
|
+
def ptr_records(host_address)
|
37
|
+
getresources(host_address, ::Resolv::DNS::Resource::IN::PTR)
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def nameserver_port(server)
|
43
|
+
server_address, server_port = server.split(':')
|
44
|
+
[server_address, server_port ? server_port.to_i : Truemail::Dns::Worker::DEFAULT_DNS_PORT]
|
45
|
+
end
|
46
|
+
|
47
|
+
def config_info(dns_servers)
|
48
|
+
@dns_gateway = { nameserver_port: dns_servers.map { |server| nameserver_port(server) } }
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -30,7 +30,7 @@ module Truemail
|
|
30
30
|
|
31
31
|
alias warnings errors
|
32
32
|
|
33
|
-
%i[validation_type_by_domain whitelisted_domains blacklisted_domains].each do |method|
|
33
|
+
%i[validation_type_by_domain whitelisted_domains blacklisted_domains dns].each do |method|
|
34
34
|
define_method(method) do
|
35
35
|
value = executor_configuration.public_send(method)
|
36
36
|
return if value.empty?
|
@@ -55,7 +55,9 @@ module Truemail
|
|
55
55
|
whitelist_validation: executor_configuration.whitelist_validation,
|
56
56
|
whitelisted_domains: whitelisted_domains,
|
57
57
|
blacklisted_domains: blacklisted_domains,
|
58
|
+
dns: dns,
|
58
59
|
not_rfc_mx_lookup_flow: executor_configuration.not_rfc_mx_lookup_flow,
|
60
|
+
smtp_fail_fast: executor_configuration.smtp_fail_fast,
|
59
61
|
smtp_safe_check: executor_configuration.smtp_safe_check,
|
60
62
|
email_pattern: email_pattern,
|
61
63
|
smtp_error_body_pattern: smtp_error_body_pattern
|
@@ -13,6 +13,10 @@ module Truemail
|
|
13
13
|
|
14
14
|
attr_reader :validation_type
|
15
15
|
|
16
|
+
def replace_invalid_chars
|
17
|
+
->(value) { value.encode('UTF-8', invalid: :replace) }
|
18
|
+
end
|
19
|
+
|
16
20
|
def smtp_debug
|
17
21
|
validation_smtp_debug = executor_result.smtp_debug
|
18
22
|
return unless validation_smtp_debug
|
@@ -22,7 +26,7 @@ module Truemail
|
|
22
26
|
mail_host: smtp_request.host,
|
23
27
|
port_opened: smtp_response.port_opened,
|
24
28
|
connection: smtp_response.connection,
|
25
|
-
errors: smtp_response.errors
|
29
|
+
errors: smtp_response.errors.transform_values(&replace_invalid_chars)
|
26
30
|
}
|
27
31
|
end
|
28
32
|
end
|
@@ -30,7 +34,7 @@ module Truemail
|
|
30
34
|
def result
|
31
35
|
@result ||=
|
32
36
|
{
|
33
|
-
date: Time.now,
|
37
|
+
date: ::Time.now,
|
34
38
|
email: executor_result.email,
|
35
39
|
validation_type: validation_type,
|
36
40
|
success: executor_result.success,
|
@@ -20,8 +20,8 @@ module Truemail
|
|
20
20
|
enumerable_object.inject([]) do |formatted_data, (key, value)|
|
21
21
|
data =
|
22
22
|
case
|
23
|
-
when value.is_a?(Hash) then "\n#{printer(value)}"
|
24
|
-
when value.is_a?(Array) then value.join(', ')
|
23
|
+
when value.is_a?(::Hash) then "\n#{printer(value)}"
|
24
|
+
when value.is_a?(::Array) then value.join(', ')
|
25
25
|
else value
|
26
26
|
end
|
27
27
|
formatted_data << "#{key.to_s.tr('_', ' ')}: #{data}".chomp << "\n"
|
data/lib/truemail/logger.rb
CHANGED
@@ -23,7 +23,7 @@ module Truemail
|
|
23
23
|
def init_log_file
|
24
24
|
output_file = Pathname(file)
|
25
25
|
return output_file if output_file.exist?
|
26
|
-
output_file.parent.mkpath && FileUtils.touch(output_file)
|
26
|
+
output_file.parent.mkpath && ::FileUtils.touch(output_file)
|
27
27
|
output_file
|
28
28
|
end
|
29
29
|
|
data/lib/truemail/validate/mx.rb
CHANGED
@@ -3,8 +3,6 @@
|
|
3
3
|
module Truemail
|
4
4
|
module Validate
|
5
5
|
class Mx < Truemail::Validate::Base
|
6
|
-
require 'resolv'
|
7
|
-
|
8
6
|
ERROR = 'target host(s) not found'
|
9
7
|
NULL_MX_RECORD = 'null_mx_record'
|
10
8
|
|
@@ -43,11 +41,11 @@ module Truemail
|
|
43
41
|
end
|
44
42
|
|
45
43
|
def mx_records(hostname)
|
46
|
-
domain_mx_records =
|
44
|
+
domain_mx_records = Truemail::Dns::Resolver.mx_records(hostname, configuration: configuration)
|
47
45
|
return [Truemail::Validate::Mx::NULL_MX_RECORD] if null_mx?(domain_mx_records)
|
48
|
-
domain_mx_records.sort_by(&:preference).
|
49
|
-
|
50
|
-
end
|
46
|
+
domain_mx_records.sort_by(&:preference).flat_map do |mx_record|
|
47
|
+
Truemail::Dns::Resolver.a_records(mx_record.exchange.to_s, configuration: configuration)
|
48
|
+
end
|
51
49
|
end
|
52
50
|
|
53
51
|
def mail_servers_found?
|
@@ -64,15 +62,15 @@ module Truemail
|
|
64
62
|
end
|
65
63
|
|
66
64
|
def a_record(hostname)
|
67
|
-
|
65
|
+
Truemail::Dns::Resolver.a_record(hostname, configuration: configuration)
|
68
66
|
end
|
69
67
|
|
70
68
|
def hosts_from_cname_records?
|
71
|
-
cname_records =
|
69
|
+
cname_records = Truemail::Dns::Resolver.cname_records(domain, configuration: configuration)
|
72
70
|
return if cname_records.empty?
|
73
71
|
cname_records.each do |cname_record|
|
74
72
|
host = a_record(cname_record.name.to_s)
|
75
|
-
hostname =
|
73
|
+
hostname = Truemail::Dns::Resolver.dns_lookup(host, configuration: configuration)
|
76
74
|
found_hosts = mx_records(hostname)
|
77
75
|
fetch_target_hosts(found_hosts.empty? ? [host] : found_hosts)
|
78
76
|
end
|