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.
- data/README.rdoc +25 -19
- data/VERSION +1 -1
- data/lib/remailer.rb +7 -1
- data/lib/remailer/{connection.rb → abstract_connection.rb} +63 -167
- data/lib/remailer/constants.rb +10 -0
- data/lib/remailer/email_address.rb +41 -0
- data/lib/remailer/imap.rb +6 -0
- data/lib/remailer/imap/client.rb +228 -0
- data/lib/remailer/imap/client/interpreter.rb +101 -0
- data/lib/remailer/imap/server.rb +15 -0
- data/lib/remailer/imap/server/interpreter.rb +2 -0
- data/lib/remailer/interpreter.rb +7 -6
- data/lib/remailer/interpreter/state_proxy.rb +20 -2
- data/lib/remailer/smtp.rb +6 -0
- data/lib/remailer/smtp/client.rb +329 -0
- data/lib/remailer/{connection/smtp_interpreter.rb → smtp/client/interpreter.rb} +4 -4
- data/lib/remailer/smtp/server.rb +130 -0
- data/lib/remailer/smtp/server/interpreter.rb +237 -0
- data/lib/remailer/smtp/server/transaction.rb +29 -0
- data/lib/remailer/socks5.rb +5 -0
- data/lib/remailer/socks5/client.rb +5 -0
- data/lib/remailer/{connection/socks5_interpreter.rb → socks5/client/interpreter.rb} +3 -2
- data/lib/remailer/support.rb +5 -0
- data/remailer.gemspec +27 -9
- data/test/unit/remailer_imap_client_interpreter_test.rb +14 -0
- data/test/unit/remailer_imap_client_test.rb +125 -0
- data/test/unit/{remailer_connection_smtp_interpreter_test.rb → remailer_smtp_client_interpreter_test.rb} +33 -33
- data/test/unit/{remailer_connection_test.rb → remailer_smtp_client_test.rb} +11 -11
- data/test/unit/remailer_smtp_server_test.rb +83 -0
- data/test/unit/{remailer_connection_socks5_interpreter_test.rb → remailer_socks5_client_interpreter_test.rb} +25 -17
- metadata +29 -11
@@ -1,34 +1,52 @@
|
|
1
1
|
class Remailer::Interpreter::StateProxy
|
2
|
-
|
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
|
-
|
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,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::
|
1
|
+
class Remailer::SMTP::Client::Interpreter < Remailer::Interpreter
|
2
2
|
# == Constants ============================================================
|
3
|
-
|
4
|
-
|
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(
|
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
|