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.
@@ -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
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{remailer}
8
- s.version = "0.2.1"
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-11-22}
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/helper.rb",
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--"
@@ -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
- EventMachine.run
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
- EventMachine.stop_event_loop
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'