class ActionMailer::ARSendmail

ActionMailer::ARSendmail delivers email from the email table to the SMTP server configured in your application's config/environment.rb. ar_sendmail does not work with sendmail delivery.

ar_mailer can deliver to SMTP with TLS using the smtp_tls gem Set the :tls option in ActionMailer::Base’s #smtp_settings to true to enable TLS.

See ar_sendmail -h for the full list of supported options.

The interesting options are:

Constants

MAX_AUTH_FAILURES

Maximum number of times authentication will be consecutively retried

VERSION

The version of ActionMailer::ARSendmail you are running.

Attributes

batch_size[RW]

Email delivery attempts per run

delay[RW]

Seconds to delay between runs

email_class[R]

ActiveRecord class that holds emails

failed_auth_count[RW]

Times authentication has failed

max_age[RW]

Maximum age of emails in seconds before they are removed from the queue.

once[R]

True if only one delivery attempt will be made per call to run

verbose[RW]

Be verbose

Public Class Methods

check_pid(pid_file) click to toggle source

Checks and writes pid_file, aborting if it already exists or this process loses the pid-writing race.

# File lib/action_mailer/ar_sendmail.rb, line 103
def self.check_pid(pid_file)
  if File.exist? pid_file then
    abort "pid file exists at #{pid_file}, exiting"
  else
    open pid_file, 'w', 0644 do |io|
      io.write $$
    end

    written_pid = File.read pid_file

    if written_pid.to_i != $$ then
      abort "pid #{written_pid} from #{pid_file} doesn't match $$ #{$$}, exiting"
    end
  end
end
create_migration(table_name) click to toggle source

Creates a new migration using table_name and prints it on stdout.

# File lib/action_mailer/ar_sendmail.rb, line 122
  def self.create_migration(table_name)
    require 'active_support'
    puts "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
      t.column :created_on, :datetime
    end
  end

  def self.down
    drop_table :#{table_name.tableize}
  end
end
"
  end
create_model(table_name) click to toggle source

Creates a new model using table_name and prints it on stdout.

# File lib/action_mailer/ar_sendmail.rb, line 146
  def self.create_model(table_name)
    require 'active_support'
    puts "class #{table_name.classify} < ActiveRecord::Base
end
"
  end
mailq(table_name) click to toggle source

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 api.rubyonrails.org/classes/ActiveRecord/Timestamp.html to learn how to enable ActiveRecord::Timestamp.

# File lib/action_mailer/ar_sendmail.rb, line 161
def self.mailq(table_name)
  klass = table_name.split('::').inject(Object) { |k,n| k.const_get n }
  emails = klass.find :all

  if emails.empty? then
    puts "Mail queue is empty"
    return
  end

  total_size = 0

  puts "-Queue ID- --Size-- ----Arrival Time---- -Sender/Recipient-------"
  emails.each do |email|
    size = email.mail.length
    total_size += size

    create_timestamp = email.created_on rescue
                       email.created_at 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

    puts "%10d %8d %s  %s" % [email.id, size, created, email.from]
    if email.last_send_attempt > 0 then
      last_send_attempt = Time.at email.last_send_attempt
      puts "Last send attempt: #{last_send_attempt.asctime}"
    end
    puts "                                         #{email.to}"
    puts
  end

  puts "-- #{total_size/1024} Kbytes in #{emails.length} Requests."
end
new(options = {}) click to toggle source

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.

# File lib/action_mailer/ar_sendmail.rb, line 430
def initialize(options = {})
  options[:Delay] ||= 60
  options[:TableName] ||= 'Email'
  options[:MaxAge] ||= 86400 * 7

  @batch_size = options[:BatchSize]
  @delay = options[:Delay]
  @email_class = Object.path2class options[:TableName]
  @once = options[:Once]
  @verbose = options[:Verbose]
  @max_age = options[:MaxAge]

  @failed_auth_count = 0
end
process_args(args) click to toggle source

Processes command line options in args

# File lib/action_mailer/ar_sendmail.rb, line 203
  def self.process_args(args)
    name = File.basename $0

    options = {}
    options[:Chdir] = '.'
    options[:Daemon] = false
    options[:Delay] = 60
    options[:MaxAge] = 86400 * 7
    options[:Once] = false
    options[:RailsEnv] = ENV['RAILS_ENV']
    options[:TableName] = 'Email'

    op = OptionParser.new do |opts|
      opts.program_name = name
      opts.version = VERSION

      opts.banner = "Usage: #{name} [options]

#{name} scans the email table for new messages and sends them to the
website's configured SMTP host.

#{name} must be run from a Rails application's root or have it specified
with --chdir.

If #{name} is started with --pid-file, it will fail to start if the PID
file already exists or the contents don't match it's PID.
"

      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(      "--max-age MAX_AGE",
              "Maxmimum age for an email. After this",
              "it will be removed from the queue.",
              "Set to 0 to disable queue cleanup.",
              "Default: #{options[:MaxAge]} seconds", Integer) do |max_age|
        options[:MaxAge] = max_age
      end

      opts.on("-o", "--once",
              "Only check for new mail and deliver once",
              "Default: #{options[:Once]}") do |once|
        options[:Once] = once
      end

      opts.on("-p", "--pid-file [PATH]",
              "File to store the pid in.",
              "Defaults to /var/run/ar_sendmail.pid",
              "when no path is given") do |pid_file|
        pid_file ||= '/var/run/ar_sendmail/ar_sendmail.pid'

        pid_dir = File.dirname pid_file
        raise OptionParser::InvalidArgument,
              "directory #{pid_dir} does not exist" unless
          File.directory? pid_dir

        options[:PidFile] = pid_file
      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("-c", "--chdir PATH",
              "Use PATH for the application path",
              "Default: #{options[:Chdir]}") do |path|
        usage opts, "#{path} is not a directory" unless File.directory? path
        usage opts, "#{path} is not readable" unless File.readable? path
        options[:Chdir] = path
      end

      opts.on("-e", "--environment RAILS_ENV",
              "Set the RAILS_ENV constant",
              "Default: #{options[:RailsEnv]}") do |env|
        options[:RailsEnv] = env
      end

      opts.on("-t", "--table-name TABLE_NAME",
              "Name of table holding emails",
              "Used for both sendmail and",
              "migration creation",
              "Default: #{options[:TableName]}") do |table_name|
        options[:TableName] = table_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
        usage opts
      end

      opts.separator ''
    end

    op.parse! args

    return options if options.include? :Migrate or options.include? :Model

    ENV['RAILS_ENV'] = options[:RailsEnv]

    Dir.chdir options[:Chdir] do
      begin
        require 'config/environment'
      rescue LoadError
        usage op, "#{name} must be run from a Rails application's root to deliver email.

#{Dir.pwd} does not appear to be a Rails application root.
"
      end
    end

    return options
  end
run(args = ARGV) click to toggle source

Processes args and runs as appropriate

# File lib/action_mailer/ar_sendmail.rb, line 365
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 options[:TableName]
    exit
  end

  if options[:Daemon] then
    require 'webrick/server'
    ActiveRecord::Base.clear_all_connections!
    WEBrick::Daemon.start
  end

  sendmail = new options

  check_pid options[:PidFile] if options.key? :PidFile

  begin
    sendmail.run
  ensure
    File.unlink options[:PidFile] if
      options.key? :PidFile and $PID == File.read(options[:PidFile]).to_i
  end

rescue SystemExit
  raise
rescue SignalException
  exit
rescue Exception => e
  $stderr.puts "Unhandled exception #{e.message}(#{e.class}):"
  $stderr.puts "\t#{e.backtrace.join "\n\t"}"
  exit 1
end
usage(opts, message = nil) click to toggle source

Prints a usage message to $stderr using opts and exits

# File lib/action_mailer/ar_sendmail.rb, line 409
def self.usage(opts, message = nil)
  $stderr.puts opts

  if message then
    $stderr.puts
    $stderr.puts message
  end

  exit 1
end

Public Instance Methods

cleanup() click to toggle source

Removes emails that have lived in the queue for too long. If #max_age is set to 0, no emails will be removed.

# File lib/action_mailer/ar_sendmail.rb, line 449
def cleanup
  return if @max_age == 0
  timeout = Time.now - @max_age
  conditions = ['last_send_attempt > 0 and created_on < ?', timeout]
  mail = @email_class.destroy_all conditions

  log "expired #{mail.length} emails from the queue"
end
deliver(emails) click to toggle source

Delivers emails to ActionMailer’s SMTP server and destroys them.

# File lib/action_mailer/ar_sendmail.rb, line 461
def deliver(emails)
  user = smtp_settings[:user] || smtp_settings[:user_name]
  server = Net::SMTP.new smtp_settings[:address], smtp_settings[:port]
  server.start smtp_settings[:domain], user, smtp_settings[:password],
               smtp_settings[:authentication] do |smtp|
    if smtp_settings[:tls] then
      raise 'gem install smtp_tls for 1.8.6' unless
        server.respond_to? :starttls
      smtp.enable_starttls
    end
    @failed_auth_count = 0
    until emails.empty? do
      email = emails.shift
      begin
        res = smtp.send_message email.mail, email.from, email.to
        email.destroy
        log "sent email %011d from %s to %s: %p" %
              [email.id, email.from, email.to, res]
      rescue Net::SMTPFatalError => e
        log "5xx error sending email %d, removing from queue: %p(%s):\n\t%s" %
              [email.id, e.message, e.class, e.backtrace.join("\n\t")]
        email.destroy
        smtp.reset
      rescue Net::SMTPServerBusy => e
        log "server too busy, sleeping #{@delay} seconds"
        sleep delay
        return
      rescue Net::SMTPUnknownError, Net::SMTPSyntaxError, TimeoutError => e
        email.last_send_attempt = Time.now.to_i
        email.save rescue nil
        log "error sending email %d: %p(%s):\n\t%s" %
              [email.id, e.message, e.class, e.backtrace.join("\n\t")]

        raise e if TimeoutError === e

        smtp.reset
      end
    end
  end
rescue Net::SMTPAuthenticationError => e
  @failed_auth_count += 1
  if @failed_auth_count >= MAX_AUTH_FAILURES then
    log "authentication error, giving up: #{e.message}"
    raise e
  else
    log "authentication error, retrying: #{e.message}"
  end
  sleep delay
rescue Net::SMTPServerBusy, SystemCallError, OpenSSL::SSL::SSLError
  # ignore SMTPServerBusy/EPIPE/ECONNRESET from Net::SMTP.start's ensure
rescue TimeoutError
  # terminate our connection since Net::SMTP may be in a bogus state.
  sleep delay
end
do_exit() click to toggle source

Prepares ar_sendmail for exiting

# File lib/action_mailer/ar_sendmail.rb, line 519
def do_exit
  log "caught signal, shutting down"
  exit
end
find_emails() click to toggle source

Returns emails in #email_class that haven’t had a delivery attempt in the last 300 seconds.

# File lib/action_mailer/ar_sendmail.rb, line 528
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
install_signal_handlers() click to toggle source

Installs signal handlers to gracefully exit.

# File lib/action_mailer/ar_sendmail.rb, line 540
def install_signal_handlers
  trap 'TERM' do do_exit end
  trap 'INT'  do do_exit end
end
log(message) click to toggle source

Logs message if verbose

# File lib/action_mailer/ar_sendmail.rb, line 548
def log(message)
  $stderr.puts message if @verbose
  ActionMailer::Base.logger.info "ar_sendmail: #{message}"
end
run() click to toggle source

Scans for emails and delivers them every delay seconds. Only returns if once is true.

# File lib/action_mailer/ar_sendmail.rb, line 557
def run
  install_signal_handlers

  loop do
    now = Time.now
    begin
      cleanup
      deliver find_emails
    rescue ActiveRecord::Transactions::TransactionError
    end
    break if @once
    sleep @delay if now + @delay > Time.now
  end
end
smtp_settings() click to toggle source

Proxy to ActionMailer::Base::smtp_settings. See api.rubyonrails.org/classes/ActionMailer/Base.html for instructions on how to configure ActionMailer’s SMTP server.

Falls back to ::server_settings if ::smtp_settings doesn’t exist for backwards compatibility.

# File lib/action_mailer/ar_sendmail.rb, line 580
def smtp_settings
  ActionMailer::Base.smtp_settings rescue ActionMailer::Base.server_settings
end