Skip to content
Browse files

Branching ar_mailer to version 1.3.1

From p4 revision #3341

git-svn-id: http://seattlerb.rubyforge.org/svn/ar_mailer/1.3.1@381 d2e05cf2-00e0-46e5-a3de-bbee4d6b9404
  • Loading branch information...
0 parents commit 3b28380adb09bd0c19e45322bc6a696187e3fbf6 zenspider committed
Showing with 1,783 additions and 0 deletions.
  1. +52 −0 History.txt
  2. +28 −0 LICENSE.txt
  3. +13 −0 Manifest.txt
  4. +39 −0 README.txt
  5. +14 −0 Rakefile
  6. +6 −0 bin/ar_sendmail
  7. +98 −0 lib/action_mailer/ar_mailer.rb
  8. +521 −0 lib/action_mailer/ar_sendmail.rb
  9. +105 −0 lib/smtp_tls.rb
  10. +30 −0 share/ar_sendmail
  11. +185 −0 test/action_mailer.rb
  12. +50 −0 test/test_armailer.rb
  13. +642 −0 test/test_arsendmail.rb
52 History.txt
@@ -0,0 +1,52 @@
+= 1.3.1
+
+* Fix bug #12530, gmail causes SSL errors. Submitted by Kyle Maxwell
+ and Alex Ostleitner.
+* Try ActionMailer::Base::server_settings then ::smtp_settings. Fixes
+ bug #12516. Submitted by Alex Ostleitner.
+
+= 1.3.0
+
+* New Features
+ * Added automatic mail queue cleanup.
+ * MAY CAUSE LOSS OF DATA. If you haven't run ar_sendmail within
+ the expiry time, set it to 0.
+* Bugs fixed
+ * Authentication errors are now handled by retrying once.
+
+= 1.2.0
+
+* Bugs fixed
+ * Handle SMTPServerBusy by backing off @delay seconds then re-queueing
+ * Allow email delivery class to be set in ARMailer.
+ * ar_sendmail --mailq works with --table-name now.
+* Miscellaneous Updates
+ * Added documentation to require 'action_mailer/ar_mailer' in
+ instructions.
+ * Moved to ZSS p4 repository
+ * Supports TLS now. Requested by Dave Thomas. smtp_tls.rb from Kyle
+ Maxwell & etc.
+
+= 1.1.0
+
+* Features
+ * Added --chdir to set rails directory
+ * Added --environment to set RAILS_ENV
+ * Exits cleanly on TERM or INT signals
+ * Added FreeBSD rc.d script
+ * Exceptions during SMTP sending are now logged
+ * No longer waits if sending email took too long
+* Bugs fixed
+ * Fixed last send attempt in --mailq
+ * Better SMTP error handling
+ * Messages are removed from the queue on 5xx errors
+ * Added Net::SMTP.reset to avoid needing to recreate the connection
+
+= 1.0.1
+
+* Bugs fixed
+ * From and to of email destination were swapped
+
+= 1.0.0
+
+* Birthday
28 LICENSE.txt
@@ -0,0 +1,28 @@
+Original code copyright 2006, 2007, Eric Hodel, The Robot Co-op. All
+rights reserved. Some code under other license, see individual files
+for details.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+
+1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+3. Neither the names of the authors nor the names of their contributors
+ may be used to endorse or promote products derived from this software
+ without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND ANY EXPRESS
+OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE
+LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
+OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
+EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
13 Manifest.txt
@@ -0,0 +1,13 @@
+History.txt
+LICENSE.txt
+Manifest.txt
+README.txt
+Rakefile
+bin/ar_sendmail
+lib/action_mailer/ar_mailer.rb
+lib/action_mailer/ar_sendmail.rb
+lib/smtp_tls.rb
+share/ar_sendmail
+test/action_mailer.rb
+test/test_armailer.rb
+test/test_arsendmail.rb
39 README.txt
@@ -0,0 +1,39 @@
+= ar_mailer
+
+A two-phase delivery agent for ActionMailer
+
+Rubyforge Project:
+
+http://rubyforge.org/projects/seattlerb
+
+Documentation:
+
+http://seattlerb.org/ar_mailer
+
+Bugs:
+
+http://rubyforge.org/tracker/?func=add&group_id=1513&atid=5921
+
+== About
+
+Even delivering email to the local machine may take too long when you have to
+send hundreds of messages. ar_mailer allows you to store messages into the
+database for later delivery by a separate process, ar_sendmail.
+
+== Installing ar_mailer
+
+Just install the gem:
+
+ $ sudo gem install ar_mailer
+
+See ActionMailer::ARMailer for instructions on converting to ARMailer.
+
+See ar_sendmail -h for options to ar_sendmail.
+
+NOTE: You may need to delete an smtp_tls.rb file if you have one lying
+around. ar_mailer supplies it own.
+
+=== ar_sendmail on FreeBSD or NetBSD
+
+An rc.d script is included in share/ar_sendmail.
+
14 Rakefile
@@ -0,0 +1,14 @@
+require 'hoe'
+
+require './lib/action_mailer/ar_sendmail'
+
+Hoe.new 'ar_mailer', ActionMailer::ARSendmail::VERSION do |s|
+ s.rubyforge_name = 'seattlerb'
+ s.summary = s.paragraphs_of('README.txt', 1).join(' ')
+ s.description = s.paragraphs_of('README.txt', 9).join(' ')
+ s.url = s.paragraphs_of('README.txt', 5).join(' ')
+ s.author = 'Eric Hodel'
+ s.email = 'drbrain@segment7.net'
+ s.changes = s.paragraphs_of('History.txt', 0..1).join("\n\n")
+end
+
6 bin/ar_sendmail
@@ -0,0 +1,6 @@
+#!/usr/local/bin/ruby
+
+require 'action_mailer/ar_sendmail'
+
+ActionMailer::ARSendmail.run
+
98 lib/action_mailer/ar_mailer.rb
@@ -0,0 +1,98 @@
+require 'action_mailer'
+
+##
+# Adds sending email through an ActiveRecord table as a delivery method for
+# ActionMailer.
+#
+# == Converting to ActionMailer::ARMailer
+#
+# Go to your Rails project:
+#
+# $ cd your_rails_project
+#
+# Create a new migration:
+#
+# $ ar_sendmail --create-migration
+#
+# You'll need to redirect this into a file. If you want a different name
+# provide the --table-name option.
+#
+# Create a new model:
+#
+# $ ar_sendmail --create-model
+#
+# You'll need to redirect this into a file. If you want a different name
+# provide the --table-name option.
+#
+# Change your email classes to inherit from ActionMailer::ARMailer instead of
+# ActionMailer::Base:
+#
+# --- app/model/emailer.rb.orig 2006-08-10 13:16:33.000000000 -0700
+# +++ app/model/emailer.rb 2006-08-10 13:16:43.000000000 -0700
+# @@ -1,4 +1,4 @@
+# -class Emailer < ActionMailer::Base
+# +class Emailer < ActionMailer::ARMailer
+#
+# def comment_notification(comment)
+# from comment.author.email
+#
+# You'll need to be sure to set the From address for your emails. Something
+# like:
+#
+# def list_send(recipient)
+# from 'no_reply@example.com'
+# # ...
+#
+# Edit config/environment.rb and require ar_mailer.rb:
+#
+# require 'action_mailer/ar_mailer'
+#
+# Edit config/environments/production.rb and set the delivery agent:
+#
+# $ grep delivery_method config/environments/production.rb
+# ActionMailer::Base.delivery_method = :activerecord
+#
+# Run ar_sendmail:
+#
+# $ ar_sendmail
+#
+# You can also run it from cron with -o, or as a daemon with -d.
+#
+# See <tt>ar_sendmail -h</tt> for full details.
+#
+# == Alternate Mail Storage
+#
+# If you want to set the ActiveRecord model that emails will be stored in,
+# see ActionMailer::ARMailer::email_class=
+
+class ActionMailer::ARMailer < ActionMailer::Base
+
+ @@email_class = Email
+
+ ##
+ # Current email class for deliveries.
+
+ def self.email_class
+ @@email_class
+ end
+
+ ##
+ # Sets the email class for deliveries.
+
+ def self.email_class=(klass)
+ @@email_class = klass
+ end
+
+ ##
+ # Adds +mail+ to the Email table. Only the first From address for +mail+ is
+ # used.
+
+ def perform_delivery_activerecord(mail)
+ mail.destinations.each do |destination|
+ @@email_class.create :mail => mail.encoded, :to => destination,
+ :from => mail.from.first
+ end
+ end
+
+end
+
521 lib/action_mailer/ar_sendmail.rb
@@ -0,0 +1,521 @@
+require 'optparse'
+require 'net/smtp'
+require 'smtp_tls'
+require 'rubygems'
+
+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
+
+##
+# Hack in RSET
+
+module Net # :nodoc:
+class SMTP # :nodoc:
+
+ unless instance_methods.include? 'reset' then
+ ##
+ # Resets the SMTP connection.
+
+ def reset
+ getok 'RSET'
+ end
+ end
+
+end
+end
+
+module ActionMailer; end # :nodoc:
+
+##
+# 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 smtp_tls.rb borrowed from Kyle
+# Maxwell's action_mailer_optional_tls plugin. Simply 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:
+# * --daemon
+# * --mailq
+# * --create-migration
+# * --create-model
+# * --table-name
+
+class ActionMailer::ARSendmail
+
+ ##
+ # The version of ActionMailer::ARSendmail you are running.
+
+ VERSION = '1.3.1'
+
+ ##
+ # Maximum number of times authentication will be consecutively retried
+
+ MAX_AUTH_FAILURES = 2
+
+ ##
+ # Email delivery attempts per run
+
+ attr_accessor :batch_size
+
+ ##
+ # Seconds to delay between runs
+
+ attr_accessor :delay
+
+ ##
+ # Maximum age of emails in seconds before they are removed from the queue.
+
+ attr_accessor :max_age
+
+ ##
+ # 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
+
+ ##
+ # Times authentication has failed
+
+ attr_accessor :failed_auth_count
+
+ ##
+ # Creates a new migration using +table_name+ and prints it on stdout.
+
+ def self.create_migration(table_name)
+ require 'active_support'
+ puts <<-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
+ t.column :created_on, :datetime
+ end
+ end
+
+ def self.down
+ drop_table :#{table_name.tableize}
+ end
+end
+ EOF
+ end
+
+ ##
+ # Creates a new model using +table_name+ and prints it on stdout.
+
+ def self.create_model(table_name)
+ require 'active_support'
+ puts <<-EOF
+class #{table_name.classify} < ActiveRecord::Base
+end
+ EOF
+ 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(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
+ puts "Last send attempt: #{Time.at email.last_send_attempt}"
+ end
+ puts " #{email.to}"
+ puts
+ end
+
+ 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[:Chdir] = '.'
+ options[:Daemon] = false
+ options[:Delay] = 60
+ options[:MaxAge] = 86400 * 7
+ options[:Once] = false
+ options[:RailsEnv] = ENV['RAILS_ENV']
+ 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 a Rails 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( "--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("-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 |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
+ usage opts
+ end
+
+ opts.separator ''
+ end
+
+ opts.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 opts, <<-EOF
+#{name} must be run from a Rails application's root to deliver email.
+#{Dir.pwd} does not appear to be a Rails application root.
+ EOF
+ end
+ 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 options[:TableName]
+ exit
+ end
+
+ if options[:Daemon] then
+ require 'webrick/server'
+ WEBrick::Daemon.start
+ end
+
+ new(options).run
+
+ 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
+
+ ##
+ # Prints a usage message to $stderr using +opts+ and exits
+
+ def self.usage(opts, message = nil)
+ if message then
+ $stderr.puts message
+ $stderr.puts
+ end
+
+ $stderr.puts opts
+ exit 1
+ 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'
+ 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
+
+ ##
+ # Removes emails that have lived in the queue for too long. If max_age is
+ # set to 0, no emails will be removed.
+
+ 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
+
+ ##
+ # Delivers +emails+ to ActionMailer's SMTP server and destroys them.
+
+ def deliver(emails)
+ user = smtp_settings[:user] || smtp_settings[:user_name]
+ Net::SMTP.start smtp_settings[:address], smtp_settings[:port],
+ smtp_settings[:domain], user,
+ smtp_settings[:password],
+ smtp_settings[:authentication],
+ smtp_settings[:tls] do |smtp|
+ @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")]
+ 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
+ end
+
+ ##
+ # Prepares ar_sendmail for exiting
+
+ def do_exit
+ log "caught signal, shutting down"
+ exit
+ 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
+
+ ##
+ # Installs signal handlers to gracefully exit.
+
+ def install_signal_handlers
+ trap 'TERM' do do_exit end
+ trap 'INT' do do_exit end
+ 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
+ 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
+
+ ##
+ # Proxy to ActionMailer::Base::smtp_settings. See
+ # http://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.
+
+ def smtp_settings
+ ActionMailer::Base.smtp_settings rescue ActionMailer::Base.server_settings
+ end
+
+end
+
105 lib/smtp_tls.rb
@@ -0,0 +1,105 @@
+# Original code believed public domain from ruby-talk or ruby-core email.
+# Modifications by Kyle Maxwell <kyle@kylemaxwell.com> used under MIT license.
+
+require "openssl"
+require "net/smtp"
+
+# :stopdoc:
+
+class Net::SMTP
+
+ class << self
+ send :remove_method, :start
+ end
+
+ def self.start( address, port = nil,
+ helo = 'localhost.localdomain',
+ user = nil, secret = nil, authtype = nil, use_tls = false,
+ &block) # :yield: smtp
+ new(address, port).start(helo, user, secret, authtype, use_tls, &block)
+ end
+
+ alias tls_old_start start
+
+ def start( helo = 'localhost.localdomain',
+ user = nil, secret = nil, authtype = nil, use_tls = false ) # :yield: smtp
+ start_method = use_tls ? :do_tls_start : :do_start
+ if block_given?
+ begin
+ send start_method, helo, user, secret, authtype
+ return yield(self)
+ ensure
+ do_finish
+ end
+ else
+ send start_method, helo, user, secret, authtype
+ return self
+ end
+ end
+
+ private
+
+ def do_tls_start(helodomain, user, secret, authtype)
+ raise IOError, 'SMTP session already started' if @started
+ check_auth_args user, secret, authtype if user or secret
+
+ sock = timeout(@open_timeout) { TCPSocket.open(@address, @port) }
+ @socket = Net::InternetMessageIO.new(sock)
+ @socket.read_timeout = 60 #@read_timeout
+ @socket.debug_output = STDERR #@debug_output
+
+ check_response(critical { recv_response() })
+ do_helo(helodomain)
+
+ raise 'openssl library not installed' unless defined?(OpenSSL)
+ starttls
+ ssl = OpenSSL::SSL::SSLSocket.new(sock)
+ ssl.sync_close = true
+ ssl.connect
+ @socket = Net::InternetMessageIO.new(ssl)
+ @socket.read_timeout = 60 #@read_timeout
+ @socket.debug_output = STDERR #@debug_output
+ do_helo(helodomain)
+
+ authenticate user, secret, authtype if user
+ @started = true
+ ensure
+ unless @started
+ # authentication failed, cancel connection.
+ @socket.close if not @started and @socket and not @socket.closed?
+ @socket = nil
+ end
+ end
+
+ def do_helo(helodomain)
+ begin
+ if @esmtp
+ ehlo helodomain
+ else
+ helo helodomain
+ end
+ rescue Net::ProtocolError
+ if @esmtp
+ @esmtp = false
+ @error_occured = false
+ retry
+ end
+ raise
+ end
+ end
+
+ def starttls
+ getok('STARTTLS')
+ end
+
+ alias tls_old_quit quit
+
+ def quit
+ begin
+ getok('QUIT')
+ rescue EOFError
+ end
+ end
+
+end unless Net::SMTP.private_method_defined? :do_tls_start or
+ Net::SMTP.method_defined? :tls?
30 share/ar_sendmail
@@ -0,0 +1,30 @@
+#!/bin/sh
+# PROVIDE: ar_sendmail
+# REQUIRE: DAEMON
+# BEFORE: LOGIN
+# KEYWORD: FreeBSD shutdown
+
+#
+# Add the following lines to /etc/rc.conf to enable ar_sendmail:
+#
+#ar_sendmail_enable="YES"
+
+. /etc/rc.subr
+
+name="ar_sendmail"
+rcvar=`set_rcvar`
+
+command="/usr/local/bin/ar_sendmail"
+command_interpreter="/usr/local/bin/ruby18"
+
+# set defaults
+
+ar_sendmail_rails_env=${ar_sendmail_rails_env:-"production"}
+ar_sendmail_chdir=${ar_sendmail_chdir:-"/"}
+ar_sendmail_enable=${ar_sendmail_enable:-"NO"}
+ar_sendmail_flags=${ar_sendmail_flags:-"-d"}
+
+load_rc_config $name
+export RAILS_ENV=$ar_sendmail_rails_env
+run_rc_command "$1"
+
185 test/action_mailer.rb
@@ -0,0 +1,185 @@
+require 'net/smtp'
+require 'smtp_tls'
+require 'time'
+
+class Net::SMTP
+
+ @reset_called = 0
+
+ @deliveries = []
+
+ @send_message_block = nil
+
+ @start_block = nil
+
+ class << self
+
+ attr_reader :deliveries
+ attr_reader :send_message_block
+ attr_accessor :reset_called
+
+ send :remove_method, :start
+
+ end
+
+ def self.start(*args)
+ @start_block.call if @start_block
+ yield new(nil)
+ end
+
+ def self.on_send_message(&block)
+ @send_message_block = block
+ end
+
+ def self.on_start(&block)
+ @start_block = block
+ end
+
+ def self.reset
+ deliveries.clear
+ on_start
+ on_send_message
+ @reset_called = 0
+ end
+
+ alias test_old_reset reset if instance_methods.include? 'reset'
+
+ def reset
+ self.class.reset_called += 1
+ end
+
+ alias test_old_send_message send_message
+
+ def send_message(mail, to, from)
+ return self.class.send_message_block.call(mail, to, from) unless
+ self.class.send_message_block.nil?
+ self.class.deliveries << [mail, to, from]
+ return "queued"
+ end
+
+end
+
+##
+# Stub for ActionMailer::Base
+
+module ActionMailer; end
+
+class ActionMailer::Base
+
+ @server_settings = {}
+
+ def self.logger
+ o = Object.new
+ def o.info(arg) end
+ return o
+ end
+
+ def self.method_missing(meth, *args)
+ meth.to_s =~ /deliver_(.*)/
+ super unless $1
+ new($1, *args).deliver!
+ end
+
+ def self.reset
+ server_settings.clear
+ end
+
+ def self.server_settings
+ @server_settings
+ end
+
+ def initialize(meth = nil)
+ send meth if meth
+ end
+
+ def deliver!
+ perform_delivery_activerecord @mail
+ end
+
+end
+
+##
+# Stub for an ActiveRecord model
+
+class Email
+
+ START = Time.parse 'Thu Aug 10 2006 11:19:48'
+
+ attr_accessor :from, :to, :mail, :last_send_attempt, :created_on, :id
+
+ @records = []
+ @id = 0
+
+ class << self; attr_accessor :records, :id; end
+
+ def self.create(record)
+ record = new record[:from], record[:to], record[:mail],
+ record[:last_send_attempt]
+ records << record
+ return record
+ end
+
+ def self.destroy_all(conditions)
+ timeout = conditions.last
+ found = []
+
+ records.each do |record|
+ next if record.last_send_attempt == 0
+ next if record.created_on == 0
+ next unless record.created_on < timeout
+ record.destroy
+ found << record
+ end
+
+ found
+ end
+
+ def self.find(_, conditions = nil)
+ return records if conditions.nil?
+ now = Time.now.to_i - 300
+ return records.select do |r|
+ r.last_send_attempt < now
+ end
+ end
+
+ def self.reset
+ @id = 0
+ records.clear
+ end
+
+ def initialize(from, to, mail, last_send_attempt = nil)
+ @from = from
+ @to = to
+ @mail = mail
+ @id = self.class.id += 1
+ @created_on = START + @id
+ @last_send_attempt = last_send_attempt || 0
+ end
+
+ def destroy
+ self.class.records.delete self
+ self.freeze
+ end
+
+ def ==(other)
+ other.id == id
+ end
+
+ def save
+ end
+
+end
+
+Mail = Email
+
+class String
+ def classify
+ self
+ end
+
+ def tableize
+ self.downcase
+ end
+
+end
+
50 test/test_armailer.rb
@@ -0,0 +1,50 @@
+require 'test/unit'
+require 'action_mailer'
+require 'action_mailer/ar_mailer'
+
+##
+# Pretend mailer
+
+class Mailer < ActionMailer::ARMailer
+
+ def mail
+ @mail = Object.new
+ def @mail.encoded() 'email' end
+ def @mail.from() ['nobody@example.com'] end
+ def @mail.destinations() %w[user1@example.com user2@example.com] end
+ end
+
+end
+
+class TestARMailer < Test::Unit::TestCase
+
+ def setup
+ Mailer.email_class = Email
+
+ Email.records.clear
+ Mail.records.clear
+ end
+
+ def test_self_email_class_equals
+ Mailer.email_class = Mail
+
+ Mailer.deliver_mail
+
+ assert_equal 2, Mail.records.length
+ end
+
+ def test_perform_delivery_activerecord
+ Mailer.deliver_mail
+
+ assert_equal 2, Email.records.length
+
+ record = Email.records.first
+ assert_equal 'email', record.mail
+ assert_equal 'user1@example.com', record.to
+ assert_equal 'nobody@example.com', record.from
+
+ assert_equal 'user2@example.com', Email.records.last.to
+ end
+
+end
+
642 test/test_arsendmail.rb
@@ -0,0 +1,642 @@
+require 'test/unit'
+require 'action_mailer'
+require 'action_mailer/ar_sendmail'
+require 'rubygems'
+require 'test/zentest_assertions'
+
+class ActionMailer::ARSendmail
+ attr_accessor :slept
+ def sleep(secs)
+ @slept ||= []
+ @slept << secs
+ end
+end
+
+class TestARSendmail < Test::Unit::TestCase
+
+ def setup
+ ActionMailer::Base.reset
+ Email.reset
+ Net::SMTP.reset
+
+ @sm = ActionMailer::ARSendmail.new
+ @sm.verbose = true
+
+ @include_c_e = ! $".grep(/config\/environment.rb/).empty?
+ $" << 'config/environment.rb' unless @include_c_e
+ end
+
+ def teardown
+ $".delete 'config/environment.rb' unless @include_c_e
+ end
+
+ def test_class_create_migration
+ out, = util_capture do
+ ActionMailer::ARSendmail.create_migration 'Mail'
+ end
+
+ expected = <<-EOF
+class AddMail < ActiveRecord::Migration
+ def self.up
+ create_table :mail 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 :mail
+ end
+end
+ EOF
+
+ assert_equal expected, out.string
+ end
+
+ def test_class_create_model
+ out, = util_capture do
+ ActionMailer::ARSendmail.create_model 'Mail'
+ end
+
+ expected = <<-EOF
+class Mail < ActiveRecord::Base
+end
+ EOF
+
+ assert_equal expected, out.string
+ end
+
+ def test_class_mailq
+ Email.create :from => nobody, :to => 'recip@h1.example.com',
+ :mail => 'body0'
+ Email.create :from => nobody, :to => 'recip@h1.example.com',
+ :mail => 'body1'
+ last = Email.create :from => nobody, :to => 'recip@h2.example.com',
+ :mail => 'body2'
+
+ last.last_send_attempt = Time.parse('Thu Aug 10 2006 11:40:05').to_i
+
+ out, err = util_capture do
+ ActionMailer::ARSendmail.mailq 'Email'
+ end
+
+ expected = <<-EOF
+-Queue ID- --Size-- ----Arrival Time---- -Sender/Recipient-------
+ 1 5 Thu Aug 10 11:19:49 nobody@example.com
+ recip@h1.example.com
+
+ 2 5 Thu Aug 10 11:19:50 nobody@example.com
+ recip@h1.example.com
+
+ 3 5 Thu Aug 10 11:19:51 nobody@example.com
+Last send attempt: Thu Aug 10 11:40:05 -0700 2006
+ recip@h2.example.com
+
+-- 0 Kbytes in 3 Requests.
+ EOF
+
+ assert_equal expected, out.string
+ end
+
+ def test_class_mailq_empty
+ out, err = util_capture do
+ ActionMailer::ARSendmail.mailq 'Email'
+ end
+
+ assert_equal "Mail queue is empty\n", out.string
+ end
+
+ def test_class_new
+ @sm = ActionMailer::ARSendmail.new
+
+ assert_equal 60, @sm.delay
+ assert_equal Email, @sm.email_class
+ assert_equal nil, @sm.once
+ assert_equal nil, @sm.verbose
+ assert_equal nil, @sm.batch_size
+
+ @sm = ActionMailer::ARSendmail.new :Delay => 75, :Verbose => true,
+ :TableName => 'Object', :Once => true,
+ :BatchSize => 1000
+
+ assert_equal 75, @sm.delay
+ assert_equal Object, @sm.email_class
+ assert_equal true, @sm.once
+ assert_equal true, @sm.verbose
+ assert_equal 1000, @sm.batch_size
+ end
+
+ def test_class_parse_args_batch_size
+ options = ActionMailer::ARSendmail.process_args %w[-b 500]
+
+ assert_equal 500, options[:BatchSize]
+
+ options = ActionMailer::ARSendmail.process_args %w[--batch-size 500]
+
+ assert_equal 500, options[:BatchSize]
+ end
+
+ def test_class_parse_args_chdir
+ argv = %w[-c /tmp]
+
+ options = ActionMailer::ARSendmail.process_args argv
+
+ assert_equal '/tmp', options[:Chdir]
+
+ argv = %w[--chdir /tmp]
+
+ options = ActionMailer::ARSendmail.process_args argv
+
+ assert_equal '/tmp', options[:Chdir]
+
+ argv = %w[-c /nonexistent]
+
+ out, err = util_capture do
+ assert_raises SystemExit do
+ ActionMailer::ARSendmail.process_args argv
+ end
+ end
+ end
+
+ def test_class_parse_args_daemon
+ argv = %w[-d]
+
+ options = ActionMailer::ARSendmail.process_args argv
+
+ assert_equal true, options[:Daemon]
+
+ argv = %w[--daemon]
+
+ options = ActionMailer::ARSendmail.process_args argv
+
+ assert_equal true, options[:Daemon]
+ end
+
+ def test_class_parse_args_delay
+ argv = %w[--delay 75]
+
+ options = ActionMailer::ARSendmail.process_args argv
+
+ assert_equal 75, options[:Delay]
+ end
+
+ def test_class_parse_args_environment
+ assert_equal nil, ENV['RAILS_ENV']
+
+ argv = %w[-e production]
+
+ options = ActionMailer::ARSendmail.process_args argv
+
+ assert_equal 'production', options[:RailsEnv]
+
+ assert_equal 'production', ENV['RAILS_ENV']
+
+ argv = %w[--environment production]
+
+ options = ActionMailer::ARSendmail.process_args argv
+
+ assert_equal 'production', options[:RailsEnv]
+ end
+
+ def test_class_parse_args_mailq
+ options = ActionMailer::ARSendmail.process_args []
+ deny_includes :MailQ, options
+
+ argv = %w[--mailq]
+
+ options = ActionMailer::ARSendmail.process_args argv
+
+ assert_equal true, options[:MailQ]
+ end
+
+ def test_class_parse_args_max_age
+ options = ActionMailer::ARSendmail.process_args []
+ assert_equal 86400 * 7, options[:MaxAge]
+
+ argv = %w[--max-age 86400]
+
+ options = ActionMailer::ARSendmail.process_args argv
+
+ assert_equal 86400, options[:MaxAge]
+ end
+
+ def test_class_parse_args_migration
+ options = ActionMailer::ARSendmail.process_args []
+ deny_includes :Migration, options
+
+ argv = %w[--create-migration]
+
+ options = ActionMailer::ARSendmail.process_args argv
+
+ assert_equal true, options[:Migrate]
+ end
+
+ def test_class_parse_args_model
+ options = ActionMailer::ARSendmail.process_args []
+ deny_includes :Model, options
+
+ argv = %w[--create-model]
+
+ options = ActionMailer::ARSendmail.process_args argv
+
+ assert_equal true, options[:Model]
+ end
+
+ def test_class_parse_args_no_config_environment
+ $".delete 'config/environment.rb'
+
+ out, err = util_capture do
+ assert_raise SystemExit do
+ ActionMailer::ARSendmail.process_args []
+ end
+ end
+
+ ensure
+ $" << 'config/environment.rb' if @include_c_e
+ end
+
+ def test_class_parse_args_no_config_environment_migrate
+ $".delete 'config/environment.rb'
+
+ out, err = util_capture do
+ ActionMailer::ARSendmail.process_args %w[--create-migration]
+ end
+
+ assert true # count
+
+ ensure
+ $" << 'config/environment.rb' if @include_c_e
+ end
+
+ def test_class_parse_args_no_config_environment_model
+ $".delete 'config/environment.rb'
+
+ out, err = util_capture do
+ ActionMailer::ARSendmail.process_args %w[--create-model]
+ end
+
+ assert true # count
+
+ rescue SystemExit
+ flunk 'Should not exit'
+
+ ensure
+ $" << 'config/environment.rb' if @include_c_e
+ end
+
+ def test_class_parse_args_once
+ argv = %w[-o]
+
+ options = ActionMailer::ARSendmail.process_args argv
+
+ assert_equal true, options[:Once]
+
+ argv = %w[--once]
+
+ options = ActionMailer::ARSendmail.process_args argv
+
+ assert_equal true, options[:Once]
+ end
+
+ def test_class_parse_args_table_name
+ argv = %w[-t Email]
+
+ options = ActionMailer::ARSendmail.process_args argv
+
+ assert_equal 'Email', options[:TableName]
+
+ argv = %w[--table-name=Email]
+
+ options = ActionMailer::ARSendmail.process_args argv
+
+ assert_equal 'Email', options[:TableName]
+ end
+
+ def test_class_usage
+ out, err = util_capture do
+ assert_raises SystemExit do
+ ActionMailer::ARSendmail.usage 'opts'
+ end
+ end
+
+ assert_equal '', out.string
+ assert_equal "opts\n", err.string
+
+ out, err = util_capture do
+ assert_raises SystemExit do
+ ActionMailer::ARSendmail.usage 'opts', 'hi'
+ end
+ end
+
+ assert_equal '', out.string
+ assert_equal "hi\n\nopts\n", err.string
+ end
+
+ def test_cleanup
+ e1 = Email.create :mail => 'body', :to => 'to', :from => 'from'
+ e1.created_on = Time.now
+ e2 = Email.create :mail => 'body', :to => 'to', :from => 'from'
+ e3 = Email.create :mail => 'body', :to => 'to', :from => 'from'
+ e3.last_send_attempt = Time.now
+
+ out, err = util_capture do
+ @sm.cleanup
+ end
+
+ assert_equal '', out.string
+ assert_equal "expired 1 emails from the queue\n", err.string
+ assert_equal 2, Email.records.length
+
+ assert_equal [e1, e2], Email.records
+ end
+
+ def test_cleanup_disabled
+ e1 = Email.create :mail => 'body', :to => 'to', :from => 'from'
+ e1.created_on = Time.now
+ e2 = Email.create :mail => 'body', :to => 'to', :from => 'from'
+
+ @sm.max_age = 0
+
+ out, err = util_capture do
+ @sm.cleanup
+ end
+
+ assert_equal '', out.string
+ assert_equal 2, Email.records.length
+ end
+
+ def test_deliver
+ email = Email.create :mail => 'body', :to => 'to', :from => 'from'
+
+ out, err = util_capture do
+ @sm.deliver [email]
+ end
+
+ assert_equal 1, Net::SMTP.deliveries.length
+ assert_equal ['body', 'from', 'to'], Net::SMTP.deliveries.first
+ assert_equal 0, Email.records.length
+ assert_equal 0, Net::SMTP.reset_called, 'Reset connection on SyntaxError'
+
+ assert_equal '', out.string
+ assert_equal "sent email 00000000001 from from to to: \"queued\"\n", err.string
+ end
+
+ def test_deliver_auth_error
+ Net::SMTP.on_start do
+ e = Net::SMTPAuthenticationError.new 'try again'
+ e.set_backtrace %w[one two three]
+ raise e
+ end
+
+ now = Time.now.to_i
+
+ email = Email.create :mail => 'body', :to => 'to', :from => 'from'
+
+ out, err = util_capture do
+ @sm.deliver [email]
+ end
+
+ assert_equal 0, Net::SMTP.deliveries.length
+ assert_equal 1, Email.records.length
+ assert_equal 0, Email.records.first.last_send_attempt
+ assert_equal 0, Net::SMTP.reset_called
+ assert_equal 1, @sm.failed_auth_count
+ assert_equal [60], @sm.slept
+
+ assert_equal '', out.string
+ assert_equal "authentication error, retrying: try again\n", err.string
+ end
+
+ def test_deliver_auth_error_recover
+ email = Email.create :mail => 'body', :to => 'to', :from => 'from'
+ @sm.failed_auth_count = 1
+
+ out, err = util_capture do @sm.deliver [email] end
+
+ assert_equal 0, @sm.failed_auth_count
+ assert_equal 1, Net::SMTP.deliveries.length
+ end
+
+ def test_deliver_auth_error_twice
+ Net::SMTP.on_start do
+ e = Net::SMTPAuthenticationError.new 'try again'
+ e.set_backtrace %w[one two three]
+ raise e
+ end
+
+ @sm.failed_auth_count = 1
+
+ out, err = util_capture do
+ assert_raise Net::SMTPAuthenticationError do
+ @sm.deliver []
+ end
+ end
+
+ assert_equal 2, @sm.failed_auth_count
+ assert_equal "authentication error, giving up: try again\n", err.string
+ end
+
+ def test_deliver_4xx_error
+ Net::SMTP.on_send_message do
+ e = Net::SMTPSyntaxError.new 'try again'
+ e.set_backtrace %w[one two three]
+ raise e
+ end
+
+ now = Time.now.to_i
+
+ email = Email.create :mail => 'body', :to => 'to', :from => 'from'
+
+ out, err = util_capture do
+ @sm.deliver [email]
+ end
+
+ assert_equal 0, Net::SMTP.deliveries.length
+ assert_equal 1, Email.records.length
+ assert_operator now, :<=, Email.records.first.last_send_attempt
+ assert_equal 1, Net::SMTP.reset_called, 'Reset connection on SyntaxError'
+
+ assert_equal '', out.string
+ assert_equal "error sending email 1: \"try again\"(Net::SMTPSyntaxError):\n\tone\n\ttwo\n\tthree\n", err.string
+ end
+
+ def test_deliver_5xx_error
+ Net::SMTP.on_send_message do
+ e = Net::SMTPFatalError.new 'unknown recipient'
+ e.set_backtrace %w[one two three]
+ raise e
+ end
+
+ now = Time.now.to_i
+
+ email = Email.create :mail => 'body', :to => 'to', :from => 'from'
+
+ out, err = util_capture do
+ @sm.deliver [email]
+ end
+
+ assert_equal 0, Net::SMTP.deliveries.length
+ assert_equal 0, Email.records.length
+ assert_equal 1, Net::SMTP.reset_called, 'Reset connection on SyntaxError'
+
+ assert_equal '', out.string
+ assert_equal "5xx error sending email 1, removing from queue: \"unknown recipient\"(Net::SMTPFatalError):\n\tone\n\ttwo\n\tthree\n", err.string
+ end
+
+ def test_deliver_errno_epipe
+ Net::SMTP.on_send_message do
+ raise Errno::EPIPE
+ end
+
+ now = Time.now.to_i
+
+ email = Email.create :mail => 'body', :to => 'to', :from => 'from'
+
+ out, err = util_capture do
+ @sm.deliver [email]
+ end
+
+ assert_equal 0, Net::SMTP.deliveries.length
+ assert_equal 1, Email.records.length
+ assert_operator now, :>=, Email.records.first.last_send_attempt
+ assert_equal 0, Net::SMTP.reset_called, 'Reset connection on SyntaxError'
+
+ assert_equal '', out.string
+ assert_equal '', err.string
+ end
+
+ def test_deliver_server_busy
+ Net::SMTP.on_send_message do
+ e = Net::SMTPServerBusy.new 'try again'
+ e.set_backtrace %w[one two three]
+ raise e
+ end
+
+ now = Time.now.to_i
+
+ email = Email.create :mail => 'body', :to => 'to', :from => 'from'
+
+ out, err = util_capture do
+ @sm.deliver [email]
+ end
+
+ assert_equal 0, Net::SMTP.deliveries.length
+ assert_equal 1, Email.records.length
+ assert_operator now, :>=, Email.records.first.last_send_attempt
+ assert_equal 0, Net::SMTP.reset_called, 'Reset connection on SyntaxError'
+ assert_equal [60], @sm.slept
+
+ assert_equal '', out.string
+ assert_equal "server too busy, sleeping 60 seconds\n", err.string
+ end
+
+ def test_deliver_syntax_error
+ Net::SMTP.on_send_message do
+ Net::SMTP.on_send_message # clear
+ e = Net::SMTPSyntaxError.new 'blah blah blah'
+ e.set_backtrace %w[one two three]
+ raise e
+ end
+
+ now = Time.now.to_i
+
+ email1 = Email.create :mail => 'body', :to => 'to', :from => 'from'
+ email2 = Email.create :mail => 'body', :to => 'to', :from => 'from'
+
+ out, err = util_capture do
+ @sm.deliver [email1, email2]
+ end
+
+ assert_equal 1, Net::SMTP.deliveries.length, 'delivery count'
+ assert_equal 1, Email.records.length
+ assert_equal 1, Net::SMTP.reset_called, 'Reset connection on SyntaxError'
+ assert_operator now, :<=, Email.records.first.last_send_attempt
+
+ assert_equal '', out.string
+ assert_equal "error sending email 1: \"blah blah blah\"(Net::SMTPSyntaxError):\n\tone\n\ttwo\n\tthree\nsent email 00000000002 from from to to: \"queued\"\n", err.string
+ end
+
+ def test_deliver_timeout
+ Net::SMTP.on_send_message do
+ e = Timeout::Error.new 'timed out'
+ e.set_backtrace %w[one two three]
+ raise e
+ end
+
+ now = Time.now.to_i
+
+ email = Email.create :mail => 'body', :to => 'to', :from => 'from'
+
+ out, err = util_capture do
+ @sm.deliver [email]
+ end
+
+ assert_equal 0, Net::SMTP.deliveries.length
+ assert_equal 1, Email.records.length
+ assert_operator now, :>=, Email.records.first.last_send_attempt
+ assert_equal 1, Net::SMTP.reset_called, 'Reset connection on Timeout'
+
+ assert_equal '', out.string
+ assert_equal "error sending email 1: \"timed out\"(Timeout::Error):\n\tone\n\ttwo\n\tthree\n", err.string
+ end
+
+ def test_do_exit
+ out, err = util_capture do
+ assert_raise SystemExit do
+ @sm.do_exit
+ end
+ end
+
+ assert_equal '', out.string
+ assert_equal "caught signal, shutting down\n", err.string
+ end
+
+ def test_log
+ out, err = util_capture do
+ @sm.log 'hi'
+ end
+
+ assert_equal "hi\n", err.string
+ end
+
+ def test_find_emails
+ email_data = [
+ { :mail => 'body0', :to => 'recip@h1.example.com', :from => nobody },
+ { :mail => 'body1', :to => 'recip@h1.example.com', :from => nobody },
+ { :mail => 'body2', :to => 'recip@h2.example.com', :from => nobody },
+ ]
+
+ emails = email_data.map do |email_data| Email.create email_data end
+
+ tried = Email.create :mail => 'body3', :to => 'recip@h3.example.com',
+ :from => nobody
+
+ tried.last_send_attempt = Time.now.to_i - 258
+
+ found_emails = []
+
+ out, err = util_capture do
+ found_emails = @sm.find_emails
+ end
+
+ assert_equal emails, found_emails
+
+ assert_equal '', out.string
+ assert_equal "found 3 emails to send\n", err.string
+ end
+
+ def test_smtp_settings
+ ActionMailer::Base.server_settings[:address] = 'localhost'
+
+ assert_equal 'localhost', @sm.smtp_settings[:address]
+ end
+
+ def nobody
+ 'nobody@example.com'
+ end
+
+end
+

0 comments on commit 3b28380

Please sign in to comment.
Something went wrong with that request. Please try again.