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