actionmailer 6.0.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of actionmailer might be problematic. Click here for more details.

@@ -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