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
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
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
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
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 (3)
Great post (from the past!), thanks. Just for future reference: to be fully Rails autoloading / reloading friendly - the code in the initializser should be wrapped with
Rails.application.config.to_prepare do
see guides.rubyonrails.org/autoloading...
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?
Yup, I called that out after the example
We actually ended up missing the additional CC emails so that block has since been replaced with:
So now it does basically the same thing.