remailer 0.2.1 → 0.3.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/VERSION +1 -1
- data/lib/remailer.rb +1 -0
- data/lib/remailer/connection.rb +132 -377
- data/lib/remailer/connection/smtp_interpreter.rb +270 -0
- data/lib/remailer/connection/socks5_interpreter.rb +186 -0
- data/lib/remailer/interpreter.rb +253 -0
- data/lib/remailer/interpreter/state_proxy.rb +43 -0
- data/remailer.gemspec +19 -3
- data/test/config.example.rb +17 -0
- data/test/helper.rb +61 -2
- data/test/unit/remailer_connection_smtp_interpreter_test.rb +347 -0
- data/test/unit/remailer_connection_socks5_interpreter_test.rb +116 -0
- data/test/unit/remailer_connection_test.rb +287 -0
- data/test/unit/remailer_interpreter_state_proxy_test.rb +86 -0
- data/test/unit/remailer_interpreter_test.rb +153 -0
- data/test/unit/remailer_test.rb +2 -223
- metadata +20 -4
@@ -0,0 +1,347 @@
|
|
1
|
+
require File.expand_path(File.join(*%w[ .. helper ]), File.dirname(__FILE__))
|
2
|
+
|
3
|
+
class SmtpDelegate
|
4
|
+
attr_accessor :options, :active_message
|
5
|
+
|
6
|
+
def initialize(options = { })
|
7
|
+
@sent = [ ]
|
8
|
+
@options = options
|
9
|
+
end
|
10
|
+
|
11
|
+
def hostname
|
12
|
+
'localhost.local'
|
13
|
+
end
|
14
|
+
|
15
|
+
def requires_authentication?
|
16
|
+
!!@options[:username]
|
17
|
+
end
|
18
|
+
|
19
|
+
def use_tls?
|
20
|
+
!!@options[:use_tls]
|
21
|
+
end
|
22
|
+
|
23
|
+
def send_line(data = '')
|
24
|
+
@sent << data
|
25
|
+
end
|
26
|
+
|
27
|
+
def start_tls
|
28
|
+
@started_tls = true
|
29
|
+
end
|
30
|
+
|
31
|
+
def started_tls?
|
32
|
+
!!@started_tls
|
33
|
+
end
|
34
|
+
|
35
|
+
def close_connection
|
36
|
+
@closed = true
|
37
|
+
end
|
38
|
+
|
39
|
+
def closed?
|
40
|
+
!!@closed
|
41
|
+
end
|
42
|
+
|
43
|
+
def clear!
|
44
|
+
@sent = [ ]
|
45
|
+
end
|
46
|
+
|
47
|
+
def size
|
48
|
+
@sent.size
|
49
|
+
end
|
50
|
+
|
51
|
+
def read
|
52
|
+
@sent.shift
|
53
|
+
end
|
54
|
+
|
55
|
+
def method_missing(*args)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
class RemailerConnectionSmtpInterpreterTest < Test::Unit::TestCase
|
60
|
+
def test_split_reply
|
61
|
+
assert_mapping(
|
62
|
+
'250 OK' => [ 250, 'OK' ],
|
63
|
+
'250 Long message' => [ 250, 'Long message' ],
|
64
|
+
'OK' => nil,
|
65
|
+
'100-Example' => [ 100, 'Example', :continued ]
|
66
|
+
) do |reply|
|
67
|
+
Remailer::Connection::SmtpInterpreter.split_reply(reply)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def test_parser
|
72
|
+
interpreter = Remailer::Connection::SmtpInterpreter.new
|
73
|
+
|
74
|
+
assert_mapping(
|
75
|
+
"250 OK\r\n" => [ 250, 'OK' ],
|
76
|
+
"250 Long message\r\n" => [ 250, 'Long message' ],
|
77
|
+
"OK\r\n" => nil,
|
78
|
+
"100-Example\r\n" => [ 100, 'Example', :continued ],
|
79
|
+
"100-Example" => nil
|
80
|
+
) do |reply|
|
81
|
+
interpreter.parse(reply.dup)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def test_encode_data
|
86
|
+
sample_data = "Line 1\r\nLine 2\r\n.\r\nLine 3\r\n.Line 4\r\n"
|
87
|
+
|
88
|
+
assert_equal "Line 1\r\nLine 2\r\n..\r\nLine 3\r\n..Line 4\r\n", Remailer::Connection::SmtpInterpreter.encode_data(sample_data)
|
89
|
+
end
|
90
|
+
|
91
|
+
def test_base64
|
92
|
+
assert_mapping(
|
93
|
+
'example' => 'example',
|
94
|
+
"\x7F" => "\x7F",
|
95
|
+
nil => ''
|
96
|
+
) do |example|
|
97
|
+
Remailer::Connection::SmtpInterpreter.base64(example).unpack('m')[0]
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def test_encode_authentication
|
102
|
+
assert_mapping(
|
103
|
+
%w[ tester tester ] => 'AHRlc3RlcgB0ZXN0ZXI='
|
104
|
+
) do |username, password|
|
105
|
+
Remailer::Connection::SmtpInterpreter.encode_authentication(username, password)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def test_defaults
|
110
|
+
interpreter = Remailer::Connection::SmtpInterpreter.new
|
111
|
+
|
112
|
+
assert_equal :initialized, interpreter.state
|
113
|
+
end
|
114
|
+
|
115
|
+
def test_delegate_default_state
|
116
|
+
delegate = SmtpDelegate.new
|
117
|
+
|
118
|
+
assert_equal false, delegate.closed?
|
119
|
+
assert_equal nil, delegate.read
|
120
|
+
assert_equal 0, delegate.size
|
121
|
+
end
|
122
|
+
|
123
|
+
def test_delegate_options
|
124
|
+
delegate = SmtpDelegate.new(:use_tls => true)
|
125
|
+
|
126
|
+
assert_equal true, delegate.use_tls?
|
127
|
+
assert_equal false, delegate.requires_authentication?
|
128
|
+
|
129
|
+
delegate = SmtpDelegate.new(:username => 'test@example.com', :password => 'tester')
|
130
|
+
|
131
|
+
assert_equal false, delegate.use_tls?
|
132
|
+
assert_equal true, delegate.requires_authentication?
|
133
|
+
end
|
134
|
+
|
135
|
+
def test_standard_smtp_connection
|
136
|
+
delegate = SmtpDelegate.new
|
137
|
+
interpreter = Remailer::Connection::SmtpInterpreter.new(:delegate => delegate)
|
138
|
+
|
139
|
+
assert_equal :initialized, interpreter.state
|
140
|
+
|
141
|
+
interpreter.process("220 mail.example.com SMTP Example\r\n")
|
142
|
+
|
143
|
+
assert_equal :helo, interpreter.state
|
144
|
+
assert_equal 'HELO localhost.local', delegate.read
|
145
|
+
|
146
|
+
interpreter.process("250 mail.example.com Hello\r\n")
|
147
|
+
assert_equal :ready, interpreter.state
|
148
|
+
|
149
|
+
interpreter.enter_state(:quit)
|
150
|
+
|
151
|
+
assert_equal :quit, interpreter.state
|
152
|
+
assert_equal 'QUIT', delegate.read
|
153
|
+
|
154
|
+
interpreter.process("221 mail.example.com closing connection\r\n")
|
155
|
+
assert_equal true, delegate.closed?
|
156
|
+
end
|
157
|
+
|
158
|
+
def test_standard_smtp_connection_send_email
|
159
|
+
delegate = SmtpDelegate.new
|
160
|
+
interpreter = Remailer::Connection::SmtpInterpreter.new(:delegate => delegate)
|
161
|
+
|
162
|
+
assert_equal :initialized, interpreter.state
|
163
|
+
|
164
|
+
interpreter.process("220 mail.example.com SMTP Example\r\n")
|
165
|
+
|
166
|
+
assert_equal :helo, interpreter.state
|
167
|
+
assert_equal 'HELO localhost.local', delegate.read
|
168
|
+
|
169
|
+
interpreter.process("250 mail.example.com Hello\r\n")
|
170
|
+
assert_equal :ready, interpreter.state
|
171
|
+
|
172
|
+
interpreter.enter_state(:quit)
|
173
|
+
|
174
|
+
assert_equal :quit, interpreter.state
|
175
|
+
assert_equal 'QUIT', delegate.read
|
176
|
+
|
177
|
+
interpreter.process("221 mail.example.com closing connection\r\n")
|
178
|
+
assert_equal true, delegate.closed?
|
179
|
+
|
180
|
+
delegate.active_message = {
|
181
|
+
:from => 'from@example.com',
|
182
|
+
:to => 'to@example.com',
|
183
|
+
:data => "Subject: Test Message\r\n\r\nThis is a message!\r\n"
|
184
|
+
}
|
185
|
+
|
186
|
+
interpreter.enter_state(:send)
|
187
|
+
|
188
|
+
assert_equal :mail_from, interpreter.state
|
189
|
+
|
190
|
+
assert_equal 'MAIL FROM:from@example.com', delegate.read
|
191
|
+
|
192
|
+
interpreter.process("250 OK\r\n")
|
193
|
+
|
194
|
+
assert_equal :rcpt_to, interpreter.state
|
195
|
+
|
196
|
+
assert_equal 'RCPT TO:to@example.com', delegate.read
|
197
|
+
|
198
|
+
interpreter.process("250 Accepted\r\n")
|
199
|
+
|
200
|
+
assert_equal :data, interpreter.state
|
201
|
+
|
202
|
+
assert_equal 'DATA', delegate.read
|
203
|
+
|
204
|
+
interpreter.process("354 Enter message, ending with \".\" on a line by itself\r\n")
|
205
|
+
|
206
|
+
assert_equal :sending, interpreter.state
|
207
|
+
|
208
|
+
interpreter.process("250 OK id=1PN95Q-00072L-Uw\r\n")
|
209
|
+
|
210
|
+
assert_equal :ready, interpreter.state
|
211
|
+
end
|
212
|
+
|
213
|
+
def test_standard_esmtp_connection
|
214
|
+
delegate = SmtpDelegate.new
|
215
|
+
interpreter = Remailer::Connection::SmtpInterpreter.new(:delegate => delegate)
|
216
|
+
|
217
|
+
assert_equal :initialized, interpreter.state
|
218
|
+
|
219
|
+
interpreter.process("220 mail.example.com ESMTP Exim 4.63\r\n")
|
220
|
+
|
221
|
+
assert_equal :ehlo, interpreter.state
|
222
|
+
assert_equal 'EHLO localhost.local', delegate.read
|
223
|
+
|
224
|
+
interpreter.process("250-mail.example.com Hello\r\n")
|
225
|
+
assert_equal :ehlo, interpreter.state
|
226
|
+
|
227
|
+
interpreter.process("250-SIZE 52428800\r\n")
|
228
|
+
assert_equal :ehlo, interpreter.state
|
229
|
+
|
230
|
+
interpreter.process("250-PIPELINING\r\n")
|
231
|
+
assert_equal :ehlo, interpreter.state
|
232
|
+
|
233
|
+
interpreter.process("250-STARTTLS\r\n")
|
234
|
+
assert_equal :ehlo, interpreter.state
|
235
|
+
|
236
|
+
interpreter.process("250 HELP\r\n")
|
237
|
+
assert_equal :ready, interpreter.state
|
238
|
+
|
239
|
+
interpreter.enter_state(:quit)
|
240
|
+
|
241
|
+
assert_equal :quit, interpreter.state
|
242
|
+
assert_equal 'QUIT', delegate.read
|
243
|
+
|
244
|
+
interpreter.process("221 mail.example.com closing connection\r\n")
|
245
|
+
|
246
|
+
assert_equal :terminated, interpreter.state
|
247
|
+
assert_equal true, delegate.closed?
|
248
|
+
end
|
249
|
+
|
250
|
+
def test_tls_connection_with_support
|
251
|
+
delegate = SmtpDelegate.new(:use_tls => true)
|
252
|
+
interpreter = Remailer::Connection::SmtpInterpreter.new(:delegate => delegate)
|
253
|
+
|
254
|
+
interpreter.process("220 mail.example.com ESMTP Exim 4.63\r\n")
|
255
|
+
assert_equal 'EHLO localhost.local', delegate.read
|
256
|
+
|
257
|
+
interpreter.process("250-mail.example.com Hello\r\n")
|
258
|
+
interpreter.process("250-RANDOMCOMMAND\r\n")
|
259
|
+
interpreter.process("250-EXAMPLECOMMAND\r\n")
|
260
|
+
interpreter.process("250-SIZE 52428800\r\n")
|
261
|
+
interpreter.process("250-PIPELINING\r\n")
|
262
|
+
interpreter.process("250-STARTTLS\r\n")
|
263
|
+
interpreter.process("250 HELP\r\n")
|
264
|
+
|
265
|
+
assert_equal :starttls, interpreter.state
|
266
|
+
assert_equal 'STARTTLS', delegate.read
|
267
|
+
assert_equal false, delegate.started_tls?
|
268
|
+
|
269
|
+
interpreter.process("220 TLS go ahead\r\n")
|
270
|
+
assert_equal true, delegate.started_tls?
|
271
|
+
|
272
|
+
assert_equal :ready, interpreter.state
|
273
|
+
end
|
274
|
+
|
275
|
+
def test_tls_connection_without_support
|
276
|
+
delegate = SmtpDelegate.new(:use_tls => true)
|
277
|
+
interpreter = Remailer::Connection::SmtpInterpreter.new(:delegate => delegate)
|
278
|
+
|
279
|
+
interpreter.process("220 mail.example.com ESMTP Exim 4.63\r\n")
|
280
|
+
assert_equal 'EHLO localhost.local', delegate.read
|
281
|
+
|
282
|
+
interpreter.process("250-mail.example.com Hello\r\n")
|
283
|
+
interpreter.process("250 HELP\r\n")
|
284
|
+
|
285
|
+
assert_equal false, delegate.started_tls?
|
286
|
+
|
287
|
+
assert_equal :ready, interpreter.state
|
288
|
+
end
|
289
|
+
|
290
|
+
def test_basic_plaintext_auth_accepted
|
291
|
+
delegate = SmtpDelegate.new(:username => 'tester@example.com', :password => 'tester')
|
292
|
+
interpreter = Remailer::Connection::SmtpInterpreter.new(:delegate => delegate)
|
293
|
+
|
294
|
+
interpreter.process("220 mail.example.com ESMTP Exim 4.63\r\n")
|
295
|
+
assert_equal 'EHLO localhost.local', delegate.read
|
296
|
+
|
297
|
+
interpreter.process("250-mail.example.com Hello\r\n")
|
298
|
+
interpreter.process("250 HELP\r\n")
|
299
|
+
|
300
|
+
assert_equal false, delegate.started_tls?
|
301
|
+
|
302
|
+
assert_equal :auth, interpreter.state
|
303
|
+
assert_equal "AUTH PLAIN AHRlc3RlckBleGFtcGxlLmNvbQB0ZXN0ZXI=", delegate.read
|
304
|
+
|
305
|
+
interpreter.process("235 Accepted\r\n")
|
306
|
+
|
307
|
+
assert_equal :ready, interpreter.state
|
308
|
+
end
|
309
|
+
|
310
|
+
def test_basic_plaintext_auth_rejected
|
311
|
+
delegate = SmtpDelegate.new(:username => 'tester@example.com', :password => 'tester')
|
312
|
+
interpreter = Remailer::Connection::SmtpInterpreter.new(:delegate => delegate)
|
313
|
+
|
314
|
+
interpreter.process("220 mx.google.com ESMTP\r\n")
|
315
|
+
assert_equal 'EHLO localhost.local', delegate.read
|
316
|
+
|
317
|
+
interpreter.process("250-mx.google.com at your service\r\n")
|
318
|
+
interpreter.process("250 HELP\r\n")
|
319
|
+
|
320
|
+
assert_equal false, delegate.started_tls?
|
321
|
+
|
322
|
+
assert_equal :auth, interpreter.state
|
323
|
+
assert_equal "AUTH PLAIN AHRlc3RlckBleGFtcGxlLmNvbQB0ZXN0ZXI=", delegate.read
|
324
|
+
|
325
|
+
interpreter.process("535-5.7.1 Username and Password not accepted. Learn more at\r\n")
|
326
|
+
interpreter.process("535 5.7.1 http://mail.google.com/support/bin/answer.py?answer=14257\r\n")
|
327
|
+
|
328
|
+
assert_equal '5.7.1 Username and Password not accepted. Learn more at http://mail.google.com/support/bin/answer.py?answer=14257', interpreter.error
|
329
|
+
|
330
|
+
assert_equal :quit, interpreter.state
|
331
|
+
|
332
|
+
interpreter.process("221 2.0.0 closing connection\r\n")
|
333
|
+
|
334
|
+
assert_equal :terminated, interpreter.state
|
335
|
+
assert_equal true, delegate.closed?
|
336
|
+
end
|
337
|
+
|
338
|
+
def test_unexpected_response
|
339
|
+
delegate = SmtpDelegate.new(:username => 'tester@example.com', :password => 'tester')
|
340
|
+
interpreter = Remailer::Connection::SmtpInterpreter.new(:delegate => delegate)
|
341
|
+
|
342
|
+
interpreter.process("530 Go away\r\n")
|
343
|
+
|
344
|
+
assert_equal :terminated, interpreter.state
|
345
|
+
assert_equal true, delegate.closed?
|
346
|
+
end
|
347
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
require File.expand_path(File.join(*%w[ .. helper ]), File.dirname(__FILE__))
|
2
|
+
|
3
|
+
class Socks5Delegate
|
4
|
+
attr_reader :options
|
5
|
+
|
6
|
+
def initialize(options = nil)
|
7
|
+
@sent = [ ]
|
8
|
+
@options = (options or { })
|
9
|
+
end
|
10
|
+
|
11
|
+
def resolve_hostname(hostname)
|
12
|
+
record = Socket.gethostbyname(hostname)
|
13
|
+
|
14
|
+
record and record.last
|
15
|
+
end
|
16
|
+
|
17
|
+
def hostname
|
18
|
+
'localhost.local'
|
19
|
+
end
|
20
|
+
|
21
|
+
def send_data(data)
|
22
|
+
@sent << data
|
23
|
+
end
|
24
|
+
|
25
|
+
def close_connection
|
26
|
+
@closed = true
|
27
|
+
end
|
28
|
+
|
29
|
+
def closed?
|
30
|
+
!!@closed
|
31
|
+
end
|
32
|
+
|
33
|
+
def clear!
|
34
|
+
@sent = [ ]
|
35
|
+
end
|
36
|
+
|
37
|
+
def size
|
38
|
+
@sent.size
|
39
|
+
end
|
40
|
+
|
41
|
+
def read
|
42
|
+
@sent.shift
|
43
|
+
end
|
44
|
+
|
45
|
+
def method_missing(*args)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
class RemailerConnectionSocks5InterpreterTest < Test::Unit::TestCase
|
50
|
+
def test_defaults
|
51
|
+
delegate = Socks5Delegate.new(
|
52
|
+
:proxy => {
|
53
|
+
:host => 'example.net'
|
54
|
+
}
|
55
|
+
)
|
56
|
+
interpreter = Remailer::Connection::Socks5Interpreter.new(:delegate => delegate)
|
57
|
+
|
58
|
+
assert_equal :connect_to_proxy, interpreter.state
|
59
|
+
assert_equal false, delegate.closed?
|
60
|
+
end
|
61
|
+
|
62
|
+
def test_simple_connection
|
63
|
+
delegate = Socks5Delegate.new(
|
64
|
+
:host => '1.2.3.4',
|
65
|
+
:port => 4321,
|
66
|
+
:proxy => {
|
67
|
+
:host => 'example.net'
|
68
|
+
}
|
69
|
+
)
|
70
|
+
interpreter = Remailer::Connection::Socks5Interpreter.new(:delegate => delegate)
|
71
|
+
|
72
|
+
assert_equal :connect_to_proxy, interpreter.state
|
73
|
+
assert_equal false, delegate.closed?
|
74
|
+
|
75
|
+
sent = delegate.read
|
76
|
+
|
77
|
+
assert_equal 2, sent.length
|
78
|
+
|
79
|
+
assert_equal [ Remailer::Connection::Socks5Interpreter::SOCKS5_VERSION, 0 ], sent.unpack('CC')
|
80
|
+
|
81
|
+
reply = [
|
82
|
+
Remailer::Connection::Socks5Interpreter::SOCKS5_VERSION,
|
83
|
+
Remailer::Connection::Socks5Interpreter::SOCKS5_METHOD[:no_auth]
|
84
|
+
].pack('CC')
|
85
|
+
|
86
|
+
interpreter.process(reply)
|
87
|
+
|
88
|
+
assert_equal false, interpreter.error?
|
89
|
+
assert_equal :connect_through_proxy, interpreter.state
|
90
|
+
assert_equal '', reply
|
91
|
+
|
92
|
+
sent = delegate.read
|
93
|
+
|
94
|
+
assert_equal 10, sent.length
|
95
|
+
|
96
|
+
assert_equal [
|
97
|
+
Remailer::Connection::Socks5Interpreter::SOCKS5_VERSION,
|
98
|
+
Remailer::Connection::Socks5Interpreter::SOCKS5_COMMAND[:connect],
|
99
|
+
0,
|
100
|
+
Remailer::Connection::Socks5Interpreter::SOCKS5_ADDRESS_TYPE[:ipv4],
|
101
|
+
[ 1, 2, 3, 4 ].pack('CCCC'),
|
102
|
+
4321
|
103
|
+
], sent.unpack('CCCCA4n')
|
104
|
+
|
105
|
+
interpreter.process([
|
106
|
+
Remailer::Connection::Socks5Interpreter::SOCKS5_VERSION,
|
107
|
+
0, # No error
|
108
|
+
0,
|
109
|
+
Remailer::Connection::Socks5Interpreter::SOCKS5_ADDRESS_TYPE[:ipv4],
|
110
|
+
[ 1, 2, 3, 4 ].pack('CCCC'),
|
111
|
+
4321
|
112
|
+
].pack('CCCCA4n'))
|
113
|
+
|
114
|
+
assert_equal :connected, interpreter.state
|
115
|
+
end
|
116
|
+
end
|