Rails 7.1 adds *_deliver callbacks to Action Mailer

Calvin Chiramal

By Calvin Chiramal

on September 26, 2023

This blog is part of our  Rails 7 series.

Rails 7.1 has added *_deliver callbacks to Action Mailer. Let's understand the use of these callbacks with an example. Consider a case where you want to send an email after a user has signed up.

1class UserMailer < ApplicationMailer
2  default from: 'notifications@neeto.com'
4  def welcome_email
5    @user = params[:user]
6    mail(to: @user.email, subject: 'Welcome to neeto')
7  end

When the mail method is called in the above code, it just renders the mail template from a view. The mail is actually not sent. The actual delivery may happen synchronously or asynchronously. To send the mail, we need to call one of many deliver methods.

1UserMailer.with(user: @user).welcome_email.deliver_later

Before Rails 7.1, we did not have any callbacks around the deliver methods to execute code around the delivery lifecycle. You would need to use interceptors and observers to hook into the mail delivery lifecycle. Say, you need to send emails to only a few allowed email addresses in the staging environment. You would need to use an interceptor and register it to Action Mailer.

1# interceptors/staging_email_interceptor.rb
2module Interceptors
3  class StagingEmailInterceptor
4    def self.delivering_email(mail)
5      if rails.env.staging? && !allowed_emails.include?(mail.to)
6        mail.perform_deliveries = false
7      end
8    end
9  end
1# config/initializers/action_mailer.rb

The new before_deliver callback allows you to handle this situation without using the Interceptors or Observers. You would just need to include the following in UserMailer.

1class UserMailer < ApplicationMailer
2  default from: 'notifications@neeto.com'
3  before_deliver :filter_allowed_emails
5  def welcome_email
6    @user = params[:user]
7    mail(to: @user.email, subject: 'Welcome to neeto')
8  end
10  private
12    def filter_allowed_emails
13      if rails.env.staging? && !allowed_emails.include?(mail.to)
14        mail.perform_deliveries = false
15      end
16    end

Similarly, suppose you want to update the mail_delivered_at attribute of the user instance, you would have to use an observer and register it like so:

1# observers/staging_email_interceptor.rb
2module Observers
3  class SetDeliveredAtObserver
4    def self.delivered_email(mail)
5      user = User.find_by(email: mail.to)
6      user.update(mail_delivered_at: mail.date)
7    end
8  end
1# config/initializers/action_mailer.rb

With the new after_deliver callback this becomes as simple as defining a method in UserMailer.

1class UserMailer < ApplicationMailer
2  default from: 'notifications@neeto.com'
3  before_deliver :filter_allowed_emails
4  after_deliver :set_delivered_at
6  def welcome_email
7    @user = params[:user]
8    mail(to: @user.email, subject: 'Welcome to neeto')
9  end
11  private
13    def filter_allowed_emails
14      if rails.env.staging? && !allowed_emails.include?(mail.to)
15        mail.perform_deliveries = false
16      end
17    end
19    def set_delivered_at
20      @user.update(mail_delivered_at: mail.date)
21    end

An important thing to keep in mind is the order of execution of the callbacks.

  • before_action
  • after_action
  • before_deliver
  • after_deliver

This makes sense as the deliver callbacks only wrap around the deliver methods while the action callbacks wrap around the mail render method.

Please check out this pull request for more details.

Stay up to date with our blogs. Sign up for our newsletter.

We write about Ruby on Rails, React.js, React Native, remote work,open source, engineering & design.