remailer 0.1.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/.document ADDED
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
data/.gitignore ADDED
@@ -0,0 +1,23 @@
1
+ *.gem
2
+
3
+ ## MAC OS
4
+ .DS_Store
5
+
6
+ ## TEXTMATE
7
+ *.tmproj
8
+ tmtags
9
+
10
+ ## EMACS
11
+ *~
12
+ \#*
13
+ .\#*
14
+
15
+ ## VIM
16
+ *.swp
17
+
18
+ ## PROJECT::GENERAL
19
+ coverage
20
+ rdoc
21
+ pkg
22
+
23
+ ## PROJECT::SPECIFIC
data/README.rdoc ADDED
@@ -0,0 +1,87 @@
1
+ = remailer
2
+
3
+ Reactor-Ready Mailer Engine
4
+
5
+ == Overview
6
+
7
+ This is an EventMachine Connection implementation of a high-performance
8
+ asynchronous SMTP client. Although EventMachine ships with a built-in SMTP
9
+ client, that version is limited to sending a single email per connection,
10
+ and since establishing a connection can be the majority of the time required
11
+ to send email, this limits throughput considerably.
12
+
13
+ == Use
14
+
15
+ The Remailer system consists of the Remailer::Connection class which works
16
+ within the EventMachine environment. To use it, create a connection and then
17
+ make one or more requests to send email messages.
18
+
19
+ EventMachine.run do
20
+ # Establish a connection to a particular SMTP server and send debugging
21
+ # messages to STDERR.
22
+ connection = Remailer::Connection.open(
23
+ 'smtp.google.com',
24
+ :debug => STDERR
25
+ )
26
+
27
+ # Send a single email message through the connection at the earliest
28
+ # opportunity. Note that the connection will need to be fully
29
+ # established first and this may take upwards of ten seconds.
30
+ connection.send_email(
31
+ 'from@example.net',
32
+ 'to@example.com',
33
+ email_content
34
+ )
35
+
36
+ # Send an additional message through the connection. This will queue up
37
+ # until the first has been transmitted.
38
+ connection.send_email(
39
+ 'from@example.net',
40
+ 'to@example.com',
41
+ email_content
42
+ )
43
+
44
+ # Tells the connection to close out when finished.
45
+ connection.close_when_complete!
46
+ end
47
+
48
+ A Proc can be supplied as the :debug option to Remailer::Connection.open and
49
+ in this case it will be called with two parameters, type and message. An
50
+ example is given here where the information is simply dumped on STDOUT:
51
+
52
+ connection = Remailer::Connection.open(
53
+ 'smtp.google.com',
54
+ :debug => lambda { |type, message|
55
+ puts "#{type}> #{message.inspect}"
56
+ }
57
+ )
58
+
59
+ The types defined include:
60
+
61
+ * :send - Raw data sent by the client
62
+ * :reply - Raw replies from the server
63
+ * :options - The finalized options used to connect to the server
64
+
65
+ This callback procedure can be defined or replaced after the connection is
66
+ initialized:
67
+
68
+ connection.debug do |type, message|
69
+ STDERR.puts "%s> %s" % [ type, message.inspect ]
70
+ end
71
+
72
+ It's also possible to define a handler for when the message queue has been
73
+ exhausted:
74
+
75
+ connection.after_complete do
76
+ STDERR.puts "Sending complete."
77
+ end
78
+
79
+ == Status
80
+
81
+ This software is currently experimental and is not recommended for production
82
+ use. Many of the internals may change significantly before a proper beta
83
+ release is made.
84
+
85
+ == Copyright
86
+
87
+ Copyright (c) 2010 Scott Tadman, The Working Group
data/Rakefile ADDED
@@ -0,0 +1,28 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "remailer"
8
+ gem.summary = %Q{Reactor-Ready SMTP Mailer}
9
+ gem.description = %Q{EventMachine capable SMTP engine}
10
+ gem.email = "scott@twg.ca"
11
+ gem.homepage = "http://github.com/twg/remailer"
12
+ gem.authors = [ "Scott Tadman" ]
13
+ end
14
+ Jeweler::GemcutterTasks.new
15
+ rescue LoadError
16
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
17
+ end
18
+
19
+ require 'rake/testtask'
20
+ Rake::TestTask.new(:test) do |test|
21
+ test.libs << 'lib' << 'test'
22
+ test.pattern = 'test/**/*_test.rb'
23
+ test.verbose = true
24
+ end
25
+
26
+ task :test => :check_dependencies
27
+
28
+ task :default => :test
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
data/lib/remailer.rb ADDED
@@ -0,0 +1,5 @@
1
+ module Remailer
2
+ # == Submodules ===========================================================
3
+
4
+ autoload(:Connection, 'remailer/connection')
5
+ end
@@ -0,0 +1,357 @@
1
+ require 'socket'
2
+ require 'eventmachine'
3
+
4
+ class Remailer::Connection < EventMachine::Connection
5
+ # == Constants ============================================================
6
+
7
+ DEFAULT_TIMEOUT = 5
8
+ SMTP_PORT = 25
9
+ CRLF = "\r\n".freeze
10
+ CRLF_LENGTH = CRLF.length
11
+
12
+ # == Properties ===========================================================
13
+
14
+ attr_accessor :timeout
15
+ attr_reader :state, :mode
16
+ attr_accessor :options
17
+ attr_reader :remote, :max_size, :protocol
18
+
19
+ # == Extensions ===========================================================
20
+
21
+ include EventMachine::Deferrable
22
+
23
+ # == Class Methods ========================================================
24
+
25
+ # Opens a connection to a specific SMTP server. Options can be specified:
26
+ # * port => Numerical port number (default is 25)
27
+ # * require_tls => If true will fail connections to non-TLS capable
28
+ # servers (default is false)
29
+ # * use_tls => Will use TLS if availble (default is true)
30
+ def self.open(smtp_server, options = nil)
31
+ options ||= { }
32
+ options[:port] ||= 25
33
+ options[:use_tls] = true unless (options.key?(:use_tls))
34
+
35
+ EventMachine.connect(smtp_server, options[:port], self, options)
36
+ end
37
+
38
+ # EHLO address
39
+ # MAIL FROM:<reverse-path> [SP <mail-parameters> ] <CRLF>
40
+ # RCPT TO:<forward-path> [ SP <rcpt-parameters> ] <CRLF>
41
+ # DATA <CRLF>
42
+ # NOOP
43
+ # QUIT
44
+
45
+ # 250-mx.google.com at your service, [99.231.152.248]
46
+ # 250-SIZE 35651584
47
+ # 250-8BITMIME
48
+ # 250-STARTTLS
49
+ # 250 ENHANCEDSTATUSCODES
50
+
51
+ def self.encode_data(data)
52
+ data.gsub(/((?:\r\n|\n)\.)/m, '\\1.')
53
+ end
54
+
55
+ # == Instance Methods =====================================================
56
+
57
+ def initialize(options)
58
+ @options = options
59
+
60
+ @options[:hostname] ||= Socket.gethostname
61
+ @messages = [ ]
62
+
63
+ debug_notification(:options, @options.inspect)
64
+
65
+ @timeout_at = Time.now + (@timeout || DEFAULT_TIMEOUT)
66
+ end
67
+
68
+ # Returns true if the connection has advertised TLS support, or false if
69
+ # not availble or could not be detected. This will only work with ESMTP
70
+ # capable servers.
71
+ def tls_support?
72
+ !!@tls_support
73
+ end
74
+
75
+ # This is used to create a callback that will be called if no more messages
76
+ # are schedueld to be sent.
77
+ def after_complete(&block)
78
+ @options[:after_complete] = block
79
+ end
80
+
81
+ def close_when_complete!
82
+ @options[:close] = true
83
+ end
84
+
85
+ def send_email(from, to, data, &block)
86
+ message = {
87
+ :from => from,
88
+ :to => to,
89
+ :data => data,
90
+ :callback => block
91
+ }
92
+
93
+ @messages << message
94
+
95
+ if (@state == :ready)
96
+ send_queued_message!
97
+ end
98
+ end
99
+
100
+ # Returns the details of the active message being sent, or nil if no message
101
+ # is being sent.
102
+ def active_message
103
+ @active_message
104
+ end
105
+
106
+ # Reassigns the timeout which is specified in seconds. Values equal to
107
+ # or less than zero are ignored and a default is used instead.
108
+ def timeout=(value)
109
+ @timeout = value.to_i
110
+ @timeout = DEFAULT_TIMEOUT if (@timeout <= 0)
111
+ end
112
+
113
+ # This implements the EventMachine::Connection#completed method by
114
+ # flagging the connection as estasblished.
115
+ def connection_completed
116
+ @timeout_at = nil
117
+ @state = :connected
118
+ end
119
+
120
+ # This implements the EventMachine::Connection#unbind method to capture
121
+ # a connection closed event.
122
+ def unbind
123
+ @state = :closed
124
+
125
+ if (@active_message)
126
+ if (callback = @active_message[:callback])
127
+ callback.call(nil)
128
+ end
129
+ end
130
+ end
131
+
132
+ def receive_data(data)
133
+ # Data is received in arbitrary sized chunks, so there is no guarantee
134
+ # a whole line will be ready to process, or that there is only one line.
135
+ @buffer ||= ''
136
+ @buffer << data
137
+
138
+ while (line_index = @buffer.index(CRLF))
139
+ if (line_index > 0)
140
+ receive_reply(@buffer[0, line_index])
141
+ end
142
+
143
+ @buffer = (@buffer[line_index + CRLF_LENGTH, @buffer.length] || '')
144
+ end
145
+ end
146
+
147
+ def post_init
148
+ @state = :connecting
149
+
150
+ EventMachine.add_periodic_timer(1) do
151
+ check_for_timeouts!
152
+ end
153
+ end
154
+
155
+ def send_line(line = '')
156
+ send_data(line + CRLF)
157
+
158
+ debug_notification(:send, line.inspect)
159
+ end
160
+
161
+ # Returns true if the reply has been completed, or false if it is still
162
+ # in the process of being received.
163
+ def reply_complete?
164
+ !!@reply_complete
165
+ end
166
+
167
+ def receive_reply(reply)
168
+ debug_notification(:reply, reply.inspect)
169
+
170
+ return unless (reply)
171
+
172
+ if (reply.match(/(\d+)([ \-])(.*)/))
173
+ reply_code = $1.to_i
174
+ @reply_complete = $2 != '-'
175
+ reply_message = $3
176
+ end
177
+
178
+ case (state)
179
+ when :connected
180
+ case (reply_code)
181
+ when 220
182
+ reply_parts = reply_message.split(/\s+/)
183
+ @remote = reply_parts.first
184
+
185
+ if (reply_parts.include?('ESMTP'))
186
+ @state = :sent_ehlo
187
+ @protocol = :esmtp
188
+ send_line("EHLO #{@options[:hostname]}")
189
+ else
190
+ @state = :sent_helo
191
+ @protocol = :smtp
192
+ send_line("HELO #{@options[:hostname]}")
193
+ end
194
+ else
195
+ fail_unanticipated_response!(reply)
196
+ end
197
+ when :sent_ehlo
198
+ case (reply_code)
199
+ when 250
200
+ reply_parts = reply_message.split(/\s+/)
201
+ case (reply_parts[0].to_s.upcase)
202
+ when 'SIZE'
203
+ @max_size = reply_parts[1].to_i
204
+ when 'PIPELINING'
205
+ @pipelining = true
206
+ when 'STARTTLS'
207
+ @tls_support = true
208
+ end
209
+
210
+ # FIX: Add TLS support
211
+ # if (@tls_support and @options[:use_tls])
212
+ # @state = :tls_init
213
+ # end
214
+
215
+ if (@reply_complete)
216
+ # Add authentication hook
217
+ @state = :ready
218
+
219
+ send_queued_message!
220
+ end
221
+ else
222
+ fail_unanticipated_response!(reply)
223
+ end
224
+ when :sent_helo
225
+ case (reply_code)
226
+ when 250
227
+ @state = :ready
228
+ else
229
+ fail_unanticipated_response!(reply)
230
+ end
231
+ when :sent_mail_from
232
+ case (reply_code)
233
+ when 250
234
+ @state = :sent_rcpt_to
235
+ send_line("RCPT TO:#{@active_message[:to]}")
236
+ else
237
+ fail_unanticipated_response!(reply)
238
+ end
239
+ when :sent_rcpt_to
240
+ case (reply_code)
241
+ when 250
242
+ @state = :sent_data
243
+ send_line("DATA")
244
+
245
+ @data_offset = 0
246
+ else
247
+ fail_unanticipated_response!(reply)
248
+ end
249
+ when :sent_data
250
+ case (reply_code)
251
+ when 354
252
+ @state = :data_sending
253
+
254
+ transmit_data_chunk!
255
+ else
256
+ fail_unanticipated_response!(reply)
257
+ end
258
+ when :sent_data_content
259
+ if (callback = @active_message[:callback])
260
+ callback.call(reply_code)
261
+ end
262
+
263
+ @state = :ready
264
+
265
+ send_queued_message!
266
+ when :sent_quit
267
+ case (reply_code)
268
+ when 221
269
+ @state = :closed
270
+ close_connection
271
+ else
272
+ fail_unanticipated_response!(reply)
273
+ end
274
+ when :sent_reset
275
+ case (reply_code)
276
+ when 250
277
+ @state = :ready
278
+
279
+
280
+ send_queued_message!
281
+ end
282
+ end
283
+ end
284
+
285
+ def transmit_data_chunk!(chunk_size = nil)
286
+ data = @active_message[:data]
287
+ chunk_size ||= data.length
288
+
289
+ chunk = data[@data_offset, chunk_size]
290
+ debug_notification(:send, chunk.inspect)
291
+ send_data(self.class.encode_data(data))
292
+ @data_offset += chunk_size
293
+
294
+ if (@data_offset >= data.length)
295
+ @state = :sent_data_content
296
+
297
+ # Ensure that a blank line is sent after the last bit of email content
298
+ # to ensure that the dot is on its own line.
299
+ send_line
300
+ send_line(".")
301
+ end
302
+ end
303
+
304
+ def notify_writable
305
+ # FIXME: Get EventMachine to trigger this
306
+ end
307
+
308
+ def send_queued_message!
309
+ return if (@active_message)
310
+
311
+ if (@active_message = @messages.shift)
312
+ @state = :sent_mail_from
313
+ send_line("MAIL FROM:#{@active_message[:from]}")
314
+ elsif (@options[:close])
315
+ if (callback = @options[:after_complete])
316
+ callback.call
317
+ end
318
+
319
+ send_line("QUIT")
320
+ @state = :sent_quit
321
+ end
322
+ end
323
+
324
+ def check_for_timeouts!
325
+ return if (!@timeout_at or Time.now < @timeout_at)
326
+
327
+ callback(nil)
328
+ @state = :timeout
329
+ close_connection
330
+ end
331
+
332
+ def debug_notification(type, message)
333
+ case (@options[:debug])
334
+ when nil, false
335
+ # No debugging in this case
336
+ when Proc
337
+ @options[:debug].call(type, message)
338
+ when IO
339
+ @options[:debug].puts("%s: %s" % [ type, message ])
340
+ else
341
+ STDERR.puts("%s: %s" % [ type, message ])
342
+ end
343
+ end
344
+
345
+ def fail_unanticipated_response!(reply)
346
+ if (@active_message)
347
+ if (callback = @active_message[:callback])
348
+ callback.call(nil)
349
+ end
350
+ end
351
+
352
+ @active_message = nil
353
+
354
+ @state = :sent_reset
355
+ send_line("RESET")
356
+ end
357
+ end
data/remailer.gemspec ADDED
@@ -0,0 +1,50 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{remailer}
8
+ s.version = "0.1.0"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Scott Tadman"]
12
+ s.date = %q{2010-11-17}
13
+ s.description = %q{EventMachine capable SMTP engine}
14
+ s.email = %q{scott@twg.ca}
15
+ s.extra_rdoc_files = [
16
+ "README.rdoc"
17
+ ]
18
+ s.files = [
19
+ ".document",
20
+ ".gitignore",
21
+ "README.rdoc",
22
+ "Rakefile",
23
+ "VERSION",
24
+ "lib/remailer.rb",
25
+ "lib/remailer/connection.rb",
26
+ "remailer.gemspec",
27
+ "test/helper.rb",
28
+ "test/unit/remailer_test.rb"
29
+ ]
30
+ s.homepage = %q{http://github.com/twg/remailer}
31
+ s.rdoc_options = ["--charset=UTF-8"]
32
+ s.require_paths = ["lib"]
33
+ s.rubygems_version = %q{1.3.7}
34
+ s.summary = %q{Reactor-Ready SMTP Mailer}
35
+ s.test_files = [
36
+ "test/helper.rb",
37
+ "test/unit/remailer_test.rb"
38
+ ]
39
+
40
+ if s.respond_to? :specification_version then
41
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
42
+ s.specification_version = 3
43
+
44
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
45
+ else
46
+ end
47
+ else
48
+ end
49
+ end
50
+
data/test/helper.rb ADDED
@@ -0,0 +1,64 @@
1
+ require 'rubygems'
2
+ require 'test/unit'
3
+
4
+ $LOAD_PATH.unshift(File.expand_path(*%w[ .. lib ]), File.dirname(__FILE__))
5
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
6
+
7
+ require 'timeout'
8
+ require 'thwait'
9
+ require 'rubygems'
10
+
11
+ if (Gem.available?('eventmachine'))
12
+ gem 'eventmachine'
13
+ require 'eventmachine'
14
+ else
15
+ raise "EventMachine gem is not installed."
16
+ end
17
+
18
+ require 'remailer'
19
+
20
+ class Test::Unit::TestCase
21
+ def engine
22
+ exception = nil
23
+
24
+ ThreadsWait.all_waits(
25
+ Thread.new do
26
+ Thread.abort_on_exception = true
27
+ # Create a thread for the engine to run on
28
+ EventMachine.run
29
+ end,
30
+ Thread.new do
31
+ # Execute the test code in a separate thread to avoid blocking
32
+ # the EventMachine loop.
33
+ begin
34
+ yield
35
+ rescue Object => exception
36
+ ensure
37
+ EventMachine.stop_event_loop
38
+ end
39
+ end
40
+ )
41
+
42
+ if (exception)
43
+ raise exception
44
+ end
45
+ end
46
+
47
+ def assert_timeout(time, message = nil, &block)
48
+ Timeout::timeout(time, &block)
49
+ rescue Timeout::Error
50
+ flunk(message || 'assert_timeout timed out')
51
+ end
52
+
53
+ def assert_eventually(time = nil, message = nil, &block)
54
+ start_time = Time.now.to_i
55
+
56
+ while (!block.call)
57
+ select(nil, nil, nil, 0.1)
58
+
59
+ if (time and (Time.now.to_i - start_time > time))
60
+ flunk(message || 'assert_eventually timed out')
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,137 @@
1
+ require File.expand_path(File.join(*%w[ .. helper ]), File.dirname(__FILE__))
2
+
3
+ class RemailerTest < Test::Unit::TestCase
4
+ TEST_SMTP_SERVER = 'mail.postageapp.com'.freeze
5
+
6
+ def test_encode_data
7
+ sample_data = "Line 1\r\nLine 2\r\n.\r\nLine 3\r\n.Line 4\r\n"
8
+
9
+ assert_equal "Line 1\r\nLine 2\r\n..\r\nLine 3\r\n..Line 4\r\n", Remailer::Connection.encode_data(sample_data)
10
+ end
11
+
12
+ def test_connect
13
+ engine do
14
+ debug = { }
15
+
16
+ connection = Remailer::Connection.open(
17
+ TEST_SMTP_SERVER,
18
+ :debug => STDERR
19
+ )
20
+
21
+ after_complete_trigger = false
22
+
23
+ connection.close_when_complete!
24
+ connection.after_complete do
25
+ after_complete_trigger = true
26
+ end
27
+
28
+ assert_equal :connecting, connection.state
29
+ assert !connection.error?
30
+
31
+ assert_eventually(15) do
32
+ connection.state == :closed
33
+ end
34
+
35
+ assert_equal TEST_SMTP_SERVER, connection.remote
36
+
37
+ assert_equal true, after_complete_trigger
38
+
39
+ assert_equal 52428800, connection.max_size
40
+ assert_equal :esmtp, connection.protocol
41
+ assert_equal true, connection.tls_support?
42
+ end
43
+ end
44
+
45
+ def test_connect_and_send_after_start
46
+ engine do
47
+ connection = Remailer::Connection.open(
48
+ TEST_SMTP_SERVER,
49
+ :debug => STDERR
50
+ )
51
+
52
+ assert_equal :connecting, connection.state
53
+
54
+ assert_eventually(10) do
55
+ connection.state == :ready
56
+ end
57
+
58
+ result_code = nil
59
+ connection.send_email(
60
+ 'remailer+test@example.postageapp.com',
61
+ 'remailer+test@example.postageapp.com',
62
+ example_message
63
+ ) do |c|
64
+ result_code = c
65
+ end
66
+
67
+ assert_eventually(5) do
68
+ result_code == 250
69
+ end
70
+ end
71
+ end
72
+
73
+ def test_connect_and_send_dotted_message
74
+ engine do
75
+ connection = Remailer::Connection.open(
76
+ TEST_SMTP_SERVER,
77
+ :debug => STDERR
78
+ )
79
+
80
+ assert_equal :connecting, connection.state
81
+ assert !connection.error?
82
+
83
+ result_code = nil
84
+ connection.send_email(
85
+ 'remailer+test@example.postageapp.com',
86
+ 'remailer+test@example.postageapp.com',
87
+ example_message + "\r\n\.\r\nHam sandwich.\r\n"
88
+ ) do |c|
89
+ result_code = c
90
+ end
91
+
92
+ assert_eventually(15) do
93
+ result_code == 250
94
+ end
95
+ end
96
+ end
97
+
98
+ def test_connect_and_long_send
99
+ engine do
100
+ connection = Remailer::Connection.open('twgmail.twg.ca')
101
+
102
+ assert_equal :connecting, connection.state
103
+ assert !connection.error?
104
+
105
+ result_code = nil
106
+ connection.send_email(
107
+ 'sender@postageapp.com',
108
+ 'remailer+test@example.postageapp.com',
109
+ example_message + 'a' * 100000
110
+ ) do |c|
111
+ result_code = c
112
+ end
113
+
114
+ assert_eventually(15) do
115
+ result_code == 250
116
+ end
117
+ end
118
+ end
119
+
120
+ protected
121
+ def example_message
122
+ example = <<__END__
123
+ Date: Sat, 13 Nov 2010 02:25:24 +0000
124
+ From: sender@postageapp.com
125
+ To: Remailer Test <remailer@twg.ca>
126
+ Message-Id: <hfLkcIByfjYoNIxCO7DMsxBTX9svsFHikIOfAiYy@twg.ca>
127
+ Subject: Example Subject
128
+ Mime-Version: 1.0
129
+ Content-Type: text/plain
130
+ Auto-Submitted: auto-generated
131
+
132
+ This is a very boring message. It is dreadfully dull.
133
+ __END__
134
+
135
+ example.gsub(/\n/, "\r\n")
136
+ end
137
+ end
metadata ADDED
@@ -0,0 +1,74 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: remailer
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 1
8
+ - 0
9
+ version: 0.1.0
10
+ platform: ruby
11
+ authors:
12
+ - Scott Tadman
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2010-11-17 00:00:00 -05:00
18
+ default_executable:
19
+ dependencies: []
20
+
21
+ description: EventMachine capable SMTP engine
22
+ email: scott@twg.ca
23
+ executables: []
24
+
25
+ extensions: []
26
+
27
+ extra_rdoc_files:
28
+ - README.rdoc
29
+ files:
30
+ - .document
31
+ - .gitignore
32
+ - README.rdoc
33
+ - Rakefile
34
+ - VERSION
35
+ - lib/remailer.rb
36
+ - lib/remailer/connection.rb
37
+ - remailer.gemspec
38
+ - test/helper.rb
39
+ - test/unit/remailer_test.rb
40
+ has_rdoc: true
41
+ homepage: http://github.com/twg/remailer
42
+ licenses: []
43
+
44
+ post_install_message:
45
+ rdoc_options:
46
+ - --charset=UTF-8
47
+ require_paths:
48
+ - lib
49
+ required_ruby_version: !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ segments:
55
+ - 0
56
+ version: "0"
57
+ required_rubygems_version: !ruby/object:Gem::Requirement
58
+ none: false
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ segments:
63
+ - 0
64
+ version: "0"
65
+ requirements: []
66
+
67
+ rubyforge_project:
68
+ rubygems_version: 1.3.7
69
+ signing_key:
70
+ specification_version: 3
71
+ summary: Reactor-Ready SMTP Mailer
72
+ test_files:
73
+ - test/helper.rb
74
+ - test/unit/remailer_test.rb