omg-actionmailer 8.0.0.alpha2

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.
@@ -0,0 +1,154 @@
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, ...)
118
+ if @mailer.action_methods.include?(method_name.name)
119
+ ActionMailer::Parameterized::MessageDelivery.new(@mailer, method_name, @params, ...)
120
+ else
121
+ super
122
+ end
123
+ end
124
+
125
+ def respond_to_missing?(method, include_all = false)
126
+ @mailer.respond_to?(method, include_all)
127
+ end
128
+ end
129
+
130
+ class MessageDelivery < ActionMailer::MessageDelivery # :nodoc:
131
+ def initialize(mailer_class, action, params, ...)
132
+ super(mailer_class, action, ...)
133
+ @params = params
134
+ end
135
+
136
+ private
137
+ def processed_mailer
138
+ @processed_mailer ||= @mailer_class.new.tap do |mailer|
139
+ mailer.params = @params
140
+ mailer.process @action, *@args
141
+ end
142
+ end
143
+
144
+ def enqueue_delivery(delivery_method, options = {})
145
+ if processed?
146
+ super
147
+ else
148
+ @mailer_class.delivery_job.set(options).perform_later(
149
+ @mailer_class.name, @action.to_s, delivery_method.to_s, params: @params, args: @args)
150
+ end
151
+ end
152
+ end
153
+ end
154
+ end
@@ -0,0 +1,142 @@
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
+ module ClassMethods
29
+ # Register one or more Interceptors which will be called before mail is previewed.
30
+ def register_preview_interceptors(*interceptors)
31
+ interceptors.flatten.compact.each { |interceptor| register_preview_interceptor(interceptor) }
32
+ end
33
+
34
+ # Unregister one or more previously registered Interceptors.
35
+ def unregister_preview_interceptors(*interceptors)
36
+ interceptors.flatten.compact.each { |interceptor| unregister_preview_interceptor(interceptor) }
37
+ end
38
+
39
+ # Register an Interceptor which will be called before mail is previewed.
40
+ # Either a class or a string can be passed in as the Interceptor. If a
41
+ # string is passed in it will be constantized.
42
+ def register_preview_interceptor(interceptor)
43
+ preview_interceptor = interceptor_class_for(interceptor)
44
+
45
+ unless preview_interceptors.include?(preview_interceptor)
46
+ preview_interceptors << preview_interceptor
47
+ end
48
+ end
49
+
50
+ # Unregister a previously registered Interceptor.
51
+ # Either a class or a string can be passed in as the Interceptor. If a
52
+ # string is passed in it will be constantized.
53
+ def unregister_preview_interceptor(interceptor)
54
+ preview_interceptors.delete(interceptor_class_for(interceptor))
55
+ end
56
+
57
+ private
58
+ def interceptor_class_for(interceptor)
59
+ case interceptor
60
+ when String, Symbol
61
+ interceptor.to_s.camelize.constantize
62
+ else
63
+ interceptor
64
+ end
65
+ end
66
+ end
67
+ end
68
+
69
+ class Preview
70
+ extend ActiveSupport::DescendantsTracker
71
+
72
+ attr_reader :params
73
+
74
+ def initialize(params = {})
75
+ @params = params
76
+ end
77
+
78
+ class << self
79
+ # Returns all mailer preview classes.
80
+ def all
81
+ load_previews if descendants.empty?
82
+ descendants.sort_by { |mailer| mailer.name.titleize }
83
+ end
84
+
85
+ # Returns the mail object for the given email name. The registered preview
86
+ # interceptors will be informed so that they can transform the message
87
+ # as they would if the mail was actually being delivered.
88
+ def call(email, params = {})
89
+ preview = new(params)
90
+ message = preview.public_send(email)
91
+ inform_preview_interceptors(message)
92
+ message
93
+ end
94
+
95
+ # Returns all of the available email previews.
96
+ def emails
97
+ public_instance_methods(false).map(&:to_s).sort
98
+ end
99
+
100
+ # Returns +true+ if the email exists.
101
+ def email_exists?(email)
102
+ emails.include?(email)
103
+ end
104
+
105
+ # Returns +true+ if the preview exists.
106
+ def exists?(preview)
107
+ all.any? { |p| p.preview_name == preview }
108
+ end
109
+
110
+ # Find a mailer preview by its underscored class name.
111
+ def find(preview)
112
+ all.find { |p| p.preview_name == preview }
113
+ end
114
+
115
+ # Returns the underscored name of the mailer preview without the suffix.
116
+ def preview_name
117
+ name.delete_suffix("Preview").underscore
118
+ end
119
+
120
+ private
121
+ def load_previews
122
+ preview_paths.each do |preview_path|
123
+ Dir["#{preview_path}/**/*_preview.rb"].sort.each { |file| require file }
124
+ end
125
+ end
126
+
127
+ def preview_paths
128
+ Base.preview_paths
129
+ end
130
+
131
+ def show_previews
132
+ Base.show_previews
133
+ end
134
+
135
+ def inform_preview_interceptors(message)
136
+ Base.preview_interceptors.each do |interceptor|
137
+ interceptor.previewing_email(message)
138
+ end
139
+ end
140
+ end
141
+ end
142
+ 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
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_job/railtie"
4
+ require "action_mailer"
5
+ require "rails"
6
+ require "abstract_controller/railties/routes_helpers"
7
+
8
+ module ActionMailer
9
+ class Railtie < Rails::Railtie # :nodoc:
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
17
+
18
+ initializer "action_mailer.logger" do
19
+ ActiveSupport.on_load(:action_mailer) { self.logger ||= Rails.logger }
20
+ end
21
+
22
+ initializer "action_mailer.set_configs" do |app|
23
+ paths = app.config.paths
24
+ options = app.config.action_mailer
25
+
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
75
+
76
+ initializer "action_mailer.compile_config_methods" do
77
+ ActiveSupport.on_load(:action_mailer) do
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
91
+ end
92
+ end
93
+ 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
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/test_case"
4
+ require "rails-dom-testing"
5
+
6
+ module ActionMailer
7
+ class NonInferrableMailerError < ::StandardError
8
+ def initialize(name)
9
+ super "Unable to determine the mailer to test from #{name}. " \
10
+ "You'll need to specify it using tests YourMailer in your " \
11
+ "test case definition"
12
+ end
13
+ end
14
+
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
+
32
+ module Behavior
33
+ extend ActiveSupport::Concern
34
+
35
+ include ActiveSupport::Testing::ConstantLookup
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
47
+
48
+ module ClassMethods
49
+ def tests(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
58
+ end
59
+
60
+ def mailer_class
61
+ if mailer = _mailer_class
62
+ mailer
63
+ else
64
+ tests determine_default_mailer(name)
65
+ end
66
+ end
67
+
68
+ def determine_default_mailer(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
74
+ end
75
+ end
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
+
86
+ private
87
+ def initialize_test_deliveries
88
+ set_delivery_method :test
89
+ @old_perform_deliveries = ActionMailer::Base.perform_deliveries
90
+ ActionMailer::Base.perform_deliveries = true
91
+ ActionMailer::Base.deliveries.clear
92
+ end
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
+
109
+ def set_expected_mail
110
+ @expected = Mail.new
111
+ @expected.content_type ["text", "plain", { "charset" => charset }]
112
+ @expected.mime_version = "1.0"
113
+ end
114
+
115
+ def charset
116
+ "UTF-8"
117
+ end
118
+
119
+ def encode(subject)
120
+ Mail::Encodings.q_value_encode(subject, charset)
121
+ end
122
+ end
123
+
124
+ include Behavior
125
+ end
126
+ end