laserlemon-ar_mailer 2.1.5

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