DEV Community

Drew Bragg
Drew Bragg

Posted on

Using Mail Interceptors in Rails

I stumbled upon a super neat feature of ActionMailer the other night. If you scroll all the way to the bottom of the Rails Guide for ActionMailer you'll find a section on Intercepting and Observing Emails. Intercepting an email allows you to modify your email before sending it. Why would you want to do this you might ask? Well, take the following real life production code in my companies app (slightly modified for confidentiality and brevity):

class ApplicationMailer < ActionMailer::Base
  def self.deliver_mail(mail)
    if Rails.env.development? || Rails.env.staging?
        rerouted_email_address = "#{Rails.env}@email.com"
        original_to = mail.header[:to].to_s
        original_subject = mail.header[:subject].to_s

        if mail.cc
          original_cc_emails = mail.cc.dup
          mail.cc = []

          original_cc_emails.each do |email_str|
            other_cc_emails = original_cc_emails.select { |cc| cc != email_str }
            mail.to = rerouted_email_address
            mail.subject = "This is an email with copied folks [cc: #{email_str} +
                             #{other_cc_emails.length} others, to: #{original_to}]"
            super(mail)
          end
        end

        logger.info "Rerouted '#{original_to}' to '#{rerouted_email_address}'."
        mail.subject = "#{original_subject} [originally to: #{original_to}]"
        mail.to = rerouted_email_address
      end
    end

    super(mail)
  end
end
Enter fullscreen mode Exit fullscreen mode

That's a lot of code to have in our base mailer that really only runs in some environments. Also, I've found that code like this, where we're actually overriding a core method, makes it hard to debug problems. It also adds a lot of unneeded complexity when trying to understand what our mailers actually do.

Enter Interceptors.

An interceptor is a special class that has a delivering_email(mail) method. The delivering_email method is what will be called before the email is actually sent. Inside the method we'll be able to interact and modify our email before it is sent by the original mailer. So simple but so awesome.

I created an interceptors directory in app/mailer (if you think there's a better place to stash them I'm all ears) and added a reroute_email_interceptor.rb file that looks a little something like this:

module Interceptors
  class RerouteEmailInterceptor
    def self.delivering_email(mail)
        original_to = mail.header[:to].to_s
        original_subject = mail.header[:subject].to_s

        mail.to = rerouted_email_address
        mail.subject = "#{original_subject} [originally to: #{original_to}]"

        if mail.cc.present?
          original_cc_emails = mail.cc.dup.join(", ")
          mail.cc = []
          mail.subject = "This is an email with copied folks [cc: #{original_cc_emails}, to: #{original_to}]"
        end

        Rails.logger.info "Rerouted '#{original_to}' to '#{rerouted_email_address}'."
      end
    end

    def self.rerouted_email_address
      @rerouted_email_address ||= "#{Rails.env}@email.com"
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

Aside from the slight changes to how we handle CC's (which the team is happy with) this will do the same thing as the original code but allows our ApplicationMailer do go back to:

class ApplicationMailer < ActionMailer::Base

end
Enter fullscreen mode Exit fullscreen mode

In order to make the interceptor actually do it's thing we need to add an initializer to add it to ActionMailer::Base

# config/initializers/mailer_interceptor.rb
if Rails.env.development? || Rails.env.staging?

 ActionMailer::Base.register_interceptor(Interceptors::RerouteEmailInterceptor)
end
Enter fullscreen mode Exit fullscreen mode

And we're done! This is a far better, cleaner, and OOP way of handling our development/staging emails.

I can't believe that I've been working in Rails as long as I have and just now learning about Mail Interceptors but I'm sure glad I did!

Top comments (2)

Collapse
 
nuclearspike profile image
Paul

But doesn't the Interceptors version not do anything with the cc array? In the ApplicationMailer version, it iterates over the ccs and calls super(mail) for each. In the Interceptors module, it just creates a string of the cc's but then wipes out that array, then creates a string that says it is going to all of those users but there's no code that causes that to happen. Correct?

Collapse
 
drbragg profile image
Drew Bragg

Yup, I called that out after the example

Aside from the slight changes to how we handle CC's (which the team is happy with) this will do the same thing as the original code

We actually ended up missing the additional CC emails so that block has since been replaced with:

if mail.cc.present?
  mail.cc.each do |recipient|
    RawMailer.raw(
      "from" => mail.from,
      "recipients" => recipient,
      "subject" => "CCed version of: #{original_subject}",
      "plain_text_content" => mail.parts.first.body.to_s,
      "html_content" => mail.parts.last.body.to_s
    ).deliver_later
  end
  mail.cc = []
end
Enter fullscreen mode Exit fullscreen mode

So now it does basically the same thing.