remailer 0.2.1 → 0.3.0

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