tuktuk 0.4.5 → 0.4.6

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