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 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
- begin
192
- send_many_now(server, mails).each do |mail, resp|
193
- responses.push [resp, mail]
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 last_error # got error at server level, mark all messages with errors
203
- mails.each {|mail| responses.push [last_error, mail] }
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
- smtp = init_connection(server)
216
- smtp.start(get_helo_domain(from), nil, nil, nil) do |smtp|
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
- smtp = init_connection(server)
232
- smtp.start(get_helo_domain, nil, nil, nil) do |smtp|
234
+ socket = init_connection(server)
235
+ socket.start(get_helo_domain, nil, nil, nil) do |smtp|
233
236
  mails.each do |mail|
234
- unless timeout_error
235
- begin
236
- resp = smtp.send_message(get_raw_mail(mail), get_from(mail), mail.to)
237
- smtp.send(:getok, 'RSET') if server['hotmail'] # fix for '503 Sender already specified'
238
- rescue Net::SMTPError, EOFError, Timeout::Error => e # may be Net::SMTPFatalError (550 Mailbox not found)
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] = timeout_error || resp
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)
@@ -1,7 +1,7 @@
1
1
  module Tuktuk
2
2
  MAJOR = 0
3
3
  MINOR = 4
4
- PATCH = 5
4
+ PATCH = 6
5
5
 
6
6
  VERSION = [MAJOR, MINOR, PATCH].join('.')
7
7
  end
@@ -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
- hash: 5
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
- - "Tom\xC3\xA1s Pollak"
7
+ authors:
8
+ - Tomás Pollak
14
9
  autorequire:
15
10
  bindir: bin
16
11
  cert_chain: []
17
-
18
- date: 2013-05-19 00:00:00 Z
19
- dependencies:
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
- prerelease: false
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
- requirement: &id002 !ruby/object:Gem::Requirement
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
- requirement: &id003 !ruby/object:Gem::Requirement
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
- hash: 5
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
- requirement: &id004 !ruby/object:Gem::Requirement
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
- requirement: &id005 !ruby/object:Gem::Requirement
72
+ version_requirements: !ruby/object:Gem::Requirement
87
73
  none: false
88
- requirements:
74
+ requirements:
89
75
  - - ~>
90
- - !ruby/object:Gem::Version
91
- hash: 27
92
- segments:
93
- - 2
94
- - 5
95
- - 0
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
- version_requirements: *id005
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
- hash: 3
134
- segments:
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.15
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
-