Tame Rails Email Dragons with Mailhopper
There are plenty of pitfalls lurking in the seemingly straightforward topic of email processing in Rails.
Let's lift a few rocks to find where there be dragons...
Synchronous Mail Delivery
Synchronous mail delivery, the default in Rails, seems so simple. A controller responds to a request by generating an email through a mailer. The mailer in turn hands off the email to a mail server for delivery to the user.
No problem, right?
Unfortunately, this scenario is rife with potential problems. Synchronous mail processing requires a successful round trip to your SMTP or sendmail server in order to respond to your users' requests. The first danger is that users might fall asleep while waiting for some signs of life from your application. Greater dangers lurk along the path from your mailer to your mail server. Your mail server may be down or inaccessible, or perhaps you've been blacklisted (mistakenly, of course). Regardless, the best your users can hope for is an error message telling them that their message couldn't be sent. And as the developer, you may need to hunt through the logs to try to find and resend their missing email. Ugh.
Asynchronous Mail Delivery
By sending email asynchronously, you can keep responses snappy and insulate
your users from the problems of synchronous mail processing. As you would
expect, there are already some asynchronous email solutions for Rails.
DelayedJob adds a custom
delay() method to mailers that can be used instead of
deliver(). If you use
Resque to process background jobs, check out
ResqueMailer. Both solutions queue
mail jobs to be performed later, without delaying a response to the user.
Queued jobs will continue to be retried until successful, thereby minimizing
problems caused by a temporarily inaccessible mail server.
Sounds like the right answer, doesn't it?
While asynchronous mail delivery usually works well, there are still a few gotchas. Since both the generation and delivery of messages is queued, it is possible that the data required to generate your messages could change from the time the message gets queued to the time it is generated. Therefore, you'll want to pass ids of objects instead of the objects themselves to mailer methods. You'll also need to verify that the objects still exist and are in the required state for the requested message. Imagine that a day passes without the queue being processed: you'll either be glad that you put the work in up front to ensure that your mailer methods handle all possible exceptions, or you'll simply delete your mail queue and apologize profusely to your users (which you should probably do after a day of downtime anyway).
Mail Delivery with Mailhopper
Keeping in mind the problems inherent in both synchronous and asynchronous mail delivery, we devised a new blended approach. Mailhopper acts as a delivery agent that synchronously generates messages and stores their content in your database. A separate gem, DelayedMailhopper, adds those messages to the DelayedJob queue for asynchronous delivery via a delivery agent such as SMTP or sendmail.
By synchronously generating messages, including their full content and headers, Mailhopper eliminates the need to carefully craft your mailer methods to function asynchronously. Want to pass an object instead of an id? Go for it! You can safely assume that your mailer methods are called as part of a synchronous request.
By using DelayedMailhopper to deliver messages asynchronously, the speed and reliability issues of synchronous mailing are avoided.
Because Mailhopper acts as a delivery agent, there are no special methods that must be called to use it for delivering messages. Instead, you can define Mailhopper as the default agent for a mailer method, for a whole mailer, or even your entire application.
And by storing messages in a separate table (called Emails by default), the full contents of those messages are archived automatically. You can optionally run a separate background process to purge messages after they've been sent or reached a certain age. Or you can choose to keep them in your database permanently. It's pretty handy for customer support to have an admin interface which can pull up all the emails sent to a particular address.
Both Mailhopper and DelayedMailhopper were built as Engines that are compatible with Rails 3.1+. If you'd like to try them out, please see the installation and configuration instructions in the README files. If you'd like to use Mailhopper with Resque, there's an obvious opportunity to write "ResqueMailhopper", using DelayedMailhopper as a starting point (please let us know if you tackle this).