tuktuk 0.3.1 → 0.4.4
Sign up to get free protection for your applications and to get access to all the features.
- 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
|