tuktuk 0.3.1 → 0.4.4

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