truemail 1.8.0 → 2.0.1
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/.circleci/config.yml +1 -1
- data/.codeclimate.yml +1 -1
- data/.github/ISSUE_TEMPLATE.md +3 -3
- data/.github/PULL_REQUEST_TEMPLATE.md +1 -1
- data/.reek.yml +4 -0
- data/.rubocop.yml +186 -6
- data/CHANGELOG.md +118 -31
- data/Gemfile.lock +47 -41
- data/LICENSE.txt +1 -1
- data/README.md +143 -103
- data/lib/truemail.rb +9 -2
- data/lib/truemail/audit/dns.rb +1 -1
- data/lib/truemail/audit/ptr.rb +2 -2
- data/lib/truemail/auditor.rb +5 -3
- data/lib/truemail/configuration.rb +8 -2
- data/lib/truemail/core.rb +8 -4
- data/lib/truemail/executor.rb +11 -0
- data/lib/truemail/log/serializer/auditor_json.rb +25 -0
- data/lib/truemail/log/serializer/base.rb +17 -42
- data/lib/truemail/log/serializer/validator_base.rb +45 -0
- data/lib/truemail/log/serializer/{json.rb → validator_json.rb} +1 -1
- data/lib/truemail/log/serializer/{text.rb → validator_text.rb} +2 -2
- data/lib/truemail/logger.rb +1 -1
- data/lib/truemail/validate/smtp/request.rb +2 -3
- data/lib/truemail/validator.rb +4 -4
- data/lib/truemail/version.rb +1 -1
- data/truemail.gemspec +17 -9
- metadata +42 -22
data/lib/truemail.rb
CHANGED
@@ -5,6 +5,7 @@ require_relative 'truemail/core'
|
|
5
5
|
module Truemail
|
6
6
|
INCOMPLETE_CONFIG = 'verifier_email is required parameter'
|
7
7
|
NOT_CONFIGURED = 'use Truemail.configure before or pass custom configuration'
|
8
|
+
INVALID_TYPE = 'email should be a String'
|
8
9
|
|
9
10
|
class << self
|
10
11
|
def configuration(&block)
|
@@ -25,10 +26,12 @@ module Truemail
|
|
25
26
|
end
|
26
27
|
|
27
28
|
def validate(email, custom_configuration: nil, **options)
|
29
|
+
check_argument_type(email)
|
28
30
|
Truemail::Validator.new(email, configuration: determine_configuration(custom_configuration), **options).run
|
29
31
|
end
|
30
32
|
|
31
33
|
def valid?(email, **options)
|
34
|
+
check_argument_type(email)
|
32
35
|
validate(email, **options).result.valid?
|
33
36
|
end
|
34
37
|
|
@@ -38,8 +41,12 @@ module Truemail
|
|
38
41
|
|
39
42
|
private
|
40
43
|
|
41
|
-
def raise_unless(condition, message)
|
42
|
-
raise
|
44
|
+
def raise_unless(condition, message, error_class = Truemail::ConfigurationError)
|
45
|
+
raise error_class, message unless condition
|
46
|
+
end
|
47
|
+
|
48
|
+
def check_argument_type(argument)
|
49
|
+
raise_unless(argument.is_a?(String), Truemail::INVALID_TYPE, Truemail::TypeError)
|
43
50
|
end
|
44
51
|
|
45
52
|
def determine_configuration(custom_configuration)
|
data/lib/truemail/audit/dns.rb
CHANGED
@@ -3,7 +3,7 @@
|
|
3
3
|
module Truemail
|
4
4
|
module Audit
|
5
5
|
class Dns < Truemail::Audit::Base
|
6
|
-
VERIFIER_DOMAIN_NOT_REFER = '
|
6
|
+
VERIFIER_DOMAIN_NOT_REFER = 'A-record of verifier domain not refers to current host ip address'
|
7
7
|
|
8
8
|
def run
|
9
9
|
return if verifier_domain_refer_to_current_host_ip?
|
data/lib/truemail/audit/ptr.rb
CHANGED
@@ -3,8 +3,8 @@
|
|
3
3
|
module Truemail
|
4
4
|
module Audit
|
5
5
|
class Ptr < Truemail::Audit::Base
|
6
|
-
PTR_NOT_FOUND = '
|
7
|
-
PTR_NOT_REFER = '
|
6
|
+
PTR_NOT_FOUND = 'PTR-record for current host ip address was not found'
|
7
|
+
PTR_NOT_REFER = 'PTR-record does not reference to current verifier domain'
|
8
8
|
|
9
9
|
def run
|
10
10
|
return add_warning(Truemail::Audit::Ptr::PTR_NOT_FOUND) if ptr_records.empty?
|
data/lib/truemail/auditor.rb
CHANGED
@@ -1,15 +1,13 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Truemail
|
4
|
-
class Auditor
|
4
|
+
class Auditor < Truemail::Executor
|
5
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
|
9
9
|
end
|
10
10
|
|
11
|
-
attr_reader :result
|
12
|
-
|
13
11
|
def initialize(configuration:)
|
14
12
|
@result = Truemail::Auditor::Result.new(configuration: configuration)
|
15
13
|
end
|
@@ -18,5 +16,9 @@ module Truemail
|
|
18
16
|
Truemail::Audit::Ip.check(result)
|
19
17
|
self
|
20
18
|
end
|
19
|
+
|
20
|
+
def as_json
|
21
|
+
Truemail::Log::Serializer::AuditorJson.call(self)
|
22
|
+
end
|
21
23
|
end
|
22
24
|
end
|
@@ -6,6 +6,7 @@ module Truemail
|
|
6
6
|
DEFAULT_RESPONSE_TIMEOUT = 2
|
7
7
|
DEFAULT_CONNECTION_ATTEMPTS = 2
|
8
8
|
DEFAULT_VALIDATION_TYPE = :smtp
|
9
|
+
DEFAULT_LOGGER_OPTIONS = { tracking_event: :error, stdout: false, log_absolute_path: nil }.freeze
|
9
10
|
|
10
11
|
attr_reader :email_pattern,
|
11
12
|
:smtp_error_body_pattern,
|
@@ -71,7 +72,8 @@ module Truemail
|
|
71
72
|
end
|
72
73
|
end
|
73
74
|
|
74
|
-
def logger=(
|
75
|
+
def logger=(options)
|
76
|
+
tracking_event, stdout, log_absolute_path = logger_options(options)
|
75
77
|
valid_event = Truemail::Log::Event::TRACKING_EVENTS.key?(tracking_event)
|
76
78
|
stdout_only = stdout && log_absolute_path.nil?
|
77
79
|
file_only = log_absolute_path.is_a?(String)
|
@@ -109,7 +111,7 @@ module Truemail
|
|
109
111
|
end
|
110
112
|
|
111
113
|
def validate_arguments(argument, method)
|
112
|
-
constant = Truemail::RegexConstant.const_get("regex_#{method[/\A.+_(.+)
|
114
|
+
constant = Truemail::RegexConstant.const_get("regex_#{method[/\A.+_(.+)=\z/, 1]}_pattern".upcase)
|
113
115
|
raise_unless(argument, method, constant.match?(argument.to_s))
|
114
116
|
end
|
115
117
|
|
@@ -140,5 +142,9 @@ module Truemail
|
|
140
142
|
check_validation_type(validation_type)
|
141
143
|
end
|
142
144
|
end
|
145
|
+
|
146
|
+
def logger_options(current_options)
|
147
|
+
Truemail::Configuration::DEFAULT_LOGGER_OPTIONS.merge(current_options).values
|
148
|
+
end
|
143
149
|
end
|
144
150
|
end
|
data/lib/truemail/core.rb
CHANGED
@@ -4,12 +4,14 @@ module Truemail
|
|
4
4
|
require_relative '../truemail/version'
|
5
5
|
require_relative '../truemail/configuration'
|
6
6
|
require_relative '../truemail/worker'
|
7
|
+
require_relative '../truemail/executor'
|
7
8
|
require_relative '../truemail/wrapper'
|
8
9
|
require_relative '../truemail/auditor'
|
9
10
|
require_relative '../truemail/validator'
|
10
11
|
require_relative '../truemail/logger'
|
11
12
|
|
12
13
|
ConfigurationError = Class.new(StandardError)
|
14
|
+
TypeError = Class.new(StandardError)
|
13
15
|
|
14
16
|
ArgumentError = Class.new(StandardError) do
|
15
17
|
def initialize(arg_value, arg_name)
|
@@ -29,8 +31,8 @@ module Truemail
|
|
29
31
|
end
|
30
32
|
|
31
33
|
module RegexConstant
|
32
|
-
REGEX_DOMAIN = /[\p{L}0-9]+([
|
33
|
-
REGEX_EMAIL_PATTERN = /(?=\A.{6,255}\z)(\A([\p{L}0-9]+[\w
|
34
|
+
REGEX_DOMAIN = /[\p{L}0-9]+([\-.]{1}[\p{L}0-9]+)*\.\p{L}{2,63}/i.freeze
|
35
|
+
REGEX_EMAIL_PATTERN = /(?=\A.{6,255}\z)(\A([\p{L}0-9]+[\w|\-|.|+]*)@(#{REGEX_DOMAIN})\z)/.freeze
|
34
36
|
REGEX_DOMAIN_PATTERN = /(?=\A.{4,255}\z)(\A#{REGEX_DOMAIN}\z)/.freeze
|
35
37
|
REGEX_DOMAIN_FROM_EMAIL = /\A.+@(.+)\z/.freeze
|
36
38
|
REGEX_SMTP_ERROR_BODY_PATTERN = /(?=.*550)(?=.*(user|account|customer|mailbox)).*/i.freeze
|
@@ -56,7 +58,9 @@ module Truemail
|
|
56
58
|
module Log
|
57
59
|
require_relative '../truemail/log/event'
|
58
60
|
require_relative '../truemail/log/serializer/base'
|
59
|
-
require_relative '../truemail/log/serializer/
|
60
|
-
require_relative '../truemail/log/serializer/
|
61
|
+
require_relative '../truemail/log/serializer/auditor_json'
|
62
|
+
require_relative '../truemail/log/serializer/validator_base'
|
63
|
+
require_relative '../truemail/log/serializer/validator_text'
|
64
|
+
require_relative '../truemail/log/serializer/validator_json'
|
61
65
|
end
|
62
66
|
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Truemail
|
4
|
+
module Log
|
5
|
+
module Serializer
|
6
|
+
class AuditorJson < Truemail::Log::Serializer::Base
|
7
|
+
def serialize
|
8
|
+
result.to_json
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
def result
|
14
|
+
@result ||=
|
15
|
+
{
|
16
|
+
date: Time.now,
|
17
|
+
current_host_ip: executor_result.current_host_ip,
|
18
|
+
warnings: warnings(executor_result.warnings),
|
19
|
+
configuration: configuration
|
20
|
+
}
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -4,47 +4,35 @@ module Truemail
|
|
4
4
|
module Log
|
5
5
|
module Serializer
|
6
6
|
class Base
|
7
|
+
require 'json'
|
8
|
+
|
7
9
|
DEFAULT_GEM_VALUE = 'default gem value'
|
8
10
|
|
9
|
-
def self.call(
|
10
|
-
new(
|
11
|
+
def self.call(executor_instance)
|
12
|
+
new(executor_instance).serialize
|
11
13
|
end
|
12
14
|
|
13
|
-
def initialize(
|
14
|
-
@
|
15
|
-
@
|
16
|
-
@validation_configuration = validation_result.configuration
|
15
|
+
def initialize(executor_instance)
|
16
|
+
@executor_result = executor_instance.result
|
17
|
+
@executor_configuration = executor_result.configuration
|
17
18
|
end
|
18
19
|
|
19
20
|
def serialize; end
|
20
21
|
|
21
22
|
private
|
22
23
|
|
23
|
-
attr_reader :
|
24
|
+
attr_reader :executor_result, :executor_configuration
|
24
25
|
|
25
|
-
def errors
|
26
|
-
|
27
|
-
|
28
|
-
validation_errors
|
26
|
+
def errors(executor_result_target)
|
27
|
+
return if executor_result_target.empty?
|
28
|
+
executor_result_target
|
29
29
|
end
|
30
30
|
|
31
|
-
|
32
|
-
validation_smtp_debug = validation_result.smtp_debug
|
33
|
-
return unless validation_smtp_debug
|
34
|
-
validation_smtp_debug.map do |smtp_request|
|
35
|
-
smtp_response = smtp_request.response
|
36
|
-
{
|
37
|
-
mail_host: smtp_request.host,
|
38
|
-
port_opened: smtp_response.port_opened,
|
39
|
-
connection: smtp_response.connection,
|
40
|
-
errors: smtp_response.errors
|
41
|
-
}
|
42
|
-
end
|
43
|
-
end
|
31
|
+
alias warnings errors
|
44
32
|
|
45
33
|
%i[validation_type_by_domain whitelisted_domains blacklisted_domains].each do |method|
|
46
34
|
define_method(method) do
|
47
|
-
value =
|
35
|
+
value = executor_configuration.public_send(method)
|
48
36
|
return if value.empty?
|
49
37
|
value
|
50
38
|
end
|
@@ -52,7 +40,7 @@ module Truemail
|
|
52
40
|
|
53
41
|
%i[email_pattern smtp_error_body_pattern].each do |method|
|
54
42
|
define_method(method) do
|
55
|
-
value =
|
43
|
+
value = executor_configuration.public_send(method)
|
56
44
|
default_pattern = Truemail::RegexConstant.const_get(
|
57
45
|
(method.eql?(:email_pattern) ? :regex_email_pattern : :regex_smtp_error_body_pattern).upcase
|
58
46
|
)
|
@@ -64,28 +52,15 @@ module Truemail
|
|
64
52
|
def configuration
|
65
53
|
{
|
66
54
|
validation_type_by_domain: validation_type_by_domain,
|
67
|
-
whitelist_validation:
|
55
|
+
whitelist_validation: executor_configuration.whitelist_validation,
|
68
56
|
whitelisted_domains: whitelisted_domains,
|
69
57
|
blacklisted_domains: blacklisted_domains,
|
70
|
-
not_rfc_mx_lookup_flow:
|
71
|
-
smtp_safe_check:
|
58
|
+
not_rfc_mx_lookup_flow: executor_configuration.not_rfc_mx_lookup_flow,
|
59
|
+
smtp_safe_check: executor_configuration.smtp_safe_check,
|
72
60
|
email_pattern: email_pattern,
|
73
61
|
smtp_error_body_pattern: smtp_error_body_pattern
|
74
62
|
}
|
75
63
|
end
|
76
|
-
|
77
|
-
def result
|
78
|
-
@result ||=
|
79
|
-
{
|
80
|
-
date: Time.now,
|
81
|
-
email: validation_result.email,
|
82
|
-
validation_type: validation_type,
|
83
|
-
success: validation_result.success,
|
84
|
-
errors: errors,
|
85
|
-
smtp_debug: smtp_debug,
|
86
|
-
configuration: configuration
|
87
|
-
}
|
88
|
-
end
|
89
64
|
end
|
90
65
|
end
|
91
66
|
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Truemail
|
4
|
+
module Log
|
5
|
+
module Serializer
|
6
|
+
class ValidatorBase < Truemail::Log::Serializer::Base
|
7
|
+
def initialize(executor_instance)
|
8
|
+
@validation_type = executor_instance.validation_type
|
9
|
+
super
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
attr_reader :validation_type
|
15
|
+
|
16
|
+
def smtp_debug
|
17
|
+
validation_smtp_debug = executor_result.smtp_debug
|
18
|
+
return unless validation_smtp_debug
|
19
|
+
validation_smtp_debug.map do |smtp_request|
|
20
|
+
smtp_response = smtp_request.response
|
21
|
+
{
|
22
|
+
mail_host: smtp_request.host,
|
23
|
+
port_opened: smtp_response.port_opened,
|
24
|
+
connection: smtp_response.connection,
|
25
|
+
errors: smtp_response.errors
|
26
|
+
}
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def result
|
31
|
+
@result ||=
|
32
|
+
{
|
33
|
+
date: Time.now,
|
34
|
+
email: executor_result.email,
|
35
|
+
validation_type: validation_type,
|
36
|
+
success: executor_result.success,
|
37
|
+
errors: errors(executor_result.errors),
|
38
|
+
smtp_debug: smtp_debug,
|
39
|
+
configuration: configuration
|
40
|
+
}
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -3,7 +3,7 @@
|
|
3
3
|
module Truemail
|
4
4
|
module Log
|
5
5
|
module Serializer
|
6
|
-
class
|
6
|
+
class ValidatorText < Truemail::Log::Serializer::ValidatorBase
|
7
7
|
ATTEMPT = 'ATTEMPT #'
|
8
8
|
|
9
9
|
def serialize
|
@@ -30,7 +30,7 @@ module Truemail
|
|
30
30
|
|
31
31
|
def collection_printer(collection)
|
32
32
|
collection.inject([]) { |array, hash| array << printer(hash) }.map.with_index do |item, index|
|
33
|
-
"\n#{Truemail::Log::Serializer::
|
33
|
+
"\n#{Truemail::Log::Serializer::ValidatorText::ATTEMPT}#{index + 1}:\n#{item}\n"
|
34
34
|
end
|
35
35
|
end
|
36
36
|
|
data/lib/truemail/logger.rb
CHANGED
@@ -15,7 +15,7 @@ module Truemail
|
|
15
15
|
def push(validator_instance)
|
16
16
|
current_event = Truemail::Log::Event.new(event, validator_instance)
|
17
17
|
return unless current_event.valid?
|
18
|
-
create_logs(current_event.log_level, Truemail::Log::Serializer::
|
18
|
+
create_logs(current_event.log_level, Truemail::Log::Serializer::ValidatorText.call(validator_instance))
|
19
19
|
end
|
20
20
|
|
21
21
|
private
|
@@ -31,8 +31,8 @@ module Truemail
|
|
31
31
|
end
|
32
32
|
|
33
33
|
def run
|
34
|
-
session.start do |smtp_request|
|
35
|
-
response.connection = true
|
34
|
+
session.start(configuration.verifier_domain) do |smtp_request|
|
35
|
+
response.connection = response.helo = true
|
36
36
|
smtp_handshakes(smtp_request, response)
|
37
37
|
end
|
38
38
|
rescue => error
|
@@ -83,7 +83,6 @@ module Truemail
|
|
83
83
|
|
84
84
|
def session_data
|
85
85
|
{
|
86
|
-
helo: configuration.verifier_domain,
|
87
86
|
mailfrom: configuration.verifier_email,
|
88
87
|
rcptto: email
|
89
88
|
}
|
data/lib/truemail/validator.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Truemail
|
4
|
-
class Validator
|
4
|
+
class Validator < Truemail::Executor
|
5
5
|
RESULT_ATTRS = %i[success email domain mail_servers errors smtp_debug configuration].freeze
|
6
6
|
VALIDATION_TYPES = %i[regex mx smtp].freeze
|
7
7
|
|
@@ -16,9 +16,9 @@ module Truemail
|
|
16
16
|
alias_method :valid?, :success
|
17
17
|
end
|
18
18
|
|
19
|
-
attr_reader :validation_type
|
19
|
+
attr_reader :validation_type
|
20
20
|
|
21
|
-
def initialize(email, with: nil
|
21
|
+
def initialize(email, configuration:, with: nil)
|
22
22
|
with ||= configuration.default_validation_type
|
23
23
|
raise Truemail::ArgumentError.new(with, :argument) unless Truemail::Validator::VALIDATION_TYPES.include?(with)
|
24
24
|
@result = Truemail::Validator::Result.new(email: email, configuration: configuration)
|
@@ -33,7 +33,7 @@ module Truemail
|
|
33
33
|
end
|
34
34
|
|
35
35
|
def as_json
|
36
|
-
Truemail::Log::Serializer::
|
36
|
+
Truemail::Log::Serializer::ValidatorJson.call(self)
|
37
37
|
end
|
38
38
|
|
39
39
|
private
|
data/lib/truemail/version.rb
CHANGED
data/truemail.gemspec
CHANGED
@@ -13,9 +13,17 @@ Gem::Specification.new do |spec|
|
|
13
13
|
spec.summary = %(truemail)
|
14
14
|
spec.description = %(Configurable framework agnostic plain Ruby email validator. Verify email via Regex, DNS and SMTP.)
|
15
15
|
|
16
|
-
spec.homepage = 'https://github.com/
|
16
|
+
spec.homepage = 'https://github.com/truemail-rb/truemail'
|
17
17
|
spec.license = 'MIT'
|
18
18
|
|
19
|
+
spec.metadata = {
|
20
|
+
'homepage_uri' => 'https://truemail-rb.org',
|
21
|
+
'changelog_uri' => 'https://github.com/truemail-rb/truemail/blob/master/CHANGELOG.md',
|
22
|
+
'source_code_uri' => 'https://github.com/truemail-rb/truemail',
|
23
|
+
'documentation_uri' => 'https://truemail-rb.org/truemail-gem',
|
24
|
+
'bug_tracker_uri' => 'https://github.com/truemail-rb/truemail/issues'
|
25
|
+
}
|
26
|
+
|
19
27
|
spec.required_ruby_version = '>= 2.5.0'
|
20
28
|
|
21
29
|
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
@@ -26,18 +34,18 @@ Gem::Specification.new do |spec|
|
|
26
34
|
spec.add_runtime_dependency 'simpleidn', '~> 0.1.1'
|
27
35
|
|
28
36
|
spec.add_development_dependency 'bundler', '~> 1.16'
|
29
|
-
spec.add_development_dependency 'bundler-audit', '~> 0.
|
37
|
+
spec.add_development_dependency 'bundler-audit', '~> 0.7.0.1'
|
30
38
|
spec.add_development_dependency 'fasterer', '~> 0.8.3'
|
31
|
-
spec.add_development_dependency 'ffaker', '~> 2.
|
39
|
+
spec.add_development_dependency 'ffaker', '~> 2.17'
|
32
40
|
spec.add_development_dependency 'json_matchers', '~> 0.11.1'
|
33
|
-
spec.add_development_dependency 'overcommit', '~> 0.
|
41
|
+
spec.add_development_dependency 'overcommit', '~> 0.57.0'
|
34
42
|
spec.add_development_dependency 'pry-byebug', '~> 3.9'
|
35
43
|
spec.add_development_dependency 'rake', '~> 13.0', '>= 13.0.1'
|
36
|
-
spec.add_development_dependency 'reek', '~> 6.0'
|
44
|
+
spec.add_development_dependency 'reek', '~> 6.0', '>= 6.0.2'
|
37
45
|
spec.add_development_dependency 'rspec', '~> 3.9'
|
38
|
-
spec.add_development_dependency 'rubocop', '~> 0.
|
39
|
-
spec.add_development_dependency 'rubocop-performance', '~> 1.
|
40
|
-
spec.add_development_dependency 'rubocop-rspec', '~> 1.
|
46
|
+
spec.add_development_dependency 'rubocop', '~> 0.93.1'
|
47
|
+
spec.add_development_dependency 'rubocop-performance', '~> 1.8', '>= 1.8.1'
|
48
|
+
spec.add_development_dependency 'rubocop-rspec', '~> 1.43', '>= 1.43.2'
|
41
49
|
spec.add_development_dependency 'simplecov', '~> 0.17.1'
|
42
|
-
spec.add_development_dependency 'truemail-rspec', '~> 0.1
|
50
|
+
spec.add_development_dependency 'truemail-rspec', '~> 0.2.1'
|
43
51
|
end
|