Rails 7.1 adds *_deliver callbacks to Action Mailer

Calvin Chiramal

Calvin Chiramal

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.

class UserMailer < ApplicationMailer
  default from: '[email protected]'

  def welcome_email
    @user = params[:user]
    mail(to: @user.email, subject: 'Welcome to neeto')
  end
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.

UserMailer.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.

# interceptors/staging_email_interceptor.rb
module Interceptors
  class StagingEmailInterceptor
    def self.delivering_email(mail)
      if rails.env.staging? && !allowed_emails.include?(mail.to)
        mail.perform_deliveries = false
      end
    end
  end
end
# config/initializers/action_mailer.rb
ActionMailer::Base.register_interceptor(Interceptors::StagingEmailInterceptor)

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.

class UserMailer < ApplicationMailer
  default from: '[email protected]'
  before_deliver :filter_allowed_emails

  def welcome_email
    @user = params[:user]
    mail(to: @user.email, subject: 'Welcome to neeto')
  end

  private

    def filter_allowed_emails
      if rails.env.staging? && !allowed_emails.include?(mail.to)
        mail.perform_deliveries = false
      end
    end
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:

# observers/staging_email_interceptor.rb
module Observers
  class SetDeliveredAtObserver
    def self.delivered_email(mail)
      user = User.find_by(email: mail.to)
      user.update(mail_delivered_at: mail.date)
    end
  end
end
# config/initializers/action_mailer.rb
ActionMailer::Base.register_interceptor(Interceptors::StagingEmailInterceptor)
ActionMailer::Base.register_observer(Observers::SetDeliveredAtObserver)

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

class UserMailer < ApplicationMailer
  default from: '[email protected]'
  before_deliver :filter_allowed_emails
  after_deliver :set_delivered_at

  def welcome_email
    @user = params[:user]
    mail(to: @user.email, subject: 'Welcome to neeto')
  end

  private

    def filter_allowed_emails
      if rails.env.staging? && !allowed_emails.include?(mail.to)
        mail.perform_deliveries = false
      end
    end

    def set_delivered_at
      @user.update(mail_delivered_at: mail.date)
    end
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.

If this blog was helpful, check out our full blog archive.

Stay up to date with our blogs.

Subscribe to receive email notifications for new blog posts.