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.to, email.from 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