├── .gitignore ├── generators └── ar_mailer │ ├── templates │ ├── model.rb │ └── migration.rb │ └── ar_mailer_generator.rb ├── bin └── ar_sendmail ├── test ├── test_helper.rb ├── test_armailer.rb ├── resources │ └── action_mailer.rb └── test_arsendmail.rb ├── lib ├── ar_sendmail_logger.rb ├── action_mailer │ ├── ar_mailer.rb │ └── ar_sendmail.rb └── smtp_tls.rb ├── share ├── bsd │ └── ar_sendmail └── linux │ ├── ar_sendmail.conf │ └── ar_sendmail ├── LICENSE.txt ├── ar_mailer.gemspec ├── Rakefile ├── History.txt └── README.rdoc /.gitignore: -------------------------------------------------------------------------------- 1 | pkg/* 2 | -------------------------------------------------------------------------------- /generators/ar_mailer/templates/model.rb: -------------------------------------------------------------------------------- 1 | class <%= class_name %> < ActiveRecord::Base 2 | end 3 | -------------------------------------------------------------------------------- /bin/ar_sendmail: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'action_mailer/ar_sendmail' 4 | 5 | ActionMailer::ARSendmail.run 6 | 7 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | require 'test/unit' 2 | require 'rubygems' 3 | require 'active_support' 4 | require 'test/resources/action_mailer' 5 | require 'minitest/autorun' 6 | require 'mocha' 7 | 8 | require 'action_mailer/ar_mailer' 9 | require 'action_mailer/ar_sendmail' 10 | -------------------------------------------------------------------------------- /lib/ar_sendmail_logger.rb: -------------------------------------------------------------------------------- 1 | require 'logger' 2 | 3 | class ARSendmailLogger < ::Logger 4 | 5 | def initialize(path) 6 | FileUtils.mkdir_p(File.dirname(path)) 7 | super(path) 8 | end 9 | 10 | def format_message(severity, timestamp, progname, message) 11 | "#{message}\n" 12 | end 13 | 14 | end 15 | -------------------------------------------------------------------------------- /generators/ar_mailer/templates/migration.rb: -------------------------------------------------------------------------------- 1 | class <%= migration_name %> < ActiveRecord::Migration 2 | def self.up 3 | create_table :<%= table_name %> do |t| 4 | t.column :from, :string 5 | t.column :to, :string 6 | t.column :last_send_attempt, :integer, :default => 0 7 | t.column :mail, :text 8 | t.column :created_on, :datetime 9 | end 10 | end 11 | 12 | def self.down 13 | drop_table :<%= table_name %> 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /share/bsd/ar_sendmail: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # PROVIDE: ar_sendmail 3 | # REQUIRE: DAEMON 4 | # BEFORE: LOGIN 5 | # KEYWORD: FreeBSD shutdown 6 | 7 | # 8 | # Add the following lines to /etc/rc.conf to enable ar_sendmail: 9 | # 10 | #ar_sendmail_enable="YES" 11 | 12 | . /etc/rc.subr 13 | 14 | name="ar_sendmail" 15 | rcvar=`set_rcvar` 16 | 17 | command="/usr/local/bin/ar_sendmail" 18 | command_interpreter="/usr/local/bin/ruby18" 19 | 20 | # set defaults 21 | 22 | ar_sendmail_rails_env=${ar_sendmail_rails_env:-"production"} 23 | ar_sendmail_chdir=${ar_sendmail_chdir:-"/"} 24 | ar_sendmail_enable=${ar_sendmail_enable:-"NO"} 25 | ar_sendmail_flags=${ar_sendmail_flags:-"-d"} 26 | 27 | load_rc_config $name 28 | export RAILS_ENV=$ar_sendmail_rails_env 29 | run_rc_command "$1" 30 | 31 | -------------------------------------------------------------------------------- /generators/ar_mailer/ar_mailer_generator.rb: -------------------------------------------------------------------------------- 1 | class ArMailerGenerator < Rails::Generator::NamedBase 2 | 3 | def initialize(runtime_args, runtime_options = {}) 4 | runtime_args.unshift('Email') if runtime_args.empty? 5 | super 6 | end 7 | 8 | def manifest 9 | record do |m| 10 | m.class_collisions class_name 11 | 12 | m.template 'model.rb', File.join('app/models', class_path, "#{file_name}.rb") 13 | 14 | m.migration_template 'migration.rb', 'db/migrate', :assigns => { 15 | :migration_name => "Create#{class_name.pluralize.gsub(/::/, '')}" 16 | }, :migration_file_name => "create_#{file_path.gsub(/\//, '_').pluralize}" 17 | end 18 | end 19 | 20 | protected 21 | def banner 22 | "Usage: #{$0} #{spec.name} EmailModelName (default: Email)" 23 | end 24 | 25 | end 26 | -------------------------------------------------------------------------------- /lib/action_mailer/ar_mailer.rb: -------------------------------------------------------------------------------- 1 | ## 2 | # Adds sending email through an ActiveRecord table as a delivery method for 3 | # ActionMailer. 4 | # 5 | 6 | class ActionMailer::Base 7 | 8 | ## 9 | # Set the email class for deliveries. Handle class reloading issues which prevents caching the email class. 10 | # 11 | @@email_class_name = 'Email' 12 | 13 | def self.email_class=(klass) 14 | @@email_class_name = klass.to_s 15 | end 16 | 17 | def self.email_class 18 | @@email_class_name.constantize 19 | end 20 | 21 | ## 22 | # Adds +mail+ to the Email table. Only the first From address for +mail+ is 23 | # used. 24 | 25 | def perform_delivery_activerecord(mail) 26 | mail.destinations.each do |destination| 27 | self.class.email_class.create :mail => mail.encoded, :to => destination, :from => mail.from.first 28 | end 29 | end 30 | 31 | end 32 | -------------------------------------------------------------------------------- /share/linux/ar_sendmail.conf: -------------------------------------------------------------------------------- 1 | # This is a configuration file for ar_sendmail daemon init.d script 2 | # 3 | # Define the settings for each app which has a mailer that you want start 4 | # automatically on startup. 5 | # 6 | # You can define an optional defaults section which will apply to all 7 | # applications unless the setting is specified under the applications specific 8 | # config. 9 | # 10 | # Settings not specified in either the defaults or app config section will 11 | # be implied by the gem binary defaults. The option names are the same as the 12 | # long format binary option switches. Run 'ar_sendmail -h' to see option switches 13 | # and default values. 14 | # 15 | # Copy this file to /etc/ar_sendmail.conf and it will be read by the init.d 16 | # script. 17 | # 18 | 19 | ## Demo app config 20 | # 21 | #defaults: 22 | # batch-size: 10 23 | # max-age: 0 24 | # delay: 60 25 | # 26 | #app_name: 27 | # chdir: /var/www/apps/app_name 28 | # environment: production 29 | # pidfile: ./log/ar_sendmail.pid 30 | 31 | -------------------------------------------------------------------------------- /test/test_armailer.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + '/test_helper') 2 | 3 | class Mailer < ActionMailer::Base 4 | self.delivery_method = :activerecord 5 | 6 | def mail 7 | @mail = Object.new 8 | def @mail.encoded() 'email' end 9 | def @mail.from() ['nobody@example.com'] end 10 | def @mail.destinations() %w[user1@example.com user2@example.com] end 11 | end 12 | 13 | end 14 | 15 | class TestARMailer < Test::Unit::TestCase 16 | 17 | def setup 18 | Mailer.email_class = Email 19 | 20 | Email.records.clear 21 | Newsletter.records.clear 22 | end 23 | 24 | def test_self_email_class_equals 25 | Mailer.email_class = Newsletter 26 | 27 | Mailer.deliver_mail 28 | 29 | assert_equal 2, Newsletter.records.length 30 | end 31 | 32 | def test_perform_delivery_activerecord 33 | Mailer.deliver_mail 34 | 35 | assert_equal 2, Email.records.length 36 | 37 | record = Email.records.first 38 | assert_equal 'email', record.mail 39 | assert_equal 'user1@example.com', record.to 40 | assert_equal 'nobody@example.com', record.from 41 | 42 | assert_equal 'user2@example.com', Email.records.last.to 43 | end 44 | 45 | end 46 | 47 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Original code copyright 2006, 2007, Eric Hodel, The Robot Co-op. All 2 | rights reserved. Some code under other license, see individual files 3 | for details. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions 7 | are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright 10 | notice, this list of conditions and the following disclaimer. 11 | 2. Redistributions in binary form must reproduce the above copyright 12 | notice, this list of conditions and the following disclaimer in the 13 | documentation and/or other materials provided with the distribution. 14 | 3. Neither the names of the authors nor the names of their contributors 15 | may be used to endorse or promote products derived from this software 16 | without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND ANY EXPRESS 19 | OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 21 | ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE 22 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, 23 | OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT 24 | OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR 25 | BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 26 | WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 27 | OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 28 | EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /ar_mailer.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | 3 | Gem::Specification.new do |s| 4 | s.name = %q{zendesk-ar_mailer} 5 | s.version = "2.1.7" 6 | 7 | s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= 8 | s.authors = ["Eric Hodel", "Adam Meehan", "Morten Primdahl"] 9 | s.date = %q{2010-08-24} 10 | s.default_executable = %q{ar_sendmail} 11 | s.description = %q{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.} 12 | s.email = %q{adam.meehan@gmail.com} 13 | s.executables = ["ar_sendmail"] 14 | s.extra_rdoc_files = ["History.txt", "LICENSE.txt", "README.rdoc"] 15 | s.files = ["History.txt", "LICENSE.txt", "README.rdoc", "Rakefile", "bin/ar_sendmail", "generators/ar_mailer/ar_mailer_generator.rb", "generators/ar_mailer/templates/migration.rb", "generators/ar_mailer/templates/model.rb", "lib/action_mailer/ar_mailer.rb", "lib/action_mailer/ar_sendmail.rb", "lib/smtp_tls.rb", "lib/ar_sendmail_logger.rb", "share/bsd/ar_sendmail", "share/linux/ar_sendmail", "share/linux/ar_sendmail.conf", "test/resources/action_mailer.rb", "test/test_armailer.rb", "test/test_arsendmail.rb", "test/test_helper.rb"] 16 | s.homepage = %q{http://github.com/zendesk/ar_mailer} 17 | s.rdoc_options = ["--main", "README.rdoc"] 18 | s.require_paths = ["lib"] 19 | s.rubyforge_project = %q{seattlerb} 20 | s.rubygems_version = %q{1.3.7} 21 | s.summary = %q{A two-phase delivery agent for ActionMailer} 22 | s.test_files = ["test/test_armailer.rb", "test/test_arsendmail.rb"] 23 | 24 | if s.respond_to? :specification_version then 25 | current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION 26 | s.specification_version = 3 27 | 28 | if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then 29 | else 30 | end 31 | else 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /share/linux/ar_sendmail: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # 3 | # ar_sendmail Startup script for ar_mailer by Adam Meehan 4 | # 5 | # chkconfig: - 85 15 6 | # description: ar_sendmail manages sending emails for Rails apps. 7 | # 8 | require 'yaml' 9 | 10 | # Config file app mailers 11 | config_file = '/etc/ar_sendmail.conf' 12 | 13 | begin 14 | config = YAML::load(IO.read(config_file)) || {} 15 | if config.empty? || (config.has_key?('defaults') && config.size == 1) 16 | puts "No mailers defined. Exiting." 17 | exit 18 | end 19 | rescue Errno::ENOENT 20 | puts "Config file not found at '#{config_file}'!" 21 | exit 22 | end 23 | 24 | default_options = {'pidfile' => './log/ar_sendmail.pid'}.merge(config.delete('defaults') || {}) 25 | 26 | command, app_name = *ARGV 27 | 28 | def start(app, options) 29 | switches = "" 30 | options.each {|k, v| switches << " --#{k} #{v}"} 31 | STDOUT.write "Starting mailer for #{app} in #{options['environment']} mode ... " 32 | status = system("ar_sendmail -d #{switches}") ? "started" : "failed" 33 | puts status 34 | end 35 | 36 | def stop(app, options) 37 | pid_file = File.expand_path(options['pidfile'], options['chdir']) 38 | if File.exist? pid_file 39 | begin 40 | pid = open(pid_file).read.to_i 41 | STDOUT.write "Stopping mailer for #{app}... " 42 | Process.kill('TERM', pid) 43 | puts "stopped" 44 | rescue Errno::ESRCH 45 | puts "Mailer process does not exist. Is not running." 46 | end 47 | else 48 | puts "Skipping mailer for #{app}, no pid file." 49 | end 50 | end 51 | 52 | def restart(app, options) 53 | puts "Restarting mailer for #{app} ..." 54 | stop app, options 55 | start app, options 56 | end 57 | 58 | def command_error(msg) 59 | puts msg 60 | exit 61 | end 62 | 63 | if ['start', 'stop', 'restart'].include?(command) 64 | apps = config 65 | if app_name 66 | command_error "No such app defined in ar_sendmail config" unless config.include?(app_name) 67 | app_options = config[app_name] 68 | command_error "Must specify chdir for app in ar_sendmail config" if app_options['chdir'].nil? 69 | apps = {app_name => app_options} 70 | end 71 | 72 | apps.each do |app, options| 73 | options = default_options.merge(options) 74 | send(command, app, options) 75 | end 76 | else 77 | command_error "Usage: ar_sendmail {start|stop|restart} [optional app_name]" 78 | end 79 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'rake/gempackagetask' 3 | require 'rake/testtask' 4 | require 'rake/rdoctask' 5 | 6 | $:.unshift(File.expand_path(File.dirname(__FILE__) + '/lib')) 7 | 8 | require './lib/action_mailer/ar_sendmail' 9 | 10 | ar_mailer_gemspec = Gem::Specification.new do |s| 11 | s.name = %q{zendesk-ar_mailer} 12 | s.version = ActionMailer::ARSendmail::VERSION 13 | s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= 14 | s.authors = ["Eric Hodel", "Adam Meehan", "Morten Primdahl"] 15 | s.default_executable = %q{ar_sendmail} 16 | s.description = %q{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.} 17 | s.email = %q{adam.meehan@gmail.com} 18 | s.executables = ["ar_sendmail"] 19 | s.extra_rdoc_files = ["History.txt", "LICENSE.txt", "README.rdoc"] 20 | s.files = ["History.txt", "LICENSE.txt", "README.rdoc", "Rakefile", "bin/ar_sendmail", "generators/ar_mailer/ar_mailer_generator.rb", "generators/ar_mailer/templates/migration.rb", "generators/ar_mailer/templates/model.rb", "lib/action_mailer/ar_mailer.rb", "lib/action_mailer/ar_sendmail.rb", "lib/smtp_tls.rb", "lib/ar_sendmail_logger.rb", "share/bsd/ar_sendmail", "share/linux/ar_sendmail", "share/linux/ar_sendmail.conf", "test/resources/action_mailer.rb", "test/test_armailer.rb", "test/test_arsendmail.rb", "test/test_helper.rb"] 21 | s.has_rdoc = true 22 | s.homepage = %q{http://github.com/zendesk/ar_mailer} 23 | s.rdoc_options = ["--main", "README.rdoc"] 24 | s.require_paths = ["lib"] 25 | s.rubyforge_project = %q{seattlerb} 26 | s.summary = %q{A two-phase delivery agent for ActionMailer} 27 | s.test_files = ["test/test_armailer.rb", "test/test_arsendmail.rb"] 28 | end 29 | 30 | Rake::GemPackageTask.new(ar_mailer_gemspec) do |pkg| 31 | pkg.gem_spec = ar_mailer_gemspec 32 | end 33 | 34 | desc "Update ar_mailer.gemspec" 35 | task :make_spec do 36 | File.open("ar_mailer.gemspec", "w") do |f| 37 | f.puts(ar_mailer_gemspec.to_ruby) 38 | end 39 | end 40 | 41 | desc "Build packages and install" 42 | task :install => :package do 43 | sh %{sudo gem install --local --test pkg/ar_mailer-#{ActionMailer::ARSendmail::VERSION}} 44 | end 45 | 46 | desc 'Default: run unit tests.' 47 | task :default => :test 48 | 49 | desc 'Test the ar_mailer gem.' 50 | Rake::TestTask.new(:test) do |t| 51 | t.libs << 'lib' << 'test' 52 | t.test_files = FileList['test/**/test_*.rb'].exclude("test/test_helper.rb") 53 | t.verbose = true 54 | end 55 | -------------------------------------------------------------------------------- /lib/smtp_tls.rb: -------------------------------------------------------------------------------- 1 | # Original code believed public domain from ruby-talk or ruby-core email. 2 | # Modifications by Kyle Maxwell used under MIT license. 3 | 4 | require "openssl" 5 | require "net/smtp" 6 | 7 | # :stopdoc: 8 | 9 | class Net::SMTP 10 | 11 | class << self 12 | send :remove_method, :start 13 | end 14 | 15 | def self.start( address, port = nil, 16 | helo = 'localhost.localdomain', 17 | user = nil, secret = nil, authtype = nil, use_tls = false, 18 | &block) # :yield: smtp 19 | new(address, port).start(helo, user, secret, authtype, use_tls, &block) 20 | end 21 | 22 | alias tls_old_start start 23 | 24 | def start( helo = 'localhost.localdomain', 25 | user = nil, secret = nil, authtype = nil, use_tls = false ) # :yield: smtp 26 | start_method = use_tls ? :do_tls_start : :do_start 27 | if block_given? 28 | begin 29 | send start_method, helo, user, secret, authtype 30 | return yield(self) 31 | ensure 32 | do_finish 33 | end 34 | else 35 | send start_method, helo, user, secret, authtype 36 | return self 37 | end 38 | end 39 | 40 | private 41 | 42 | def do_tls_start(helodomain, user, secret, authtype) 43 | raise IOError, 'SMTP session already started' if @started 44 | check_auth_args user, secret, authtype if user or secret 45 | 46 | sock = timeout(@open_timeout) { TCPSocket.open(@address, @port) } 47 | @socket = Net::InternetMessageIO.new(sock) 48 | @socket.read_timeout = 60 #@read_timeout 49 | @socket.debug_output = STDERR #@debug_output 50 | 51 | check_response(critical { recv_response() }) 52 | do_helo(helodomain) 53 | 54 | raise 'openssl library not installed' unless defined?(OpenSSL) 55 | starttls 56 | ssl = OpenSSL::SSL::SSLSocket.new(sock) 57 | ssl.sync_close = true 58 | ssl.connect 59 | @socket = Net::InternetMessageIO.new(ssl) 60 | @socket.read_timeout = 60 #@read_timeout 61 | @socket.debug_output = STDERR #@debug_output 62 | do_helo(helodomain) 63 | 64 | authenticate user, secret, authtype if user 65 | @started = true 66 | ensure 67 | unless @started 68 | # authentication failed, cancel connection. 69 | @socket.close if not @started and @socket and not @socket.closed? 70 | @socket = nil 71 | end 72 | end 73 | 74 | def do_helo(helodomain) 75 | begin 76 | if @esmtp 77 | ehlo helodomain 78 | else 79 | helo helodomain 80 | end 81 | rescue Net::ProtocolError 82 | if @esmtp 83 | @esmtp = false 84 | @error_occured = false 85 | retry 86 | end 87 | raise 88 | end 89 | end 90 | 91 | def starttls 92 | getok('STARTTLS') 93 | end 94 | 95 | alias tls_old_quit quit 96 | 97 | def quit 98 | begin 99 | getok('QUIT') 100 | rescue EOFError 101 | end 102 | end 103 | 104 | end unless Net::SMTP.private_method_defined? :do_tls_start or 105 | Net::SMTP.method_defined? :tls? 106 | -------------------------------------------------------------------------------- /test/resources/action_mailer.rb: -------------------------------------------------------------------------------- 1 | require 'net/smtp' 2 | require 'smtp_tls' unless Net::SMTP.instance_methods.include?("enable_starttls_auto") 3 | require 'time' 4 | 5 | class Net::SMTP 6 | 7 | @reset_called = 0 8 | 9 | @deliveries = [] 10 | 11 | @send_message_block = nil 12 | 13 | @start_block = nil 14 | 15 | class << self 16 | 17 | attr_reader :deliveries 18 | attr_reader :send_message_block 19 | attr_accessor :reset_called 20 | 21 | # send :remove_method, :start 22 | end 23 | 24 | def self.on_send_message(&block) 25 | @send_message_block = block 26 | end 27 | 28 | def self.on_start(&block) 29 | if block_given? 30 | @start_block = block 31 | else 32 | @start_block 33 | end 34 | end 35 | 36 | def self.clear_on_start 37 | @start_block = nil 38 | end 39 | 40 | def self.reset 41 | deliveries.clear 42 | on_start 43 | on_send_message 44 | @reset_called = 0 45 | end 46 | 47 | def start(*args) 48 | self.class.on_start.call if self.class.on_start 49 | yield self 50 | end 51 | 52 | alias test_old_reset reset if instance_methods.include? 'reset' 53 | 54 | def reset 55 | self.class.reset_called += 1 56 | end 57 | 58 | alias test_old_send_message send_message 59 | 60 | def send_message(mail, to, from) 61 | return self.class.send_message_block.call(mail, to, from) unless 62 | self.class.send_message_block.nil? 63 | self.class.deliveries << [mail, to, from] 64 | return "queued" 65 | end 66 | 67 | end 68 | 69 | ## 70 | # Stub for ActionMailer::Base 71 | 72 | module ActionMailer; end 73 | 74 | class ActionMailer::Base 75 | 76 | @server_settings = {} 77 | 78 | class << self 79 | cattr_accessor :email_class 80 | attr_accessor :delivery_method 81 | end 82 | 83 | def self.logger 84 | o = Object.new 85 | def o.info(arg) end 86 | return o 87 | end 88 | 89 | def self.method_missing(meth, *args) 90 | meth.to_s =~ /deliver_(.*)/ 91 | super unless $1 92 | new($1, *args).deliver! 93 | end 94 | 95 | def self.reset 96 | server_settings.clear 97 | self.email_class = Email 98 | end 99 | 100 | def self.server_settings 101 | @server_settings 102 | end 103 | 104 | def initialize(meth = nil) 105 | send meth if meth 106 | end 107 | 108 | def deliver! 109 | perform_delivery_activerecord @mail 110 | end 111 | 112 | end 113 | 114 | ## 115 | # Stub for an ActiveRecord model 116 | 117 | class Email 118 | 119 | START = Time.parse 'Thu Aug 10 2006 11:19:48' 120 | 121 | attr_accessor :from, :to, :mail, :last_send_attempt, :created_on, :id 122 | 123 | @records = [] 124 | @id = 0 125 | 126 | class << self; attr_accessor :records, :id; end 127 | 128 | def self.create(record) 129 | record = new record[:from], record[:to], record[:mail], 130 | record[:last_send_attempt] 131 | records << record 132 | return record 133 | end 134 | 135 | def self.destroy_all(conditions) 136 | timeout = conditions.last 137 | found = [] 138 | 139 | records.each do |record| 140 | next if record.last_send_attempt == 0 141 | next if record.created_on == 0 142 | next unless record.created_on < timeout 143 | record.destroy 144 | found << record 145 | end 146 | 147 | found 148 | end 149 | 150 | def self.find(_, conditions = nil) 151 | return records if conditions.nil? 152 | now = Time.now.to_i - 300 153 | return records.select do |r| 154 | r.last_send_attempt < now 155 | end 156 | end 157 | 158 | def self.reset 159 | @id = 0 160 | records.clear 161 | end 162 | 163 | def initialize(from, to, mail, last_send_attempt = nil) 164 | @from = from 165 | @to = to 166 | @mail = mail 167 | @id = self.class.id += 1 168 | @created_on = START + @id 169 | @last_send_attempt = last_send_attempt || 0 170 | end 171 | 172 | def destroy 173 | self.class.records.delete self 174 | self.freeze 175 | end 176 | 177 | def ==(other) 178 | other.id == id 179 | end 180 | 181 | def save 182 | end 183 | 184 | end 185 | 186 | Newsletter = Email 187 | 188 | class String 189 | def classify 190 | self 191 | end 192 | 193 | def tableize 194 | self.downcase 195 | end 196 | 197 | end 198 | -------------------------------------------------------------------------------- /History.txt: -------------------------------------------------------------------------------- 1 | = 2.1.5 2 | 3 | * Bugs fixed 4 | * Load ar_mailer after environment to fix issue with lazy loading of ActionMailer in Rails 2.3 5 | 6 | = 2.1.4 7 | 8 | * Bugs fixed 9 | * Explicitly require ar_mailer in ar_sendmail because its not getting loaded by the Rails environment for some reason 10 | 11 | = 2.1.3 12 | 13 | * Tests now pass on gem install 14 | * Removed deprecated ActionMailer::ARMailer class 15 | * Bugs fixed 16 | * Fixed issue with pre-loading ActionMailer. No use ActionMailer::Base.email_class directly rather than store in ARSendmail instance var so no need to pre-load ActionMailer. 17 | 18 | = 2.1.2 19 | 20 | * Bugs fixed 21 | * Require ar_mailer in ar_sendmail since the change to remove TableName and use email_class 22 | 23 | = 2.1.1 24 | 25 | * Force gem rebuild 26 | 27 | = 2.1.0 28 | 29 | * Switched to using a Rails generator for migration and model files. The ar_sendmail options have been removed. 30 | 31 | = 2.0.2 32 | 33 | * Removed TableName option from ar_sendmail options as its redundant. The Rails environment gets loaded so the settings for email class also get loaded 34 | * Bugs fixed 35 | * Email class reloading issue in development mode causing AR email class defaults to be lost when cached 36 | 37 | = 2.0.1 38 | 39 | * Added option to use smtp setting of :tls => false to disable TLS auto start in Ruby 1.8.7+ 40 | * Removed some cruft which can be handled by ActiveSupport 41 | 42 | = 2.0.0 43 | 44 | * Removed need to use ARMailer subclass. Just set the delivery method and you are ready to go. Backwards compatible with a deprecation notice if you subclass old ARMailer class. 45 | * Only include SMTP TLS patch if Ruby version < 1.8.7 as it has an alternative. Changes based on Calvin Yu's [cyu] fork. 46 | * Renamed default migration name to the modern Rails default 47 | * Only authenticate if emails waiting to be sent 48 | * Added --version switch to ar_sendmail binary 49 | * Created a lighthouse account for this project (adzap fork only). See README. 50 | 51 | = 1.4.4 52 | 53 | * Exit init.d script with message if no mailers defined. 54 | 55 | = 1.4.3 56 | 57 | * Bugs fixed 58 | * Replaced mistaken call to log when removing pid file artifact for 59 | non-running daemon 60 | 61 | = 1.4.2 62 | 63 | * New Features 64 | * Added Ruby based linux init.d script for handling daemon startup using yaml 65 | config file. See files share/linux/ar_sendmail and ar_sendmail.conf 66 | * Bugs fixed 67 | * Proper handling for relative and absolute paths for the pid file 68 | * Removed hoe dependency since we need the explicit gemspec file for github and 69 | not deploying to RubyForge its not as useful. 70 | * Moved old BSD rc.d script to share/bsd folder 71 | * Updated README with github gem install, docs and init script info 72 | 73 | = 1.4.1 74 | 75 | * Bugs fixed 76 | * Daemon failed on startup fixed with expanding full path of pid file 77 | 78 | = 1.4.0 79 | 80 | * Forked gem and published on GitHub (gem sources -a http://gems.github.com) 81 | * New Features 82 | * Added pid file creation on daemonize with command line option to specify pid filename [Dylan Egan] 83 | 84 | = 1.3.1 85 | 86 | * Fix bug #12530, gmail causes SSL errors. Submitted by Kyle Maxwell 87 | and Alex Ostleitner. 88 | * Try ActionMailer::Base::server_settings then ::smtp_settings. Fixes 89 | bug #12516. Submitted by Alex Ostleitner. 90 | 91 | = 1.3.0 92 | 93 | * New Features 94 | * Added automatic mail queue cleanup. 95 | * MAY CAUSE LOSS OF DATA. If you haven't run ar_sendmail within 96 | the expiry time, set it to 0. 97 | * Bugs fixed 98 | * Authentication errors are now handled by retrying once. 99 | 100 | = 1.2.0 101 | 102 | * Bugs fixed 103 | * Handle SMTPServerBusy by backing off @delay seconds then re-queueing 104 | * Allow email delivery class to be set in ARMailer. 105 | * ar_sendmail --mailq works with --table-name now. 106 | * Miscellaneous Updates 107 | * Added documentation to require 'action_mailer/ar_mailer' in 108 | instructions. 109 | * Moved to ZSS p4 repository 110 | * Supports TLS now. Requested by Dave Thomas. smtp_tls.rb from Kyle 111 | Maxwell & etc. 112 | 113 | = 1.1.0 114 | 115 | * Features 116 | * Added --chdir to set rails directory 117 | * Added --environment to set RAILS_ENV 118 | * Exits cleanly on TERM or INT signals 119 | * Added FreeBSD rc.d script 120 | * Exceptions during SMTP sending are now logged 121 | * No longer waits if sending email took too long 122 | * Bugs fixed 123 | * Fixed last send attempt in --mailq 124 | * Better SMTP error handling 125 | * Messages are removed from the queue on 5xx errors 126 | * Added Net::SMTP.reset to avoid needing to recreate the connection 127 | 128 | = 1.0.1 129 | 130 | * Bugs fixed 131 | * From and to of email destination were swapped 132 | 133 | = 1.0.0 134 | 135 | * Birthday 136 | -------------------------------------------------------------------------------- /README.rdoc: -------------------------------------------------------------------------------- 1 | = ar_mailer 2 | 3 | A two-phase delivery agent for ActionMailer 4 | 5 | Rubyforge Project: 6 | 7 | http://rubyforge.org/projects/seattlerb 8 | 9 | Documentation: 10 | 11 | http://seattlerb.org/ar_mailer 12 | 13 | and for forked additions 14 | 15 | http://github.com/adzap/ar_mailer/wikis 16 | 17 | Bugs: 18 | 19 | http://adzap.lighthouseapp.com/projects/26997-ar_mailer 20 | 21 | == About 22 | 23 | Even delivering email to the local machine may take too long when you have to 24 | send hundreds of messages. ar_mailer allows you to store messages into the 25 | database for later delivery by a separate process, ar_sendmail. 26 | 27 | == Installing ar_mailer (forked) 28 | 29 | Before installing you will need to make sure the original gem is uninstalled as they can't coexist: 30 | 31 | $ sudo gem uninstall ar_mailer 32 | 33 | Install the gem from GitHub gems server: 34 | 35 | First, if you haven't already: 36 | 37 | $ sudo gem sources -a http://gems.github.com 38 | 39 | Then 40 | 41 | $ sudo gem install adzap-ar_mailer 42 | 43 | For Rails >= 2.1, in your environment.rb: 44 | 45 | config.gem "adzap-ar_mailer", :lib => 'action_mailer/ar_mailer', :source => 'http://gems.github.com' 46 | 47 | For Rails 2.0, in an initializer file: 48 | 49 | require 'action_mailer/ar_mailer' 50 | 51 | == Usage 52 | 53 | Go to your Rails project: 54 | 55 | $ cd your_rails_project 56 | 57 | Create the migration and model: 58 | 59 | This shows the options which are only the model name, which defaults to Email 60 | 61 | ./script/generate ar_mailer -h 62 | 63 | Then run with defaults 64 | 65 | ./script/generate ar_mailer 66 | 67 | Or specify a custom model name 68 | 69 | ./script/generate ar_mailer Newsletter 70 | 71 | See Alternate Mail Storage if you use a custom model name 72 | 73 | In your mailer class methods you must be sure to set the From address for your emails. 74 | Something like: 75 | 76 | def list_send(recipient) 77 | from 'no_reply@example.com' 78 | # ... 79 | 80 | Edit config/environments/production.rb and set the delivery method: 81 | 82 | config.action_mailer.delivery_method = :activerecord 83 | 84 | Or if you need to, you can set each mailer class delivery method individually: 85 | 86 | class MyMailer < ActionMailer::Base 87 | self.delivery_method = :activerecord 88 | end 89 | 90 | This can be useful when using plugins like ExceptionNotification. Where it 91 | might be foolish to tie the sending of the email alert to the database when the 92 | database might be causing the exception being raised. In this instance you could 93 | override ExceptionNofitier delivery method to be smtp or set the other 94 | mailer classes to use ARMailer explicitly. 95 | 96 | Then to run it: 97 | 98 | $ ar_sendmail 99 | 100 | You can also run it from cron with -o, or as a daemon with -d. 101 | 102 | See ar_sendmail -h for full details. 103 | 104 | === Using the --log-header option 105 | 106 | Ever get complaints from customers who never got that email your system sent? Try this for a strategy: 107 | 108 | 1. When building the email, add the header "X-Delivery-Context: invite-#{invite.id}" 109 | 2. Launch ar_sendmail with --log-header X-Delivery-Context 110 | 3. When emails get sent, you can now associate the email SMTP message id with an record in your DB 111 | 112 | So when customer X did not ever get invite 4, you now have the means of tracking invite 4 and find out exactly what happened. 113 | 114 | === Alternate Mail Storage 115 | 116 | By default ar_mailer assumes you are using an ActiveRecord model called 117 | Email to store the emails created before sending. If you want to change 118 | this you alter it in an intializer like so: 119 | 120 | ActionMailer::Base.email_class = Newsletter 121 | 122 | === A Word on TLS 123 | 124 | If you are using Ruby >= 1.8.7, TLS will be enabled automatically if your 125 | SMTP server supports it. If you do not want it to automatically enabled then 126 | set the :tls option to false in your smtp_settings. 127 | 128 | If you are on Ruby <= 1.8.6, then the TLS patch included in this plugin will 129 | be loaded, so you don't need another TLS plugin to add the capability. This 130 | patch allows you to explicit set if the server supports TLS by setting the 131 | :tls option to true in your smtp_settings. 132 | 133 | === Help 134 | 135 | See ar_sendmail -h for options to ar_sendmail. 136 | 137 | NOTE: You may need to delete an smtp_tls.rb file if you have one lying 138 | around. ar_mailer supplies it own. 139 | 140 | == Run as a service (init.d/rc.d scripts) 141 | 142 | For Linux both script and demo config files are in share/linux. 143 | See ar_sendmail.conf for setting up your config. Copy the ar_sendmail file 144 | to /etc/init.d/ and make it executable. Then for Debian based distros run 145 | 'sudo update-rc.d ar_sendmail defaults' and it should work. Make sure you have 146 | the config file /etc/ar_sendmail.conf in place before starting. 147 | 148 | For FreeBSD or NetBSD script is share/bsd/ar_sendmail. This is old and does not 149 | support the config file unless someone wants to submit a patch. 150 | -------------------------------------------------------------------------------- /lib/action_mailer/ar_sendmail.rb: -------------------------------------------------------------------------------- 1 | require 'optparse' 2 | require 'net/smtp' 3 | require 'smtp_tls' unless Net::SMTP.instance_methods.include?("enable_starttls_auto") 4 | require 'ar_sendmail_logger' 5 | require 'rubygems' 6 | 7 | ## 8 | # Hack in RSET 9 | 10 | module Net # :nodoc: 11 | class SMTP # :nodoc: 12 | 13 | unless instance_methods.include? 'reset' then 14 | ## 15 | # Resets the SMTP connection. 16 | 17 | def reset 18 | getok 'RSET' 19 | end 20 | end 21 | 22 | end 23 | end 24 | 25 | ## 26 | # ActionMailer::ARSendmail delivers email from the email table to the 27 | # SMTP server configured in your application's config/environment.rb. 28 | # ar_sendmail does not work with sendmail delivery. 29 | # 30 | # ar_mailer can deliver to SMTP with TLS using smtp_tls.rb borrowed from Kyle 31 | # Maxwell's action_mailer_optional_tls plugin. Simply set the :tls option in 32 | # ActionMailer::Base's smtp_settings to true to enable TLS. 33 | # 34 | # See ar_sendmail -h for the full list of supported options. 35 | # 36 | # The interesting options are: 37 | # * --daemon 38 | # * --mailq 39 | 40 | module ActionMailer; end 41 | 42 | class ActionMailer::ARSendmail 43 | 44 | ## 45 | # The version of ActionMailer::ARSendmail you are running. 46 | 47 | VERSION = '2.1.7' 48 | 49 | ## 50 | # Maximum number of times authentication will be consecutively retried 51 | 52 | MAX_AUTH_FAILURES = 2 53 | 54 | ## 55 | # Email delivery attempts per run 56 | 57 | attr_accessor :batch_size 58 | 59 | ## 60 | # Seconds to delay between runs 61 | 62 | attr_accessor :delay 63 | 64 | ## 65 | # Maximum age of emails in seconds before they are removed from the queue. 66 | 67 | attr_accessor :max_age 68 | 69 | ## 70 | # Be verbose 71 | 72 | attr_accessor :verbose 73 | 74 | 75 | ## 76 | # True if only one delivery attempt will be made per call to run 77 | 78 | attr_reader :once 79 | 80 | ## 81 | # Times authentication has failed 82 | 83 | attr_accessor :failed_auth_count 84 | 85 | ## 86 | # Logs the value of this header for successfully sent emails 87 | 88 | attr_accessor :log_header 89 | 90 | @@pid_file = nil 91 | 92 | def self.remove_pid_file 93 | if @@pid_file 94 | require 'shell' 95 | sh = Shell.new 96 | sh.rm @@pid_file 97 | end 98 | end 99 | 100 | ## 101 | # Prints a list of unsent emails and the last delivery attempt, if any. 102 | # 103 | # If ActiveRecord::Timestamp is not being used the arrival time will not be 104 | # known. See http://api.rubyonrails.org/classes/ActiveRecord/Timestamp.html 105 | # to learn how to enable ActiveRecord::Timestamp. 106 | 107 | def self.mailq 108 | emails = ActionMailer::Base.email_class.find :all 109 | 110 | if emails.empty? then 111 | puts "Mail queue is empty" 112 | return 113 | end 114 | 115 | total_size = 0 116 | 117 | puts "-Queue ID- --Size-- ----Arrival Time---- -Sender/Recipient-------" 118 | emails.each do |email| 119 | size = email.mail.length 120 | total_size += size 121 | 122 | create_timestamp = email.created_on rescue 123 | email.created_at rescue 124 | Time.at(email.created_date) rescue # for Robot Co-op 125 | nil 126 | 127 | created = if create_timestamp.nil? then 128 | ' Unknown' 129 | else 130 | create_timestamp.strftime '%a %b %d %H:%M:%S' 131 | end 132 | 133 | puts "%10d %8d %s %s" % [email.id, size, created, email.from] 134 | if email.last_send_attempt > 0 then 135 | puts "Last send attempt: #{Time.at email.last_send_attempt}" 136 | end 137 | puts " #{email.to}" 138 | puts 139 | end 140 | 141 | puts "-- #{total_size/1024} Kbytes in #{emails.length} Requests." 142 | end 143 | 144 | ## 145 | # Processes command line options in +args+ 146 | 147 | def self.process_args(args) 148 | name = File.basename $0 149 | 150 | options = {} 151 | options[:Chdir] = '.' 152 | options[:Daemon] = false 153 | options[:Delay] = 60 154 | options[:MaxAge] = 86400 * 7 155 | options[:Once] = false 156 | options[:RailsEnv] = ENV['RAILS_ENV'] 157 | options[:Pidfile] = options[:Chdir] + '/log/ar_sendmail.pid' 158 | 159 | opts = OptionParser.new do |opts| 160 | opts.banner = "Usage: #{name} [options]" 161 | opts.separator '' 162 | 163 | opts.separator "#{name} scans the email table for new messages and sends them to the" 164 | opts.separator "website's configured SMTP host." 165 | opts.separator '' 166 | opts.separator "#{name} must be run from a Rails application's root." 167 | 168 | opts.separator '' 169 | opts.separator 'Sendmail options:' 170 | 171 | opts.on("-b", "--batch-size BATCH_SIZE", 172 | "Maximum number of emails to send per delay", 173 | "Default: Deliver all available emails", Integer) do |batch_size| 174 | options[:BatchSize] = batch_size 175 | end 176 | 177 | opts.on( "--delay DELAY", 178 | "Delay between checks for new mail", 179 | "in the database", 180 | "Default: #{options[:Delay]}", Integer) do |delay| 181 | options[:Delay] = delay 182 | end 183 | 184 | opts.on( "--max-age MAX_AGE", 185 | "Maxmimum age for an email. After this", 186 | "it will be removed from the queue.", 187 | "Set to 0 to disable queue cleanup.", 188 | "Default: #{options[:MaxAge]} seconds", Integer) do |max_age| 189 | options[:MaxAge] = max_age 190 | end 191 | 192 | opts.on("-o", "--once", 193 | "Only check for new mail and deliver once", 194 | "Default: #{options[:Once]}") do |once| 195 | options[:Once] = once 196 | end 197 | 198 | opts.on("-d", "--daemonize", 199 | "Run as a daemon process", 200 | "Default: #{options[:Daemon]}") do |daemon| 201 | options[:Daemon] = true 202 | end 203 | 204 | opts.on("-p", "--pidfile PIDFILE", 205 | "Set the pidfile location", 206 | "Default: #{options[:Chdir]}#{options[:Pidfile]}", String) do |pidfile| 207 | options[:Pidfile] = pidfile 208 | end 209 | 210 | opts.on( "--mailq", 211 | "Display a list of emails waiting to be sent") do |mailq| 212 | options[:MailQ] = true 213 | end 214 | 215 | opts.separator '' 216 | opts.separator 'Setup Options:' 217 | 218 | opts.separator '' 219 | opts.separator 'Generic Options:' 220 | 221 | opts.on("-c", "--chdir PATH", 222 | "Use PATH for the application path", 223 | "Default: #{options[:Chdir]}") do |path| 224 | usage opts, "#{path} is not a directory" unless File.directory? path 225 | usage opts, "#{path} is not readable" unless File.readable? path 226 | options[:Chdir] = path 227 | end 228 | 229 | opts.on( "--log-file PATH_AND_FILE", 230 | "Full path to the file the mailer should log to", 231 | "Default: RAILS_ROOT/log/RAILS_ENV.log") do |log_file| 232 | options[:LogFile] = log_file 233 | end 234 | 235 | opts.on( "--log-header SOME_HEADER", 236 | "Logs the value of the specified header once an email gets sent") do |header| 237 | options[:LogHeader] = header 238 | end 239 | 240 | opts.on("-e", "--environment RAILS_ENV", 241 | "Set the RAILS_ENV constant", 242 | "Default: #{options[:RailsEnv]}") do |env| 243 | options[:RailsEnv] = env 244 | end 245 | 246 | opts.on("-v", "--[no-]verbose", 247 | "Be verbose", 248 | "Default: #{options[:Verbose]}") do |verbose| 249 | options[:Verbose] = verbose 250 | end 251 | 252 | opts.on("-h", "--help", 253 | "You're looking at it") do 254 | usage opts 255 | end 256 | 257 | opts.on("--version", "Version of ARMailer") do 258 | usage "ar_mailer #{VERSION} (adzap fork)" 259 | end 260 | 261 | opts.separator '' 262 | end 263 | 264 | opts.parse! args 265 | 266 | ENV['RAILS_ENV'] = options[:RailsEnv] 267 | 268 | Dir.chdir options[:Chdir] do 269 | begin 270 | require 'config/environment' 271 | require 'action_mailer/ar_mailer' 272 | rescue LoadError 273 | usage opts, <<-EOF 274 | #{name} must be run from a Rails application's root to deliver email. 275 | #{Dir.pwd} does not appear to be a Rails application root. 276 | EOF 277 | end 278 | end 279 | 280 | return options 281 | end 282 | 283 | ## 284 | # Processes +args+ and runs as appropriate 285 | 286 | def self.run(args = ARGV) 287 | options = process_args args 288 | 289 | if options.include? :MailQ then 290 | mailq 291 | exit 292 | end 293 | 294 | if options[:Daemon] then 295 | require 'webrick/server' 296 | @@pid_file = File.expand_path(options[:Pidfile], options[:Chdir]) 297 | if File.exists? @@pid_file 298 | # check to see if process is actually running 299 | pid = '' 300 | File.open(@@pid_file, 'r') {|f| pid = f.read.chomp } 301 | if system("ps -p #{pid} | grep #{pid}") # returns true if process is running, o.w. false 302 | $stderr.puts "Warning: The pid file #{@@pid_file} exists and ar_sendmail is running. Shutting down." 303 | exit 304 | else 305 | # not running, so remove existing pid file and continue 306 | self.remove_pid_file 307 | $stderr.puts "ar_sendmail is not running. Removing existing pid file and starting up..." 308 | end 309 | end 310 | WEBrick::Daemon.start 311 | File.open(@@pid_file, 'w') {|f| f.write("#{Process.pid}\n")} 312 | end 313 | 314 | new(options).run 315 | 316 | rescue SystemExit 317 | raise 318 | rescue SignalException 319 | exit 320 | rescue Exception => e 321 | $stderr.puts "Unhandled exception #{e.message}(#{e.class}):" 322 | $stderr.puts "\t#{e.backtrace.join "\n\t"}" 323 | exit 1 324 | end 325 | 326 | ## 327 | # Prints a usage message to $stderr using +opts+ and exits 328 | 329 | def self.usage(opts, message = nil) 330 | if message then 331 | $stderr.puts message 332 | $stderr.puts 333 | end 334 | 335 | $stderr.puts opts 336 | exit 1 337 | end 338 | 339 | ## 340 | # Creates a new ARSendmail. 341 | # 342 | # Valid options are: 343 | # :BatchSize:: Maximum number of emails to send per delay 344 | # :Delay:: Delay between deliver attempts 345 | # :Once:: Only attempt to deliver emails once when run is called 346 | # :Verbose:: Be verbose. 347 | 348 | def initialize(options = {}) 349 | options[:Delay] ||= 60 350 | options[:MaxAge] ||= 86400 * 7 351 | 352 | @batch_size = options[:BatchSize] 353 | @delay = options[:Delay] 354 | @once = options[:Once] 355 | @verbose = options[:Verbose] 356 | @max_age = options[:MaxAge] 357 | @log_header = options[:LogHeader] 358 | 359 | @failed_auth_count = 0 360 | @logger = options[:LogFile] ? ARSendmailLogger.new(options[:LogFile]) : ActionMailer::Base.logger 361 | end 362 | 363 | ## 364 | # Removes emails that have lived in the queue for too long. If max_age is 365 | # set to 0, no emails will be removed. 366 | 367 | def cleanup 368 | return if @max_age == 0 369 | timeout = Time.now - @max_age 370 | conditions = ['last_send_attempt > 0 and created_on < ?', timeout] 371 | mail = ActionMailer::Base.email_class.destroy_all conditions 372 | 373 | log "expired #{mail.length} emails from the queue" 374 | end 375 | 376 | ## 377 | # Delivers +emails+ to ActionMailer's SMTP server and destroys them. 378 | 379 | def deliver(emails) 380 | settings = [ 381 | smtp_settings[:domain], 382 | (smtp_settings[:user] || smtp_settings[:user_name]), 383 | smtp_settings[:password], 384 | smtp_settings[:authentication] 385 | ] 386 | 387 | smtp = Net::SMTP.new(smtp_settings[:address], smtp_settings[:port]) 388 | if smtp.respond_to?(:enable_starttls_auto) 389 | smtp.enable_starttls_auto unless smtp_settings[:tls] == false 390 | else 391 | settings << smtp_settings[:tls] 392 | end 393 | 394 | smtp.start(*settings) do |session| 395 | @failed_auth_count = 0 396 | until emails.empty? do 397 | email = emails.shift 398 | if locking_enabled? 399 | next unless email.lock_with_expiry 400 | end 401 | 402 | begin 403 | res = session.send_message email.mail, email.from, email.to 404 | hdr = '' 405 | 406 | if @log_header && email.mail =~ /#{@log_header}: (.+)/ 407 | hdr = "[#{$1.chomp}] " 408 | end 409 | 410 | email.destroy 411 | log "sent email %011d %sfrom %s to %s: %p" % 412 | [email.id, hdr, email.from, email.to, res] 413 | rescue Net::SMTPFatalError => e 414 | log "5xx error sending email %d, removing from queue: %p(%s):\n\t%s" % 415 | [email.id, e.message, e.class, e.backtrace.join("\n\t")] 416 | email.destroy 417 | session.reset 418 | rescue Net::SMTPServerBusy => e 419 | log "server too busy, sleeping #{@delay} seconds" 420 | sleep delay 421 | return 422 | rescue Net::SMTPUnknownError, Net::SMTPSyntaxError, TimeoutError => e 423 | email.last_send_attempt = Time.now.to_i 424 | email.save rescue nil 425 | log "error sending email %d: %p(%s):\n\t%s" % 426 | [email.id, e.message, e.class, e.backtrace.join("\n\t")] 427 | session.reset 428 | end 429 | 430 | if locking_enabled? 431 | email.unlock 432 | end 433 | end 434 | end 435 | rescue Net::SMTPAuthenticationError => e 436 | @failed_auth_count += 1 437 | if @failed_auth_count >= MAX_AUTH_FAILURES then 438 | log "authentication error, giving up: #{e.message}" 439 | raise e 440 | else 441 | log "authentication error, retrying: #{e.message}" 442 | end 443 | sleep delay 444 | rescue Net::SMTPServerBusy, SystemCallError, OpenSSL::SSL::SSLError 445 | # ignore SMTPServerBusy/EPIPE/ECONNRESET from Net::SMTP.start's ensure 446 | end 447 | 448 | def locking_enabled? 449 | @locking_enabled ||= ActionMailer::Base.email_class.instance_methods.include?('lock_with_expiry') 450 | end 451 | 452 | ## 453 | # Prepares ar_sendmail for exiting 454 | 455 | def do_exit 456 | log "caught signal, shutting down" 457 | self.class.remove_pid_file 458 | exit 459 | end 460 | 461 | ## 462 | # Returns emails in email_class that haven't had a delivery attempt in the 463 | # last 300 seconds. 464 | 465 | def find_emails 466 | options = { :conditions => ['last_send_attempt < ?', Time.now.to_i - 300] } 467 | options[:limit] = batch_size unless batch_size.nil? 468 | mail = email_finder(options) 469 | 470 | log "found #{mail.length} emails to send" 471 | mail 472 | end 473 | 474 | def email_finder(options) 475 | if locking_enabled? 476 | ActionMailer::Base.email_class.unlocked.all(options) 477 | else 478 | ActionMailer::Base.email_class.find(:all, options) 479 | end 480 | end 481 | 482 | ## 483 | # Installs signal handlers to gracefully exit. 484 | 485 | def install_signal_handlers 486 | trap 'TERM' do do_exit end 487 | trap 'INT' do do_exit end 488 | end 489 | 490 | ## 491 | # Logs +message+ if verbose 492 | 493 | def log(message) 494 | message = "ar_sendmail #{Time.now}: #{message}" 495 | $stderr.puts message if @verbose 496 | @logger.info message 497 | end 498 | 499 | ## 500 | # Scans for emails and delivers them every delay seconds. Only returns if 501 | # once is true. 502 | 503 | def run 504 | install_signal_handlers 505 | 506 | loop do 507 | now = Time.now 508 | begin 509 | cleanup 510 | emails = find_emails 511 | deliver(emails) unless emails.empty? 512 | rescue ActiveRecord::Transactions::TransactionError 513 | end 514 | break if @once 515 | sleep @delay if now + @delay > Time.now 516 | end 517 | end 518 | 519 | ## 520 | # Proxy to ActionMailer::Base::smtp_settings. See 521 | # http://api.rubyonrails.org/classes/ActionMailer/Base.html 522 | # for instructions on how to configure ActionMailer's SMTP server. 523 | # 524 | # Falls back to ::server_settings if ::smtp_settings doesn't exist for 525 | # backwards compatibility. 526 | 527 | def smtp_settings 528 | ActionMailer::Base.smtp_settings rescue ActionMailer::Base.server_settings 529 | end 530 | 531 | end 532 | -------------------------------------------------------------------------------- /test/test_arsendmail.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + '/test_helper') 2 | 3 | class ActionMailer::ARSendmail 4 | attr_accessor :slept 5 | def sleep(secs) 6 | @slept ||= [] 7 | @slept << secs 8 | end 9 | end 10 | 11 | class TestARSendmail < MiniTest::Unit::TestCase 12 | 13 | def setup 14 | ActionMailer::Base.reset 15 | Email.reset 16 | Net::SMTP.reset 17 | 18 | @sm = ActionMailer::ARSendmail.new 19 | @sm.verbose = true 20 | 21 | Net::SMTP.clear_on_start 22 | 23 | @include_c_e = ! $".grep(/config\/environment.rb/).empty? 24 | $" << 'config/environment.rb' unless @include_c_e 25 | end 26 | 27 | def teardown 28 | $".delete 'config/environment.rb' unless @include_c_e 29 | end 30 | 31 | def strip_log_prefix(line) 32 | line.gsub(/ar_sendmail .+ \d{4}: /,'') 33 | end 34 | 35 | def test_class_mailq 36 | Email.create :from => nobody, :to => 'recip@h1.example.com', 37 | :mail => 'body0' 38 | Email.create :from => nobody, :to => 'recip@h1.example.com', 39 | :mail => 'body1' 40 | last = Email.create :from => nobody, :to => 'recip@h2.example.com', 41 | :mail => 'body2' 42 | last_attempt_time = Time.parse('Thu Aug 10 2006 11:40:05') 43 | last.last_send_attempt = last_attempt_time.to_i 44 | 45 | out, err = capture_io do 46 | ActionMailer::ARSendmail.mailq 47 | end 48 | 49 | expected = <<-EOF 50 | -Queue ID- --Size-- ----Arrival Time---- -Sender/Recipient------- 51 | 1 5 Thu Aug 10 11:19:49 nobody@example.com 52 | recip@h1.example.com 53 | 54 | 2 5 Thu Aug 10 11:19:50 nobody@example.com 55 | recip@h1.example.com 56 | 57 | 3 5 Thu Aug 10 11:19:51 nobody@example.com 58 | Last send attempt: Thu Aug 10 11:40:05 %s 2006 59 | recip@h2.example.com 60 | 61 | -- 0 Kbytes in 3 Requests. 62 | EOF 63 | 64 | expected = expected % last_attempt_time.strftime('%z') 65 | assert_equal expected, out 66 | end 67 | 68 | def test_class_mailq_empty 69 | out, err = capture_io do 70 | ActionMailer::ARSendmail.mailq 71 | end 72 | 73 | assert_equal "Mail queue is empty\n", out 74 | end 75 | 76 | def test_class_new 77 | @sm = ActionMailer::ARSendmail.new 78 | 79 | assert_equal 60, @sm.delay 80 | assert_equal nil, @sm.once 81 | assert_equal nil, @sm.verbose 82 | assert_equal nil, @sm.batch_size 83 | 84 | @sm = ActionMailer::ARSendmail.new :Delay => 75, :Verbose => true, 85 | :Once => true, :BatchSize => 1000 86 | 87 | assert_equal 75, @sm.delay 88 | assert_equal true, @sm.once 89 | assert_equal true, @sm.verbose 90 | assert_equal 1000, @sm.batch_size 91 | end 92 | 93 | def test_class_parse_args_batch_size 94 | options = ActionMailer::ARSendmail.process_args %w[-b 500] 95 | 96 | assert_equal 500, options[:BatchSize] 97 | 98 | options = ActionMailer::ARSendmail.process_args %w[--batch-size 500] 99 | 100 | assert_equal 500, options[:BatchSize] 101 | end 102 | 103 | def test_class_parse_args_chdir 104 | argv = %w[-c /tmp] 105 | 106 | options = ActionMailer::ARSendmail.process_args argv 107 | 108 | assert_equal '/tmp', options[:Chdir] 109 | 110 | argv = %w[--chdir /tmp] 111 | 112 | options = ActionMailer::ARSendmail.process_args argv 113 | 114 | assert_equal '/tmp', options[:Chdir] 115 | 116 | argv = %w[-c /nonexistent] 117 | 118 | out, err = capture_io do 119 | assert_raises SystemExit do 120 | ActionMailer::ARSendmail.process_args argv 121 | end 122 | end 123 | end 124 | 125 | def test_class_parse_args_daemon 126 | argv = %w[-d] 127 | 128 | options = ActionMailer::ARSendmail.process_args argv 129 | 130 | assert_equal true, options[:Daemon] 131 | 132 | argv = %w[--daemon] 133 | 134 | options = ActionMailer::ARSendmail.process_args argv 135 | 136 | assert_equal true, options[:Daemon] 137 | end 138 | 139 | def test_class_parse_args_pidfile 140 | argv = %w[-p ./log/ar_sendmail.pid] 141 | 142 | options = ActionMailer::ARSendmail.process_args argv 143 | 144 | assert_equal './log/ar_sendmail.pid', options[:Pidfile] 145 | 146 | argv = %w[--pidfile ./log/ar_sendmail.pid] 147 | 148 | options = ActionMailer::ARSendmail.process_args argv 149 | 150 | assert_equal './log/ar_sendmail.pid', options[:Pidfile] 151 | end 152 | 153 | def test_class_parse_args_delay 154 | argv = %w[--delay 75] 155 | 156 | options = ActionMailer::ARSendmail.process_args argv 157 | 158 | assert_equal 75, options[:Delay] 159 | end 160 | 161 | def test_class_parse_args_environment 162 | assert_equal nil, ENV['RAILS_ENV'] 163 | 164 | argv = %w[-e production] 165 | 166 | options = ActionMailer::ARSendmail.process_args argv 167 | 168 | assert_equal 'production', options[:RailsEnv] 169 | 170 | assert_equal 'production', ENV['RAILS_ENV'] 171 | 172 | argv = %w[--environment production] 173 | 174 | options = ActionMailer::ARSendmail.process_args argv 175 | 176 | assert_equal 'production', options[:RailsEnv] 177 | end 178 | 179 | def test_class_parse_args_mailq 180 | options = ActionMailer::ARSendmail.process_args [] 181 | refute_includes options, :MailQ 182 | 183 | argv = %w[--mailq] 184 | 185 | options = ActionMailer::ARSendmail.process_args argv 186 | 187 | assert_equal true, options[:MailQ] 188 | end 189 | 190 | def test_class_parse_args_max_age 191 | options = ActionMailer::ARSendmail.process_args [] 192 | assert_equal 86400 * 7, options[:MaxAge] 193 | 194 | argv = %w[--max-age 86400] 195 | 196 | options = ActionMailer::ARSendmail.process_args argv 197 | 198 | assert_equal 86400, options[:MaxAge] 199 | end 200 | 201 | def test_class_parse_args_no_config_environment 202 | $".delete 'config/environment.rb' 203 | 204 | out, err = capture_io do 205 | assert_raises SystemExit do 206 | ActionMailer::ARSendmail.process_args [] 207 | end 208 | end 209 | 210 | ensure 211 | $" << 'config/environment.rb' if @include_c_e 212 | end 213 | 214 | def test_class_parse_args_once 215 | argv = %w[-o] 216 | 217 | options = ActionMailer::ARSendmail.process_args argv 218 | 219 | assert_equal true, options[:Once] 220 | 221 | argv = %w[--once] 222 | 223 | options = ActionMailer::ARSendmail.process_args argv 224 | 225 | assert_equal true, options[:Once] 226 | end 227 | 228 | def test_class_usage 229 | out, err = capture_io do 230 | assert_raises SystemExit do 231 | ActionMailer::ARSendmail.usage 'opts' 232 | end 233 | end 234 | 235 | assert_equal '', out 236 | assert_equal "opts\n", strip_log_prefix(err) 237 | 238 | out, err = capture_io do 239 | assert_raises SystemExit do 240 | ActionMailer::ARSendmail.usage 'opts', 'hi' 241 | end 242 | end 243 | 244 | assert_equal '', out 245 | assert_equal "hi\n\nopts\n", strip_log_prefix(err) 246 | end 247 | 248 | def test_cleanup 249 | e1 = Email.create :mail => 'body', :to => 'to', :from => 'from' 250 | e1.created_on = Time.now 251 | e2 = Email.create :mail => 'body', :to => 'to', :from => 'from' 252 | e3 = Email.create :mail => 'body', :to => 'to', :from => 'from' 253 | e3.last_send_attempt = Time.now 254 | 255 | out, err = capture_io do 256 | @sm.cleanup 257 | end 258 | 259 | assert_equal '', out 260 | assert_equal "expired 1 emails from the queue\n", strip_log_prefix(err) 261 | assert_equal 2, Email.records.length 262 | 263 | assert_equal [e1, e2], Email.records 264 | end 265 | 266 | def test_cleanup_disabled 267 | e1 = Email.create :mail => 'body', :to => 'to', :from => 'from' 268 | e1.created_on = Time.now 269 | e2 = Email.create :mail => 'body', :to => 'to', :from => 'from' 270 | 271 | @sm.max_age = 0 272 | 273 | out, err = capture_io do 274 | @sm.cleanup 275 | end 276 | 277 | assert_equal '', out 278 | assert_equal 2, Email.records.length 279 | end 280 | 281 | def test_deliver 282 | email = Email.create :mail => 'body', :to => 'to', :from => 'from' 283 | 284 | out, err = capture_io do 285 | @sm.deliver [email] 286 | end 287 | 288 | assert_equal 1, Net::SMTP.deliveries.length 289 | assert_equal ['body', 'from', 'to'], Net::SMTP.deliveries.first 290 | assert_equal 0, Email.records.length 291 | assert_equal 0, Net::SMTP.reset_called, 'Reset connection on SyntaxError' 292 | 293 | assert_equal '', out 294 | assert_equal "sent email 00000000001 from from to to: \"queued\"\n", strip_log_prefix(err) 295 | end 296 | 297 | def test_log_header_setting 298 | email = Email.create :mail => "Mailer: JunkMail 1.0\r\nX-Track: 7890\r\n\r\nbody", :to => 'to', :from => 'from' 299 | @sm.log_header = 'X-Track' 300 | out, err = capture_io do 301 | @sm.deliver [email] 302 | end 303 | 304 | assert_equal 1, Net::SMTP.deliveries.length 305 | assert_equal 0, Email.records.length 306 | assert_equal 0, Net::SMTP.reset_called, 'Reset connection on SyntaxError' 307 | 308 | assert_equal '', out 309 | assert_equal "sent email 00000000001 [7890] from from to to: \"queued\"\n", strip_log_prefix(err) 310 | end 311 | 312 | def test_deliver_not_called_when_no_emails 313 | sm = ActionMailer::ARSendmail.new({:Once => true}) 314 | sm.expects(:deliver).never 315 | sm.run 316 | end 317 | 318 | def test_deliver_auth_error 319 | Net::SMTP.on_start do 320 | e = Net::SMTPAuthenticationError.new 'try again' 321 | e.set_backtrace %w[one two three] 322 | raise e 323 | end 324 | 325 | now = Time.now.to_i 326 | 327 | email = Email.create :mail => 'body', :to => 'to', :from => 'from' 328 | 329 | out, err = capture_io do 330 | @sm.deliver [email] 331 | end 332 | 333 | assert_equal 0, Net::SMTP.deliveries.length 334 | assert_equal 1, Email.records.length 335 | assert_equal 0, Email.records.first.last_send_attempt 336 | assert_equal 0, Net::SMTP.reset_called 337 | assert_equal 1, @sm.failed_auth_count 338 | assert_equal [60], @sm.slept 339 | 340 | assert_equal '', out 341 | assert_equal "authentication error, retrying: try again\n", strip_log_prefix(err) 342 | end 343 | 344 | def test_deliver_auth_error_recover 345 | email = Email.create :mail => 'body', :to => 'to', :from => 'from' 346 | @sm.failed_auth_count = 1 347 | 348 | out, err = capture_io do @sm.deliver [email] end 349 | 350 | assert_equal 0, @sm.failed_auth_count 351 | assert_equal 1, Net::SMTP.deliveries.length 352 | end 353 | 354 | def test_deliver_auth_error_twice 355 | Net::SMTP.on_start do 356 | e = Net::SMTPAuthenticationError.new 'try again' 357 | e.set_backtrace %w[one two three] 358 | raise e 359 | end 360 | 361 | @sm.failed_auth_count = 1 362 | 363 | out, err = capture_io do 364 | assert_raises Net::SMTPAuthenticationError do 365 | @sm.deliver [] 366 | end 367 | end 368 | 369 | assert_equal 2, @sm.failed_auth_count 370 | assert_equal "authentication error, giving up: try again\n", strip_log_prefix(err) 371 | end 372 | 373 | def test_deliver_4xx_error 374 | Net::SMTP.on_send_message do 375 | e = Net::SMTPSyntaxError.new 'try again' 376 | e.set_backtrace %w[one two three] 377 | raise e 378 | end 379 | 380 | now = Time.now.to_i 381 | 382 | email = Email.create :mail => 'body', :to => 'to', :from => 'from' 383 | 384 | out, err = capture_io do 385 | @sm.deliver [email] 386 | end 387 | 388 | assert_equal 0, Net::SMTP.deliveries.length 389 | assert_equal 1, Email.records.length 390 | assert_operator now, :<=, Email.records.first.last_send_attempt 391 | assert_equal 1, Net::SMTP.reset_called, 'Reset connection on SyntaxError' 392 | 393 | assert_equal '', out 394 | assert_equal "error sending email 1: \"try again\"(Net::SMTPSyntaxError):\n\tone\n\ttwo\n\tthree\n", strip_log_prefix(err) 395 | end 396 | 397 | def test_deliver_5xx_error 398 | Net::SMTP.on_send_message do 399 | e = Net::SMTPFatalError.new 'unknown recipient' 400 | e.set_backtrace %w[one two three] 401 | raise e 402 | end 403 | 404 | now = Time.now.to_i 405 | 406 | email = Email.create :mail => 'body', :to => 'to', :from => 'from' 407 | 408 | out, err = capture_io do 409 | @sm.deliver [email] 410 | end 411 | 412 | assert_equal 0, Net::SMTP.deliveries.length 413 | assert_equal 0, Email.records.length 414 | assert_equal 1, Net::SMTP.reset_called, 'Reset connection on SyntaxError' 415 | 416 | assert_equal '', out 417 | assert_equal "5xx error sending email 1, removing from queue: \"unknown recipient\"(Net::SMTPFatalError):\n\tone\n\ttwo\n\tthree\n", strip_log_prefix(err) 418 | end 419 | 420 | def test_deliver_errno_epipe 421 | Net::SMTP.on_send_message do 422 | raise Errno::EPIPE 423 | end 424 | 425 | now = Time.now.to_i 426 | 427 | email = Email.create :mail => 'body', :to => 'to', :from => 'from' 428 | 429 | out, err = capture_io do 430 | @sm.deliver [email] 431 | end 432 | 433 | assert_equal 0, Net::SMTP.deliveries.length 434 | assert_equal 1, Email.records.length 435 | assert_operator now, :>=, Email.records.first.last_send_attempt 436 | assert_equal 0, Net::SMTP.reset_called, 'Reset connection on SyntaxError' 437 | 438 | assert_equal '', out 439 | assert_equal '', err 440 | end 441 | 442 | def test_deliver_server_busy 443 | Net::SMTP.on_send_message do 444 | e = Net::SMTPServerBusy.new 'try again' 445 | e.set_backtrace %w[one two three] 446 | raise e 447 | end 448 | 449 | now = Time.now.to_i 450 | 451 | email = Email.create :mail => 'body', :to => 'to', :from => 'from' 452 | 453 | out, err = capture_io do 454 | @sm.deliver [email] 455 | end 456 | 457 | assert_equal 0, Net::SMTP.deliveries.length 458 | assert_equal 1, Email.records.length 459 | assert_operator now, :>=, Email.records.first.last_send_attempt 460 | assert_equal 0, Net::SMTP.reset_called, 'Reset connection on SyntaxError' 461 | assert_equal [60], @sm.slept 462 | 463 | assert_equal '', out 464 | assert_equal "server too busy, sleeping 60 seconds\n", strip_log_prefix(err) 465 | end 466 | 467 | def test_deliver_syntax_error 468 | Net::SMTP.on_send_message do 469 | Net::SMTP.on_send_message # clear 470 | e = Net::SMTPSyntaxError.new 'blah blah blah' 471 | e.set_backtrace %w[one two three] 472 | raise e 473 | end 474 | 475 | now = Time.now.to_i 476 | 477 | email1 = Email.create :mail => 'body', :to => 'to', :from => 'from' 478 | email2 = Email.create :mail => 'body', :to => 'to', :from => 'from' 479 | 480 | out, err = capture_io do 481 | @sm.deliver [email1, email2] 482 | end 483 | 484 | assert_equal 1, Net::SMTP.deliveries.length, 'delivery count' 485 | assert_equal 1, Email.records.length 486 | assert_equal 1, Net::SMTP.reset_called, 'Reset connection on SyntaxError' 487 | assert_operator now, :<=, Email.records.first.last_send_attempt 488 | 489 | assert_equal '', out 490 | 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", strip_log_prefix(err) 491 | end 492 | 493 | def test_deliver_timeout 494 | Net::SMTP.on_send_message do 495 | e = Timeout::Error.new 'timed out' 496 | e.set_backtrace %w[one two three] 497 | raise e 498 | end 499 | 500 | now = Time.now.to_i 501 | 502 | email = Email.create :mail => 'body', :to => 'to', :from => 'from' 503 | 504 | out, err = capture_io do 505 | @sm.deliver [email] 506 | end 507 | 508 | assert_equal 0, Net::SMTP.deliveries.length 509 | assert_equal 1, Email.records.length 510 | assert_operator now, :>=, Email.records.first.last_send_attempt 511 | assert_equal 1, Net::SMTP.reset_called, 'Reset connection on Timeout' 512 | 513 | assert_equal '', out 514 | assert_equal "error sending email 1: \"timed out\"(Timeout::Error):\n\tone\n\ttwo\n\tthree\n", strip_log_prefix(err) 515 | end 516 | 517 | def test_deliver_without_locking 518 | email = Email.create :mail => 'body', :to => 'to', :from => 'from' 519 | assert_equal false, @sm.locking_enabled? 520 | out, err = capture_io do 521 | @sm.deliver([ email ]) 522 | end 523 | 524 | assert_equal 1, Net::SMTP.deliveries.length 525 | end 526 | 527 | def test_deliver_with_locking_acquired 528 | email = Email.create :mail => 'body', :to => 'to', :from => 'from' 529 | Email.any_instance.stubs(:lock_with_expiry).returns(true) 530 | Email.any_instance.expects(:unlock) 531 | assert_equal true, @sm.locking_enabled? 532 | out, err = capture_io do 533 | @sm.deliver([ email ]) 534 | end 535 | 536 | assert_equal 1, Net::SMTP.deliveries.length 537 | end 538 | 539 | def test_deliver_with_locking_not_acquired 540 | email = Email.create :mail => 'body', :to => 'to', :from => 'from' 541 | Email.any_instance.stubs(:lock_with_expiry).returns(false) 542 | Email.any_instance.expects(:unlock).never 543 | assert_equal true, @sm.locking_enabled? 544 | out, err = capture_io do 545 | @sm.deliver([ email ]) 546 | end 547 | 548 | assert_equal 0, Net::SMTP.deliveries.length 549 | end 550 | 551 | def test_do_exit 552 | out, err = capture_io do 553 | assert_raises SystemExit do 554 | @sm.do_exit 555 | end 556 | end 557 | 558 | assert_equal '', out 559 | assert_equal "caught signal, shutting down\n", strip_log_prefix(err) 560 | end 561 | 562 | def test_log 563 | out, err = capture_io do 564 | @sm.log 'hi' 565 | end 566 | 567 | assert_equal "hi\n", strip_log_prefix(err) 568 | end 569 | 570 | def test_find_emails 571 | email_data = [ 572 | { :mail => 'body0', :to => 'recip@h1.example.com', :from => nobody }, 573 | { :mail => 'body1', :to => 'recip@h1.example.com', :from => nobody }, 574 | { :mail => 'body2', :to => 'recip@h2.example.com', :from => nobody }, 575 | ] 576 | 577 | emails = email_data.map do |email_data| Email.create email_data end 578 | 579 | tried = Email.create :mail => 'body3', :to => 'recip@h3.example.com', 580 | :from => nobody 581 | 582 | tried.last_send_attempt = Time.now.to_i - 258 583 | 584 | found_emails = [] 585 | 586 | out, err = capture_io do 587 | found_emails = @sm.find_emails 588 | end 589 | 590 | assert_equal emails, found_emails 591 | 592 | assert_equal '', out 593 | assert_equal "found 3 emails to send\n", strip_log_prefix(err) 594 | end 595 | 596 | def test_smtp_settings 597 | ActionMailer::Base.server_settings[:address] = 'localhost' 598 | 599 | assert_equal 'localhost', @sm.smtp_settings[:address] 600 | end 601 | 602 | def nobody 603 | 'nobody@example.com' 604 | end 605 | 606 | end 607 | --------------------------------------------------------------------------------