truemail 2.2.3 → 2.3.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.circleci/config.yml +26 -3
- data/.codeclimate.yml +1 -1
- data/.reek.yml +7 -0
- data/.rubocop.yml +30 -0
- data/CHANGELOG.md +67 -1
- data/Gemfile.lock +46 -33
- data/LICENSE.txt +1 -1
- data/README.md +49 -20
- data/bin/console +3 -10
- data/lib/truemail.rb +1 -1
- 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 +38 -30
- 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 +2 -1
- data/lib/truemail/log/serializer/validator_base.rb +1 -1
- 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/request.rb +3 -3
- 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 +62 -25
data/bin/console
CHANGED
@@ -3,14 +3,7 @@
|
|
3
3
|
# frozen_string_literal: true
|
4
4
|
|
5
5
|
require 'bundler/setup'
|
6
|
-
require '
|
6
|
+
require 'pry'
|
7
|
+
require_relative '../lib/truemail'
|
7
8
|
|
8
|
-
|
9
|
-
# with your gem easier. You can also use a different console, if you like.
|
10
|
-
|
11
|
-
# (If you use this, don't forget to add pry to your Gemfile!)
|
12
|
-
# require "pry"
|
13
|
-
# Pry.start
|
14
|
-
|
15
|
-
require 'irb'
|
16
|
-
IRB.start(__FILE__)
|
9
|
+
Pry.start
|
data/lib/truemail.rb
CHANGED
@@ -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,19 +7,23 @@ 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
28
|
attr_accessor :whitelist_validation, :not_rfc_mx_lookup_flow, :smtp_fail_fast, :smtp_safe_check
|
25
29
|
|
@@ -30,13 +34,6 @@ module Truemail
|
|
30
34
|
tap(&block) if block
|
31
35
|
end
|
32
36
|
|
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
|
38
|
-
end
|
39
|
-
|
40
37
|
def verifier_email=(email)
|
41
38
|
validate_arguments(email, __method__)
|
42
39
|
@verifier_email = email.downcase
|
@@ -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,6 +104,7 @@ 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,
|
105
109
|
smtp_fail_fast: false,
|
106
110
|
smtp_safe_check: false
|
@@ -137,13 +141,17 @@ module Truemail
|
|
137
141
|
end
|
138
142
|
|
139
143
|
def validate_validation_type(settings)
|
140
|
-
raise_unless(settings, 'hash with settings', settings.is_a?(Hash))
|
144
|
+
raise_unless(settings, 'hash with settings', settings.is_a?(::Hash))
|
141
145
|
settings.each do |domain, validation_type|
|
142
146
|
check_domain(domain)
|
143
147
|
check_validation_type(validation_type)
|
144
148
|
end
|
145
149
|
end
|
146
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
|
+
|
147
155
|
def logger_options(current_options)
|
148
156
|
Truemail::Configuration::DEFAULT_LOGGER_OPTIONS.merge(current_options).values
|
149
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]\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,6 +55,7 @@ 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,
|
59
60
|
smtp_fail_fast: executor_configuration.smtp_fail_fast,
|
60
61
|
smtp_safe_check: executor_configuration.smtp_safe_check,
|
@@ -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
|