remailer 0.1.0 → 0.2.1
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/.gitignore +2 -0
- data/README.rdoc +14 -0
- data/VERSION +1 -1
- data/lib/remailer/connection.rb +340 -71
- data/remailer.gemspec +2 -2
- data/test/helper.rb +12 -0
- data/test/unit/remailer_test.rb +104 -13
- metadata +3 -3
data/.gitignore
CHANGED
data/README.rdoc
CHANGED
@@ -76,6 +76,20 @@ exhausted:
|
|
76
76
|
STDERR.puts "Sending complete."
|
77
77
|
end
|
78
78
|
|
79
|
+
The call to send a message can also take a callback method which must receive
|
80
|
+
one parameter that will be the numerical status code returned by the SMTP
|
81
|
+
server. Success is defined as 250, errors vary:
|
82
|
+
|
83
|
+
connection.send_email(
|
84
|
+
'from@example.net',
|
85
|
+
'to@example.com',
|
86
|
+
email_content
|
87
|
+
) do |status_code|
|
88
|
+
puts "Message finished with status #{status_code}"
|
89
|
+
end
|
90
|
+
|
91
|
+
A status code of nil is sent if the server timed out or the connection failed.
|
92
|
+
|
79
93
|
== Status
|
80
94
|
|
81
95
|
This software is currently experimental and is not recommended for production
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.1
|
1
|
+
0.2.1
|
data/lib/remailer/connection.rb
CHANGED
@@ -2,12 +2,55 @@ require 'socket'
|
|
2
2
|
require 'eventmachine'
|
3
3
|
|
4
4
|
class Remailer::Connection < EventMachine::Connection
|
5
|
+
# == Exceptions ===========================================================
|
6
|
+
|
7
|
+
class CallbackArgumentsRequired < Exception; end
|
8
|
+
|
5
9
|
# == Constants ============================================================
|
6
10
|
|
7
11
|
DEFAULT_TIMEOUT = 5
|
8
|
-
SMTP_PORT = 25
|
9
12
|
CRLF = "\r\n".freeze
|
10
13
|
CRLF_LENGTH = CRLF.length
|
14
|
+
|
15
|
+
SMTP_PORT = 25
|
16
|
+
SOCKS5_PORT = 1080
|
17
|
+
|
18
|
+
SOCKS5_VERSION = 5
|
19
|
+
|
20
|
+
SOCKS5_METHOD = {
|
21
|
+
:no_auth => 0,
|
22
|
+
:gssapi => 1,
|
23
|
+
:username_password => 2
|
24
|
+
}.freeze
|
25
|
+
|
26
|
+
SOCKS5_COMMAND = {
|
27
|
+
:connect => 1,
|
28
|
+
:bind => 2
|
29
|
+
}.freeze
|
30
|
+
|
31
|
+
SOCKS5_REPLY = {
|
32
|
+
0 => 'Succeeded',
|
33
|
+
1 => 'General SOCKS server failure',
|
34
|
+
2 => 'Connection not allowed',
|
35
|
+
3 => 'Network unreachable',
|
36
|
+
4 => 'Host unreachable',
|
37
|
+
5 => 'Connection refused',
|
38
|
+
6 => 'TTL expired',
|
39
|
+
7 => 'Command not supported',
|
40
|
+
8 => 'Address type not supported'
|
41
|
+
}.freeze
|
42
|
+
|
43
|
+
SOCKS5_ADDRESS_TYPE = {
|
44
|
+
:ipv4 => 1,
|
45
|
+
:domainname => 3,
|
46
|
+
:ipv6 => 4
|
47
|
+
}.freeze
|
48
|
+
|
49
|
+
NOTIFICATIONS = [
|
50
|
+
:debug,
|
51
|
+
:error,
|
52
|
+
:connect
|
53
|
+
].freeze
|
11
54
|
|
12
55
|
# == Properties ===========================================================
|
13
56
|
|
@@ -29,40 +72,65 @@ class Remailer::Connection < EventMachine::Connection
|
|
29
72
|
# * use_tls => Will use TLS if availble (default is true)
|
30
73
|
def self.open(smtp_server, options = nil)
|
31
74
|
options ||= { }
|
75
|
+
options[:host] = smtp_server
|
32
76
|
options[:port] ||= 25
|
33
77
|
options[:use_tls] = true unless (options.key?(:use_tls))
|
34
78
|
|
35
|
-
|
79
|
+
host_name = smtp_server
|
80
|
+
host_port = options[:port]
|
81
|
+
|
82
|
+
if (proxy_options = options[:proxy])
|
83
|
+
host_name = proxy_options[:host]
|
84
|
+
host_port = proxy_options[:port] || SOCKS5_PORT
|
85
|
+
end
|
86
|
+
|
87
|
+
EventMachine.connect(host_name, host_port, self, options)
|
36
88
|
end
|
37
89
|
|
38
|
-
# EHLO address
|
39
|
-
# MAIL FROM:<reverse-path> [SP <mail-parameters> ] <CRLF>
|
40
|
-
# RCPT TO:<forward-path> [ SP <rcpt-parameters> ] <CRLF>
|
41
|
-
# DATA <CRLF>
|
42
|
-
# NOOP
|
43
|
-
# QUIT
|
44
|
-
|
45
|
-
# 250-mx.google.com at your service, [99.231.152.248]
|
46
|
-
# 250-SIZE 35651584
|
47
|
-
# 250-8BITMIME
|
48
|
-
# 250-STARTTLS
|
49
|
-
# 250 ENHANCEDSTATUSCODES
|
50
|
-
|
51
90
|
def self.encode_data(data)
|
52
91
|
data.gsub(/((?:\r\n|\n)\.)/m, '\\1.')
|
53
92
|
end
|
93
|
+
|
94
|
+
def self.base64(string)
|
95
|
+
[ string ].pack('m').chomp
|
96
|
+
end
|
97
|
+
|
98
|
+
def self.encode_authentication(username, password)
|
99
|
+
base64("\0#{username}\0#{password}")
|
100
|
+
end
|
101
|
+
|
102
|
+
def self.warn_about_arguments(proc, range)
|
103
|
+
unless (range.include?(proc.arity) or proc.arity == -1)
|
104
|
+
STDERR.puts "Callback must accept #{[ range.min, range.max ].uniq.join(' to ')} arguments but accepts #{proc.arity}"
|
105
|
+
end
|
106
|
+
end
|
54
107
|
|
55
108
|
# == Instance Methods =====================================================
|
56
109
|
|
57
110
|
def initialize(options)
|
58
|
-
|
111
|
+
# Throwing exceptions inside this block is going to cause EventMachine
|
112
|
+
# to malfunction in a spectacular way and hide the actual exception. To
|
113
|
+
# allow for debugging, exceptions are dumped to STDERR as a last resort.
|
114
|
+
begin
|
115
|
+
@options = options
|
59
116
|
|
60
|
-
|
61
|
-
|
117
|
+
@options[:hostname] ||= Socket.gethostname
|
118
|
+
@messages = [ ]
|
62
119
|
|
63
|
-
|
120
|
+
NOTIFICATIONS.each do |type|
|
121
|
+
callback = @options[type]
|
122
|
+
|
123
|
+
if (callback.is_a?(Proc))
|
124
|
+
self.class.warn_about_arguments(callback, (2..2))
|
125
|
+
end
|
126
|
+
end
|
64
127
|
|
65
|
-
|
128
|
+
debug_notification(:options, @options.inspect)
|
129
|
+
|
130
|
+
reset_timeout!
|
131
|
+
rescue Object => e
|
132
|
+
STDERR.puts "#{e.class}: #{e}"
|
133
|
+
end
|
66
134
|
end
|
67
135
|
|
68
136
|
# Returns true if the connection has advertised TLS support, or false if
|
@@ -71,6 +139,18 @@ class Remailer::Connection < EventMachine::Connection
|
|
71
139
|
def tls_support?
|
72
140
|
!!@tls_support
|
73
141
|
end
|
142
|
+
|
143
|
+
# Returns true if the connection will be using a proxy to connect, false
|
144
|
+
# otherwise.
|
145
|
+
def using_proxy?
|
146
|
+
!!@options[:proxy]
|
147
|
+
end
|
148
|
+
|
149
|
+
# Returns true if the connection will require authentication to complete,
|
150
|
+
# that is a username has been supplied in the options, or false otherwise.
|
151
|
+
def requires_authentication?
|
152
|
+
@options[:username] and !@options[:username].empty?
|
153
|
+
end
|
74
154
|
|
75
155
|
# This is used to create a callback that will be called if no more messages
|
76
156
|
# are schedueld to be sent.
|
@@ -78,11 +158,19 @@ class Remailer::Connection < EventMachine::Connection
|
|
78
158
|
@options[:after_complete] = block
|
79
159
|
end
|
80
160
|
|
161
|
+
# Closes the connection after all of the queued messages have been sent.
|
81
162
|
def close_when_complete!
|
82
163
|
@options[:close] = true
|
83
164
|
end
|
84
165
|
|
166
|
+
# Sends an email message through the connection at the earliest opportunity.
|
167
|
+
# A callback block can be supplied that will be executed when the message
|
168
|
+
# has been sent, an unexpected result occurred, or the send timed out.
|
85
169
|
def send_email(from, to, data, &block)
|
170
|
+
if (block_given?)
|
171
|
+
self.class.warn_about_arguments(block, 1..2)
|
172
|
+
end
|
173
|
+
|
86
174
|
message = {
|
87
175
|
:from => from,
|
88
176
|
:to => to,
|
@@ -92,7 +180,9 @@ class Remailer::Connection < EventMachine::Connection
|
|
92
180
|
|
93
181
|
@messages << message
|
94
182
|
|
183
|
+
# If the connection is ready to send...
|
95
184
|
if (@state == :ready)
|
185
|
+
# ...send the message right away.
|
96
186
|
send_queued_message!
|
97
187
|
end
|
98
188
|
end
|
@@ -114,7 +204,14 @@ class Remailer::Connection < EventMachine::Connection
|
|
114
204
|
# flagging the connection as estasblished.
|
115
205
|
def connection_completed
|
116
206
|
@timeout_at = nil
|
117
|
-
|
207
|
+
|
208
|
+
if (using_proxy?)
|
209
|
+
enter_proxy_init_state!
|
210
|
+
else
|
211
|
+
connect_notification(true, "Connection completed")
|
212
|
+
@state = :connected
|
213
|
+
@connected = true
|
214
|
+
end
|
118
215
|
end
|
119
216
|
|
120
217
|
# This implements the EventMachine::Connection#unbind method to capture
|
@@ -130,17 +227,48 @@ class Remailer::Connection < EventMachine::Connection
|
|
130
227
|
end
|
131
228
|
|
132
229
|
def receive_data(data)
|
133
|
-
#
|
134
|
-
# a whole line will be ready to process, or that there is only one line.
|
135
|
-
@buffer ||= ''
|
136
|
-
@buffer << data
|
230
|
+
# FIX: Buffer the data anyway.
|
137
231
|
|
138
|
-
|
139
|
-
|
140
|
-
|
232
|
+
case (state)
|
233
|
+
when :proxy_init
|
234
|
+
version, method = data.unpack('CC')
|
235
|
+
|
236
|
+
if (method == SOCKS5_METHOD[:username_password])
|
237
|
+
enter_proxy_authentication_state!
|
238
|
+
else
|
239
|
+
enter_proxy_connecting_state!
|
240
|
+
end
|
241
|
+
when :proxy_connecting
|
242
|
+
version, reply, reserved, address_type, address, port = data.unpack('CCCCNn')
|
243
|
+
|
244
|
+
case (reply)
|
245
|
+
when 0
|
246
|
+
@state = :connected
|
247
|
+
@connected = true
|
248
|
+
connect_notification(true, "Connection completed")
|
249
|
+
else
|
250
|
+
debug(:error, "Proxy server returned error code #{reply}: #{SOCKS5_REPLY[reply]}")
|
251
|
+
connect_notification(false, "Proxy server returned error code #{reply}: #{SOCKS5_REPLY[reply]}")
|
252
|
+
close_connection
|
253
|
+
@state = :failed
|
141
254
|
end
|
255
|
+
when :proxy_authenticating
|
256
|
+
# Decode response of authentication request...
|
257
|
+
|
258
|
+
# ...
|
259
|
+
else
|
260
|
+
# Data is received in arbitrary sized chunks, so there is no guarantee
|
261
|
+
# a whole line will be ready to process, or that there is only one line.
|
262
|
+
@buffer ||= ''
|
263
|
+
@buffer << data
|
264
|
+
|
265
|
+
while (line_index = @buffer.index(CRLF))
|
266
|
+
if (line_index > 0)
|
267
|
+
receive_reply(@buffer[0, line_index])
|
268
|
+
end
|
142
269
|
|
143
|
-
|
270
|
+
@buffer = (@buffer[line_index + CRLF_LENGTH, @buffer.length] || '')
|
271
|
+
end
|
144
272
|
end
|
145
273
|
end
|
146
274
|
|
@@ -152,6 +280,7 @@ class Remailer::Connection < EventMachine::Connection
|
|
152
280
|
end
|
153
281
|
end
|
154
282
|
|
283
|
+
protected
|
155
284
|
def send_line(line = '')
|
156
285
|
send_data(line + CRLF)
|
157
286
|
|
@@ -164,6 +293,18 @@ class Remailer::Connection < EventMachine::Connection
|
|
164
293
|
!!@reply_complete
|
165
294
|
end
|
166
295
|
|
296
|
+
def resolve_hostname(hostname)
|
297
|
+
# FIXME: Elminitate this potentially blocking call by using an async
|
298
|
+
# resolver if available.
|
299
|
+
record = Socket.gethostbyname(hostname)
|
300
|
+
|
301
|
+
debug_notification(:resolved, record && record.last)
|
302
|
+
|
303
|
+
record and record.last
|
304
|
+
rescue
|
305
|
+
nil
|
306
|
+
end
|
307
|
+
|
167
308
|
def receive_reply(reply)
|
168
309
|
debug_notification(:reply, reply.inspect)
|
169
310
|
|
@@ -173,8 +314,11 @@ class Remailer::Connection < EventMachine::Connection
|
|
173
314
|
reply_code = $1.to_i
|
174
315
|
@reply_complete = $2 != '-'
|
175
316
|
reply_message = $3
|
317
|
+
|
318
|
+
debug_notification(:recv, reply)
|
176
319
|
end
|
177
320
|
|
321
|
+
# The connection itself will be in a particular state.
|
178
322
|
case (state)
|
179
323
|
when :connected
|
180
324
|
case (reply_code)
|
@@ -192,7 +336,7 @@ class Remailer::Connection < EventMachine::Connection
|
|
192
336
|
send_line("HELO #{@options[:hostname]}")
|
193
337
|
end
|
194
338
|
else
|
195
|
-
fail_unanticipated_response!(
|
339
|
+
fail_unanticipated_response!(reply_code, reply_message)
|
196
340
|
end
|
197
341
|
when :sent_ehlo
|
198
342
|
case (reply_code)
|
@@ -206,27 +350,46 @@ class Remailer::Connection < EventMachine::Connection
|
|
206
350
|
when 'STARTTLS'
|
207
351
|
@tls_support = true
|
208
352
|
end
|
209
|
-
|
210
|
-
# FIX: Add TLS support
|
211
|
-
# if (@tls_support and @options[:use_tls])
|
212
|
-
# @state = :tls_init
|
213
|
-
# end
|
214
353
|
|
215
354
|
if (@reply_complete)
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
355
|
+
if (@options[:use_tls])
|
356
|
+
send_line("STARTTLS")
|
357
|
+
@state = :sent_starttls
|
358
|
+
elsif (requires_authentication?)
|
359
|
+
enter_sent_auth_state!
|
360
|
+
else
|
361
|
+
enter_ready_state!
|
362
|
+
end
|
220
363
|
end
|
221
364
|
else
|
222
|
-
fail_unanticipated_response!(
|
365
|
+
fail_unanticipated_response!(reply_code, reply_message)
|
223
366
|
end
|
224
367
|
when :sent_helo
|
225
368
|
case (reply_code)
|
226
369
|
when 250
|
227
|
-
|
370
|
+
enter_ready_state!
|
228
371
|
else
|
229
|
-
fail_unanticipated_response!(
|
372
|
+
fail_unanticipated_response!(reply_code, reply_message)
|
373
|
+
end
|
374
|
+
when :sent_starttls
|
375
|
+
case (reply_code)
|
376
|
+
when 220
|
377
|
+
start_tls
|
378
|
+
|
379
|
+
if (requires_authentication?)
|
380
|
+
enter_sent_auth_state!
|
381
|
+
else
|
382
|
+
enter_ready_state!
|
383
|
+
end
|
384
|
+
else
|
385
|
+
fail_unanticipated_response!(reply_code, reply_message)
|
386
|
+
end
|
387
|
+
when :sent_auth
|
388
|
+
case (reply_code)
|
389
|
+
when 235
|
390
|
+
enter_ready_state!
|
391
|
+
else
|
392
|
+
fail_unanticipated_response!(reply_code, reply_message)
|
230
393
|
end
|
231
394
|
when :sent_mail_from
|
232
395
|
case (reply_code)
|
@@ -234,7 +397,7 @@ class Remailer::Connection < EventMachine::Connection
|
|
234
397
|
@state = :sent_rcpt_to
|
235
398
|
send_line("RCPT TO:#{@active_message[:to]}")
|
236
399
|
else
|
237
|
-
fail_unanticipated_response!(
|
400
|
+
fail_unanticipated_response!(reply_code, reply_message)
|
238
401
|
end
|
239
402
|
when :sent_rcpt_to
|
240
403
|
case (reply_code)
|
@@ -244,48 +407,121 @@ class Remailer::Connection < EventMachine::Connection
|
|
244
407
|
|
245
408
|
@data_offset = 0
|
246
409
|
else
|
247
|
-
fail_unanticipated_response!(
|
410
|
+
fail_unanticipated_response!(reply_code, reply_message)
|
248
411
|
end
|
249
412
|
when :sent_data
|
250
413
|
case (reply_code)
|
251
414
|
when 354
|
252
415
|
@state = :data_sending
|
253
416
|
|
254
|
-
|
417
|
+
transmit_data!
|
255
418
|
else
|
256
|
-
fail_unanticipated_response!(
|
419
|
+
fail_unanticipated_response!(reply_code, reply_message)
|
257
420
|
end
|
258
421
|
when :sent_data_content
|
259
|
-
|
260
|
-
callback.call(reply_code)
|
261
|
-
end
|
262
|
-
|
263
|
-
@state = :ready
|
422
|
+
send_callback(reply_code, reply_message)
|
264
423
|
|
265
|
-
|
424
|
+
enter_ready_state!
|
266
425
|
when :sent_quit
|
267
426
|
case (reply_code)
|
268
427
|
when 221
|
269
428
|
@state = :closed
|
270
429
|
close_connection
|
271
430
|
else
|
272
|
-
fail_unanticipated_response!(
|
431
|
+
fail_unanticipated_response!(reply_code, reply_message)
|
273
432
|
end
|
274
433
|
when :sent_reset
|
275
434
|
case (reply_code)
|
276
435
|
when 250
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
send_queued_message!
|
436
|
+
enter_ready_state!
|
281
437
|
end
|
282
438
|
end
|
283
439
|
end
|
284
440
|
|
285
|
-
def
|
441
|
+
def enter_proxy_init_state!
|
442
|
+
debug_notification(:proxy, "Initiating proxy connection through #{@options[:proxy][:host]}")
|
443
|
+
|
444
|
+
socks_methods = [ ]
|
445
|
+
|
446
|
+
if (@options[:proxy][:username])
|
447
|
+
socks_methods << SOCKS5_METHOD[:username_password]
|
448
|
+
end
|
449
|
+
|
450
|
+
send_data(
|
451
|
+
[
|
452
|
+
SOCKS5_VERSION,
|
453
|
+
socks_methods.length,
|
454
|
+
socks_methods
|
455
|
+
].flatten.pack('CCC*')
|
456
|
+
)
|
457
|
+
|
458
|
+
@state = :proxy_init
|
459
|
+
end
|
460
|
+
|
461
|
+
def enter_proxy_connecting_state!
|
462
|
+
# REFACTOR: Move the resolution of the hostname to an earlier point to
|
463
|
+
# avoid connecting needlessly.
|
464
|
+
|
465
|
+
debug_notification(:proxy, "Sending proxy connection request to #{@options[:host]}:#{@options[:port]}")
|
466
|
+
|
467
|
+
if (ip_address = resolve_hostname(@options[:host]))
|
468
|
+
send_data(
|
469
|
+
[
|
470
|
+
SOCKS5_VERSION,
|
471
|
+
SOCKS5_COMMAND[:connect],
|
472
|
+
0,
|
473
|
+
SOCKS5_ADDRESS_TYPE[:ipv4],
|
474
|
+
ip_address,
|
475
|
+
@options[:port]
|
476
|
+
].pack('CCCCA4n')
|
477
|
+
)
|
478
|
+
|
479
|
+
@state = :proxy_connecting
|
480
|
+
else
|
481
|
+
send_callback(:error_connecting, "Could not resolve hostname #{@options[:host]}")
|
482
|
+
|
483
|
+
@state = :failed
|
484
|
+
close_connection
|
485
|
+
end
|
486
|
+
end
|
487
|
+
|
488
|
+
def enter_proxy_authenticating_state!
|
489
|
+
debug_notification(:proxy, "Sending proxy authentication")
|
490
|
+
|
491
|
+
proxy_options = @options[:proxy]
|
492
|
+
username = proxy_options[:username]
|
493
|
+
password = proxy_options[:password]
|
494
|
+
|
495
|
+
send_data(
|
496
|
+
[
|
497
|
+
SOCKS5_VERSION,
|
498
|
+
username.length,
|
499
|
+
username,
|
500
|
+
password.length,
|
501
|
+
password
|
502
|
+
].pack('CCA*CA*')
|
503
|
+
)
|
504
|
+
|
505
|
+
@state = :proxy_authenticating
|
506
|
+
end
|
507
|
+
|
508
|
+
def enter_ready_state!
|
509
|
+
@state = :ready
|
510
|
+
|
511
|
+
send_queued_message!
|
512
|
+
end
|
513
|
+
|
514
|
+
def enter_sent_auth_state!
|
515
|
+
send_line("AUTH PLAIN #{self.class.encode_authentication(@options[:username], @options[:password])}")
|
516
|
+
@state = :sent_auth
|
517
|
+
end
|
518
|
+
|
519
|
+
def transmit_data!(chunk_size = nil)
|
286
520
|
data = @active_message[:data]
|
287
521
|
chunk_size ||= data.length
|
288
522
|
|
523
|
+
# This chunk-based sending will work better when/if EventMachine can be
|
524
|
+
# configured to support 'writable' notifications on the active socket.
|
289
525
|
chunk = data[@data_offset, chunk_size]
|
290
526
|
debug_notification(:send, chunk.inspect)
|
291
527
|
send_data(self.class.encode_data(data))
|
@@ -301,12 +537,14 @@ class Remailer::Connection < EventMachine::Connection
|
|
301
537
|
end
|
302
538
|
end
|
303
539
|
|
304
|
-
def
|
305
|
-
|
540
|
+
def reset_timeout!
|
541
|
+
@timeout_at = Time.now + (@options[:timeout] || DEFAULT_TIMEOUT)
|
306
542
|
end
|
307
543
|
|
308
544
|
def send_queued_message!
|
309
545
|
return if (@active_message)
|
546
|
+
|
547
|
+
reset_timeout!
|
310
548
|
|
311
549
|
if (@active_message = @messages.shift)
|
312
550
|
@state = :sent_mail_from
|
@@ -324,30 +562,61 @@ class Remailer::Connection < EventMachine::Connection
|
|
324
562
|
def check_for_timeouts!
|
325
563
|
return if (!@timeout_at or Time.now < @timeout_at)
|
326
564
|
|
327
|
-
|
565
|
+
error_notification(:timeout, "Connection timed out")
|
566
|
+
debug_notification(:timeout, "Connection timed out")
|
567
|
+
send_callback(:timeout, "Connection timed out before send could complete")
|
568
|
+
|
328
569
|
@state = :timeout
|
570
|
+
|
571
|
+
unless (@connected)
|
572
|
+
connect_notification(false, "Connection timed out")
|
573
|
+
end
|
574
|
+
|
329
575
|
close_connection
|
330
576
|
end
|
331
577
|
|
332
|
-
def
|
333
|
-
case (@options[
|
578
|
+
def send_notification(type, code, message)
|
579
|
+
case (callback = @options[type])
|
334
580
|
when nil, false
|
335
|
-
# No
|
581
|
+
# No notification in this case
|
336
582
|
when Proc
|
337
|
-
|
583
|
+
callback.call(code, message)
|
338
584
|
when IO
|
339
|
-
|
585
|
+
callback.puts("%s: %s" % [ code.to_s, message ])
|
340
586
|
else
|
341
|
-
STDERR.puts("%s: %s" % [
|
587
|
+
STDERR.puts("%s: %s" % [ code.to_s, message ])
|
342
588
|
end
|
343
589
|
end
|
344
590
|
|
345
|
-
def
|
346
|
-
|
347
|
-
|
348
|
-
|
591
|
+
def connect_notification(code, message)
|
592
|
+
send_notification(:connect, code, message)
|
593
|
+
end
|
594
|
+
|
595
|
+
def error_notification(code, message)
|
596
|
+
send_notification(:error, code, message)
|
597
|
+
end
|
598
|
+
|
599
|
+
def debug_notification(code, message)
|
600
|
+
send_notification(:debug, code, message)
|
601
|
+
end
|
602
|
+
|
603
|
+
def send_callback(reply_code, reply_message)
|
604
|
+
if (callback = (@active_message and @active_message[:callback]))
|
605
|
+
# The callback is screened in advance when assigned to ensure that it
|
606
|
+
# has only 1 or 2 arguments. There should be no else here.
|
607
|
+
case (callback.arity)
|
608
|
+
when 2
|
609
|
+
callback.call(reply_code, reply_message)
|
610
|
+
when 1
|
611
|
+
callback.call(reply_code)
|
349
612
|
end
|
350
613
|
end
|
614
|
+
end
|
615
|
+
|
616
|
+
def fail_unanticipated_response!(reply_code, reply_message)
|
617
|
+
send_callback(reply_code, reply_message)
|
618
|
+
debug_notification(:error, "[#{@state}] #{reply_code} #{reply_message}")
|
619
|
+
error_notification(reply_code, reply_message)
|
351
620
|
|
352
621
|
@active_message = nil
|
353
622
|
|
data/remailer.gemspec
CHANGED
@@ -5,11 +5,11 @@
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = %q{remailer}
|
8
|
-
s.version = "0.1
|
8
|
+
s.version = "0.2.1"
|
9
9
|
|
10
10
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
11
|
s.authors = ["Scott Tadman"]
|
12
|
-
s.date = %q{2010-11-
|
12
|
+
s.date = %q{2010-11-22}
|
13
13
|
s.description = %q{EventMachine capable SMTP engine}
|
14
14
|
s.email = %q{scott@twg.ca}
|
15
15
|
s.extra_rdoc_files = [
|
data/test/helper.rb
CHANGED
@@ -62,3 +62,15 @@ class Test::Unit::TestCase
|
|
62
62
|
end
|
63
63
|
end
|
64
64
|
end
|
65
|
+
|
66
|
+
require 'ostruct'
|
67
|
+
|
68
|
+
TestConfig = OpenStruct.new
|
69
|
+
|
70
|
+
config_file = File.expand_path("config.rb", File.dirname(__FILE__))
|
71
|
+
|
72
|
+
if (File.exist?(config_file))
|
73
|
+
require config_file
|
74
|
+
else
|
75
|
+
raise "No test/config.rb file found. Copy and modify test/config.example.rb"
|
76
|
+
end
|
data/test/unit/remailer_test.rb
CHANGED
@@ -1,8 +1,6 @@
|
|
1
1
|
require File.expand_path(File.join(*%w[ .. helper ]), File.dirname(__FILE__))
|
2
2
|
|
3
3
|
class RemailerTest < Test::Unit::TestCase
|
4
|
-
TEST_SMTP_SERVER = 'mail.postageapp.com'.freeze
|
5
|
-
|
6
4
|
def test_encode_data
|
7
5
|
sample_data = "Line 1\r\nLine 2\r\n.\r\nLine 3\r\n.Line 4\r\n"
|
8
6
|
|
@@ -14,7 +12,7 @@ class RemailerTest < Test::Unit::TestCase
|
|
14
12
|
debug = { }
|
15
13
|
|
16
14
|
connection = Remailer::Connection.open(
|
17
|
-
|
15
|
+
TestConfig.smtp_server[:host],
|
18
16
|
:debug => STDERR
|
19
17
|
)
|
20
18
|
|
@@ -32,10 +30,104 @@ class RemailerTest < Test::Unit::TestCase
|
|
32
30
|
connection.state == :closed
|
33
31
|
end
|
34
32
|
|
35
|
-
assert_equal
|
33
|
+
assert_equal TestConfig.smtp_server[:host], connection.remote
|
34
|
+
|
35
|
+
assert_equal true, after_complete_trigger
|
36
|
+
|
37
|
+
assert_equal 52428800, connection.max_size
|
38
|
+
assert_equal :esmtp, connection.protocol
|
39
|
+
assert_equal true, connection.tls_support?
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def test_failed_connect
|
44
|
+
engine do
|
45
|
+
error_received = nil
|
46
|
+
|
47
|
+
connection = Remailer::Connection.open(
|
48
|
+
'example.com',
|
49
|
+
:debug => STDERR,
|
50
|
+
:error => lambda { |code, message|
|
51
|
+
error_received = [ code, message ]
|
52
|
+
},
|
53
|
+
:timeout => 1
|
54
|
+
)
|
55
|
+
|
56
|
+
assert_eventually(3) do
|
57
|
+
error_received
|
58
|
+
end
|
59
|
+
|
60
|
+
assert_equal :timeout, error_received[0]
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def test_connect_with_auth
|
65
|
+
engine do
|
66
|
+
debug = { }
|
67
|
+
|
68
|
+
connection = Remailer::Connection.open(
|
69
|
+
TestConfig.public_smtp_server[:host],
|
70
|
+
:port => 587,
|
71
|
+
:debug => STDERR,
|
72
|
+
:username => TestConfig.public_smtp_server[:username],
|
73
|
+
:password => TestConfig.public_smtp_server[:password]
|
74
|
+
)
|
75
|
+
|
76
|
+
after_complete_trigger = false
|
77
|
+
|
78
|
+
connection.close_when_complete!
|
79
|
+
connection.after_complete do
|
80
|
+
after_complete_trigger = true
|
81
|
+
end
|
82
|
+
|
83
|
+
assert_equal :connecting, connection.state
|
84
|
+
assert !connection.error?
|
85
|
+
|
86
|
+
assert_eventually(15) do
|
87
|
+
connection.state == :closed
|
88
|
+
end
|
89
|
+
|
90
|
+
assert_equal TestConfig.public_smtp_server[:identifier], connection.remote
|
36
91
|
|
37
92
|
assert_equal true, after_complete_trigger
|
38
93
|
|
94
|
+
assert_equal 35651584, connection.max_size
|
95
|
+
assert_equal :esmtp, connection.protocol
|
96
|
+
assert_equal true, connection.tls_support?
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def test_connect_via_proxy
|
101
|
+
engine do
|
102
|
+
debug = { }
|
103
|
+
|
104
|
+
connection = Remailer::Connection.open(
|
105
|
+
TestConfig.smtp_server[:host],
|
106
|
+
:debug => STDERR,
|
107
|
+
:proxy => {
|
108
|
+
:proto => :socks5,
|
109
|
+
:host => TestConfig.proxy_server
|
110
|
+
}
|
111
|
+
)
|
112
|
+
|
113
|
+
after_complete_trigger = false
|
114
|
+
|
115
|
+
connection.close_when_complete!
|
116
|
+
connection.after_complete do
|
117
|
+
after_complete_trigger = true
|
118
|
+
end
|
119
|
+
|
120
|
+
assert_equal :connecting, connection.state
|
121
|
+
assert !connection.error?
|
122
|
+
|
123
|
+
assert_eventually(15) do
|
124
|
+
connection.state == :closed
|
125
|
+
end
|
126
|
+
|
127
|
+
assert_equal TestConfig.smtp_server[:identifier], connection.remote
|
128
|
+
|
129
|
+
assert_equal true, after_complete_trigger
|
130
|
+
|
39
131
|
assert_equal 52428800, connection.max_size
|
40
132
|
assert_equal :esmtp, connection.protocol
|
41
133
|
assert_equal true, connection.tls_support?
|
@@ -45,7 +137,7 @@ class RemailerTest < Test::Unit::TestCase
|
|
45
137
|
def test_connect_and_send_after_start
|
46
138
|
engine do
|
47
139
|
connection = Remailer::Connection.open(
|
48
|
-
|
140
|
+
TestConfig.smtp_server[:host],
|
49
141
|
:debug => STDERR
|
50
142
|
)
|
51
143
|
|
@@ -73,7 +165,7 @@ class RemailerTest < Test::Unit::TestCase
|
|
73
165
|
def test_connect_and_send_dotted_message
|
74
166
|
engine do
|
75
167
|
connection = Remailer::Connection.open(
|
76
|
-
|
168
|
+
TestConfig.smtp_server[:host],
|
77
169
|
:debug => STDERR
|
78
170
|
)
|
79
171
|
|
@@ -97,15 +189,14 @@ class RemailerTest < Test::Unit::TestCase
|
|
97
189
|
|
98
190
|
def test_connect_and_long_send
|
99
191
|
engine do
|
100
|
-
connection = Remailer::Connection.open(
|
192
|
+
connection = Remailer::Connection.open(TestConfig.smtp_server[:host])
|
101
193
|
|
102
194
|
assert_equal :connecting, connection.state
|
103
|
-
assert !connection.error?
|
104
195
|
|
105
196
|
result_code = nil
|
106
197
|
connection.send_email(
|
107
|
-
|
108
|
-
|
198
|
+
TestConfig.sender,
|
199
|
+
TestConfig.receiver,
|
109
200
|
example_message + 'a' * 100000
|
110
201
|
) do |c|
|
111
202
|
result_code = c
|
@@ -121,9 +212,9 @@ protected
|
|
121
212
|
def example_message
|
122
213
|
example = <<__END__
|
123
214
|
Date: Sat, 13 Nov 2010 02:25:24 +0000
|
124
|
-
From: sender
|
125
|
-
To: Remailer Test
|
126
|
-
Message-Id: <hfLkcIByfjYoNIxCO7DMsxBTX9svsFHikIOfAiYy
|
215
|
+
From: #{TestConfig.sender}
|
216
|
+
To: Remailer Test <#{TestConfig.receiver}>
|
217
|
+
Message-Id: <hfLkcIByfjYoNIxCO7DMsxBTX9svsFHikIOfAiYy@#{TestConfig.sender.split(/@/).last}>
|
127
218
|
Subject: Example Subject
|
128
219
|
Mime-Version: 1.0
|
129
220
|
Content-Type: text/plain
|
metadata
CHANGED
@@ -4,9 +4,9 @@ version: !ruby/object:Gem::Version
|
|
4
4
|
prerelease: false
|
5
5
|
segments:
|
6
6
|
- 0
|
7
|
+
- 2
|
7
8
|
- 1
|
8
|
-
|
9
|
-
version: 0.1.0
|
9
|
+
version: 0.2.1
|
10
10
|
platform: ruby
|
11
11
|
authors:
|
12
12
|
- Scott Tadman
|
@@ -14,7 +14,7 @@ autorequire:
|
|
14
14
|
bindir: bin
|
15
15
|
cert_chain: []
|
16
16
|
|
17
|
-
date: 2010-11-
|
17
|
+
date: 2010-11-22 00:00:00 -05:00
|
18
18
|
default_executable:
|
19
19
|
dependencies: []
|
20
20
|
|