Tips for Implementing Emails in Rails

Sending emails from your Rails application is a pretty common requirement. User signup, forgotten password, order creation, nightly reports – they all require emails. This is pretty straightforward in Rails – we already have the concept of mailers (with views) – you simply need to configure your mail service and you’re done!

However, every time that I implement emails on a project there are certain problems which popup – blacklisted email accounts, intercepting emails in pre-production environments and users getting confused about which environment the emails are coming from. These are some tips which help to alleviate some of these problems.

1. Use an External Email Service

This is pretty much a no-brainer – you need a scalable, cost-effective, transactional email-sending service and you don’t want to build it yourself. This usually gives you automatic reporting on sent emails – no more ‘why did this email not go through’. Most external services can also be configured to route response emails (at a certain address) to an endpoint in your Rails app.

Unless you have an amazingly compelling reason not to do so, use an external email service. SendGrid and Amazon SES are popular options.

2. Use an Environment Interceptor

Most Rails applications have multiple environments – for example, you might have Integration, QA, Staging and Production (or even more). If you are enabling emails in multiple environments you might get the problem where a user receives an email but doesn’t know which environment it came from. Did this come from QA or Production? Even if you have different hostnames you might make a mistake and send a test email to a real user.

One way of fixing this problem is with an environment interceptor – basically all emails are intercepted and if the environment is not production we prefix the environment’s name to the subject line.

class EnvironmentInterceptor
  def self.delivering_email message
    message.subject = "[#{Rails.env.capitalize}] #{message.subject}" unless Rails.env.production?
  end
end

We can register our interceptor in an initializer (config/initializers/mail.rb).

require 'environment_interceptor'
ActionMailer::Base.register_interceptor(EnvironmentInterceptor)

3. Use a Whitelist Interceptor

If you are in a situation where you are sending emails in multiple environments, you might not want emails to go out to real users in all of them. For example, you might want to enable emails in your QA environment, but you want to block emails going to real users while still allowing emails to be sent to your testers.

I’ve blogged about intercepting emails before – basically emails which are intercepted are sent to a pre-determined address (such as a temporary gmail account) which will allow testers to still see the emails that were sent. Take a look at that post if want to see more details.

The whitelist interceptor will basically look at who the email is getting sent to and intercept it unless it is being sent to one or more specified domains.

class WhitelistInterceptor
  def self.delivering_email message
    unless message.to.join(' ') =~ /(@yourcompany.com|@thoughtworks.com)/i
      message.subject = "#{message.to} #{message.subject}"
      message.to = ENV['NOTIFICATIONS_EMAIL']
    end
  end
end

Instead of using an initializer to register this interceptor I simply configure it in the environment files where it applies (config/environments/qa.rb).

require 'whitelist_interceptor'
ActionMailer::Base.register_interceptor(WhitelistInterceptor)

4. Send Emails with a Background Job

Sending an email could take a few seconds, which means if you’re sending emails on the main thread your user is staring at a spinner for that time. It also means you’re tying up a thread which could be used to service other web requests during this time – lowering the overall responsiveness of your application.

The obvious solution is to execute long-running tasks (such as sending emails) with a background job. On my current project we are using the excellent Sidekiq to execute these tasks.

Simply add it to your Gemfile and then add delay to the call where you are sending emails.

class OrderMailer < ActionMailer::Base
  def order_created(order_id)
    @order = Order.find(order_id)
    params = {
      to: 'admin@example.com',
      subject: "Order #{order_id} created"
    }
    mail(params)
  end
end

class OrderController < ApplicationController
  def create
    # save the order

    OrderMailer.delay.order_created(order_id)
  end
end

That’s all there is to it! Sidekiq also has very straighforward integration into Heroku and works seamlessly with SendGrid.

Happy coding.