Skip to content

Commit 468d806

Browse files
committed
Add *_deliver callbacks for Action Mailer
1 parent 4d171a4 commit 468d806

File tree

9 files changed

+191
-10
lines changed

9 files changed

+191
-10
lines changed

actionmailer/CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,17 @@
1+
* Added `*_deliver` callbacks to `ActionMailer::Base` that wrap mail message delivery.
2+
3+
Example:
4+
5+
```ruby
6+
class EventsMailer < ApplicationMailer
7+
after_deliver do
8+
User.find_by(email: message.to.first).update(email_provider_id: message.message_id, emailed_at: Time.current)
9+
end
10+
end
11+
```
12+
13+
*Ben Sheldon*
14+
115
* Added `deliver_enqueued_emails` to `ActionMailer::TestHelper`. This method
216
delivers all enqueued email jobs.
317

actionmailer/lib/action_mailer.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ module ActionMailer
4343
end
4444

4545
autoload :Base
46+
autoload :Callbacks
4647
autoload :DeliveryMethods
4748
autoload :InlinePreviewInterceptor
4849
autoload :MailHelper

actionmailer/lib/action_mailer/base.rb

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -316,12 +316,14 @@ module ActionMailer
316316
#
317317
# = Callbacks
318318
#
319-
# You can specify callbacks using <tt>before_action</tt> and <tt>after_action</tt> for configuring your messages.
320-
# This may be useful, for example, when you want to add default inline attachments for all
321-
# messages sent out by a certain mailer class:
319+
# You can specify callbacks using <tt>before_action</tt> and <tt>after_action</tt> for configuring your messages,
320+
# and using <tt>before_deliver</tt> and <tt>after_deliver</tt> for wrapping the delivery process.
321+
# For example, when you want to add default inline attachments and log delivery for all messages
322+
# sent out by a certain mailer class:
322323
#
323324
# class NotifierMailer < ApplicationMailer
324325
# before_action :add_inline_attachment!
326+
# after_deliver :log_delivery
325327
#
326328
# def welcome
327329
# mail
@@ -331,9 +333,13 @@ module ActionMailer
331333
# def add_inline_attachment!
332334
# attachments.inline["footer.jpg"] = File.read('/path/to/filename.jpg')
333335
# end
336+
#
337+
# def log_delivery
338+
# Rails.logger.info "Sent email with message id '#{message.message_id}' at #{Time.current}."
339+
# end
334340
# end
335341
#
336-
# Callbacks in Action Mailer are implemented using
342+
# Action callbacks in Action Mailer are implemented using
337343
# AbstractController::Callbacks, so you can define and configure
338344
# callbacks in the same manner that you would use callbacks in classes that
339345
# inherit from ActionController::Base.
@@ -466,6 +472,7 @@ module ActionMailer
466472
# * <tt>deliver_later_queue_name</tt> - The queue name used by <tt>deliver_later</tt> with the default
467473
# <tt>delivery_job</tt>. Mailers can set this to use a custom queue name.
468474
class Base < AbstractController::Base
475+
include Callbacks
469476
include DeliveryMethods
470477
include QueuedDelivery
471478
include Rescuable
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# frozen_string_literal: true
2+
3+
module ActionMailer
4+
module Callbacks
5+
extend ActiveSupport::Concern
6+
7+
included do
8+
include ActiveSupport::Callbacks
9+
define_callbacks :deliver, skip_after_callbacks_if_terminated: true
10+
end
11+
12+
module ClassMethods
13+
# Defines a callback that will get called right before the
14+
# message is sent to the delivery method.
15+
def before_deliver(*filters, &blk)
16+
set_callback(:deliver, :before, *filters, &blk)
17+
end
18+
19+
# Defines a callback that will get called right after the
20+
# message's delivery method is finished.
21+
def after_deliver(*filters, &blk)
22+
set_callback(:deliver, :after, *filters, &blk)
23+
end
24+
25+
# Defines a callback that will get called around the message's deliver method.
26+
def around_deliver(*filters, &blk)
27+
set_callback(:deliver, :around, *filters, &blk)
28+
end
29+
end
30+
end
31+
end

actionmailer/lib/action_mailer/message_delivery.rb

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,9 @@ def deliver_later(options = {})
108108
#
109109
def deliver_now!
110110
processed_mailer.handle_exceptions do
111-
message.deliver!
111+
processed_mailer.run_callbacks(:deliver) do
112+
message.deliver!
113+
end
112114
end
113115
end
114116

@@ -118,13 +120,15 @@ def deliver_now!
118120
#
119121
def deliver_now
120122
processed_mailer.handle_exceptions do
121-
message.deliver
123+
processed_mailer.run_callbacks(:deliver) do
124+
message.deliver
125+
end
122126
end
123127
end
124128

125129
private
126130
# Returns the processed Mailer instance. We keep this instance
127-
# on hand so we can delegate exception handling to it.
131+
# on hand so we can run callbacks and delegate exception handling to it.
128132
def processed_mailer
129133
@processed_mailer ||= @mailer_class.new.tap do |mailer|
130134
mailer.process @action, *@args

actionmailer/test/callbacks_test.rb

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# frozen_string_literal: true
2+
3+
require "abstract_unit"
4+
require "mailers/callback_mailer"
5+
6+
class ActionMailerCallbacksTest < ActiveSupport::TestCase
7+
include ActiveJob::TestHelper
8+
9+
setup do
10+
@previous_delivery_method = ActionMailer::Base.delivery_method
11+
ActionMailer::Base.delivery_method = :test
12+
CallbackMailer.rescue_from_error = nil
13+
CallbackMailer.after_deliver_instance = nil
14+
CallbackMailer.around_deliver_instance = nil
15+
CallbackMailer.abort_before_deliver = nil
16+
CallbackMailer.around_handles_error = nil
17+
end
18+
19+
teardown do
20+
ActionMailer::Base.deliveries.clear
21+
ActionMailer::Base.delivery_method = @previous_delivery_method
22+
CallbackMailer.rescue_from_error = nil
23+
CallbackMailer.after_deliver_instance = nil
24+
CallbackMailer.around_deliver_instance = nil
25+
CallbackMailer.abort_before_deliver = nil
26+
CallbackMailer.around_handles_error = nil
27+
end
28+
29+
test "deliver_now should call after_deliver callback and can access sent message" do
30+
mail_delivery = CallbackMailer.test_message
31+
mail_delivery.deliver_now
32+
33+
assert_kind_of CallbackMailer, CallbackMailer.after_deliver_instance
34+
assert_not_empty CallbackMailer.after_deliver_instance.message.message_id
35+
assert_equal mail_delivery.message_id, CallbackMailer.after_deliver_instance.message.message_id
36+
assert_equal "[email protected]", CallbackMailer.after_deliver_instance.message.to.first
37+
end
38+
39+
test "deliver_now! should call after_deliver callback" do
40+
CallbackMailer.test_message.deliver_now
41+
42+
assert_kind_of CallbackMailer, CallbackMailer.after_deliver_instance
43+
end
44+
45+
test "before_deliver can abort the delivery and not run after_deliver callbacks" do
46+
CallbackMailer.abort_before_deliver = true
47+
48+
mail_delivery = CallbackMailer.test_message
49+
mail_delivery.deliver_now
50+
51+
assert_nil mail_delivery.message_id
52+
assert_nil CallbackMailer.after_deliver_instance
53+
end
54+
55+
test "deliver_later should call after_deliver callback and can access sent message" do
56+
perform_enqueued_jobs { CallbackMailer.test_message.deliver_later }
57+
assert_kind_of CallbackMailer, CallbackMailer.after_deliver_instance
58+
assert_not_empty CallbackMailer.after_deliver_instance.message.message_id
59+
end
60+
61+
test "around_deliver is called after rescue_from on action processing exceptions" do
62+
CallbackMailer.around_handles_error = true
63+
64+
CallbackMailer.test_raise_action.deliver_now
65+
assert CallbackMailer.rescue_from_error
66+
end
67+
68+
test "around_deliver is called before rescue_from on deliver! exceptions" do
69+
CallbackMailer.around_handles_error = true
70+
71+
stub_any_instance(Mail::TestMailer, instance: Mail::TestMailer.new({})) do |instance|
72+
instance.stub(:deliver!, proc { raise "boom deliver exception" }) do
73+
CallbackMailer.test_message.deliver_now
74+
end
75+
end
76+
77+
assert_kind_of CallbackMailer, CallbackMailer.after_deliver_instance
78+
assert_nil CallbackMailer.rescue_from_error
79+
end
80+
end
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# frozen_string_literal: true
2+
3+
CallbackMailerError = Class.new(StandardError)
4+
class CallbackMailer < ActionMailer::Base
5+
cattr_accessor :rescue_from_error
6+
cattr_accessor :after_deliver_instance
7+
cattr_accessor :around_deliver_instance
8+
cattr_accessor :abort_before_deliver
9+
cattr_accessor :around_handles_error
10+
11+
rescue_from CallbackMailerError do |error|
12+
@@rescue_from_error = error
13+
end
14+
15+
before_deliver do
16+
throw :abort if @@abort_before_deliver
17+
end
18+
19+
after_deliver do
20+
@@after_deliver_instance = self
21+
end
22+
23+
around_deliver do |mailer, block|
24+
@@around_deliver_instance = self
25+
block.call
26+
rescue StandardError
27+
raise unless @@around_handles_error
28+
end
29+
30+
def test_message(*)
31+
mail(from: "[email protected]", to: "[email protected]", subject: "Test Subject", body: "Test Body")
32+
end
33+
34+
def test_raise_action
35+
raise CallbackMailerError, "boom action processing"
36+
end
37+
end

guides/source/action_mailer_basics.md

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -697,9 +697,10 @@ Action Mailer Callbacks
697697
-----------------------
698698
699699
Action Mailer allows for you to specify a [`before_action`][], [`after_action`][] and
700-
[`around_action`][].
700+
[`around_action`][] to configure the message, and [`before_deliver`][], [`after_deliver`][] and
701+
[`around_deliver`][] to control the delivery.
701702
702-
* Filters can be specified with a block or a symbol to a method in the mailer
703+
* Callbacks can be specified with a block or a symbol to a method in the mailer
703704
class similar to controllers.
704705
705706
* You could use a `before_action` to set instance variables, populate the mail
@@ -776,11 +777,16 @@ class UserMailer < ApplicationMailer
776777
end
777778
```
778779
779-
* Mailer Filters abort further processing if body is set to a non-nil value.
780+
* You could use an `after_delivery` to record the delivery of the message.
781+
782+
* Mailer callbacks abort further processing if body is set to a non-nil value. `before_deliver` can abort with `throw :abort`.
780783
781784
[`after_action`]: https://api.rubyonrails.org/classes/AbstractController/Callbacks/ClassMethods.html#method-i-after_action
785+
[`after_deliver`]: https://api.rubyonrails.org/classes/ActionMailer/Callbacks/ClassMethods.html#method-i-after_deliver
782786
[`around_action`]: https://api.rubyonrails.org/classes/AbstractController/Callbacks/ClassMethods.html#method-i-around_action
787+
[`around_deliver`]: https://api.rubyonrails.org/classes/ActionMailer/Callbacks/ClassMethods.html#method-i-around_deliver
783788
[`before_action`]: https://api.rubyonrails.org/classes/AbstractController/Callbacks/ClassMethods.html#method-i-before_action
789+
[`before_deliver`]: https://api.rubyonrails.org/classes/ActionMailer/Callbacks/ClassMethods.html#method-i-before_deliver
784790
785791
Using Action Mailer Helpers
786792
---------------------------

guides/source/active_storage_overview.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1290,6 +1290,7 @@ class Uploader {
12901290

12911291
To implement customized authentication, a new controller must be created on
12921292
the Rails application, similar to the following:
1293+
12931294
```ruby
12941295
class DirectUploadsController < ActiveStorage::DirectUploadsController
12951296
skip_before_action :verify_authenticity_token

0 commit comments

Comments
 (0)