require 'optparse'
require 'rubygems'
require 'action_mailer'
class Object # :nodoc:
unless respond_to? :path2class then
def self.path2class(path)
path.split(/::/).inject self do |k,n| k.const_get n end
end
end
end
##
# ActionMailer::ARSendmail delivers email from the email table to the
# configured SMTP server.
#
# See ar_sendmail -h for the full list of supported options.
#
# The interesting options are:
# * --daemon
# * --mailq
# * --create-migration
# * --create-model
# * --table-name
class ActionMailer::ARSendmail
##
# Email delivery attempts per run
attr_accessor :batch_size
##
# Seconds to delay between runs
attr_accessor :delay
##
# Be verbose
attr_accessor :verbose
##
# ActiveRecord class that holds emails
attr_reader :email_class
##
# True if only one delivery attempt will be made per call to run
attr_reader :once
##
# Creates a new migration using +table_name+ and prints it on $stdout.
def self.create_migration(table_name)
migration = <<-EOF
class Add#{table_name.classify} < ActiveRecord::Migration
def self.up
create_table :#{table_name.tableize} do |t|
t.column :from, :string
t.column :to, :string
t.column :last_send_attempt, :integer, :default => 0
t.column :mail, :text
end
end
def self.down
drop_table :email
end
end
EOF
$stdout.puts migration
end
##
# Creates a new model using +table_name+ and prints it on $stdout.
def self.create_model(table_name)
model = <<-EOF
class #{table_name.classify} < ActiveRecord::Base
end
EOF
$stdout.puts model
end
##
# Prints a list of unsent emails and the last delivery attempt, if any.
#
# If ActiveRecord::Timestamp is not being used the arrival time will not be
# known. See http://api.rubyonrails.org/classes/ActiveRecord/Timestamp.html
# to learn how to enable ActiveRecord::Timestamp.
def self.mailq
emails = Email.find :all
if emails.empty? then
$stdout.puts "Mail queue is empty"
return
end
total_size = 0
$stdout.puts "-Queue ID- --Size-- ----Arrival Time---- -Sender/Recipient-------"
emails.each do |email|
size = email.mail.length
total_size += size
create_timestamp = email.created_at rescue
email.created_on rescue
Time.at(email.created_date) rescue # for Robot Co-op
nil
created = if create_timestamp.nil? then
' Unknown'
else
create_timestamp.strftime '%a %b %d %H:%M:%S'
end
$stdout.puts "%10d %8d %s %s" % [email.id, size, created, email.from]
if email.last_send_attempt then
$stdout.puts "Last send attempt: #{email.last_send_attempt}"
end
$stdout.puts " #{email.to}"
$stdout.puts
end
$stdout.puts "-- #{total_size/1024} Kbytes in #{emails.length} Requests."
end
##
# Processes command line options in +args+
def self.process_args(args)
name = File.basename $0
options = {}
options[:Daemon] = false
options[:Delay] = 60
options[:Once] = false
options[:TableName] = 'Email'
opts = OptionParser.new do |opts|
opts.banner = "Usage: #{name} [options]"
opts.separator ''
opts.separator "#{name} scans the email table for new messages and sends them to the"
opts.separator "website's configured SMTP host."
opts.separator ''
opts.separator "#{name} must be run from the application's root."
opts.separator ''
opts.separator 'Sendmail options:'
opts.on("-b", "--batch-size BATCH_SIZE",
"Maximum number of emails to send per delay",
"Default: Deliver all available emails", Integer) do |batch_size|
options[:BatchSize] = batch_size
end
opts.on( "--delay DELAY",
"Delay between checks for new mail",
"in the database",
"Default: #{options[:Delay]}", Integer) do |delay|
options[:Delay] = delay
end
opts.on("-o", "--once",
"Only check for new mail and deliver once",
"Default: #{options[:Once]}") do |once|
options[:Once] = once
end
opts.on("-d", "--daemonize",
"Run as a daemon process",
"Default: #{options[:Daemon]}") do |daemon|
options[:Daemon] = true
end
opts.on( "--mailq",
"Display a list of emails waiting to be sent") do |mailq|
options[:MailQ] = true
end
opts.separator ''
opts.separator 'Setup Options:'
opts.on( "--create-migration",
"Prints a migration to add an Email table",
"to $stdout") do |create|
options[:Migrate] = true
end
opts.on( "--create-model",
"Prints a model for an Email ActiveRecord",
"object to $stdout") do |create|
options[:Model] = true
end
opts.separator ''
opts.separator 'Generic Options:'
opts.on("-t", "--table-name TABLE_NAME",
"Name of table holding emails",
"Used for both sendmail and",
"migration creation",
"Default: #{options[:TableName]}") do |name|
options[:TableName] = name
end
opts.on("-v", "--[no-]verbose",
"Be verbose",
"Default: #{options[:Verbose]}") do |verbose|
options[:Verbose] = verbose
end
opts.on("-h", "--help",
"You're looking at it") do
$stderr.puts opts
exit 1
end
opts.separator ''
end
opts.parse! args
unless options.include? :Migrate or options.include? :Model or
not $".grep(/config\/environment.rb/).empty? then
$stderr.puts "#{name} must be run from a Rails application's root to deliver email."
$stderr.puts
$stderr.puts opts
exit 1
end
return options
end
##
# Processes +args+ and runs as appropriate
def self.run(args = ARGV)
options = process_args args
if options.include? :Migrate then
create_migration options[:TableName]
exit
elsif options.include? :Model then
create_model options[:TableName]
exit
elsif options.include? :MailQ then
mailq
exit
end
if options[:Daemon] then
require 'webrick/server'
WEBrick::Daemon.start
end
new(options).run
end
##
# Creates a new ARSendmail.
#
# Valid options are:
# :BatchSize:: Maximum number of emails to send per delay
# :Delay:: Delay between deliver attempts
# :TableName:: Table name that stores the emails
# :Once:: Only attempt to deliver emails once when run is called
# :Verbose:: Be verbose.
def initialize(options = {})
options[:Delay] ||= 60
options[:TableName] ||= 'Email'
@batch_size = options[:BatchSize]
@delay = options[:Delay]
@email_class = Object.path2class options[:TableName]
@once = options[:Once]
@verbose = options[:Verbose]
end
##
# Delivers +emails+ to ActionMailer's SMTP server and destroys them.
def deliver(emails)
Net::SMTP.start server_settings[:address], server_settings[:port],
server_settings[:domain], server_settings[:user],
server_settings[:password],
server_settings[:authentication] do |smtp|
emails.each do |email|
begin
res = smtp.send_message email.mail, email.from, email.to
email.destroy
log "sent email from %s to %s: %p" % [email.from, email.to, res]
rescue Net::SMTPServerBusy, Net::SMTPFatalError,
Net::SMTPUnknownError, TimeoutError
email.last_send_attempt = Time.now.to_i
end
end
end
end
##
# Returns emails in email_class that haven't had a delivery attempt in the
# last 300 seconds.
def find_emails
options = { :conditions => ['last_send_attempt < ?', Time.now.to_i - 300] }
options[:limit] = batch_size unless batch_size.nil?
mail = @email_class.find :all, options
log "found #{mail.length} emails to send"
mail
end
##
# Logs +message+ if verbose
def log(message)
$stderr.puts message if @verbose
ActionMailer::Base.logger.info "ar_sendmail: #{message}"
end
##
# Scans for emails and delivers them every delay seconds. Only returns if
# once is true.
def run
loop do
deliver find_emails
break if @once
sleep @delay
end
end
##
# Proxy to ActionMailer::Base#server_settings. See
# http://api.rubyonrails.org/classes/ActionMailer/Base.html
# for instructions on how to configure ActionMailer's SMTP server.
def server_settings
ActionMailer::Base.server_settings
end
end