remailer 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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