Rails 7.1 introduces deliver callbacks for ActionMailer

railsJuly 11, 2023Dotby Alkesh Ghorpade

Rails callbacks are one of the powerful features that empower developers to enhance the functionality and behavior of their applications efficiently. Callbacks provide an elegant way to hook into different events of the object's lifecycle, allowing you to execute custom code at specific moments when a particular event occurs.

Rails provides a variety of callbacks that you can use to perform common tasks. In the case of models, Rails provides callbacks like before_validation, after_validation, before_save, after_save, after_commit etc.

For ActionMailer, Rails has provided callbacks similar to those for the controllers.

  1. before_action
  2. around_action
  3. after_action

However, these callbacks are triggered before, around, and after the mailer action is executed and not during the lifecycle of email delivery.

Before Rails 7.1

Before Rails 7.1, if you wanted to hook into the lifecycle of email delivery, you had to use Interceptors and Observers.

Interceptors are hooks triggered before the emails are handed off to the delivery agent. You can add interceptors to make modifications to emails before you deliver them.

Let's consider a Rails application that sends a welcome email to a newly signed-up user.

class UserMailer < ApplicationMailer
  before_action :set_user

  default from: "noreply@company.com"

  def welcome_email
    mail(to: @user.email, subject: "Welcome #{@user.full_name}")
  end

  private

  def set_user
    @user = User.find(params[:user_id])
  end
end

Now, let's assume that the application needs to be able to store email events in the Activity table before and after sending an email.

You need to use interceptors to log the message and store the activity before sending an email. Firstly, you must register an interceptor in the config/initializers/ directory.

# config/initializers/action_mailer.rb

ActionMailer::Base.register_interceptor(CreateActivityInterceptor)

Then add the delivering_email method which gets executed before sending the email.

# app/mailers/create_activity_interceptor.rb

class CreateActivityInterceptor
  def self.delivering_email(mail)
    email = mail.to.first
    user = User.find_by_email(email)

    Activity.create(
      type: mail.delivery_handler,
      user: user,
      status: "email_initiated"
    )

    Rails.logger.info("Welcome Email event logged for User. User ID #{user.id} and Email #{user.email}")
  end
end

Note:

  1. The mail param is passed to the delivering_email method. It is an instance of Mail::Message class.
  2. mail.delivery_handler is the name of the mailer class. In this case, the value for delivery_handler will be UserMailer.

Now, the application needs to log a message and store the activity after the email is delivered. For this, you need to make use of the Observers.

Observers execute after delivering the mail.

Here is an example of adding an observer to log the delivery status of emails and update the activity table. You must register observers in the config/initializers directory like interceptors.

# config/initializers/action_mailer.rb

ActionMailer::Base.register_interceptor(CreateActivityInterceptor)
ActionMailer::Base.register_observer(CreateActivityObserver)
# app/mailers/create_activity_observer.rb

class CreateActivityObserver
  def self.delivered_email(mail)
    email = mail.to.first
    user = User.find_by_email(email)

    Activity.create(
      type: mail.delivery_handler,
      user: user,
      status: "email_delivered"
    )

    Rails.logger.info("Welcome Email sent successfully to User. User ID #{user.id} and Email #{user.email}")
  end
end

Note:

  1. The mail param gets passed to the delivered_email method, similar to the Interceptor's delivering_email method.

The problem with Interceptors and Observers is they get applied to all emails by default. Please refer to this issue for more details.

To ensure that the Interceptor and Observer get applied to a particular mailer, you need to include the delivery_handler check, as shown below:

# app/mailers/create_activity_interceptor.rb

class CreateActivityInterceptor
  def self.delivering_email(mail)
    allowed_mailers = ["UserMailer"]

    return if allowed_mailers.exclude?(mail.delivery_handler)

    email = mail.to.first
    user = User.find_by_email(email)

    activity = Activity.create(
      type: mail.delivery_handler,
      user: user,
      status: "email_initiated"
    )

    Rails.logger.info("Welcome Email event logged for User. User ID #{user.id} and Email #{user.email}")
  end
end

In Rails 7.1

To simplify code and avoid using Interceptors and Observers, Rails 7.1 added deliver callbacks to ActionMailer.

It means that there is no need to initialize interceptors and observers. Instead, the mailer class can use the callbacks before_deliver, around_deliver, and after_deliver.

Here is an example of how to use deliver callbacks:

class UserMailer < ApplicationMailer
  before_action :set_user
  before_deliver :record_email_initiated_event
  after_deliver :record_email_delivered_event

  default from: "noreply@company.com"

  def welcome_email
    mail(to: @user.email, subject: "Welcome #{@user.full_name}")
  end

  private

  def set_user
    @user = User.find(params[:user_id])
  end

  def record_email_initiated_event
    Activity.create(
      type: mail.delivery_handler,
      user: @user,
      status: "email_initiated"
    )

    Rails.logger.info("Welcome Email event logged for User. User ID #{@user.id} and Email #{@user.email}")
  end

  def record_email_delivered_event
    Activity.create(
      type: mail.delivery_handler,
      user: @user,
      status: "email_delivered"
    )

    Rails.logger.info("Welcome Email sent successfully to User. User ID #{@user.id} and Email #{@user.email}")
  end
end

These changes can help Rails developers to track the logs accurately and verify if anything went wrong with emails.

Here is a note about the callback sequence in ActionMailer:

NOTE:

You might expect the callback sequence in ActionMailer to be:

  1. before_action
  2. before_deliver
  3. after_deliver
  4. after_action

However, the actual sequence is as below:

  1. before_action
  2. after_action
  3. before_deliver
  4. after_deliver

To know more about this issue, please refer to the Additional information in PR description and this comment.

Closing Remark

Could your team use some help with topics like this and others covered by ShakaCode's blog and open source? We specialize in optimizing Rails applications, especially those with advanced JavaScript frontends, like React. We can also help you optimize your CI processes with lower costs and faster, more reliable tests. Scraping web data and lowering infrastructure costs are two other areas of specialization. Feel free to reach out to ShakaCode's CEO, Justin Gordon, at justin@shakacode.com or schedule an appointment to discuss how ShakaCode can help your project!
Are you looking for a software development partner who can
develop modern, high-performance web apps and sites?
See what we've doneArrow right