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:
  # <tt>:BatchSize</tt>:: Maximum number of emails to send per delay
  # <tt>:Delay</tt>:: Delay between deliver attempts
  # <tt>:TableName</tt>:: Table name that stores the emails
  # <tt>:Once</tt>:: Only attempt to deliver emails once when run is called
  # <tt>:Verbose</tt>:: 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