Skip to content

Commit 1cec84a

Browse files
author
David Heinemeier Hansson
authored
Offer the option to use parameterization for shared processing of headers and ivars (#27825)
Offer the option to use parameterization for shared processing of headers and ivars
1 parent 05112b2 commit 1cec84a

6 files changed

Lines changed: 212 additions & 7 deletions

File tree

actionmailer/CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
1+
* Add parameterized invocation of mailers as a way to share before filters and defaults between actions.
2+
See ActionMailer::Parameterized for a full example of the benefit.
3+
4+
*DHH*
5+
6+
* Allow lambdas to be used as lazy defaults in addition to procs.
7+
8+
*DHH*
9+
110
* Mime type: allow to custom content type when setting body in headers
211
and attachments.
312

actionmailer/lib/action_mailer.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ module ActionMailer
4242
autoload :DeliveryMethods
4343
autoload :InlinePreviewInterceptor
4444
autoload :MailHelper
45+
autoload :Parameterized
4546
autoload :Preview
4647
autoload :Previews, "action_mailer/preview"
4748
autoload :TestCase

actionmailer/lib/action_mailer/base.rb

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -288,20 +288,19 @@ module ActionMailer
288288
# content_description: 'This is a description'
289289
# end
290290
#
291-
# Finally, Action Mailer also supports passing <tt>Proc</tt> objects into the default hash, so you
292-
# can define methods that evaluate as the message is being generated:
291+
# Finally, Action Mailer also supports passing <tt>Proc</tt> and <tt>Lambda</tt> objects into the default hash,
292+
# so you can define methods that evaluate as the message is being generated:
293293
#
294294
# class NotifierMailer < ApplicationMailer
295-
# default 'X-Special-Header' => Proc.new { my_method }
295+
# default 'X-Special-Header' => Proc.new { my_method }, to: -> { @inviter.email_address }
296296
#
297297
# private
298-
#
299298
# def my_method
300299
# 'some complex call'
301300
# end
302301
# end
303302
#
304-
# Note that the proc is evaluated right at the start of the mail message generation, so if you
303+
# Note that the proc/lambda is evaluated right at the start of the mail message generation, so if you
305304
# set something in the default hash using a proc, and then set the same thing inside of your
306305
# mailer method, it will get overwritten by the mailer method.
307306
#
@@ -324,7 +323,6 @@ module ActionMailer
324323
# end
325324
#
326325
# private
327-
#
328326
# def add_inline_attachment!
329327
# attachments.inline["footer.jpg"] = File.read('/path/to/filename.jpg')
330328
# end
@@ -434,6 +432,7 @@ module ActionMailer
434432
class Base < AbstractController::Base
435433
include DeliveryMethods
436434
include Rescuable
435+
include Parameterized
437436
include Previews
438437

439438
abstract!
@@ -888,7 +887,7 @@ def apply_defaults(headers)
888887
default_values = self.class.default.map do |key, value|
889888
[
890889
key,
891-
value.is_a?(Proc) ? instance_eval(&value) : value
890+
value.is_a?(Proc) ? instance_exec(&value) : value
892891
]
893892
end.to_h
894893

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
module ActionMailer
2+
# Provides the option to parameterize mailers in other to share ivar setup, processing, and common headers.
3+
#
4+
# Consider this example that does not use parameterization:
5+
#
6+
# class InvitationsMailer < ApplicationMailer
7+
# def account_invitation(inviter, invitee)
8+
# @account = inviter.account
9+
# @inviter = inviter
10+
# @invitee = invitee
11+
#
12+
# subject = "#{@inviter.name} invited you to their Basecamp (#{@account.name})"
13+
#
14+
# mail \
15+
# subject: subject,
16+
# to: invitee.email_address,
17+
# from: common_address(inviter),
18+
# reply_to: inviter.email_address_with_name
19+
# end
20+
#
21+
# def project_invitation(project, inviter, invitee)
22+
# @account = inviter.account
23+
# @project = project
24+
# @inviter = inviter
25+
# @invitee = invitee
26+
# @summarizer = ProjectInvitationSummarizer.new(@project.bucket)
27+
#
28+
# subject = "#{@inviter.name.familiar} added you to a project in Basecamp (#{@account.name})"
29+
#
30+
# mail \
31+
# subject: subject,
32+
# to: invitee.email_address,
33+
# from: common_address(inviter),
34+
# reply_to: inviter.email_address_with_name
35+
# end
36+
#
37+
# def bulk_project_invitation(projects, inviter, invitee)
38+
# @account = inviter.account
39+
# @projects = projects.sort_by(&:name)
40+
# @inviter = inviter
41+
# @invitee = invitee
42+
#
43+
# subject = "#{@inviter.name.familiar} added you to some new stuff in Basecamp (#{@account.name})"
44+
#
45+
# mail \
46+
# subject: subject,
47+
# to: invitee.email_address,
48+
# from: common_address(inviter),
49+
# reply_to: inviter.email_address_with_name
50+
# end
51+
# end
52+
#
53+
# InvitationsMailer.account_invitation(person_a, person_b).deliver_later
54+
#
55+
# Using parameterized mailers, this can be rewritten as:
56+
#
57+
# class InvitationsMailer < ApplicationMailer
58+
# before_action { @inviter, @invitee = params[:inviter], params[:invitee] }
59+
# before_action { @account = params[:inviter].account }
60+
#
61+
# default to: -> { @invitee.email_address },
62+
# from: -> { common_address(@inviter) },
63+
# reply_to: -> { @inviter.email_address_with_name }
64+
#
65+
# def account_invitation
66+
# mail subject: "#{@inviter.name} invited you to their Basecamp (#{@account.name})"
67+
# end
68+
#
69+
# def project_invitation
70+
# @project = params[:project]
71+
# @summarizer = ProjectInvitationSummarizer.new(@project.bucket)
72+
#
73+
# mail subject: "#{@inviter.name.familiar} added you to a project in Basecamp (#{@account.name})"
74+
# end
75+
#
76+
# def bulk_project_invitation
77+
# @projects = params[:projects].sort_by(&:name)
78+
#
79+
# mail subject: "#{@inviter.name.familiar} added you to some new stuff in Basecamp (#{@account.name})"
80+
# end
81+
# end
82+
#
83+
# InvitationsMailer.with(inviter: person_a, invitee: person_b).account_invitation.deliver_later
84+
#
85+
# That's a big improvement! It's also fully backwards compatible. So you can start to gradually transition
86+
# mailers that stand to benefit the most from parameterization one by one and leave the others behind.
87+
module Parameterized
88+
extend ActiveSupport::Concern
89+
90+
included do
91+
attr_accessor :params
92+
end
93+
94+
class_methods do
95+
def with(params)
96+
ActionMailer::Parameterized::Mailer.new(self, params)
97+
end
98+
end
99+
100+
class Mailer
101+
def initialize(mailer, params)
102+
@mailer, @params = mailer, params
103+
end
104+
105+
def method_missing(method_name, *args)
106+
if @mailer.action_methods.include?(method_name.to_s)
107+
ActionMailer::Parameterized::MessageDelivery.new(@mailer, method_name, *args).tap { |pmd| pmd.params = @params }
108+
else
109+
super
110+
end
111+
end
112+
end
113+
114+
class MessageDelivery < ActionMailer::MessageDelivery
115+
attr_accessor :params
116+
117+
private
118+
def processed_mailer
119+
@processed_mailer ||= @mailer_class.new.tap do |mailer|
120+
mailer.params = params
121+
mailer.process @action, *@args
122+
end
123+
end
124+
125+
def enqueue_delivery(delivery_method, options = {})
126+
if processed?
127+
super
128+
else
129+
args = @mailer_class.name, @action.to_s, delivery_method.to_s, @params, *@args
130+
ActionMailer::Parameterized::DeliveryJob.set(options).perform_later(*args)
131+
end
132+
end
133+
end
134+
135+
class DeliveryJob < ActionMailer::DeliveryJob # :nodoc:
136+
def perform(mailer, mail_method, delivery_method, params, *args) #:nodoc:
137+
mailer.constantize.with(params).public_send(mail_method, *args).send(delivery_method)
138+
end
139+
end
140+
end
141+
end
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
class ParamsMailer < ActionMailer::Base
2+
before_action { @inviter, @invitee = params[:inviter], params[:invitee] }
3+
4+
default to: Proc.new { @invitee }, from: -> { @inviter }
5+
6+
def invitation
7+
mail(subject: "Welcome to the project!") do |format|
8+
format.text { render plain: "So says #{@inviter}" }
9+
end
10+
end
11+
end
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
require "abstract_unit"
2+
require "active_job"
3+
require "mailers/params_mailer"
4+
5+
class ParameterizedTest < ActiveSupport::TestCase
6+
include ActiveJob::TestHelper
7+
8+
setup do
9+
@previous_logger = ActiveJob::Base.logger
10+
ActiveJob::Base.logger = Logger.new(nil)
11+
12+
@original_delivery_method = ActionMailer::Base.delivery_method
13+
ActionMailer::Base.delivery_method = :test
14+
15+
@previous_delivery_method = ActionMailer::Base.delivery_method
16+
@previous_deliver_later_queue_name = ActionMailer::Base.deliver_later_queue_name
17+
ActionMailer::Base.deliver_later_queue_name = :test_queue
18+
ActionMailer::Base.delivery_method = :test
19+
20+
@mail = ParamsMailer.with(inviter: "david@basecamp.com", invitee: "jason@basecamp.com").invitation
21+
end
22+
23+
teardown do
24+
ActiveJob::Base.logger = @previous_logger
25+
ParamsMailer.deliveries.clear
26+
27+
ActionMailer::Base.delivery_method = @original_delivery_method
28+
29+
ActionMailer::Base.delivery_method = @previous_delivery_method
30+
ActionMailer::Base.deliver_later_queue_name = @previous_deliver_later_queue_name
31+
end
32+
33+
test "parameterized headers" do
34+
assert_equal(["jason@basecamp.com"], @mail.to)
35+
assert_equal(["david@basecamp.com"], @mail.from)
36+
assert_equal("So says david@basecamp.com", @mail.body.encoded)
37+
end
38+
39+
test "should enqueue the email with params" do
40+
assert_performed_with(job: ActionMailer::Parameterized::DeliveryJob, args: ["ParamsMailer", "invitation", "deliver_now", { inviter: "david@basecamp.com", invitee: "jason@basecamp.com" } ]) do
41+
@mail.deliver_later
42+
end
43+
end
44+
end

0 commit comments

Comments
 (0)