leftbrained-ar_mailer 2.1.9

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.
@@ -0,0 +1,16 @@
1
+ class <%= migration_name %> < ActiveRecord::Migration
2
+ def self.up
3
+ create_table :<%= table_name %> do |t|
4
+ t.column :from, :string
5
+ t.column :to, :string
6
+ t.column :last_send_attempt, :integer, :default => 0
7
+ t.column :mail, :text
8
+ t.column :worker, :string, :default=>""
9
+ t.column :created_on, :datetime
10
+ end
11
+ end
12
+
13
+ def self.down
14
+ drop_table :<%= table_name %>
15
+ end
16
+ end
@@ -0,0 +1,2 @@
1
+ class <%= class_name %> < ActiveRecord::Base
2
+ end
@@ -0,0 +1,34 @@
1
+ ##
2
+ # Adds sending email through an ActiveRecord table as a delivery method for
3
+ # ActionMailer.
4
+ #
5
+
6
+ class ActionMailer::Base
7
+
8
+ ##
9
+ # Set the email class for deliveries. Handle class reloading issues which prevents caching the email class.
10
+ #
11
+ @@email_class_name = 'Email'
12
+
13
+ def self.email_class=(klass)
14
+ @@email_class_name = klass.to_s
15
+ end
16
+
17
+ def self.email_class
18
+ @@email_class_name.constantize
19
+ end
20
+
21
+ ##
22
+ # Adds +mail+ to the Email table. Only the first From address for +mail+ is
23
+ # used.
24
+
25
+ def perform_delivery_activerecord(mail)
26
+ destinations = mail.destinations
27
+ mail.ready_to_send
28
+ sender = (mail['return-path'] && mail['return-path'].spec) || mail.from.first
29
+ destinations.each do |destination|
30
+ self.class.email_class.create :mail => mail.encoded, :to => destination, :from => sender
31
+ end
32
+ end
33
+
34
+ end
@@ -0,0 +1,499 @@
1
+ require 'optparse'
2
+ require 'net/smtp'
3
+ require 'smtp_tls' unless Net::SMTP.instance_methods.include?("enable_starttls_auto")
4
+
5
+ ##
6
+ # Hack in RSET
7
+
8
+ module Net # :nodoc:
9
+ class SMTP # :nodoc:
10
+
11
+ unless instance_methods.include? 'reset' then
12
+ ##
13
+ # Resets the SMTP connection.
14
+
15
+ def reset
16
+ getok 'RSET'
17
+ end
18
+ end
19
+
20
+ end
21
+ end
22
+
23
+ ##
24
+ # ActionMailer::ARSendmail delivers email from the email table to the
25
+ # SMTP server configured in your application's config/environment.rb.
26
+ # ar_sendmail does not work with sendmail delivery.
27
+ #
28
+ # ar_mailer can deliver to SMTP with TLS using smtp_tls.rb borrowed from Kyle
29
+ # Maxwell's action_mailer_optional_tls plugin. Simply set the :tls option in
30
+ # ActionMailer::Base's smtp_settings to true to enable TLS.
31
+ #
32
+ # See ar_sendmail -h for the full list of supported options.
33
+ #
34
+ # The interesting options are:
35
+ # * --daemon
36
+ # * --mailq
37
+
38
+ module ActionMailer; end
39
+
40
+ class ActionMailer::ARSendmail
41
+
42
+ ##
43
+ # The version of ActionMailer::ARSendmail you are running.
44
+
45
+ VERSION = '2.1.9'
46
+
47
+ ##
48
+ # Maximum number of times authentication will be consecutively retried
49
+
50
+ MAX_AUTH_FAILURES = 2
51
+
52
+ ##
53
+ # Email delivery attempts per run
54
+
55
+ attr_accessor :batch_size
56
+
57
+ ##
58
+ # Seconds to delay between runs
59
+
60
+ attr_accessor :delay
61
+
62
+ ##
63
+ # Maximum age of emails in seconds before they are removed from the queue.
64
+
65
+ attr_accessor :max_age
66
+
67
+ ##
68
+ # Be verbose
69
+
70
+ attr_accessor :verbose
71
+
72
+
73
+ ##
74
+ # True if only one delivery attempt will be made per call to run
75
+
76
+ attr_reader :once
77
+
78
+ ##
79
+ # Times authentication has failed
80
+
81
+ attr_accessor :failed_auth_count
82
+
83
+ ##
84
+ # Is this email assigned to a specific worker?
85
+
86
+ attr_reader :worker
87
+
88
+
89
+
90
+
91
+ @@pid_file = nil
92
+
93
+ def self.remove_pid_file
94
+ if @@pid_file
95
+ require 'shell'
96
+ sh = Shell.new
97
+ sh.rm @@pid_file
98
+ end
99
+ end
100
+
101
+ ##
102
+ # Prints a list of unsent emails and the last delivery attempt, if any.
103
+ #
104
+ # If ActiveRecord::Timestamp is not being used the arrival time will not be
105
+ # known. See http://api.rubyonrails.org/classes/ActiveRecord/Timestamp.html
106
+ # to learn how to enable ActiveRecord::Timestamp.
107
+
108
+ def self.mailq
109
+ emails = ActionMailer::Base.email_class.find :all
110
+
111
+ if emails.empty? then
112
+ puts "Mail queue is empty"
113
+ return
114
+ end
115
+
116
+ total_size = 0
117
+
118
+ puts "-Queue ID- --Size-- ----Arrival Time---- -Sender/Recipient-------"
119
+ emails.each do |email|
120
+ size = email.mail.length
121
+ total_size += size
122
+
123
+ create_timestamp = email.created_on rescue
124
+ email.created_at rescue
125
+ Time.at(email.created_date) rescue # for Robot Co-op
126
+ nil
127
+
128
+ created = if create_timestamp.nil? then
129
+ ' Unknown'
130
+ else
131
+ create_timestamp.strftime '%a %b %d %H:%M:%S'
132
+ end
133
+
134
+ puts "%10d %8d %s %s" % [email.id, size, created, email.from]
135
+ if email.last_send_attempt > 0 then
136
+ puts "Last send attempt: #{Time.at email.last_send_attempt}"
137
+ end
138
+ puts " #{email.to}"
139
+ puts
140
+ end
141
+
142
+ puts "-- #{total_size/1024} Kbytes in #{emails.length} Requests."
143
+ end
144
+
145
+ ##
146
+ # Processes command line options in +args+
147
+
148
+ def self.process_args(args)
149
+ name = File.basename $0
150
+
151
+ options = {}
152
+ options[:Chdir] = '.'
153
+ options[:Daemon] = false
154
+ options[:Worker] = ""
155
+ options[:Delay] = 60
156
+ options[:MaxAge] = 86400 * 7
157
+ options[:Once] = false
158
+ options[:RailsEnv] = ENV['RAILS_ENV']
159
+ options[:Pidfile] = options[:Chdir] + '/log/ar_sendmail.pid'
160
+
161
+ opts = OptionParser.new do |opts|
162
+ opts.banner = "Usage: #{name} [options]"
163
+ opts.separator ''
164
+
165
+ opts.separator "#{name} scans the email table for new messages and sends them to the"
166
+ opts.separator "website's configured SMTP host."
167
+ opts.separator ''
168
+ opts.separator "#{name} must be run from a Rails application's root."
169
+
170
+ opts.separator ''
171
+ opts.separator 'Sendmail options:'
172
+
173
+ opts.on("-b", "--batch-size BATCH_SIZE",
174
+ "Maximum number of emails to send per delay",
175
+ "Default: Deliver all available emails", Integer) do |batch_size|
176
+ options[:BatchSize] = batch_size
177
+ end
178
+
179
+ opts.on( "--delay DELAY",
180
+ "Delay between checks for new mail",
181
+ "in the database",
182
+ "Default: #{options[:Delay]}", Integer) do |delay|
183
+ options[:Delay] = delay
184
+ end
185
+
186
+ opts.on( "--max-age MAX_AGE",
187
+ "Maxmimum age for an email. After this",
188
+ "it will be removed from the queue.",
189
+ "Set to 0 to disable queue cleanup.",
190
+ "Default: #{options[:MaxAge]} seconds", Integer) do |max_age|
191
+ options[:MaxAge] = max_age
192
+ end
193
+
194
+ opts.on("-o", "--once",
195
+ "Only check for new mail and deliver once",
196
+ "Default: #{options[:Once]}") do |once|
197
+ options[:Once] = once
198
+ end
199
+
200
+ opts.on("-d", "--daemonize",
201
+ "Run as a daemon process",
202
+ "Default: #{options[:Daemon]}") do |daemon|
203
+ options[:Daemon] = true
204
+ end
205
+
206
+ opts.on("-p", "--pidfile PIDFILE",
207
+ "Set the pidfile location",
208
+ "Default: #{options[:Chdir]}#{options[:Pidfile]}", String) do |pidfile|
209
+ options[:Pidfile] = pidfile
210
+ end
211
+
212
+ opts.on( "--mailq",
213
+ "Display a list of emails waiting to be sent") do |mailq|
214
+ options[:MailQ] = true
215
+ end
216
+
217
+ opts.on("-w", "--worker WORKER",
218
+ "Specify a worker name that the email should be processed through",
219
+ "Default: #{options[:Worker]}", String) do |worker|
220
+ options[:Worker] = worker
221
+ end
222
+
223
+ opts.separator ''
224
+ opts.separator 'Setup Options:'
225
+
226
+ opts.separator ''
227
+ opts.separator 'Generic Options:'
228
+
229
+ opts.on("-c", "--chdir PATH",
230
+ "Use PATH for the application path",
231
+ "Default: #{options[:Chdir]}") do |path|
232
+ usage opts, "#{path} is not a directory" unless File.directory? path
233
+ usage opts, "#{path} is not readable" unless File.readable? path
234
+ options[:Chdir] = path
235
+ end
236
+
237
+ opts.on("-e", "--environment RAILS_ENV",
238
+ "Set the RAILS_ENV constant",
239
+ "Default: #{options[:RailsEnv]}") do |env|
240
+ options[:RailsEnv] = env
241
+ end
242
+
243
+ opts.on("-v", "--[no-]verbose",
244
+ "Be verbose",
245
+ "Default: #{options[:Verbose]}") do |verbose|
246
+ options[:Verbose] = verbose
247
+ end
248
+
249
+ opts.on("-h", "--help",
250
+ "You're looking at it") do
251
+ usage opts
252
+ end
253
+
254
+ opts.on("--version", "Version of ARMailer") do
255
+ usage "ar_mailer #{VERSION} (adzap fork)"
256
+ end
257
+
258
+ opts.separator ''
259
+ end
260
+
261
+ opts.parse! args
262
+
263
+ ENV['RAILS_ENV'] = options[:RailsEnv]
264
+
265
+ Dir.chdir options[:Chdir] do
266
+ begin
267
+ require 'config/environment'
268
+ require 'action_mailer/ar_mailer'
269
+ rescue LoadError
270
+ usage opts, <<-EOF
271
+ #{name} must be run from a Rails application's root to deliver email.
272
+ #{Dir.pwd} does not appear to be a Rails application root.
273
+ EOF
274
+ end
275
+ end
276
+
277
+ return options
278
+ end
279
+
280
+ ##
281
+ # Processes +args+ and runs as appropriate
282
+
283
+ def self.run(args = ARGV)
284
+ options = process_args args
285
+
286
+ if options.include? :MailQ then
287
+ mailq
288
+ exit
289
+ end
290
+
291
+ if options[:Daemon] then
292
+ require 'webrick/server'
293
+ @@pid_file = File.expand_path(options[:Pidfile], options[:Chdir])
294
+ if File.exists? @@pid_file
295
+ # check to see if process is actually running
296
+ pid = ''
297
+ File.open(@@pid_file, 'r') {|f| pid = f.read.chomp }
298
+ if system("ps -p #{pid} | grep #{pid}") # returns true if process is running, o.w. false
299
+ $stderr.puts "Warning: The pid file #{@@pid_file} exists and ar_sendmail is running. Shutting down."
300
+ exit -1
301
+ else
302
+ # not running, so remove existing pid file and continue
303
+ self.remove_pid_file
304
+ $stderr.puts "ar_sendmail is not running. Removing existing pid file and starting up..."
305
+ end
306
+ end
307
+ WEBrick::Daemon.start
308
+ File.open(@@pid_file, 'w') {|f| f.write("#{Process.pid}\n")}
309
+ end
310
+
311
+ new(options).run
312
+
313
+ rescue SystemExit
314
+ raise
315
+ rescue SignalException
316
+ exit
317
+ rescue Exception => e
318
+ $stderr.puts "Unhandled exception #{e.message}(#{e.class}):"
319
+ $stderr.puts "\t#{e.backtrace.join "\n\t"}"
320
+ exit -2
321
+ end
322
+
323
+ ##
324
+ # Prints a usage message to $stderr using +opts+ and exits
325
+
326
+ def self.usage(opts, message = nil)
327
+ if message then
328
+ $stderr.puts message
329
+ $stderr.puts
330
+ end
331
+
332
+ $stderr.puts opts
333
+ exit 1
334
+ end
335
+
336
+ ##
337
+ # Creates a new ARSendmail.
338
+ #
339
+ # Valid options are:
340
+ # <tt>:BatchSize</tt>:: Maximum number of emails to send per delay
341
+ # <tt>:Delay</tt>:: Delay between deliver attempts
342
+ # <tt>:Once</tt>:: Only attempt to deliver emails once when run is called
343
+ # <tt>:Verbose</tt>:: Be verbose.
344
+
345
+ def initialize(options = {})
346
+ options[:Delay] ||= 60
347
+ options[:MaxAge] ||= 86400 * 7
348
+ options[:Worker] ||= ""
349
+
350
+ @batch_size = options[:BatchSize]
351
+ @delay = options[:Delay]
352
+ @once = options[:Once]
353
+ @verbose = options[:Verbose]
354
+ @max_age = options[:MaxAge]
355
+ @worker = options[:Worker]
356
+
357
+ @failed_auth_count = 0
358
+ end
359
+
360
+ ##
361
+ # Removes emails that have lived in the queue for too long. If max_age is
362
+ # set to 0, no emails will be removed.
363
+
364
+ def cleanup
365
+ return if @max_age == 0
366
+ timeout = Time.now - @max_age
367
+ conditions = ['last_send_attempt > 0 and created_on < ?', timeout]
368
+ mail = ActionMailer::Base.email_class.destroy_all conditions
369
+
370
+ log "expired #{mail.length} emails from the queue"
371
+ end
372
+
373
+ ##
374
+ # Delivers +emails+ to ActionMailer's SMTP server and destroys them.
375
+
376
+ def deliver(emails)
377
+ settings = [
378
+ smtp_settings[:domain],
379
+ (smtp_settings[:user] || smtp_settings[:user_name]),
380
+ smtp_settings[:password],
381
+ smtp_settings[:authentication]
382
+ ]
383
+
384
+ smtp = Net::SMTP.new(smtp_settings[:address], smtp_settings[:port])
385
+ if smtp.respond_to?(:enable_starttls_auto)
386
+ smtp.enable_starttls_auto unless smtp_settings[:tls] == false
387
+ else
388
+ settings << smtp_settings[:tls]
389
+ end
390
+
391
+ smtp.start(*settings) do |session|
392
+ @failed_auth_count = 0
393
+ until emails.empty? do
394
+ email = emails.shift
395
+ begin
396
+ res = session.send_message email.mail, email.from, email.to
397
+ email.destroy
398
+ log "sent email %011d from %s to %s: %p" %
399
+ [email.id, email.from, email.to, res]
400
+ rescue Net::SMTPFatalError => e
401
+ log "5xx error sending email %d, removing from queue: %p(%s):\n\t%s" %
402
+ [email.id, e.message, e.class, e.backtrace.join("\n\t")]
403
+ email.destroy
404
+ session.reset
405
+ rescue Net::SMTPServerBusy => e
406
+ log "server too busy, stopping delivery cycle"
407
+ return
408
+ rescue Net::SMTPUnknownError, Net::SMTPSyntaxError, TimeoutError, Timeout::Error => e
409
+ email.last_send_attempt = Time.now.to_i
410
+ email.save rescue nil
411
+ log "error sending email %d: %p(%s):\n\t%s" %
412
+ [email.id, e.message, e.class, e.backtrace.join("\n\t")]
413
+ session.reset
414
+ end
415
+ end
416
+ end
417
+ rescue Net::SMTPAuthenticationError => e
418
+ @failed_auth_count += 1
419
+ if @failed_auth_count >= MAX_AUTH_FAILURES then
420
+ log "authentication error, giving up: #{e.message}"
421
+ raise e
422
+ else
423
+ log "authentication error, retrying: #{e.message}"
424
+ end
425
+ sleep delay
426
+ rescue Net::SMTPServerBusy, SystemCallError, OpenSSL::SSL::SSLError
427
+ # ignore SMTPServerBusy/EPIPE/ECONNRESET from Net::SMTP.start's ensure
428
+ end
429
+
430
+ ##
431
+ # Prepares ar_sendmail for exiting
432
+
433
+ def do_exit
434
+ log "caught signal, shutting down"
435
+ self.class.remove_pid_file
436
+ exit 130
437
+ end
438
+
439
+ ##
440
+ # Returns emails in email_class that haven't had a delivery attempt in the
441
+ # last 300 seconds.
442
+
443
+ def find_emails
444
+ options = { :conditions => ['last_send_attempt < ? AND worker=?', Time.now.to_i - 300, worker.to_s] }
445
+ options[:limit] = batch_size unless batch_size.nil?
446
+ mail = ActionMailer::Base.email_class.find :all, options
447
+
448
+ log "found #{mail.length} emails to send"
449
+ mail
450
+ end
451
+
452
+ ##
453
+ # Installs signal handlers to gracefully exit.
454
+
455
+ def install_signal_handlers
456
+ trap 'TERM' do do_exit end
457
+ trap 'INT' do do_exit end
458
+ end
459
+
460
+ ##
461
+ # Logs +message+ if verbose
462
+
463
+ def log(message)
464
+ $stderr.puts message if @verbose
465
+ ActionMailer::Base.logger.info "ar_sendmail: #{message}"
466
+ end
467
+
468
+ ##
469
+ # Scans for emails and delivers them every delay seconds. Only returns if
470
+ # once is true.
471
+
472
+ def run
473
+ install_signal_handlers
474
+
475
+ loop do
476
+ begin
477
+ cleanup
478
+ emails = find_emails
479
+ deliver(emails) unless emails.empty?
480
+ rescue ActiveRecord::Transactions::TransactionError
481
+ end
482
+ break if @once
483
+ sleep @delay
484
+ end
485
+ end
486
+
487
+ ##
488
+ # Proxy to ActionMailer::Base::smtp_settings. See
489
+ # http://api.rubyonrails.org/classes/ActionMailer/Base.html
490
+ # for instructions on how to configure ActionMailer's SMTP server.
491
+ #
492
+ # Falls back to ::server_settings if ::smtp_settings doesn't exist for
493
+ # backwards compatibility.
494
+
495
+ def smtp_settings
496
+ ActionMailer::Base.smtp_settings rescue ActionMailer::Base.server_settings
497
+ end
498
+
499
+ end