ar_mailer 1.0.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/LICENSE +26 -0
- data/Manifest.txt +10 -0
- data/README +26 -0
- data/Rakefile +67 -0
- data/bin/ar_sendmail +11 -0
- data/lib/action_mailer/ar_mailer.rb +66 -0
- data/lib/action_mailer/ar_sendmail.rb +351 -0
- data/test/action_mailer.rb +131 -0
- data/test/test_armailer.rb +39 -0
- data/test/test_arsendmail.rb +367 -0
- metadata +55 -0
data/LICENSE
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
Copyright 2006, Eric Hodel, The Robot Co-op. All rights reserved.
|
2
|
+
|
3
|
+
Redistribution and use in source and binary forms, with or without
|
4
|
+
modification, are permitted provided that the following conditions
|
5
|
+
are met:
|
6
|
+
|
7
|
+
1. Redistributions of source code must retain the above copyright
|
8
|
+
notice, this list of conditions and the following disclaimer.
|
9
|
+
2. Redistributions in binary form must reproduce the above copyright
|
10
|
+
notice, this list of conditions and the following disclaimer in the
|
11
|
+
documentation and/or other materials provided with the distribution.
|
12
|
+
3. Neither the names of the authors nor the names of their contributors
|
13
|
+
may be used to endorse or promote products derived from this software
|
14
|
+
without specific prior written permission.
|
15
|
+
|
16
|
+
THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND ANY EXPRESS
|
17
|
+
OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
18
|
+
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
19
|
+
ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE
|
20
|
+
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
|
21
|
+
OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
|
22
|
+
OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
|
23
|
+
BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
24
|
+
WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
|
25
|
+
OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
|
26
|
+
EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
data/Manifest.txt
ADDED
data/README
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
= ar_mailer
|
2
|
+
|
3
|
+
Rubyforge Project:
|
4
|
+
|
5
|
+
http://rubyforge.org/projects/rctools
|
6
|
+
|
7
|
+
Documentation:
|
8
|
+
|
9
|
+
http://dev.robotcoop.com/Libraries/ar_mailer
|
10
|
+
|
11
|
+
== About
|
12
|
+
|
13
|
+
Even deliviring email to the local machine may take too long when you have to
|
14
|
+
send hundreds of messages. ar_mailer allows you to store messages into the
|
15
|
+
database for later delivery by a separate process, ar_sendmail.
|
16
|
+
|
17
|
+
== Installing ar_mailer
|
18
|
+
|
19
|
+
Just install the gem:
|
20
|
+
|
21
|
+
$ sudo gem install ar_mailer
|
22
|
+
|
23
|
+
See ActionMailer::ARMailer for instructions on converting to ARMailer.
|
24
|
+
|
25
|
+
See ar_sendmail -h for options to ar_sendmail.
|
26
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
require 'rake/testtask'
|
4
|
+
require 'rake/rdoctask'
|
5
|
+
require 'rake/gempackagetask'
|
6
|
+
|
7
|
+
$VERBOSE = nil
|
8
|
+
|
9
|
+
spec = Gem::Specification.new do |s|
|
10
|
+
s.name = 'ar_mailer'
|
11
|
+
s.version = '1.0.0'
|
12
|
+
s.summary = 'A two-phase deliver agent for ActionMailer'
|
13
|
+
s.description = 'Queues emails from ActionMailer in the database and uses a separate process to send them. Reduces sending overhead when sending hundreds of emails.'
|
14
|
+
s.author = 'Eric Hodel'
|
15
|
+
s.email = 'eric@robotcoop.com'
|
16
|
+
|
17
|
+
s.has_rdoc = true
|
18
|
+
s.files = File.read('Manifest.txt').split($/)
|
19
|
+
s.require_path = 'lib'
|
20
|
+
|
21
|
+
s.executables = ['ar_sendmail']
|
22
|
+
end
|
23
|
+
|
24
|
+
desc 'Run tests'
|
25
|
+
task :default => [ :test ]
|
26
|
+
|
27
|
+
Rake::TestTask.new('test') do |t|
|
28
|
+
t.libs << 'test'
|
29
|
+
t.pattern = 'test/test_*.rb'
|
30
|
+
t.verbose = true
|
31
|
+
end
|
32
|
+
|
33
|
+
desc 'Update Manifest.txt'
|
34
|
+
task :update_manifest do
|
35
|
+
sh "find . -type f | sed -e 's%./%%' | egrep -v 'svn|swp|~' | egrep -v '^(doc|pkg)/' | sort > Manifest.txt"
|
36
|
+
end
|
37
|
+
|
38
|
+
desc 'Generate RDoc'
|
39
|
+
Rake::RDocTask.new :rdoc do |rd|
|
40
|
+
rd.rdoc_dir = 'doc'
|
41
|
+
rd.rdoc_files.add 'lib', 'README', 'LICENSE'
|
42
|
+
rd.main = 'README'
|
43
|
+
rd.options << '-d' if `which dot` =~ /\/dot/
|
44
|
+
rd.options << '-t ar_mailer'
|
45
|
+
end
|
46
|
+
|
47
|
+
desc 'Generate RDoc for dev.robotcoop.com'
|
48
|
+
Rake::RDocTask.new :dev_rdoc do |rd|
|
49
|
+
rd.rdoc_dir = '../../../www/trunk/dev/html/Tools/ar_mailer'
|
50
|
+
rd.rdoc_files.add 'lib', 'README', 'LICENSE'
|
51
|
+
rd.main = 'README'
|
52
|
+
rd.options << '-d' if `which dot` =~ /\/dot/
|
53
|
+
rd.options << '-t ar_mailer'
|
54
|
+
end
|
55
|
+
|
56
|
+
desc 'Build Gem'
|
57
|
+
Rake::GemPackageTask.new spec do |pkg|
|
58
|
+
pkg.need_tar = true
|
59
|
+
end
|
60
|
+
|
61
|
+
desc 'Clean up'
|
62
|
+
task :clean => [ :clobber_rdoc, :clobber_package ]
|
63
|
+
|
64
|
+
desc 'Clean up'
|
65
|
+
task :clobber => [ :clean ]
|
66
|
+
|
67
|
+
# vim: syntax=Ruby
|
data/bin/ar_sendmail
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
require 'action_mailer'
|
2
|
+
|
3
|
+
##
|
4
|
+
# Adds sending email through an ActiveRecord table as a delivery method for
|
5
|
+
# ActionMailer.
|
6
|
+
#
|
7
|
+
# == Converting to ActionMailer::ARMailer
|
8
|
+
#
|
9
|
+
# Go to your Rails project:
|
10
|
+
#
|
11
|
+
# $ cd your_rails_project
|
12
|
+
#
|
13
|
+
# Create a new migration:
|
14
|
+
#
|
15
|
+
# $ ar_sendmail --create-migration
|
16
|
+
#
|
17
|
+
# You'll need to redirect this into a file. If you want a different name
|
18
|
+
# provide the --table-name option.
|
19
|
+
#
|
20
|
+
# Create a new model:
|
21
|
+
#
|
22
|
+
# $ ar_sendmail --create-model
|
23
|
+
#
|
24
|
+
# You'll need to redirect this into a file. If you want a different name
|
25
|
+
# provide the --table-name option.
|
26
|
+
#
|
27
|
+
# Change your email classes to inherit from ActionMailer::ARMailer instead of
|
28
|
+
# ActionMailer::Base:
|
29
|
+
#
|
30
|
+
# --- app/model/emailer.rb.orig 2006-08-10 13:16:33.000000000 -0700
|
31
|
+
# +++ app/model/emailer.rb 2006-08-10 13:16:43.000000000 -0700
|
32
|
+
# @@ -1,4 +1,4 @@
|
33
|
+
# -class Emailer < ActionMailer::Base
|
34
|
+
# +class Emailer < ActionMailer::ARMailer
|
35
|
+
#
|
36
|
+
# def comment_notification(comment)
|
37
|
+
# from comment.author.email
|
38
|
+
#
|
39
|
+
# Edit config/environment/production.rb and set the delivery agent:
|
40
|
+
#
|
41
|
+
# $ ActionMailer::Base.delivery_method = :activerecord
|
42
|
+
#
|
43
|
+
# Run ar_sendmail:
|
44
|
+
#
|
45
|
+
# $ ar_sendmail
|
46
|
+
#
|
47
|
+
# You can also run it from cron with -o, or as a daemon with -d.
|
48
|
+
#
|
49
|
+
# See <tt>ar_sendmail -h</tt> for full details.
|
50
|
+
|
51
|
+
class ActionMailer::ARMailer < ActionMailer::Base
|
52
|
+
|
53
|
+
##
|
54
|
+
# Adds +mail+ to the Email table. Only the first From address for +mail+ is
|
55
|
+
# used.
|
56
|
+
|
57
|
+
def perform_delivery_activerecord(mail)
|
58
|
+
mail.destinations.each do |destination|
|
59
|
+
Email.create :mail => mail.encoded,
|
60
|
+
:to => destination,
|
61
|
+
:from => mail.from.first
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
66
|
+
|
@@ -0,0 +1,351 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
require 'rubygems'
|
3
|
+
require 'action_mailer'
|
4
|
+
|
5
|
+
class Object # :nodoc:
|
6
|
+
unless respond_to? :path2class then
|
7
|
+
def self.path2class(path)
|
8
|
+
path.split(/::/).inject self do |k,n| k.const_get n end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
##
|
14
|
+
# ActionMailer::ARSendmail delivers email from the email table to the
|
15
|
+
# configured SMTP server.
|
16
|
+
#
|
17
|
+
# See ar_sendmail -h for the full list of supported options.
|
18
|
+
#
|
19
|
+
# The interesting options are:
|
20
|
+
# * --daemon
|
21
|
+
# * --mailq
|
22
|
+
# * --create-migration
|
23
|
+
# * --create-model
|
24
|
+
# * --table-name
|
25
|
+
|
26
|
+
class ActionMailer::ARSendmail
|
27
|
+
|
28
|
+
##
|
29
|
+
# Email delivery attempts per run
|
30
|
+
|
31
|
+
attr_accessor :batch_size
|
32
|
+
|
33
|
+
##
|
34
|
+
# Seconds to delay between runs
|
35
|
+
|
36
|
+
attr_accessor :delay
|
37
|
+
|
38
|
+
##
|
39
|
+
# Be verbose
|
40
|
+
|
41
|
+
attr_accessor :verbose
|
42
|
+
|
43
|
+
##
|
44
|
+
# ActiveRecord class that holds emails
|
45
|
+
|
46
|
+
attr_reader :email_class
|
47
|
+
|
48
|
+
##
|
49
|
+
# True if only one delivery attempt will be made per call to run
|
50
|
+
|
51
|
+
attr_reader :once
|
52
|
+
|
53
|
+
##
|
54
|
+
# Creates a new migration using +table_name+ and prints it on $stdout.
|
55
|
+
|
56
|
+
def self.create_migration(table_name)
|
57
|
+
migration = <<-EOF
|
58
|
+
class Add#{table_name.classify} < ActiveRecord::Migration
|
59
|
+
def self.up
|
60
|
+
create_table :#{table_name.tableize} do |t|
|
61
|
+
t.column :from, :string
|
62
|
+
t.column :to, :string
|
63
|
+
t.column :last_send_attempt, :integer, :default => 0
|
64
|
+
t.column :mail, :text
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def self.down
|
69
|
+
drop_table :email
|
70
|
+
end
|
71
|
+
end
|
72
|
+
EOF
|
73
|
+
|
74
|
+
$stdout.puts migration
|
75
|
+
end
|
76
|
+
|
77
|
+
##
|
78
|
+
# Creates a new model using +table_name+ and prints it on $stdout.
|
79
|
+
|
80
|
+
def self.create_model(table_name)
|
81
|
+
model = <<-EOF
|
82
|
+
class #{table_name.classify} < ActiveRecord::Base
|
83
|
+
end
|
84
|
+
EOF
|
85
|
+
|
86
|
+
$stdout.puts model
|
87
|
+
end
|
88
|
+
|
89
|
+
##
|
90
|
+
# Prints a list of unsent emails and the last delivery attempt, if any.
|
91
|
+
#
|
92
|
+
# If ActiveRecord::Timestamp is not being used the arrival time will not be
|
93
|
+
# known. See http://api.rubyonrails.org/classes/ActiveRecord/Timestamp.html
|
94
|
+
# to learn how to enable ActiveRecord::Timestamp.
|
95
|
+
|
96
|
+
def self.mailq
|
97
|
+
emails = Email.find :all
|
98
|
+
|
99
|
+
if emails.empty? then
|
100
|
+
$stdout.puts "Mail queue is empty"
|
101
|
+
return
|
102
|
+
end
|
103
|
+
|
104
|
+
total_size = 0
|
105
|
+
|
106
|
+
$stdout.puts "-Queue ID- --Size-- ----Arrival Time---- -Sender/Recipient-------"
|
107
|
+
emails.each do |email|
|
108
|
+
size = email.mail.length
|
109
|
+
total_size += size
|
110
|
+
|
111
|
+
create_timestamp = email.created_at rescue
|
112
|
+
email.created_on rescue
|
113
|
+
Time.at(email.created_date) rescue # for Robot Co-op
|
114
|
+
nil
|
115
|
+
|
116
|
+
created = if create_timestamp.nil? then
|
117
|
+
' Unknown'
|
118
|
+
else
|
119
|
+
create_timestamp.strftime '%a %b %d %H:%M:%S'
|
120
|
+
end
|
121
|
+
|
122
|
+
$stdout.puts "%10d %8d %s %s" % [email.id, size, created, email.from]
|
123
|
+
if email.last_send_attempt then
|
124
|
+
$stdout.puts "Last send attempt: #{email.last_send_attempt}"
|
125
|
+
end
|
126
|
+
$stdout.puts " #{email.to}"
|
127
|
+
$stdout.puts
|
128
|
+
end
|
129
|
+
|
130
|
+
$stdout.puts "-- #{total_size/1024} Kbytes in #{emails.length} Requests."
|
131
|
+
end
|
132
|
+
|
133
|
+
##
|
134
|
+
# Processes command line options in +args+
|
135
|
+
|
136
|
+
def self.process_args(args)
|
137
|
+
name = File.basename $0
|
138
|
+
|
139
|
+
options = {}
|
140
|
+
options[:Daemon] = false
|
141
|
+
options[:Delay] = 60
|
142
|
+
options[:Once] = false
|
143
|
+
options[:TableName] = 'Email'
|
144
|
+
|
145
|
+
opts = OptionParser.new do |opts|
|
146
|
+
opts.banner = "Usage: #{name} [options]"
|
147
|
+
opts.separator ''
|
148
|
+
|
149
|
+
opts.separator "#{name} scans the email table for new messages and sends them to the"
|
150
|
+
opts.separator "website's configured SMTP host."
|
151
|
+
opts.separator ''
|
152
|
+
opts.separator "#{name} must be run from the application's root."
|
153
|
+
|
154
|
+
opts.separator ''
|
155
|
+
opts.separator 'Sendmail options:'
|
156
|
+
|
157
|
+
opts.on("-b", "--batch-size BATCH_SIZE",
|
158
|
+
"Maximum number of emails to send per delay",
|
159
|
+
"Default: Deliver all available emails", Integer) do |batch_size|
|
160
|
+
options[:BatchSize] = batch_size
|
161
|
+
end
|
162
|
+
|
163
|
+
opts.on( "--delay DELAY",
|
164
|
+
"Delay between checks for new mail",
|
165
|
+
"in the database",
|
166
|
+
"Default: #{options[:Delay]}", Integer) do |delay|
|
167
|
+
options[:Delay] = delay
|
168
|
+
end
|
169
|
+
|
170
|
+
opts.on("-o", "--once",
|
171
|
+
"Only check for new mail and deliver once",
|
172
|
+
"Default: #{options[:Once]}") do |once|
|
173
|
+
options[:Once] = once
|
174
|
+
end
|
175
|
+
|
176
|
+
opts.on("-d", "--daemonize",
|
177
|
+
"Run as a daemon process",
|
178
|
+
"Default: #{options[:Daemon]}") do |daemon|
|
179
|
+
options[:Daemon] = true
|
180
|
+
end
|
181
|
+
|
182
|
+
opts.on( "--mailq",
|
183
|
+
"Display a list of emails waiting to be sent") do |mailq|
|
184
|
+
options[:MailQ] = true
|
185
|
+
end
|
186
|
+
|
187
|
+
opts.separator ''
|
188
|
+
opts.separator 'Setup Options:'
|
189
|
+
|
190
|
+
opts.on( "--create-migration",
|
191
|
+
"Prints a migration to add an Email table",
|
192
|
+
"to $stdout") do |create|
|
193
|
+
options[:Migrate] = true
|
194
|
+
end
|
195
|
+
|
196
|
+
opts.on( "--create-model",
|
197
|
+
"Prints a model for an Email ActiveRecord",
|
198
|
+
"object to $stdout") do |create|
|
199
|
+
options[:Model] = true
|
200
|
+
end
|
201
|
+
|
202
|
+
opts.separator ''
|
203
|
+
opts.separator 'Generic Options:'
|
204
|
+
|
205
|
+
opts.on("-t", "--table-name TABLE_NAME",
|
206
|
+
"Name of table holding emails",
|
207
|
+
"Used for both sendmail and",
|
208
|
+
"migration creation",
|
209
|
+
"Default: #{options[:TableName]}") do |name|
|
210
|
+
options[:TableName] = name
|
211
|
+
end
|
212
|
+
|
213
|
+
opts.on("-v", "--[no-]verbose",
|
214
|
+
"Be verbose",
|
215
|
+
"Default: #{options[:Verbose]}") do |verbose|
|
216
|
+
options[:Verbose] = verbose
|
217
|
+
end
|
218
|
+
|
219
|
+
opts.on("-h", "--help",
|
220
|
+
"You're looking at it") do
|
221
|
+
$stderr.puts opts
|
222
|
+
exit 1
|
223
|
+
end
|
224
|
+
|
225
|
+
opts.separator ''
|
226
|
+
end
|
227
|
+
|
228
|
+
opts.parse! args
|
229
|
+
|
230
|
+
unless options.include? :Migrate or options.include? :Model or
|
231
|
+
not $".grep(/config\/environment.rb/).empty? then
|
232
|
+
$stderr.puts "#{name} must be run from a Rails application's root to deliver email."
|
233
|
+
$stderr.puts
|
234
|
+
$stderr.puts opts
|
235
|
+
exit 1
|
236
|
+
end
|
237
|
+
|
238
|
+
return options
|
239
|
+
end
|
240
|
+
|
241
|
+
##
|
242
|
+
# Processes +args+ and runs as appropriate
|
243
|
+
|
244
|
+
def self.run(args = ARGV)
|
245
|
+
options = process_args args
|
246
|
+
|
247
|
+
if options.include? :Migrate then
|
248
|
+
create_migration options[:TableName]
|
249
|
+
exit
|
250
|
+
elsif options.include? :Model then
|
251
|
+
create_model options[:TableName]
|
252
|
+
exit
|
253
|
+
elsif options.include? :Mailq then
|
254
|
+
mailq
|
255
|
+
exit
|
256
|
+
end
|
257
|
+
|
258
|
+
if options[:Daemon] then
|
259
|
+
require 'webrick/server'
|
260
|
+
WEBrick::Daemon.start
|
261
|
+
end
|
262
|
+
|
263
|
+
new(options).run
|
264
|
+
end
|
265
|
+
|
266
|
+
##
|
267
|
+
# Creates a new ARSendmail.
|
268
|
+
#
|
269
|
+
# Valid options are:
|
270
|
+
# <tt>:BatchSize</tt>:: Maximum number of emails to send per delay
|
271
|
+
# <tt>:Delay</tt>:: Delay between deliver attempts
|
272
|
+
# <tt>:TableName</tt>:: Table name that stores the emails
|
273
|
+
# <tt>:Once</tt>:: Only attempt to deliver emails once when run is called
|
274
|
+
# <tt>:Verbose</tt>:: Be verbose.
|
275
|
+
|
276
|
+
def initialize(options = {})
|
277
|
+
options[:Delay] ||= 60
|
278
|
+
options[:TableName] ||= 'Email'
|
279
|
+
|
280
|
+
@batch_size = options[:BatchSize]
|
281
|
+
@delay = options[:Delay]
|
282
|
+
@email_class = Object.path2class options[:TableName]
|
283
|
+
@once = options[:Once]
|
284
|
+
@verbose = options[:Verbose]
|
285
|
+
end
|
286
|
+
|
287
|
+
##
|
288
|
+
# Delivers +emails+ to ActionMailer's SMTP server and destroys them.
|
289
|
+
|
290
|
+
def deliver(emails)
|
291
|
+
Net::SMTP.start server_settings[:address], server_settings[:port],
|
292
|
+
server_settings[:domain], server_settings[:user],
|
293
|
+
server_settings[:password],
|
294
|
+
server_settings[:authentication] do |smtp|
|
295
|
+
emails.each do |email|
|
296
|
+
begin
|
297
|
+
res = smtp.send_message email.mail, email.to, email.from
|
298
|
+
email.destroy
|
299
|
+
log "sent email from %s to %s: %p" % [email.from, email.to, res]
|
300
|
+
rescue Net::SMTPServerBusy, Net::SMTPFatalError,
|
301
|
+
Net::SMTPUnknownError, TimeoutError
|
302
|
+
email.last_send_attempt = Time.now.to_i
|
303
|
+
end
|
304
|
+
end
|
305
|
+
end
|
306
|
+
end
|
307
|
+
|
308
|
+
##
|
309
|
+
# Returns emails in email_class that haven't had a delivery attempt in the
|
310
|
+
# last 300 seconds.
|
311
|
+
|
312
|
+
def find_emails
|
313
|
+
options = { :conditions => ['last_send_attempt < ?', Time.now.to_i - 300] }
|
314
|
+
options[:limit] = batch_size unless batch_size.nil?
|
315
|
+
mail = @email_class.find :all, options
|
316
|
+
|
317
|
+
log "found #{mail.length} emails to send"
|
318
|
+
mail
|
319
|
+
end
|
320
|
+
|
321
|
+
##
|
322
|
+
# Logs +message+ if verbose
|
323
|
+
|
324
|
+
def log(message)
|
325
|
+
$stderr.puts message if @verbose
|
326
|
+
ActionMailer::Base.logger.info "ar_sendmail: #{message}"
|
327
|
+
end
|
328
|
+
|
329
|
+
##
|
330
|
+
# Scans for emails and delivers them every delay seconds. Only returns if
|
331
|
+
# once is true.
|
332
|
+
|
333
|
+
def run
|
334
|
+
loop do
|
335
|
+
deliver find_emails
|
336
|
+
break if @once
|
337
|
+
sleep @delay
|
338
|
+
end
|
339
|
+
end
|
340
|
+
|
341
|
+
##
|
342
|
+
# Proxy to ActionMailer::Base#server_settings. See
|
343
|
+
# http://api.rubyonrails.org/classes/ActionMailer/Base.html
|
344
|
+
# for instructions on how to configure ActionMailer's SMTP server.
|
345
|
+
|
346
|
+
def server_settings
|
347
|
+
ActionMailer::Base.server_settings
|
348
|
+
end
|
349
|
+
|
350
|
+
end
|
351
|
+
|
@@ -0,0 +1,131 @@
|
|
1
|
+
require 'net/smtp'
|
2
|
+
require 'time'
|
3
|
+
|
4
|
+
class Net::SMTP
|
5
|
+
|
6
|
+
@deliveries = []
|
7
|
+
|
8
|
+
@send_message_block = nil
|
9
|
+
|
10
|
+
class << self
|
11
|
+
|
12
|
+
attr_reader :deliveries
|
13
|
+
attr_reader :send_message_block
|
14
|
+
|
15
|
+
alias old_start start
|
16
|
+
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.start(*args)
|
20
|
+
yield new(nil)
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.on_send_message(&block)
|
24
|
+
@send_message_block = block
|
25
|
+
end
|
26
|
+
|
27
|
+
alias old_send_message send_message
|
28
|
+
|
29
|
+
def send_message(mail, to, from)
|
30
|
+
return self.class.send_message_block.call(mail, to, from) unless
|
31
|
+
self.class.send_message_block.nil?
|
32
|
+
self.class.deliveries << [mail, to, from]
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
|
37
|
+
##
|
38
|
+
# Stub for ActionMailer::Base
|
39
|
+
|
40
|
+
module ActionMailer; end
|
41
|
+
|
42
|
+
class ActionMailer::Base
|
43
|
+
|
44
|
+
@server_settings = {}
|
45
|
+
|
46
|
+
def self.logger
|
47
|
+
o = Object.new
|
48
|
+
def o.info(arg) end
|
49
|
+
return o
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.method_missing(meth, *args)
|
53
|
+
meth.to_s =~ /deliver_(.*)/
|
54
|
+
super unless $1
|
55
|
+
new($1, *args).deliver!
|
56
|
+
end
|
57
|
+
|
58
|
+
def self.server_settings
|
59
|
+
@server_settings
|
60
|
+
end
|
61
|
+
|
62
|
+
def initialize(meth = nil)
|
63
|
+
send meth if meth
|
64
|
+
end
|
65
|
+
|
66
|
+
def deliver!
|
67
|
+
perform_delivery_activerecord @mail
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
|
72
|
+
##
|
73
|
+
# Stub for an ActiveRecord model
|
74
|
+
|
75
|
+
class Email
|
76
|
+
|
77
|
+
START = Time.parse 'Thu Aug 10 11:19:48'
|
78
|
+
|
79
|
+
attr_accessor :from, :to, :mail, :last_send_attempt, :created_on, :id
|
80
|
+
|
81
|
+
@records = []
|
82
|
+
@id = 0
|
83
|
+
|
84
|
+
class << self; attr_accessor :records, :id; end
|
85
|
+
|
86
|
+
def self.create(record)
|
87
|
+
record = new record[:from], record[:to], record[:mail]
|
88
|
+
records << record
|
89
|
+
return record
|
90
|
+
end
|
91
|
+
|
92
|
+
def self.find(_, conditions = nil)
|
93
|
+
return records if conditions.nil?
|
94
|
+
now = Time.now.to_i - 300
|
95
|
+
return records.select do |r|
|
96
|
+
r.last_send_attempt.nil? or r.last_send_attempt < now
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def initialize(from, to, mail)
|
101
|
+
@from = from
|
102
|
+
@to = to
|
103
|
+
@mail = mail
|
104
|
+
@id = self.class.id += 1
|
105
|
+
@created_on = START + @id
|
106
|
+
end
|
107
|
+
|
108
|
+
def destroy
|
109
|
+
self.class.records.delete self
|
110
|
+
self.freeze
|
111
|
+
end
|
112
|
+
|
113
|
+
def ==(other)
|
114
|
+
other.from == from and
|
115
|
+
other.to == to and
|
116
|
+
other.mail == mail
|
117
|
+
end
|
118
|
+
|
119
|
+
end
|
120
|
+
|
121
|
+
class String
|
122
|
+
def classify
|
123
|
+
self
|
124
|
+
end
|
125
|
+
|
126
|
+
def tableize
|
127
|
+
self.downcase
|
128
|
+
end
|
129
|
+
|
130
|
+
end
|
131
|
+
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
require 'action_mailer'
|
3
|
+
require 'action_mailer/ar_mailer'
|
4
|
+
|
5
|
+
##
|
6
|
+
# Pretend mailer
|
7
|
+
|
8
|
+
class Mailer < ActionMailer::ARMailer
|
9
|
+
|
10
|
+
def mail
|
11
|
+
@mail = Object.new
|
12
|
+
def @mail.encoded() 'email' end
|
13
|
+
def @mail.from() ['nobody@example.com'] end
|
14
|
+
def @mail.destinations() %w[user1@example.com user2@example.com] end
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
|
19
|
+
class TestARMailer < Test::Unit::TestCase
|
20
|
+
|
21
|
+
def setup
|
22
|
+
Email.records.clear
|
23
|
+
end
|
24
|
+
|
25
|
+
def test_perform_delivery_activerecord
|
26
|
+
Mailer.deliver_mail
|
27
|
+
|
28
|
+
assert_equal 2, Email.records.length
|
29
|
+
|
30
|
+
record = Email.records.first
|
31
|
+
assert_equal 'email', record.mail
|
32
|
+
assert_equal 'user1@example.com', record.to
|
33
|
+
assert_equal 'nobody@example.com', record.from
|
34
|
+
|
35
|
+
assert_equal 'user2@example.com', Email.records.last.to
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
|
@@ -0,0 +1,367 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
require 'action_mailer'
|
3
|
+
require 'action_mailer/ar_sendmail'
|
4
|
+
require 'rubygems'
|
5
|
+
require 'test/zentest_assertions'
|
6
|
+
|
7
|
+
class TestARSendmail < Test::Unit::TestCase
|
8
|
+
|
9
|
+
def setup
|
10
|
+
ActionMailer::Base.server_settings.clear
|
11
|
+
Email.id = 0
|
12
|
+
Email.records.clear
|
13
|
+
Net::SMTP.deliveries.clear
|
14
|
+
Net::SMTP.on_send_message # reset
|
15
|
+
|
16
|
+
@sm = ActionMailer::ARSendmail.new
|
17
|
+
|
18
|
+
@include_c_e = ! $".grep(/config\/environment.rb/).empty?
|
19
|
+
$" << 'config/environment.rb' unless @include_c_e
|
20
|
+
end
|
21
|
+
|
22
|
+
def teardown
|
23
|
+
$".delete 'config/environment.rb' unless @include_c_e
|
24
|
+
end
|
25
|
+
|
26
|
+
def test_class_create_migration
|
27
|
+
out, = util_capture do
|
28
|
+
ActionMailer::ARSendmail.create_migration 'Email'
|
29
|
+
end
|
30
|
+
|
31
|
+
expected = <<-EOF
|
32
|
+
class AddEmail < ActiveRecord::Migration
|
33
|
+
def self.up
|
34
|
+
create_table :email do |t|
|
35
|
+
t.column :from, :string
|
36
|
+
t.column :to, :string
|
37
|
+
t.column :last_send_attempt, :integer, :default => 0
|
38
|
+
t.column :mail, :text
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.down
|
43
|
+
drop_table :email
|
44
|
+
end
|
45
|
+
end
|
46
|
+
EOF
|
47
|
+
|
48
|
+
assert_equal expected, out.string
|
49
|
+
end
|
50
|
+
|
51
|
+
def test_class_create_migration_table_name
|
52
|
+
out, = util_capture do
|
53
|
+
ActionMailer::ARSendmail.create_migration 'Mail'
|
54
|
+
end
|
55
|
+
|
56
|
+
expected = <<-EOF
|
57
|
+
class AddMail < ActiveRecord::Migration
|
58
|
+
def self.up
|
59
|
+
create_table :mail do |t|
|
60
|
+
t.column :from, :string
|
61
|
+
t.column :to, :string
|
62
|
+
t.column :last_send_attempt, :integer, :default => 0
|
63
|
+
t.column :mail, :text
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def self.down
|
68
|
+
drop_table :email
|
69
|
+
end
|
70
|
+
end
|
71
|
+
EOF
|
72
|
+
|
73
|
+
assert_equal expected, out.string
|
74
|
+
end
|
75
|
+
|
76
|
+
def test_class_create_model
|
77
|
+
out, = util_capture do
|
78
|
+
ActionMailer::ARSendmail.create_model 'Email'
|
79
|
+
end
|
80
|
+
|
81
|
+
expected = <<-EOF
|
82
|
+
class Email < ActiveRecord::Base
|
83
|
+
end
|
84
|
+
EOF
|
85
|
+
|
86
|
+
assert_equal expected, out.string
|
87
|
+
end
|
88
|
+
|
89
|
+
def test_class_create_model_table_name
|
90
|
+
out, = util_capture do
|
91
|
+
ActionMailer::ARSendmail.create_model 'Mail'
|
92
|
+
end
|
93
|
+
|
94
|
+
expected = <<-EOF
|
95
|
+
class Mail < ActiveRecord::Base
|
96
|
+
end
|
97
|
+
EOF
|
98
|
+
|
99
|
+
assert_equal expected, out.string
|
100
|
+
end
|
101
|
+
|
102
|
+
def test_class_mailq
|
103
|
+
Email.create :from => nobody, :to => 'recip@h1.example.com',
|
104
|
+
:mail => 'body0'
|
105
|
+
Email.create :from => nobody, :to => 'recip@h1.example.com',
|
106
|
+
:mail => 'body1'
|
107
|
+
last = Email.create :from => nobody, :to => 'recip@h2.example.com',
|
108
|
+
:mail => 'body2'
|
109
|
+
|
110
|
+
last.last_send_attempt = Time.parse 'Thu Aug 10 11:40:05'
|
111
|
+
|
112
|
+
out, err = util_capture do
|
113
|
+
ActionMailer::ARSendmail.mailq
|
114
|
+
end
|
115
|
+
|
116
|
+
expected = <<-EOF
|
117
|
+
-Queue ID- --Size-- ----Arrival Time---- -Sender/Recipient-------
|
118
|
+
1 5 Thu Aug 10 11:19:49 nobody@example.com
|
119
|
+
recip@h1.example.com
|
120
|
+
|
121
|
+
2 5 Thu Aug 10 11:19:50 nobody@example.com
|
122
|
+
recip@h1.example.com
|
123
|
+
|
124
|
+
3 5 Thu Aug 10 11:19:51 nobody@example.com
|
125
|
+
Last send attempt: Thu Aug 10 11:40:05 -0700 2006
|
126
|
+
recip@h2.example.com
|
127
|
+
|
128
|
+
-- 0 Kbytes in 3 Requests.
|
129
|
+
EOF
|
130
|
+
|
131
|
+
assert_equal expected, out.string
|
132
|
+
end
|
133
|
+
|
134
|
+
def test_class_mailq_empty
|
135
|
+
out, err = util_capture do
|
136
|
+
ActionMailer::ARSendmail.mailq
|
137
|
+
end
|
138
|
+
|
139
|
+
assert_equal "Mail queue is empty\n", out.string
|
140
|
+
end
|
141
|
+
|
142
|
+
def test_class_new
|
143
|
+
assert_equal 60, @sm.delay
|
144
|
+
assert_equal Email, @sm.email_class
|
145
|
+
assert_equal nil, @sm.once
|
146
|
+
assert_equal nil, @sm.verbose
|
147
|
+
assert_equal nil, @sm.batch_size
|
148
|
+
|
149
|
+
@sm = ActionMailer::ARSendmail.new :Delay => 75, :Verbose => true,
|
150
|
+
:TableName => 'Object', :Once => true,
|
151
|
+
:BatchSize => 1000
|
152
|
+
|
153
|
+
assert_equal 75, @sm.delay
|
154
|
+
assert_equal Object, @sm.email_class
|
155
|
+
assert_equal true, @sm.once
|
156
|
+
assert_equal true, @sm.verbose
|
157
|
+
assert_equal 1000, @sm.batch_size
|
158
|
+
end
|
159
|
+
|
160
|
+
def test_class_parse_args_batch_size
|
161
|
+
options = ActionMailer::ARSendmail.process_args %w[-b 500]
|
162
|
+
|
163
|
+
assert_equal 500, options[:BatchSize]
|
164
|
+
|
165
|
+
options = ActionMailer::ARSendmail.process_args %w[--batch-size 500]
|
166
|
+
|
167
|
+
assert_equal 500, options[:BatchSize]
|
168
|
+
end
|
169
|
+
|
170
|
+
def test_class_parse_args_daemon
|
171
|
+
argv = %w[-d]
|
172
|
+
|
173
|
+
options = ActionMailer::ARSendmail.process_args argv
|
174
|
+
|
175
|
+
assert_equal true, options[:Daemon]
|
176
|
+
|
177
|
+
argv = %w[--daemon]
|
178
|
+
|
179
|
+
options = ActionMailer::ARSendmail.process_args argv
|
180
|
+
|
181
|
+
assert_equal true, options[:Daemon]
|
182
|
+
end
|
183
|
+
|
184
|
+
def test_class_parse_args_delay
|
185
|
+
argv = %w[--delay 75]
|
186
|
+
|
187
|
+
options = ActionMailer::ARSendmail.process_args argv
|
188
|
+
|
189
|
+
assert_equal 75, options[:Delay]
|
190
|
+
end
|
191
|
+
|
192
|
+
def test_class_parse_args_mailq
|
193
|
+
options = ActionMailer::ARSendmail.process_args []
|
194
|
+
deny_includes options, :MailQ
|
195
|
+
|
196
|
+
argv = %w[--mailq]
|
197
|
+
|
198
|
+
options = ActionMailer::ARSendmail.process_args argv
|
199
|
+
|
200
|
+
assert_equal true, options[:MailQ]
|
201
|
+
end
|
202
|
+
|
203
|
+
def test_class_parse_args_migration
|
204
|
+
options = ActionMailer::ARSendmail.process_args []
|
205
|
+
deny_includes options, :Migrate
|
206
|
+
|
207
|
+
argv = %w[--create-migration]
|
208
|
+
|
209
|
+
options = ActionMailer::ARSendmail.process_args argv
|
210
|
+
|
211
|
+
assert_equal true, options[:Migrate]
|
212
|
+
end
|
213
|
+
|
214
|
+
def test_class_parse_args_model
|
215
|
+
options = ActionMailer::ARSendmail.process_args []
|
216
|
+
deny_includes options, :Model
|
217
|
+
|
218
|
+
argv = %w[--create-model]
|
219
|
+
|
220
|
+
options = ActionMailer::ARSendmail.process_args argv
|
221
|
+
|
222
|
+
assert_equal true, options[:Model]
|
223
|
+
end
|
224
|
+
|
225
|
+
def test_class_parse_args_no_config_environment
|
226
|
+
$".delete 'config/environment.rb'
|
227
|
+
|
228
|
+
assert_raise SystemExit do
|
229
|
+
out, err = util_capture do
|
230
|
+
ActionMailer::ARSendmail.process_args []
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
ensure
|
235
|
+
$" << 'config/environment.rb' if @include_c_e
|
236
|
+
end
|
237
|
+
|
238
|
+
def test_class_parse_args_no_config_environment_migrate
|
239
|
+
$".delete 'config/environment.rb'
|
240
|
+
|
241
|
+
out, err = util_capture do
|
242
|
+
ActionMailer::ARSendmail.process_args %w[--create-migration]
|
243
|
+
end
|
244
|
+
|
245
|
+
assert true # count
|
246
|
+
|
247
|
+
ensure
|
248
|
+
$" << 'config/environment.rb' if @include_c_e
|
249
|
+
end
|
250
|
+
|
251
|
+
def test_class_parse_args_no_config_environment_model
|
252
|
+
$".delete 'config/environment.rb'
|
253
|
+
|
254
|
+
out, err = util_capture do
|
255
|
+
ActionMailer::ARSendmail.process_args %w[--create-model]
|
256
|
+
end
|
257
|
+
|
258
|
+
assert true # count
|
259
|
+
|
260
|
+
rescue SystemExit
|
261
|
+
flunk 'Should not exit'
|
262
|
+
|
263
|
+
ensure
|
264
|
+
$" << 'config/environment.rb' if @include_c_e
|
265
|
+
end
|
266
|
+
|
267
|
+
def test_class_parse_args_once
|
268
|
+
argv = %w[-o]
|
269
|
+
|
270
|
+
options = ActionMailer::ARSendmail.process_args argv
|
271
|
+
|
272
|
+
assert_equal true, options[:Once]
|
273
|
+
|
274
|
+
argv = %w[--once]
|
275
|
+
|
276
|
+
options = ActionMailer::ARSendmail.process_args argv
|
277
|
+
|
278
|
+
assert_equal true, options[:Once]
|
279
|
+
end
|
280
|
+
|
281
|
+
def test_class_parse_args_table_name
|
282
|
+
argv = %w[-t Email]
|
283
|
+
|
284
|
+
options = ActionMailer::ARSendmail.process_args argv
|
285
|
+
|
286
|
+
assert_equal 'Email', options[:TableName]
|
287
|
+
|
288
|
+
argv = %w[--table-name=Email]
|
289
|
+
|
290
|
+
options = ActionMailer::ARSendmail.process_args argv
|
291
|
+
|
292
|
+
assert_equal 'Email', options[:TableName]
|
293
|
+
end
|
294
|
+
|
295
|
+
def test_deliver
|
296
|
+
email = Email.create :mail => 'body', :to => 'to', :from => 'from'
|
297
|
+
|
298
|
+
@sm.deliver [email]
|
299
|
+
|
300
|
+
assert_equal 1, Net::SMTP.deliveries.length
|
301
|
+
assert_equal ['body', 'to', 'from'], Net::SMTP.deliveries.first
|
302
|
+
assert_equal 0, Email.records.length
|
303
|
+
end
|
304
|
+
|
305
|
+
def test_deliver_500
|
306
|
+
Net::SMTP.on_send_message do
|
307
|
+
raise Net::SMTPServerBusy
|
308
|
+
end
|
309
|
+
|
310
|
+
now = Time.now.to_i
|
311
|
+
|
312
|
+
email = Email.create :mail => 'body', :to => 'to', :from => 'from'
|
313
|
+
|
314
|
+
@sm.deliver [email]
|
315
|
+
|
316
|
+
assert_equal 0, Net::SMTP.deliveries.length
|
317
|
+
assert_equal 1, Email.records.length
|
318
|
+
assert_operator now, :<=, Email.records.first.last_send_attempt
|
319
|
+
end
|
320
|
+
|
321
|
+
def test_log
|
322
|
+
@sm = ActionMailer::ARSendmail.new :Verbose => true
|
323
|
+
|
324
|
+
out, err = util_capture do
|
325
|
+
@sm.log 'hi'
|
326
|
+
end
|
327
|
+
|
328
|
+
assert_equal "hi\n", err.string
|
329
|
+
end
|
330
|
+
|
331
|
+
def test_find_emails
|
332
|
+
emails = [
|
333
|
+
{ :mail => 'body0', :to => 'recip@h1.example.com', :from => nobody },
|
334
|
+
{ :mail => 'body1', :to => 'recip@h1.example.com', :from => nobody },
|
335
|
+
{ :mail => 'body2', :to => 'recip@h2.example.com', :from => nobody },
|
336
|
+
]
|
337
|
+
|
338
|
+
emails.each do |email| Email.create email end
|
339
|
+
|
340
|
+
tried = Email.create :mail => 'body3', :to => 'recip@h3.example.com',
|
341
|
+
:from => nobody
|
342
|
+
|
343
|
+
tried.last_send_attempt = Time.now.to_i - 258
|
344
|
+
|
345
|
+
found_emails = @sm.find_emails
|
346
|
+
|
347
|
+
expected = [
|
348
|
+
Email.new(nobody, 'recip@h1.example.com', 'body0'),
|
349
|
+
Email.new(nobody, 'recip@h1.example.com', 'body1'),
|
350
|
+
Email.new(nobody, 'recip@h2.example.com', 'body2'),
|
351
|
+
]
|
352
|
+
|
353
|
+
assert_equal expected, found_emails
|
354
|
+
end
|
355
|
+
|
356
|
+
def test_server_settings
|
357
|
+
ActionMailer::Base.server_settings[:address] = 'localhost'
|
358
|
+
|
359
|
+
assert_equal 'localhost', @sm.server_settings[:address]
|
360
|
+
end
|
361
|
+
|
362
|
+
def nobody
|
363
|
+
'nobody@example.com'
|
364
|
+
end
|
365
|
+
|
366
|
+
end
|
367
|
+
|
metadata
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
rubygems_version: 0.8.99
|
3
|
+
specification_version: 1
|
4
|
+
name: ar_mailer
|
5
|
+
version: !ruby/object:Gem::Version
|
6
|
+
version: 1.0.0
|
7
|
+
date: 2006-08-10 00:00:00 -07:00
|
8
|
+
summary: A two-phase deliver agent for ActionMailer
|
9
|
+
require_paths:
|
10
|
+
- lib
|
11
|
+
email: eric@robotcoop.com
|
12
|
+
homepage:
|
13
|
+
rubyforge_project:
|
14
|
+
description: Queues emails from ActionMailer in the database and uses a separate process to send them. Reduces sending overhead when sending hundreds of emails.
|
15
|
+
autorequire:
|
16
|
+
default_executable:
|
17
|
+
bindir: bin
|
18
|
+
has_rdoc: true
|
19
|
+
required_ruby_version: !ruby/object:Gem::Version::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">"
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 0.0.0
|
24
|
+
version:
|
25
|
+
platform: ruby
|
26
|
+
signing_key:
|
27
|
+
cert_chain:
|
28
|
+
post_install_message:
|
29
|
+
authors:
|
30
|
+
- Eric Hodel
|
31
|
+
files:
|
32
|
+
- LICENSE
|
33
|
+
- Manifest.txt
|
34
|
+
- README
|
35
|
+
- Rakefile
|
36
|
+
- bin/ar_sendmail
|
37
|
+
- lib/action_mailer/ar_mailer.rb
|
38
|
+
- lib/action_mailer/ar_sendmail.rb
|
39
|
+
- test/action_mailer.rb
|
40
|
+
- test/test_armailer.rb
|
41
|
+
- test/test_arsendmail.rb
|
42
|
+
test_files: []
|
43
|
+
|
44
|
+
rdoc_options: []
|
45
|
+
|
46
|
+
extra_rdoc_files: []
|
47
|
+
|
48
|
+
executables:
|
49
|
+
- ar_sendmail
|
50
|
+
extensions: []
|
51
|
+
|
52
|
+
requirements: []
|
53
|
+
|
54
|
+
dependencies: []
|
55
|
+
|