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,41 @@
|
|
1
|
+
# encoding: BINARY
|
2
|
+
#
|
3
|
+
# RFC822 Email Address Regex
|
4
|
+
# --------------------------
|
5
|
+
#
|
6
|
+
# Originally written by Cal Henderson
|
7
|
+
# c.f. http://iamcal.com/publish/articles/php/parsing_email/
|
8
|
+
#
|
9
|
+
# Translated to Ruby by Tim Fletcher, with changes suggested by Dan Kubb.
|
10
|
+
#
|
11
|
+
# Licensed under a Creative Commons Attribution-ShareAlike 2.5 License
|
12
|
+
# http://creativecommons.org/licenses/by-sa/2.5/
|
13
|
+
#
|
14
|
+
|
15
|
+
class Remailer::EmailAddress
|
16
|
+
# == Class Methods ========================================================
|
17
|
+
|
18
|
+
PATTERN =
|
19
|
+
begin
|
20
|
+
qtext = '[^\\x0d\\x22\\x5c\\x80-\\xff]'
|
21
|
+
dtext = '[^\\x0d\\x5b-\\x5d\\x80-\\xff]'
|
22
|
+
atom = '[^\\x00-\\x20\\x22\\x28\\x29\\x2c\\x2e\\x3a-' +
|
23
|
+
'\\x3c\\x3e\\x40\\x5b-\\x5d\\x7f-\\xff]+'
|
24
|
+
quoted_pair = '\\x5c[\\x00-\\x7f]'
|
25
|
+
domain_literal = "\\x5b(?:#{dtext}|#{quoted_pair})*\\x5d"
|
26
|
+
quoted_string = "\\x22(?:#{qtext}|#{quoted_pair})*\\x22"
|
27
|
+
domain_ref = atom
|
28
|
+
sub_domain = "(?:#{domain_ref}|#{domain_literal})"
|
29
|
+
word = "(?:#{atom}|#{quoted_string})"
|
30
|
+
domain = "#{sub_domain}(?:\\x2e#{sub_domain})*"
|
31
|
+
local_part = "#{word}(?:\\x2e#{word})*"
|
32
|
+
addr_spec = "#{local_part}\\x40#{domain}"
|
33
|
+
pattern = /\A#{addr_spec}\z/
|
34
|
+
end
|
35
|
+
|
36
|
+
# == Class Methods ========================================================
|
37
|
+
|
38
|
+
def self.valid?(address)
|
39
|
+
PATTERN.match(address)
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,228 @@
|
|
1
|
+
class Remailer::IMAP::Client < Remailer::AbstractConnection
|
2
|
+
# == Exceptions ===========================================================
|
3
|
+
|
4
|
+
# == Extensions ===========================================================
|
5
|
+
|
6
|
+
include Remailer::Support
|
7
|
+
|
8
|
+
# == Submodules ===========================================================
|
9
|
+
|
10
|
+
autoload(:Interpreter, 'remailer/imap/client/interpreter')
|
11
|
+
|
12
|
+
# == Constants ============================================================
|
13
|
+
|
14
|
+
DEFAUT_TIMEOUT = 60
|
15
|
+
|
16
|
+
# == Properties ===========================================================
|
17
|
+
|
18
|
+
# == Class Methods ========================================================
|
19
|
+
|
20
|
+
def self.default_timeout
|
21
|
+
DEFAULT_TIMEOUT
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.default_port
|
25
|
+
IMAPS_PORT
|
26
|
+
end
|
27
|
+
|
28
|
+
# Opens a connection to a specific IMAP server. Options can be specified:
|
29
|
+
# * port => Numerical port number (default is 993)
|
30
|
+
# * require_tls => If true will fail connections to non-TLS capable
|
31
|
+
# servers (default is false)
|
32
|
+
# * username => Username to authenticate with the IMAP server
|
33
|
+
# * password => Password to authenticate with the IMAP server
|
34
|
+
# * use_tls => Will use TLS if availble (default is true)
|
35
|
+
# * debug => Where to send debugging output (IO or Proc)
|
36
|
+
# * connect => Where to send a connection notification (IO or Proc)
|
37
|
+
# * error => Where to send errors (IO or Proc)
|
38
|
+
# * on_connect => Called upon successful connection (Proc)
|
39
|
+
# * on_error => Called upon connection error (Proc)
|
40
|
+
# * on_disconnect => Called when connection is closed (Proc)
|
41
|
+
# A block can be supplied in which case it will stand in as the :connect
|
42
|
+
# option. The block will recieve a first argument that is the status of
|
43
|
+
# the connection, and an optional second that is a diagnostic message.
|
44
|
+
def self.open(imap_server, options = nil, &block)
|
45
|
+
super(imap_server, options, &block)
|
46
|
+
end
|
47
|
+
|
48
|
+
# == Instance Methods =====================================================
|
49
|
+
|
50
|
+
def after_initialize
|
51
|
+
if (using_proxy?)
|
52
|
+
@connecting_to_proxy = true
|
53
|
+
use_socks5_interpreter!
|
54
|
+
else
|
55
|
+
if (@options[:use_tls])
|
56
|
+
self.start_tls
|
57
|
+
end
|
58
|
+
|
59
|
+
use_imap_interpreter!
|
60
|
+
end
|
61
|
+
|
62
|
+
@command_tags = { }
|
63
|
+
@issued_command = { }
|
64
|
+
end
|
65
|
+
|
66
|
+
# Callback receiver for when the proxy connection has been completed.
|
67
|
+
def after_proxy_connected
|
68
|
+
if (@options[:use_tls])
|
69
|
+
self.start_tls
|
70
|
+
end
|
71
|
+
|
72
|
+
use_imap_interpreter!
|
73
|
+
end
|
74
|
+
|
75
|
+
def after_unbind
|
76
|
+
debug_notification(:disconnect, "Disconnected by remote.")
|
77
|
+
|
78
|
+
@command_tags.each do |tag, callback|
|
79
|
+
callback[1].call(nil)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def receive_response(tag, status, message, additional = nil)
|
84
|
+
if (set = @command_tags.delete(tag))
|
85
|
+
@issued_command.delete(set[0])
|
86
|
+
set[1].call(status, message, additional)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# -- Commands -------------------------------------------------------------
|
91
|
+
|
92
|
+
def capability
|
93
|
+
self.issue_command('CAPABILITY') do |status, message, additional|
|
94
|
+
yield(additional)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def login(username, password)
|
99
|
+
self.issue_command('LOGIN', quoted(username), quoted(password)) do |status, message, additional|
|
100
|
+
yield(status, message, additional)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def list(reference_name = '/', mailbox_name = '*')
|
105
|
+
self.issue_command('LIST', quoted(reference_name), quoted(mailbox_name)) do |status, message, additional|
|
106
|
+
yield(status, message, additional)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def select(mailbox_name = 'INBOX')
|
111
|
+
self.issue_command('SELECT', quoted(mailbox_name)) do |status, message, additional|
|
112
|
+
yield(status, message, additional)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def examine(mailbox_name = 'INBOX')
|
117
|
+
self.issue_command('EXAMINE', quoted(mailbox_name)) do |status, message, additional|
|
118
|
+
yield(status, message, additional)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def status(mailbox_name = 'INBOX', *flags)
|
123
|
+
flags.flatten!
|
124
|
+
flags = [ :uidnext, :messages ] if (flags.empty?)
|
125
|
+
|
126
|
+
# Valid flags are: MESSAGES RECENT UIDNEXT UIDVALIDITY UNSEEN
|
127
|
+
|
128
|
+
self.issue_command('STATUS', quoted(mailbox_name), "(#{flags.collect { |f| f.to_s.upcase }.join(' ')})") do |status, message, additional|
|
129
|
+
yield(status, message, additional)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def noop
|
134
|
+
self.issue_command('NOOP') do |status, message, additional|
|
135
|
+
yield(status, message, additional)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
def fetch(range, *options)
|
140
|
+
fetch_options = { }
|
141
|
+
|
142
|
+
options.each do |option|
|
143
|
+
case (option)
|
144
|
+
when Hash
|
145
|
+
fetch_options.merge(option)
|
146
|
+
else
|
147
|
+
fetch_options[option] = true
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
if (fetch_options.empty?)
|
152
|
+
fetch_options[:all] = true
|
153
|
+
end
|
154
|
+
|
155
|
+
sequence_set =
|
156
|
+
case (range)
|
157
|
+
when Range
|
158
|
+
"#{range.min}:#{range.max}"
|
159
|
+
else
|
160
|
+
range.to_s
|
161
|
+
end
|
162
|
+
|
163
|
+
items =
|
164
|
+
if (fetch_options[:all])
|
165
|
+
'ALL'
|
166
|
+
elsif (fetch_options[:fast])
|
167
|
+
'FAST'
|
168
|
+
elsif (fetch_options[:full])
|
169
|
+
'FULL'
|
170
|
+
elsif (body_options = fetch_options[:body])
|
171
|
+
case (body_options)
|
172
|
+
when Hash
|
173
|
+
# ...
|
174
|
+
else
|
175
|
+
'BODY'
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
self.issue_command('FETCH', sequence_set, items) do |status, message, additional|
|
180
|
+
yield(status, message, additional)
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
def idle
|
185
|
+
self.issue_command('IDLE') do |status, message, additional|
|
186
|
+
yield(status, message, additional)
|
187
|
+
end
|
188
|
+
|
189
|
+
lambda { self.send_line('DONE') }
|
190
|
+
end
|
191
|
+
|
192
|
+
protected
|
193
|
+
# Switches to use the IMAP interpreter for all subsequent communication
|
194
|
+
def use_imap_interpreter!
|
195
|
+
@interpreter = Interpreter.new(:delegate => self)
|
196
|
+
end
|
197
|
+
|
198
|
+
def next_tag
|
199
|
+
@next_tag ||= (rand(1 << 32) << 32) | (1 << 64)
|
200
|
+
|
201
|
+
@next_tag += 1
|
202
|
+
|
203
|
+
@next_tag.to_s(16)
|
204
|
+
end
|
205
|
+
|
206
|
+
def issue_command(*args, &block)
|
207
|
+
tag = self.next_tag
|
208
|
+
|
209
|
+
self.send_line(([ tag ] + args.flatten).join(' '))
|
210
|
+
|
211
|
+
@command_tags[tag] = [ args.first, block ]
|
212
|
+
@issued_command[args.first] = [ tag, block ]
|
213
|
+
|
214
|
+
tag
|
215
|
+
end
|
216
|
+
|
217
|
+
def pending_command_with_tag(tag)
|
218
|
+
set = @command_tags[tag]
|
219
|
+
|
220
|
+
set and set[0] or false
|
221
|
+
end
|
222
|
+
|
223
|
+
def pending_command_of_type(type)
|
224
|
+
set = @issued_command[tag]
|
225
|
+
|
226
|
+
set and set[0] or false
|
227
|
+
end
|
228
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
class Remailer::IMAP::Client::Interpreter < Remailer::Interpreter
|
2
|
+
# == Constants ============================================================
|
3
|
+
|
4
|
+
LINE_REGEXP = /^(.*?)\r?\n/.freeze
|
5
|
+
RESPONSE_REGEXP = /^(\S+)(?:\s+(?:(OK|NO|BAD)\s+)?([^\r\n]*))?\r?\n/.freeze
|
6
|
+
|
7
|
+
# == Class Methods ========================================================
|
8
|
+
|
9
|
+
def self.split_reply(reply)
|
10
|
+
if (m = reply.match(RESPONSE_REGEXP))
|
11
|
+
parts = m.to_a
|
12
|
+
parts.shift
|
13
|
+
|
14
|
+
parts
|
15
|
+
else
|
16
|
+
nil
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.split_list_definition(string)
|
21
|
+
m = string.match(/^\(([^\)]+)\)\s+\"((?:\\\"|\\x[0-9a-fA-F][0-9a-fA-F]|\\[abtnvfr]|.)+)\"\s+\"((?:\\\"|\\x[0-9a-fA-F][0-9a-fA-F]|\\[abtnvfr]|.)+)\"/)
|
22
|
+
|
23
|
+
return unless (m)
|
24
|
+
|
25
|
+
split = m.to_a
|
26
|
+
split.shift
|
27
|
+
split[0] = split[0].split(/\s+/)
|
28
|
+
|
29
|
+
split
|
30
|
+
end
|
31
|
+
|
32
|
+
# == State Definitions ====================================================
|
33
|
+
|
34
|
+
# Based on the RFC3501 specification with extensions
|
35
|
+
|
36
|
+
parse(LINE_REGEXP) do |data|
|
37
|
+
split_reply(data)
|
38
|
+
end
|
39
|
+
|
40
|
+
state :initialized do
|
41
|
+
interpret('*') do |tag, status, message|
|
42
|
+
delegate.connect_notification(status, message)
|
43
|
+
|
44
|
+
enter_state(:connected)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
state :connected do
|
49
|
+
interpret('*') do |tag, status, message|
|
50
|
+
message_parts = message.split(/\s+/)
|
51
|
+
message_key = message_parts.shift
|
52
|
+
|
53
|
+
case (message_key)
|
54
|
+
when 'CAPABILITY'
|
55
|
+
@additional ||= [ ]
|
56
|
+
@additional += message_parts
|
57
|
+
when 'LIST'
|
58
|
+
message = message.sub(/^LIST\s+/, '')
|
59
|
+
|
60
|
+
if (split = self.class.split_list_definition(message))
|
61
|
+
@additional ||= [ ]
|
62
|
+
@additional << split.reverse
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
default do |tag, status, message|
|
68
|
+
delegate.receive_response(tag, status, message, @additional)
|
69
|
+
|
70
|
+
@additional = nil
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
state :fetch do
|
75
|
+
parse(LINE_REGEXP)
|
76
|
+
|
77
|
+
enter do
|
78
|
+
@additional = ''
|
79
|
+
end
|
80
|
+
|
81
|
+
interpret(/^\*\s+(\d+)\s+FETCH\s+(.*)/) do |uid, line|
|
82
|
+
@additional << line
|
83
|
+
end
|
84
|
+
|
85
|
+
interpret(/^(\w+)\s+(OK|NO|BAD)\s+(FETCH\s+.*)/) do |tag, status, message|
|
86
|
+
delegate.receive_response(tag, [ status, message ], @additional)
|
87
|
+
|
88
|
+
@additional = nil
|
89
|
+
end
|
90
|
+
|
91
|
+
default do |line|
|
92
|
+
@additional << line
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# == Instance Methods =====================================================
|
97
|
+
|
98
|
+
def label
|
99
|
+
'IMAP'
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'socket'
|
2
|
+
require 'eventmachine'
|
3
|
+
|
4
|
+
class Remailer::IMAP::Server < EventMachine::Protocols::LineAndTextProtocol
|
5
|
+
# == Submodules ===========================================================
|
6
|
+
|
7
|
+
autoload(:Interpreter, 'remailer/imap/server/imap_interpreter')
|
8
|
+
|
9
|
+
# == Constants ============================================================
|
10
|
+
|
11
|
+
# == Class Methods ========================================================
|
12
|
+
|
13
|
+
# == Instance Methods =====================================================
|
14
|
+
|
15
|
+
end
|
data/lib/remailer/interpreter.rb
CHANGED
@@ -67,9 +67,9 @@ class Remailer::Interpreter
|
|
67
67
|
|
68
68
|
# This is a method to convert a spec and a block into a proper parser
|
69
69
|
# method. If spec is specified, it should be a Fixnum, or a Regexp. A
|
70
|
-
# Fixnum defines a
|
71
|
-
# streams,
|
72
|
-
# is engaged.
|
70
|
+
# Fixnum defines a minimum size to process, useful for packed binary
|
71
|
+
# streams, while a Regexp defines a pattern that must match before the
|
72
|
+
# parser is engaged.
|
73
73
|
def self.create_parser_for_spec(spec, &block)
|
74
74
|
case (spec)
|
75
75
|
when nil
|
@@ -202,17 +202,18 @@ class Remailer::Interpreter
|
|
202
202
|
end
|
203
203
|
|
204
204
|
# Processes a given input string into interpretable tokens, processes these
|
205
|
-
# tokens, and removes them from the input string.
|
206
|
-
# tokens could be found, returns immediately. An optional block can be
|
205
|
+
# tokens, and removes them from the input string. An optional block can be
|
207
206
|
# given that will be called as each interpretable token is discovered with
|
208
207
|
# the token provided as the argument.
|
209
208
|
def process(s)
|
210
209
|
_parser = parser
|
211
210
|
|
212
|
-
while (parsed =
|
211
|
+
while (parsed = instance_exec(s, &_parser))
|
213
212
|
yield(parsed) if (block_given?)
|
214
213
|
|
215
214
|
interpret(*parsed)
|
215
|
+
|
216
|
+
break if (s.empty?)
|
216
217
|
end
|
217
218
|
end
|
218
219
|
|