Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Offer the option to use parameterization for shared processing of headers and ivars #27825

Merged
merged 9 commits into from Jan 28, 2017
9 changes: 9 additions & 0 deletions actionmailer/CHANGELOG.md
@@ -1,3 +1,12 @@
* Add parameterized invocation of mailers as a way to share before filters and defaults between actions.
See ActionMailer::Parameterized for a full example of the benefit.

*DHH*

* Allow lambdas to be used as lazy defaults in addition to procs.

*DHH*

* Mime type: allow to custom content type when setting body in headers
and attachments.

Expand Down
1 change: 1 addition & 0 deletions actionmailer/lib/action_mailer.rb
Expand Up @@ -42,6 +42,7 @@ module ActionMailer
autoload :DeliveryMethods
autoload :InlinePreviewInterceptor
autoload :MailHelper
autoload :Parameterized
autoload :Preview
autoload :Previews, "action_mailer/preview"
autoload :TestCase
Expand Down
13 changes: 6 additions & 7 deletions actionmailer/lib/action_mailer/base.rb
Expand Up @@ -288,20 +288,19 @@ module ActionMailer
# content_description: 'This is a description'
# end
#
# Finally, Action Mailer also supports passing <tt>Proc</tt> objects into the default hash, so you
# can define methods that evaluate as the message is being generated:
# Finally, Action Mailer also supports passing <tt>Proc</tt> and <tt>Lambda</tt> objects into the default hash,
# so you can define methods that evaluate as the message is being generated:
#
# class NotifierMailer < ApplicationMailer
# default 'X-Special-Header' => Proc.new { my_method }
# default 'X-Special-Header' => Proc.new { my_method }, to: -> { @inviter.email_address }
#
# private
#
# def my_method
# 'some complex call'
# end
# end
#
# Note that the proc is evaluated right at the start of the mail message generation, so if you
# Note that the proc/lambda is evaluated right at the start of the mail message generation, so if you
# set something in the default hash using a proc, and then set the same thing inside of your
# mailer method, it will get overwritten by the mailer method.
#
Expand All @@ -324,7 +323,6 @@ module ActionMailer
# end
#
# private
#
# def add_inline_attachment!
# attachments.inline["footer.jpg"] = File.read('/path/to/filename.jpg')
# end
Expand Down Expand Up @@ -434,6 +432,7 @@ module ActionMailer
class Base < AbstractController::Base
include DeliveryMethods
include Rescuable
include Parameterized
include Previews

abstract!
Expand Down Expand Up @@ -888,7 +887,7 @@ def apply_defaults(headers)
default_values = self.class.default.map do |key, value|
[
key,
value.is_a?(Proc) ? instance_eval(&value) : value
value.is_a?(Proc) ? instance_exec(&value) : value
]
end.to_h

Expand Down
141 changes: 141 additions & 0 deletions actionmailer/lib/action_mailer/parameterized.rb
@@ -0,0 +1,141 @@
module ActionMailer
# Provides the option to parameterize mailers in other to share ivar setup, processing, and common headers.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"in other" -> "in order"

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd also expand instance variable.

#
# Consider this example that does not use parameterization:
#
# class InvitationsMailer < ApplicationMailer
# def account_invitation(inviter, invitee)
# @account = inviter.account
# @inviter = inviter
# @invitee = invitee
#
# subject = "#{@inviter.name} invited you to their Basecamp (#{@account.name})"
#
# mail \
# subject: subject,
# to: invitee.email_address,
# from: common_address(inviter),
# reply_to: inviter.email_address_with_name
# end
#
# def project_invitation(project, inviter, invitee)
# @account = inviter.account
# @project = project
# @inviter = inviter
# @invitee = invitee
# @summarizer = ProjectInvitationSummarizer.new(@project.bucket)
#
# subject = "#{@inviter.name.familiar} added you to a project in Basecamp (#{@account.name})"
#
# mail \
# subject: subject,
# to: invitee.email_address,
# from: common_address(inviter),
# reply_to: inviter.email_address_with_name
# end
#
# def bulk_project_invitation(projects, inviter, invitee)
# @account = inviter.account
# @projects = projects.sort_by(&:name)
# @inviter = inviter
# @invitee = invitee
#
# subject = "#{@inviter.name.familiar} added you to some new stuff in Basecamp (#{@account.name})"
#
# mail \
# subject: subject,
# to: invitee.email_address,
# from: common_address(inviter),
# reply_to: inviter.email_address_with_name
# end
# end
#
# InvitationsMailer.account_invitation(person_a, person_b).deliver_later
#
# Using parameterized mailers, this can be rewritten as:
#
# class InvitationsMailer < ApplicationMailer
# before_action { @inviter, @invitee = params[:inviter], params[:invitee] }
# before_action { @account = params[:inviter].account }
#
# default to: -> { @invitee.email_address },
# from: -> { common_address(@inviter) },
# reply_to: -> { @inviter.email_address_with_name }
#
# def account_invitation
# mail subject: "#{@inviter.name} invited you to their Basecamp (#{@account.name})"
# end
#
# def project_invitation
# @project = params[:project]
# @summarizer = ProjectInvitationSummarizer.new(@project.bucket)
#
# mail subject: "#{@inviter.name.familiar} added you to a project in Basecamp (#{@account.name})"
# end
#
# def bulk_project_invitation
# @projects = params[:projects].sort_by(&:name)
#
# mail subject: "#{@inviter.name.familiar} added you to some new stuff in Basecamp (#{@account.name})"
# end
# end
#
# InvitationsMailer.with(inviter: person_a, invitee: person_b).account_invitation.deliver_later
#
# That's a big improvement! It's also fully backwards compatible. So you can start to gradually transition
# mailers that stand to benefit the most from parameterization one by one and leave the others behind.
module Parameterized
extend ActiveSupport::Concern

included do
attr_accessor :params
end

class_methods do
def with(params)
ActionMailer::Parameterized::Mailer.new(self, params)
end
end

class Mailer
def initialize(mailer, params)
@mailer, @params = mailer, params
end

def method_missing(method_name, *args)
if @mailer.action_methods.include?(method_name.to_s)
ActionMailer::Parameterized::MessageDelivery.new(@mailer, method_name, *args).tap { |pmd| pmd.params = @params }
else
super
end
end
end

class MessageDelivery < ActionMailer::MessageDelivery
attr_accessor :params

private
def processed_mailer
@processed_mailer ||= @mailer_class.new.tap do |mailer|
mailer.params = params
mailer.process @action, *@args
end
end

def enqueue_delivery(delivery_method, options = {})
if processed?
super
else
args = @mailer_class.name, @action.to_s, delivery_method.to_s, @params, *@args
ActionMailer::Parameterized::DeliveryJob.set(options).perform_later(*args)
end
end
end

class DeliveryJob < ActionMailer::DeliveryJob # :nodoc:
def perform(mailer, mail_method, delivery_method, params, *args) #:nodoc:
mailer.constantize.with(params).public_send(mail_method, *args).send(delivery_method)
end
end
end
end
11 changes: 11 additions & 0 deletions actionmailer/test/mailers/params_mailer.rb
@@ -0,0 +1,11 @@
class ParamsMailer < ActionMailer::Base
before_action { @inviter, @invitee = params[:inviter], params[:invitee] }

default to: Proc.new { @invitee }, from: -> { @inviter }

def invitation
mail(subject: "Welcome to the project!") do |format|
format.text { render plain: "So says #{@inviter}" }
end
end
end
44 changes: 44 additions & 0 deletions actionmailer/test/parameterized_test.rb
@@ -0,0 +1,44 @@
require "abstract_unit"
require "active_job"
require "mailers/params_mailer"

class ParameterizedTest < ActiveSupport::TestCase
include ActiveJob::TestHelper

setup do
@previous_logger = ActiveJob::Base.logger
ActiveJob::Base.logger = Logger.new(nil)

@original_delivery_method = ActionMailer::Base.delivery_method
ActionMailer::Base.delivery_method = :test

@previous_delivery_method = ActionMailer::Base.delivery_method
@previous_deliver_later_queue_name = ActionMailer::Base.deliver_later_queue_name
ActionMailer::Base.deliver_later_queue_name = :test_queue
ActionMailer::Base.delivery_method = :test

@mail = ParamsMailer.with(inviter: "david@basecamp.com", invitee: "jason@basecamp.com").invitation
end

teardown do
ActiveJob::Base.logger = @previous_logger
ParamsMailer.deliveries.clear

ActionMailer::Base.delivery_method = @original_delivery_method

ActionMailer::Base.delivery_method = @previous_delivery_method
ActionMailer::Base.deliver_later_queue_name = @previous_deliver_later_queue_name
end

test "parameterized headers" do
assert_equal(["jason@basecamp.com"], @mail.to)
assert_equal(["david@basecamp.com"], @mail.from)
assert_equal("So says david@basecamp.com", @mail.body.encoded)
end

test "should enqueue the email with params" do
assert_performed_with(job: ActionMailer::Parameterized::DeliveryJob, args: ["ParamsMailer", "invitation", "deliver_now", { inviter: "david@basecamp.com", invitee: "jason@basecamp.com" } ]) do
@mail.deliver_later
end
end
end