actionmailer 6.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,171 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionMailer
4
+ # Provides the option to parameterize mailers in order to share instance variable
5
+ # setup, processing, and common headers.
6
+ #
7
+ # Consider this example that does not use parameterization:
8
+ #
9
+ # class InvitationsMailer < ApplicationMailer
10
+ # def account_invitation(inviter, invitee)
11
+ # @account = inviter.account
12
+ # @inviter = inviter
13
+ # @invitee = invitee
14
+ #
15
+ # subject = "#{@inviter.name} invited you to their Basecamp (#{@account.name})"
16
+ #
17
+ # mail \
18
+ # subject: subject,
19
+ # to: invitee.email_address,
20
+ # from: common_address(inviter),
21
+ # reply_to: inviter.email_address_with_name
22
+ # end
23
+ #
24
+ # def project_invitation(project, inviter, invitee)
25
+ # @account = inviter.account
26
+ # @project = project
27
+ # @inviter = inviter
28
+ # @invitee = invitee
29
+ # @summarizer = ProjectInvitationSummarizer.new(@project.bucket)
30
+ #
31
+ # subject = "#{@inviter.name.familiar} added you to a project in Basecamp (#{@account.name})"
32
+ #
33
+ # mail \
34
+ # subject: subject,
35
+ # to: invitee.email_address,
36
+ # from: common_address(inviter),
37
+ # reply_to: inviter.email_address_with_name
38
+ # end
39
+ #
40
+ # def bulk_project_invitation(projects, inviter, invitee)
41
+ # @account = inviter.account
42
+ # @projects = projects.sort_by(&:name)
43
+ # @inviter = inviter
44
+ # @invitee = invitee
45
+ #
46
+ # subject = "#{@inviter.name.familiar} added you to some new stuff in Basecamp (#{@account.name})"
47
+ #
48
+ # mail \
49
+ # subject: subject,
50
+ # to: invitee.email_address,
51
+ # from: common_address(inviter),
52
+ # reply_to: inviter.email_address_with_name
53
+ # end
54
+ # end
55
+ #
56
+ # InvitationsMailer.account_invitation(person_a, person_b).deliver_later
57
+ #
58
+ # Using parameterized mailers, this can be rewritten as:
59
+ #
60
+ # class InvitationsMailer < ApplicationMailer
61
+ # before_action { @inviter, @invitee = params[:inviter], params[:invitee] }
62
+ # before_action { @account = params[:inviter].account }
63
+ #
64
+ # default to: -> { @invitee.email_address },
65
+ # from: -> { common_address(@inviter) },
66
+ # reply_to: -> { @inviter.email_address_with_name }
67
+ #
68
+ # def account_invitation
69
+ # mail subject: "#{@inviter.name} invited you to their Basecamp (#{@account.name})"
70
+ # end
71
+ #
72
+ # def project_invitation
73
+ # @project = params[:project]
74
+ # @summarizer = ProjectInvitationSummarizer.new(@project.bucket)
75
+ #
76
+ # mail subject: "#{@inviter.name.familiar} added you to a project in Basecamp (#{@account.name})"
77
+ # end
78
+ #
79
+ # def bulk_project_invitation
80
+ # @projects = params[:projects].sort_by(&:name)
81
+ #
82
+ # mail subject: "#{@inviter.name.familiar} added you to some new stuff in Basecamp (#{@account.name})"
83
+ # end
84
+ # end
85
+ #
86
+ # InvitationsMailer.with(inviter: person_a, invitee: person_b).account_invitation.deliver_later
87
+ module Parameterized
88
+ extend ActiveSupport::Concern
89
+
90
+ included do
91
+ attr_accessor :params
92
+ end
93
+
94
+ module ClassMethods
95
+ # Provide the parameters to the mailer in order to use them in the instance methods and callbacks.
96
+ #
97
+ # InvitationsMailer.with(inviter: person_a, invitee: person_b).account_invitation.deliver_later
98
+ #
99
+ # See Parameterized documentation for full example.
100
+ def with(params)
101
+ ActionMailer::Parameterized::Mailer.new(self, params)
102
+ end
103
+ end
104
+
105
+ class Mailer # :nodoc:
106
+ def initialize(mailer, params)
107
+ @mailer, @params = mailer, params
108
+ end
109
+
110
+ private
111
+ def method_missing(method_name, *args)
112
+ if @mailer.action_methods.include?(method_name.to_s)
113
+ ActionMailer::Parameterized::MessageDelivery.new(@mailer, method_name, @params, *args)
114
+ else
115
+ super
116
+ end
117
+ end
118
+
119
+ def respond_to_missing?(method, include_all = false)
120
+ @mailer.respond_to?(method, include_all)
121
+ end
122
+ end
123
+
124
+ class DeliveryJob < ActionMailer::DeliveryJob # :nodoc:
125
+ def perform(mailer, mail_method, delivery_method, params, *args)
126
+ mailer.constantize.with(params).public_send(mail_method, *args).send(delivery_method)
127
+ end
128
+ end
129
+
130
+ class MessageDelivery < ActionMailer::MessageDelivery # :nodoc:
131
+ def initialize(mailer_class, action, params, *args)
132
+ super(mailer_class, action, *args)
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
+ job = delivery_job_class
149
+ args = arguments_for(job, delivery_method)
150
+ job.set(options).perform_later(*args)
151
+ end
152
+ end
153
+
154
+ def delivery_job_class
155
+ if @mailer_class.delivery_job <= MailDeliveryJob
156
+ @mailer_class.delivery_job
157
+ else
158
+ Parameterized::DeliveryJob
159
+ end
160
+ end
161
+
162
+ def arguments_for(delivery_job, delivery_method)
163
+ if delivery_job <= MailDeliveryJob
164
+ [@mailer_class.name, @action.to_s, delivery_method.to_s, params: @params, args: @args]
165
+ else
166
+ [@mailer_class.name, @action.to_s, delivery_method.to_s, @params, *@args]
167
+ end
168
+ end
169
+ end
170
+ end
171
+ end
@@ -0,0 +1,143 @@
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
+ # Set the location of mailer previews through app configuration:
11
+ #
12
+ # config.action_mailer.preview_path = "#{Rails.root}/lib/mailer_previews"
13
+ #
14
+ mattr_accessor :preview_path, instance_writer: false
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
+
59
+ def interceptor_class_for(interceptor)
60
+ case interceptor
61
+ when String, Symbol
62
+ interceptor.to_s.camelize.constantize
63
+ else
64
+ interceptor
65
+ end
66
+ end
67
+ end
68
+ end
69
+
70
+ class Preview
71
+ extend ActiveSupport::DescendantsTracker
72
+
73
+ attr_reader :params
74
+
75
+ def initialize(params = {})
76
+ @params = params
77
+ end
78
+
79
+ class << self
80
+ # Returns all mailer preview classes.
81
+ def all
82
+ load_previews if descendants.empty?
83
+ descendants
84
+ end
85
+
86
+ # Returns the mail object for the given email name. The registered preview
87
+ # interceptors will be informed so that they can transform the message
88
+ # as they would if the mail was actually being delivered.
89
+ def call(email, params = {})
90
+ preview = new(params)
91
+ message = preview.public_send(email)
92
+ inform_preview_interceptors(message)
93
+ message
94
+ end
95
+
96
+ # Returns all of the available email previews.
97
+ def emails
98
+ public_instance_methods(false).map(&:to_s).sort
99
+ end
100
+
101
+ # Returns +true+ if the email exists.
102
+ def email_exists?(email)
103
+ emails.include?(email)
104
+ end
105
+
106
+ # Returns +true+ if the preview exists.
107
+ def exists?(preview)
108
+ all.any? { |p| p.preview_name == preview }
109
+ end
110
+
111
+ # Find a mailer preview by its underscored class name.
112
+ def find(preview)
113
+ all.find { |p| p.preview_name == preview }
114
+ end
115
+
116
+ # Returns the underscored name of the mailer preview without the suffix.
117
+ def preview_name
118
+ name.sub(/Preview$/, "").underscore
119
+ end
120
+
121
+ private
122
+ def load_previews
123
+ if preview_path
124
+ Dir["#{preview_path}/**/*_preview.rb"].sort.each { |file| require_dependency file }
125
+ end
126
+ end
127
+
128
+ def preview_path
129
+ Base.preview_path
130
+ end
131
+
132
+ def show_previews
133
+ Base.show_previews
134
+ end
135
+
136
+ def inform_preview_interceptors(message)
137
+ Base.preview_interceptors.each do |interceptor|
138
+ interceptor.previewing_email(message)
139
+ end
140
+ end
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,93 @@
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.eager_load_namespaces << ActionMailer
12
+
13
+ initializer "action_mailer.logger" do
14
+ ActiveSupport.on_load(:action_mailer) { self.logger ||= Rails.logger }
15
+ end
16
+
17
+ initializer "action_mailer.set_configs" do |app|
18
+ paths = app.config.paths
19
+ options = app.config.action_mailer
20
+
21
+ if app.config.force_ssl
22
+ options.default_url_options ||= {}
23
+ options.default_url_options[:protocol] ||= "https"
24
+ end
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
+
32
+ if options.show_previews
33
+ options.preview_path ||= defined?(Rails.root) ? "#{Rails.root}/test/mailers/previews" : nil
34
+ end
35
+
36
+ # make sure readers methods get compiled
37
+ options.asset_host ||= app.config.asset_host
38
+ options.relative_url_root ||= app.config.relative_url_root
39
+
40
+ ActiveSupport.on_load(:action_mailer) do
41
+ include AbstractController::UrlFor
42
+ extend ::AbstractController::Railties::RoutesHelpers.with(app.routes, false)
43
+ include app.routes.mounted_helpers
44
+
45
+ register_interceptors(options.delete(:interceptors))
46
+ register_preview_interceptors(options.delete(:preview_interceptors))
47
+ register_observers(options.delete(:observers))
48
+
49
+ if delivery_job = options.delete(:delivery_job)
50
+ self.delivery_job = delivery_job.constantize
51
+ end
52
+
53
+ options.each { |k, v| send("#{k}=", v) }
54
+ end
55
+
56
+ ActiveSupport.on_load(:action_dispatch_integration_test) do
57
+ include ActionMailer::TestHelper
58
+ include ActionMailer::TestCase::ClearTestDeliveries
59
+ end
60
+ end
61
+
62
+ initializer "action_mailer.set_autoload_paths" do |app|
63
+ options = app.config.action_mailer
64
+
65
+ if options.show_previews && options.preview_path
66
+ ActiveSupport::Dependencies.autoload_paths << options.preview_path
67
+ end
68
+ end
69
+
70
+ initializer "action_mailer.compile_config_methods" do
71
+ ActiveSupport.on_load(:action_mailer) do
72
+ config.compile_methods! if config.respond_to?(:compile_methods!)
73
+ end
74
+ end
75
+
76
+ initializer "action_mailer.eager_load_actions" do
77
+ ActiveSupport.on_load(:after_initialize) do
78
+ ActionMailer::Base.descendants.each(&:action_methods) if config.eager_load
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/*path" => "rails/mailers#preview", internal: true
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionMailer #:nodoc:
4
+ # Provides +rescue_from+ for mailers. Wraps mailer action processing,
5
+ # mail job processing, and mail delivery.
6
+ module Rescuable
7
+ extend ActiveSupport::Concern
8
+ include ActiveSupport::Rescuable
9
+
10
+ class_methods do
11
+ def handle_exception(exception) #:nodoc:
12
+ rescue_with_handler(exception) || raise(exception)
13
+ end
14
+ end
15
+
16
+ def handle_exceptions #:nodoc:
17
+ yield
18
+ rescue => exception
19
+ rescue_with_handler(exception) || raise
20
+ end
21
+
22
+ private
23
+ def process(*)
24
+ handle_exceptions do
25
+ super
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,123 @@
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
+
26
+ def clear_test_deliveries
27
+ if ActionMailer::Base.delivery_method == :test
28
+ ActionMailer::Base.deliveries.clear
29
+ end
30
+ end
31
+ end
32
+
33
+ module Behavior
34
+ extend ActiveSupport::Concern
35
+
36
+ include ActiveSupport::Testing::ConstantLookup
37
+ include TestHelper
38
+ include Rails::Dom::Testing::Assertions::SelectorAssertions
39
+ include Rails::Dom::Testing::Assertions::DomAssertions
40
+
41
+ included do
42
+ class_attribute :_mailer_class
43
+ setup :initialize_test_deliveries
44
+ setup :set_expected_mail
45
+ teardown :restore_test_deliveries
46
+ ActiveSupport.run_load_hooks(:action_mailer_test_case, self)
47
+ end
48
+
49
+ module ClassMethods
50
+ def tests(mailer)
51
+ case mailer
52
+ when String, Symbol
53
+ self._mailer_class = mailer.to_s.camelize.constantize
54
+ when Module
55
+ self._mailer_class = mailer
56
+ else
57
+ raise NonInferrableMailerError.new(mailer)
58
+ end
59
+ end
60
+
61
+ def mailer_class
62
+ if mailer = _mailer_class
63
+ mailer
64
+ else
65
+ tests determine_default_mailer(name)
66
+ end
67
+ end
68
+
69
+ def determine_default_mailer(name)
70
+ mailer = determine_constant_from_test_name(name) do |constant|
71
+ Class === constant && constant < ActionMailer::Base
72
+ end
73
+ raise NonInferrableMailerError.new(name) if mailer.nil?
74
+ mailer
75
+ end
76
+ end
77
+
78
+ private
79
+
80
+ def initialize_test_deliveries
81
+ set_delivery_method :test
82
+ @old_perform_deliveries = ActionMailer::Base.perform_deliveries
83
+ ActionMailer::Base.perform_deliveries = true
84
+ ActionMailer::Base.deliveries.clear
85
+ end
86
+
87
+ def restore_test_deliveries
88
+ restore_delivery_method
89
+ ActionMailer::Base.perform_deliveries = @old_perform_deliveries
90
+ end
91
+
92
+ def set_delivery_method(method)
93
+ @old_delivery_method = ActionMailer::Base.delivery_method
94
+ ActionMailer::Base.delivery_method = method
95
+ end
96
+
97
+ def restore_delivery_method
98
+ ActionMailer::Base.deliveries.clear
99
+ ActionMailer::Base.delivery_method = @old_delivery_method
100
+ end
101
+
102
+ def set_expected_mail
103
+ @expected = Mail.new
104
+ @expected.content_type ["text", "plain", { "charset" => charset }]
105
+ @expected.mime_version = "1.0"
106
+ end
107
+
108
+ def charset
109
+ "UTF-8"
110
+ end
111
+
112
+ def encode(subject)
113
+ Mail::Encodings.q_value_encode(subject, charset)
114
+ end
115
+
116
+ def read_fixture(action)
117
+ IO.readlines(File.join(Rails.root, "test", "fixtures", self.class.mailer_class.name.underscore, action))
118
+ end
119
+ end
120
+
121
+ include Behavior
122
+ end
123
+ end