Class: Remailer::AbstractConnection

Inherits:
EventMachine::Connection
  • Object
show all
Includes:
EventMachine::Deferrable, Constants
Defined in:
lib/remailer/abstract_connection.rb

Direct Known Subclasses

IMAP::Client, SMTP::Client

Defined Under Namespace

Classes: CallbackArgumentsRequired

Constant Summary collapse

DEFAULT_TIMEOUT =
60
NOTIFICATIONS =
[
  :debug,
  :error,
  :connect
].freeze

Constants included from Constants

Constants::CRLF, Constants::IMAPS_PORT, Constants::LINE_REGEXP, Constants::SMTP_PORT, Constants::SOCKS5_PORT

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(options) ⇒ AbstractConnection

EventMachine will call this constructor and it is not to be called directly. Use the Remailer::Connection.open method to facilitate the correct creation of a new connection.



129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
# File 'lib/remailer/abstract_connection.rb', line 129

def initialize(options)
  # Throwing exceptions inside this block is going to cause EventMachine
  # to malfunction in a spectacular way and hide the actual exception. To
  # allow for debugging, exceptions are dumped to STDERR as a last resort.
  @options = options
  @hostname = @options[:hostname] || Socket.gethostname
  @timeout = @options[:timeout] || self.class.default_timeout
  @timed_out = false

  @active_message = nil
  @established = false
  @connected = false
  @closed = false
  @unbound = false
  @connecting_to_proxy = false

  @messages = [ ]

  NOTIFICATIONS.each do |type|
    callback = @options[type]

    if (callback.is_a?(Proc))
      self.class.warn_about_arguments(callback, (2..2))
    end
  end

  debug_notification(:options, @options.inspect)

  reset_timeout!

  self.after_initialize
  
rescue Object => e
  self.class.report_exception(e, @options)

  STDERR.puts "#{e.class}: #{e}" rescue nil
end

Instance Attribute Details

#errorObject (readonly)

Returns the value of attribute error.



21
22
23
# File 'lib/remailer/abstract_connection.rb', line 21

def error
  @error
end

#error_messageObject (readonly)

Returns the value of attribute error_message.



21
22
23
# File 'lib/remailer/abstract_connection.rb', line 21

def error_message
  @error_message
end

#optionsObject

Properties ===========================================================



20
21
22
# File 'lib/remailer/abstract_connection.rb', line 20

def options
  @options
end

Class Method Details

.default_timeoutObject

Defines the default timeout for connect operations.



30
31
32
# File 'lib/remailer/abstract_connection.rb', line 30

def self.default_timeout
  DEFAULT_TIMEOUT
end

.establish!(host_name, host_port, options) ⇒ Object



82
83
84
85
86
87
88
89
# File 'lib/remailer/abstract_connection.rb', line 82

def self.establish!(host_name, host_port, options)
  EventMachine.connect(host_name, host_port, self, options)

rescue EventMachine::ConnectionError => e
  self.report_exception(e, options)

  false
end

.open(host, options = nil, &block) ⇒ Object

Opens a connection to a specific server. Options can be specified:

  • port => Numerical port number

  • require_tls => If true will fail connections to non-TLS capable servers (default is false)

  • username => Username to authenticate with the server

  • password => Password to authenticate with the server

  • use_tls => Will use TLS if availble (default is true)

  • debug => Where to send debugging output (IO or Proc)

  • connect => Where to send a connection notification (IO or Proc)

  • error => Where to send errors (IO or Proc)

  • on_connect => Called upon successful connection (Proc)

  • on_error => Called upon connection error (Proc)

  • on_disconnect => Called when connection is closed (Proc)

A block can be supplied in which case it will stand in as the :connect option. The block will recieve a first argument that is the status of the connection, and an optional second that is a diagnostic message.



50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
# File 'lib/remailer/abstract_connection.rb', line 50

def self.open(host, options = nil, &block)
  options ||= { }
  options[:host] = host
  options[:port] ||= self.default_port

  unless (options.key?(:use_tls))
    options[:use_tls] = true
  end

  if (block_given?)
    options[:connect] = block
  end
  
  host_name = host
  host_port = options[:port]
  
  if (proxy_options = options[:proxy])
    host_name = proxy_options[:host]
    host_port = proxy_options[:port] || SOCKS5_PORT
  end

  establish!(host_name, host_port, options)
end

.report_exception(e, options) ⇒ Object

Handles callbacks driven by exceptions before an instance could be created.



92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
# File 'lib/remailer/abstract_connection.rb', line 92

def self.report_exception(e, options)
  case (options[:connect])
  when Proc
    options[:connect].call(false, e.to_s)
  when IO
    options[:connect].puts(e.to_s)
  end
  
  case (options[:on_error])
  when Proc
    options[:on_error].call(e.to_s)
  when IO
    options[:on_error].puts(e.to_s)
  end

  case (options[:debug])
  when Proc
    options[:debug].call(:error, e.to_s)
  when IO
    options[:debug].puts(e.to_s)
  end
  
  case (options[:error])
  when Proc
    options[:error].call(:connect_error, e.to_s)
  when IO
    options[:error].puts(e.to_s)
  end
  
  false
end

.warn_about_arguments(proc, range) ⇒ Object

Warns about supplying a Proc which does not appear to accept the required number of arguments.



76
77
78
79
80
# File 'lib/remailer/abstract_connection.rb', line 76

def self.warn_about_arguments(proc, range)
  unless (range.include?(proc.arity) or proc.arity == -1)
    STDERR.puts "Callback must accept #{[ range.min, range.max ].uniq.join(' to ')} arguments but accepts #{proc.arity}"
  end
end

Instance Method Details

#after_complete(&block) ⇒ Object



167
168
169
170
171
172
173
# File 'lib/remailer/abstract_connection.rb', line 167

def after_complete(&block)
  if (block_given?)
    @options[:after_complete] = block
  elsif (@options[:after_complete])
    @options[:after_complete].call
  end
end

#after_readyObject



418
419
420
421
422
# File 'lib/remailer/abstract_connection.rb', line 418

def after_ready
  @established = true
  
  reset_timeout!
end

#auth_support?(type = nil) ⇒ Boolean

Returns true if the connection has advertised authentication support, or false if not availble or could not be detected. If type is specified, returns true only if that type is supported, false otherwise.

Returns:

  • (Boolean)


189
190
191
192
193
194
195
196
# File 'lib/remailer/abstract_connection.rb', line 189

def auth_support?(type = nil)
  case (type)
  when nil
    !!@auth_support
  else
    !!(@auth_support&.include?(type))
  end
end

#cancel_timer!Object



337
338
339
340
341
342
# File 'lib/remailer/abstract_connection.rb', line 337

def cancel_timer!
  if (@timer)
    @timer.cancel
    @timer = nil
  end
end

#check_for_timeouts!Object

Checks for a timeout condition, and if one is detected, will close the connection and send appropriate callbacks.



346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
# File 'lib/remailer/abstract_connection.rb', line 346

def check_for_timeouts!
  return if (!@timeout_at or Time.now < @timeout_at or @timed_out)

  @timed_out = true
  @timeout_at = nil

  if (@connected and @active_message)
    message_callback(:timeout, "Response timed out before send could complete")
    error_notification(:timeout, "Response timed out")
    debug_notification(:timeout, "Response timed out")
    send_callback(:on_error)
  elsif (!@connected)
    remote_options = @options
    interpreter = @interpreter
    
    if (self.proxy_connection_initiated?)
      remote_options = @options[:proxy]
    end
    
    message = "Timed out before a connection could be established to #{remote_options[:host]}:#{remote_options[:port]}"
    
    if (interpreter)
      message << " using #{interpreter.label}"
    end
    
    connect_notification(false, message)
    debug_notification(:timeout, message)
    error_notification(:timeout, message)

    send_callback(:on_error)
  else
    interpreter = @interpreter

    if (interpreter and interpreter.respond_to?(:close))
      interpreter.close
    else
      send_callback(:on_disconnect)
    end
  end

  self.close_connection
end

#close_connectionObject Also known as: close

EventMachine: Closes down the connection.



400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
# File 'lib/remailer/abstract_connection.rb', line 400

def close_connection
  return if (@closed)

  unless (@timed_out)
    send_callback(:on_disconnect)
  end

  debug_notification(:closed, "Connection closed")
  
  super

  @connected = false
  @closed = true
  @timeout_at = nil
  @interpreter = nil
end

#closed?Boolean

Returns true if the connection has been closed, false otherwise.

Returns:

  • (Boolean)


390
391
392
# File 'lib/remailer/abstract_connection.rb', line 390

def closed?
  !!@closed
end

#connect_notification(code, message = nil) ⇒ Object



453
454
455
456
457
458
459
460
461
# File 'lib/remailer/abstract_connection.rb', line 453

def connect_notification(code, message = nil)
  @connected = code

  send_notification(:connect, code, message || self.remote)
  
  if (code)
    send_callback(:on_connect)
  end
end

#connected?Boolean

Returns:

  • (Boolean)


449
450
451
# File 'lib/remailer/abstract_connection.rb', line 449

def connected?
  @connected
end

#connection_completedObject

This implements the EventMachine::Connection#completed method by flagging the connection as estasblished.



227
228
229
# File 'lib/remailer/abstract_connection.rb', line 227

def connection_completed
  self.reset_timeout!
end

#debug_notification(code, message) ⇒ Object



470
471
472
# File 'lib/remailer/abstract_connection.rb', line 470

def debug_notification(code, message)
  send_notification(:debug, code, message)
end

#error?Boolean

Returns true if an error has occurred, false otherwise.

Returns:

  • (Boolean)


395
396
397
# File 'lib/remailer/abstract_connection.rb', line 395

def error?
  !!@error
end

#error_notification(code, message) ⇒ Object



463
464
465
466
467
468
# File 'lib/remailer/abstract_connection.rb', line 463

def error_notification(code, message)
  @error = code
  @error_message = message

  send_notification(:error, code, message)
end

#interpreter_entered_state(interpreter, state) ⇒ Object

– Callbacks and Notifications ——————————————



426
427
428
# File 'lib/remailer/abstract_connection.rb', line 426

def interpreter_entered_state(interpreter, state)
  debug_notification(:state, "#{interpreter.label.downcase}=#{state}")
end

#message_callback(reply_code, reply_message) ⇒ Object



474
475
476
477
478
479
480
481
482
483
484
485
486
487
# File 'lib/remailer/abstract_connection.rb', line 474

def message_callback(reply_code, reply_message)
  active_message = @active_message
  
  if (callback = (active_message and active_message[:callback]))
    # The callback is screened in advance when assigned to ensure that it
    # has only 1 or 2 arguments. There should be no else here.
    case (callback.arity)
    when 2
      callback.call(reply_code, reply_message)
    when 1
      callback.call(reply_code)
    end
  end
end

#post_initObject



277
278
279
# File 'lib/remailer/abstract_connection.rb', line 277

def post_init
  self.set_timer!
end

#proxy_connection_initiated!Object



217
218
219
# File 'lib/remailer/abstract_connection.rb', line 217

def proxy_connection_initiated!
  @connecting_to_proxy = false
end

#proxy_connection_initiated?Boolean

Returns:

  • (Boolean)


221
222
223
# File 'lib/remailer/abstract_connection.rb', line 221

def proxy_connection_initiated?
  !!@connecting_to_proxy
end

#receive_data(data = nil) ⇒ Object

This implements the EventMachine::Connection#receive_data method that is called each time new data is received from the socket.



256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
# File 'lib/remailer/abstract_connection.rb', line 256

def receive_data(data = nil)
  reset_timeout!

  @buffer ||= ''
  @buffer << data if (data)

  if (interpreter = @interpreter)
    interpreter.process(@buffer) do |reply|
      debug_notification(:receive, "[#{interpreter.label}] #{reply.inspect}")
    end
  else
    error_notification(:out_of_band, "Receiving data before a protocol has been established.")
  end
  
rescue Object => e
  self.class.report_exception(e, @options)
  STDERR.puts("[#{e.class}] #{e}") rescue nil

  raise e
end

#requires_authentication?Boolean

Returns true if the connection will require authentication to complete, that is a username has been supplied in the options, or false otherwise.

Returns:

  • (Boolean)


206
207
208
# File 'lib/remailer/abstract_connection.rb', line 206

def requires_authentication?
  @options[:username] and !@options[:username].empty?
end

#reset_timeout!Object

Resets the timeout time. Returns the time at which a timeout will occur.



321
322
323
# File 'lib/remailer/abstract_connection.rb', line 321

def reset_timeout!
  @timeout_at = Time.now + @timeout
end

#resolve_hostname(hostname) ⇒ Object



301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
# File 'lib/remailer/abstract_connection.rb', line 301

def resolve_hostname(hostname)
  record = Socket.gethostbyname(hostname)
  
  # FIXME: IPv6 Support here
  address = (record and record[3])
  
  if (address)
    debug_notification(:resolver, "Address #{hostname} resolved as #{address.unpack('CCCC').join('.')}")
  else
    debug_notification(:resolver, "Address #{hostname} could not be resolved")
  end
  
  yield(address) if (block_given?)

  address
rescue
  nil
end

#send_callback(type) ⇒ Object



489
490
491
492
493
494
495
496
497
498
# File 'lib/remailer/abstract_connection.rb', line 489

def send_callback(type)
  if (callback = @options[type])
    case (callback.arity)
    when 1
      callback.call(self)
    else
      callback.call
    end
  end
end

#send_line(line = '') ⇒ Object

Sends a single line to the remote host with the appropriate CR+LF delmiter at the end.



293
294
295
296
297
298
299
# File 'lib/remailer/abstract_connection.rb', line 293

def send_line(line = '')
  reset_timeout!

  send_data(line + CRLF)

  debug_notification(:send, line.inspect)
end

#send_notification(type, code, message) ⇒ Object



430
431
432
433
434
435
436
437
438
439
440
441
# File 'lib/remailer/abstract_connection.rb', line 430

def send_notification(type, code, message)
  case (callback = @options[type])
  when nil, false
    # No notification in this case
  when Proc
    callback.call(code, message)
  when IO
    callback.puts("%s: %s" % [ code.to_s, message ])
  else
    STDERR.puts("%s: %s" % [ code.to_s, message ])
  end
end

#set_timer!Object



331
332
333
334
335
# File 'lib/remailer/abstract_connection.rb', line 331

def set_timer!
  @timer = EventMachine.add_periodic_timer(1) do
    self.check_for_timeouts!
  end
end

#start_tlsObject

EventMachine: Enables TLS support on the connection.



444
445
446
447
# File 'lib/remailer/abstract_connection.rb', line 444

def start_tls
  debug_notification(:tls, "Started")
  super
end

#stateObject

Returns the current state of the active interpreter, or nil if no state is assigned.



283
284
285
286
287
288
289
# File 'lib/remailer/abstract_connection.rb', line 283

def state
  if (interpreter = @interpreter)
    @interpreter.state
  else
    nil
  end
end

#time_remaningObject

Returns the number of seconds remaining until a timeout will occur, or nil if no time-out is pending.



327
328
329
# File 'lib/remailer/abstract_connection.rb', line 327

def time_remaning
  @timeout_at and (@timeout_at.to_i - Time.now.to_i)
end

#timeout=(value) ⇒ Object

Reassigns the timeout which is specified in seconds. Values equal to or less than zero are ignored and a default is used instead.



212
213
214
215
# File 'lib/remailer/abstract_connection.rb', line 212

def timeout=(value)
  @timeout = value.to_i
  @timeout = DEFAULT_TIMEOUT if (@timeout <= 0)
end

#tls_support?Boolean

Returns true if the connection has advertised TLS support, or false if not availble or could not be detected.

Returns:

  • (Boolean)


182
183
184
# File 'lib/remailer/abstract_connection.rb', line 182

def tls_support?
  !!@tls_support
end

#unbindObject

This implements the EventMachine::Connection#unbind method to capture a connection closed event.



233
234
235
236
237
238
239
240
241
242
243
244
245
246
# File 'lib/remailer/abstract_connection.rb', line 233

def unbind
  return if (@unbound)

  self.cancel_timer!

  self.after_unbind

  @unbound = true
  @connected = false
  @timeout_at = nil
  @interpreter = nil

  send_callback(:on_disconnect)
end

#unbound?Boolean

Returns true if the connection has been unbound by EventMachine, false otherwise.

Returns:

  • (Boolean)


250
251
252
# File 'lib/remailer/abstract_connection.rb', line 250

def unbound?
  !!@unbound
end

#use_tls?Boolean

Returns true if the connection requires TLS support, or false otherwise.

Returns:

  • (Boolean)


176
177
178
# File 'lib/remailer/abstract_connection.rb', line 176

def use_tls?
  !!@options[:use_tls]
end

#using_proxy?Boolean

Returns true if the connection will be using a proxy to connect, false otherwise.

Returns:

  • (Boolean)


200
201
202
# File 'lib/remailer/abstract_connection.rb', line 200

def using_proxy?
  !!@options[:proxy]
end