Four Action Mailer features you should know about

[Graduating from startup to scaleup means having to constantly evolve your applications to keep pace with your customers’ growth. In this Freshworks Engineering series, Rails@Scale, we talk about some of the techniques we employ to scale up our products.]

Action Mailer 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, as part of the Rails@Scale series, we will cover some of those features, and understand how to scale and debug email sending better with Action Mailer.

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:


class MailboxInterceptor
  def self.delivering_email(mail)
    set_smtp_settings(mail)
    fix_encodings
  ensure
      unset_email_config
  end

  private
  def self.set_smtp_settings(mail)
      smtp_settings = tenant_mail_config&.smtp_settings : default_smtp_settings
      ActionMailer.smtp_settings = smtp_settings
      mail.delivery_method(:smtp, smtp_settings)
  end

  def unset_email_config
    Thread.current[:tenant_mail_config] = nil
  end

  def tenant_mail_config=(mail_config)
    Thread.current[:tenant_mail_config] = mail_config
  end

  def tenant_mail_config
    Thread.current[:tenant_mail_config]
  end

end
ActionMailer::Base.register_interceptor(MailboxInterceptor)

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.


module MailObserver
  def self.delivered_email(mail)
    logger = ActionMailer::Base.logger
    logger.tagged(mail.message_id) do
      logger.info do
        recipients = Array(mail.to).join(', ')
        "Sent mail to #{recipients}"
      end
    end
    logger.debug { mail.encoded }
  end
end
ActionMailer::Base.register_observer(MailObserver)

module MailLogSubscriber
  def deliver(event)
    # no-op
  end
end
ActionMailer::LogSubscriber.prepend(MailLogSubscriber)

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’ event from Action Mailer.

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: 


class MailboxInterceptor
  def self.delivering_email(mail)
    set_smtp_settings(mail)
    fix_encodings
    mail.perform_deliveries = Rails.env.production? || File.exists?("#{Rails.root}/tmp/send_emails.txt")
  ensure
    unset_email_config
  end
end
ActionMailer::Base.register_interceptor(MailboxInterceptor)

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 changelogAllow 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: 


class ApplicationMailer < ActionMailer::Base
  before_action :block_spam_accounts, if: -> { Tenant.current.spam? }

  private
  def block_spam_accounts
    message.perform_deliveries = false
  end
end

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.


class ApplicationMailer < ActionMailer::Base
  before_action :block_spam_accounts, if: -> { Tenant.current.spam? }

  private
  def block_spam_accounts
    self.response_body= "Abort!"
  end
end

 

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.