leftbrained-ar_mailer 2.1.9

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