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,253 @@
|
|
1
|
+
class Remailer::Interpreter
|
2
|
+
# == Constants ============================================================
|
3
|
+
|
4
|
+
# == Exceptions ===========================================================
|
5
|
+
|
6
|
+
class DefinitionException < Exception; end
|
7
|
+
|
8
|
+
# == Submodules ===========================================================
|
9
|
+
|
10
|
+
autoload(:StateProxy, 'remailer/interpreter/state_proxy')
|
11
|
+
|
12
|
+
# == Properties ===========================================================
|
13
|
+
|
14
|
+
attr_reader :delegate
|
15
|
+
attr_reader :state
|
16
|
+
attr_reader :error
|
17
|
+
|
18
|
+
# == Class Methods ========================================================
|
19
|
+
|
20
|
+
# Defines the initial state for objects of this class.
|
21
|
+
def self.initial_state
|
22
|
+
@initial_state || :initialized
|
23
|
+
end
|
24
|
+
|
25
|
+
# Can be used to reassign the initial state for this class. May be easier
|
26
|
+
# than re-defining the initial_state method.
|
27
|
+
def self.initial_state=(state)
|
28
|
+
@initial_state = state
|
29
|
+
end
|
30
|
+
|
31
|
+
# Returns the states that are defined as a has with their associated
|
32
|
+
# options. The default keys are :initialized and :terminated.
|
33
|
+
def self.states
|
34
|
+
@states ||= {
|
35
|
+
:initialized => { },
|
36
|
+
:terminated => { }
|
37
|
+
}
|
38
|
+
end
|
39
|
+
|
40
|
+
# Returns true if a given state is defined, false otherwise.
|
41
|
+
def self.state_defined?(state)
|
42
|
+
!!self.states[state]
|
43
|
+
end
|
44
|
+
|
45
|
+
# Returns a list of the defined states.
|
46
|
+
def self.states_defined
|
47
|
+
self.states.keys
|
48
|
+
end
|
49
|
+
|
50
|
+
# Defines a new state for this class. A block will be executed in the
|
51
|
+
# context of a StateProxy that is used to provide a simple interface to
|
52
|
+
# the underlying options. A block can contain calls to enter and leave,
|
53
|
+
# or default, which do not require arguments, or interpret, which requries
|
54
|
+
# at least one argument that will be the class-specific object to interpret.
|
55
|
+
# Other paramters may be supplied by the class.
|
56
|
+
def self.state(state, &block)
|
57
|
+
config = self.states[state] = { }
|
58
|
+
|
59
|
+
StateProxy.new(config, &block)
|
60
|
+
end
|
61
|
+
|
62
|
+
def self.parser_for_spec(spec, &block)
|
63
|
+
case (spec)
|
64
|
+
when nil
|
65
|
+
block
|
66
|
+
when Fixnum
|
67
|
+
lambda do |s|
|
68
|
+
if (s.length >= spec)
|
69
|
+
part = s.slice!(0, spec)
|
70
|
+
block.call(part)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
when Regexp
|
74
|
+
lambda do |s|
|
75
|
+
if (m = spec.match(s))
|
76
|
+
part = m.to_s
|
77
|
+
part = s.slice!(0, part.length)
|
78
|
+
block.call(part)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
else
|
82
|
+
raise DefinitionException, "Invalid specification for parse declaration: #{spec.inspect}"
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
# Defines a parser for this interpreter. The supplied block is executed in
|
87
|
+
# the context of a parser instance.
|
88
|
+
def self.parse(spec = nil, &block)
|
89
|
+
@parser = parser_for_spec(spec, &block)
|
90
|
+
end
|
91
|
+
|
92
|
+
# Assigns the default interpreter.
|
93
|
+
def self.default(&block)
|
94
|
+
@default = block if (block_given?)
|
95
|
+
end
|
96
|
+
|
97
|
+
# Assigns the error handler for when a specific interpretation could not be
|
98
|
+
# found and a default was not specified.
|
99
|
+
def self.on_error(&block)
|
100
|
+
@on_error = block
|
101
|
+
end
|
102
|
+
|
103
|
+
# Returns the currently defined parser.
|
104
|
+
def self.default_parser
|
105
|
+
@parser ||= lambda { |s| _s = s.dup; s.replace(''); _s }
|
106
|
+
end
|
107
|
+
|
108
|
+
# Returns the current default_interpreter.
|
109
|
+
def self.default_interpreter
|
110
|
+
@default
|
111
|
+
end
|
112
|
+
|
113
|
+
# Returns the defined error handler
|
114
|
+
def self.on_error_handler
|
115
|
+
@on_error
|
116
|
+
end
|
117
|
+
|
118
|
+
# == Instance Methods =====================================================
|
119
|
+
|
120
|
+
# Creates a new interpreter with an optional set of options. Valid options
|
121
|
+
# include:
|
122
|
+
# * :delegate => Which object to use as a delegate, if applicable.
|
123
|
+
# * :state => What the initial state should be. The default is :initalized
|
124
|
+
def initialize(options = nil)
|
125
|
+
@delegate = (options and options[:delegate])
|
126
|
+
|
127
|
+
enter_state(options && options[:state] || self.class.initial_state)
|
128
|
+
end
|
129
|
+
|
130
|
+
# Enters the given state. Will call the appropriate leave_state trigger if
|
131
|
+
# one is defined for the previous state, and will trigger the callbacks for
|
132
|
+
# entry into the new state. If this state is set as a terminate state, then
|
133
|
+
# an immediate transition to the :terminate state will be performed after
|
134
|
+
# these callbacks.
|
135
|
+
def enter_state(state)
|
136
|
+
if (@state)
|
137
|
+
leave_state(@state)
|
138
|
+
end
|
139
|
+
|
140
|
+
@state = state
|
141
|
+
|
142
|
+
delegate_call(:interpreter_entered_state, self, @state)
|
143
|
+
|
144
|
+
trigger_callbacks(state, :enter)
|
145
|
+
|
146
|
+
# :terminated is the state, :terminate is the trigger.
|
147
|
+
if (@state != :terminated)
|
148
|
+
if (trigger_callbacks(state, :terminate))
|
149
|
+
enter_state(:terminated)
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
# Parses a given string and returns the first interpretable token, if any,
|
155
|
+
# or nil otherwise. The string is not modified.
|
156
|
+
def parse(s)
|
157
|
+
instance_exec(s, &parser)
|
158
|
+
end
|
159
|
+
|
160
|
+
# Returns the parser defined for the current state, or the default parser.
|
161
|
+
# The default parser simply accepts everything but this can be re-defined
|
162
|
+
# using the class-level parse method.
|
163
|
+
def parser
|
164
|
+
config = self.class.states[@state]
|
165
|
+
|
166
|
+
config and config[:parser] or self.class.default_parser
|
167
|
+
end
|
168
|
+
|
169
|
+
# Processes a given input string into interpretable tokens, processes these
|
170
|
+
# tokens, and removes them from the input string. If no interpretable
|
171
|
+
# tokens could be found, returns immediately. An optional block can be
|
172
|
+
# given that will be called as each interpretable token is discovered with
|
173
|
+
# the token provided as the argument.
|
174
|
+
def process(s)
|
175
|
+
_parser = parser
|
176
|
+
|
177
|
+
while (parsed = s.empty? ? false : instance_exec(s, &_parser))
|
178
|
+
yield(parsed) if (block_given?)
|
179
|
+
|
180
|
+
interpret(*parsed)
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
# Interprets a given object with an optional set of arguments. The actual
|
185
|
+
# interpretation should be defined by declaring a state with an interpret
|
186
|
+
# block defined.
|
187
|
+
def interpret(object, *args)
|
188
|
+
config = self.class.states[@state]
|
189
|
+
callbacks = (config and config[:interpret])
|
190
|
+
|
191
|
+
if (callbacks)
|
192
|
+
matched, proc = callbacks.find do |on, proc|
|
193
|
+
object == on
|
194
|
+
end
|
195
|
+
|
196
|
+
if (matched)
|
197
|
+
instance_exec(*args, &proc)
|
198
|
+
|
199
|
+
return true
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
if (trigger_callbacks(@state, :default, *([ object ] + args)))
|
204
|
+
# Handled by default
|
205
|
+
true
|
206
|
+
elsif (proc = self.class.default)
|
207
|
+
instance_exec(*args, &proc)
|
208
|
+
else
|
209
|
+
if (proc = self.class.on_error_handler)
|
210
|
+
instance_exec(*args, &proc)
|
211
|
+
end
|
212
|
+
|
213
|
+
@error = "No handler for response #{object.inspect} in state #{@state.inspect}"
|
214
|
+
enter_state(:terminated)
|
215
|
+
|
216
|
+
false
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
# Returns true if an error has been generated, false otherwise. The error
|
221
|
+
# content can be retrived by calling error.
|
222
|
+
def error?
|
223
|
+
!!@error
|
224
|
+
end
|
225
|
+
|
226
|
+
protected
|
227
|
+
def delegate_call(method, *args)
|
228
|
+
@delegate and @delegate.respond_to?(method) and @delegate.send(method, *args)
|
229
|
+
end
|
230
|
+
|
231
|
+
def delegate_assign(property, value)
|
232
|
+
method = :"#{property}="
|
233
|
+
|
234
|
+
@delegate and @delegate.respond_to?(method) and @delegate.send(method, value)
|
235
|
+
end
|
236
|
+
|
237
|
+
def leave_state(state)
|
238
|
+
trigger_callbacks(state, :leave)
|
239
|
+
end
|
240
|
+
|
241
|
+
def trigger_callbacks(state, type, *args)
|
242
|
+
config = self.class.states[state]
|
243
|
+
callbacks = (config and config[type])
|
244
|
+
|
245
|
+
return unless (callbacks)
|
246
|
+
|
247
|
+
callbacks.compact.each do |proc|
|
248
|
+
instance_exec(*args, &proc)
|
249
|
+
end
|
250
|
+
|
251
|
+
true
|
252
|
+
end
|
253
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
class Remailer::Interpreter::StateProxy
|
2
|
+
STATIC_CLASSES = [ String, Fixnum, NilClass, TrueClass, FalseClass, Float ].freeze
|
3
|
+
|
4
|
+
def initialize(options, &block)
|
5
|
+
@options = options
|
6
|
+
|
7
|
+
instance_eval(&block) if (block_given?)
|
8
|
+
end
|
9
|
+
|
10
|
+
def parse(spec = nil, &block)
|
11
|
+
@options[:parser] = Remailer::Interpreter.parser_for_spec(spec, &block)
|
12
|
+
end
|
13
|
+
|
14
|
+
def enter(&block)
|
15
|
+
@options[:enter] ||= [ ]
|
16
|
+
@options[:enter] << block
|
17
|
+
end
|
18
|
+
|
19
|
+
def interpret(response, &block)
|
20
|
+
@options[:interpret] ||= [ ]
|
21
|
+
@options[:interpret] << [ response, block ]
|
22
|
+
end
|
23
|
+
|
24
|
+
def default(&block)
|
25
|
+
@options[:default] ||= [ ]
|
26
|
+
@options[:default] << block
|
27
|
+
end
|
28
|
+
|
29
|
+
def leave(&block)
|
30
|
+
@options[:leave] ||= [ ]
|
31
|
+
@options[:leave] << block
|
32
|
+
end
|
33
|
+
|
34
|
+
def terminate(&block)
|
35
|
+
@options[:terminate] ||= [ ]
|
36
|
+
@options[:terminate] << block
|
37
|
+
end
|
38
|
+
|
39
|
+
protected
|
40
|
+
def rebind(options)
|
41
|
+
@options = options
|
42
|
+
end
|
43
|
+
end
|
data/remailer.gemspec
CHANGED
@@ -5,11 +5,11 @@
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = %q{remailer}
|
8
|
-
s.version = "0.
|
8
|
+
s.version = "0.3.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 = %q{2010-
|
12
|
+
s.date = %q{2010-12-02}
|
13
13
|
s.description = %q{EventMachine capable SMTP engine}
|
14
14
|
s.email = %q{scott@twg.ca}
|
15
15
|
s.extra_rdoc_files = [
|
@@ -23,8 +23,18 @@ Gem::Specification.new do |s|
|
|
23
23
|
"VERSION",
|
24
24
|
"lib/remailer.rb",
|
25
25
|
"lib/remailer/connection.rb",
|
26
|
+
"lib/remailer/connection/smtp_interpreter.rb",
|
27
|
+
"lib/remailer/connection/socks5_interpreter.rb",
|
28
|
+
"lib/remailer/interpreter.rb",
|
29
|
+
"lib/remailer/interpreter/state_proxy.rb",
|
26
30
|
"remailer.gemspec",
|
31
|
+
"test/config.example.rb",
|
27
32
|
"test/helper.rb",
|
33
|
+
"test/unit/remailer_connection_smtp_interpreter_test.rb",
|
34
|
+
"test/unit/remailer_connection_socks5_interpreter_test.rb",
|
35
|
+
"test/unit/remailer_connection_test.rb",
|
36
|
+
"test/unit/remailer_interpreter_state_proxy_test.rb",
|
37
|
+
"test/unit/remailer_interpreter_test.rb",
|
28
38
|
"test/unit/remailer_test.rb"
|
29
39
|
]
|
30
40
|
s.homepage = %q{http://github.com/twg/remailer}
|
@@ -33,7 +43,13 @@ Gem::Specification.new do |s|
|
|
33
43
|
s.rubygems_version = %q{1.3.7}
|
34
44
|
s.summary = %q{Reactor-Ready SMTP Mailer}
|
35
45
|
s.test_files = [
|
36
|
-
"test/
|
46
|
+
"test/config.example.rb",
|
47
|
+
"test/helper.rb",
|
48
|
+
"test/unit/remailer_connection_smtp_interpreter_test.rb",
|
49
|
+
"test/unit/remailer_connection_socks5_interpreter_test.rb",
|
50
|
+
"test/unit/remailer_connection_test.rb",
|
51
|
+
"test/unit/remailer_interpreter_state_proxy_test.rb",
|
52
|
+
"test/unit/remailer_interpreter_test.rb",
|
37
53
|
"test/unit/remailer_test.rb"
|
38
54
|
]
|
39
55
|
|
@@ -0,0 +1,17 @@
|
|
1
|
+
TestConfig.smtp_server = {
|
2
|
+
:host => "smtp.example.com",
|
3
|
+
:identifier => "smtp.example.com"
|
4
|
+
}
|
5
|
+
|
6
|
+
TestConfig.public_smtp_server = {
|
7
|
+
:host => "smtp.gmail.com",
|
8
|
+
:identifier => "mx.google.com",
|
9
|
+
:username => "--your--username--gmail.com",
|
10
|
+
:password => "--your--password--",
|
11
|
+
:port => 587
|
12
|
+
}
|
13
|
+
|
14
|
+
TestConfig.proxy_server = "proxy.example.com"
|
15
|
+
|
16
|
+
TestConfig.recipient = "--your--email--"
|
17
|
+
TestConfig.sender = "--your--email--"
|
data/test/helper.rb
CHANGED
@@ -17,6 +17,39 @@ end
|
|
17
17
|
|
18
18
|
require 'remailer'
|
19
19
|
|
20
|
+
class Proc
|
21
|
+
def inspect
|
22
|
+
"\#<Proc: #{object_id}>"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
unless (Hash.respond_to?(:slice))
|
27
|
+
class Hash
|
28
|
+
def slice(*keys)
|
29
|
+
keys.inject({ }) do |h, k|
|
30
|
+
h[k] = self[k]
|
31
|
+
h
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
module TestTriggerHelper
|
38
|
+
def self.included(base)
|
39
|
+
base.class_eval do
|
40
|
+
attr_reader :triggered
|
41
|
+
|
42
|
+
def triggered
|
43
|
+
@triggered ||= Hash.new(false)
|
44
|
+
end
|
45
|
+
|
46
|
+
def trigger(action, value = true)
|
47
|
+
self.triggered[action] = value
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
20
53
|
class Test::Unit::TestCase
|
21
54
|
def engine
|
22
55
|
exception = nil
|
@@ -24,8 +57,12 @@ class Test::Unit::TestCase
|
|
24
57
|
ThreadsWait.all_waits(
|
25
58
|
Thread.new do
|
26
59
|
Thread.abort_on_exception = true
|
60
|
+
|
27
61
|
# Create a thread for the engine to run on
|
28
|
-
|
62
|
+
begin
|
63
|
+
EventMachine.run
|
64
|
+
rescue Object => exception
|
65
|
+
end
|
29
66
|
end,
|
30
67
|
Thread.new do
|
31
68
|
# Execute the test code in a separate thread to avoid blocking
|
@@ -34,7 +71,12 @@ class Test::Unit::TestCase
|
|
34
71
|
yield
|
35
72
|
rescue Object => exception
|
36
73
|
ensure
|
37
|
-
|
74
|
+
begin
|
75
|
+
EventMachine.stop_event_loop
|
76
|
+
rescue Object
|
77
|
+
# Shutting down may trigger an exception from time to time
|
78
|
+
# if the engine itself has failed.
|
79
|
+
end
|
38
80
|
end
|
39
81
|
end
|
40
82
|
)
|
@@ -61,6 +103,23 @@ class Test::Unit::TestCase
|
|
61
103
|
end
|
62
104
|
end
|
63
105
|
end
|
106
|
+
|
107
|
+
def assert_mapping(map, &block)
|
108
|
+
result_map = map.inject({ }) do |h, (k,v)|
|
109
|
+
h[k] = yield(k)
|
110
|
+
h
|
111
|
+
end
|
112
|
+
|
113
|
+
differences = result_map.inject([ ]) do |a, (k,v)|
|
114
|
+
if (v != map[k])
|
115
|
+
a << k
|
116
|
+
end
|
117
|
+
|
118
|
+
a
|
119
|
+
end
|
120
|
+
|
121
|
+
assert_equal map, result_map, "Difference: #{map.slice(*differences).inspect} vs #{result_map.slice(*differences).inspect}"
|
122
|
+
end
|
64
123
|
end
|
65
124
|
|
66
125
|
require 'ostruct'
|