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