actionmailer 7.0.8 → 7.1.3.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -3,6 +3,8 @@
3
3
  require "tmpdir"
4
4
 
5
5
  module ActionMailer
6
+ # = Action Mailer \DeliveryMethods
7
+ #
6
8
  # This module handles everything related to mail delivery, from registering
7
9
  # new delivery methods to configuring the mail object to be sent.
8
10
  module DeliveryMethods
@@ -12,7 +14,6 @@ module ActionMailer
12
14
  # Do not make this inheritable, because we always want it to propagate
13
15
  cattr_accessor :raise_delivery_errors, default: true
14
16
  cattr_accessor :perform_deliveries, default: true
15
- cattr_accessor :deliver_later_queue_name, default: :mailers
16
17
 
17
18
  class_attribute :delivery_methods, default: {}.freeze
18
19
  class_attribute :delivery_method, default: :smtp
@@ -31,7 +32,8 @@ module ActionMailer
31
32
 
32
33
  add_delivery_method :sendmail, Mail::Sendmail,
33
34
  location: "/usr/sbin/sendmail",
34
- arguments: "-i"
35
+ # See breaking change in the mail gem - https://github.com/mikel/mail/commit/7e1196bd29815a0901d7290c82a332c0959b163a
36
+ arguments: Gem::Version.new(Mail::VERSION.version) >= Gem::Version.new("2.8.0") ? %w[-i] : "-i"
35
37
 
36
38
  add_delivery_method :test, Mail::TestMailer
37
39
  end
@@ -46,7 +48,7 @@ module ActionMailer
46
48
  #
47
49
  # add_delivery_method :sendmail, Mail::Sendmail,
48
50
  # location: '/usr/sbin/sendmail',
49
- # arguments: '-i'
51
+ # arguments: %w[ -i ]
50
52
  def add_delivery_method(symbol, klass, default_options = {})
51
53
  class_attribute(:"#{symbol}_settings") unless respond_to?(:"#{symbol}_settings")
52
54
  public_send(:"#{symbol}_settings=", default_options)
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionMailer
4
+ def self.deprecator # :nodoc:
5
+ @deprecator ||= ActiveSupport::Deprecation.new
6
+ end
7
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionMailer
4
+ # = Action Mailer Form Builder
5
+ #
6
+ # Override the default form builder for all views rendered by this
7
+ # mailer and any of its descendants. Accepts a subclass of
8
+ # ActionView::Helpers::FormBuilder.
9
+ #
10
+ # While emails typically will not include forms, this can be used
11
+ # by views that are shared between controllers and mailers.
12
+ #
13
+ # For more information, see +ActionController::FormBuilder+.
14
+ module FormBuilder
15
+ extend ActiveSupport::Concern
16
+
17
+ included do
18
+ class_attribute :_default_form_builder, instance_accessor: false
19
+ end
20
+
21
+ module ClassMethods
22
+ # Set the form builder to be used as the default for all forms
23
+ # in the views rendered by this mailer and its subclasses.
24
+ #
25
+ # ==== Parameters
26
+ # * <tt>builder</tt> - Default form builder, an instance of ActionView::Helpers::FormBuilder
27
+ def default_form_builder(builder)
28
+ self._default_form_builder = builder
29
+ end
30
+ end
31
+
32
+ # Default form builder for the mailer
33
+ def default_form_builder
34
+ self.class._default_form_builder
35
+ end
36
+ end
37
+ end
@@ -1,16 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActionMailer
4
- # Returns the currently loaded version of Action Mailer as a <tt>Gem::Version</tt>.
4
+ # Returns the currently loaded version of Action Mailer as a +Gem::Version+.
5
5
  def self.gem_version
6
6
  Gem::Version.new VERSION::STRING
7
7
  end
8
8
 
9
9
  module VERSION
10
10
  MAJOR = 7
11
- MINOR = 0
12
- TINY = 8
13
- PRE = nil
11
+ MINOR = 1
12
+ TINY = 3
13
+ PRE = "2"
14
14
 
15
15
  STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
16
16
  end
@@ -3,6 +3,8 @@
3
3
  require "base64"
4
4
 
5
5
  module ActionMailer
6
+ # = Action Mailer \InlinePreviewInterceptor
7
+ #
6
8
  # Implements a mailer preview interceptor that converts image tag src attributes
7
9
  # that use inline cid: style URLs to data: style URLs so that they are visible
8
10
  # when previewing an HTML email in a web browser.
@@ -3,14 +3,17 @@
3
3
  require "active_support/log_subscriber"
4
4
 
5
5
  module ActionMailer
6
+ # = Action Mailer \LogSubscriber
7
+ #
6
8
  # Implements the ActiveSupport::LogSubscriber for logging notifications when
7
9
  # email is delivered or received.
8
10
  class LogSubscriber < ActiveSupport::LogSubscriber
9
11
  # An email was delivered.
10
12
  def deliver(event)
11
13
  info do
12
- perform_deliveries = event.payload[:perform_deliveries]
13
- if perform_deliveries
14
+ if exception = event.payload[:exception_object]
15
+ "Failed delivery of mail #{event.payload[:message_id]} error_class=#{exception.class} error_message=#{exception.message.inspect}"
16
+ elsif event.payload[:perform_deliveries]
14
17
  "Delivered mail #{event.payload[:message_id]} (#{event.duration.round(1)}ms)"
15
18
  else
16
19
  "Skipped delivery of mail #{event.payload[:message_id]} as `perform_deliveries` is false"
@@ -19,6 +22,7 @@ module ActionMailer
19
22
 
20
23
  debug { event.payload[:mail] }
21
24
  end
25
+ subscribe_log_level :deliver, :debug
22
26
 
23
27
  # An email was generated.
24
28
  def process(event)
@@ -28,6 +32,7 @@ module ActionMailer
28
32
  "#{mailer}##{action}: processed outbound mail in #{event.duration.round(1)}ms"
29
33
  end
30
34
  end
35
+ subscribe_log_level :process, :debug
31
36
 
32
37
  # Use the logger configured for ActionMailer::Base.
33
38
  def logger
@@ -3,13 +3,18 @@
3
3
  require "active_job"
4
4
 
5
5
  module ActionMailer
6
- # The <tt>ActionMailer::MailDeliveryJob</tt> class is used when you
6
+ # = Action Mailer \MailDeliveryJob
7
+ #
8
+ # The +ActionMailer::MailDeliveryJob+ class is used when you
7
9
  # want to send emails outside of the request-response cycle. It supports
8
10
  # sending either parameterized or normal mail.
9
11
  #
10
12
  # Exceptions are rescued and handled by the mailer class.
11
13
  class MailDeliveryJob < ActiveJob::Base # :nodoc:
12
- queue_as { ActionMailer::Base.deliver_later_queue_name }
14
+ queue_as do
15
+ mailer_class = arguments.first.constantize
16
+ mailer_class.deliver_later_queue_name
17
+ end
13
18
 
14
19
  rescue_from StandardError, with: :handle_exception_with_mailer_class
15
20
 
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActionMailer
4
+ # = Action Mailer \MailHelper
5
+ #
4
6
  # Provides helper methods for ActionMailer::Base that can be used for easily
5
7
  # formatting messages, accessing mailer or message instances, and the
6
8
  # attachments list.
@@ -3,11 +3,13 @@
3
3
  require "delegate"
4
4
 
5
5
  module ActionMailer
6
- # The <tt>ActionMailer::MessageDelivery</tt> class is used by
6
+ # = Action Mailer \MessageDelivery
7
+ #
8
+ # The +ActionMailer::MessageDelivery+ class is used by
7
9
  # ActionMailer::Base when creating a new mailer.
8
10
  # <tt>MessageDelivery</tt> is a wrapper (+Delegator+ subclass) around a lazy
9
- # created <tt>Mail::Message</tt>. You can get direct access to the
10
- # <tt>Mail::Message</tt>, deliver the email or schedule the email to be sent
11
+ # created +Mail::Message+. You can get direct access to the
12
+ # +Mail::Message+, deliver the email or schedule the email to be sent
11
13
  # through Active Job.
12
14
  #
13
15
  # Notifier.welcome(User.first) # an ActionMailer::MessageDelivery object
@@ -62,9 +64,10 @@ module ActionMailer
62
64
  # * <tt>:queue</tt> - Enqueue the email on the specified queue
63
65
  # * <tt>:priority</tt> - Enqueues the email with the specified priority
64
66
  #
65
- # By default, the email will be enqueued using <tt>ActionMailer::MailDeliveryJob</tt>. Each
66
- # ActionMailer::Base class can specify the job to use by setting the class variable
67
- # +delivery_job+.
67
+ # By default, the email will be enqueued using ActionMailer::MailDeliveryJob on
68
+ # the default queue. Mailer classes can customize the queue name used for the default
69
+ # job by assigning a +deliver_later_queue_name+ class variable, or provide a custom job
70
+ # by assigning a +delivery_job+. When a custom job is used, it controls the queue name.
68
71
  #
69
72
  # class AccountRegistrationMailer < ApplicationMailer
70
73
  # self.delivery_job = RegistrationDeliveryJob
@@ -88,9 +91,10 @@ module ActionMailer
88
91
  # * <tt>:queue</tt> - Enqueue the email on the specified queue.
89
92
  # * <tt>:priority</tt> - Enqueues the email with the specified priority
90
93
  #
91
- # By default, the email will be enqueued using <tt>ActionMailer::MailDeliveryJob</tt>. Each
92
- # ActionMailer::Base class can specify the job to use by setting the class variable
93
- # +delivery_job+.
94
+ # By default, the email will be enqueued using ActionMailer::MailDeliveryJob on
95
+ # the default queue. Mailer classes can customize the queue name used for the default
96
+ # job by assigning a +deliver_later_queue_name+ class variable, or provide a custom job
97
+ # by assigning a +delivery_job+. When a custom job is used, it controls the queue name.
94
98
  #
95
99
  # class AccountRegistrationMailer < ApplicationMailer
96
100
  # self.delivery_job = RegistrationDeliveryJob
@@ -106,7 +110,9 @@ module ActionMailer
106
110
  #
107
111
  def deliver_now!
108
112
  processed_mailer.handle_exceptions do
109
- message.deliver!
113
+ processed_mailer.run_callbacks(:deliver) do
114
+ message.deliver!
115
+ end
110
116
  end
111
117
  end
112
118
 
@@ -116,13 +122,15 @@ module ActionMailer
116
122
  #
117
123
  def deliver_now
118
124
  processed_mailer.handle_exceptions do
119
- message.deliver
125
+ processed_mailer.run_callbacks(:deliver) do
126
+ message.deliver
127
+ end
120
128
  end
121
129
  end
122
130
 
123
131
  private
124
132
  # Returns the processed Mailer instance. We keep this instance
125
- # on hand so we can delegate exception handling to it.
133
+ # on hand so we can run callbacks and delegate exception handling to it.
126
134
  def processed_mailer
127
135
  @processed_mailer ||= @mailer_class.new.tap do |mailer|
128
136
  mailer.process @action, *@args
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActionMailer
4
+ # = Action Mailer \Parameterized
5
+ #
4
6
  # Provides the option to parameterize mailers in order to share instance variable
5
7
  # setup, processing, and common headers.
6
8
  #
@@ -88,7 +90,11 @@ module ActionMailer
88
90
  extend ActiveSupport::Concern
89
91
 
90
92
  included do
91
- attr_accessor :params
93
+ attr_writer :params
94
+
95
+ def params
96
+ @params ||= {}
97
+ end
92
98
  end
93
99
 
94
100
  module ClassMethods
@@ -7,11 +7,11 @@ module ActionMailer
7
7
  extend ActiveSupport::Concern
8
8
 
9
9
  included do
10
- # Set the location of mailer previews through app configuration:
10
+ # Add the location of mailer previews through app configuration:
11
11
  #
12
- # config.action_mailer.preview_path = "#{Rails.root}/lib/mailer_previews"
12
+ # config.action_mailer.preview_paths << "#{Rails.root}/lib/mailer_previews"
13
13
  #
14
- mattr_accessor :preview_path, instance_writer: false
14
+ mattr_accessor :preview_paths, instance_writer: false, default: []
15
15
 
16
16
  # Enable or disable mailer previews through app configuration:
17
17
  #
@@ -25,7 +25,31 @@ module ActionMailer
25
25
  mattr_accessor :preview_interceptors, instance_writer: false, default: [ActionMailer::InlinePreviewInterceptor]
26
26
  end
27
27
 
28
+ def preview_path
29
+ ActionMailer.deprecator.warn(<<-MSG.squish)
30
+ Using preview_path option is deprecated and will be removed in Rails 7.2.
31
+ Please use preview_paths instead.
32
+ MSG
33
+ self.class.preview_paths.first
34
+ end
35
+
28
36
  module ClassMethods
37
+ def preview_path=(value)
38
+ ActionMailer.deprecator.warn(<<-MSG.squish)
39
+ Using preview_path= option is deprecated and will be removed in Rails 7.2.
40
+ Please use preview_paths= instead.
41
+ MSG
42
+ self.preview_paths << value
43
+ end
44
+
45
+ def preview_path
46
+ ActionMailer.deprecator.warn(<<-MSG.squish)
47
+ Using preview_path option is deprecated and will be removed in Rails 7.2.
48
+ Please use preview_paths instead.
49
+ MSG
50
+ self.preview_paths.first
51
+ end
52
+
29
53
  # Register one or more Interceptors which will be called before mail is previewed.
30
54
  def register_preview_interceptors(*interceptors)
31
55
  interceptors.flatten.compact.each { |interceptor| register_preview_interceptor(interceptor) }
@@ -79,7 +103,7 @@ module ActionMailer
79
103
  # Returns all mailer preview classes.
80
104
  def all
81
105
  load_previews if descendants.empty?
82
- descendants
106
+ descendants.sort_by { |mailer| mailer.name.titleize }
83
107
  end
84
108
 
85
109
  # Returns the mail object for the given email name. The registered preview
@@ -119,13 +143,13 @@ module ActionMailer
119
143
 
120
144
  private
121
145
  def load_previews
122
- if preview_path
123
- Dir["#{preview_path}/**/*_preview.rb"].sort.each { |file| require_dependency file }
146
+ preview_paths.each do |preview_path|
147
+ Dir["#{preview_path}/**/*_preview.rb"].sort.each { |file| require file }
124
148
  end
125
149
  end
126
150
 
127
- def preview_path
128
- Base.preview_path
151
+ def preview_paths
152
+ Base.preview_paths
129
153
  end
130
154
 
131
155
  def show_previews
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionMailer
4
+ module QueuedDelivery
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ class_attribute :delivery_job, default: ::ActionMailer::MailDeliveryJob
9
+ class_attribute :deliver_later_queue_name, default: :mailers
10
+ end
11
+ end
12
+ end
@@ -8,8 +8,13 @@ require "abstract_controller/railties/routes_helpers"
8
8
  module ActionMailer
9
9
  class Railtie < Rails::Railtie # :nodoc:
10
10
  config.action_mailer = ActiveSupport::OrderedOptions.new
11
+ config.action_mailer.preview_paths = []
11
12
  config.eager_load_namespaces << ActionMailer
12
13
 
14
+ initializer "action_mailer.deprecator", before: :load_environment_config do |app|
15
+ app.deprecators[:action_mailer] = ActionMailer.deprecator
16
+ end
17
+
13
18
  initializer "action_mailer.logger" do
14
19
  ActiveSupport.on_load(:action_mailer) { self.logger ||= Rails.logger }
15
20
  end
@@ -23,10 +28,7 @@ module ActionMailer
23
28
  options.stylesheets_dir ||= paths["public/stylesheets"].first
24
29
  options.show_previews = Rails.env.development? if options.show_previews.nil?
25
30
  options.cache_store ||= Rails.cache
26
-
27
- if options.show_previews
28
- options.preview_path ||= defined?(Rails.root) ? "#{Rails.root}/test/mailers/previews" : nil
29
- end
31
+ options.preview_paths |= ["#{Rails.root}/test/mailers/previews"]
30
32
 
31
33
  # make sure readers methods get compiled
32
34
  options.asset_host ||= app.config.asset_host
@@ -40,6 +42,7 @@ module ActionMailer
40
42
  register_interceptors(options.delete(:interceptors))
41
43
  register_preview_interceptors(options.delete(:preview_interceptors))
42
44
  register_observers(options.delete(:observers))
45
+ self.preview_paths |= options[:preview_paths]
43
46
 
44
47
  if delivery_job = options.delete(:delivery_job)
45
48
  self.delivery_job = delivery_job.constantize
@@ -65,12 +68,9 @@ module ActionMailer
65
68
  end
66
69
  end
67
70
 
68
- initializer "action_mailer.set_autoload_paths" do |app|
71
+ initializer "action_mailer.set_autoload_paths", before: :set_autoload_paths do |app|
69
72
  options = app.config.action_mailer
70
-
71
- if options.show_previews && options.preview_path
72
- ActiveSupport::Dependencies.autoload_paths << options.preview_path
73
- end
73
+ app.config.paths["test/mailers/previews"].concat(options.preview_paths)
74
74
  end
75
75
 
76
76
  initializer "action_mailer.compile_config_methods" do
@@ -79,18 +79,13 @@ module ActionMailer
79
79
  end
80
80
  end
81
81
 
82
- initializer "action_mailer.eager_load_actions" do
83
- ActiveSupport.on_load(:after_initialize) do
84
- ActionMailer::Base.descendants.each(&:action_methods) if config.eager_load
85
- end
86
- end
87
-
88
82
  config.after_initialize do |app|
89
83
  options = app.config.action_mailer
90
84
 
91
85
  if options.show_previews
92
86
  app.routes.prepend do
93
- get "/rails/mailers" => "rails/mailers#index", internal: true
87
+ get "/rails/mailers" => "rails/mailers#index", internal: true
88
+ get "/rails/mailers/download/*path" => "rails/mailers#download", internal: true
94
89
  get "/rails/mailers/*path" => "rails/mailers#preview", internal: true
95
90
  end
96
91
  end
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActionMailer # :nodoc:
4
+ # = Action Mailer \Rescuable
5
+ #
4
6
  # Provides
5
7
  # {rescue_from}[rdoc-ref:ActiveSupport::Rescuable::ClassMethods#rescue_from]
6
8
  # for mailers. Wraps mailer action processing, mail job processing, and mail
@@ -74,6 +74,15 @@ module ActionMailer
74
74
  end
75
75
  end
76
76
 
77
+ # Reads the fixture file for the given mailer.
78
+ #
79
+ # This is useful when testing mailers by being able to write the body of
80
+ # an email inside a fixture. See the testing guide for a concrete example:
81
+ # https://guides.rubyonrails.org/testing.html#revenge-of-the-fixtures
82
+ def read_fixture(action)
83
+ IO.readlines(File.join(Rails.root, "test", "fixtures", self.class.mailer_class.name.underscore, action))
84
+ end
85
+
77
86
  private
78
87
  def initialize_test_deliveries
79
88
  set_delivery_method :test
@@ -110,10 +119,6 @@ module ActionMailer
110
119
  def encode(subject)
111
120
  Mail::Encodings.q_value_encode(subject, charset)
112
121
  end
113
-
114
- def read_fixture(action)
115
- IO.readlines(File.join(Rails.root, "test", "fixtures", self.class.mailer_class.name.underscore, action))
116
- end
117
122
  end
118
123
 
119
124
  include Behavior
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "active_support/core_ext/array/extract_options"
3
4
  require "active_job"
4
5
 
5
6
  module ActionMailer
@@ -33,10 +34,8 @@ module ActionMailer
33
34
  # end
34
35
  def assert_emails(number, &block)
35
36
  if block_given?
36
- original_count = ActionMailer::Base.deliveries.size
37
- perform_enqueued_jobs(only: ->(job) { delivery_job_filter(job) }, &block)
38
- new_count = ActionMailer::Base.deliveries.size
39
- assert_equal number, new_count - original_count, "#{number} emails expected, but #{new_count - original_count} were sent"
37
+ diff = capture_emails(&block).length
38
+ assert_equal number, diff, "#{number} emails expected, but #{diff} were sent"
40
39
  else
41
40
  assert_equal number, ActionMailer::Base.deliveries.size
42
41
  end
@@ -94,18 +93,50 @@ module ActionMailer
94
93
  end
95
94
 
96
95
  # Asserts that a specific email has been enqueued, optionally
97
- # matching arguments.
96
+ # matching arguments and/or params.
98
97
  #
99
98
  # def test_email
100
99
  # ContactMailer.welcome.deliver_later
101
100
  # assert_enqueued_email_with ContactMailer, :welcome
102
101
  # end
103
102
  #
103
+ # def test_email_with_parameters
104
+ # ContactMailer.with(greeting: "Hello").welcome.deliver_later
105
+ # assert_enqueued_email_with ContactMailer, :welcome, args: { greeting: "Hello" }
106
+ # end
107
+ #
104
108
  # def test_email_with_arguments
105
109
  # ContactMailer.welcome("Hello", "Goodbye").deliver_later
106
110
  # assert_enqueued_email_with ContactMailer, :welcome, args: ["Hello", "Goodbye"]
107
111
  # end
108
112
  #
113
+ # def test_email_with_named_arguments
114
+ # ContactMailer.welcome(greeting: "Hello", farewell: "Goodbye").deliver_later
115
+ # assert_enqueued_email_with ContactMailer, :welcome, args: [{ greeting: "Hello", farewell: "Goodbye" }]
116
+ # end
117
+ #
118
+ # def test_email_with_parameters_and_arguments
119
+ # ContactMailer.with(greeting: "Hello").welcome("Cheers", "Goodbye").deliver_later
120
+ # assert_enqueued_email_with ContactMailer, :welcome, params: { greeting: "Hello" }, args: ["Cheers", "Goodbye"]
121
+ # end
122
+ #
123
+ # def test_email_with_parameters_and_named_arguments
124
+ # ContactMailer.with(greeting: "Hello").welcome(farewell: "Goodbye").deliver_later
125
+ # assert_enqueued_email_with ContactMailer, :welcome, params: { greeting: "Hello" }, args: [{farewell: "Goodbye"}]
126
+ # end
127
+ #
128
+ # def test_email_with_parameterized_mailer
129
+ # ContactMailer.with(greeting: "Hello").welcome.deliver_later
130
+ # assert_enqueued_email_with ContactMailer.with(greeting: "Hello"), :welcome
131
+ # end
132
+ #
133
+ # def test_email_with_matchers
134
+ # ContactMailer.with(greeting: "Hello").welcome("Cheers", "Goodbye").deliver_later
135
+ # assert_enqueued_email_with ContactMailer, :welcome,
136
+ # params: ->(params) { /hello/i.match?(params[:greeting]) },
137
+ # args: ->(args) { /cheers/i.match?(args[0]) }
138
+ # end
139
+ #
109
140
  # If a block is passed, that block should cause the specified email
110
141
  # to be enqueued.
111
142
  #
@@ -123,13 +154,43 @@ module ActionMailer
123
154
  # ContactMailer.with(email: 'user@example.com').welcome.deliver_later
124
155
  # end
125
156
  # end
126
- def assert_enqueued_email_with(mailer, method, args: nil, queue: ActionMailer::Base.deliver_later_queue_name || "default", &block)
127
- args = if args.is_a?(Hash)
128
- [mailer.to_s, method.to_s, "deliver_now", params: args, args: []]
129
- else
130
- [mailer.to_s, method.to_s, "deliver_now", args: Array(args)]
157
+ def assert_enqueued_email_with(mailer, method, params: nil, args: nil, queue: nil, &block)
158
+ if mailer.is_a? ActionMailer::Parameterized::Mailer
159
+ params = mailer.instance_variable_get(:@params)
160
+ mailer = mailer.instance_variable_get(:@mailer)
131
161
  end
132
- assert_enqueued_with(job: mailer.delivery_job, args: args, queue: queue.to_s, &block)
162
+
163
+ if args.is_a?(Hash)
164
+ ActionMailer.deprecator.warn <<~MSG
165
+ Passing a Hash to the assert_enqueued_email_with :args kwarg causes the
166
+ Hash to be treated as params. This behavior is deprecated and will be
167
+ removed in Rails 7.2.
168
+
169
+ To specify a params Hash, use the :params kwarg:
170
+
171
+ assert_enqueued_email_with MyMailer, :my_method, params: { my_param: "value" }
172
+
173
+ Or, to specify named mailer args as a Hash, wrap the Hash in an array:
174
+
175
+ assert_enqueued_email_with MyMailer, :my_method, args: [{ my_arg: "value" }]
176
+ # OR
177
+ assert_enqueued_email_with MyMailer, :my_method, args: [my_arg: "value"]
178
+ MSG
179
+
180
+ params, args = args, nil
181
+ end
182
+
183
+ args = Array(args) unless args.is_a?(Proc)
184
+ queue ||= mailer.deliver_later_queue_name || ActiveJob::Base.default_queue_name
185
+
186
+ expected = ->(job_args) do
187
+ job_kwargs = job_args.extract_options!
188
+
189
+ [mailer.to_s, method.to_s, "deliver_now"] == job_args &&
190
+ params === job_kwargs[:params] && args === job_kwargs[:args]
191
+ end
192
+
193
+ assert_enqueued_with(job: mailer.delivery_job, args: expected, queue: queue.to_s, &block)
133
194
  end
134
195
 
135
196
  # Asserts that no emails are enqueued for later delivery.
@@ -151,6 +212,68 @@ module ActionMailer
151
212
  assert_enqueued_emails 0, &block
152
213
  end
153
214
 
215
+ # Delivers all enqueued emails. If a block is given, delivers all of the emails
216
+ # that were enqueued throughout the duration of the block. If a block is
217
+ # not given, delivers all the enqueued emails up to this point in the test.
218
+ #
219
+ # def test_deliver_enqueued_emails
220
+ # deliver_enqueued_emails do
221
+ # ContactMailer.welcome.deliver_later
222
+ # end
223
+ #
224
+ # assert_emails 1
225
+ # end
226
+ #
227
+ # def test_deliver_enqueued_emails_without_block
228
+ # ContactMailer.welcome.deliver_later
229
+ #
230
+ # deliver_enqueued_emails
231
+ #
232
+ # assert_emails 1
233
+ # end
234
+ #
235
+ # If the +:queue+ option is specified,
236
+ # then only the emails(s) enqueued to a specific queue will be performed.
237
+ #
238
+ # def test_deliver_enqueued_emails_with_queue
239
+ # deliver_enqueued_emails queue: :external_mailers do
240
+ # CustomerMailer.deliver_later_queue_name = :external_mailers
241
+ # CustomerMailer.welcome.deliver_later # will be performed
242
+ # EmployeeMailer.deliver_later_queue_name = :internal_mailers
243
+ # EmployeeMailer.welcome.deliver_later # will not be performed
244
+ # end
245
+ #
246
+ # assert_emails 1
247
+ # end
248
+ #
249
+ # If the +:at+ option is specified, then only delivers emails enqueued to deliver
250
+ # immediately or before the given time.
251
+ def deliver_enqueued_emails(queue: nil, at: nil, &block)
252
+ perform_enqueued_jobs(only: ->(job) { delivery_job_filter(job) }, queue: queue, at: at, &block)
253
+ end
254
+
255
+ # Returns any emails that are sent in the block.
256
+ #
257
+ # def test_emails
258
+ # emails = capture_emails do
259
+ # ContactMailer.welcome.deliver_now
260
+ # end
261
+ # assert_equal "Hi there", emails.first.subject
262
+ #
263
+ # emails = capture_emails do
264
+ # ContactMailer.welcome.deliver_now
265
+ # ContactMailer.welcome.deliver_later
266
+ # end
267
+ # assert_equal "Hi there", emails.first.subject
268
+ # end
269
+ def capture_emails(&block)
270
+ original_count = ActionMailer::Base.deliveries.size
271
+ deliver_enqueued_emails(&block)
272
+ new_count = ActionMailer::Base.deliveries.size
273
+ diff = new_count - original_count
274
+ ActionMailer::Base.deliveries.last(diff)
275
+ end
276
+
154
277
  private
155
278
  def delivery_job_filter(job)
156
279
  job_class = job.is_a?(Hash) ? job.fetch(:job) : job.class
@@ -4,7 +4,7 @@ require_relative "gem_version"
4
4
 
5
5
  module ActionMailer
6
6
  # Returns the currently loaded version of Action Mailer as a
7
- # <tt>Gem::Version</tt>.
7
+ # +Gem::Version+.
8
8
  def self.version
9
9
  gem_version
10
10
  end