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.
@@ -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
- :read_timeout => nil,
11
- :open_timeout => nil,
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?(TimeoutError))
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
- raise MissingFieldsError, "No destinations found! You need to pass a :to field." if mail.destinations.empty?
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("Unknown host: #{domain}"), attempt) && next if servers.empty?
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
- raw_mail = use_dkim? ? Dkim.sign(mail.to_s).to_s : mail.to_s
120
- from = mail.return_path || mail.sender || mail.from_addrs.first
121
- helo_domain = Dkim::domain || config[:helo_domain] || get_domain(from)
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
@@ -1,7 +1,7 @@
1
1
  module Tuktuk
2
2
  MAJOR = 0
3
- MINOR = 3
4
- PATCH = 1
3
+ MINOR = 4
4
+ PATCH = 4
5
5
 
6
6
  VERSION = [MAJOR, MINOR, PATCH].join('.')
7
7
  end
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.3.1
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: 2012-11-07 00:00:00.000000000 Z
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