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: '[email protected]' 3 4 def welcome_email 5 @user = params[:user] 6 mail(to: @user.email, subject: 'Welcome to neeto') 7 end 8end
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 10end
1# config/initializers/action_mailer.rb 2ActionMailer::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.
1class UserMailer < ApplicationMailer 2 default from: '[email protected]' 3 before_deliver :filter_allowed_emails 4 5 def welcome_email 6 @user = params[:user] 7 mail(to: @user.email, subject: 'Welcome to neeto') 8 end 9 10 private 11 12 def filter_allowed_emails 13 if rails.env.staging? && !allowed_emails.include?(mail.to) 14 mail.perform_deliveries = false 15 end 16 end 17end
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 9end
1# config/initializers/action_mailer.rb 2ActionMailer::Base.register_interceptor(Interceptors::StagingEmailInterceptor) 3ActionMailer::Base.register_observer(Observers::SetDeliveredAtObserver)
With the new after_deliver callback this becomes as simple as defining a method in UserMailer.
1class UserMailer < ApplicationMailer 2 default from: '[email protected]' 3 before_deliver :filter_allowed_emails 4 after_deliver :set_delivered_at 5 6 def welcome_email 7 @user = params[:user] 8 mail(to: @user.email, subject: 'Welcome to neeto') 9 end 10 11 private 12 13 def filter_allowed_emails 14 if rails.env.staging? && !allowed_emails.include?(mail.to) 15 mail.perform_deliveries = false 16 end 17 end 18 19 def set_delivered_at 20 @user.update(mail_delivered_at: mail.date) 21 end 22end
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.