remailer 0.4.21 → 0.5.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.
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