brunoaalves-ar_mailer 2.0.0

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