Rails 7.1 introduces deliver callbacks for ActionMailer
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.
before_action
around_action
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: "[email protected]"
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:
- The
mail
param is passed to thedelivering_email
method. It is an instance of Mail::Message class. mail.delivery_handler
is the name of the mailer class. In this case, the value fordelivery_handler
will beUserMailer
.
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:
- The
mail
param gets passed to thedelivered_email
method, similar to the Interceptor'sdelivering_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: "[email protected]"
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:
before_action
before_deliver
after_deliver
after_action
However, the actual sequence is as below:
before_action
after_action
before_deliver
after_deliver
To know more about this issue, please refer to the Additional information in PR description and this comment.