tuktuk 0.3.1 → 0.4.4
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.
- data/lib/tuktuk/package.rb +7 -1
- data/lib/tuktuk/tuktuk.rb +155 -25
- data/lib/tuktuk/version.rb +2 -2
- data/tuktuk.gemspec +1 -0
- metadata +18 -2
data/lib/tuktuk/package.rb
CHANGED
@@ -1,16 +1,22 @@
|
|
1
1
|
require 'mail'
|
2
2
|
|
3
|
+
class Mail::Message
|
4
|
+
attr_accessor :array_index
|
5
|
+
end
|
6
|
+
|
3
7
|
module Package
|
4
8
|
|
5
9
|
class << self
|
6
10
|
|
7
|
-
def new(message)
|
11
|
+
def new(message, index = nil)
|
8
12
|
mail = message[:html_body] ? mixed(message) : plain(message)
|
13
|
+
mail.array_index = index if index
|
9
14
|
mail.charset = 'UTF-8'
|
10
15
|
|
11
16
|
mail['In-Reply-To'] = message[:in_reply_to] if message[:in_reply_to]
|
12
17
|
mail['List-Archive'] = message[:list_archive] if message[:list_archive]
|
13
18
|
mail['List-Id'] = message[:list_id] if message[:list_id]
|
19
|
+
mail['X-Mailer'] = "Tuktuk SMTP v#{Tuktuk::VERSION}"
|
14
20
|
|
15
21
|
if message[:return_path]
|
16
22
|
mail['Return-Path'] = message[:return_path]
|
data/lib/tuktuk/tuktuk.rb
CHANGED
@@ -1,14 +1,17 @@
|
|
1
1
|
require 'net/smtp'
|
2
2
|
require 'dkim'
|
3
3
|
require 'logger'
|
4
|
+
require 'work_queue'
|
5
|
+
|
4
6
|
%w(package cache dns).each { |lib| require "tuktuk/#{lib}" }
|
5
7
|
require 'tuktuk/version' unless defined?(Tuktuk::VERSION)
|
6
8
|
|
7
9
|
DEFAULTS = {
|
8
10
|
:retry_sleep => 10,
|
9
11
|
:max_attempts => 3,
|
10
|
-
:
|
11
|
-
:
|
12
|
+
:max_workers => 0,
|
13
|
+
:read_timeout => 20,
|
14
|
+
:open_timeout => 20,
|
12
15
|
:verify_ssl => true,
|
13
16
|
:log_to => nil # $stdout,
|
14
17
|
}
|
@@ -27,11 +30,16 @@ module Tuktuk
|
|
27
30
|
def deliver(message, opts = {})
|
28
31
|
self.options = opts if opts.any?
|
29
32
|
mail = Package.new(message)
|
30
|
-
mail['X-Mailer'] = "Tuktuk SMTP v#{VERSION}"
|
31
33
|
response = lookup_and_deliver(mail)
|
32
34
|
return response, mail
|
33
35
|
end
|
34
36
|
|
37
|
+
def deliver_many(messages, opts = {})
|
38
|
+
self.options = opts if opts.any?
|
39
|
+
messages_by_domain = reorder_by_domain(messages)
|
40
|
+
lookup_and_deliver_many(messages_by_domain)
|
41
|
+
end
|
42
|
+
|
35
43
|
def options=(hash)
|
36
44
|
if dkim_opts = hash.delete(:dkim)
|
37
45
|
self.dkim = dkim_opts
|
@@ -59,16 +67,12 @@ module Tuktuk
|
|
59
67
|
@logger ||= Logger.new(config[:log_to])
|
60
68
|
end
|
61
69
|
|
62
|
-
def get_domain(email_address)
|
63
|
-
email_address && email_address.to_s[/@([a-z0-9\._-]+)/i, 1]
|
64
|
-
end
|
65
|
-
|
66
70
|
def success(to)
|
67
71
|
logger.info("#{to} - Successfully sent!")
|
68
72
|
end
|
69
73
|
|
70
74
|
def error(mail, to, error, attempt)
|
71
|
-
if attempt < config[:max_attempts] and (error.is_a?(EOFError) || error.is_a?(
|
75
|
+
if attempt < config[:max_attempts] and (error.is_a?(EOFError) || error.is_a?(Timeout::Error))
|
72
76
|
logger.info "#{to} - Got #{error.class.name} error. Retrying after #{config[:retry_sleep]} secs..."
|
73
77
|
sleep config[:retry_sleep]
|
74
78
|
lookup_and_deliver(mail, attempt+1)
|
@@ -78,26 +82,44 @@ module Tuktuk
|
|
78
82
|
end
|
79
83
|
end
|
80
84
|
|
85
|
+
def get_domain(email_address)
|
86
|
+
email_address && email_address.to_s[/@([a-z0-9\._-]+)/i, 1]
|
87
|
+
end
|
88
|
+
|
89
|
+
def reorder_by_domain(array)
|
90
|
+
hash = {}
|
91
|
+
array.each_with_index do |message, i|
|
92
|
+
mail = Package.new(message, i)
|
93
|
+
raise "Invalid destination count: #{mail.destinations.count}" if mail.destinations.count != 1
|
94
|
+
|
95
|
+
if to = mail.destinations.first and domain = get_domain(to)
|
96
|
+
hash[domain] = [] if hash[domain].nil?
|
97
|
+
hash[domain].push(mail)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
hash
|
101
|
+
end
|
102
|
+
|
81
103
|
def smtp_servers_for_domain(domain)
|
82
104
|
unless servers = cache.get(domain)
|
83
105
|
if servers = DNS.get_mx(domain) and servers.any?
|
84
106
|
cache.set(domain, servers)
|
85
|
-
else
|
86
|
-
raise DNSError, "No MX records found for domain #{domain}."
|
87
107
|
end
|
88
108
|
end
|
89
|
-
servers
|
109
|
+
servers.any? && servers
|
90
110
|
end
|
91
111
|
|
92
112
|
def lookup_and_deliver(mail, attempt = 1)
|
93
|
-
|
113
|
+
if mail.destinations.empty?
|
114
|
+
raise MissingFieldsError, "No destinations found! You need to pass a :to field."
|
115
|
+
end
|
94
116
|
|
95
117
|
response = nil
|
96
118
|
mail.destinations.each do |to|
|
97
119
|
|
98
120
|
domain = get_domain(to)
|
99
121
|
servers = smtp_servers_for_domain(domain)
|
100
|
-
error(mail, to, DNSError.new("
|
122
|
+
error(mail, to, DNSError.new("No MX records for domain #{domain}"), attempt) && next if servers.empty?
|
101
123
|
|
102
124
|
last_error = nil
|
103
125
|
servers.each do |server|
|
@@ -113,13 +135,129 @@ module Tuktuk
|
|
113
135
|
response
|
114
136
|
end
|
115
137
|
|
138
|
+
def lookup_and_deliver_many(by_domain)
|
139
|
+
if config[:max_workers] && config[:max_workers] > 0
|
140
|
+
lookup_and_deliver_many_threaded(by_domain)
|
141
|
+
else
|
142
|
+
lookup_and_deliver_many_sync(by_domain)
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
def lookup_and_deliver_many_threaded(by_domain)
|
147
|
+
queue = WorkQueue.new(config[:max_workers])
|
148
|
+
responses = []
|
149
|
+
|
150
|
+
by_domain.each do |domain, mails|
|
151
|
+
queue.enqueue_b(domain, mails) do |domain, mails|
|
152
|
+
# send emails and then assign responses to array according to mail index
|
153
|
+
rr = lookup_and_deliver_by_domain(domain, mails)
|
154
|
+
rr.each do |resp, mail|
|
155
|
+
responses[mail.array_index] = [resp, mail]
|
156
|
+
end
|
157
|
+
end # worker
|
158
|
+
end
|
159
|
+
|
160
|
+
queue.join
|
161
|
+
responses
|
162
|
+
end
|
163
|
+
|
164
|
+
def lookup_and_deliver_many_sync(by_domain)
|
165
|
+
responses = []
|
166
|
+
by_domain.each do |domain, mails|
|
167
|
+
# send emails and then assign responses to array according to mail index
|
168
|
+
rr = lookup_and_deliver_by_domain(domain, mails)
|
169
|
+
rr.each do |resp, mail|
|
170
|
+
responses[mail.array_index] = [resp, mail]
|
171
|
+
end
|
172
|
+
end
|
173
|
+
responses
|
174
|
+
end
|
175
|
+
|
176
|
+
def lookup_and_deliver_by_domain(domain, mails)
|
177
|
+
responses = []
|
178
|
+
|
179
|
+
unless servers = smtp_servers_for_domain(domain)
|
180
|
+
err = DNSError.new("No MX Records for domain #{domain}")
|
181
|
+
mails.each {|mail| responses.push [err, mail] }
|
182
|
+
return responses
|
183
|
+
end
|
184
|
+
|
185
|
+
last_error = nil
|
186
|
+
servers.each do |server|
|
187
|
+
begin
|
188
|
+
send_many_now(server, mails).each do |mail, resp|
|
189
|
+
responses.push [resp, mail]
|
190
|
+
end
|
191
|
+
break
|
192
|
+
rescue => e
|
193
|
+
# logger.error e.message
|
194
|
+
last_error = e
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
if last_error # got error at server level, mark all messages with errors
|
199
|
+
mails.each {|mail| responses.push [last_error, mail] }
|
200
|
+
end
|
201
|
+
|
202
|
+
responses
|
203
|
+
end
|
204
|
+
|
116
205
|
def send_now(mail, server, to)
|
117
206
|
logger.info "#{to} - Delivering email at #{server}..."
|
207
|
+
from = get_from(mail)
|
208
|
+
|
209
|
+
response = nil
|
210
|
+
server = 'localhost' if ENV['DEBUG']
|
211
|
+
smtp = init_connection(server)
|
212
|
+
smtp.start(get_helo_domain(from), nil, nil, nil) do |smtp|
|
213
|
+
response = smtp.send_message(get_raw_mail(mail), from, to)
|
214
|
+
logger.info "#{to} - [SENT] #{response.message.strip}"
|
215
|
+
end
|
216
|
+
|
217
|
+
success(to)
|
218
|
+
response
|
219
|
+
end
|
220
|
+
|
221
|
+
def send_many_now(server, mails)
|
222
|
+
logger.info "Delivering #{mails.count} mails at #{server}..."
|
223
|
+
responses = {}
|
224
|
+
timeout_error = nil
|
118
225
|
|
119
|
-
|
120
|
-
|
121
|
-
|
226
|
+
server = 'localhost' if ENV['DEBUG']
|
227
|
+
smtp = init_connection(server)
|
228
|
+
smtp.start(get_helo_domain, nil, nil, nil) do |smtp|
|
229
|
+
mails.each do |mail|
|
230
|
+
unless timeout_error
|
231
|
+
begin
|
232
|
+
resp = smtp.send_message(get_raw_mail(mail), get_from(mail), mail.to)
|
233
|
+
rescue Net::SMTPError, EOFError, Timeout::Error => e # may be Net::SMTPFatalError (550 Mailbox not found)
|
234
|
+
# logger.error e.inspect
|
235
|
+
timeout_error = e if e.is_a?(Timeout::Error)
|
236
|
+
resp = e
|
237
|
+
end
|
238
|
+
end
|
239
|
+
responses[mail] = timeout_error || resp
|
240
|
+
status = resp.is_a?(Net::SMTP::Response) ? 'SENT' : 'ERROR'
|
241
|
+
logger.info "#{mail.to} [#{status}] #{responses[mail].message.strip}" # both error and response have this method
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
responses
|
246
|
+
end
|
247
|
+
|
248
|
+
def get_raw_mail(mail)
|
249
|
+
use_dkim? ? Dkim.sign(mail.to_s).to_s : mail.to_s
|
250
|
+
end
|
251
|
+
|
252
|
+
def get_from(mail)
|
253
|
+
mail.return_path || mail.sender || mail.from_addrs.first
|
254
|
+
end
|
122
255
|
|
256
|
+
def get_helo_domain(from = nil)
|
257
|
+
Dkim::domain || config[:helo_domain] || (from && get_domain(from))
|
258
|
+
end
|
259
|
+
|
260
|
+
def init_connection(server)
|
123
261
|
context = OpenSSL::SSL::SSLContext.new
|
124
262
|
context.verify_mode = config[:verify_ssl] ?
|
125
263
|
OpenSSL::SSL::VERIFY_PEER : OpenSSL::SSL::VERIFY_NONE
|
@@ -128,15 +266,7 @@ module Tuktuk
|
|
128
266
|
smtp.enable_starttls_auto(context)
|
129
267
|
smtp.read_timeout = config[:read_timeout] if config[:read_timeout]
|
130
268
|
smtp.open_timeout = config[:open_timeout] if config[:open_timeout]
|
131
|
-
|
132
|
-
response = nil
|
133
|
-
smtp.start(helo_domain, nil, nil, nil) do |smtp|
|
134
|
-
response = smtp.send_message(raw_mail, from, to)
|
135
|
-
logger.info "#{to} - #{response.message.strip}"
|
136
|
-
end
|
137
|
-
|
138
|
-
success(to)
|
139
|
-
response
|
269
|
+
smtp
|
140
270
|
end
|
141
271
|
|
142
272
|
end
|
data/lib/tuktuk/version.rb
CHANGED
data/tuktuk.gemspec
CHANGED
@@ -18,6 +18,7 @@ Gem::Specification.new do |s|
|
|
18
18
|
s.add_runtime_dependency "net-dns", "= 0.6.1"
|
19
19
|
s.add_runtime_dependency "mail", "~> 2.3"
|
20
20
|
s.add_runtime_dependency "dkim", "~> 0.0.2"
|
21
|
+
s.add_runtime_dependency 'work_queue', '~> 2.5.0'
|
21
22
|
|
22
23
|
s.files = `git ls-files`.split("\n")
|
23
24
|
s.executables = `git ls-files`.split("\n").map{|f| f =~ /^bin\/(.*)/ ? $1 : nil}.compact
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: tuktuk
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.4.4
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date:
|
12
|
+
date: 2013-02-22 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: bundler
|
@@ -75,6 +75,22 @@ dependencies:
|
|
75
75
|
- - ~>
|
76
76
|
- !ruby/object:Gem::Version
|
77
77
|
version: 0.0.2
|
78
|
+
- !ruby/object:Gem::Dependency
|
79
|
+
name: work_queue
|
80
|
+
requirement: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ~>
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: 2.5.0
|
86
|
+
type: :runtime
|
87
|
+
prerelease: false
|
88
|
+
version_requirements: !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - ~>
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: 2.5.0
|
78
94
|
description: Easy way of sending DKIM-signed emails from Ruby, no dependencies needed.
|
79
95
|
email:
|
80
96
|
- tomas@forkhq.com
|