remailer 0.4.21 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (31) hide show
  1. data/README.rdoc +25 -19
  2. data/VERSION +1 -1
  3. data/lib/remailer.rb +7 -1
  4. data/lib/remailer/{connection.rb → abstract_connection.rb} +63 -167
  5. data/lib/remailer/constants.rb +10 -0
  6. data/lib/remailer/email_address.rb +41 -0
  7. data/lib/remailer/imap.rb +6 -0
  8. data/lib/remailer/imap/client.rb +228 -0
  9. data/lib/remailer/imap/client/interpreter.rb +101 -0
  10. data/lib/remailer/imap/server.rb +15 -0
  11. data/lib/remailer/imap/server/interpreter.rb +2 -0
  12. data/lib/remailer/interpreter.rb +7 -6
  13. data/lib/remailer/interpreter/state_proxy.rb +20 -2
  14. data/lib/remailer/smtp.rb +6 -0
  15. data/lib/remailer/smtp/client.rb +329 -0
  16. data/lib/remailer/{connection/smtp_interpreter.rb → smtp/client/interpreter.rb} +4 -4
  17. data/lib/remailer/smtp/server.rb +130 -0
  18. data/lib/remailer/smtp/server/interpreter.rb +237 -0
  19. data/lib/remailer/smtp/server/transaction.rb +29 -0
  20. data/lib/remailer/socks5.rb +5 -0
  21. data/lib/remailer/socks5/client.rb +5 -0
  22. data/lib/remailer/{connection/socks5_interpreter.rb → socks5/client/interpreter.rb} +3 -2
  23. data/lib/remailer/support.rb +5 -0
  24. data/remailer.gemspec +27 -9
  25. data/test/unit/remailer_imap_client_interpreter_test.rb +14 -0
  26. data/test/unit/remailer_imap_client_test.rb +125 -0
  27. data/test/unit/{remailer_connection_smtp_interpreter_test.rb → remailer_smtp_client_interpreter_test.rb} +33 -33
  28. data/test/unit/{remailer_connection_test.rb → remailer_smtp_client_test.rb} +11 -11
  29. data/test/unit/remailer_smtp_server_test.rb +83 -0
  30. data/test/unit/{remailer_connection_socks5_interpreter_test.rb → remailer_socks5_client_interpreter_test.rb} +25 -17
  31. metadata +29 -11
@@ -1,34 +1,52 @@
1
1
  class Remailer::Interpreter::StateProxy
2
- STATIC_CLASSES = [ String, Fixnum, NilClass, TrueClass, FalseClass, Float ].freeze
2
+ # == Constants ============================================================
3
+
4
+ # == Instance Methods =====================================================
3
5
 
6
+ # Creates a new state proxy object with a particular set of options. An
7
+ # optional supplied block can be used to perform additional configuration,
8
+ # and will be evaluated within the context of this new object.
4
9
  def initialize(options, &block)
5
10
  @options = options
6
11
 
7
12
  instance_eval(&block) if (block_given?)
8
13
  end
9
14
 
15
+ # Defines a parser specification.
10
16
  def parse(spec = nil, &block)
11
17
  @options[:parser] = Remailer::Interpreter.parse(spec, &block)
12
18
  end
13
19
 
20
+ # Defines a block that will execute when the state is entered.
14
21
  def enter(&block)
15
22
  (@options[:enter] ||= [ ]) << block
16
23
  end
17
24
 
25
+ # Defines an interpreter block that will execute if the given response
26
+ # condition is met.
18
27
  def interpret(response, &block)
19
28
  (@options[:interpret] ||= [ ]) << [ response, block ]
20
29
  end
21
30
 
31
+ # Defines a default behavior that will trigger in the event no interpreter
32
+ # definition was triggered first.
22
33
  def default(&block)
23
34
  (@options[:default] ||= [ ]) << block
24
35
  end
25
36
 
37
+ # Defines a block that will execute when the state is left.
26
38
  def leave(&block)
27
39
  (@options[:leave] ||= [ ]) << block
28
40
  end
29
41
 
42
+ # Terminates the interpreter after this state has been entered. Will execute
43
+ # a block if one is supplied.
30
44
  def terminate(&block)
31
- (@options[:terminate] ||= [ ]) << block
45
+ @options[:terminate] ||= [ ]
46
+
47
+ if (block_given?)
48
+ @options[:terminate] << block
49
+ end
32
50
  end
33
51
 
34
52
  protected
@@ -0,0 +1,6 @@
1
+ module Remailer::SMTP
2
+ # == Submodules ===========================================================
3
+
4
+ autoload(:Client, 'remailer/smtp/client')
5
+ autoload(:Server, 'remailer/smtp/server')
6
+ end
@@ -0,0 +1,329 @@
1
+ require 'socket'
2
+ require 'eventmachine'
3
+
4
+ class Remailer::SMTP::Client < Remailer::AbstractConnection
5
+ # == Submodules ===========================================================
6
+
7
+ autoload(:Interpreter, 'remailer/smtp/client/interpreter')
8
+
9
+ # == Constants ============================================================
10
+
11
+ include Remailer::Constants
12
+
13
+ DEFAULT_TIMEOUT = 5
14
+
15
+ # == Properties ===========================================================
16
+
17
+ attr_accessor :active_message
18
+ attr_accessor :remote, :max_size, :protocol, :hostname
19
+ attr_accessor :pipelining, :tls_support, :auth_support
20
+ attr_accessor :timeout
21
+ attr_accessor :options
22
+ attr_reader :error, :error_message
23
+
24
+ # == Extensions ===========================================================
25
+
26
+ include EventMachine::Deferrable
27
+
28
+ # == Class Methods ========================================================
29
+
30
+ def self.default_timeout
31
+ DEFAULT_TIMEOUT
32
+ end
33
+
34
+ def self.default_port
35
+ SMTP_PORT
36
+ end
37
+
38
+ # Opens a connection to a specific SMTP server. Options can be specified:
39
+ # * port => Numerical port number (default is 25)
40
+ # * require_tls => If true will fail connections to non-TLS capable
41
+ # servers (default is false)
42
+ # * username => Username to authenticate with the SMTP server (optional)
43
+ # * password => Password to authenticate with the SMTP server (optional)
44
+ # * use_tls => Will use TLS if availble (default is true)
45
+ # * debug => Where to send debugging output (IO or Proc)
46
+ # * connect => Where to send a connection notification (IO or Proc)
47
+ # * error => Where to send errors (IO or Proc)
48
+ # * on_connect => Called upon successful connection (Proc)
49
+ # * on_error => Called upon connection error (Proc)
50
+ # * on_disconnect => Called when connection is closed (Proc)
51
+ # A block can be supplied in which case it will stand in as the :connect
52
+ # option. The block will recieve a first argument that is the status of
53
+ # the connection, and an optional second that is a diagnostic message.
54
+ def self.open(smtp_server, options = nil, &block)
55
+ super(smtp_server, options, &block)
56
+ end
57
+
58
+ # == Instance Methods =====================================================
59
+
60
+ # Called by AbstractConnection at the end of the initialize procedure
61
+ def after_initialize
62
+ @protocol = :smtp
63
+
64
+ if (using_proxy?)
65
+ proxy_connection_initiated!
66
+ use_socks5_interpreter!
67
+ else
68
+ use_smtp_interpreter!
69
+ end
70
+ end
71
+
72
+ # Closes the connection after all of the queued messages have been sent.
73
+ def close_when_complete!
74
+ @options[:close] = true
75
+ end
76
+
77
+ # Sends an email message through the connection at the earliest opportunity.
78
+ # A callback block can be supplied that will be executed when the message
79
+ # has been sent, an unexpected result occurred, or the send timed out.
80
+ def send_email(from, to, data, &block)
81
+ if (block_given?)
82
+ self.class.warn_about_arguments(block, 1..2)
83
+ end
84
+
85
+ message = {
86
+ :from => from,
87
+ :to => to,
88
+ :data => data,
89
+ :callback => block
90
+ }
91
+
92
+ @messages << message
93
+
94
+ # If the connection is ready to send...
95
+ if (@interpreter and @interpreter.state == :ready)
96
+ # ...send the message right away.
97
+ after_ready
98
+ end
99
+ end
100
+
101
+ # Tests the validity of an email address through the connection at the
102
+ # earliest opportunity. A callback block can be supplied that will be
103
+ # executed when the address has been tested, an unexpected result occurred,
104
+ # or the request timed out.
105
+ def test_email(from, to, &block)
106
+ if (block_given?)
107
+ self.class.warn_about_arguments(block, 1..2)
108
+ end
109
+
110
+ message = {
111
+ :from => from,
112
+ :to => to,
113
+ :test => true,
114
+ :callback => block
115
+ }
116
+
117
+ @messages << message
118
+
119
+ # If the connection is ready to send...
120
+ if (@interpreter and @interpreter.state == :ready)
121
+ # ...send the message right away.
122
+ after_ready
123
+ end
124
+ end
125
+
126
+ def after_unbind
127
+ if (@active_message)
128
+ debug_notification(:disconnect, "Disconnected by remote before transaction could be completed.")
129
+
130
+ if (callback = @active_message[:callback])
131
+ callback.call(nil)
132
+
133
+ @active_message = nil
134
+ end
135
+ elsif (@closed)
136
+ debug_notification(:disconnect, "Disconnected from remote.")
137
+ elsif (!@established)
138
+ error_notification(:hangup, "Disconnected from remote before fully established.")
139
+ else
140
+ debug_notification(:disconnect, "Disconnected by remote while connection was idle.")
141
+ end
142
+ end
143
+
144
+ # Returns true if the connection has been unbound by EventMachine, false
145
+ # otherwise.
146
+ def unbound?
147
+ !!@unbound
148
+ end
149
+
150
+ # This implements the EventMachine::Connection#receive_data method that
151
+ # is called each time new data is received from the socket.
152
+ def receive_data(data)
153
+ reset_timeout!
154
+
155
+ @buffer ||= ''
156
+ @buffer << data
157
+
158
+ if (interpreter = @interpreter)
159
+ interpreter.process(@buffer) do |reply|
160
+ debug_notification(:receive, "[#{interpreter.label}] #{reply.inspect}")
161
+ end
162
+ else
163
+ error_notification(:out_of_band, "Receiving data before a protocol has been established.")
164
+ end
165
+ end
166
+
167
+ def post_init
168
+ @timer = EventMachine.add_periodic_timer(1) do
169
+ check_for_timeouts!
170
+ end
171
+ end
172
+
173
+ #
174
+ def detach
175
+ @timer.cancel
176
+ super
177
+ end
178
+
179
+ # Returns the current state of the active interpreter, or nil if no state
180
+ # is assigned.
181
+ def state
182
+ if (interpreter = @interpreter)
183
+ @interpreter.state
184
+ else
185
+ nil
186
+ end
187
+ end
188
+
189
+ # Sends a single line to the remote host with the appropriate CR+LF
190
+ # delmiter at the end.
191
+ def send_line(line = '')
192
+ reset_timeout!
193
+
194
+ send_data(line + CRLF)
195
+
196
+ debug_notification(:send, line.inspect)
197
+ end
198
+
199
+ def resolve_hostname(hostname)
200
+ record = Socket.gethostbyname(hostname)
201
+
202
+ # FIXME: IPv6 Support here
203
+ address = (record and record[3])
204
+
205
+ if (address)
206
+ debug_notification(:resolver, "Address #{hostname} resolved as #{address.unpack('CCCC').join('.')}")
207
+ else
208
+ debug_notification(:resolver, "Address #{hostname} could not be resolved")
209
+ end
210
+
211
+ yield(address) if (block_given?)
212
+
213
+ address
214
+ rescue
215
+ nil
216
+ end
217
+
218
+ # Resets the timeout time. Returns the time at which a timeout will occur.
219
+ def reset_timeout!
220
+ @timeout_at = Time.now + @timeout
221
+ end
222
+
223
+ # Returns the number of seconds remaining until a timeout will occur, or
224
+ # nil if no time-out is pending.
225
+ def time_remaning
226
+ @timeout_at and (@timeout_at.to_i - Time.now.to_i)
227
+ end
228
+
229
+ # Checks for a timeout condition, and if one is detected, will close the
230
+ # connection and send appropriate callbacks.
231
+ def check_for_timeouts!
232
+ return if (!@timeout_at or Time.now < @timeout_at or @timed_out)
233
+
234
+ @timed_out = true
235
+ @timeout_at = nil
236
+
237
+ if (@connected and @active_message)
238
+ message_callback(:timeout, "Response timed out before send could complete")
239
+ error_notification(:timeout, "Response timed out")
240
+ debug_notification(:timeout, "Response timed out")
241
+ send_callback(:on_error)
242
+ elsif (!@connected)
243
+ remote_options = @options
244
+ interpreter = @interpreter
245
+
246
+ if (self.proxy_connection_initiated?)
247
+ remote_options = @options[:proxy]
248
+ end
249
+
250
+ message = "Timed out before a connection could be established to #{remote_options[:host]}:#{remote_options[:port]}"
251
+
252
+ if (interpreter)
253
+ message << " using #{interpreter.label}"
254
+ end
255
+
256
+ connect_notification(false, message)
257
+ debug_notification(:timeout, message)
258
+ error_notification(:timeout, message)
259
+
260
+ send_callback(:on_error)
261
+ else
262
+ interpreter = @interpreter
263
+
264
+ if (interpreter and interpreter.respond_to?(:close))
265
+ interpreter.close
266
+ else
267
+ send_callback(:on_disconnect)
268
+ end
269
+ end
270
+
271
+ close_connection
272
+ end
273
+
274
+ # Returns true if pipelining support has been detected on the connection,
275
+ # false otherwise.
276
+ def pipelining?
277
+ !!@pipelining
278
+ end
279
+
280
+ # Returns true if pipelining support has been detected on the connection,
281
+ # false otherwise.
282
+ def tls_support?
283
+ !!@tls_support
284
+ end
285
+
286
+ # Returns true if the connection has been closed, false otherwise.
287
+ def closed?
288
+ !!@closed
289
+ end
290
+
291
+ # Returns true if an error has occurred, false otherwise.
292
+ def error?
293
+ !!@error
294
+ end
295
+
296
+ # Switches to use the SMTP interpreter for all subsequent communication
297
+ def use_smtp_interpreter!
298
+ @interpreter = Interpreter.new(:delegate => self)
299
+ end
300
+
301
+ # Callback receiver for when the proxy connection has been completed.
302
+ def after_proxy_connected
303
+ use_smtp_interpreter!
304
+ end
305
+
306
+ def after_ready
307
+ super
308
+
309
+ return if (@active_message)
310
+
311
+ if (@active_message = @messages.shift)
312
+ if (@interpreter.state == :ready)
313
+ @interpreter.enter_state(:send)
314
+ end
315
+ elsif (@options[:close])
316
+ if (callback = @options[:after_complete])
317
+ callback.call
318
+ end
319
+
320
+ @interpreter.enter_state(:quit)
321
+ end
322
+ end
323
+
324
+ def after_message_sent(reply_code, reply_message)
325
+ message_callback(reply_code, reply_message)
326
+
327
+ @active_message = nil
328
+ end
329
+ end
@@ -1,7 +1,7 @@
1
- class Remailer::Connection::SmtpInterpreter < Remailer::Interpreter
1
+ class Remailer::SMTP::Client::Interpreter < Remailer::Interpreter
2
2
  # == Constants ============================================================
3
-
4
- LINE_REGEXP = /^.*?\r?\n/.freeze
3
+
4
+ include Remailer::Constants
5
5
 
6
6
  # == Properties ===========================================================
7
7
 
@@ -10,7 +10,7 @@ class Remailer::Connection::SmtpInterpreter < Remailer::Interpreter
10
10
  # Expands a standard SMTP reply into three parts: Numerical code, message
11
11
  # and a boolean indicating if this reply is continued on a subsequent line.
12
12
  def self.split_reply(reply)
13
- reply.match(/(\d+)([ \-])(.*)/) and [ $1.to_i, $3, $2 == '-' ? :continued : nil ].compact
13
+ reply.match(/^(\d+)([ \-])(.*)/) and [ $1.to_i, $3, $2 == '-' ? :continued : nil ].compact
14
14
  end
15
15
 
16
16
  # Encodes the given user authentication paramters as a Base64-encoded
@@ -0,0 +1,130 @@
1
+ require 'socket'
2
+ require 'eventmachine'
3
+
4
+ class Remailer::SMTP::Server < EventMachine::Protocols::LineAndTextProtocol
5
+ # == Submodules ===========================================================
6
+
7
+ autoload(:Interpreter, 'remailer/smtp/server/interpreter')
8
+ autoload(:Transaction, 'remailer/smtp/server/transaction')
9
+
10
+ # == Constants ============================================================
11
+
12
+ DEFAULT_BIND_ADDR = '0.0.0.0'.freeze
13
+
14
+ # == Extensions ===========================================================
15
+
16
+ include Remailer::Constants
17
+
18
+ # == Properties ===========================================================
19
+
20
+ attr_accessor :logger
21
+ attr_reader :server_name, :quirks
22
+ attr_reader :remote_ip, :remote_port
23
+ attr_reader :local_ip, :local_port
24
+ attr_reader :local_config
25
+
26
+ attr_accessor :private_key_path
27
+ attr_accessor :ssl_cert_path
28
+
29
+ # == Class Methods ========================================================
30
+
31
+ # This returns the hostname for the specified IP if one is to be assigned.
32
+ # The default is to return the IP as-is, but this can be customized in
33
+ # a subclass.
34
+ def self.hostname(ip)
35
+ ip
36
+ end
37
+
38
+ def self.hostname_for_ip(ip)
39
+ nil
40
+ end
41
+
42
+ def self.bind(bind_addr = nil, port = nil, options = nil)
43
+ EventMachine.start_server(
44
+ bind_addr || DEFAULT_BIND_ADDR,
45
+ port || SMTP_PORT,
46
+ self,
47
+ options
48
+ )
49
+ end
50
+
51
+ # == Instance Methods =====================================================
52
+
53
+ def initialize(options = nil)
54
+ super
55
+
56
+ @options = options || { }
57
+
58
+ @remote_port, @remote_ip = Socket.unpack_sockaddr_in(get_peername)
59
+ @local_port, @local_ip = Socket.unpack_sockaddr_in(get_sockname)
60
+
61
+ @server_name = @options[:server_name] || self.class.hostname(@local_ip) || @local_ip
62
+
63
+ log(:debug, "Connection from #{@remote_ip}:#{@remote_port} to #{@local_ip}:#{@local_port}")
64
+
65
+ @on_transaction = @options[:on_transaction]
66
+ @on_connect = @options[:on_connect]
67
+ end
68
+
69
+ def post_init
70
+ super
71
+
72
+ @interpreter = Interpreter.new(:delegate => self)
73
+
74
+ if (@on_connect)
75
+ @on_connect.call(@remote_ip)
76
+ end
77
+ end
78
+
79
+ def on_transaction
80
+ @on_transaction = Proc.new
81
+ end
82
+
83
+ def receive_line(line)
84
+ @interpreter.process(line)
85
+
86
+ rescue Object => e
87
+ STDERR.puts("#{e.class}: #{e}")
88
+ end
89
+
90
+ def log(level, message)
91
+ @logger and @logger.send(level, message)
92
+ end
93
+
94
+ def unbind
95
+ super
96
+
97
+ log(:debug, "Connection from #{@remote_ip} to #{@local_ip} closed")
98
+ end
99
+
100
+ def send_line(line)
101
+ send_data(line + CRLF)
102
+ end
103
+
104
+ def tls?
105
+ ENV['TLS'] and self.private_key_path and self.ssl_cert_path
106
+ end
107
+
108
+ def remote_host
109
+ @remote_host
110
+ end
111
+
112
+ # This is called with the transaction state established by the SMTP
113
+ # client. The return value should be the response code and message
114
+ # sent back to the client.
115
+ def receive_transaction(transaction)
116
+ if (@on_transaction)
117
+ @on_transaction.call(transaction)
118
+ end
119
+
120
+ [ true, "250 Message received" ]
121
+ end
122
+
123
+ def check_for_timeout!
124
+ @interpreter.check_for_timeout!
125
+ end
126
+
127
+ def validate_hostname(remote_host)
128
+ yield(true) if (block_given?)
129
+ end
130
+ end