ActionMailer is the default email library that comes with Rails. It has a ton of hidden features that aren’t spoken about or discussed as much as some of its counterparts like ActiveRecord or ActiveSupport. In this article, we will cover some of those features, and understand how to scale and debug email sending better with ActionMailer.
Interceptors
According to the official Rails documentation , "Interceptors allow you to make modifications to emails before they are handed off to the delivery agents. An interceptor class must implement the :delivering_email(message)
method, which will be called before the email is sent."
This can be a very powerful hook to help you extract some generic processes or rule out of your mailer or notifier classes. We leverage interceptors in Freshservice to be able to set default mailboxes and the From
address that our customers intended to use for sending out emails to their recipients. The following code snippet demonstrates how you can leverage Interceptors to achieve the same:
The set_smtp_settings
method retrieves the current tenant's configured mailbox from the thread and assigns it to the mail’s smtp_settings
attribute. The mail class internally uses the ‘smtp_settings’ attribute to connect to the mailbox for delivering the email.
This method also takes care of setting all product-specific custom headers required for product functionalities and for the platform scaling. To avoid IP reputation abuse through spamming and a potential service disruption to our premium customers, depending on the state of the current tenant, we also append headers to ensure that the right IP is selected when delivering the corresponding emails.
The fix_encodings
method, as the name suggests, fixes any encoding issues that arise out of unsupported user entered data.
Observers
Right back from the official Rails documentation , “Observers give you access to the email message after it has been sent. An observer class must implement the :delivered_email(message)
method, which will be called after the email is sent.” Like interceptors, observers offer hooks that can be leveraged for generic processes.
For email debugging purposes, we wanted to print an email’s message-id
header tagged along with the default ActionMailer
log that prints Sent email to #{recipients_list}
. This log is printed by the default LogSubscriber
from ActionMailer
. But this LogSubscriber
did not have access to the mail object. Hence we couldn’t leverage the LogSubscriber
for this.
We then decided to use Observers to achieve this. We also had to override the default LogSubscriber’s deliver method to avoid duplicate Sent email to #{recipients_list}
log messages.
We can also log other headers from the mail object for analytics or logging purposes.
Alternatively, this can be achieved through ActiveSupport::Notification
as well by subscribing to the deliver.action_mailer
from ActionMailer
.
Perform Deliveries (or not)
According to the Official Rails documentation , perform deliveries determine "whether deliveries are actually carried out when the deliver method is invoked on the Mail message. By default they are, but this can be turned off to help functional testing. If this value is false, deliveries array will not be populated even if the delivery_method
is :test
."
This option is useful for test environments or your CI pipelines to avoid consuming your email quotas provided by email service providers. This can also be leveraged in development mode to avoid spamming your mailbox with too many emails when developing or testing a feature. The mail content is printed out by default on STDOUT
. You can even make this configurable using a thread variable or file in tmp directory. Another use case for setting this to false dynamically is to avoid sending spam emails depending on the tenant that's sending the email or the content of the email.
Here's how you do this using the interceptor method defined above:
Setting this at mail level ensures that other emails are not impacted. This is achievable through ActionMailer callbacks as well.
Callbacks — Have better control over your emails
With Rails 4.0, ActionMailer was introduced to ActiveSupport callback hooks before_action
similar to ActionController. According to the changelog: Allow callbacks to be defined in mailers similar to ActionController::Base. You can configure default settings, headers, attachments, delivery settings or change delivery using before_filter, after_filter, etc. Justin S. Leitgeb
This was a great addition to the framework as it allowed for great possibilities. Some of the items discussed in the previous sections are easily achievable via callbacks.
Freshservice is a cloud based SaaS product that implements a multitenant architecture at its core. As a product, we take pride in making it easy to sign up and get started on from the very first day. But this ease comes with a pain of handling excessive spam. The multitenant architecture becomes the victim here as one bad fish can make the entire pond dirty. To ensure our customers aren't impacted by spammed signups, we have several spam filters and blockers at different levels within the system. For example, upon signup, we do a spam lookup for the tenant based on historical pattern and data and set a spam score for it. Depending on the score, access to certain features or channels are blocked. Email being one of the primary channels, we wanted to safeguard our email reputation and avoid emails delivered from Freshservice being marked as spam. This essentially meant that we had to block email sending from the application for spammy tenants. While not enqueuing jobs for these tenants was the easy way to do this, it was getting incredibly hard to ensure that these checks were followed every time we were enqueuing a background job to deliver an email. We missed a few times and that's when we wanted to add another layer of protection — one at Action Mailer
level.
The simplest solution was to set perform_deliveries
to false from the interceptor or through a before_action
defined in a base ApplicationMailer like below:
But we were still processing the mail templates and the entire job just to not deliver the emails. We weren't satisfied and wanted something better. That's when we uncovered a hidden gem that's rarely talked about. We came across that setting , where the response_body
in a before_action
callback aborts the mailer processing right away.
Rails comes preloaded with a bunch of really powerful frameworks loaded with tons of features. We are constantly evolving our codebase and in turn our product to leverage whatever Rails has to offer to better serve our customers.
Top comments (0)