tuktuk 0.4.5 → 0.4.6
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/tuktuk.rb +29 -26
- data/lib/tuktuk/version.rb +1 -1
- data/spec/deliver_spec.rb +189 -0
- metadata +79 -98
data/lib/tuktuk/tuktuk.rb
CHANGED
@@ -29,6 +29,7 @@ module Tuktuk
|
|
29
29
|
end
|
30
30
|
|
31
31
|
def deliver(message, opts = {})
|
32
|
+
raise 'Please pass a valid message object.' unless messages.is_a?(Hash)
|
32
33
|
self.options = opts if opts.any?
|
33
34
|
mail = Package.new(message)
|
34
35
|
response = lookup_and_deliver(mail)
|
@@ -36,6 +37,7 @@ module Tuktuk
|
|
36
37
|
end
|
37
38
|
|
38
39
|
def deliver_many(messages, opts = {})
|
40
|
+
raise 'Please pass an array of messages.' unless messages.is_a?(Array) and messages.any?
|
39
41
|
self.options = opts if opts.any?
|
40
42
|
messages_by_domain = reorder_by_domain(messages)
|
41
43
|
lookup_and_deliver_many(messages_by_domain)
|
@@ -150,6 +152,7 @@ module Tuktuk
|
|
150
152
|
queue = WorkQueue.new(count)
|
151
153
|
responses = []
|
152
154
|
|
155
|
+
logger.info("Delivering emails to #{by_domain.keys.count} domains...")
|
153
156
|
by_domain.each do |domain, mails|
|
154
157
|
queue.enqueue_b(domain, mails) do |domain, mails|
|
155
158
|
# send emails and then assign responses to array according to mail index
|
@@ -167,6 +170,8 @@ module Tuktuk
|
|
167
170
|
|
168
171
|
def lookup_and_deliver_many_sync(by_domain)
|
169
172
|
responses = []
|
173
|
+
|
174
|
+
logger.info("Delivering emails to #{by_domain.keys.count} domains...")
|
170
175
|
by_domain.each do |domain, mails|
|
171
176
|
# send emails and then assign responses to array according to mail index
|
172
177
|
rr = lookup_and_deliver_by_domain(domain, mails)
|
@@ -179,28 +184,26 @@ module Tuktuk
|
|
179
184
|
|
180
185
|
def lookup_and_deliver_by_domain(domain, mails)
|
181
186
|
responses = []
|
187
|
+
total = mails.count
|
182
188
|
|
183
189
|
unless servers = smtp_servers_for_domain(domain)
|
184
190
|
err = DNSError.new("No MX Records for domain #{domain}")
|
185
|
-
mails.each {|mail| responses.push [err, mail] }
|
191
|
+
mails.each { |mail| responses.push [err, mail] }
|
186
192
|
return responses
|
187
193
|
end
|
188
194
|
|
189
|
-
last_error = nil
|
190
195
|
servers.each do |server|
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
end
|
195
|
-
break
|
196
|
-
rescue => e
|
197
|
-
# logger.error e.message
|
198
|
-
last_error = e
|
196
|
+
send_many_now(server, mails).each do |mail, resp|
|
197
|
+
responses.push [resp, mail]
|
198
|
+
mails.delete(mail) # remove it from list, to avoid duplicate delivery
|
199
199
|
end
|
200
|
+
logger.info "#{responses.count}/#{total} mails processed on #{domain}."
|
201
|
+
break if responses.count == total
|
200
202
|
end
|
201
203
|
|
202
|
-
if
|
203
|
-
|
204
|
+
# if we still have emails in queue, mark them with the last error which prevented delivery
|
205
|
+
if mails.any? and @last_error
|
206
|
+
mails.each { |m| responses.push [@last_error, m] }
|
204
207
|
end
|
205
208
|
|
206
209
|
responses
|
@@ -212,8 +215,8 @@ module Tuktuk
|
|
212
215
|
|
213
216
|
response = nil
|
214
217
|
server = 'localhost' if ENV['DEBUG']
|
215
|
-
|
216
|
-
|
218
|
+
socket = init_connection(server)
|
219
|
+
socket.start(get_helo_domain(from), nil, nil, nil) do |smtp|
|
217
220
|
response = smtp.send_message(get_raw_mail(mail), from, to)
|
218
221
|
logger.info "#{to} - [SENT] #{response.message.strip}"
|
219
222
|
end
|
@@ -228,26 +231,26 @@ module Tuktuk
|
|
228
231
|
timeout_error = nil
|
229
232
|
|
230
233
|
server = 'localhost' if ENV['DEBUG']
|
231
|
-
|
232
|
-
|
234
|
+
socket = init_connection(server)
|
235
|
+
socket.start(get_helo_domain, nil, nil, nil) do |smtp|
|
233
236
|
mails.each do |mail|
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
# logger.error e.inspect
|
240
|
-
timeout_error = e if e.is_a?(Timeout::Error)
|
241
|
-
resp = e
|
242
|
-
end
|
237
|
+
begin
|
238
|
+
resp = smtp.send_message(get_raw_mail(mail), get_from(mail), mail.to)
|
239
|
+
smtp.send(:getok, 'RSET') if server['hotmail'] # fix for '503 Sender already specified'
|
240
|
+
rescue Net::SMTPFatalError => e # error code 5xx, except for 500, like: 550 Mailbox not found
|
241
|
+
resp = e
|
243
242
|
end
|
244
|
-
responses[mail] =
|
243
|
+
responses[mail] = resp
|
245
244
|
status = resp.is_a?(Net::SMTP::Response) ? 'SENT' : 'ERROR'
|
246
245
|
logger.info "#{mail.to} [#{status}] #{responses[mail].message.strip}" # both error and response have this method
|
247
246
|
end
|
248
247
|
end
|
249
248
|
|
250
249
|
responses
|
250
|
+
rescue => e # SMTPServerBusy, SMTPSyntaxError, SMTPUnsupportedCommand, SMTPUnknownError (unexpected reply code)
|
251
|
+
logger.error "[SERVER ERROR: #{server}] #{e.message}"
|
252
|
+
@last_error = e
|
253
|
+
responses
|
251
254
|
end
|
252
255
|
|
253
256
|
def get_raw_mail(mail)
|
data/lib/tuktuk/version.rb
CHANGED
@@ -0,0 +1,189 @@
|
|
1
|
+
require './lib/tuktuk/tuktuk'
|
2
|
+
require 'rspec/mocks'
|
3
|
+
|
4
|
+
describe 'deliver' do
|
5
|
+
|
6
|
+
end
|
7
|
+
|
8
|
+
describe 'deliver many' do
|
9
|
+
|
10
|
+
before(:each) do
|
11
|
+
@mock_smtp = mock('Net::SMTP')
|
12
|
+
Net::SMTP.stub!(:new).and_return(@mock_smtp)
|
13
|
+
end
|
14
|
+
|
15
|
+
describe 'when no emails are passed' do
|
16
|
+
|
17
|
+
it 'raises' do
|
18
|
+
lambda do
|
19
|
+
Tuktuk.deliver_many []
|
20
|
+
end.should raise_error
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
describe 'when one email contains multiple addresses' do
|
26
|
+
|
27
|
+
it 'raises' do
|
28
|
+
lambda do
|
29
|
+
Tuktuk.deliver_many [ email, email(:to => 'one@user.com, two@user.com')]
|
30
|
+
end.should raise_error
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
|
35
|
+
describe 'when emails are valid' do
|
36
|
+
|
37
|
+
it 'groups them by domain' do
|
38
|
+
|
39
|
+
end
|
40
|
+
|
41
|
+
describe 'and max_workers is 0' do
|
42
|
+
|
43
|
+
it 'does not start any threads' do
|
44
|
+
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
|
49
|
+
describe 'and max_workers is >0' do
|
50
|
+
|
51
|
+
it 'does not spawn any more threads than the max allowed' do
|
52
|
+
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
|
57
|
+
describe 'and max workers is auto' do
|
58
|
+
|
59
|
+
it 'spawns a new thread for each domain' do
|
60
|
+
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
|
65
|
+
describe 'when delivering to domain' do
|
66
|
+
|
67
|
+
before do
|
68
|
+
@mock_smtp.stub!(:start).and_yield()
|
69
|
+
@emails = [email, email, email]
|
70
|
+
|
71
|
+
@success = mock('Net::SMTP::Response')
|
72
|
+
@soft_email_bounce = Net::SMTPFatalError.new('503 Sender already specified')
|
73
|
+
@hard_email_bounce = Net::SMTPFatalError.new('505 Mailbox not found')
|
74
|
+
@soft_server_bounce = Net::SMTPServerBusy.new('Be back in a sec')
|
75
|
+
@hard_server_bounce = Tuktuk::DNSError.new('No MX records found.')
|
76
|
+
end
|
77
|
+
|
78
|
+
describe 'when domain exists' do
|
79
|
+
|
80
|
+
before do
|
81
|
+
@domain = 'domain.com'
|
82
|
+
end
|
83
|
+
|
84
|
+
describe 'and has valid MX servers' do
|
85
|
+
|
86
|
+
before do
|
87
|
+
@servers = ['mx1.domain.com', 'mx2.domain.com', 'mx3.domain.com']
|
88
|
+
Tuktuk.stub!(:smtp_servers_for_domain).and_return(@servers)
|
89
|
+
end
|
90
|
+
|
91
|
+
it 'starts by delivering to first one' do
|
92
|
+
Tuktuk.should_receive(:send_many_now).once.with('mx1.domain.com', [1]).and_return([[1,'ok']])
|
93
|
+
Tuktuk.send(:lookup_and_deliver_by_domain, 'domain.com', [1])
|
94
|
+
end
|
95
|
+
|
96
|
+
describe 'and first server processes all our mail' do
|
97
|
+
|
98
|
+
describe 'and all mail goes through' do
|
99
|
+
|
100
|
+
before do
|
101
|
+
@responses = []
|
102
|
+
@emails.each { |e| @responses.push [e, @success] }
|
103
|
+
end
|
104
|
+
|
105
|
+
it 'does not try to connect to second server' do
|
106
|
+
Tuktuk.should_receive(:send_many_now).once.with('mx1.domain.com', @emails).and_return(@responses)
|
107
|
+
Tuktuk.should_not_receive(:send_many_now).with('mx2.domain.com')
|
108
|
+
Tuktuk.send(:lookup_and_deliver_by_domain, 'domain.com', @emails)
|
109
|
+
end
|
110
|
+
|
111
|
+
end
|
112
|
+
|
113
|
+
describe 'and all emails were hard failures (bounces)' do
|
114
|
+
|
115
|
+
before do
|
116
|
+
@responses = []
|
117
|
+
@emails.each { |e| @responses.push [e, @hard_email_bounce] }
|
118
|
+
end
|
119
|
+
|
120
|
+
it 'does not try to connect to second server' do
|
121
|
+
Tuktuk.should_receive(:send_many_now).once.with('mx1.domain.com', @emails).and_return(@responses)
|
122
|
+
Tuktuk.should_not_receive(:send_many_now).with('mx2.domain.com')
|
123
|
+
Tuktuk.send(:lookup_and_deliver_by_domain, 'domain.com', @emails)
|
124
|
+
end
|
125
|
+
|
126
|
+
end
|
127
|
+
|
128
|
+
end
|
129
|
+
|
130
|
+
describe 'and first server is down' do
|
131
|
+
|
132
|
+
before do
|
133
|
+
Tuktuk.stub(:init_connection).and_return(@mock_smtp)
|
134
|
+
Tuktuk.stub(:init_connection).with('mx1.domain.com').and_raise('Unable to connect.')
|
135
|
+
@responses = []
|
136
|
+
@emails.each { |e| @responses.push [e, @success] }
|
137
|
+
end
|
138
|
+
|
139
|
+
it 'does not raise error' do
|
140
|
+
lambda do
|
141
|
+
Tuktuk.send(:lookup_and_deliver_by_domain, 'domain.com', @emails)
|
142
|
+
end.should_not raise_error(RuntimeError)
|
143
|
+
end
|
144
|
+
|
145
|
+
it 'tries to connect to second server' do
|
146
|
+
Tuktuk.should_receive(:send_many_now).once.with('mx1.domain.com', @emails).and_return([])
|
147
|
+
Tuktuk.should_receive(:send_many_now).once.with('mx2.domain.com', @emails).and_return(@responses)
|
148
|
+
Tuktuk.should_not_receive(:send_many_now).with('mx3.domain.com')
|
149
|
+
Tuktuk.send(:lookup_and_deliver_by_domain, 'domain.com', @emails)
|
150
|
+
end
|
151
|
+
|
152
|
+
end
|
153
|
+
|
154
|
+
describe 'and first server receives only one email' do
|
155
|
+
|
156
|
+
before do
|
157
|
+
@first = [@emails[0], @success]
|
158
|
+
@last_two = [[@emails[1], @success], [@emails[2], @soft_email_bounce]]
|
159
|
+
end
|
160
|
+
|
161
|
+
it 'does not try to send that same email to second server' do
|
162
|
+
Tuktuk.should_receive(:send_many_now).once.with('mx1.domain.com', @emails).and_return([@first])
|
163
|
+
last_two_emails = @emails.last(2)
|
164
|
+
last_two_emails.include?(@emails.first).should be_false
|
165
|
+
Tuktuk.should_receive(:send_many_now).once.with('mx2.domain.com', last_two_emails).and_return(@last_two)
|
166
|
+
Tuktuk.should_not_receive(:send_many_now).with('mx3.domain.com')
|
167
|
+
Tuktuk.send(:lookup_and_deliver_by_domain, 'domain.com', @emails)
|
168
|
+
end
|
169
|
+
|
170
|
+
end
|
171
|
+
|
172
|
+
end
|
173
|
+
|
174
|
+
end
|
175
|
+
|
176
|
+
end
|
177
|
+
|
178
|
+
end
|
179
|
+
|
180
|
+
def email(attrs = {})
|
181
|
+
{
|
182
|
+
:to => "user#{rand(1000)}@domain.com",
|
183
|
+
:from => 'me@company.com',
|
184
|
+
:subject => 'Test email',
|
185
|
+
:body => 'Hello world.'
|
186
|
+
}.merge(attrs)
|
187
|
+
end
|
188
|
+
|
189
|
+
end
|
metadata
CHANGED
@@ -1,111 +1,103 @@
|
|
1
|
-
--- !ruby/object:Gem::Specification
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
2
|
name: tuktuk
|
3
|
-
version: !ruby/object:Gem::Version
|
4
|
-
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.4.6
|
5
5
|
prerelease:
|
6
|
-
segments:
|
7
|
-
- 0
|
8
|
-
- 4
|
9
|
-
- 5
|
10
|
-
version: 0.4.5
|
11
6
|
platform: ruby
|
12
|
-
authors:
|
13
|
-
-
|
7
|
+
authors:
|
8
|
+
- Tomás Pollak
|
14
9
|
autorequire:
|
15
10
|
bindir: bin
|
16
11
|
cert_chain: []
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
- !ruby/object:Gem::Dependency
|
12
|
+
date: 2013-05-24 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
21
15
|
name: bundler
|
22
|
-
|
23
|
-
requirement: &id001 !ruby/object:Gem::Requirement
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
24
17
|
none: false
|
25
|
-
requirements:
|
26
|
-
- -
|
27
|
-
- !ruby/object:Gem::Version
|
28
|
-
hash: 23
|
29
|
-
segments:
|
30
|
-
- 1
|
31
|
-
- 0
|
32
|
-
- 0
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
33
21
|
version: 1.0.0
|
34
22
|
type: :development
|
35
|
-
version_requirements: *id001
|
36
|
-
- !ruby/object:Gem::Dependency
|
37
|
-
name: net-dns
|
38
23
|
prerelease: false
|
39
|
-
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: 1.0.0
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: net-dns
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
40
33
|
none: false
|
41
|
-
requirements:
|
42
|
-
- -
|
43
|
-
- !ruby/object:Gem::Version
|
44
|
-
hash: 5
|
45
|
-
segments:
|
46
|
-
- 0
|
47
|
-
- 6
|
48
|
-
- 1
|
34
|
+
requirements:
|
35
|
+
- - '='
|
36
|
+
- !ruby/object:Gem::Version
|
49
37
|
version: 0.6.1
|
50
38
|
type: :runtime
|
51
|
-
version_requirements: *id002
|
52
|
-
- !ruby/object:Gem::Dependency
|
53
|
-
name: mail
|
54
39
|
prerelease: false
|
55
|
-
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
56
41
|
none: false
|
57
|
-
requirements:
|
42
|
+
requirements:
|
43
|
+
- - '='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: 0.6.1
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: mail
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
58
51
|
- - ~>
|
59
|
-
- !ruby/object:Gem::Version
|
60
|
-
|
61
|
-
segments:
|
62
|
-
- 2
|
63
|
-
- 3
|
64
|
-
version: "2.3"
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '2.3'
|
65
54
|
type: :runtime
|
66
|
-
version_requirements: *id003
|
67
|
-
- !ruby/object:Gem::Dependency
|
68
|
-
name: dkim
|
69
55
|
prerelease: false
|
70
|
-
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ~>
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '2.3'
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: dkim
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
71
65
|
none: false
|
72
|
-
requirements:
|
66
|
+
requirements:
|
73
67
|
- - ~>
|
74
|
-
- !ruby/object:Gem::Version
|
75
|
-
hash: 27
|
76
|
-
segments:
|
77
|
-
- 0
|
78
|
-
- 0
|
79
|
-
- 2
|
68
|
+
- !ruby/object:Gem::Version
|
80
69
|
version: 0.0.2
|
81
70
|
type: :runtime
|
82
|
-
version_requirements: *id004
|
83
|
-
- !ruby/object:Gem::Dependency
|
84
|
-
name: work_queue
|
85
71
|
prerelease: false
|
86
|
-
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
87
73
|
none: false
|
88
|
-
requirements:
|
74
|
+
requirements:
|
89
75
|
- - ~>
|
90
|
-
- !ruby/object:Gem::Version
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
76
|
+
- !ruby/object:Gem::Version
|
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
|
96
85
|
version: 2.5.0
|
97
86
|
type: :runtime
|
98
|
-
|
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
|
99
94
|
description: Easy way of sending DKIM-signed emails from Ruby, no dependencies needed.
|
100
|
-
email:
|
95
|
+
email:
|
101
96
|
- tomas@forkhq.com
|
102
97
|
executables: []
|
103
|
-
|
104
98
|
extensions: []
|
105
|
-
|
106
99
|
extra_rdoc_files: []
|
107
|
-
|
108
|
-
files:
|
100
|
+
files:
|
109
101
|
- .gitignore
|
110
102
|
- Gemfile
|
111
103
|
- README.md
|
@@ -116,41 +108,30 @@ files:
|
|
116
108
|
- lib/tuktuk/package.rb
|
117
109
|
- lib/tuktuk/tuktuk.rb
|
118
110
|
- lib/tuktuk/version.rb
|
111
|
+
- spec/deliver_spec.rb
|
119
112
|
- tuktuk.gemspec
|
120
113
|
homepage: https://github.com/tomas/tuktuk
|
121
114
|
licenses: []
|
122
|
-
|
123
115
|
post_install_message:
|
124
116
|
rdoc_options: []
|
125
|
-
|
126
|
-
require_paths:
|
117
|
+
require_paths:
|
127
118
|
- lib
|
128
|
-
required_ruby_version: !ruby/object:Gem::Requirement
|
119
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
129
120
|
none: false
|
130
|
-
requirements:
|
131
|
-
- -
|
132
|
-
- !ruby/object:Gem::Version
|
133
|
-
|
134
|
-
|
135
|
-
- 0
|
136
|
-
version: "0"
|
137
|
-
required_rubygems_version: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ! '>='
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
125
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
138
126
|
none: false
|
139
|
-
requirements:
|
140
|
-
- -
|
141
|
-
- !ruby/object:Gem::Version
|
142
|
-
hash: 23
|
143
|
-
segments:
|
144
|
-
- 1
|
145
|
-
- 3
|
146
|
-
- 6
|
127
|
+
requirements:
|
128
|
+
- - ! '>='
|
129
|
+
- !ruby/object:Gem::Version
|
147
130
|
version: 1.3.6
|
148
131
|
requirements: []
|
149
|
-
|
150
132
|
rubyforge_project: tuktuk
|
151
|
-
rubygems_version: 1.8.
|
133
|
+
rubygems_version: 1.8.23
|
152
134
|
signing_key:
|
153
135
|
specification_version: 3
|
154
136
|
summary: SMTP client for Ruby with DKIM support.
|
155
137
|
test_files: []
|
156
|
-
|