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
# 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

  @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_completeObject



159
160
161
162
163
164
165
# File 'lib/remailer/abstract_connection.rb', line 159

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

#after_readyObject



410
411
412
413
414
# File 'lib/remailer/abstract_connection.rb', line 410

def after_ready
  @established = true
  
  reset_timeout!
end

#auth_support?Boolean

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

Returns:

  • (Boolean)


180
181
182
# File 'lib/remailer/abstract_connection.rb', line 180

def auth_support?
  !!@auth_support
end

#cancel_timer!Object



323
324
325
326
327
328
# File 'lib/remailer/abstract_connection.rb', line 323

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.



332
333
334
335
336
337
338
339
340
341
342
343
344
345
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
# File 'lib/remailer/abstract_connection.rb', line 332

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.



392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
# File 'lib/remailer/abstract_connection.rb', line 392

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)


376
377
378
# File 'lib/remailer/abstract_connection.rb', line 376

def closed?
  !!@closed
end

#connect_notification(code, message = nil) ⇒ Object



445
446
447
448
449
450
451
452
453
# File 'lib/remailer/abstract_connection.rb', line 445

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)


441
442
443
# File 'lib/remailer/abstract_connection.rb', line 441

def connected?
  @connected
end

#connection_completedObject

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



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

def connection_completed
  self.reset_timeout!
end

#debug_notification(code, message) ⇒ Object



462
463
464
# File 'lib/remailer/abstract_connection.rb', line 462

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

#error?Boolean

Returns true if an error has occurred, false otherwise.

Returns:

  • (Boolean)


381
382
383
# File 'lib/remailer/abstract_connection.rb', line 381

def error?
  !!@error
end

#error_notification(code, message) ⇒ Object



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

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 ——————————————



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

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

#message_callback(reply_code, reply_message) ⇒ Object



466
467
468
469
470
471
472
473
474
475
476
477
478
479
# File 'lib/remailer/abstract_connection.rb', line 466

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



263
264
265
# File 'lib/remailer/abstract_connection.rb', line 263

def post_init
  self.set_timer!
end

#proxy_connection_initiated!Object



203
204
205
# File 'lib/remailer/abstract_connection.rb', line 203

def proxy_connection_initiated!
  @connecting_to_proxy = false
end

#proxy_connection_initiated?Boolean

Returns:

  • (Boolean)


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

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.



242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
# File 'lib/remailer/abstract_connection.rb', line 242

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)


192
193
194
# File 'lib/remailer/abstract_connection.rb', line 192

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.



307
308
309
# File 'lib/remailer/abstract_connection.rb', line 307

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

#resolve_hostname(hostname) ⇒ Object



287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
# File 'lib/remailer/abstract_connection.rb', line 287

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



481
482
483
484
485
486
487
488
489
490
# File 'lib/remailer/abstract_connection.rb', line 481

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.



279
280
281
282
283
284
285
# File 'lib/remailer/abstract_connection.rb', line 279

def send_line(line = '')
  reset_timeout!

  send_data(line + CRLF)

  debug_notification(:send, line.inspect)
end

#send_notification(type, code, message) ⇒ Object



422
423
424
425
426
427
428
429
430
431
432
433
# File 'lib/remailer/abstract_connection.rb', line 422

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



317
318
319
320
321
# File 'lib/remailer/abstract_connection.rb', line 317

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.



386
387
388
389
# File 'lib/remailer/abstract_connection.rb', line 386

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.



269
270
271
272
273
274
275
# File 'lib/remailer/abstract_connection.rb', line 269

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.



313
314
315
# File 'lib/remailer/abstract_connection.rb', line 313

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.



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

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)


174
175
176
# File 'lib/remailer/abstract_connection.rb', line 174

def tls_support?
  !!@tls_support
end

#unbindObject

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



219
220
221
222
223
224
225
226
227
228
229
230
231
232
# File 'lib/remailer/abstract_connection.rb', line 219

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)


236
237
238
# File 'lib/remailer/abstract_connection.rb', line 236

def unbound?
  !!@unbound
end

#use_tls?Boolean

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

Returns:

  • (Boolean)


168
169
170
# File 'lib/remailer/abstract_connection.rb', line 168

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)


186
187
188
# File 'lib/remailer/abstract_connection.rb', line 186

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