Skip to content

Commit 6cd8ddd

Browse files
authored
Merge pull request rails#47630 from bensheldon/action_mailer_around_delivery
Add `*_deliver` callbacks for Action Mailer
2 parents 946fc17 + 468d806 commit 6cd8ddd

File tree

8 files changed

+190
-10
lines changed

8 files changed

+190
-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
@@ -44,6 +44,7 @@ module ActionMailer
4444
end
4545

4646
autoload :Base
47+
autoload :Callbacks
4748
autoload :DeliveryMethods
4849
autoload :InlinePreviewInterceptor
4950
autoload :MailHelper

actionmailer/lib/action_mailer/base.rb

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -318,12 +318,14 @@ module ActionMailer
318318
#
319319
# = Callbacks
320320
#
321-
# You can specify callbacks using <tt>before_action</tt> and <tt>after_action</tt> for configuring your messages.
322-
# This may be useful, for example, when you want to add default inline attachments for all
323-
# messages sent out by a certain mailer class:
321+
# You can specify callbacks using <tt>before_action</tt> and <tt>after_action</tt> for configuring your messages,
322+
# and using <tt>before_deliver</tt> and <tt>after_deliver</tt> for wrapping the delivery process.
323+
# For example, when you want to add default inline attachments and log delivery for all messages
324+
# sent out by a certain mailer class:
324325
#
325326
# class NotifierMailer < ApplicationMailer
326327
# before_action :add_inline_attachment!
328+
# after_deliver :log_delivery
327329
#
328330
# def welcome
329331
# mail
@@ -333,9 +335,13 @@ module ActionMailer
333335
# def add_inline_attachment!
334336
# attachments.inline["footer.jpg"] = File.read('/path/to/filename.jpg')
335337
# end
338+
#
339+
# def log_delivery
340+
# Rails.logger.info "Sent email with message id '#{message.message_id}' at #{Time.current}."
341+
# end
336342
# end
337343
#
338-
# Callbacks in Action Mailer are implemented using
344+
# Action callbacks in Action Mailer are implemented using
339345
# AbstractController::Callbacks, so you can define and configure
340346
# callbacks in the same manner that you would use callbacks in classes that
341347
# inherit from ActionController::Base.
@@ -468,6 +474,7 @@ module ActionMailer
468474
# * <tt>deliver_later_queue_name</tt> - The queue name used by <tt>deliver_later</tt> with the default
469475
# <tt>delivery_job</tt>. Mailers can set this to use a custom queue name.
470476
class Base < AbstractController::Base
477+
include Callbacks
471478
include DeliveryMethods
472479
include QueuedDelivery
473480
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
@@ -110,7 +110,9 @@ def deliver_later(options = {})
110110
#
111111
def deliver_now!
112112
processed_mailer.handle_exceptions do
113-
message.deliver!
113+
processed_mailer.run_callbacks(:deliver) do
114+
message.deliver!
115+
end
114116
end
115117
end
116118

@@ -120,13 +122,15 @@ def deliver_now!
120122
#
121123
def deliver_now
122124
processed_mailer.handle_exceptions do
123-
message.deliver
125+
processed_mailer.run_callbacks(:deliver) do
126+
message.deliver
127+
end
124128
end
125129
end
126130

127131
private
128132
# Returns the processed Mailer instance. We keep this instance
129-
# on hand so we can delegate exception handling to it.
133+
# on hand so we can run callbacks and delegate exception handling to it.
130134
def processed_mailer
131135
@processed_mailer ||= @mailer_class.new.tap do |mailer|
132136
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
---------------------------

0 commit comments

Comments
 (0)