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 (2)
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.