Mass mailing with active mailer

in
So, you have plenty of users, and you want to notify them of a significant update. You have all recipient emails in you database, and the corresponding ActiveRecord models. Rails also provides the nice ActiveMailer tool. Writing a script using all your existing infrastructure seems the best solution, and it's also how I wanted to send an anoucement to MyOwnDB subscribers. There are, however, two points that you need to take into account:
  • you can't simply loop over all emails, and for each address open a connection to the SMTP server. You'll get in trouble, as illustrated here
  • Active Mailer doesn't let you send several mails in one SMTP connection
  • the limit of mails allowed to be sent in one SMTP connection is usually limited to 100
These problems don't prevent the use of ActiveMailer though. The solution I describe below is based on a suggestion on the Rails mailing list by Gerret Apelt. First, create your ActiveMailer infrastructure:
./script/generate mailer Mailings mailing
This puts a Mailing class in app/models with one method, mailing, that you should edit to set the Subject and the From address of the mails sent. The body of the mail can be found in app/views/mailings/mailing.rhtml For sending the mails, I decided to go for a script in lib/send_mailing.rb that can be run with
./script/runner "require 'send_mailing'"
This script first creates the mail with ActiveMailer, but without sending it:
tmail = Mailings.create_mailing()
Once we have the mail object, we can loop over the recipients, keeping the remarks above in mind:
exceptions = {}
recipients.each_slice(SENDING_BATCH_SIZE) do |recipients_slice|
  Net::SMTP.start('localhost', 25) do |sender|
    recipients_slice.each do |recipient|
      tmail.to = recipient
      begin
        sender.sendmail tmail.encoded, tmail.from, recipient
      rescue Exception => e
        exceptions[recipient] = e
        #needed as the next mail will send command MAIL FROM, which would 
        #raise a 503 error: "Sender already given"
        sender.finish
        sender.start
      end
    end
  end
end
the loop processes the recipients in slice of the size SENDING_BATCH_SIZE which is set at the beginning of the script. It configures the mail object we created before with the recipient's address, and then uses the SMTP connection to send the mail. Exceptions are catched and collected in an array. This loop set the To header to the recipient's address. If you accept to have the To header set to something like "mailing@mydomain.com", you can pass the slice of addresses directly to the sendmail method, which could possibly be more efficient too (though I haven't checked):
tmail.to = "mailing@mydomain.com"
sender.sendmail tmail.encoded, tmail.from, recipients_slice
At the end of the script, if exceptions have been collected, it is possible to open a breakpoint session to access the exceptions array. In any case, this array is dump in YAML format in the log directory:
if exceptions.length>0
  answer = ""
  while not ["y","n"].include? answer
    puts "There were #{exceptions.length} errors! Do you want to use breakpoint to see them? (y/n)"
    answer = STDIN.readline.chop
  end
  breakpoint if (answer=='y')
  logfile = "log/mailing-exceptions-#{Time.now.strftime("%Y-%m-%dT%H:%M:%S")}.yaml"
  File.open( logfile, 'w' ) do |out| YAML.dump( exceptions, out ) end
  puts "You can find a dump of the exceptions in #{logfile}"
end
If you have exceptions raised and want to resend the mail to those addresses only, you can easily do it by editing the script and set the recipients accordingly:
recipients= YAML.load("log/dumpfile.yaml").keys
You can download the script which you just have to edit it to build the array of recipients.