axtro-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,480 @@
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 -1
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 -2
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, Net::SMTPUnknownError, Net::SMTPSyntaxError, TimeoutError => e
389
+ email.last_send_attempt = Time.now.to_i
390
+ email.save rescue nil
391
+ log "error sending email %d: %p(%s):\n\t%s" %
392
+ [email.id, e.message, e.class, e.backtrace.join("\n\t")]
393
+ session.reset
394
+ end
395
+ end
396
+ end
397
+ rescue Net::SMTPAuthenticationError => e
398
+ @failed_auth_count += 1
399
+ if @failed_auth_count >= MAX_AUTH_FAILURES then
400
+ log "authentication error, giving up: #{e.message}"
401
+ raise e
402
+ else
403
+ log "authentication error, retrying: #{e.message}"
404
+ end
405
+ sleep delay
406
+ rescue Net::SMTPServerBusy, SystemCallError, OpenSSL::SSL::SSLError
407
+ # ignore SMTPServerBusy/EPIPE/ECONNRESET from Net::SMTP.start's ensure
408
+ end
409
+
410
+ ##
411
+ # Prepares ar_sendmail for exiting
412
+
413
+ def do_exit
414
+ log "caught signal, shutting down"
415
+ self.class.remove_pid_file
416
+ exit 130
417
+ end
418
+
419
+ ##
420
+ # Returns emails in email_class that haven't had a delivery attempt in the
421
+ # last 300 seconds.
422
+
423
+ def find_emails
424
+ options = { :conditions => ['last_send_attempt < ?', Time.now.to_i - 300] }
425
+ options[:limit] = batch_size unless batch_size.nil?
426
+ mail = ActionMailer::Base.email_class.find :all, options
427
+
428
+ log "found #{mail.length} emails to send"
429
+ mail
430
+ end
431
+
432
+ ##
433
+ # Installs signal handlers to gracefully exit.
434
+
435
+ def install_signal_handlers
436
+ trap 'TERM' do do_exit end
437
+ trap 'INT' do do_exit end
438
+ end
439
+
440
+ ##
441
+ # Logs +message+ if verbose
442
+
443
+ def log(message)
444
+ $stderr.puts message if @verbose
445
+ ActionMailer::Base.logger.info "ar_sendmail: #{message}"
446
+ end
447
+
448
+ ##
449
+ # Scans for emails and delivers them every delay seconds. Only returns if
450
+ # once is true.
451
+
452
+ def run
453
+ install_signal_handlers
454
+
455
+ loop do
456
+ now = Time.now
457
+ begin
458
+ cleanup
459
+ emails = find_emails
460
+ deliver(emails) unless emails.empty?
461
+ rescue ActiveRecord::Transactions::TransactionError
462
+ end
463
+ break if @once
464
+ sleep @delay if now + @delay > Time.now
465
+ end
466
+ end
467
+
468
+ ##
469
+ # Proxy to ActionMailer::Base::smtp_settings. See
470
+ # http://api.rubyonrails.org/classes/ActionMailer/Base.html
471
+ # for instructions on how to configure ActionMailer's SMTP server.
472
+ #
473
+ # Falls back to ::server_settings if ::smtp_settings doesn't exist for
474
+ # backwards compatibility.
475
+
476
+ def smtp_settings
477
+ ActionMailer::Base.smtp_settings rescue ActionMailer::Base.server_settings
478
+ end
479
+
480
+ end
data/lib/smtp_tls.rb ADDED
@@ -0,0 +1,105 @@
1
+ # Original code believed public domain from ruby-talk or ruby-core email.
2
+ # Modifications by Kyle Maxwell <kyle@kylemaxwell.com> used under MIT license.
3
+
4
+ require "openssl"
5
+ require "net/smtp"
6
+
7
+ # :stopdoc:
8
+
9
+ class Net::SMTP
10
+
11
+ class << self
12
+ send :remove_method, :start
13
+ end
14
+
15
+ def self.start( address, port = nil,
16
+ helo = 'localhost.localdomain',
17
+ user = nil, secret = nil, authtype = nil, use_tls = false,
18
+ &block) # :yield: smtp
19
+ new(address, port).start(helo, user, secret, authtype, use_tls, &block)
20
+ end
21
+
22
+ alias tls_old_start start
23
+
24
+ def start( helo = 'localhost.localdomain',
25
+ user = nil, secret = nil, authtype = nil, use_tls = false ) # :yield: smtp
26
+ start_method = use_tls ? :do_tls_start : :do_start
27
+ if block_given?
28
+ begin
29
+ send start_method, helo, user, secret, authtype
30
+ return yield(self)
31
+ ensure
32
+ do_finish
33
+ end
34
+ else
35
+ send start_method, helo, user, secret, authtype
36
+ return self
37
+ end
38
+ end
39
+
40
+ private
41
+
42
+ def do_tls_start(helodomain, user, secret, authtype)
43
+ raise IOError, 'SMTP session already started' if @started
44
+ check_auth_args user, secret, authtype if user or secret
45
+
46
+ sock = timeout(@open_timeout) { TCPSocket.open(@address, @port) }
47
+ @socket = Net::InternetMessageIO.new(sock)
48
+ @socket.read_timeout = 60 #@read_timeout
49
+ @socket.debug_output = STDERR #@debug_output
50
+
51
+ check_response(critical { recv_response() })
52
+ do_helo(helodomain)
53
+
54
+ raise 'openssl library not installed' unless defined?(OpenSSL)
55
+ starttls
56
+ ssl = OpenSSL::SSL::SSLSocket.new(sock)
57
+ ssl.sync_close = true
58
+ ssl.connect
59
+ @socket = Net::InternetMessageIO.new(ssl)
60
+ @socket.read_timeout = 60 #@read_timeout
61
+ @socket.debug_output = STDERR #@debug_output
62
+ do_helo(helodomain)
63
+
64
+ authenticate user, secret, authtype if user
65
+ @started = true
66
+ ensure
67
+ unless @started
68
+ # authentication failed, cancel connection.
69
+ @socket.close if not @started and @socket and not @socket.closed?
70
+ @socket = nil
71
+ end
72
+ end
73
+
74
+ def do_helo(helodomain)
75
+ begin
76
+ if @esmtp
77
+ ehlo helodomain
78
+ else
79
+ helo helodomain
80
+ end
81
+ rescue Net::ProtocolError
82
+ if @esmtp
83
+ @esmtp = false
84
+ @error_occured = false
85
+ retry
86
+ end
87
+ raise
88
+ end
89
+ end
90
+
91
+ def starttls
92
+ getok('STARTTLS')
93
+ end
94
+
95
+ alias tls_old_quit quit
96
+
97
+ def quit
98
+ begin
99
+ getok('QUIT')
100
+ rescue EOFError
101
+ end
102
+ end
103
+
104
+ end unless Net::SMTP.private_method_defined? :do_tls_start or
105
+ Net::SMTP.method_defined? :tls?
@@ -0,0 +1,30 @@
1
+ #!/bin/sh
2
+ # PROVIDE: ar_sendmail
3
+ # REQUIRE: DAEMON
4
+ # BEFORE: LOGIN
5
+ # KEYWORD: FreeBSD shutdown
6
+
7
+ #
8
+ # Add the following lines to /etc/rc.conf to enable ar_sendmail:
9
+ #
10
+ #ar_sendmail_enable="YES"
11
+
12
+ . /etc/rc.subr
13
+
14
+ name="ar_sendmail"
15
+ rcvar=`set_rcvar`
16
+
17
+ command="/usr/local/bin/ar_sendmail"
18
+ command_interpreter="/usr/local/bin/ruby18"
19
+
20
+ # set defaults
21
+
22
+ ar_sendmail_rails_env=${ar_sendmail_rails_env:-"production"}
23
+ ar_sendmail_chdir=${ar_sendmail_chdir:-"/"}
24
+ ar_sendmail_enable=${ar_sendmail_enable:-"NO"}
25
+ ar_sendmail_flags=${ar_sendmail_flags:-"-d"}
26
+
27
+ load_rc_config $name
28
+ export RAILS_ENV=$ar_sendmail_rails_env
29
+ run_rc_command "$1"
30
+
@@ -0,0 +1,78 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # ar_sendmail Startup script for ar_mailer by Adam Meehan
4
+ #
5
+ # chkconfig: - 85 15
6
+ # description: ar_sendmail manages sending emails for Rails apps.
7
+ #
8
+ require 'yaml'
9
+
10
+ # Config file app mailers
11
+ config_file = '/etc/ar_sendmail.conf'
12
+
13
+ begin
14
+ config = YAML::load(IO.read(config_file)) || {}
15
+ if config.empty? || (config.has_key?('defaults') && config.size == 1)
16
+ puts "No mailers defined. Exiting."
17
+ exit
18
+ end
19
+ rescue Errno::ENOENT
20
+ puts "Config file not found at '#{config_file}'!"
21
+ exit
22
+ end
23
+
24
+ default_options = {'pidfile' => './log/ar_sendmail.pid'}.merge(config.delete('defaults') || {})
25
+
26
+ command, app_name = *ARGV
27
+
28
+ def start(app, options)
29
+ switches = ""
30
+ options.each {|k, v| switches << " --#{k} #{v}"}
31
+ STDOUT.write "Starting mailer for #{app} in #{options['environment']} mode ... "
32
+ status = system("ar_sendmail -d #{switches}") ? "started" : "failed"
33
+ puts status
34
+ end
35
+
36
+ def stop(app, options)
37
+ pid_file = File.expand_path(options['pidfile'], options['chdir'])
38
+ if File.exist? pid_file
39
+ begin
40
+ pid = open(pid_file).read.to_i
41
+ STDOUT.write "Stopping mailer for #{app}... "
42
+ Process.kill('TERM', pid)
43
+ puts "stopped"
44
+ rescue Errno::ESRCH
45
+ puts "Mailer process does not exist. Is not running."
46
+ end
47
+ else
48
+ puts "Skipping mailer for #{app}, no pid file."
49
+ end
50
+ end
51
+
52
+ def restart(app, options)
53
+ puts "Restarting mailer for #{app} ..."
54
+ stop app, options
55
+ start app, options
56
+ end
57
+
58
+ def command_error(msg)
59
+ puts msg
60
+ exit
61
+ end
62
+
63
+ if ['start', 'stop', 'restart'].include?(command)
64
+ apps = config
65
+ if app_name
66
+ command_error "No such app defined in ar_sendmail config" unless config.include?(app_name)
67
+ app_options = config[app_name]
68
+ command_error "Must specify chdir for app in ar_sendmail config" if app_options['chdir'].nil?
69
+ apps = {app_name => app_options}
70
+ end
71
+
72
+ apps.each do |app, options|
73
+ options = default_options.merge(options)
74
+ send(command, app, options)
75
+ end
76
+ else
77
+ command_error "Usage: ar_sendmail {start|stop|restart} [optional app_name]"
78
+ end