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 +5 -0
- data/.gitignore +23 -0
- data/README.rdoc +87 -0
- data/Rakefile +28 -0
- data/VERSION +1 -0
- data/lib/remailer.rb +5 -0
- data/lib/remailer/connection.rb +357 -0
- data/remailer.gemspec +50 -0
- data/test/helper.rb +64 -0
- data/test/unit/remailer_test.rb +137 -0
- metadata +74 -0
data/.document
ADDED
data/.gitignore
ADDED
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,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
|