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
@@ -0,0 +1,237 @@
|
|
1
|
+
class Remailer::SMTP::Server::Interpreter < Remailer::Interpreter
|
2
|
+
# == State Definitions ====================================================
|
3
|
+
|
4
|
+
default do |error|
|
5
|
+
delegate.send_line("500 Invalid command")
|
6
|
+
end
|
7
|
+
|
8
|
+
state :initialized do
|
9
|
+
enter do
|
10
|
+
self.send_banner
|
11
|
+
|
12
|
+
enter_state(:reset)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
state :reset do
|
17
|
+
enter do
|
18
|
+
self.reset_transaction!
|
19
|
+
|
20
|
+
enter_state(:ready)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
state :ready do
|
25
|
+
interpret(/^\s*EHLO\s+(\S+)\s*$/) do |remote_host|
|
26
|
+
delegate.validate_hostname(remote_host) do |valid|
|
27
|
+
if (valid)
|
28
|
+
delegate.log(:debug, "#{delegate.remote_ip}:#{delegate.remote_port} to #{delegate.local_ip}:#{delegate.local_port} Accepting connection from #{remote_host}")
|
29
|
+
@remote_host = remote_host
|
30
|
+
|
31
|
+
delegate.send_line("250-#{delegate.server_name} Hello #{delegate.remote_host} [#{delegate.remote_ip}]")
|
32
|
+
delegate.send_line("250-AUTH PLAIN")
|
33
|
+
delegate.send_line("250-SIZE 35651584")
|
34
|
+
delegate.send_line("250-STARTTLS") if (delegate.tls?)
|
35
|
+
delegate.send_line("250 OK")
|
36
|
+
else
|
37
|
+
delegate.log(:debug, "#{delegate.remote_ip}:#{delegate.remote_port} to #{delegate.local_ip}:#{delegate.local_port} Rejecting connection from #{remote_host} because of invalid FQDN")
|
38
|
+
delegate.send_line("504 Need fully qualified hostname")
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
interpret(/^\s*HELO\s+(\S+)\s*$/) do |remote_host|
|
44
|
+
delegate.validate_hostname(remote_host) do |valid|
|
45
|
+
if (valid)
|
46
|
+
delegate.log(:debug, "#{delegate.remote_ip}:#{delegate.remote_port} to #{delegate.local_ip}:#{delegate.local_port} Accepting connection from #{remote_host}")
|
47
|
+
@remote_host = remote_host
|
48
|
+
|
49
|
+
delegate.send_line("250 #{delegate.server_name} Hello #{delegate.remote_host} [#{delegate.remote_ip}]")
|
50
|
+
else
|
51
|
+
delegate.log(:debug, "#{delegate.remote_ip}:#{delegate.remote_port} to #{delegate.local_ip}:#{delegate.local_port} Rejecting connection from #{remote_host} because of invalid FQDN")
|
52
|
+
delegate.send_line("504 Need fully qualified hostname")
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
interpret(/^\s*MAIL\s+FROM:\s*<([^>]+)>\s*/) do |address|
|
58
|
+
if (Remailer::EmailAddress.valid?(address))
|
59
|
+
accept, message = will_accept_sender(address)
|
60
|
+
|
61
|
+
if (accept)
|
62
|
+
@transaction.sender = address
|
63
|
+
end
|
64
|
+
|
65
|
+
delegate.send_line(message)
|
66
|
+
else
|
67
|
+
delegate.send_line("501 Email address is not RFC compliant")
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
interpret(/^\s*RCPT\s+TO:\s*<([^>]+)>\s*/) do |address|
|
72
|
+
if (@transaction.sender)
|
73
|
+
if (Remailer::EmailAddress.valid?(address))
|
74
|
+
accept, message = will_accept_recipient(address)
|
75
|
+
|
76
|
+
if (accept)
|
77
|
+
@transaction.recipients ||= [ ]
|
78
|
+
@transaction.recipients << address
|
79
|
+
end
|
80
|
+
|
81
|
+
delegate.send_line(message)
|
82
|
+
else
|
83
|
+
delegate.send_line("501 Email address is not RFC compliant")
|
84
|
+
end
|
85
|
+
else
|
86
|
+
delegate.send_line("503 Sender not specified")
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
interpret(/^\s*AUTH\s+PLAIN\s+(.*)\s*$/) do |auth|
|
91
|
+
# 235 2.7.0 Authentication successful
|
92
|
+
delegate.send("235 whatever")
|
93
|
+
end
|
94
|
+
|
95
|
+
interpret(/^\s*AUTH\s+PLAIN\s*$/) do
|
96
|
+
# Multi-line authentication method
|
97
|
+
enter_state(:auth_plain)
|
98
|
+
end
|
99
|
+
|
100
|
+
interpret(/^\s*STARTTLS\s*$/) do
|
101
|
+
if (@tls_started)
|
102
|
+
delegate.send_line("454 TLS already started")
|
103
|
+
elsif (delegate.tls?)
|
104
|
+
delegate.send_line("220 TLS ready to start")
|
105
|
+
delegate.start_tls(
|
106
|
+
:private_key_file => Remailer::SMTP::Server.private_key_path,
|
107
|
+
:cert_chain_file => Remailer::SMTP::Server.ssl_cert_path
|
108
|
+
)
|
109
|
+
|
110
|
+
@tls_started = true
|
111
|
+
else
|
112
|
+
delegate.send_line("421 TLS not supported")
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
interpret(/^\s*DATA\s*$/) do
|
117
|
+
if (@transaction.sender)
|
118
|
+
else
|
119
|
+
delegate.send_line("503 valid RCPT command must precede DATA")
|
120
|
+
end
|
121
|
+
|
122
|
+
enter_state(:data)
|
123
|
+
delegate.send_line("354 Supply message data")
|
124
|
+
end
|
125
|
+
|
126
|
+
interpret(/^\s*NOOP\s*$/) do |remote_host|
|
127
|
+
delegate.send_line("250 OK")
|
128
|
+
end
|
129
|
+
|
130
|
+
interpret(/^\s*RSET\s*$/) do |remote_host|
|
131
|
+
delegate.send_line("250 Reset OK")
|
132
|
+
|
133
|
+
enter_state(:reset)
|
134
|
+
end
|
135
|
+
|
136
|
+
interpret(/^\s*QUIT\s*$/) do
|
137
|
+
delegate.send_line("221 #{delegate.server_name} closing connection")
|
138
|
+
|
139
|
+
delegate.close_connection(true)
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
state :data do
|
144
|
+
interpret(/^\.$/) do
|
145
|
+
accept, message = will_accept_transaction(@transaction)
|
146
|
+
|
147
|
+
if (accept)
|
148
|
+
accept, message = delegate.receive_transaction(@transaction)
|
149
|
+
|
150
|
+
delegate.send_line(message)
|
151
|
+
else
|
152
|
+
delegate.send_line(message)
|
153
|
+
end
|
154
|
+
|
155
|
+
self.reset_transaction!
|
156
|
+
|
157
|
+
enter_state(:ready)
|
158
|
+
end
|
159
|
+
|
160
|
+
default do |line|
|
161
|
+
# RFC5321 4.5.2 - Leading dot is removed if line has content
|
162
|
+
|
163
|
+
@transaction.data << (line.sub(/^\./, '') + Remailer::Constants::CRLF)
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
state :auth_plain do
|
168
|
+
# Receive a single line of authentication
|
169
|
+
# ...
|
170
|
+
end
|
171
|
+
|
172
|
+
state :reply do
|
173
|
+
enter do
|
174
|
+
# Random delay if required
|
175
|
+
delegate.send_line(@reply)
|
176
|
+
end
|
177
|
+
|
178
|
+
default do
|
179
|
+
delegate.send_line("554 SMTP Synchronization Error")
|
180
|
+
enter_state(:ready)
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
state :timeout do
|
185
|
+
enter do
|
186
|
+
delegate.send_line("420 Idle connection closed")
|
187
|
+
|
188
|
+
delegate.close_connection(true)
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
# == Instance Methods =====================================================
|
193
|
+
|
194
|
+
def reset_transaction!
|
195
|
+
@transaction = Remailer::SMTP::Server::Transaction.new
|
196
|
+
end
|
197
|
+
|
198
|
+
def send_banner
|
199
|
+
delegate.send_line("220 #{delegate.server_name} Remailer ESMTP Server Ready")
|
200
|
+
end
|
201
|
+
|
202
|
+
def reset_ttl!
|
203
|
+
@timeout_at = Time.now + self.connection_ttl
|
204
|
+
end
|
205
|
+
|
206
|
+
def enter_state(state)
|
207
|
+
self.reset_ttl!
|
208
|
+
|
209
|
+
super(state)
|
210
|
+
end
|
211
|
+
|
212
|
+
def connection_ttl
|
213
|
+
10
|
214
|
+
end
|
215
|
+
|
216
|
+
def ttl_expired?
|
217
|
+
@timeout_at ? (Time.now > @timeout_at) : false
|
218
|
+
end
|
219
|
+
|
220
|
+
def check_for_timeout!
|
221
|
+
if (self.ttl_expired?)
|
222
|
+
enter_state(:timeout)
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
def will_accept_sender(sender)
|
227
|
+
[ true, "250 Accepted" ]
|
228
|
+
end
|
229
|
+
|
230
|
+
def will_accept_recipient(recipient)
|
231
|
+
[ true, "250 Accepted" ]
|
232
|
+
end
|
233
|
+
|
234
|
+
def will_accept_transaction(transaction)
|
235
|
+
[ true, "250 Accepted" ]
|
236
|
+
end
|
237
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
class Remailer::SMTP::Server::Transaction
|
2
|
+
# == Constants ============================================================
|
3
|
+
|
4
|
+
ATTRIBUTES = [ :sender, :recipients, :data ].freeze
|
5
|
+
|
6
|
+
# == Properties ===========================================================
|
7
|
+
|
8
|
+
attr_accessor *ATTRIBUTES
|
9
|
+
|
10
|
+
# == Class Methods ========================================================
|
11
|
+
|
12
|
+
# == Instance Methods =====================================================
|
13
|
+
|
14
|
+
def initialize(options = nil)
|
15
|
+
case (options)
|
16
|
+
when Remailer::SMTP::Server::Transaction
|
17
|
+
ATTRIBUTES.each do |attribute|
|
18
|
+
instance_variable_set("@#{attribute}", options.send(attribute))
|
19
|
+
end
|
20
|
+
when Hash
|
21
|
+
ATTRIBUTES.each do |attr|
|
22
|
+
instance_variable_set("@#{attribute}", options[attribute])
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
self.recipients = [ self.recipients ].compact.flatten
|
27
|
+
self.data ||= ''
|
28
|
+
end
|
29
|
+
end
|
@@ -1,6 +1,7 @@
|
|
1
|
-
class Remailer::
|
1
|
+
class Remailer::SOCKS5::Client::Interpreter < Remailer::Interpreter
|
2
2
|
# == Constants ============================================================
|
3
3
|
|
4
|
+
# RFC1928 and RFC1929 define these values
|
4
5
|
SOCKS5_VERSION = 5
|
5
6
|
|
6
7
|
SOCKS5_METHOD = {
|
@@ -44,7 +45,7 @@ class Remailer::Connection::Socks5Interpreter < Remailer::Interpreter
|
|
44
45
|
socks_methods << SOCKS5_METHOD[:username_password]
|
45
46
|
end
|
46
47
|
|
47
|
-
proxy_options[:port] ||= Remailer::
|
48
|
+
proxy_options[:port] ||= Remailer::Constants::SOCKS5_PORT
|
48
49
|
|
49
50
|
delegate.debug_notification(:proxy, "Initiating proxy connection through #{proxy_options[:host]}:#{proxy_options[:port]}")
|
50
51
|
|
data/remailer.gemspec
CHANGED
@@ -5,11 +5,11 @@
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = "remailer"
|
8
|
-
s.version = "0.
|
8
|
+
s.version = "0.5.0"
|
9
9
|
|
10
10
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
11
|
s.authors = ["Scott Tadman"]
|
12
|
-
s.date = "
|
12
|
+
s.date = "2012-03-19"
|
13
13
|
s.description = "EventMachine SMTP Mail User Agent"
|
14
14
|
s.email = "scott@twg.ca"
|
15
15
|
s.extra_rdoc_files = [
|
@@ -21,24 +21,42 @@ Gem::Specification.new do |s|
|
|
21
21
|
"Rakefile",
|
22
22
|
"VERSION",
|
23
23
|
"lib/remailer.rb",
|
24
|
-
"lib/remailer/
|
25
|
-
"lib/remailer/
|
26
|
-
"lib/remailer/
|
24
|
+
"lib/remailer/abstract_connection.rb",
|
25
|
+
"lib/remailer/constants.rb",
|
26
|
+
"lib/remailer/email_address.rb",
|
27
|
+
"lib/remailer/imap.rb",
|
28
|
+
"lib/remailer/imap/client.rb",
|
29
|
+
"lib/remailer/imap/client/interpreter.rb",
|
30
|
+
"lib/remailer/imap/server.rb",
|
31
|
+
"lib/remailer/imap/server/interpreter.rb",
|
27
32
|
"lib/remailer/interpreter.rb",
|
28
33
|
"lib/remailer/interpreter/state_proxy.rb",
|
34
|
+
"lib/remailer/smtp.rb",
|
35
|
+
"lib/remailer/smtp/client.rb",
|
36
|
+
"lib/remailer/smtp/client/interpreter.rb",
|
37
|
+
"lib/remailer/smtp/server.rb",
|
38
|
+
"lib/remailer/smtp/server/interpreter.rb",
|
39
|
+
"lib/remailer/smtp/server/transaction.rb",
|
40
|
+
"lib/remailer/socks5.rb",
|
41
|
+
"lib/remailer/socks5/client.rb",
|
42
|
+
"lib/remailer/socks5/client/interpreter.rb",
|
43
|
+
"lib/remailer/support.rb",
|
29
44
|
"remailer.gemspec",
|
30
45
|
"test/config.example.rb",
|
31
46
|
"test/helper.rb",
|
32
|
-
"test/unit/
|
33
|
-
"test/unit/
|
34
|
-
"test/unit/remailer_connection_test.rb",
|
47
|
+
"test/unit/remailer_imap_client_interpreter_test.rb",
|
48
|
+
"test/unit/remailer_imap_client_test.rb",
|
35
49
|
"test/unit/remailer_interpreter_state_proxy_test.rb",
|
36
50
|
"test/unit/remailer_interpreter_test.rb",
|
51
|
+
"test/unit/remailer_smtp_client_interpreter_test.rb",
|
52
|
+
"test/unit/remailer_smtp_client_test.rb",
|
53
|
+
"test/unit/remailer_smtp_server_test.rb",
|
54
|
+
"test/unit/remailer_socks5_client_interpreter_test.rb",
|
37
55
|
"test/unit/remailer_test.rb"
|
38
56
|
]
|
39
57
|
s.homepage = "http://github.com/twg/remailer"
|
40
58
|
s.require_paths = ["lib"]
|
41
|
-
s.rubygems_version = "1.8.
|
59
|
+
s.rubygems_version = "1.8.17"
|
42
60
|
s.summary = "Reactor-Ready SMTP Mailer"
|
43
61
|
|
44
62
|
if s.respond_to? :specification_version then
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require File.expand_path(File.join(*%w[ .. helper ]), File.dirname(__FILE__))
|
2
|
+
|
3
|
+
class RemailerIMAPClientInterpreterTest < Test::Unit::TestCase
|
4
|
+
def test_split_list_definition
|
5
|
+
assert_mapping(
|
6
|
+
'(\HasChildren \HasNoChildren) "/" "[Gmail]/All Mail"' =>
|
7
|
+
[ %w[ \HasChildren \HasNoChildren ], "/", "[Gmail]/All Mail" ],
|
8
|
+
'(\HasNoChildren) "/" "INBOX"' =>
|
9
|
+
[ %w[ \HasNoChildren ], "/", "INBOX" ]
|
10
|
+
) do |string|
|
11
|
+
Remailer::IMAP::Client::Interpreter.split_list_definition(string)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
require File.expand_path(File.join(*%w[ .. helper ]), File.dirname(__FILE__))
|
2
|
+
|
3
|
+
class RemailerIMAPClientTest < Test::Unit::TestCase
|
4
|
+
def setup
|
5
|
+
STDERR.sync = true
|
6
|
+
end
|
7
|
+
|
8
|
+
def test_connect
|
9
|
+
engine do
|
10
|
+
debug = { }
|
11
|
+
|
12
|
+
client = Remailer::IMAP::Client.open(
|
13
|
+
TestConfig.imap_server[:host],
|
14
|
+
:debug => STDERR,
|
15
|
+
:connect => lambda { |success, host| connected_host = host }
|
16
|
+
)
|
17
|
+
|
18
|
+
assert client
|
19
|
+
|
20
|
+
assert_eventually(30) do
|
21
|
+
client.connected?
|
22
|
+
end
|
23
|
+
|
24
|
+
capabilities = nil
|
25
|
+
|
26
|
+
client.capability do |list|
|
27
|
+
capabilities = list
|
28
|
+
end
|
29
|
+
|
30
|
+
assert_eventually(10) do
|
31
|
+
capabilities
|
32
|
+
end
|
33
|
+
|
34
|
+
assert_equal %w[ IMAP4rev1 UNSELECT IDLE NAMESPACE QUOTA ID XLIST CHILDREN X-GM-EXT-1 XYZZY SASL-IR AUTH=XOAUTH ], capabilities
|
35
|
+
|
36
|
+
# -- LOGIN ------------------------------------------------------------
|
37
|
+
|
38
|
+
login_status = nil
|
39
|
+
|
40
|
+
client.login(TestConfig.smtp_server[:username], TestConfig.smtp_server[:password]) do |status, message|
|
41
|
+
login_status = status
|
42
|
+
end
|
43
|
+
|
44
|
+
assert_eventually(10) do
|
45
|
+
login_status
|
46
|
+
end
|
47
|
+
|
48
|
+
assert_equal 'OK', login_status
|
49
|
+
|
50
|
+
# -- LIST -------------------------------------------------------------
|
51
|
+
|
52
|
+
list = nil
|
53
|
+
|
54
|
+
client.list do |status, message, additional|
|
55
|
+
list = additional
|
56
|
+
end
|
57
|
+
|
58
|
+
assert_eventually(10) do
|
59
|
+
list
|
60
|
+
end
|
61
|
+
|
62
|
+
assert list.find { |i| i[0] == 'INBOX' }
|
63
|
+
|
64
|
+
# -- EXAMINE -----------------------------------------------------------
|
65
|
+
|
66
|
+
examine_status = nil
|
67
|
+
|
68
|
+
client.examine('INBOX') do |status, message, additional|
|
69
|
+
examine_status = status
|
70
|
+
end
|
71
|
+
|
72
|
+
assert_eventually(10) do
|
73
|
+
examine_status
|
74
|
+
end
|
75
|
+
|
76
|
+
assert_equal 'OK', examine_status
|
77
|
+
|
78
|
+
# -- SELECT -----------------------------------------------------------
|
79
|
+
|
80
|
+
select_status = nil
|
81
|
+
|
82
|
+
client.select('INBOX') do |status, message, additional|
|
83
|
+
select_status = status
|
84
|
+
end
|
85
|
+
|
86
|
+
assert_eventually(10) do
|
87
|
+
select_status
|
88
|
+
end
|
89
|
+
|
90
|
+
assert_equal 'OK', select_status
|
91
|
+
|
92
|
+
# -- IDLE ------------------------------------------------------------
|
93
|
+
|
94
|
+
idle_status = nil
|
95
|
+
|
96
|
+
idle = client.idle do |status|
|
97
|
+
idle_status = status
|
98
|
+
end
|
99
|
+
|
100
|
+
assert_equal nil, idle_status
|
101
|
+
|
102
|
+
idle.call
|
103
|
+
|
104
|
+
assert_eventually(5) do
|
105
|
+
idle_status
|
106
|
+
end
|
107
|
+
|
108
|
+
assert_equal 'OK', idle_status
|
109
|
+
|
110
|
+
# -- FETCH -----------------------------------------------------------
|
111
|
+
|
112
|
+
fetch_status = nil
|
113
|
+
|
114
|
+
client.fetch(2..5) do |status, message, additional|
|
115
|
+
fetch_status = status
|
116
|
+
end
|
117
|
+
|
118
|
+
assert_eventually(10) do
|
119
|
+
fetch_status
|
120
|
+
end
|
121
|
+
|
122
|
+
assert_equal 'OK', fetch_status
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|