remailer 0.2.1 → 0.3.0

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/VERSION CHANGED
@@ -1 +1 @@
1
- 0.2.1
1
+ 0.3.0
@@ -2,4 +2,5 @@ module Remailer
2
2
  # == Submodules ===========================================================
3
3
 
4
4
  autoload(:Connection, 'remailer/connection')
5
+ autoload(:Interpreter, 'remailer/interpreter')
5
6
  end
@@ -6,46 +6,19 @@ class Remailer::Connection < EventMachine::Connection
6
6
 
7
7
  class CallbackArgumentsRequired < Exception; end
8
8
 
9
+ # == Submodules ===========================================================
10
+
11
+ autoload(:SmtpInterpreter, 'remailer/connection/smtp_interpreter')
12
+ autoload(:Socks5Interpreter, 'remailer/connection/socks5_interpreter')
13
+
9
14
  # == Constants ============================================================
10
15
 
11
- DEFAULT_TIMEOUT = 5
12
16
  CRLF = "\r\n".freeze
13
- CRLF_LENGTH = CRLF.length
17
+ DEFAULT_TIMEOUT = 5
14
18
 
15
19
  SMTP_PORT = 25
16
20
  SOCKS5_PORT = 1080
17
21
 
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
22
  NOTIFICATIONS = [
50
23
  :debug,
51
24
  :error,
@@ -54,10 +27,10 @@ class Remailer::Connection < EventMachine::Connection
54
27
 
55
28
  # == Properties ===========================================================
56
29
 
30
+ attr_accessor :remote, :max_size, :protocol, :hostname
31
+ attr_accessor :pipelining, :tls_support
57
32
  attr_accessor :timeout
58
- attr_reader :state, :mode
59
33
  attr_accessor :options
60
- attr_reader :remote, :max_size, :protocol
61
34
 
62
35
  # == Extensions ===========================================================
63
36
 
@@ -70,11 +43,24 @@ class Remailer::Connection < EventMachine::Connection
70
43
  # * require_tls => If true will fail connections to non-TLS capable
71
44
  # servers (default is false)
72
45
  # * use_tls => Will use TLS if availble (default is true)
73
- def self.open(smtp_server, options = nil)
46
+ # * debug => Where to send debugging output (IO or Proc)
47
+ # * connect => Where to send a connection notification (IO or Proc)
48
+ # * error => Where to send errors (IO or Proc)
49
+ # A block can be supplied in which case it will stand in as the :connect
50
+ # option. The block will recieve a first argument that is the status of
51
+ # the connection, and an optional second that is a diagnostic message.
52
+ def self.open(smtp_server, options = nil, &block)
74
53
  options ||= { }
75
54
  options[:host] = smtp_server
76
55
  options[:port] ||= 25
77
- options[:use_tls] = true unless (options.key?(:use_tls))
56
+
57
+ unless (options.key?(:use_tls))
58
+ options[:use_tls] = true
59
+ end
60
+
61
+ if (block_given?)
62
+ options[:connect] = block
63
+ end
78
64
 
79
65
  host_name = smtp_server
80
66
  host_port = options[:port]
@@ -84,21 +70,17 @@ class Remailer::Connection < EventMachine::Connection
84
70
  host_port = proxy_options[:port] || SOCKS5_PORT
85
71
  end
86
72
 
87
- EventMachine.connect(host_name, host_port, self, options)
88
- end
89
-
90
- def self.encode_data(data)
91
- data.gsub(/((?:\r\n|\n)\.)/m, '\\1.')
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}")
73
+ begin
74
+ EventMachine.connect(host_name, host_port, self, options)
75
+ rescue EventMachine::ConnectionError => e
76
+ options[:connect].is_a?(Proc) and options[:connect].call(false, e.to_s)
77
+ options[:debug].is_a?(Proc) and options[:debug].call(:error, e.to_s)
78
+ options[:error].is_a?(Proc) and options[:error].call(:connect_error, e.to_s)
79
+ end
100
80
  end
101
81
 
82
+ # Warns about supplying a Proc which does not appear to accept the required
83
+ # number of arguments.
102
84
  def self.warn_about_arguments(proc, range)
103
85
  unless (range.include?(proc.arity) or proc.arity == -1)
104
86
  STDERR.puts "Callback must accept #{[ range.min, range.max ].uniq.join(' to ')} arguments but accepts #{proc.arity}"
@@ -107,30 +89,42 @@ class Remailer::Connection < EventMachine::Connection
107
89
 
108
90
  # == Instance Methods =====================================================
109
91
 
92
+ # EventMachine will call this constructor and it is not to be called
93
+ # directly. Use the Remailer::Connection.open method to facilitate the
94
+ # correct creation of a new connection.
110
95
  def initialize(options)
111
96
  # Throwing exceptions inside this block is going to cause EventMachine
112
97
  # to malfunction in a spectacular way and hide the actual exception. To
113
98
  # allow for debugging, exceptions are dumped to STDERR as a last resort.
114
- begin
115
- @options = options
99
+ @options = options
100
+ @hostname = @options[:hostname] || Socket.gethostname
116
101
 
117
- @options[:hostname] ||= Socket.gethostname
118
- @messages = [ ]
119
-
120
- NOTIFICATIONS.each do |type|
121
- callback = @options[type]
102
+ @messages = [ ]
103
+
104
+ NOTIFICATIONS.each do |type|
105
+ callback = @options[type]
122
106
 
123
- if (callback.is_a?(Proc))
124
- self.class.warn_about_arguments(callback, (2..2))
125
- end
107
+ if (callback.is_a?(Proc))
108
+ self.class.warn_about_arguments(callback, (2..2))
126
109
  end
127
-
128
- debug_notification(:options, @options.inspect)
129
-
130
- reset_timeout!
131
- rescue Object => e
132
- STDERR.puts "#{e.class}: #{e}"
133
110
  end
111
+
112
+ debug_notification(:options, @options.inspect)
113
+
114
+ reset_timeout!
115
+
116
+ if (using_proxy?)
117
+ use_socks5_interpreter!
118
+ else
119
+ use_smtp_interpreter!
120
+ end
121
+
122
+ rescue Object => e
123
+ STDERR.puts "#{e.class}: #{e}"
124
+ end
125
+
126
+ def use_tls?
127
+ !!@options[:use_tls]
134
128
  end
135
129
 
136
130
  # Returns true if the connection has advertised TLS support, or false if
@@ -181,9 +175,9 @@ class Remailer::Connection < EventMachine::Connection
181
175
  @messages << message
182
176
 
183
177
  # If the connection is ready to send...
184
- if (@state == :ready)
178
+ if (@interpreter and @interpreter.state == :ready)
185
179
  # ...send the message right away.
186
- send_queued_message!
180
+ after_ready
187
181
  end
188
182
  end
189
183
 
@@ -204,20 +198,12 @@ class Remailer::Connection < EventMachine::Connection
204
198
  # flagging the connection as estasblished.
205
199
  def connection_completed
206
200
  @timeout_at = nil
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
215
201
  end
216
202
 
217
203
  # This implements the EventMachine::Connection#unbind method to capture
218
204
  # a connection closed event.
219
205
  def unbind
220
- @state = :closed
206
+ @interpreter = nil
221
207
 
222
208
  if (@active_message)
223
209
  if (callback = @active_message[:callback])
@@ -226,355 +212,135 @@ class Remailer::Connection < EventMachine::Connection
226
212
  end
227
213
  end
228
214
 
215
+ # This implements the EventMachine::Connection#receive_data method that
216
+ # is called each time new data is received from the socket.
229
217
  def receive_data(data)
230
- # FIX: Buffer the data anyway.
231
-
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
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
218
+ reset_timeout!
269
219
 
270
- @buffer = (@buffer[line_index + CRLF_LENGTH, @buffer.length] || '')
220
+ @buffer ||= ''
221
+ @buffer << data
222
+
223
+ if (@interpreter)
224
+ @interpreter.process(@buffer) do |reply|
225
+ debug_notification(:receive, "[#{@interpreter.label}] #{reply.inspect}")
271
226
  end
227
+ else
228
+ error_notification(:out_of_band, "Receiving data before a protocol has been established.")
272
229
  end
273
230
  end
274
231
 
275
232
  def post_init
276
- @state = :connecting
277
-
278
233
  EventMachine.add_periodic_timer(1) do
279
234
  check_for_timeouts!
280
235
  end
281
236
  end
237
+
238
+ def state
239
+ @interpreter and @interpreter.state
240
+ end
282
241
 
283
- protected
284
242
  def send_line(line = '')
243
+ reset_timeout!
244
+
285
245
  send_data(line + CRLF)
286
246
 
287
247
  debug_notification(:send, line.inspect)
288
248
  end
289
249
 
290
- # Returns true if the reply has been completed, or false if it is still
291
- # in the process of being received.
292
- def reply_complete?
293
- !!@reply_complete
294
- end
295
-
296
250
  def resolve_hostname(hostname)
297
251
  # FIXME: Elminitate this potentially blocking call by using an async
298
252
  # resolver if available.
299
253
  record = Socket.gethostbyname(hostname)
300
254
 
301
- debug_notification(:resolved, record && record.last)
255
+ # FIXME: IPv6 Support here
256
+ debug_notification(:resolved, record && record.last.unpack('CCCC').join('.'))
302
257
 
303
258
  record and record.last
304
259
  rescue
305
260
  nil
306
261
  end
307
262
 
308
- def receive_reply(reply)
309
- debug_notification(:reply, reply.inspect)
310
-
311
- return unless (reply)
312
-
313
- if (reply.match(/(\d+)([ \-])(.*)/))
314
- reply_code = $1.to_i
315
- @reply_complete = $2 != '-'
316
- reply_message = $3
317
-
318
- debug_notification(:recv, reply)
319
- end
320
-
321
- # The connection itself will be in a particular state.
322
- case (state)
323
- when :connected
324
- case (reply_code)
325
- when 220
326
- reply_parts = reply_message.split(/\s+/)
327
- @remote = reply_parts.first
328
-
329
- if (reply_parts.include?('ESMTP'))
330
- @state = :sent_ehlo
331
- @protocol = :esmtp
332
- send_line("EHLO #{@options[:hostname]}")
333
- else
334
- @state = :sent_helo
335
- @protocol = :smtp
336
- send_line("HELO #{@options[:hostname]}")
337
- end
338
- else
339
- fail_unanticipated_response!(reply_code, reply_message)
340
- end
341
- when :sent_ehlo
342
- case (reply_code)
343
- when 250
344
- reply_parts = reply_message.split(/\s+/)
345
- case (reply_parts[0].to_s.upcase)
346
- when 'SIZE'
347
- @max_size = reply_parts[1].to_i
348
- when 'PIPELINING'
349
- @pipelining = true
350
- when 'STARTTLS'
351
- @tls_support = true
352
- end
353
-
354
- if (@reply_complete)
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
363
- end
364
- else
365
- fail_unanticipated_response!(reply_code, reply_message)
366
- end
367
- when :sent_helo
368
- case (reply_code)
369
- when 250
370
- enter_ready_state!
371
- else
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)
393
- end
394
- when :sent_mail_from
395
- case (reply_code)
396
- when 250
397
- @state = :sent_rcpt_to
398
- send_line("RCPT TO:#{@active_message[:to]}")
399
- else
400
- fail_unanticipated_response!(reply_code, reply_message)
401
- end
402
- when :sent_rcpt_to
403
- case (reply_code)
404
- when 250
405
- @state = :sent_data
406
- send_line("DATA")
407
-
408
- @data_offset = 0
409
- else
410
- fail_unanticipated_response!(reply_code, reply_message)
411
- end
412
- when :sent_data
413
- case (reply_code)
414
- when 354
415
- @state = :data_sending
416
-
417
- transmit_data!
418
- else
419
- fail_unanticipated_response!(reply_code, reply_message)
420
- end
421
- when :sent_data_content
422
- send_callback(reply_code, reply_message)
423
-
424
- enter_ready_state!
425
- when :sent_quit
426
- case (reply_code)
427
- when 221
428
- @state = :closed
429
- close_connection
430
- else
431
- fail_unanticipated_response!(reply_code, reply_message)
432
- end
433
- when :sent_reset
434
- case (reply_code)
435
- when 250
436
- enter_ready_state!
437
- end
438
- end
263
+ def reset_timeout!
264
+ @timeout_at = Time.now + (@options[:timeout] || DEFAULT_TIMEOUT)
439
265
  end
440
266
 
441
- def enter_proxy_init_state!
442
- debug_notification(:proxy, "Initiating proxy connection through #{@options[:proxy][:host]}")
267
+ def check_for_timeouts!
268
+ return if (!@timeout_at or Time.now < @timeout_at)
443
269
 
444
- socks_methods = [ ]
445
-
446
- if (@options[:proxy][:username])
447
- socks_methods << SOCKS5_METHOD[:username_password]
270
+ error_notification(:timeout, "Connection timed out")
271
+ debug_notification(:timeout, "Connection timed out")
272
+ send_callback(:timeout, "Connection timed out before send could complete")
273
+
274
+ unless (@connected)
275
+ connect_notification(false, "Connection timed out")
448
276
  end
449
-
450
- send_data(
451
- [
452
- SOCKS5_VERSION,
453
- socks_methods.length,
454
- socks_methods
455
- ].flatten.pack('CCC*')
456
- )
457
277
 
458
- @state = :proxy_init
278
+ close_connection
459
279
  end
460
280
 
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
281
+ def pipelining?
282
+ !!@pipelining
486
283
  end
487
-
488
- def enter_proxy_authenticating_state!
489
- debug_notification(:proxy, "Sending proxy authentication")
490
284
 
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
285
+ def tls_support?
286
+ !!@tls_support
506
287
  end
507
288
 
508
- def enter_ready_state!
509
- @state = :ready
510
-
511
- send_queued_message!
289
+ def closed?
290
+ !!@closed
512
291
  end
513
292
 
514
- def enter_sent_auth_state!
515
- send_line("AUTH PLAIN #{self.class.encode_authentication(@options[:username], @options[:password])}")
516
- @state = :sent_auth
293
+ def start_tls
294
+ debug_notification(:tls, "Started")
295
+ super
517
296
  end
518
297
 
519
- def transmit_data!(chunk_size = nil)
520
- data = @active_message[:data]
521
- chunk_size ||= data.length
522
-
523
- # This chunk-based sending will work better when/if EventMachine can be
524
- # configured to support 'writable' notifications on the active socket.
525
- chunk = data[@data_offset, chunk_size]
526
- debug_notification(:send, chunk.inspect)
527
- send_data(self.class.encode_data(data))
528
- @data_offset += chunk_size
529
-
530
- if (@data_offset >= data.length)
531
- @state = :sent_data_content
298
+ def close_connection
299
+ debug_notification(:closed, "Connection closed")
300
+ super
301
+ @closed = true
302
+ end
532
303
 
533
- # Ensure that a blank line is sent after the last bit of email content
534
- # to ensure that the dot is on its own line.
535
- send_line
536
- send_line(".")
537
- end
304
+ def use_socks5_interpreter!
305
+ @interpreter = Remailer::Connection::Socks5Interpreter.new(:delegate => self)
538
306
  end
539
-
540
- def reset_timeout!
541
- @timeout_at = Time.now + (@options[:timeout] || DEFAULT_TIMEOUT)
307
+
308
+ def use_smtp_interpreter!
309
+ @interpreter = Remailer::Connection::SmtpInterpreter.new(:delegate => self)
542
310
  end
543
311
 
544
- def send_queued_message!
312
+ def after_proxy_connected
313
+ use_smtp_interpreter!
314
+ end
315
+
316
+ def after_ready
545
317
  return if (@active_message)
546
318
 
547
319
  reset_timeout!
548
-
320
+
549
321
  if (@active_message = @messages.shift)
550
- @state = :sent_mail_from
551
- send_line("MAIL FROM:#{@active_message[:from]}")
322
+ if (@interpreter.state == :ready)
323
+ @interpreter.enter_state(:send)
324
+ end
552
325
  elsif (@options[:close])
553
326
  if (callback = @options[:after_complete])
554
327
  callback.call
555
328
  end
556
-
557
- send_line("QUIT")
558
- @state = :sent_quit
329
+
330
+ @interpreter.enter_state(:quit)
559
331
  end
560
332
  end
561
333
 
562
- def check_for_timeouts!
563
- return if (!@timeout_at or Time.now < @timeout_at)
564
-
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
-
569
- @state = :timeout
570
-
571
- unless (@connected)
572
- connect_notification(false, "Connection timed out")
573
- end
334
+ def after_message_sent(reply_code, reply_message)
335
+ send_callback(reply_code, reply_message)
574
336
 
575
- close_connection
337
+ @active_message = nil
576
338
  end
577
339
 
340
+ def interpreter_entered_state(interpreter, state)
341
+ debug_notification(:state, "#{interpreter.label.downcase}=#{state}")
342
+ end
343
+
578
344
  def send_notification(type, code, message)
579
345
  case (callback = @options[type])
580
346
  when nil, false
@@ -588,8 +354,8 @@ protected
588
354
  end
589
355
  end
590
356
 
591
- def connect_notification(code, message)
592
- send_notification(:connect, code, message)
357
+ def connect_notification(code, message = nil)
358
+ send_notification(:connect, code, message || self.remote)
593
359
  end
594
360
 
595
361
  def error_notification(code, message)
@@ -612,15 +378,4 @@ protected
612
378
  end
613
379
  end
614
380
  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)
620
-
621
- @active_message = nil
622
-
623
- @state = :sent_reset
624
- send_line("RESET")
625
- end
626
381
  end