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