actionmailer 3.0.4 → 7.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +219 -0
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +49 -55
  5. data/lib/action_mailer/base.rb +688 -387
  6. data/lib/action_mailer/callbacks.rb +31 -0
  7. data/lib/action_mailer/collector.rb +11 -9
  8. data/lib/action_mailer/delivery_methods.rb +35 -37
  9. data/lib/action_mailer/deprecator.rb +7 -0
  10. data/lib/action_mailer/form_builder.rb +37 -0
  11. data/lib/action_mailer/gem_version.rb +17 -0
  12. data/lib/action_mailer/inline_preview_interceptor.rb +59 -0
  13. data/lib/action_mailer/log_subscriber.rb +30 -8
  14. data/lib/action_mailer/mail_delivery_job.rb +48 -0
  15. data/lib/action_mailer/mail_helper.rb +59 -18
  16. data/lib/action_mailer/message_delivery.rb +156 -0
  17. data/lib/action_mailer/parameterized.rb +156 -0
  18. data/lib/action_mailer/preview.rb +166 -0
  19. data/lib/action_mailer/queued_delivery.rb +12 -0
  20. data/lib/action_mailer/railtie.rb +75 -7
  21. data/lib/action_mailer/rescuable.rb +33 -0
  22. data/lib/action_mailer/test_case.rb +75 -25
  23. data/lib/action_mailer/test_helper.rb +238 -15
  24. data/lib/action_mailer/version.rb +8 -7
  25. data/lib/action_mailer.rb +45 -18
  26. data/lib/rails/generators/mailer/USAGE +13 -8
  27. data/lib/rails/generators/mailer/mailer_generator.rb +26 -4
  28. data/lib/rails/generators/mailer/templates/application_mailer.rb.tt +6 -0
  29. data/lib/rails/generators/mailer/templates/mailer.rb.tt +17 -0
  30. metadata +175 -87
  31. data/CHANGELOG +0 -424
  32. data/lib/action_mailer/adv_attr_accessor.rb +0 -26
  33. data/lib/action_mailer/deprecated_api.rb +0 -147
  34. data/lib/action_mailer/old_api.rb +0 -259
  35. data/lib/action_mailer/tmail_compat.rb +0 -34
  36. data/lib/rails/generators/mailer/templates/mailer.rb +0 -16
@@ -0,0 +1,156 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionMailer
4
+ # = Action Mailer \Parameterized
5
+ #
6
+ # Provides the option to parameterize mailers in order to share instance variable
7
+ # setup, processing, and common headers.
8
+ #
9
+ # Consider this example that does not use parameterization:
10
+ #
11
+ # class InvitationsMailer < ApplicationMailer
12
+ # def account_invitation(inviter, invitee)
13
+ # @account = inviter.account
14
+ # @inviter = inviter
15
+ # @invitee = invitee
16
+ #
17
+ # subject = "#{@inviter.name} invited you to their Basecamp (#{@account.name})"
18
+ #
19
+ # mail \
20
+ # subject: subject,
21
+ # to: invitee.email_address,
22
+ # from: common_address(inviter),
23
+ # reply_to: inviter.email_address_with_name
24
+ # end
25
+ #
26
+ # def project_invitation(project, inviter, invitee)
27
+ # @account = inviter.account
28
+ # @project = project
29
+ # @inviter = inviter
30
+ # @invitee = invitee
31
+ # @summarizer = ProjectInvitationSummarizer.new(@project.bucket)
32
+ #
33
+ # subject = "#{@inviter.name.familiar} added you to a project in Basecamp (#{@account.name})"
34
+ #
35
+ # mail \
36
+ # subject: subject,
37
+ # to: invitee.email_address,
38
+ # from: common_address(inviter),
39
+ # reply_to: inviter.email_address_with_name
40
+ # end
41
+ #
42
+ # def bulk_project_invitation(projects, inviter, invitee)
43
+ # @account = inviter.account
44
+ # @projects = projects.sort_by(&:name)
45
+ # @inviter = inviter
46
+ # @invitee = invitee
47
+ #
48
+ # subject = "#{@inviter.name.familiar} added you to some new stuff in Basecamp (#{@account.name})"
49
+ #
50
+ # mail \
51
+ # subject: subject,
52
+ # to: invitee.email_address,
53
+ # from: common_address(inviter),
54
+ # reply_to: inviter.email_address_with_name
55
+ # end
56
+ # end
57
+ #
58
+ # InvitationsMailer.account_invitation(person_a, person_b).deliver_later
59
+ #
60
+ # Using parameterized mailers, this can be rewritten as:
61
+ #
62
+ # class InvitationsMailer < ApplicationMailer
63
+ # before_action { @inviter, @invitee = params[:inviter], params[:invitee] }
64
+ # before_action { @account = params[:inviter].account }
65
+ #
66
+ # default to: -> { @invitee.email_address },
67
+ # from: -> { common_address(@inviter) },
68
+ # reply_to: -> { @inviter.email_address_with_name }
69
+ #
70
+ # def account_invitation
71
+ # mail subject: "#{@inviter.name} invited you to their Basecamp (#{@account.name})"
72
+ # end
73
+ #
74
+ # def project_invitation
75
+ # @project = params[:project]
76
+ # @summarizer = ProjectInvitationSummarizer.new(@project.bucket)
77
+ #
78
+ # mail subject: "#{@inviter.name.familiar} added you to a project in Basecamp (#{@account.name})"
79
+ # end
80
+ #
81
+ # def bulk_project_invitation
82
+ # @projects = params[:projects].sort_by(&:name)
83
+ #
84
+ # mail subject: "#{@inviter.name.familiar} added you to some new stuff in Basecamp (#{@account.name})"
85
+ # end
86
+ # end
87
+ #
88
+ # InvitationsMailer.with(inviter: person_a, invitee: person_b).account_invitation.deliver_later
89
+ module Parameterized
90
+ extend ActiveSupport::Concern
91
+
92
+ included do
93
+ attr_writer :params
94
+
95
+ def params
96
+ @params ||= {}
97
+ end
98
+ end
99
+
100
+ module ClassMethods
101
+ # Provide the parameters to the mailer in order to use them in the instance methods and callbacks.
102
+ #
103
+ # InvitationsMailer.with(inviter: person_a, invitee: person_b).account_invitation.deliver_later
104
+ #
105
+ # See Parameterized documentation for full example.
106
+ def with(params)
107
+ ActionMailer::Parameterized::Mailer.new(self, params)
108
+ end
109
+ end
110
+
111
+ class Mailer # :nodoc:
112
+ def initialize(mailer, params)
113
+ @mailer, @params = mailer, params
114
+ end
115
+
116
+ private
117
+ def method_missing(method_name, *args)
118
+ if @mailer.action_methods.include?(method_name.to_s)
119
+ ActionMailer::Parameterized::MessageDelivery.new(@mailer, method_name, @params, *args)
120
+ else
121
+ super
122
+ end
123
+ end
124
+ ruby2_keywords(:method_missing)
125
+
126
+ def respond_to_missing?(method, include_all = false)
127
+ @mailer.respond_to?(method, include_all)
128
+ end
129
+ end
130
+
131
+ class MessageDelivery < ActionMailer::MessageDelivery # :nodoc:
132
+ def initialize(mailer_class, action, params, *args)
133
+ super(mailer_class, action, *args)
134
+ @params = params
135
+ end
136
+ ruby2_keywords(:initialize)
137
+
138
+ private
139
+ def processed_mailer
140
+ @processed_mailer ||= @mailer_class.new.tap do |mailer|
141
+ mailer.params = @params
142
+ mailer.process @action, *@args
143
+ end
144
+ end
145
+
146
+ def enqueue_delivery(delivery_method, options = {})
147
+ if processed?
148
+ super
149
+ else
150
+ @mailer_class.delivery_job.set(options).perform_later(
151
+ @mailer_class.name, @action.to_s, delivery_method.to_s, params: @params, args: @args)
152
+ end
153
+ end
154
+ end
155
+ end
156
+ end
@@ -0,0 +1,166 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/descendants_tracker"
4
+
5
+ module ActionMailer
6
+ module Previews # :nodoc:
7
+ extend ActiveSupport::Concern
8
+
9
+ included do
10
+ # Add the location of mailer previews through app configuration:
11
+ #
12
+ # config.action_mailer.preview_paths << "#{Rails.root}/lib/mailer_previews"
13
+ #
14
+ mattr_accessor :preview_paths, instance_writer: false, default: []
15
+
16
+ # Enable or disable mailer previews through app configuration:
17
+ #
18
+ # config.action_mailer.show_previews = true
19
+ #
20
+ # Defaults to +true+ for development environment
21
+ #
22
+ mattr_accessor :show_previews, instance_writer: false
23
+
24
+ # :nodoc:
25
+ mattr_accessor :preview_interceptors, instance_writer: false, default: [ActionMailer::InlinePreviewInterceptor]
26
+ end
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
+
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
+
53
+ # Register one or more Interceptors which will be called before mail is previewed.
54
+ def register_preview_interceptors(*interceptors)
55
+ interceptors.flatten.compact.each { |interceptor| register_preview_interceptor(interceptor) }
56
+ end
57
+
58
+ # Unregister one or more previously registered Interceptors.
59
+ def unregister_preview_interceptors(*interceptors)
60
+ interceptors.flatten.compact.each { |interceptor| unregister_preview_interceptor(interceptor) }
61
+ end
62
+
63
+ # Register an Interceptor which will be called before mail is previewed.
64
+ # Either a class or a string can be passed in as the Interceptor. If a
65
+ # string is passed in it will be constantized.
66
+ def register_preview_interceptor(interceptor)
67
+ preview_interceptor = interceptor_class_for(interceptor)
68
+
69
+ unless preview_interceptors.include?(preview_interceptor)
70
+ preview_interceptors << preview_interceptor
71
+ end
72
+ end
73
+
74
+ # Unregister a previously registered Interceptor.
75
+ # Either a class or a string can be passed in as the Interceptor. If a
76
+ # string is passed in it will be constantized.
77
+ def unregister_preview_interceptor(interceptor)
78
+ preview_interceptors.delete(interceptor_class_for(interceptor))
79
+ end
80
+
81
+ private
82
+ def interceptor_class_for(interceptor)
83
+ case interceptor
84
+ when String, Symbol
85
+ interceptor.to_s.camelize.constantize
86
+ else
87
+ interceptor
88
+ end
89
+ end
90
+ end
91
+ end
92
+
93
+ class Preview
94
+ extend ActiveSupport::DescendantsTracker
95
+
96
+ attr_reader :params
97
+
98
+ def initialize(params = {})
99
+ @params = params
100
+ end
101
+
102
+ class << self
103
+ # Returns all mailer preview classes.
104
+ def all
105
+ load_previews if descendants.empty?
106
+ descendants.sort_by { |mailer| mailer.name.titleize }
107
+ end
108
+
109
+ # Returns the mail object for the given email name. The registered preview
110
+ # interceptors will be informed so that they can transform the message
111
+ # as they would if the mail was actually being delivered.
112
+ def call(email, params = {})
113
+ preview = new(params)
114
+ message = preview.public_send(email)
115
+ inform_preview_interceptors(message)
116
+ message
117
+ end
118
+
119
+ # Returns all of the available email previews.
120
+ def emails
121
+ public_instance_methods(false).map(&:to_s).sort
122
+ end
123
+
124
+ # Returns +true+ if the email exists.
125
+ def email_exists?(email)
126
+ emails.include?(email)
127
+ end
128
+
129
+ # Returns +true+ if the preview exists.
130
+ def exists?(preview)
131
+ all.any? { |p| p.preview_name == preview }
132
+ end
133
+
134
+ # Find a mailer preview by its underscored class name.
135
+ def find(preview)
136
+ all.find { |p| p.preview_name == preview }
137
+ end
138
+
139
+ # Returns the underscored name of the mailer preview without the suffix.
140
+ def preview_name
141
+ name.delete_suffix("Preview").underscore
142
+ end
143
+
144
+ private
145
+ def load_previews
146
+ preview_paths.each do |preview_path|
147
+ Dir["#{preview_path}/**/*_preview.rb"].sort.each { |file| require file }
148
+ end
149
+ end
150
+
151
+ def preview_paths
152
+ Base.preview_paths
153
+ end
154
+
155
+ def show_previews
156
+ Base.show_previews
157
+ end
158
+
159
+ def inform_preview_interceptors(message)
160
+ Base.preview_interceptors.each do |interceptor|
161
+ interceptor.previewing_email(message)
162
+ end
163
+ end
164
+ end
165
+ end
166
+ end
@@ -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
@@ -1,9 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_job/railtie"
1
4
  require "action_mailer"
2
5
  require "rails"
6
+ require "abstract_controller/railties/routes_helpers"
3
7
 
4
8
  module ActionMailer
5
- class Railtie < Rails::Railtie
9
+ class Railtie < Rails::Railtie # :nodoc:
6
10
  config.action_mailer = ActiveSupport::OrderedOptions.new
11
+ config.action_mailer.preview_paths = []
12
+ config.eager_load_namespaces << ActionMailer
13
+
14
+ initializer "action_mailer.deprecator", before: :load_environment_config do |app|
15
+ app.deprecators[:action_mailer] = ActionMailer.deprecator
16
+ end
7
17
 
8
18
  initializer "action_mailer.logger" do
9
19
  ActiveSupport.on_load(:action_mailer) { self.logger ||= Rails.logger }
@@ -13,14 +23,72 @@ module ActionMailer
13
23
  paths = app.config.paths
14
24
  options = app.config.action_mailer
15
25
 
16
- options.assets_dir ||= paths.public.to_a.first
17
- options.javascripts_dir ||= paths.public.javascripts.to_a.first
18
- options.stylesheets_dir ||= paths.public.stylesheets.to_a.first
26
+ options.assets_dir ||= paths["public"].first
27
+ options.javascripts_dir ||= paths["public/javascripts"].first
28
+ options.stylesheets_dir ||= paths["public/stylesheets"].first
29
+ options.show_previews = Rails.env.development? if options.show_previews.nil?
30
+ options.cache_store ||= Rails.cache
31
+ options.preview_paths |= ["#{Rails.root}/test/mailers/previews"]
32
+
33
+ # make sure readers methods get compiled
34
+ options.asset_host ||= app.config.asset_host
35
+ options.relative_url_root ||= app.config.relative_url_root
36
+
37
+ ActiveSupport.on_load(:action_mailer) do
38
+ include AbstractController::UrlFor
39
+ extend ::AbstractController::Railties::RoutesHelpers.with(app.routes, false)
40
+ include app.routes.mounted_helpers
41
+
42
+ register_interceptors(options.delete(:interceptors))
43
+ register_preview_interceptors(options.delete(:preview_interceptors))
44
+ register_observers(options.delete(:observers))
45
+ self.preview_paths |= options[:preview_paths]
46
+
47
+ if delivery_job = options.delete(:delivery_job)
48
+ self.delivery_job = delivery_job.constantize
49
+ end
50
+
51
+ if options.smtp_settings
52
+ self.smtp_settings = options.smtp_settings
53
+ end
54
+
55
+ smtp_timeout = options.delete(:smtp_timeout)
56
+
57
+ if self.smtp_settings && smtp_timeout
58
+ self.smtp_settings[:open_timeout] ||= smtp_timeout
59
+ self.smtp_settings[:read_timeout] ||= smtp_timeout
60
+ end
61
+
62
+ options.each { |k, v| send("#{k}=", v) }
63
+ end
64
+
65
+ ActiveSupport.on_load(:action_dispatch_integration_test) do
66
+ include ActionMailer::TestHelper
67
+ include ActionMailer::TestCase::ClearTestDeliveries
68
+ end
69
+ end
70
+
71
+ initializer "action_mailer.set_autoload_paths", before: :set_autoload_paths do |app|
72
+ options = app.config.action_mailer
73
+ app.config.paths["test/mailers/previews"].concat(options.preview_paths)
74
+ end
19
75
 
76
+ initializer "action_mailer.compile_config_methods" do
20
77
  ActiveSupport.on_load(:action_mailer) do
21
- include app.routes.url_helpers
22
- options.each { |k,v| send("#{k}=", v) }
78
+ config.compile_methods! if config.respond_to?(:compile_methods!)
79
+ end
80
+ end
81
+
82
+ config.after_initialize do |app|
83
+ options = app.config.action_mailer
84
+
85
+ if options.show_previews
86
+ app.routes.prepend do
87
+ get "/rails/mailers" => "rails/mailers#index", internal: true
88
+ get "/rails/mailers/download/*path" => "rails/mailers#download", internal: true
89
+ get "/rails/mailers/*path" => "rails/mailers#preview", internal: true
90
+ end
23
91
  end
24
92
  end
25
93
  end
26
- end
94
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionMailer # :nodoc:
4
+ # = Action Mailer \Rescuable
5
+ #
6
+ # Provides
7
+ # {rescue_from}[rdoc-ref:ActiveSupport::Rescuable::ClassMethods#rescue_from]
8
+ # for mailers. Wraps mailer action processing, mail job processing, and mail
9
+ # delivery to handle configured errors.
10
+ module Rescuable
11
+ extend ActiveSupport::Concern
12
+ include ActiveSupport::Rescuable
13
+
14
+ class_methods do
15
+ def handle_exception(exception) # :nodoc:
16
+ rescue_with_handler(exception) || raise(exception)
17
+ end
18
+ end
19
+
20
+ def handle_exceptions # :nodoc:
21
+ yield
22
+ rescue => exception
23
+ rescue_with_handler(exception) || raise
24
+ end
25
+
26
+ private
27
+ def process(...)
28
+ handle_exceptions do
29
+ super
30
+ end
31
+ end
32
+ end
33
+ end
@@ -1,25 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/test_case"
4
+ require "rails-dom-testing"
5
+
1
6
  module ActionMailer
2
7
  class NonInferrableMailerError < ::StandardError
3
8
  def initialize(name)
4
- super "Unable to determine the mailer to test from #{name}. " +
5
- "You'll need to specify it using tests YourMailer in your " +
9
+ super "Unable to determine the mailer to test from #{name}. " \
10
+ "You'll need to specify it using tests YourMailer in your " \
6
11
  "test case definition"
7
12
  end
8
13
  end
9
14
 
10
15
  class TestCase < ActiveSupport::TestCase
16
+ module ClearTestDeliveries
17
+ extend ActiveSupport::Concern
18
+
19
+ included do
20
+ setup :clear_test_deliveries
21
+ teardown :clear_test_deliveries
22
+ end
23
+
24
+ private
25
+ def clear_test_deliveries
26
+ if ActionMailer::Base.delivery_method == :test
27
+ ActionMailer::Base.deliveries.clear
28
+ end
29
+ end
30
+ end
31
+
11
32
  module Behavior
12
33
  extend ActiveSupport::Concern
13
34
 
35
+ include ActiveSupport::Testing::ConstantLookup
14
36
  include TestHelper
37
+ include Rails::Dom::Testing::Assertions::SelectorAssertions
38
+ include Rails::Dom::Testing::Assertions::DomAssertions
39
+
40
+ included do
41
+ class_attribute :_mailer_class
42
+ setup :initialize_test_deliveries
43
+ setup :set_expected_mail
44
+ teardown :restore_test_deliveries
45
+ ActiveSupport.run_load_hooks(:action_mailer_test_case, self)
46
+ end
15
47
 
16
48
  module ClassMethods
17
49
  def tests(mailer)
18
- write_inheritable_attribute(:mailer_class, mailer)
50
+ case mailer
51
+ when String, Symbol
52
+ self._mailer_class = mailer.to_s.camelize.constantize
53
+ when Module
54
+ self._mailer_class = mailer
55
+ else
56
+ raise NonInferrableMailerError.new(mailer)
57
+ end
19
58
  end
20
59
 
21
60
  def mailer_class
22
- if mailer = read_inheritable_attribute(:mailer_class)
61
+ if mailer = _mailer_class
23
62
  mailer
24
63
  else
25
64
  tests determine_default_mailer(name)
@@ -27,30 +66,52 @@ module ActionMailer
27
66
  end
28
67
 
29
68
  def determine_default_mailer(name)
30
- name.sub(/Test$/, '').constantize
31
- rescue NameError
32
- raise NonInferrableMailerError.new(name)
69
+ mailer = determine_constant_from_test_name(name) do |constant|
70
+ Class === constant && constant < ActionMailer::Base
71
+ end
72
+ raise NonInferrableMailerError.new(name) if mailer.nil?
73
+ mailer
33
74
  end
34
75
  end
35
76
 
36
- module InstanceMethods
37
-
38
- protected
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
39
85
 
86
+ private
40
87
  def initialize_test_deliveries
41
- ActionMailer::Base.delivery_method = :test
88
+ set_delivery_method :test
89
+ @old_perform_deliveries = ActionMailer::Base.perform_deliveries
42
90
  ActionMailer::Base.perform_deliveries = true
43
91
  ActionMailer::Base.deliveries.clear
44
92
  end
45
93
 
94
+ def restore_test_deliveries
95
+ restore_delivery_method
96
+ ActionMailer::Base.perform_deliveries = @old_perform_deliveries
97
+ end
98
+
99
+ def set_delivery_method(method)
100
+ @old_delivery_method = ActionMailer::Base.delivery_method
101
+ ActionMailer::Base.delivery_method = method
102
+ end
103
+
104
+ def restore_delivery_method
105
+ ActionMailer::Base.deliveries.clear
106
+ ActionMailer::Base.delivery_method = @old_delivery_method
107
+ end
108
+
46
109
  def set_expected_mail
47
110
  @expected = Mail.new
48
111
  @expected.content_type ["text", "plain", { "charset" => charset }]
49
- @expected.mime_version = '1.0'
112
+ @expected.mime_version = "1.0"
50
113
  end
51
114
 
52
- private
53
-
54
115
  def charset
55
116
  "UTF-8"
56
117
  end
@@ -58,19 +119,8 @@ module ActionMailer
58
119
  def encode(subject)
59
120
  Mail::Encodings.q_value_encode(subject, charset)
60
121
  end
61
-
62
- def read_fixture(action)
63
- IO.readlines(File.join(Rails.root, 'test', 'fixtures', self.class.mailer_class.name.underscore, action))
64
- end
65
- end
66
-
67
- included do
68
- setup :initialize_test_deliveries
69
- setup :set_expected_mail
70
- end
71
122
  end
72
123
 
73
124
  include Behavior
74
-
75
125
  end
76
126
  end