diff options
Diffstat (limited to 'actionmailer')
56 files changed, 1967 insertions, 729 deletions
diff --git a/actionmailer/CHANGELOG.md b/actionmailer/CHANGELOG.md index b19d6b87c0..35574a2c73 100644 --- a/actionmailer/CHANGELOG.md +++ b/actionmailer/CHANGELOG.md @@ -3,95 +3,65 @@ *Marcus Ilgner* -* `config.force_ssl = true` will set - `config.action_mailer.default_url_options = { protocol: 'https' }` - - *Andrew Kampjes* - -* Add `config.action_mailer.deliver_later_queue_name` configuration to set the - mailer queue name. - - *Chris McGrath* - -* `assert_emails` in block form use the given number as expected value. - This makes the error message much easier to understand. - - *Yuji Yaginuma* - -* Add support for inline images in mailer previews by using an interceptor - class to convert cid: urls in image src attributes to data urls. - - *Andrew White* +* Allow ActionMailer classes to configure the parameterized delivery job + Example: + ``` + class MyMailer < ApplicationMailer + self.parameterized_delivery_job = MyCustomDeliveryJob + ... + end + ``` -* Mailer preview now uses `url_for` to fix links to emails for apps running on - a subdirectory. + *Luke Pearce* - *Remo Mueller* +* `ActionDispatch::IntegrationTest` includes `ActionMailer::TestHelper` module by default. -* Mailer previews no longer crash when the `mail` method wasn't called - (`NullMail`). + *Ricardo Díaz* - Fixes #19849. +* Add `perform_deliveries` to a payload of `deliver.action_mailer` notification. - *Yves Senn* + *Yoshiyuki Kinjo* -* Make sure labels and values line up in mailer previews. +* Change delivery logging message when `perform_deliveries` is false. - *Yves Senn* + *Yoshiyuki Kinjo* -* Add `assert_enqueued_emails` and `assert_no_enqueued_emails`. +* Allow call `assert_enqueued_email_with` with no block. Example: + ``` + def test_email + ContactMailer.welcome.deliver_later + assert_enqueued_email_with ContactMailer, :welcome + end - def test_emails - assert_enqueued_emails 2 do - ContactMailer.welcome.deliver_later - ContactMailer.welcome.deliver_later - end - end - - def test_no_emails - assert_no_enqueued_emails do - # No emails enqueued here - end - end - - *George Claghorn* - -* Add `_mailer` suffix to mailers created via generator, following the same - naming convention used in controllers and jobs. - - *Carlos Souza* - -* Remove deprecate `*_path` helpers in email views. - - *Rafael Mendonça França* - -* Remove deprecated `deliver` and `deliver!` methods. + def test_email_with_arguments + ContactMailer.welcome("Hello", "Goodbye").deliver_later + assert_enqueued_email_with ContactMailer, :welcome, args: ["Hello", "Goodbye"] + end + ``` - *claudiob* + *bogdanvlviv* -* Template lookup now respects default locale and I18n fallbacks. +* Ensure mail gem is eager autoloaded when eager load is true to prevent thread deadlocks. - Given the following templates: + *Samuel Cochran* - mailer/demo.html.erb - mailer/demo.en.html.erb - mailer/demo.pt.html.erb +* Perform email jobs in `assert_emails`. - Before this change, for a locale that doesn't have its associated file, the - `mailer/demo.html.erb` would be rendered even if `en` was the default locale. + *Gannon McGibbon* - Now `mailer/demo.en.html.erb` has precedence over the file without locale. +* Add `Base.unregister_observer`, `Base.unregister_observers`, + `Base.unregister_interceptor`, `Base.unregister_interceptors`, + `Base.unregister_preview_interceptor` and `Base.unregister_preview_interceptors`. + This makes it possible to dynamically add and remove email observers and + interceptors at runtime in the same way they're registered. - Also, it is possible to give a fallback. + *Claudio Ortolina*, *Kota Miyake* - mailer/demo.pt.html.erb - mailer/demo.pt-BR.html.erb +* Rails 6 requires Ruby 2.4.1 or newer. - So if the locale is `pt-PT`, `mailer/demo.pt.html.erb` will be rendered given - the right I18n fallback configuration. + *Jeremy Daer* - *Rafael Mendonça França* -Please check [4-2-stable](https://github.com/rails/rails/blob/4-2-stable/actionmailer/CHANGELOG.md) for previous changes. +Please check [5-2-stable](https://github.com/rails/rails/blob/5-2-stable/actionmailer/CHANGELOG.md) for previous changes. diff --git a/actionmailer/MIT-LICENSE b/actionmailer/MIT-LICENSE index 3ec7a617cf..1cb3add0fc 100644 --- a/actionmailer/MIT-LICENSE +++ b/actionmailer/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2015 David Heinemeier Hansson +Copyright (c) 2004-2018 David Heinemeier Hansson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/actionmailer/README.rdoc b/actionmailer/README.rdoc index 397ebe4201..14dfb82234 100644 --- a/actionmailer/README.rdoc +++ b/actionmailer/README.rdoc @@ -148,7 +148,7 @@ The latest version of Action Mailer can be installed with RubyGems: $ gem install actionmailer -Source code can be downloaded as part of the Rails project on GitHub +Source code can be downloaded as part of the Rails project on GitHub: * https://github.com/rails/rails/tree/master/actionmailer @@ -157,7 +157,7 @@ Source code can be downloaded as part of the Rails project on GitHub Action Mailer is released under the MIT license: -* http://www.opensource.org/licenses/MIT +* https://opensource.org/licenses/MIT == Support @@ -166,7 +166,7 @@ API documentation is at * http://api.rubyonrails.org -Bug reports can be filed for the Ruby on Rails project here: +Bug reports for the Ruby on Rails project can be filed here: * https://github.com/rails/rails/issues diff --git a/actionmailer/Rakefile b/actionmailer/Rakefile index 7197ea5e27..6ac408e1cb 100644 --- a/actionmailer/Rakefile +++ b/actionmailer/Rakefile @@ -1,12 +1,16 @@ -require 'rake/testtask' +# frozen_string_literal: true + +require "rake/testtask" desc "Default Task" task default: [ :test ] +task :package + # Run the unit tests Rake::TestTask.new { |t| t.libs << "test" - t.pattern = 'test/**/*_test.rb' + t.pattern = "test/**/*_test.rb" t.warning = true t.verbose = true t.ruby_opts = ["--dev"] if defined?(JRUBY_VERSION) @@ -15,7 +19,7 @@ Rake::TestTask.new { |t| namespace :test do task :isolated do Dir.glob("test/**/*_test.rb").all? do |file| - sh(Gem.ruby, '-w', '-Ilib:test', file) - end or raise "Failures" + sh(Gem.ruby, "-w", "-Ilib:test", file) + end || raise("Failures") end end diff --git a/actionmailer/actionmailer.gemspec b/actionmailer/actionmailer.gemspec index 782b208ef4..6f8d6d0699 100644 --- a/actionmailer/actionmailer.gemspec +++ b/actionmailer/actionmailer.gemspec @@ -1,28 +1,38 @@ -version = File.read(File.expand_path('../../RAILS_VERSION', __FILE__)).strip +# frozen_string_literal: true + +version = File.read(File.expand_path("../RAILS_VERSION", __dir__)).strip Gem::Specification.new do |s| s.platform = Gem::Platform::RUBY - s.name = 'actionmailer' + s.name = "actionmailer" s.version = version - s.summary = 'Email composition, delivery, and receiving framework (part of Rails).' - s.description = 'Email on Rails. Compose, deliver, receive, and test emails using the familiar controller/view pattern. First-class support for multipart email and attachments.' + s.summary = "Email composition, delivery, and receiving framework (part of Rails)." + s.description = "Email on Rails. Compose, deliver, receive, and test emails using the familiar controller/view pattern. First-class support for multipart email and attachments." + + s.required_ruby_version = ">= 2.4.1" + + s.license = "MIT" - s.required_ruby_version = '>= 2.2.2' + s.author = "David Heinemeier Hansson" + s.email = "david@loudthinking.com" + s.homepage = "http://rubyonrails.org" - s.license = 'MIT' + s.files = Dir["CHANGELOG.md", "README.rdoc", "MIT-LICENSE", "lib/**/*"] + s.require_path = "lib" + s.requirements << "none" - s.author = 'David Heinemeier Hansson' - s.email = 'david@loudthinking.com' - s.homepage = 'http://www.rubyonrails.org' + s.metadata = { + "source_code_uri" => "https://github.com/rails/rails/tree/v#{version}/actionmailer", + "changelog_uri" => "https://github.com/rails/rails/blob/v#{version}/actionmailer/CHANGELOG.md" + } - s.files = Dir['CHANGELOG.md', 'README.rdoc', 'MIT-LICENSE', 'lib/**/*'] - s.require_path = 'lib' - s.requirements << 'none' + # NOTE: Please read our dependency guidelines before updating versions: + # https://edgeguides.rubyonrails.org/security.html#dependency-management-and-cves - s.add_dependency 'actionpack', version - s.add_dependency 'actionview', version - s.add_dependency 'activejob', version + s.add_dependency "actionpack", version + s.add_dependency "actionview", version + s.add_dependency "activejob", version - s.add_dependency 'mail', ['~> 2.5', '>= 2.5.4'] - s.add_dependency 'rails-dom-testing', '~> 1.0', '>= 1.0.5' + s.add_dependency "mail", ["~> 2.5", ">= 2.5.4"] + s.add_dependency "rails-dom-testing", "~> 2.0" end diff --git a/actionmailer/bin/test b/actionmailer/bin/test index 404cabba51..c53377cc97 100755 --- a/actionmailer/bin/test +++ b/actionmailer/bin/test @@ -1,4 +1,5 @@ #!/usr/bin/env ruby -COMPONENT_ROOT = File.expand_path("../../", __FILE__) -require File.expand_path("../tools/test", COMPONENT_ROOT) -exit Minitest.run(ARGV) +# frozen_string_literal: true + +COMPONENT_ROOT = File.expand_path("..", __dir__) +require_relative "../../tools/test" diff --git a/actionmailer/lib/action_mailer.rb b/actionmailer/lib/action_mailer.rb index 312dd1997c..69eae65d60 100644 --- a/actionmailer/lib/action_mailer.rb +++ b/actionmailer/lib/action_mailer.rb @@ -1,5 +1,7 @@ +# frozen_string_literal: true + #-- -# Copyright (c) 2004-2015 David Heinemeier Hansson +# Copyright (c) 2004-2018 David Heinemeier Hansson # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -21,15 +23,16 @@ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. #++ -require 'abstract_controller' -require 'action_mailer/version' +require "abstract_controller" +require "action_mailer/version" # Common Active Support usage in Action Mailer -require 'active_support/rails' -require 'active_support/core_ext/class' -require 'active_support/core_ext/module/attr_internal' -require 'active_support/core_ext/string/inflections' -require 'active_support/lazy_load_hooks' +require "active_support" +require "active_support/rails" +require "active_support/core_ext/class" +require "active_support/core_ext/module/attr_internal" +require "active_support/core_ext/string/inflections" +require "active_support/lazy_load_hooks" module ActionMailer extend ::ActiveSupport::Autoload @@ -42,15 +45,23 @@ module ActionMailer autoload :DeliveryMethods autoload :InlinePreviewInterceptor autoload :MailHelper + autoload :Parameterized autoload :Preview - autoload :Previews, 'action_mailer/preview' + autoload :Previews, "action_mailer/preview" autoload :TestCase autoload :TestHelper autoload :MessageDelivery autoload :DeliveryJob + + def self.eager_load! + super + + require "mail" + Mail.eager_autoload! + end end -autoload :Mime, 'action_dispatch/http/mime_type' +autoload :Mime, "action_dispatch/http/mime_type" ActiveSupport.on_load(:action_view) do ActionView::Base.default_formats ||= Mime::SET.symbols diff --git a/actionmailer/lib/action_mailer/base.rb b/actionmailer/lib/action_mailer/base.rb index 9dd3f151ad..2bb1078bb8 100644 --- a/actionmailer/lib/action_mailer/base.rb +++ b/actionmailer/lib/action_mailer/base.rb @@ -1,10 +1,13 @@ -require 'mail' -require 'action_mailer/collector' -require 'active_support/core_ext/string/inflections' -require 'active_support/core_ext/hash/except' -require 'active_support/core_ext/module/anonymous' +# frozen_string_literal: true -require 'action_mailer/log_subscriber' +require "mail" +require "action_mailer/collector" +require "active_support/core_ext/string/inflections" +require "active_support/core_ext/hash/except" +require "active_support/core_ext/module/anonymous" + +require "action_mailer/log_subscriber" +require "action_mailer/rescuable" module ActionMailer # Action Mailer allows you to send email from your application using a mailer model and views. @@ -56,7 +59,7 @@ module ActionMailer # The hash passed to the mail method allows you to specify any header that a <tt>Mail::Message</tt> # will accept (any valid email header including optional fields). # - # The mail method, if not passed a block, will inspect your views and send all the views with + # The +mail+ method, if not passed a block, will inspect your views and send all the views with # the same name as the method, so the above action would send the +welcome.text.erb+ view # file as well as the +welcome.html.erb+ view file in a +multipart/alternative+ email. # @@ -86,7 +89,7 @@ module ActionMailer # Like Action Controller, each mailer class has a corresponding view directory in which each # method of the class looks for a template with its name. # - # To define a template to be used with a mailing, create an <tt>.erb</tt> file with the same + # To define a template to be used with a mailer, create an <tt>.erb</tt> file with the same # name as the method in your mailer model. For example, in the mailer defined above, the template at # <tt>app/views/notifier_mailer/welcome.text.erb</tt> would be used to generate the email. # @@ -132,7 +135,10 @@ module ActionMailer # # config.action_mailer.default_url_options = { host: "example.com" } # - # By default when <tt>config.force_ssl</tt> is true, URLs generated for hosts will use the HTTPS protocol. + # You can also define a <tt>default_url_options</tt> method on individual mailers to override these + # default settings per-mailer. + # + # By default when <tt>config.force_ssl</tt> is +true+, URLs generated for hosts will use the HTTPS protocol. # # = Sending mail # @@ -144,7 +150,7 @@ module ActionMailer # mail.deliver_now # generates and sends the email now # # The <tt>ActionMailer::MessageDelivery</tt> class is a wrapper around a delegate that will call - # your method to generate the mail. If you want direct access to delegator, or <tt>Mail::Message</tt>, + # your method to generate the mail. If you want direct access to the delegator, or <tt>Mail::Message</tt>, # you can call the <tt>message</tt> method on the <tt>ActionMailer::MessageDelivery</tt> object. # # NotifierMailer.welcome(User.first).message # => a Mail::Message object @@ -163,7 +169,7 @@ module ActionMailer # # Multipart messages can also be used implicitly because Action Mailer will automatically detect and use # multipart templates, where each template is named after the name of the action, followed by the content - # type. Each such detected template will be added as a separate part to the message. + # type. Each such detected template will be added to the message, as a separate part. # # For example, if the following templates exist: # * signup_notification.text.erb @@ -207,6 +213,19 @@ module ActionMailer # end # end # + # You can also send attachments with html template, in this case you need to add body, attachments, + # and custom content type like this: + # + # class NotifierMailer < ApplicationMailer + # def welcome(recipient) + # attachments["free_book.pdf"] = File.read("path/to/file.pdf") + # mail(to: recipient, + # subject: "New account information", + # content_type: "text/html", + # body: "<html><body>Hello there</body></html>") + # end + # end + # # = Inline Attachments # # You can also specify that a file should be displayed inline with other HTML. This is useful @@ -274,22 +293,21 @@ module ActionMailer # content_description: 'This is a description' # end # - # Finally, Action Mailer also supports passing <tt>Proc</tt> objects into the default hash, so you - # can define methods that evaluate as the message is being generated: + # Finally, Action Mailer also supports passing <tt>Proc</tt> and <tt>Lambda</tt> objects into the default hash, + # so you can define methods that evaluate as the message is being generated: # # class NotifierMailer < ApplicationMailer - # default 'X-Special-Header' => Proc.new { my_method } + # default 'X-Special-Header' => Proc.new { my_method }, to: -> { @inviter.email_address } # # private - # # def my_method # 'some complex call' # end # end # - # Note that the proc is evaluated right at the start of the mail message generation, so if you - # set something in the default using a proc, and then set the same thing inside of your - # mailer method, it will get over written by the mailer method. + # Note that the proc/lambda is evaluated right at the start of the mail message generation, so if you + # set something in the default hash using a proc, and then set the same thing inside of your + # mailer method, it will get overwritten by the mailer method. # # It is also possible to set these default options that will be used in all mailers through # the <tt>default_options=</tt> configuration in <tt>config/application.rb</tt>: @@ -298,7 +316,7 @@ module ActionMailer # # = Callbacks # - # You can specify callbacks using before_action and after_action for configuring your messages. + # You can specify callbacks using <tt>before_action</tt> and <tt>after_action</tt> for configuring your messages. # This may be useful, for example, when you want to add default inline attachments for all # messages sent out by a certain mailer class: # @@ -310,7 +328,6 @@ module ActionMailer # end # # private - # # def add_inline_attachment! # attachments.inline["footer.jpg"] = File.read('/path/to/filename.jpg') # end @@ -389,13 +406,13 @@ module ActionMailer # to use it. Defaults to <tt>true</tt>. # * <tt>:openssl_verify_mode</tt> - When using TLS, you can set how OpenSSL checks the certificate. This is # really useful if you need to validate a self-signed and/or a wildcard certificate. You can use the name - # of an OpenSSL verify constant (<tt>'none'</tt>, <tt>'peer'</tt>, <tt>'client_once'</tt>, - # <tt>'fail_if_no_peer_cert'</tt>) or directly the constant (<tt>OpenSSL::SSL::VERIFY_NONE</tt>, - # <tt>OpenSSL::SSL::VERIFY_PEER</tt>, ...). + # of an OpenSSL verify constant (<tt>'none'</tt> or <tt>'peer'</tt>) or directly the constant + # (<tt>OpenSSL::SSL::VERIFY_NONE</tt> or <tt>OpenSSL::SSL::VERIFY_PEER</tt>). + # <tt>:ssl/:tls</tt> Enables the SMTP connection to use SMTP/TLS (SMTPS: SMTP over direct TLS connection) # # * <tt>sendmail_settings</tt> - Allows you to override options for the <tt>:sendmail</tt> delivery method. # * <tt>:location</tt> - The location of the sendmail executable. Defaults to <tt>/usr/sbin/sendmail</tt>. - # * <tt>:arguments</tt> - The command line arguments. Defaults to <tt>-i -t</tt> with <tt>-f sender@address</tt> + # * <tt>:arguments</tt> - The command line arguments. Defaults to <tt>-i</tt> with <tt>-f sender@address</tt> # added automatically before the message is sent. # # * <tt>file_settings</tt> - Allows you to override options for the <tt>:file</tt> delivery method. @@ -416,9 +433,11 @@ module ActionMailer # * <tt>deliveries</tt> - Keeps an array of all the emails sent out through the Action Mailer with # <tt>delivery_method :test</tt>. Most useful for unit and functional testing. # - # * <tt>deliver_later_queue_name</tt> - The name of the queue used with <tt>deliver_later</tt>. + # * <tt>deliver_later_queue_name</tt> - The name of the queue used with <tt>deliver_later</tt>. Defaults to +mailers+. class Base < AbstractController::Base include DeliveryMethods + include Rescuable + include Parameterized include Previews abstract! @@ -430,6 +449,7 @@ module ActionMailer include AbstractController::Translation include AbstractController::AssetPaths include AbstractController::Callbacks + include AbstractController::Caching include ActionView::Layouts @@ -441,8 +461,9 @@ module ActionMailer helper ActionMailer::MailHelper - class_attribute :default_params - self.default_params = { + class_attribute :delivery_job, default: ::ActionMailer::DeliveryJob + class_attribute :parameterized_delivery_job, default: ::ActionMailer::Parameterized::DeliveryJob + class_attribute :default_params, default: { mime_version: "1.0", charset: "UTF-8", content_type: "text/plain", @@ -455,11 +476,21 @@ module ActionMailer observers.flatten.compact.each { |observer| register_observer(observer) } end + # Unregister one or more previously registered Observers. + def unregister_observers(*observers) + observers.flatten.compact.each { |observer| unregister_observer(observer) } + end + # Register one or more Interceptors which will be called before mail is sent. def register_interceptors(*interceptors) interceptors.flatten.compact.each { |interceptor| register_interceptor(interceptor) } end + # Unregister one or more previously registered Interceptors. + def unregister_interceptors(*interceptors) + interceptors.flatten.compact.each { |interceptor| unregister_interceptor(interceptor) } + end + # Register an Observer which will be notified when mail is delivered. # Either a class, string or symbol can be passed in as the Observer. # If a string or symbol is passed in it will be camelized and constantized. @@ -467,6 +498,13 @@ module ActionMailer Mail.register_observer(observer_class_for(observer)) end + # Unregister a previously registered Observer. + # Either a class, string or symbol can be passed in as the Observer. + # If a string or symbol is passed in it will be camelized and constantized. + def unregister_observer(observer) + Mail.unregister_observer(observer_class_for(observer)) + end + # Register an Interceptor which will be called before mail is sent. # Either a class, string or symbol can be passed in as the Interceptor. # If a string or symbol is passed in it will be camelized and constantized. @@ -474,6 +512,13 @@ module ActionMailer Mail.register_interceptor(observer_class_for(interceptor)) end + # Unregister a previously registered Interceptor. + # Either a class, string or symbol can be passed in as the Interceptor. + # If a string or symbol is passed in it will be camelized and constantized. + def unregister_interceptor(interceptor) + Mail.unregister_interceptor(observer_class_for(interceptor)) + end + def observer_class_for(value) # :nodoc: case value when String, Symbol @@ -484,7 +529,7 @@ module ActionMailer end private :observer_class_for - # Returns the name of current mailer. This method is also being used as a path for a view lookup. + # Returns the name of the current mailer. This method is also being used as a path for a view lookup. # If this is an anonymous mailer, this method will return +anonymous+ instead. def mailer_name @mailer_name ||= anonymous? ? "anonymous" : name.underscore @@ -541,21 +586,22 @@ module ActionMailer end end - protected + private - def set_payload_for_mail(payload, mail) #:nodoc: - payload[:mailer] = name - payload[:message_id] = mail.message_id - payload[:subject] = mail.subject - payload[:to] = mail.to - payload[:from] = mail.from - payload[:bcc] = mail.bcc if mail.bcc.present? - payload[:cc] = mail.cc if mail.cc.present? - payload[:date] = mail.date - payload[:mail] = mail.encoded + def set_payload_for_mail(payload, mail) + payload[:mailer] = name + payload[:message_id] = mail.message_id + payload[:subject] = mail.subject + payload[:to] = mail.to + payload[:from] = mail.from + payload[:bcc] = mail.bcc if mail.bcc.present? + payload[:cc] = mail.cc if mail.cc.present? + payload[:date] = mail.date + payload[:mail] = mail.encoded + payload[:perform_deliveries] = mail.perform_deliveries end - def method_missing(method_name, *args) # :nodoc: + def method_missing(method_name, *args) if action_methods.include?(method_name.to_s) MessageDelivery.new(self, method_name, *args) else @@ -563,19 +609,13 @@ module ActionMailer end end - private - - def respond_to_missing?(method, include_all = false) #:nodoc: - action_methods.include?(method.to_s) + def respond_to_missing?(method, include_all = false) + action_methods.include?(method.to_s) || super end end attr_internal :message - # Instantiate a new mailer object. If +method_name+ is not +nil+, the mailer - # will be initialized according to the named method. If not, the mailer will - # remain uninitialized (useful when you only need to invoke the "receive" - # method, for instance). def initialize super() @_mail_was_called = false @@ -585,7 +625,8 @@ module ActionMailer def process(method_name, *args) #:nodoc: payload = { mailer: self.class.name, - action: method_name + action: method_name, + args: args } ActiveSupport::Notifications.instrument("process.action_mailer", payload) do @@ -595,10 +636,10 @@ module ActionMailer end class NullMail #:nodoc: - def body; '' end + def body; "" end def header; {} end - def respond_to?(string, include_all=false) + def respond_to?(string, include_all = false) true end @@ -658,21 +699,21 @@ module ActionMailer # # mail.attachments['filename.jpg'] = File.read('/path/to/filename.jpg') # - # If you do this, then Mail will take the file name and work out the mime type - # set the Content-Type, Content-Disposition, Content-Transfer-Encoding and - # base64 encode the contents of the attachment all for you. + # If you do this, then Mail will take the file name and work out the mime type. + # It will also set the Content-Type, Content-Disposition, Content-Transfer-Encoding + # and encode the contents of the attachment in Base64. # # You can also specify overrides if you want by passing a hash instead of a string: # - # mail.attachments['filename.jpg'] = {mime_type: 'application/x-gzip', + # mail.attachments['filename.jpg'] = {mime_type: 'application/gzip', # content: File.read('/path/to/filename.jpg')} # - # If you want to use a different encoding than Base64, you can pass an encoding in, - # but then it is up to you to pass in the content pre-encoded, and don't expect - # Mail to know how to decode this data: + # If you want to use encoding other than Base64 then you will need to pass encoding + # type along with the pre-encoded content as Mail doesn't know how to decode the + # data: # # file_content = SpecialEncode(File.read('/path/to/filename.jpg')) - # mail.attachments['filename.jpg'] = {mime_type: 'application/x-gzip', + # mail.attachments['filename.jpg'] = {mime_type: 'application/gzip', # encoding: 'SpecialEncoding', # content: file_content } # @@ -827,131 +868,155 @@ module ActionMailer message end - protected - - # Used by #mail to set the content type of the message. - # - # It will use the given +user_content_type+, or multipart if the mail - # message has any attachments. If the attachments are inline, the content - # type will be "multipart/related", otherwise "multipart/mixed". - # - # If there is no content type passed in via headers, and there are no - # attachments, or the message is multipart, then the default content type is - # used. - def set_content_type(m, user_content_type, class_default) - params = m.content_type_parameters || {} - case - when user_content_type.present? - user_content_type - when m.has_attachments? - if m.attachments.detect(&:inline?) - ["multipart", "related", params] + private + + # Used by #mail to set the content type of the message. + # + # It will use the given +user_content_type+, or multipart if the mail + # message has any attachments. If the attachments are inline, the content + # type will be "multipart/related", otherwise "multipart/mixed". + # + # If there is no content type passed in via headers, and there are no + # attachments, or the message is multipart, then the default content type is + # used. + def set_content_type(m, user_content_type, class_default) # :doc: + params = m.content_type_parameters || {} + case + when user_content_type.present? + user_content_type + when m.has_attachments? + if m.attachments.detect(&:inline?) + ["multipart", "related", params] + else + ["multipart", "mixed", params] + end + when m.multipart? + ["multipart", "alternative", params] else - ["multipart", "mixed", params] + m.content_type || class_default end - when m.multipart? - ["multipart", "alternative", params] - else - m.content_type || class_default end - end - # Translates the +subject+ using Rails I18n class under <tt>[mailer_scope, action_name]</tt> scope. - # If it does not find a translation for the +subject+ under the specified scope it will default to a - # humanized version of the <tt>action_name</tt>. - # If the subject has interpolations, you can pass them through the +interpolations+ parameter. - def default_i18n_subject(interpolations = {}) - mailer_scope = self.class.mailer_name.tr('/', '.') - I18n.t(:subject, interpolations.merge(scope: [mailer_scope, action_name], default: action_name.humanize)) - end + # Translates the +subject+ using Rails I18n class under <tt>[mailer_scope, action_name]</tt> scope. + # If it does not find a translation for the +subject+ under the specified scope it will default to a + # humanized version of the <tt>action_name</tt>. + # If the subject has interpolations, you can pass them through the +interpolations+ parameter. + def default_i18n_subject(interpolations = {}) # :doc: + mailer_scope = self.class.mailer_name.tr("/", ".") + I18n.t(:subject, interpolations.merge(scope: [mailer_scope, action_name], default: action_name.humanize)) + end - # Emails do not support relative path links. - def self.supports_path? - false - end + # Emails do not support relative path links. + def self.supports_path? # :doc: + false + end - private + def apply_defaults(headers) + default_values = self.class.default.map do |key, value| + [ + key, + compute_default(value) + ] + end.to_h + + headers_with_defaults = headers.reverse_merge(default_values) + headers_with_defaults[:subject] ||= default_i18n_subject + headers_with_defaults + end - def apply_defaults(headers) - default_values = self.class.default.map do |key, value| - [ - key, - value.is_a?(Proc) ? instance_eval(&value) : value - ] - end.to_h - - headers_with_defaults = headers.reverse_merge(default_values) - headers_with_defaults[:subject] ||= default_i18n_subject - headers_with_defaults - end + def compute_default(value) + return value unless value.is_a?(Proc) - def assign_headers_to_message(message, headers) - assignable = headers.except(:parts_order, :content_type, :body, :template_name, - :template_path, :delivery_method, :delivery_method_options) - assignable.each { |k, v| message[k] = v } - end + if value.arity == 1 + instance_exec(self, &value) + else + instance_exec(&value) + end + end + + def assign_headers_to_message(message, headers) + assignable = headers.except(:parts_order, :content_type, :body, :template_name, + :template_path, :delivery_method, :delivery_method_options) + assignable.each { |k, v| message[k] = v } + end + + def collect_responses(headers) + if block_given? + collect_responses_from_block(headers, &Proc.new) + elsif headers[:body] + collect_responses_from_text(headers) + else + collect_responses_from_templates(headers) + end + end + + def collect_responses_from_block(headers) + templates_name = headers[:template_name] || action_name + collector = ActionMailer::Collector.new(lookup_context) { render(templates_name) } + yield(collector) + collector.responses + end - def collect_responses(headers) - if block_given? - collect_responses_from_block(headers, &Proc.new) - elsif headers[:body] + def collect_responses_from_text(headers) [{ body: headers.delete(:body), - content_type: self.class.default[:content_type] || "text/plain" + content_type: headers[:content_type] || "text/plain" }] - else - collect_responses_from_templates(headers) end - end - def collect_responses_from_block(headers) - templates_name = headers[:template_name] || action_name - collector = ActionMailer::Collector.new(lookup_context) { render(templates_name) } - yield(collector) - collector.responses - end + def collect_responses_from_templates(headers) + templates_path = headers[:template_path] || self.class.mailer_name + templates_name = headers[:template_name] || action_name + + each_template(Array(templates_path), templates_name).map do |template| + self.formats = template.formats + { + body: render(template: template), + content_type: template.type.to_s + } + end + end - def collect_responses_from_templates(headers) - templates_path = headers[:template_path] || self.class.mailer_name - templates_name = headers[:template_name] || action_name + def each_template(paths, name, &block) + templates = lookup_context.find_all(name, paths) + if templates.empty? + raise ActionView::MissingTemplate.new(paths, name, paths, false, "mailer") + else + templates.uniq(&:formats).each(&block) + end + end - each_template(Array(templates_path), templates_name).map do |template| - self.formats = template.formats - { - body: render(template: template), - content_type: template.type.to_s - } + def create_parts_from_responses(m, responses) + if responses.size == 1 && !m.has_attachments? + responses[0].each { |k, v| m[k] = v } + elsif responses.size > 1 && m.has_attachments? + container = Mail::Part.new + container.content_type = "multipart/alternative" + responses.each { |r| insert_part(container, r, m.charset) } + m.add_part(container) + else + responses.each { |r| insert_part(m, r, m.charset) } + end end - end - def each_template(paths, name, &block) - templates = lookup_context.find_all(name, paths) - if templates.empty? - raise ActionView::MissingTemplate.new(paths, name, paths, false, 'mailer') - else - templates.uniq(&:formats).each(&block) + def insert_part(container, response, charset) + response[:charset] ||= charset + part = Mail::Part.new(response) + container.add_part(part) end - end - def create_parts_from_responses(m, responses) - if responses.size == 1 && !m.has_attachments? - responses[0].each { |k,v| m[k] = v } - elsif responses.size > 1 && m.has_attachments? - container = Mail::Part.new - container.content_type = "multipart/alternative" - responses.each { |r| insert_part(container, r, m.charset) } - m.add_part(container) - else - responses.each { |r| insert_part(m, r, m.charset) } + # This and #instrument_name is for caching instrument + def instrument_payload(key) + { + mailer: mailer_name, + key: key + } end - end - def insert_part(container, response, charset) - response[:charset] ||= charset - part = Mail::Part.new(response) - container.add_part(part) - end + def instrument_name + "action_mailer" + end - ActiveSupport.run_load_hooks(:action_mailer, self) + ActiveSupport.run_load_hooks(:action_mailer, self) end end diff --git a/actionmailer/lib/action_mailer/collector.rb b/actionmailer/lib/action_mailer/collector.rb index e8883a8235..888410fa75 100644 --- a/actionmailer/lib/action_mailer/collector.rb +++ b/actionmailer/lib/action_mailer/collector.rb @@ -1,6 +1,8 @@ -require 'abstract_controller/collector' -require 'active_support/core_ext/hash/reverse_merge' -require 'active_support/core_ext/array/extract_options' +# frozen_string_literal: true + +require "abstract_controller/collector" +require "active_support/core_ext/hash/reverse_merge" +require "active_support/core_ext/array/extract_options" module ActionMailer class Collector diff --git a/actionmailer/lib/action_mailer/delivery_job.rb b/actionmailer/lib/action_mailer/delivery_job.rb index 52772af2d3..40f26d8ad1 100644 --- a/actionmailer/lib/action_mailer/delivery_job.rb +++ b/actionmailer/lib/action_mailer/delivery_job.rb @@ -1,13 +1,36 @@ -require 'active_job' +# frozen_string_literal: true + +require "active_job" module ActionMailer # The <tt>ActionMailer::DeliveryJob</tt> class is used when you # want to send emails outside of the request-response cycle. + # + # Exceptions are rescued and handled by the mailer class. class DeliveryJob < ActiveJob::Base # :nodoc: queue_as { ActionMailer::Base.deliver_later_queue_name } + rescue_from StandardError, with: :handle_exception_with_mailer_class + def perform(mailer, mail_method, delivery_method, *args) #:nodoc: mailer.constantize.public_send(mail_method, *args).send(delivery_method) end + + private + # "Deserialize" the mailer class name by hand in case another argument + # (like a Global ID reference) raised DeserializationError. + def mailer_class + if mailer = Array(@serialized_arguments).first || Array(arguments).first + mailer.constantize + end + end + + def handle_exception_with_mailer_class(exception) + if klass = mailer_class + klass.handle_exception exception + else + raise exception + end + end end end diff --git a/actionmailer/lib/action_mailer/delivery_methods.rb b/actionmailer/lib/action_mailer/delivery_methods.rb index 4758b55a2a..5cd62307e6 100644 --- a/actionmailer/lib/action_mailer/delivery_methods.rb +++ b/actionmailer/lib/action_mailer/delivery_methods.rb @@ -1,4 +1,6 @@ -require 'tmpdir' +# frozen_string_literal: true + +require "tmpdir" module ActionMailer # This module handles everything related to mail delivery, from registering @@ -7,25 +9,18 @@ module ActionMailer extend ActiveSupport::Concern included do - class_attribute :delivery_methods, :delivery_method - # Do not make this inheritable, because we always want it to propagate - cattr_accessor :raise_delivery_errors - self.raise_delivery_errors = true - - cattr_accessor :perform_deliveries - self.perform_deliveries = true - - cattr_accessor :deliver_later_queue_name - self.deliver_later_queue_name = :mailers + cattr_accessor :raise_delivery_errors, default: true + cattr_accessor :perform_deliveries, default: true + cattr_accessor :deliver_later_queue_name, default: :mailers - self.delivery_methods = {}.freeze - self.delivery_method = :smtp + class_attribute :delivery_methods, default: {}.freeze + class_attribute :delivery_method, default: :smtp add_delivery_method :smtp, Mail::SMTP, address: "localhost", port: 25, - domain: 'localhost.localdomain', + domain: "localhost.localdomain", user_name: nil, password: nil, authentication: nil, @@ -35,8 +30,8 @@ module ActionMailer location: defined?(Rails.root) ? "#{Rails.root}/tmp/mails" : "#{Dir.tmpdir}/mails" add_delivery_method :sendmail, Mail::Sendmail, - location: '/usr/sbin/sendmail', - arguments: '-i -t' + location: "/usr/sbin/sendmail", + arguments: "-i" add_delivery_method :test, Mail::TestMailer end @@ -51,15 +46,15 @@ module ActionMailer # # add_delivery_method :sendmail, Mail::Sendmail, # location: '/usr/sbin/sendmail', - # arguments: '-i -t' - def add_delivery_method(symbol, klass, default_options={}) + # arguments: '-i' + def add_delivery_method(symbol, klass, default_options = {}) class_attribute(:"#{symbol}_settings") unless respond_to?(:"#{symbol}_settings") send(:"#{symbol}_settings=", default_options) self.delivery_methods = delivery_methods.merge(symbol.to_sym => klass).freeze end - def wrap_delivery_behavior(mail, method=nil, options=nil) # :nodoc: - method ||= self.delivery_method + def wrap_delivery_behavior(mail, method = nil, options = nil) # :nodoc: + method ||= delivery_method mail.delivery_handler = self case method diff --git a/actionmailer/lib/action_mailer/gem_version.rb b/actionmailer/lib/action_mailer/gem_version.rb index b35d2ed965..72eb5d61e8 100644 --- a/actionmailer/lib/action_mailer/gem_version.rb +++ b/actionmailer/lib/action_mailer/gem_version.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActionMailer # Returns the version of the currently loaded Action Mailer as a <tt>Gem::Version</tt>. def self.gem_version @@ -5,7 +7,7 @@ module ActionMailer end module VERSION - MAJOR = 5 + MAJOR = 6 MINOR = 0 TINY = 0 PRE = "alpha" diff --git a/actionmailer/lib/action_mailer/inline_preview_interceptor.rb b/actionmailer/lib/action_mailer/inline_preview_interceptor.rb index 6d02b39225..2b97ac5b94 100644 --- a/actionmailer/lib/action_mailer/inline_preview_interceptor.rb +++ b/actionmailer/lib/action_mailer/inline_preview_interceptor.rb @@ -1,9 +1,11 @@ -require 'base64' +# frozen_string_literal: true + +require "base64" module ActionMailer # Implements a mailer preview interceptor that converts image tag src attributes - # that use inline cid: style urls to data: style urls so that they are visible - # when previewing a HTML email in a web browser. + # that use inline cid: style URLs to data: style URLs so that they are visible + # when previewing an HTML email in a web browser. # # This interceptor is enabled by default. To disable it, delete it from the # <tt>ActionMailer::Base.preview_interceptors</tt> array: @@ -11,7 +13,7 @@ module ActionMailer # ActionMailer::Base.preview_interceptors.delete(ActionMailer::InlinePreviewInterceptor) # class InlinePreviewInterceptor - PATTERN = /src=(?:"cid:[^"]+"|'cid:[^']+')/i + PATTERN = /src=(?:"cid:[^"]+"|'cid:[^']+')/i include Base64 @@ -26,7 +28,7 @@ module ActionMailer def transform! #:nodoc: return message if html_part.blank? - html_source.gsub!(PATTERN) do |match| + html_part.body = html_part.decoded.gsub(PATTERN) do |match| if part = find_part(match[9..-2]) %[src="#{data_url(part)}"] else @@ -38,24 +40,18 @@ module ActionMailer end private - def message - @message - end + attr_reader :message def html_part @html_part ||= message.html_part end - def html_source - html_part.body.raw_source - end - def data_url(part) "data:#{part.mime_type};base64,#{strict_encode64(part.body.raw_source)}" end def find_part(cid) - message.all_parts.find{ |p| p.attachment? && p.cid == cid } + message.all_parts.find { |p| p.attachment? && p.cid == cid } end end end diff --git a/actionmailer/lib/action_mailer/log_subscriber.rb b/actionmailer/lib/action_mailer/log_subscriber.rb index 2867bf90fb..25c99342c2 100644 --- a/actionmailer/lib/action_mailer/log_subscriber.rb +++ b/actionmailer/lib/action_mailer/log_subscriber.rb @@ -1,4 +1,6 @@ -require 'active_support/log_subscriber' +# frozen_string_literal: true + +require "active_support/log_subscriber" module ActionMailer # Implements the ActiveSupport::LogSubscriber for logging notifications when @@ -7,8 +9,13 @@ module ActionMailer # An email was delivered. def deliver(event) info do - recipients = Array(event.payload[:to]).join(', ') - "Sent mail to #{recipients} (#{event.duration.round(1)}ms)" + perform_deliveries = event.payload[:perform_deliveries] + recipients = Array(event.payload[:to]).join(", ") + if perform_deliveries + "Sent mail to #{recipients} (#{event.duration.round(1)}ms)" + else + "Skipped sending mail to #{recipients} as `perform_deliveries` is false" + end end debug { event.payload[:mail] } diff --git a/actionmailer/lib/action_mailer/mail_helper.rb b/actionmailer/lib/action_mailer/mail_helper.rb index 239974e7b1..e7bed41f8d 100644 --- a/actionmailer/lib/action_mailer/mail_helper.rb +++ b/actionmailer/lib/action_mailer/mail_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActionMailer # Provides helper methods for ActionMailer::Base that can be used for easily # formatting messages, accessing mailer or message instances, and the @@ -54,7 +56,7 @@ module ActionMailer sentences = [[]] text.split.each do |word| - if sentences.first.present? && (sentences.last + [word]).join(' ').length > len + if sentences.first.present? && (sentences.last + [word]).join(" ").length > len sentences << [word] else sentences.last << word diff --git a/actionmailer/lib/action_mailer/message_delivery.rb b/actionmailer/lib/action_mailer/message_delivery.rb index 5fcb5a0c88..a57980c322 100644 --- a/actionmailer/lib/action_mailer/message_delivery.rb +++ b/actionmailer/lib/action_mailer/message_delivery.rb @@ -1,9 +1,10 @@ -require 'delegate' +# frozen_string_literal: true -module ActionMailer +require "delegate" +module ActionMailer # The <tt>ActionMailer::MessageDelivery</tt> class is used by - # <tt>ActionMailer::Base</tt> when creating a new mailer. + # ActionMailer::Base when creating a new mailer. # <tt>MessageDelivery</tt> is a wrapper (+Delegator+ subclass) around a lazy # created <tt>Mail::Message</tt>. You can get direct access to the # <tt>Mail::Message</tt>, deliver the email or schedule the email to be sent @@ -14,29 +15,35 @@ module ActionMailer # Notifier.welcome(User.first).deliver_later # enqueue email delivery as a job through Active Job # Notifier.welcome(User.first).message # a Mail::Message object class MessageDelivery < Delegator - def initialize(mailer, mail_method, *args) #:nodoc: - @mailer = mailer - @mail_method = mail_method - @args = args + def initialize(mailer_class, action, *args) #:nodoc: + @mailer_class, @action, @args = mailer_class, action, args + + # The mail is only processed if we try to call any methods on it. + # Typical usage will leave it unloaded and call deliver_later. + @processed_mailer = nil + @mail_message = nil end + # Method calls are delegated to the Mail::Message that's ready to deliver. def __getobj__ #:nodoc: - @obj ||= begin - mailer = @mailer.new - mailer.process @mail_method, *@args - mailer.message - end + @mail_message ||= processed_mailer.message end - def __setobj__(obj) #:nodoc: - @obj = obj + # Unused except for delegator internals (dup, marshalling). + def __setobj__(mail_message) #:nodoc: + @mail_message = mail_message end - # Returns the Mail::Message object + # Returns the resulting Mail::Message def message __getobj__ end + # Was the delegate loaded, causing the mailer action to be processed? + def processed? + @processed_mailer || @mail_message + end + # Enqueues the email to be delivered through Active Job. When the # job runs it will send the email using +deliver_now!+. That means # that the message will be sent bypassing checking +perform_deliveries+ @@ -51,7 +58,15 @@ module ActionMailer # * <tt>:wait</tt> - Enqueue the email to be delivered with a delay # * <tt>:wait_until</tt> - Enqueue the email to be delivered at (after) a specific date / time # * <tt>:queue</tt> - Enqueue the email on the specified queue - def deliver_later!(options={}) + # + # By default, the email will be enqueued using <tt>ActionMailer::DeliveryJob</tt>. Each + # <tt>ActionMailer::Base</tt> class can specify the job to use by setting the class variable + # +delivery_job+. + # + # class AccountRegistrationMailer < ApplicationMailer + # self.delivery_job = RegistrationDeliveryJob + # end + def deliver_later!(options = {}) enqueue_delivery :deliver_now!, options end @@ -67,7 +82,15 @@ module ActionMailer # * <tt>:wait</tt> - Enqueue the email to be delivered with a delay. # * <tt>:wait_until</tt> - Enqueue the email to be delivered at (after) a specific date / time. # * <tt>:queue</tt> - Enqueue the email on the specified queue. - def deliver_later(options={}) + # + # By default, the email will be enqueued using <tt>ActionMailer::DeliveryJob</tt>. Each + # <tt>ActionMailer::Base</tt> class can specify the job to use by setting the class variable + # +delivery_job+. + # + # class AccountRegistrationMailer < ApplicationMailer + # self.delivery_job = RegistrationDeliveryJob + # end + def deliver_later(options = {}) enqueue_delivery :deliver_now, options end @@ -77,7 +100,9 @@ module ActionMailer # Notifier.welcome(User.first).deliver_now! # def deliver_now! - message.deliver! + processed_mailer.handle_exceptions do + message.deliver! + end end # Delivers an email: @@ -85,14 +110,35 @@ module ActionMailer # Notifier.welcome(User.first).deliver_now # def deliver_now - message.deliver + processed_mailer.handle_exceptions do + message.deliver + end end private + # Returns the processed Mailer instance. We keep this instance + # on hand so we can delegate exception handling to it. + def processed_mailer + @processed_mailer ||= @mailer_class.new.tap do |mailer| + mailer.process @action, *@args + end + end - def enqueue_delivery(delivery_method, options={}) - args = @mailer.name, @mail_method.to_s, delivery_method.to_s, *@args - ActionMailer::DeliveryJob.set(options).perform_later(*args) + def enqueue_delivery(delivery_method, options = {}) + if processed? + ::Kernel.raise "You've accessed the message before asking to " \ + "deliver it later, so you may have made local changes that would " \ + "be silently lost if we enqueued a job to deliver it. Why? Only " \ + "the mailer method *arguments* are passed with the delivery job! " \ + "Do not access the message in any way if you mean to deliver it " \ + "later. Workarounds: 1. don't touch the message before calling " \ + "#deliver_later, 2. only touch the message *within your mailer " \ + "method*, or 3. use a custom Active Job instead of #deliver_later." + else + args = @mailer_class.name, @action.to_s, delivery_method.to_s, *@args + job = @mailer_class.delivery_job + job.set(options).perform_later(*args) + end end end end diff --git a/actionmailer/lib/action_mailer/parameterized.rb b/actionmailer/lib/action_mailer/parameterized.rb new file mode 100644 index 0000000000..0fe417affe --- /dev/null +++ b/actionmailer/lib/action_mailer/parameterized.rb @@ -0,0 +1,155 @@ +# frozen_string_literal: true + +module ActionMailer + # Provides the option to parameterize mailers in order to share instance variable + # setup, processing, and common headers. + # + # Consider this example that does not use parameterization: + # + # class InvitationsMailer < ApplicationMailer + # def account_invitation(inviter, invitee) + # @account = inviter.account + # @inviter = inviter + # @invitee = invitee + # + # subject = "#{@inviter.name} invited you to their Basecamp (#{@account.name})" + # + # mail \ + # subject: subject, + # to: invitee.email_address, + # from: common_address(inviter), + # reply_to: inviter.email_address_with_name + # end + # + # def project_invitation(project, inviter, invitee) + # @account = inviter.account + # @project = project + # @inviter = inviter + # @invitee = invitee + # @summarizer = ProjectInvitationSummarizer.new(@project.bucket) + # + # subject = "#{@inviter.name.familiar} added you to a project in Basecamp (#{@account.name})" + # + # mail \ + # subject: subject, + # to: invitee.email_address, + # from: common_address(inviter), + # reply_to: inviter.email_address_with_name + # end + # + # def bulk_project_invitation(projects, inviter, invitee) + # @account = inviter.account + # @projects = projects.sort_by(&:name) + # @inviter = inviter + # @invitee = invitee + # + # subject = "#{@inviter.name.familiar} added you to some new stuff in Basecamp (#{@account.name})" + # + # mail \ + # subject: subject, + # to: invitee.email_address, + # from: common_address(inviter), + # reply_to: inviter.email_address_with_name + # end + # end + # + # InvitationsMailer.account_invitation(person_a, person_b).deliver_later + # + # Using parameterized mailers, this can be rewritten as: + # + # class InvitationsMailer < ApplicationMailer + # before_action { @inviter, @invitee = params[:inviter], params[:invitee] } + # before_action { @account = params[:inviter].account } + # + # default to: -> { @invitee.email_address }, + # from: -> { common_address(@inviter) }, + # reply_to: -> { @inviter.email_address_with_name } + # + # def account_invitation + # mail subject: "#{@inviter.name} invited you to their Basecamp (#{@account.name})" + # end + # + # def project_invitation + # @project = params[:project] + # @summarizer = ProjectInvitationSummarizer.new(@project.bucket) + # + # mail subject: "#{@inviter.name.familiar} added you to a project in Basecamp (#{@account.name})" + # end + # + # def bulk_project_invitation + # @projects = params[:projects].sort_by(&:name) + # + # mail subject: "#{@inviter.name.familiar} added you to some new stuff in Basecamp (#{@account.name})" + # end + # end + # + # InvitationsMailer.with(inviter: person_a, invitee: person_b).account_invitation.deliver_later + module Parameterized + extend ActiveSupport::Concern + + included do + attr_accessor :params + end + + module ClassMethods + # Provide the parameters to the mailer in order to use them in the instance methods and callbacks. + # + # InvitationsMailer.with(inviter: person_a, invitee: person_b).account_invitation.deliver_later + # + # See Parameterized documentation for full example. + def with(params) + ActionMailer::Parameterized::Mailer.new(self, params) + end + end + + class Mailer # :nodoc: + def initialize(mailer, params) + @mailer, @params = mailer, params + end + + private + def method_missing(method_name, *args) + if @mailer.action_methods.include?(method_name.to_s) + ActionMailer::Parameterized::MessageDelivery.new(@mailer, method_name, @params, *args) + else + super + end + end + + def respond_to_missing?(method, include_all = false) + @mailer.respond_to?(method, include_all) + end + end + + class MessageDelivery < ActionMailer::MessageDelivery # :nodoc: + def initialize(mailer_class, action, params, *args) + super(mailer_class, action, *args) + @params = params + end + + private + def processed_mailer + @processed_mailer ||= @mailer_class.new.tap do |mailer| + mailer.params = @params + mailer.process @action, *@args + end + end + + def enqueue_delivery(delivery_method, options = {}) + if processed? + super + else + args = @mailer_class.name, @action.to_s, delivery_method.to_s, @params, *@args + job = @mailer_class.parameterized_delivery_job + job.set(options).perform_later(*args) + end + end + end + + class DeliveryJob < ActionMailer::DeliveryJob # :nodoc: + def perform(mailer, mail_method, delivery_method, params, *args) + mailer.constantize.with(params).public_send(mail_method, *args).send(delivery_method) + end + end + end +end diff --git a/actionmailer/lib/action_mailer/preview.rb b/actionmailer/lib/action_mailer/preview.rb index aab92fe8db..500b3bede0 100644 --- a/actionmailer/lib/action_mailer/preview.rb +++ b/actionmailer/lib/action_mailer/preview.rb @@ -1,4 +1,6 @@ -require 'active_support/descendants_tracker' +# frozen_string_literal: true + +require "active_support/descendants_tracker" module ActionMailer module Previews #:nodoc: @@ -15,13 +17,12 @@ module ActionMailer # # config.action_mailer.show_previews = true # - # Defaults to true for development environment + # Defaults to +true+ for development environment # mattr_accessor :show_previews, instance_writer: false # :nodoc: - mattr_accessor :preview_interceptors, instance_writer: false - self.preview_interceptors = [ActionMailer::InlinePreviewInterceptor] + mattr_accessor :preview_interceptors, instance_writer: false, default: [ActionMailer::InlinePreviewInterceptor] end module ClassMethods @@ -30,27 +31,51 @@ module ActionMailer interceptors.flatten.compact.each { |interceptor| register_preview_interceptor(interceptor) } end + # Unregister one or more previously registered Interceptors. + def unregister_preview_interceptors(*interceptors) + interceptors.flatten.compact.each { |interceptor| unregister_preview_interceptor(interceptor) } + end + # Register an Interceptor which will be called before mail is previewed. # Either a class or a string can be passed in as the Interceptor. If a - # string is passed in it will be <tt>constantize</tt>d. + # string is passed in it will be constantized. def register_preview_interceptor(interceptor) - preview_interceptor = case interceptor + preview_interceptor = interceptor_class_for(interceptor) + + unless preview_interceptors.include?(preview_interceptor) + preview_interceptors << preview_interceptor + end + end + + # Unregister a previously registered Interceptor. + # Either a class or a string can be passed in as the Interceptor. If a + # string is passed in it will be constantized. + def unregister_preview_interceptor(interceptor) + preview_interceptors.delete(interceptor_class_for(interceptor)) + end + + private + + def interceptor_class_for(interceptor) + case interceptor when String, Symbol interceptor.to_s.camelize.constantize else interceptor end - - unless preview_interceptors.include?(preview_interceptor) - preview_interceptors << preview_interceptor end - end end end class Preview extend ActiveSupport::DescendantsTracker + attr_reader :params + + def initialize(params = {}) + @params = params + end + class << self # Returns all mailer preview classes. def all @@ -61,8 +86,8 @@ module ActionMailer # Returns the mail object for the given email name. The registered preview # interceptors will be informed so that they can transform the message # as they would if the mail was actually being delivered. - def call(email) - preview = self.new + def call(email, params = {}) + preview = new(params) message = preview.public_send(email) inform_preview_interceptors(message) message @@ -73,42 +98,42 @@ module ActionMailer public_instance_methods(false).map(&:to_s).sort end - # Returns true if the email exists. + # Returns +true+ if the email exists. def email_exists?(email) emails.include?(email) end - # Returns true if the preview exists. + # Returns +true+ if the preview exists. def exists?(preview) - all.any?{ |p| p.preview_name == preview } + all.any? { |p| p.preview_name == preview } end # Find a mailer preview by its underscored class name. def find(preview) - all.find{ |p| p.preview_name == preview } + all.find { |p| p.preview_name == preview } end # Returns the underscored name of the mailer preview without the suffix. def preview_name - name.sub(/Preview$/, '').underscore + name.sub(/Preview$/, "").underscore end - protected - def load_previews #:nodoc: + private + def load_previews if preview_path - Dir["#{preview_path}/**/*_preview.rb"].each{ |file| require_dependency file } + Dir["#{preview_path}/**/*_preview.rb"].sort.each { |file| require_dependency file } end end - def preview_path #:nodoc: + def preview_path Base.preview_path end - def show_previews #:nodoc: + def show_previews Base.show_previews end - def inform_preview_interceptors(message) #:nodoc: + def inform_preview_interceptors(message) Base.preview_interceptors.each do |interceptor| interceptor.previewing_email(message) end diff --git a/actionmailer/lib/action_mailer/railtie.rb b/actionmailer/lib/action_mailer/railtie.rb index fa707021c7..bb6141b406 100644 --- a/actionmailer/lib/action_mailer/railtie.rb +++ b/actionmailer/lib/action_mailer/railtie.rb @@ -1,4 +1,6 @@ -require 'active_job/railtie' +# frozen_string_literal: true + +require "active_job/railtie" require "action_mailer" require "rails" require "abstract_controller/railties/routes_helpers" @@ -18,16 +20,17 @@ module ActionMailer if app.config.force_ssl options.default_url_options ||= {} - options.default_url_options[:protocol] ||= 'https' + options.default_url_options[:protocol] ||= "https" end options.assets_dir ||= paths["public"].first options.javascripts_dir ||= paths["public/javascripts"].first options.stylesheets_dir ||= paths["public/stylesheets"].first options.show_previews = Rails.env.development? if options.show_previews.nil? + options.cache_store ||= Rails.cache if options.show_previews - options.preview_path ||= defined?(Rails.root) ? "#{Rails.root}/test/mailers/previews" : nil + options.preview_path ||= defined?(Rails.root) ? "#{Rails.root}/test/mailers/previews" : nil end # make sure readers methods get compiled @@ -43,14 +46,12 @@ module ActionMailer register_preview_interceptors(options.delete(:preview_interceptors)) register_observers(options.delete(:observers)) - options.each { |k,v| send("#{k}=", v) } + options.each { |k, v| send("#{k}=", v) } + end - if options.show_previews - app.routes.prepend do - get '/rails/mailers' => "rails/mailers#index" - get '/rails/mailers/*path' => "rails/mailers#preview" - end - end + ActiveSupport.on_load(:action_dispatch_integration_test) do + include ActionMailer::TestHelper + include ActionMailer::TestCase::ClearTestDeliveries end end @@ -60,9 +61,24 @@ module ActionMailer end end - config.after_initialize do - if ActionMailer::Base.preview_path - ActiveSupport::Dependencies.autoload_paths << ActionMailer::Base.preview_path + initializer "action_mailer.eager_load_actions" do + ActiveSupport.on_load(:after_initialize) do + ActionMailer::Base.descendants.each(&:action_methods) if config.eager_load + end + end + + config.after_initialize do |app| + options = app.config.action_mailer + + if options.show_previews + app.routes.prepend do + get "/rails/mailers" => "rails/mailers#index", internal: true + get "/rails/mailers/*path" => "rails/mailers#preview", internal: true + end + + if options.preview_path + ActiveSupport::Dependencies.autoload_paths << options.preview_path + end end end end diff --git a/actionmailer/lib/action_mailer/rescuable.rb b/actionmailer/lib/action_mailer/rescuable.rb new file mode 100644 index 0000000000..5b567eb500 --- /dev/null +++ b/actionmailer/lib/action_mailer/rescuable.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module ActionMailer #:nodoc: + # Provides +rescue_from+ for mailers. Wraps mailer action processing, + # mail job processing, and mail delivery. + module Rescuable + extend ActiveSupport::Concern + include ActiveSupport::Rescuable + + class_methods do + def handle_exception(exception) #:nodoc: + rescue_with_handler(exception) || raise(exception) + end + end + + def handle_exceptions #:nodoc: + yield + rescue => exception + rescue_with_handler(exception) || raise + end + + private + def process(*) + handle_exceptions do + super + end + end + end +end diff --git a/actionmailer/lib/action_mailer/test_case.rb b/actionmailer/lib/action_mailer/test_case.rb index 0aa15e31ba..ee5a864847 100644 --- a/actionmailer/lib/action_mailer/test_case.rb +++ b/actionmailer/lib/action_mailer/test_case.rb @@ -1,16 +1,35 @@ -require 'active_support/test_case' -require 'rails-dom-testing' +# frozen_string_literal: true + +require "active_support/test_case" +require "rails-dom-testing" module ActionMailer class NonInferrableMailerError < ::StandardError def initialize(name) - super "Unable to determine the mailer to test from #{name}. " + - "You'll need to specify it using tests YourMailer in your " + + super "Unable to determine the mailer to test from #{name}. " \ + "You'll need to specify it using tests YourMailer in your " \ "test case definition" end end class TestCase < ActiveSupport::TestCase + module ClearTestDeliveries + extend ActiveSupport::Concern + + included do + setup :clear_test_deliveries + teardown :clear_test_deliveries + end + + private + + def clear_test_deliveries + if ActionMailer::Base.delivery_method == :test + ActionMailer::Base.deliveries.clear + end + end + end + module Behavior extend ActiveSupport::Concern @@ -24,6 +43,7 @@ module ActionMailer setup :initialize_test_deliveries setup :set_expected_mail teardown :restore_test_deliveries + ActiveSupport.run_load_hooks(:action_mailer_test_case, self) end module ClassMethods @@ -39,7 +59,7 @@ module ActionMailer end def mailer_class - if mailer = self._mailer_class + if mailer = _mailer_class mailer else tests determine_default_mailer(name) @@ -55,37 +75,36 @@ module ActionMailer end end - protected + private - def initialize_test_deliveries # :nodoc: + def initialize_test_deliveries set_delivery_method :test @old_perform_deliveries = ActionMailer::Base.perform_deliveries ActionMailer::Base.perform_deliveries = true + ActionMailer::Base.deliveries.clear end - def restore_test_deliveries # :nodoc: + def restore_test_deliveries restore_delivery_method ActionMailer::Base.perform_deliveries = @old_perform_deliveries - ActionMailer::Base.deliveries.clear end - def set_delivery_method(method) # :nodoc: + def set_delivery_method(method) @old_delivery_method = ActionMailer::Base.delivery_method ActionMailer::Base.delivery_method = method end - def restore_delivery_method # :nodoc: + def restore_delivery_method + ActionMailer::Base.deliveries.clear ActionMailer::Base.delivery_method = @old_delivery_method end - def set_expected_mail # :nodoc: + def set_expected_mail @expected = Mail.new @expected.content_type ["text", "plain", { "charset" => charset }] - @expected.mime_version = '1.0' + @expected.mime_version = "1.0" end - private - def charset "UTF-8" end @@ -95,7 +114,7 @@ module ActionMailer end def read_fixture(action) - IO.readlines(File.join(Rails.root, 'test', 'fixtures', self.class.mailer_class.name.underscore, action)) + IO.readlines(File.join(Rails.root, "test", "fixtures", self.class.mailer_class.name.underscore, action)) end end diff --git a/actionmailer/lib/action_mailer/test_helper.rb b/actionmailer/lib/action_mailer/test_helper.rb index e423aac389..a4751916af 100644 --- a/actionmailer/lib/action_mailer/test_helper.rb +++ b/actionmailer/lib/action_mailer/test_helper.rb @@ -1,4 +1,6 @@ -require 'active_job' +# frozen_string_literal: true + +require "active_job" module ActionMailer # Provides helper methods for testing Action Mailer, including #assert_emails @@ -26,13 +28,13 @@ module ActionMailer # # assert_emails 2 do # ContactMailer.welcome.deliver_now - # ContactMailer.welcome.deliver_now + # ContactMailer.welcome.deliver_later # end # end - def assert_emails(number) + def assert_emails(number, &block) if block_given? original_count = ActionMailer::Base.deliveries.size - yield + perform_enqueued_jobs(only: [ActionMailer::DeliveryJob, ActionMailer::Parameterized::DeliveryJob], &block) new_count = ActionMailer::Base.deliveries.size assert_equal number, new_count - original_count, "#{number} emails expected, but #{new_count - original_count} were sent" else @@ -58,7 +60,7 @@ module ActionMailer # # Note: This assertion is simply a shortcut for: # - # assert_emails 0 + # assert_emails 0, &block def assert_no_emails(&block) assert_emails 0, &block end @@ -88,7 +90,49 @@ module ActionMailer # end # end def assert_enqueued_emails(number, &block) - assert_enqueued_jobs number, only: ActionMailer::DeliveryJob, &block + assert_enqueued_jobs number, only: [ ActionMailer::DeliveryJob, ActionMailer::Parameterized::DeliveryJob ], &block + end + + # Asserts that a specific email has been enqueued, optionally + # matching arguments. + # + # def test_email + # ContactMailer.welcome.deliver_later + # assert_enqueued_email_with ContactMailer, :welcome + # end + # + # def test_email_with_arguments + # ContactMailer.welcome("Hello", "Goodbye").deliver_later + # assert_enqueued_email_with ContactMailer, :welcome, args: ["Hello", "Goodbye"] + # end + # + # If a block is passed, that block should cause the specified email + # to be enqueued. + # + # def test_email_in_block + # assert_enqueued_email_with ContactMailer, :welcome do + # ContactMailer.welcome.deliver_later + # end + # end + # + # If +args+ is provided as a Hash, a parameterized email is matched. + # + # def test_parameterized_email + # assert_enqueued_email_with ContactMailer, :welcome, + # args: {email: 'user@example.com'} do + # ContactMailer.with(email: 'user@example.com').welcome.deliver_later + # end + # end + def assert_enqueued_email_with(mailer, method, args: nil, queue: "mailers", &block) + if args.is_a? Hash + job = ActionMailer::Parameterized::DeliveryJob + args = [mailer.to_s, method.to_s, "deliver_now", args] + else + job = ActionMailer::DeliveryJob + args = [mailer.to_s, method.to_s, "deliver_now", *args] + end + + assert_enqueued_with(job: job, args: args, queue: queue, &block) end # Asserts that no emails are enqueued for later delivery. @@ -107,7 +151,7 @@ module ActionMailer # end # end def assert_no_enqueued_emails(&block) - assert_no_enqueued_jobs only: ActionMailer::DeliveryJob, &block + assert_no_enqueued_jobs only: [ ActionMailer::DeliveryJob, ActionMailer::Parameterized::DeliveryJob ], &block end end end diff --git a/actionmailer/lib/action_mailer/version.rb b/actionmailer/lib/action_mailer/version.rb index 06f80a8fdc..4549d6eb57 100644 --- a/actionmailer/lib/action_mailer/version.rb +++ b/actionmailer/lib/action_mailer/version.rb @@ -1,4 +1,6 @@ -require_relative 'gem_version' +# frozen_string_literal: true + +require_relative "gem_version" module ActionMailer # Returns the version of the currently loaded Action Mailer as a diff --git a/actionmailer/lib/rails/generators/mailer/mailer_generator.rb b/actionmailer/lib/rails/generators/mailer/mailer_generator.rb index 3ec7d3d896..c37a74c762 100644 --- a/actionmailer/lib/rails/generators/mailer/mailer_generator.rb +++ b/actionmailer/lib/rails/generators/mailer/mailer_generator.rb @@ -1,24 +1,37 @@ +# frozen_string_literal: true + module Rails module Generators class MailerGenerator < NamedBase - source_root File.expand_path("../templates", __FILE__) + source_root File.expand_path("templates", __dir__) argument :actions, type: :array, default: [], banner: "method method" check_class_collision suffix: "Mailer" def create_mailer_file - template "mailer.rb", File.join('app/mailers', class_path, "#{file_name}_mailer.rb") - if self.behavior == :invoke - template "application_mailer.rb", 'app/mailers/application_mailer.rb' + template "mailer.rb", File.join("app/mailers", class_path, "#{file_name}_mailer.rb") + + in_root do + if behavior == :invoke && !File.exist?(application_mailer_file_name) + template "application_mailer.rb", application_mailer_file_name + end end end hook_for :template_engine, :test_framework - protected - def file_name - @_file_name ||= super.gsub(/\_mailer/i, '') + private + def file_name # :doc: + @_file_name ||= super.sub(/_mailer\z/i, "") + end + + def application_mailer_file_name + @_application_mailer_file_name ||= if mountable_engine? + "app/mailers/#{namespaced_path}/application_mailer.rb" + else + "app/mailers/application_mailer.rb" + end end end end diff --git a/actionmailer/lib/rails/generators/mailer/templates/application_mailer.rb b/actionmailer/lib/rails/generators/mailer/templates/application_mailer.rb deleted file mode 100644 index d25d8892dd..0000000000 --- a/actionmailer/lib/rails/generators/mailer/templates/application_mailer.rb +++ /dev/null @@ -1,4 +0,0 @@ -class ApplicationMailer < ActionMailer::Base - default from: "from@example.com" - layout 'mailer' -end diff --git a/actionmailer/lib/rails/generators/mailer/templates/application_mailer.rb.tt b/actionmailer/lib/rails/generators/mailer/templates/application_mailer.rb.tt new file mode 100644 index 0000000000..00fb9bd48f --- /dev/null +++ b/actionmailer/lib/rails/generators/mailer/templates/application_mailer.rb.tt @@ -0,0 +1,6 @@ +<% module_namespacing do -%> +class ApplicationMailer < ActionMailer::Base + default from: 'from@example.com' + layout 'mailer' +end +<% end %> diff --git a/actionmailer/lib/rails/generators/mailer/templates/mailer.rb b/actionmailer/lib/rails/generators/mailer/templates/mailer.rb.tt index 348d314758..348d314758 100644 --- a/actionmailer/lib/rails/generators/mailer/templates/mailer.rb +++ b/actionmailer/lib/rails/generators/mailer/templates/mailer.rb.tt diff --git a/actionmailer/test/abstract_unit.rb b/actionmailer/test/abstract_unit.rb index 285b2cfcb5..f647896374 100644 --- a/actionmailer/test/abstract_unit.rb +++ b/actionmailer/test/abstract_unit.rb @@ -1,26 +1,27 @@ -require File.expand_path('../../../load_paths', __FILE__) -require 'active_support/core_ext/kernel/reporting' +# frozen_string_literal: true + +require "active_support/core_ext/kernel/reporting" # These are the normal settings that will be set up by Railties # TODO: Have these tests support other combinations of these values silence_warnings do - Encoding.default_internal = "UTF-8" - Encoding.default_external = "UTF-8" + Encoding.default_internal = Encoding::UTF_8 + Encoding.default_external = Encoding::UTF_8 end module Rails def self.root - File.expand_path('../', File.dirname(__FILE__)) + File.expand_path("..", __dir__) end end -require 'active_support/testing/autorun' -require 'active_support/testing/method_call_assertions' -require 'action_mailer' -require 'action_mailer/test_case' +require "active_support/testing/autorun" +require "active_support/testing/method_call_assertions" +require "action_mailer" +require "action_mailer/test_case" # Emulate AV railtie -require 'action_view' +require "action_view" ActionMailer::Base.include(ActionView::Layouts) # Show backtraces for deprecated behavior for quicker cleanup. @@ -29,18 +30,20 @@ ActiveSupport::Deprecation.debug = true # Disable available locale checks to avoid warnings running the test suite. I18n.enforce_available_locales = false -FIXTURE_LOAD_PATH = File.expand_path('fixtures', File.dirname(__FILE__)) +FIXTURE_LOAD_PATH = File.expand_path("fixtures", __dir__) ActionMailer::Base.view_paths = FIXTURE_LOAD_PATH -# Skips the current run on Rubinius using Minitest::Assertions#skip -def rubinius_skip(message = '') - skip message if RUBY_ENGINE == 'rbx' -end -# Skips the current run on JRuby using Minitest::Assertions#skip -def jruby_skip(message = '') - skip message if defined?(JRUBY_VERSION) -end - class ActiveSupport::TestCase include ActiveSupport::Testing::MethodCallAssertions + + private + # Skips the current run on Rubinius using Minitest::Assertions#skip + def rubinius_skip(message = "") + skip message if RUBY_ENGINE == "rbx" + end + + # Skips the current run on JRuby using Minitest::Assertions#skip + def jruby_skip(message = "") + skip message if defined?(JRUBY_VERSION) + end end diff --git a/actionmailer/test/assert_select_email_test.rb b/actionmailer/test/assert_select_email_test.rb index cae2e20abd..9699fe4000 100644 --- a/actionmailer/test/assert_select_email_test.rb +++ b/actionmailer/test/assert_select_email_test.rb @@ -1,4 +1,6 @@ -require 'abstract_unit' +# frozen_string_literal: true + +require "abstract_unit" class AssertSelectEmailTest < ActionMailer::TestCase class AssertSelectMailer < ActionMailer::Base @@ -11,8 +13,8 @@ class AssertSelectEmailTest < ActionMailer::TestCase class AssertMultipartSelectMailer < ActionMailer::Base def test(options) mail subject: "Test e-mail", from: "test@test.host", to: "test <test@test.host>" do |format| - format.text { render text: options[:text] } - format.html { render text: options[:html] } + format.text { render plain: options[:text] } + format.html { render plain: options[:html] } end end end @@ -23,7 +25,7 @@ class AssertSelectEmailTest < ActionMailer::TestCase def test_assert_select_email assert_raise ActiveSupport::TestCase::Assertion do - assert_select_email {} + assert_select_email { } end AssertSelectMailer.test("<div><p>foo</p><p>bar</p></div>").deliver_now @@ -36,7 +38,7 @@ class AssertSelectEmailTest < ActionMailer::TestCase end def test_assert_select_email_multipart - AssertMultipartSelectMailer.test(html: "<div><p>foo</p><p>bar</p></div>", text: 'foo bar').deliver_now + AssertMultipartSelectMailer.test(html: "<div><p>foo</p><p>bar</p></div>", text: "foo bar").deliver_now assert_select_email do assert_select "div:root" do assert_select "p:first-child", "foo" diff --git a/actionmailer/test/asset_host_test.rb b/actionmailer/test/asset_host_test.rb index 10cfdcf693..9cd8cae88c 100644 --- a/actionmailer/test/asset_host_test.rb +++ b/actionmailer/test/asset_host_test.rb @@ -1,11 +1,13 @@ -require 'abstract_unit' -require 'action_controller' +# frozen_string_literal: true + +require "abstract_unit" +require "action_controller" class AssetHostMailer < ActionMailer::Base def email_with_asset - mail to: 'test@localhost', - subject: 'testing email containing asset path while asset_host is set', - from: 'tester@example.com' + mail to: "test@localhost", + subject: "testing email containing asset path while asset_host is set", + from: "tester@example.com" end end @@ -22,16 +24,16 @@ class AssetHostTest < ActionMailer::TestCase def test_asset_host_as_string mail = AssetHostMailer.email_with_asset - assert_dom_equal %Q{<img alt="Somelogo" src="http://www.example.com/images/somelogo.png" />}, mail.body.to_s.strip + assert_dom_equal '<img src="http://www.example.com/images/somelogo.png" />', mail.body.to_s.strip end def test_asset_host_as_one_argument_proc AssetHostMailer.config.asset_host = Proc.new { |source| - if source.starts_with?('/images') - 'http://images.example.com' + if source.starts_with?("/images") + "http://images.example.com" end } mail = AssetHostMailer.email_with_asset - assert_dom_equal %Q{<img alt="Somelogo" src="http://images.example.com/images/somelogo.png" />}, mail.body.to_s.strip + assert_dom_equal '<img src="http://images.example.com/images/somelogo.png" />', mail.body.to_s.strip end end diff --git a/actionmailer/test/base_test.rb b/actionmailer/test/base_test.rb index fe5358f209..86c0172772 100644 --- a/actionmailer/test/base_test.rb +++ b/actionmailer/test/base_test.rb @@ -1,12 +1,14 @@ -require 'abstract_unit' -require 'set' +# frozen_string_literal: true -require 'action_dispatch' -require 'active_support/time' +require "abstract_unit" +require "set" -require 'mailers/base_mailer' -require 'mailers/proc_mailer' -require 'mailers/asset_mailer' +require "action_dispatch" +require "active_support/time" + +require "mailers/base_mailer" +require "mailers/proc_mailer" +require "mailers/asset_mailer" class BaseTest < ActiveSupport::TestCase include Rails::Dom::Testing::Assertions::DomAssertions @@ -32,33 +34,33 @@ class BaseTest < ActiveSupport::TestCase # Basic mail usage without block test "mail() should set the headers of the mail message" do email = BaseMailer.welcome - assert_equal(['system@test.lindsaar.net'], email.to) - assert_equal(['jose@test.plataformatec.com'], email.from) - assert_equal('The first email on new API!', email.subject) + assert_equal(["system@test.lindsaar.net"], email.to) + assert_equal(["jose@test.plataformatec.com"], email.from) + assert_equal("The first email on new API!", email.subject) end test "mail() with from overwrites the class level default" do - email = BaseMailer.welcome(from: 'someone@example.com', - to: 'another@example.org') - assert_equal(['someone@example.com'], email.from) - assert_equal(['another@example.org'], email.to) + email = BaseMailer.welcome(from: "someone@example.com", + to: "another@example.org") + assert_equal(["someone@example.com"], email.from) + assert_equal(["another@example.org"], email.to) end test "mail() with bcc, cc, content_type, charset, mime_version, reply_to and date" do time = Time.now.beginning_of_day.to_datetime - email = BaseMailer.welcome(bcc: 'bcc@test.lindsaar.net', - cc: 'cc@test.lindsaar.net', - content_type: 'multipart/mixed', - charset: 'iso-8559-1', - mime_version: '2.0', - reply_to: 'reply-to@test.lindsaar.net', + email = BaseMailer.welcome(bcc: "bcc@test.lindsaar.net", + cc: "cc@test.lindsaar.net", + content_type: "multipart/mixed", + charset: "iso-8559-1", + mime_version: "2.0", + reply_to: "reply-to@test.lindsaar.net", date: time) - assert_equal(['bcc@test.lindsaar.net'], email.bcc) - assert_equal(['cc@test.lindsaar.net'], email.cc) - assert_equal('multipart/mixed; charset=iso-8559-1', email.content_type) - assert_equal('iso-8559-1', email.charset) - assert_equal('2.0', email.mime_version) - assert_equal(['reply-to@test.lindsaar.net'], email.reply_to) + assert_equal(["bcc@test.lindsaar.net"], email.bcc) + assert_equal(["cc@test.lindsaar.net"], email.cc) + assert_equal("multipart/mixed; charset=iso-8559-1", email.content_type) + assert_equal("iso-8559-1", email.charset) + assert_equal("2.0", email.mime_version) + assert_equal(["reply-to@test.lindsaar.net"], email.reply_to) assert_equal(time, email.date) end @@ -75,63 +77,63 @@ class BaseTest < ActiveSupport::TestCase test "should set template content type if mail has only one part" do mail = BaseMailer.html_only - assert_equal('text/html', mail.mime_type) + assert_equal("text/html", mail.mime_type) mail = BaseMailer.plain_text_only - assert_equal('text/plain', mail.mime_type) + assert_equal("text/plain", mail.mime_type) end # Custom headers test "custom headers" do email = BaseMailer.welcome - assert_equal("Not SPAM", email['X-SPAM'].decoded) + assert_equal("Not SPAM", email["X-SPAM"].decoded) end test "can pass random headers in as a hash to mail" do - hash = {'X-Special-Domain-Specific-Header' => "SecretValue", - 'In-Reply-To' => '1234@mikel.me.com' } + hash = { "X-Special-Domain-Specific-Header" => "SecretValue", + "In-Reply-To" => "1234@mikel.me.com" } mail = BaseMailer.welcome(hash) - assert_equal('SecretValue', mail['X-Special-Domain-Specific-Header'].decoded) - assert_equal('1234@mikel.me.com', mail['In-Reply-To'].decoded) + assert_equal("SecretValue", mail["X-Special-Domain-Specific-Header"].decoded) + assert_equal("1234@mikel.me.com", mail["In-Reply-To"].decoded) end test "can pass random headers in as a hash to headers" do - hash = {'X-Special-Domain-Specific-Header' => "SecretValue", - 'In-Reply-To' => '1234@mikel.me.com' } + hash = { "X-Special-Domain-Specific-Header" => "SecretValue", + "In-Reply-To" => "1234@mikel.me.com" } mail = BaseMailer.welcome_with_headers(hash) - assert_equal('SecretValue', mail['X-Special-Domain-Specific-Header'].decoded) - assert_equal('1234@mikel.me.com', mail['In-Reply-To'].decoded) + assert_equal("SecretValue", mail["X-Special-Domain-Specific-Header"].decoded) + assert_equal("1234@mikel.me.com", mail["In-Reply-To"].decoded) end # Attachments test "attachment with content" do email = BaseMailer.attachment_with_content assert_equal(1, email.attachments.length) - assert_equal('invoice.pdf', email.attachments[0].filename) - assert_equal('This is test File content', email.attachments['invoice.pdf'].decoded) + assert_equal("invoice.pdf", email.attachments[0].filename) + assert_equal("This is test File content", email.attachments["invoice.pdf"].decoded) end test "attachment gets content type from filename" do email = BaseMailer.attachment_with_content - assert_equal('invoice.pdf', email.attachments[0].filename) - assert_equal('application/pdf', email.attachments[0].mime_type) + assert_equal("invoice.pdf", email.attachments[0].filename) + assert_equal("application/pdf", email.attachments[0].mime_type) end test "attachment with hash" do email = BaseMailer.attachment_with_hash assert_equal(1, email.attachments.length) - assert_equal('invoice.jpg', email.attachments[0].filename) - expected = "\312\213\254\232)b" + assert_equal("invoice.jpg", email.attachments[0].filename) + expected = +"\312\213\254\232)b" expected.force_encoding(Encoding::BINARY) - assert_equal expected, email.attachments['invoice.jpg'].decoded + assert_equal expected, email.attachments["invoice.jpg"].decoded end test "attachment with hash using default mail encoding" do email = BaseMailer.attachment_with_hash_default_encoding assert_equal(1, email.attachments.length) - assert_equal('invoice.jpg', email.attachments[0].filename) - expected = "\312\213\254\232)b" + assert_equal("invoice.jpg", email.attachments[0].filename) + expected = +"\312\213\254\232)b" expected.force_encoding(Encoding::BINARY) - assert_equal expected, email.attachments['invoice.jpg'].decoded + assert_equal expected, email.attachments["invoice.jpg"].decoded end test "sets mime type to multipart/mixed when attachment is included" do @@ -140,14 +142,19 @@ class BaseTest < ActiveSupport::TestCase assert_equal("multipart/mixed", email.mime_type) end + test "set mime type to text/html when attachment is included and body is set" do + email = BaseMailer.attachment_with_content(body: "Hello there", content_type: "text/html") + assert_equal("text/html", email.mime_type) + end + test "adds the rendered template as part" do email = BaseMailer.attachment_with_content assert_equal(2, email.parts.length) assert_equal("multipart/mixed", email.mime_type) assert_equal("text/html", email.parts[0].mime_type) - assert_equal("Attachment with content", email.parts[0].body.encoded) + assert_equal("Attachment with content", email.parts[0].decoded) assert_equal("application/pdf", email.parts[1].mime_type) - assert_equal("VGhpcyBpcyB0ZXN0IEZpbGUgY29udGVudA==\r\n", email.parts[1].body.encoded) + assert_equal("This is test File content", email.parts[1].decoded) end test "adds the given :body as part" do @@ -155,9 +162,9 @@ class BaseTest < ActiveSupport::TestCase assert_equal(2, email.parts.length) assert_equal("multipart/mixed", email.mime_type) assert_equal("text/plain", email.parts[0].mime_type) - assert_equal("I'm the eggman", email.parts[0].body.encoded) + assert_equal("I'm the eggman", email.parts[0].decoded) assert_equal("application/pdf", email.parts[1].mime_type) - assert_equal("VGhpcyBpcyB0ZXN0IEZpbGUgY29udGVudA==\r\n", email.parts[1].body.encoded) + assert_equal("This is test File content", email.parts[1].decoded) end test "can embed an inline attachment" do @@ -215,24 +222,24 @@ class BaseTest < ActiveSupport::TestCase email = BaseMailer.welcome(subject: nil) assert_equal "Welcome", email.subject - with_translation 'en', base_mailer: {welcome: {subject: "New Subject!"}} do + with_translation "en", base_mailer: { welcome: { subject: "New Subject!" } } do email = BaseMailer.welcome(subject: nil) assert_equal "New Subject!", email.subject end end end - test 'default subject can have interpolations' do - with_translation 'en', base_mailer: {with_subject_interpolations: {subject: 'Will the real %{rapper_or_impersonator} please stand up?'}} do + test "default subject can have interpolations" do + with_translation "en", base_mailer: { with_subject_interpolations: { subject: "Will the real %{rapper_or_impersonator} please stand up?" } } do email = BaseMailer.with_subject_interpolations - assert_equal 'Will the real Slim Shady please stand up?', email.subject + assert_equal "Will the real Slim Shady please stand up?", email.subject end end test "translations are scoped properly" do - with_translation 'en', base_mailer: {email_with_translations: {greet_user: "Hello %{name}!"}} do + with_translation "en", base_mailer: { email_with_translations: { greet_user: "Hello %{name}!" } } do email = BaseMailer.email_with_translations - assert_equal 'Hello lifo!', email.body.encoded + assert_equal "Hello lifo!", email.body.encoded end end @@ -240,7 +247,7 @@ class BaseTest < ActiveSupport::TestCase class LateAttachmentMailer < ActionMailer::Base def welcome mail body: "yay", from: "welcome@example.com", to: "to@example.com" - attachments['invoice.pdf'] = 'This is test File content' + attachments["invoice.pdf"] = "This is test File content" end end @@ -252,7 +259,7 @@ class BaseTest < ActiveSupport::TestCase class LateInlineAttachmentMailer < ActionMailer::Base def welcome mail body: "yay", from: "welcome@example.com", to: "to@example.com" - attachments.inline['invoice.pdf'] = 'This is test File content' + attachments.inline["invoice.pdf"] = "This is test File content" end end @@ -271,13 +278,13 @@ class BaseTest < ActiveSupport::TestCase assert_nothing_raised { mail.message } assert_equal ["image/jpeg; filename=controller_attachments.jpg", - "image/jpeg; filename=attachments.jpg"], mail.attachments.inline.map {|a| a['Content-Type'].to_s } + "image/jpeg; filename=attachments.jpg"], mail.attachments.inline.map { |a| a["Content-Type"].to_s } end test "accessing attachments works after mail was called" do class LateAttachmentAccessorMailer < ActionMailer::Base def welcome - attachments['invoice.pdf'] = 'This is test File content' + attachments["invoice.pdf"] = "This is test File content" mail body: "yay", from: "welcome@example.com", to: "to@example.com" unless attachments.map(&:filename) == ["invoice.pdf"] @@ -315,22 +322,21 @@ class BaseTest < ActiveSupport::TestCase test "implicit multipart with attachments creates nested parts" do email = BaseMailer.implicit_multipart(attachments: true) - assert_equal("application/pdf", email.parts[0].mime_type) - assert_equal("multipart/alternative", email.parts[1].mime_type) - assert_equal("text/plain", email.parts[1].parts[0].mime_type) - assert_equal("TEXT Implicit Multipart", email.parts[1].parts[0].body.encoded) - assert_equal("text/html", email.parts[1].parts[1].mime_type) - assert_equal("HTML Implicit Multipart", email.parts[1].parts[1].body.encoded) + assert_equal(%w[ application/pdf multipart/alternative ], email.parts.map(&:mime_type).sort) + multipart = email.parts.detect { |p| p.mime_type == "multipart/alternative" } + assert_equal("text/plain", multipart.parts[0].mime_type) + assert_equal("TEXT Implicit Multipart", multipart.parts[0].body.encoded) + assert_equal("text/html", multipart.parts[1].mime_type) + assert_equal("HTML Implicit Multipart", multipart.parts[1].body.encoded) end test "implicit multipart with attachments and sort order" do order = ["text/html", "text/plain"] with_default BaseMailer, parts_order: order do email = BaseMailer.implicit_multipart(attachments: true) - assert_equal("application/pdf", email.parts[0].mime_type) - assert_equal("multipart/alternative", email.parts[1].mime_type) - assert_equal("text/plain", email.parts[1].parts[1].mime_type) - assert_equal("text/html", email.parts[1].parts[0].mime_type) + assert_equal(%w[ application/pdf multipart/alternative ], email.parts.map(&:mime_type).sort) + multipart = email.parts.detect { |p| p.mime_type == "multipart/alternative" } + assert_equal(%w[ text/html text/plain ], multipart.parts.map(&:mime_type).sort) end end @@ -366,7 +372,7 @@ class BaseTest < ActiveSupport::TestCase I18n.backend = fallback_backend.new I18n.fallbacks[:"de-AT"] = [:de] - swap I18n, locale: 'de-AT' do + swap I18n, locale: "de-AT" do email = BaseMailer.implicit_with_locale assert_equal(2, email.parts.size) assert_equal("multipart/alternative", email.mime_type) @@ -380,7 +386,6 @@ class BaseTest < ActiveSupport::TestCase end end - test "implicit multipart with several view paths uses the first one with template" do old = BaseMailer.view_paths begin @@ -421,12 +426,12 @@ class BaseTest < ActiveSupport::TestCase test "explicit multipart with attachments creates nested parts" do email = BaseMailer.explicit_multipart(attachments: true) - assert_equal("application/pdf", email.parts[0].mime_type) - assert_equal("multipart/alternative", email.parts[1].mime_type) - assert_equal("text/plain", email.parts[1].parts[0].mime_type) - assert_equal("TEXT Explicit Multipart", email.parts[1].parts[0].body.encoded) - assert_equal("text/html", email.parts[1].parts[1].mime_type) - assert_equal("HTML Explicit Multipart", email.parts[1].parts[1].body.encoded) + assert_equal(%w[ application/pdf multipart/alternative ], email.parts.map(&:mime_type).sort) + multipart = email.parts.detect { |p| p.mime_type == "multipart/alternative" } + assert_equal("text/plain", multipart.parts[0].mime_type) + assert_equal("TEXT Explicit Multipart", multipart.parts[0].body.encoded) + assert_equal("text/html", multipart.parts[1].mime_type) + assert_equal("HTML Explicit Multipart", multipart.parts[1].body.encoded) end test "explicit multipart with templates" do @@ -449,7 +454,7 @@ class BaseTest < ActiveSupport::TestCase assert_equal("Format with any!", email.parts[1].body.encoded) end - test 'explicit without specifying format with format.any' do + test "explicit without specifying format with format.any" do error = assert_raises(ArgumentError) do BaseMailer.explicit_without_specifying_format_with_any.parts end @@ -501,20 +506,20 @@ class BaseTest < ActiveSupport::TestCase test "should respond to action methods" do assert_respond_to BaseMailer, :welcome assert_respond_to BaseMailer, :implicit_multipart - assert !BaseMailer.respond_to?(:mail) - assert !BaseMailer.respond_to?(:headers) + assert_not_respond_to BaseMailer, :mail + assert_not_respond_to BaseMailer, :headers end test "calling just the action should return the generated mail object" do email = BaseMailer.welcome assert_equal(0, BaseMailer.deliveries.length) - assert_equal('The first email on new API!', email.subject) + assert_equal("The first email on new API!", email.subject) end test "calling deliver on the action should deliver the mail object" do assert_called(BaseMailer, :deliver_mail) do mail = BaseMailer.welcome.deliver_now - assert_equal 'The first email on new API!', mail.subject + assert_equal "The first email on new API!", mail.subject end end @@ -534,41 +539,41 @@ class BaseTest < ActiveSupport::TestCase # Rendering test "you can specify a different template for implicit render" do - mail = BaseMailer.implicit_different_template('implicit_multipart').deliver_now + mail = BaseMailer.implicit_different_template("implicit_multipart").deliver_now assert_equal("HTML Implicit Multipart", mail.html_part.body.decoded) assert_equal("TEXT Implicit Multipart", mail.text_part.body.decoded) end test "you can specify a different template for multipart render" do - mail = BaseMailer.implicit_different_template_with_block('explicit_multipart_templates').deliver + mail = BaseMailer.implicit_different_template_with_block("explicit_multipart_templates").deliver assert_equal("HTML Explicit Multipart Templates", mail.html_part.body.decoded) assert_equal("TEXT Explicit Multipart Templates", mail.text_part.body.decoded) end test "should raise if missing template in implicit render" do assert_raises ActionView::MissingTemplate do - BaseMailer.implicit_different_template('missing_template').deliver_now + BaseMailer.implicit_different_template("missing_template").deliver_now end assert_equal(0, BaseMailer.deliveries.length) end test "you can specify a different template for explicit render" do - mail = BaseMailer.explicit_different_template('explicit_multipart_templates').deliver_now + mail = BaseMailer.explicit_different_template("explicit_multipart_templates").deliver_now assert_equal("HTML Explicit Multipart Templates", mail.html_part.body.decoded) assert_equal("TEXT Explicit Multipart Templates", mail.text_part.body.decoded) end test "you can specify a different layout" do - mail = BaseMailer.different_layout('different_layout').deliver_now + mail = BaseMailer.different_layout("different_layout").deliver_now assert_equal("HTML -- HTML", mail.html_part.body.decoded) assert_equal("PLAIN -- PLAIN", mail.text_part.body.decoded) end test "you can specify the template path for implicit lookup" do - mail = BaseMailer.welcome_from_another_path('another.path/base_mailer').deliver_now + mail = BaseMailer.welcome_from_another_path("another.path/base_mailer").deliver_now assert_equal("Welcome from another path", mail.body.encoded) - mail = BaseMailer.welcome_from_another_path(['unknown/invalid', 'another.path/base_mailer']).deliver_now + mail = BaseMailer.welcome_from_another_path(["unknown/invalid", "another.path/base_mailer"]).deliver_now assert_equal("Welcome from another path", mail.body.encoded) end @@ -578,7 +583,7 @@ class BaseTest < ActiveSupport::TestCase mail = AssetMailer.welcome - assert_dom_equal(%{<img alt="Dummy" src="http://global.com/images/dummy.png" />}, mail.body.to_s.strip) + assert_dom_equal(%{<img src="http://global.com/images/dummy.png" />}, mail.body.to_s.strip) end test "assets tags should use a Mailer's asset_host settings when available" do @@ -592,18 +597,18 @@ class BaseTest < ActiveSupport::TestCase mail = TempAssetMailer.welcome - assert_dom_equal(%{<img alt="Dummy" src="http://local.com/images/dummy.png" />}, mail.body.to_s.strip) + assert_dom_equal(%{<img src="http://local.com/images/dummy.png" />}, mail.body.to_s.strip) end - test 'the view is not rendered when mail was never called' do + test "the view is not rendered when mail was never called" do mail = BaseMailer.without_mail_call - assert_equal('', mail.body.to_s.strip) + assert_equal("", mail.body.to_s.strip) mail.deliver_now end - test 'the return value of mailer methods is not relevant' do + test "the return value of mailer methods is not relevant" do mail = BaseMailer.with_nil_as_return_value - assert_equal('Welcome', mail.body.to_s.strip) + assert_equal("Welcome", mail.body.to_s.strip) mail.deliver_now end @@ -619,37 +624,52 @@ class BaseTest < ActiveSupport::TestCase end end - test "you can register an observer to the mail object that gets informed on email delivery" do + test "you can register and unregister an observer to the mail object that gets informed on email delivery" do mail_side_effects do ActionMailer::Base.register_observer(MyObserver) mail = BaseMailer.welcome assert_called_with(MyObserver, :delivered_email, [mail]) do mail.deliver_now end + + ActionMailer::Base.unregister_observer(MyObserver) + assert_not_called(MyObserver, :delivered_email, returns: mail) do + mail.deliver_now + end end end - test "you can register an observer using its stringified name to the mail object that gets informed on email delivery" do + test "you can register and unregister an observer using its stringified name to the mail object that gets informed on email delivery" do mail_side_effects do ActionMailer::Base.register_observer("BaseTest::MyObserver") mail = BaseMailer.welcome assert_called_with(MyObserver, :delivered_email, [mail]) do mail.deliver_now end + + ActionMailer::Base.unregister_observer("BaseTest::MyObserver") + assert_not_called(MyObserver, :delivered_email, returns: mail) do + mail.deliver_now + end end end - test "you can register an observer using its symbolized underscored name to the mail object that gets informed on email delivery" do + test "you can register and unregister an observer using its symbolized underscored name to the mail object that gets informed on email delivery" do mail_side_effects do ActionMailer::Base.register_observer(:"base_test/my_observer") mail = BaseMailer.welcome assert_called_with(MyObserver, :delivered_email, [mail]) do mail.deliver_now end + + ActionMailer::Base.unregister_observer(:"base_test/my_observer") + assert_not_called(MyObserver, :delivered_email, returns: mail) do + mail.deliver_now + end end end - test "you can register multiple observers to the mail object that both get informed on email delivery" do + test "you can register and unregister multiple observers to the mail object that both get informed on email delivery" do mail_side_effects do ActionMailer::Base.register_observers("BaseTest::MyObserver", MySecondObserver) mail = BaseMailer.welcome @@ -658,6 +678,14 @@ class BaseTest < ActiveSupport::TestCase mail.deliver_now end end + + ActionMailer::Base.unregister_observers("BaseTest::MyObserver", MySecondObserver) + assert_not_called(MyObserver, :delivered_email, returns: mail) do + mail.deliver_now + end + assert_not_called(MySecondObserver, :delivered_email, returns: mail) do + mail.deliver_now + end end end @@ -671,37 +699,52 @@ class BaseTest < ActiveSupport::TestCase def self.previewing_email(mail); end end - test "you can register an interceptor to the mail object that gets passed the mail object before delivery" do + test "you can register and unregister an interceptor to the mail object that gets passed the mail object before delivery" do mail_side_effects do ActionMailer::Base.register_interceptor(MyInterceptor) mail = BaseMailer.welcome assert_called_with(MyInterceptor, :delivering_email, [mail]) do mail.deliver_now end + + ActionMailer::Base.unregister_interceptor(MyInterceptor) + assert_not_called(MyInterceptor, :delivering_email, returns: mail) do + mail.deliver_now + end end end - test "you can register an interceptor using its stringified name to the mail object that gets passed the mail object before delivery" do + test "you can register and unregister an interceptor using its stringified name to the mail object that gets passed the mail object before delivery" do mail_side_effects do ActionMailer::Base.register_interceptor("BaseTest::MyInterceptor") mail = BaseMailer.welcome assert_called_with(MyInterceptor, :delivering_email, [mail]) do mail.deliver_now end + + ActionMailer::Base.unregister_interceptor("BaseTest::MyInterceptor") + assert_not_called(MyInterceptor, :delivering_email, returns: mail) do + mail.deliver_now + end end end - test "you can register an interceptor using its symbolized underscored name to the mail object that gets passed the mail object before delivery" do + test "you can register and unregister an interceptor using its symbolized underscored name to the mail object that gets passed the mail object before delivery" do mail_side_effects do ActionMailer::Base.register_interceptor(:"base_test/my_interceptor") mail = BaseMailer.welcome assert_called_with(MyInterceptor, :delivering_email, [mail]) do mail.deliver_now end + + ActionMailer::Base.unregister_interceptor(:"base_test/my_interceptor") + assert_not_called(MyInterceptor, :delivering_email, returns: mail) do + mail.deliver_now + end end end - test "you can register multiple interceptors to the mail object that both get passed the mail object before delivery" do + test "you can register and unregister multiple interceptors to the mail object that both get passed the mail object before delivery" do mail_side_effects do ActionMailer::Base.register_interceptors("BaseTest::MyInterceptor", MySecondInterceptor) mail = BaseMailer.welcome @@ -710,20 +753,37 @@ class BaseTest < ActiveSupport::TestCase mail.deliver_now end end + + ActionMailer::Base.unregister_interceptors("BaseTest::MyInterceptor", MySecondInterceptor) + assert_not_called(MyInterceptor, :delivering_email, returns: mail) do + mail.deliver_now + end + assert_not_called(MySecondInterceptor, :delivering_email, returns: mail) do + mail.deliver_now + end end end test "being able to put proc's into the defaults hash and they get evaluated on mail sending" do - mail1 = ProcMailer.welcome['X-Proc-Method'] + mail1 = ProcMailer.welcome["X-Proc-Method"] yesterday = 1.day.ago Time.stub(:now, yesterday) do - mail2 = ProcMailer.welcome['X-Proc-Method'] + mail2 = ProcMailer.welcome["X-Proc-Method"] assert(mail1.to_s.to_i > mail2.to_s.to_i) end end - test 'default values which have to_proc (e.g. symbols) should not be considered procs' do - assert(ProcMailer.welcome['x-has-to-proc'].to_s == 'symbol') + test "default values which have to_proc (e.g. symbols) should not be considered procs" do + assert(ProcMailer.welcome["x-has-to-proc"].to_s == "symbol") + end + + test "proc default values can have arity of 1 where arg is a mailer instance" do + assert_equal(ProcMailer.welcome["X-Lambda-Arity-1-arg"].to_s, "complex_value") + assert_equal(ProcMailer.welcome["X-Lambda-Arity-1-self"].to_s, "complex_value") + end + + test "proc default values with fixed arity of 0 can be called" do + assert_equal("0", ProcMailer.welcome["X-Lambda-Arity-0"].to_s) end test "we can call other defined methods on the class as needed" do @@ -738,12 +798,12 @@ class BaseTest < ActiveSupport::TestCase def welcome ; mail ; end private - def add_special_header! - headers('X-Special-Header' => 'Wow, so special') - end + def add_special_header! + headers("X-Special-Header" => "Wow, so special") + end end - assert_equal('Wow, so special', BeforeActionMailer.welcome['X-Special-Header'].to_s) + assert_equal("Wow, so special", BeforeActionMailer.welcome["X-Special-Header"].to_s) end test "modifying the mail message with an after_action" do @@ -753,12 +813,12 @@ class BaseTest < ActiveSupport::TestCase def welcome ; mail ; end private - def add_special_header! - headers('X-Special-Header' => 'Testing') - end + def add_special_header! + headers("X-Special-Header" => "Testing") + end end - assert_equal('Testing', AfterActionMailer.welcome['X-Special-Header'].to_s) + assert_equal("Testing", AfterActionMailer.welcome["X-Special-Header"].to_s) end test "adding an inline attachment using a before_action" do @@ -768,19 +828,19 @@ class BaseTest < ActiveSupport::TestCase def welcome ; mail ; end private - def add_inline_attachment! - attachments.inline["footer.jpg"] = 'hey there' - end + def add_inline_attachment! + attachments.inline["footer.jpg"] = "hey there" + end end mail = DefaultInlineAttachmentMailer.welcome - assert_equal('image/jpeg; filename=footer.jpg', mail.attachments.inline.first['Content-Type'].to_s) + assert_equal("image/jpeg; filename=footer.jpg", mail.attachments.inline.first["Content-Type"].to_s) end test "action methods should be refreshed after defining new method" do class FooMailer < ActionMailer::Base - # this triggers action_methods - self.respond_to?(:foo) + # This triggers action_methods. + respond_to?(:foo) def notify end @@ -804,8 +864,8 @@ class BaseTest < ActiveSupport::TestCase test "default_from can be set" do class DefaultFromMailer < ActionMailer::Base - default to: 'system@test.lindsaar.net' - self.default_options = {from: "robert.pankowecki@gmail.com"} + default to: "system@test.lindsaar.net" + self.default_options = { from: "robert.pankowecki@gmail.com" } def welcome mail(subject: "subject", body: "hello world") @@ -820,7 +880,7 @@ class BaseTest < ActiveSupport::TestCase after_action :a_callback def welcome - headers('X-Special-Header' => 'special indeed!') + headers("X-Special-Header" => "special indeed!") mail subject: "subject", body: "hello world", to: ["joe@example.com"] end @@ -836,7 +896,26 @@ class BaseTest < ActiveSupport::TestCase assert_equal "special indeed!", mail["X-Special-Header"].to_s end - protected + test "notification for process" do + begin + events = [] + ActiveSupport::Notifications.subscribe("process.action_mailer") do |*args| + events << ActiveSupport::Notifications::Event.new(*args) + end + + BaseMailer.welcome(body: "Hello there").deliver_now + + assert_equal 1, events.length + assert_equal "process.action_mailer", events[0].name + assert_equal "BaseMailer", events[0].payload[:mailer] + assert_equal :welcome, events[0].payload[:action] + assert_equal [{ body: "Hello there" }], events[0].payload[:args] + ensure + ActiveSupport::Notifications.unsubscribe "process.action_mailer" + end + end + + private # Execute the block setting the given values and restoring old values after # the block is executed. @@ -861,8 +940,6 @@ class BaseTest < ActiveSupport::TestCase klass.default_params = old end - # A simple hack to restore the observers and interceptors for Mail, as it - # does not have an unregister API yet. def mail_side_effects old_observers = Mail.class_variable_get(:@@delivery_notification_observers) old_delivery_interceptors = Mail.class_variable_get(:@@delivery_interceptors) @@ -901,7 +978,7 @@ class BasePreviewInterceptorsTest < ActiveSupport::TestCase def self.previewing_email(mail); end end - test "you can register a preview interceptor to the mail object that gets passed the mail object before previewing" do + test "you can register and unregister a preview interceptor to the mail object that gets passed the mail object before previewing" do ActionMailer::Base.register_preview_interceptor(MyInterceptor) mail = BaseMailer.welcome stub_any_instance(BaseMailerPreview) do |instance| @@ -911,9 +988,14 @@ class BasePreviewInterceptorsTest < ActiveSupport::TestCase end end end + + ActionMailer::Base.unregister_preview_interceptor(MyInterceptor) + assert_not_called(MyInterceptor, :previewing_email, returns: mail) do + BaseMailerPreview.call(:welcome) + end end - test "you can register a preview interceptor using its stringified name to the mail object that gets passed the mail object before previewing" do + test "you can register and unregister a preview interceptor using its stringified name to the mail object that gets passed the mail object before previewing" do ActionMailer::Base.register_preview_interceptor("BasePreviewInterceptorsTest::MyInterceptor") mail = BaseMailer.welcome stub_any_instance(BaseMailerPreview) do |instance| @@ -923,9 +1005,14 @@ class BasePreviewInterceptorsTest < ActiveSupport::TestCase end end end + + ActionMailer::Base.unregister_preview_interceptor("BasePreviewInterceptorsTest::MyInterceptor") + assert_not_called(MyInterceptor, :previewing_email, returns: mail) do + BaseMailerPreview.call(:welcome) + end end - test "you can register an interceptor using its symbolized underscored name to the mail object that gets passed the mail object before previewing" do + test "you can register and unregister a preview interceptor using its symbolized underscored name to the mail object that gets passed the mail object before previewing" do ActionMailer::Base.register_preview_interceptor(:"base_preview_interceptors_test/my_interceptor") mail = BaseMailer.welcome stub_any_instance(BaseMailerPreview) do |instance| @@ -935,9 +1022,14 @@ class BasePreviewInterceptorsTest < ActiveSupport::TestCase end end end + + ActionMailer::Base.unregister_preview_interceptor(:"base_preview_interceptors_test/my_interceptor") + assert_not_called(MyInterceptor, :previewing_email, returns: mail) do + BaseMailerPreview.call(:welcome) + end end - test "you can register multiple preview interceptors to the mail object that both get passed the mail object before previewing" do + test "you can register and unregister multiple preview interceptors to the mail object that both get passed the mail object before previewing" do ActionMailer::Base.register_preview_interceptors("BasePreviewInterceptorsTest::MyInterceptor", MySecondInterceptor) mail = BaseMailer.welcome stub_any_instance(BaseMailerPreview) do |instance| @@ -949,5 +1041,28 @@ class BasePreviewInterceptorsTest < ActiveSupport::TestCase end end end + + ActionMailer::Base.unregister_preview_interceptors("BasePreviewInterceptorsTest::MyInterceptor", MySecondInterceptor) + assert_not_called(MyInterceptor, :previewing_email, returns: mail) do + BaseMailerPreview.call(:welcome) + end + assert_not_called(MySecondInterceptor, :previewing_email, returns: mail) do + BaseMailerPreview.call(:welcome) + end + end +end + +class BasePreviewTest < ActiveSupport::TestCase + class BaseMailerPreview < ActionMailer::Preview + def welcome + BaseMailer.welcome(params) + end + end + + test "has access to params" do + params = { name: "World" } + + message = BaseMailerPreview.call(:welcome, params) + assert_equal "World", message["name"].decoded end end diff --git a/actionmailer/test/caching_test.rb b/actionmailer/test/caching_test.rb new file mode 100644 index 0000000000..22f310f39f --- /dev/null +++ b/actionmailer/test/caching_test.rb @@ -0,0 +1,271 @@ +# frozen_string_literal: true + +require "fileutils" +require "abstract_unit" +require "mailers/base_mailer" +require "mailers/caching_mailer" + +CACHE_DIR = "test_cache" +# Don't change '/../temp/' cavalierly or you might hose something you don't want hosed +FILE_STORE_PATH = File.join(__dir__, "/../temp/", CACHE_DIR) + +class FragmentCachingMailer < ActionMailer::Base + abstract! + + def some_action; end +end + +class BaseCachingTest < ActiveSupport::TestCase + def setup + super + @store = ActiveSupport::Cache::MemoryStore.new + @mailer = FragmentCachingMailer.new + @mailer.perform_caching = true + @mailer.cache_store = @store + end +end + +class FragmentCachingTest < BaseCachingTest + def test_read_fragment_with_caching_enabled + @store.write("views/name", "value") + assert_equal "value", @mailer.read_fragment("name") + end + + def test_read_fragment_with_caching_disabled + @mailer.perform_caching = false + @store.write("views/name", "value") + assert_nil @mailer.read_fragment("name") + end + + def test_fragment_exist_with_caching_enabled + @store.write("views/name", "value") + assert @mailer.fragment_exist?("name") + assert_not @mailer.fragment_exist?("other_name") + end + + def test_fragment_exist_with_caching_disabled + @mailer.perform_caching = false + @store.write("views/name", "value") + assert_not @mailer.fragment_exist?("name") + assert_not @mailer.fragment_exist?("other_name") + end + + def test_write_fragment_with_caching_enabled + assert_nil @store.read("views/name") + assert_equal "value", @mailer.write_fragment("name", "value") + assert_equal "value", @store.read("views/name") + end + + def test_write_fragment_with_caching_disabled + assert_nil @store.read("views/name") + @mailer.perform_caching = false + assert_equal "value", @mailer.write_fragment("name", "value") + assert_nil @store.read("views/name") + end + + def test_expire_fragment_with_simple_key + @store.write("views/name", "value") + @mailer.expire_fragment "name" + assert_nil @store.read("views/name") + end + + def test_expire_fragment_with_regexp + @store.write("views/name", "value") + @store.write("views/another_name", "another_value") + @store.write("views/primalgrasp", "will not expire ;-)") + + @mailer.expire_fragment(/name/) + + assert_nil @store.read("views/name") + assert_nil @store.read("views/another_name") + assert_equal "will not expire ;-)", @store.read("views/primalgrasp") + end + + def test_fragment_for + @store.write("views/expensive", "fragment content") + fragment_computed = false + + view_context = @mailer.view_context + + buffer = "generated till now -> ".html_safe + buffer << view_context.send(:fragment_for, "expensive") { fragment_computed = true } + + assert_not fragment_computed + assert_equal "generated till now -> fragment content", buffer + end + + def test_html_safety + assert_nil @store.read("views/name") + content = "value".html_safe + assert_equal content, @mailer.write_fragment("name", content) + + cached = @store.read("views/name") + assert_equal content, cached + assert_equal String, cached.class + + html_safe = @mailer.read_fragment("name") + assert_equal content, html_safe + assert_predicate html_safe, :html_safe? + end +end + +class FunctionalFragmentCachingTest < BaseCachingTest + def setup + super + @store = ActiveSupport::Cache::MemoryStore.new + @mailer = CachingMailer.new + @mailer.perform_caching = true + @mailer.cache_store = @store + end + + def test_fragment_caching + email = @mailer.fragment_cache + expected_body = "\"Welcome\"" + + assert_match expected_body, email.body.encoded + assert_match expected_body, + @store.read("views/caching_mailer/fragment_cache:#{template_digest("caching_mailer/fragment_cache")}/caching") + end + + def test_fragment_caching_in_partials + email = @mailer.fragment_cache_in_partials + expected_body = "Old fragment caching in a partial" + assert_match(expected_body, email.body.encoded) + + assert_match(expected_body, + @store.read("views/caching_mailer/_partial:#{template_digest("caching_mailer/_partial")}/caching")) + end + + def test_skip_fragment_cache_digesting + email = @mailer.skip_fragment_cache_digesting + expected_body = "No Digest" + + assert_match expected_body, email.body.encoded + assert_match expected_body, @store.read("views/no_digest") + end + + def test_fragment_caching_options + time = Time.now + email = @mailer.fragment_caching_options + expected_body = "No Digest" + + assert_match expected_body, email.body.encoded + Time.stub(:now, time + 11) do + assert_nil @store.read("views/no_digest") + end + end + + def test_multipart_fragment_caching + email = @mailer.multipart_cache + + expected_text_body = "\"Welcome text\"" + expected_html_body = "\"Welcome html\"" + encoded_body = email.body.encoded + assert_match expected_text_body, encoded_body + assert_match expected_html_body, encoded_body + assert_match expected_text_body, + @store.read("views/text_caching") + assert_match expected_html_body, + @store.read("views/html_caching") + end + + def test_fragment_cache_instrumentation + @mailer.enable_fragment_cache_logging = true + payload = nil + + subscriber = proc do |*args| + event = ActiveSupport::Notifications::Event.new(*args) + payload = event.payload + end + + ActiveSupport::Notifications.subscribed(subscriber, "read_fragment.action_mailer") do + @mailer.fragment_cache + end + + assert_equal "caching_mailer", payload[:mailer] + assert_equal [ :views, "caching_mailer/fragment_cache:#{template_digest("caching_mailer/fragment_cache")}", :caching ], payload[:key] + ensure + @mailer.enable_fragment_cache_logging = true + end + + private + + def template_digest(name) + ActionView::Digestor.digest(name: name, finder: @mailer.lookup_context) + end +end + +class CacheHelperOutputBufferTest < BaseCachingTest + class MockController + def read_fragment(name, options) + false + end + + def write_fragment(name, fragment, options) + fragment + end + end + + def setup + super + end + + def test_output_buffer + output_buffer = ActionView::OutputBuffer.new + controller = MockController.new + cache_helper = Class.new do + def self.controller; end + def self.output_buffer; end + def self.output_buffer=; end + end + cache_helper.extend(ActionView::Helpers::CacheHelper) + + cache_helper.stub :controller, controller do + cache_helper.stub :output_buffer, output_buffer do + assert_called_with cache_helper, :output_buffer=, [output_buffer.class.new(output_buffer)] do + assert_nothing_raised do + cache_helper.send :fragment_for, "Test fragment name", "Test fragment", &Proc.new { nil } + end + end + end + end + end + + def test_safe_buffer + output_buffer = ActiveSupport::SafeBuffer.new + controller = MockController.new + cache_helper = Class.new do + def self.controller; end + def self.output_buffer; end + def self.output_buffer=; end + end + cache_helper.extend(ActionView::Helpers::CacheHelper) + + cache_helper.stub :controller, controller do + cache_helper.stub :output_buffer, output_buffer do + assert_called_with cache_helper, :output_buffer=, [output_buffer.class.new(output_buffer)] do + assert_nothing_raised do + cache_helper.send :fragment_for, "Test fragment name", "Test fragment", &Proc.new { nil } + end + end + end + end + end +end + +class ViewCacheDependencyTest < BaseCachingTest + class NoDependenciesMailer < ActionMailer::Base + end + class HasDependenciesMailer < ActionMailer::Base + view_cache_dependency { "trombone" } + view_cache_dependency { "flute" } + end + + def test_view_cache_dependencies_are_empty_by_default + assert_empty NoDependenciesMailer.new.view_cache_dependencies + end + + def test_view_cache_dependencies_are_listed_in_declaration_order + assert_equal %w(trombone flute), HasDependenciesMailer.new.view_cache_dependencies + end +end diff --git a/actionmailer/test/delivery_methods_test.rb b/actionmailer/test/delivery_methods_test.rb index 2786fe0d07..025f7152bb 100644 --- a/actionmailer/test/delivery_methods_test.rb +++ b/actionmailer/test/delivery_methods_test.rb @@ -1,4 +1,6 @@ -require 'abstract_unit' +# frozen_string_literal: true + +require "abstract_unit" class MyCustomDelivery end @@ -23,7 +25,7 @@ class DefaultsDeliveryMethodsTest < ActiveSupport::TestCase test "default smtp settings" do settings = { address: "localhost", port: 25, - domain: 'localhost.localdomain', + domain: "localhost.localdomain", user_name: nil, password: nil, authentication: nil, @@ -32,14 +34,14 @@ class DefaultsDeliveryMethodsTest < ActiveSupport::TestCase end test "default file delivery settings (with Rails.root)" do - settings = {location: "#{Rails.root}/tmp/mails"} + settings = { location: "#{Rails.root}/tmp/mails" } assert_equal settings, ActionMailer::Base.file_settings end test "default sendmail settings" do settings = { - location: '/usr/sbin/sendmail', - arguments: '-i -t' + location: "/usr/sbin/sendmail", + arguments: "-i" } assert_equal settings, ActionMailer::Base.sendmail_settings end @@ -83,11 +85,11 @@ end class MailDeliveryTest < ActiveSupport::TestCase class DeliveryMailer < ActionMailer::Base DEFAULT_HEADERS = { - to: 'mikel@test.lindsaar.net', - from: 'jose@test.plataformatec.com' + to: "mikel@test.lindsaar.net", + from: "jose@test.plataformatec.com" } - def welcome(hash={}) + def welcome(hash = {}) mail(DEFAULT_HEADERS.merge(hash)) end end @@ -127,16 +129,16 @@ class MailDeliveryTest < ActiveSupport::TestCase end test "delivery method options default to class level options" do - default_options = {a: "b"} + default_options = { a: "b" } ActionMailer::Base.add_delivery_method :optioned, MyOptionedDelivery, default_options mail_instance = DeliveryMailer.welcome(delivery_method: :optioned) assert_equal default_options, mail_instance.delivery_method.options end test "delivery method options can be overridden per mail instance" do - default_options = {a: "b"} + default_options = { a: "b" } ActionMailer::Base.add_delivery_method :optioned, MyOptionedDelivery, default_options - overridden_options = {a: "a"} + overridden_options = { a: "a" } mail_instance = DeliveryMailer.welcome(delivery_method: :optioned, delivery_method_options: overridden_options) assert_equal overridden_options, mail_instance.delivery_method.options end @@ -145,14 +147,14 @@ class MailDeliveryTest < ActiveSupport::TestCase settings = { address: "localhost", port: 25, - domain: 'localhost.localdomain', + domain: "localhost.localdomain", user_name: nil, password: nil, authentication: nil, enable_starttls_auto: true } assert_equal settings, ActionMailer::Base.smtp_settings - overridden_options = {user_name: "overridden", password: "somethingobtuse"} + overridden_options = { user_name: "overridden", password: "somethingobtuse" } mail_instance = DeliveryMailer.welcome(delivery_method_options: overridden_options) delivery_method_instance = mail_instance.delivery_method assert_equal "overridden", delivery_method_instance.settings[:user_name] diff --git a/actionmailer/test/fixtures/async_mailer/welcome.erb b/actionmailer/test/fixtures/async_mailer/welcome.erb deleted file mode 100644 index 01f3f00c63..0000000000 --- a/actionmailer/test/fixtures/async_mailer/welcome.erb +++ /dev/null @@ -1 +0,0 @@ -Welcome
\ No newline at end of file diff --git a/actionmailer/test/fixtures/caching_mailer/_partial.html.erb b/actionmailer/test/fixtures/caching_mailer/_partial.html.erb new file mode 100644 index 0000000000..8e965f52b4 --- /dev/null +++ b/actionmailer/test/fixtures/caching_mailer/_partial.html.erb @@ -0,0 +1,3 @@ +<% cache :caching do %> + Old fragment caching in a partial +<% end %> diff --git a/actionmailer/test/fixtures/caching_mailer/fragment_cache.html.erb b/actionmailer/test/fixtures/caching_mailer/fragment_cache.html.erb new file mode 100644 index 0000000000..90189627da --- /dev/null +++ b/actionmailer/test/fixtures/caching_mailer/fragment_cache.html.erb @@ -0,0 +1,3 @@ +<% cache :caching do %> +"Welcome" +<% end %> diff --git a/actionmailer/test/fixtures/caching_mailer/fragment_cache_in_partials.html.erb b/actionmailer/test/fixtures/caching_mailer/fragment_cache_in_partials.html.erb new file mode 100644 index 0000000000..2957d083e8 --- /dev/null +++ b/actionmailer/test/fixtures/caching_mailer/fragment_cache_in_partials.html.erb @@ -0,0 +1 @@ +<%= render "partial" %> diff --git a/actionmailer/test/fixtures/caching_mailer/fragment_caching_options.html.erb b/actionmailer/test/fixtures/caching_mailer/fragment_caching_options.html.erb new file mode 100644 index 0000000000..0541ac321b --- /dev/null +++ b/actionmailer/test/fixtures/caching_mailer/fragment_caching_options.html.erb @@ -0,0 +1,3 @@ +<%= cache :no_digest, skip_digest: true, expires_in: 0 do %> + No Digest +<% end %> diff --git a/actionmailer/test/fixtures/caching_mailer/multipart_cache.html.erb b/actionmailer/test/fixtures/caching_mailer/multipart_cache.html.erb new file mode 100644 index 0000000000..0d26baa2d7 --- /dev/null +++ b/actionmailer/test/fixtures/caching_mailer/multipart_cache.html.erb @@ -0,0 +1,3 @@ +<% cache :html_caching, skip_digest: true do %> + "Welcome html" +<% end %> diff --git a/actionmailer/test/fixtures/caching_mailer/multipart_cache.text.erb b/actionmailer/test/fixtures/caching_mailer/multipart_cache.text.erb new file mode 100644 index 0000000000..ef97326e03 --- /dev/null +++ b/actionmailer/test/fixtures/caching_mailer/multipart_cache.text.erb @@ -0,0 +1,3 @@ +<% cache :text_caching, skip_digest: true do %> + "Welcome text" +<% end %> diff --git a/actionmailer/test/fixtures/caching_mailer/skip_fragment_cache_digesting.html.erb b/actionmailer/test/fixtures/caching_mailer/skip_fragment_cache_digesting.html.erb new file mode 100644 index 0000000000..0d52429a81 --- /dev/null +++ b/actionmailer/test/fixtures/caching_mailer/skip_fragment_cache_digesting.html.erb @@ -0,0 +1,3 @@ +<%= cache :no_digest, skip_digest: true do %> + No Digest +<% end %> diff --git a/actionmailer/test/i18n_with_controller_test.rb b/actionmailer/test/i18n_with_controller_test.rb index 6124ffeb52..6e75cff347 100644 --- a/actionmailer/test/i18n_with_controller_test.rb +++ b/actionmailer/test/i18n_with_controller_test.rb @@ -1,10 +1,12 @@ -require 'abstract_unit' -require 'action_view' -require 'action_controller' +# frozen_string_literal: true + +require "abstract_unit" +require "action_view" +require "action_controller" class I18nTestMailer < ActionMailer::Base configure do |c| - c.assets_dir = '' + c.assets_dir = "" end def mail_with_i18n_subject(recipient) @@ -18,14 +20,16 @@ end class TestController < ActionController::Base def send_mail email = I18nTestMailer.mail_with_i18n_subject("test@localhost").deliver_now - render text: "Mail sent - Subject: #{email.subject}" + render plain: "Mail sent - Subject: #{email.subject}" end end class ActionMailerI18nWithControllerTest < ActionDispatch::IntegrationTest Routes = ActionDispatch::Routing::RouteSet.new Routes.draw do - get ':controller(/:action(/:id))' + ActiveSupport::Deprecation.silence do + get ":controller(/:action(/:id))" + end end class RoutedRackApp @@ -54,22 +58,20 @@ class ActionMailerI18nWithControllerTest < ActionDispatch::IntegrationTest def test_send_mail stub_any_instance(Mail::SMTP, instance: Mail::SMTP.new({})) do |instance| assert_called(instance, :deliver!) do - with_translation 'de', email_subject: '[Anmeldung] Willkommen' do - ActiveSupport::Deprecation.silence do - get '/test/send_mail' - end + with_translation "de", email_subject: "[Anmeldung] Willkommen" do + get "/test/send_mail" assert_equal "Mail sent - Subject: [Anmeldung] Willkommen", @response.body end end end end - protected + private - def with_translation(locale, data) - I18n.backend.store_translations(locale, data) - yield - ensure - I18n.backend.reload! - end + def with_translation(locale, data) + I18n.backend.store_translations(locale, data) + yield + ensure + I18n.backend.reload! + end end diff --git a/actionmailer/test/log_subscriber_test.rb b/actionmailer/test/log_subscriber_test.rb index 3871b16840..7686fd10c9 100644 --- a/actionmailer/test/log_subscriber_test.rb +++ b/actionmailer/test/log_subscriber_test.rb @@ -1,7 +1,9 @@ -require 'abstract_unit' -require 'mailers/base_mailer' -require 'active_support/log_subscriber/test_helper' -require 'action_mailer/log_subscriber' +# frozen_string_literal: true + +require "abstract_unit" +require "mailers/base_mailer" +require "active_support/log_subscriber/test_helper" +require "action_mailer/log_subscriber" class AMLogSubscriberTest < ActionMailer::TestCase include ActiveSupport::LogSubscriber::TestHelper @@ -26,7 +28,7 @@ class AMLogSubscriberTest < ActionMailer::TestCase wait assert_equal(1, @logger.logged(:info).size) - assert_match(/Sent mail to system@test.lindsaar.net/, @logger.logged(:info).first) + assert_match(/Sent mail to system@test\.lindsaar\.net/, @logger.logged(:info).first) assert_equal(2, @logger.logged(:debug).size) assert_match(/BaseMailer#welcome: processed outbound mail in [\d.]+ms/, @logger.logged(:debug).first) @@ -35,8 +37,22 @@ class AMLogSubscriberTest < ActionMailer::TestCase BaseMailer.deliveries.clear end + def test_deliver_message_when_perform_deliveries_is_false + BaseMailer.welcome_without_deliveries.deliver_now + wait + + assert_equal(1, @logger.logged(:info).size) + assert_match("Skipped sending mail to system@test.lindsaar.net as `perform_deliveries` is false", @logger.logged(:info).first) + + assert_equal(2, @logger.logged(:debug).size) + assert_match(/BaseMailer#welcome_without_deliveries: processed outbound mail in [\d.]+ms/, @logger.logged(:debug).first) + assert_match("Welcome", @logger.logged(:debug).second) + ensure + BaseMailer.deliveries.clear + end + def test_receive_is_notified - fixture = File.read(File.dirname(__FILE__) + "/fixtures/raw_email") + fixture = File.read(File.expand_path("fixtures/raw_email", __dir__)) TestMailer.receive(fixture) wait assert_equal(1, @logger.logged(:info).size) diff --git a/actionmailer/test/mail_helper_test.rb b/actionmailer/test/mail_helper_test.rb index ff6b25b0c7..51d6ccb10f 100644 --- a/actionmailer/test/mail_helper_test.rb +++ b/actionmailer/test/mail_helper_test.rb @@ -1,12 +1,14 @@ -require 'abstract_unit' +# frozen_string_literal: true + +require "abstract_unit" class HelperMailer < ActionMailer::Base def use_mail_helper - @text = "But soft! What light through yonder window breaks? It is the east, " + - "and Juliet is the sun. Arise, fair sun, and kill the envious moon, " + - "which is sick and pale with grief that thou, her maid, art far more " + - "fair than she. Be not her maid, for she is envious! Her vestal " + - "livery is but sick and green, and none but fools do wear it. Cast " + + @text = "But soft! What light through yonder window breaks? It is the east, " \ + "and Juliet is the sun. Arise, fair sun, and kill the envious moon, " \ + "which is sick and pale with grief that thou, her maid, art far more " \ + "fair than she. Be not her maid, for she is envious! Her vestal " \ + "livery is but sick and green, and none but fools do wear it. Cast " \ "it off!" mail_with_defaults do |format| @@ -65,12 +67,12 @@ The second end end - protected + private - def mail_with_defaults(&block) - mail(to: "test@localhost", from: "tester@example.com", - subject: "using helpers", &block) - end + def mail_with_defaults(&block) + mail(to: "test@localhost", from: "tester@example.com", + subject: "using helpers", &block) + end end class MailerHelperTest < ActionMailer::TestCase diff --git a/actionmailer/test/mail_layout_test.rb b/actionmailer/test/mail_layout_test.rb index 166dd096d4..16d77ed61d 100644 --- a/actionmailer/test/mail_layout_test.rb +++ b/actionmailer/test/mail_layout_test.rb @@ -1,7 +1,9 @@ -require 'abstract_unit' +# frozen_string_literal: true + +require "abstract_unit" class AutoLayoutMailer < ActionMailer::Base - default to: 'test@localhost', + default to: "test@localhost", subject: "You have a mail", from: "tester@example.com" @@ -11,7 +13,7 @@ class AutoLayoutMailer < ActionMailer::Base def spam @world = "Earth" - mail(body: render(inline: "Hello, <%= @world %>", layout: 'spam')) + mail(body: render(inline: "Hello, <%= @world %>", layout: "spam")) end def nolayout @@ -28,9 +30,9 @@ class AutoLayoutMailer < ActionMailer::Base end class ExplicitLayoutMailer < ActionMailer::Base - layout 'spam', except: [:logout] + layout "spam", except: [:logout] - default to: 'test@localhost', + default to: "test@localhost", subject: "You have a mail", from: "tester@example.com" @@ -54,10 +56,10 @@ class LayoutMailerTest < ActiveSupport::TestCase assert_equal "multipart/alternative", mail.mime_type assert_equal 2, mail.parts.size - assert_equal 'text/plain', mail.parts.first.mime_type + assert_equal "text/plain", mail.parts.first.mime_type assert_equal "text/plain layout - text/plain multipart", mail.parts.first.body.to_s - assert_equal 'text/html', mail.parts.last.mime_type + assert_equal "text/html", mail.parts.last.mime_type assert_equal "Hello from layout text/html multipart", mail.parts.last.body.to_s end @@ -66,10 +68,10 @@ class LayoutMailerTest < ActiveSupport::TestCase assert_equal "multipart/mixed", mail.mime_type assert_equal 2, mail.parts.size - assert_equal 'text/plain', mail.parts.first.mime_type + assert_equal "text/plain", mail.parts.first.mime_type assert_equal "text/plain layout - text/plain multipart", mail.parts.first.body.to_s - assert_equal 'text/html', mail.parts.last.mime_type + assert_equal "text/html", mail.parts.last.mime_type assert_equal "Hello from layout text/html multipart", mail.parts.last.body.to_s end diff --git a/actionmailer/test/mailers/asset_mailer.rb b/actionmailer/test/mailers/asset_mailer.rb index f54a50d00d..7a9aba2629 100644 --- a/actionmailer/test/mailers/asset_mailer.rb +++ b/actionmailer/test/mailers/asset_mailer.rb @@ -1,7 +1,9 @@ +# frozen_string_literal: true + class AssetMailer < ActionMailer::Base self.mailer_name = "asset_mailer" def welcome - mail + mail end end diff --git a/actionmailer/test/mailers/base_mailer.rb b/actionmailer/test/mailers/base_mailer.rb index f75bd48a9e..c1bb48cc96 100644 --- a/actionmailer/test/mailers/base_mailer.rb +++ b/actionmailer/test/mailers/base_mailer.rb @@ -1,13 +1,15 @@ +# frozen_string_literal: true + class BaseMailer < ActionMailer::Base self.mailer_name = "base_mailer" - default to: 'system@test.lindsaar.net', - from: 'jose@test.plataformatec.com', - reply_to: 'mikel@test.lindsaar.net' + default to: "system@test.lindsaar.net", + from: "jose@test.plataformatec.com", + reply_to: "mikel@test.lindsaar.net" def welcome(hash = {}) - headers['X-SPAM'] = "Not SPAM" - mail({subject: "The first email on new API!"}.merge!(hash)) + headers["X-SPAM"] = "Not SPAM" + mail({ subject: "The first email on new API!" }.merge!(hash)) end def welcome_with_headers(hash = {}) @@ -19,6 +21,11 @@ class BaseMailer < ActionMailer::Base mail(template_name: "welcome", template_path: path) end + def welcome_without_deliveries + mail(template_name: "welcome") + mail.perform_deliveries = false + end + def html_only(hash = {}) mail(hash) end @@ -28,30 +35,30 @@ class BaseMailer < ActionMailer::Base end def inline_attachment - attachments.inline['logo.png'] = "\312\213\254\232" + attachments.inline["logo.png"] = "\312\213\254\232" mail end def attachment_with_content(hash = {}) - attachments['invoice.pdf'] = 'This is test File content' + attachments["invoice.pdf"] = "This is test File content" mail(hash) end def attachment_with_hash - attachments['invoice.jpg'] = { data: ::Base64.encode64("\312\213\254\232)b"), + attachments["invoice.jpg"] = { data: ::Base64.encode64("\312\213\254\232)b"), mime_type: "image/x-jpg", transfer_encoding: "base64" } mail end def attachment_with_hash_default_encoding - attachments['invoice.jpg'] = { data: "\312\213\254\232)b", + attachments["invoice.jpg"] = { data: "\312\213\254\232)b", mime_type: "image/x-jpg" } mail end def implicit_multipart(hash = {}) - attachments['invoice.pdf'] = 'This is test File content' if hash.delete(:attachments) + attachments["invoice.pdf"] = "This is test File content" if hash.delete(:attachments) mail(hash) end @@ -60,10 +67,10 @@ class BaseMailer < ActionMailer::Base end def explicit_multipart(hash = {}) - attachments['invoice.pdf'] = 'This is test File content' if hash.delete(:attachments) + attachments["invoice.pdf"] = "This is test File content" if hash.delete(:attachments) mail(hash) do |format| - format.text { render text: "TEXT Explicit Multipart" } - format.html { render text: "HTML Explicit Multipart" } + format.text { render plain: "TEXT Explicit Multipart" } + format.html { render plain: "HTML Explicit Multipart" } end end @@ -76,7 +83,7 @@ class BaseMailer < ActionMailer::Base def explicit_multipart_with_any(hash = {}) mail(hash) do |format| - format.any(:text, :html){ render text: "Format with any!" } + format.any(:text, :html) { render plain: "Format with any!" } end end @@ -88,8 +95,8 @@ class BaseMailer < ActionMailer::Base def explicit_multipart_with_options(include_html = false) mail do |format| - format.text(content_transfer_encoding: "base64"){ render "welcome" } - format.html{ render "welcome" } if include_html + format.text(content_transfer_encoding: "base64") { render "welcome" } + format.html { render "welcome" } if include_html end end @@ -100,25 +107,25 @@ class BaseMailer < ActionMailer::Base end end - def implicit_different_template(template_name='') + def implicit_different_template(template_name = "") mail(template_name: template_name) end - def implicit_different_template_with_block(template_name='') + def implicit_different_template_with_block(template_name = "") mail(template_name: template_name) do |format| format.text format.html end end - def explicit_different_template(template_name='') + def explicit_different_template(template_name = "") mail do |format| format.text { render template: "#{mailer_name}/#{template_name}" } format.html { render template: "#{mailer_name}/#{template_name}" } end end - def different_layout(layout_name='') + def different_layout(layout_name = "") mail do |format| format.text { render layout: layout_name } format.html { render layout: layout_name } @@ -138,6 +145,6 @@ class BaseMailer < ActionMailer::Base end def with_subject_interpolations - mail(subject: default_i18n_subject(rapper_or_impersonator: 'Slim Shady'), body: '') + mail(subject: default_i18n_subject(rapper_or_impersonator: "Slim Shady"), body: "") end end diff --git a/actionmailer/test/mailers/caching_mailer.rb b/actionmailer/test/mailers/caching_mailer.rb new file mode 100644 index 0000000000..02f0c6c103 --- /dev/null +++ b/actionmailer/test/mailers/caching_mailer.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +class CachingMailer < ActionMailer::Base + self.mailer_name = "caching_mailer" + + def fragment_cache + mail(subject: "welcome", template_name: "fragment_cache") + end + + def fragment_cache_in_partials + mail(subject: "welcome", template_name: "fragment_cache_in_partials") + end + + def skip_fragment_cache_digesting + mail(subject: "welcome", template_name: "skip_fragment_cache_digesting") + end + + def fragment_caching_options + mail(subject: "welcome", template_name: "fragment_caching_options") + end + + def multipart_cache + mail(subject: "welcome", template_name: "multipart_cache") + end +end diff --git a/actionmailer/test/mailers/delayed_mailer.rb b/actionmailer/test/mailers/delayed_mailer.rb index 62d4baa434..b0f5ecc2fb 100644 --- a/actionmailer/test/mailers/delayed_mailer.rb +++ b/actionmailer/test/mailers/delayed_mailer.rb @@ -1,6 +1,28 @@ +# frozen_string_literal: true + +require "active_job/arguments" + +class DelayedMailerError < StandardError; end + class DelayedMailer < ActionMailer::Base + cattr_accessor :last_error + cattr_accessor :last_rescue_from_instance + + rescue_from DelayedMailerError do |error| + @@last_error = error + @@last_rescue_from_instance = self + end + + rescue_from ActiveJob::DeserializationError do |error| + @@last_error = error + @@last_rescue_from_instance = self + end def test_message(*) - mail(from: 'test-sender@test.com', to: 'test-receiver@test.com', subject: 'Test Subject', body: 'Test Body') + mail(from: "test-sender@test.com", to: "test-receiver@test.com", subject: "Test Subject", body: "Test Body") + end + + def test_raise(klass_name) + raise klass_name.constantize, "boom" end end diff --git a/actionmailer/test/mailers/params_mailer.rb b/actionmailer/test/mailers/params_mailer.rb new file mode 100644 index 0000000000..84aa336311 --- /dev/null +++ b/actionmailer/test/mailers/params_mailer.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class ParamsMailer < ActionMailer::Base + before_action { @inviter, @invitee = params[:inviter], params[:invitee] } + + default to: Proc.new { @invitee }, from: -> { @inviter } + + def invitation + mail(subject: "Welcome to the project!") do |format| + format.text { render plain: "So says #{@inviter}" } + end + end +end diff --git a/actionmailer/test/mailers/proc_mailer.rb b/actionmailer/test/mailers/proc_mailer.rb index 7e189d861f..76e730bb79 100644 --- a/actionmailer/test/mailers/proc_mailer.rb +++ b/actionmailer/test/mailers/proc_mailer.rb @@ -1,17 +1,25 @@ +# frozen_string_literal: true + class ProcMailer < ActionMailer::Base - default to: 'system@test.lindsaar.net', - 'X-Proc-Method' => Proc.new { Time.now.to_i.to_s }, + default to: "system@test.lindsaar.net", + "X-Proc-Method" => Proc.new { Time.now.to_i.to_s }, subject: Proc.new { give_a_greeting }, - 'x-has-to-proc' => :symbol + "x-has-to-proc" => :symbol, + "X-Lambda-Arity-0" => ->() { "0" }, + "X-Lambda-Arity-1-arg" => ->(arg) { arg.computed_value }, + "X-Lambda-Arity-1-self" => ->(_) { self.computed_value } def welcome mail end - private - - def give_a_greeting - "Thanks for signing up this afternoon" + def computed_value + "complex_value" end + private + + def give_a_greeting + "Thanks for signing up this afternoon" + end end diff --git a/actionmailer/test/message_delivery_test.rb b/actionmailer/test/message_delivery_test.rb index b834cdd08c..f8dcb3f4ba 100644 --- a/actionmailer/test/message_delivery_test.rb +++ b/actionmailer/test/message_delivery_test.rb @@ -1,6 +1,8 @@ -require 'abstract_unit' -require 'active_job' -require 'mailers/delayed_mailer' +# frozen_string_literal: true + +require "abstract_unit" +require "active_job" +require "mailers/delayed_mailer" class MessageDeliveryTest < ActiveSupport::TestCase include ActiveJob::TestHelper @@ -12,85 +14,154 @@ class MessageDeliveryTest < ActiveSupport::TestCase ActionMailer::Base.deliver_later_queue_name = :test_queue ActionMailer::Base.delivery_method = :test ActiveJob::Base.logger = Logger.new(nil) - @mail = DelayedMailer.test_message(1, 2, 3) - ActionMailer::Base.deliveries.clear ActiveJob::Base.queue_adapter.perform_enqueued_at_jobs = true ActiveJob::Base.queue_adapter.perform_enqueued_jobs = true + + DelayedMailer.last_error = nil + DelayedMailer.last_rescue_from_instance = nil + + @mail = DelayedMailer.test_message(1, 2, 3) end teardown do + ActionMailer::Base.deliveries.clear + ActiveJob::Base.logger = @previous_logger ActionMailer::Base.delivery_method = @previous_delivery_method ActionMailer::Base.deliver_later_queue_name = @previous_deliver_later_queue_name + + DelayedMailer.last_error = nil + DelayedMailer.last_rescue_from_instance = nil end - test 'should have a message' do + test "should have a message" do assert @mail.message end - test 'its message should be a Mail::Message' do - assert_equal Mail::Message , @mail.message.class + test "its message should be a Mail::Message" do + assert_equal Mail::Message, @mail.message.class end - test 'should respond to .deliver_later' do + test "should respond to .deliver_later" do assert_respond_to @mail, :deliver_later end - test 'should respond to .deliver_later!' do + test "should respond to .deliver_later!" do assert_respond_to @mail, :deliver_later! end - test 'should respond to .deliver_now' do + test "should respond to .deliver_now" do assert_respond_to @mail, :deliver_now end - test 'should respond to .deliver_now!' do + test "should respond to .deliver_now!" do assert_respond_to @mail, :deliver_now! end def test_should_enqueue_and_run_correctly_in_activejob @mail.deliver_later! assert_equal 1, ActionMailer::Base.deliveries.size - ensure - ActionMailer::Base.deliveries.clear end - test 'should enqueue the email with :deliver_now delivery method' do - assert_performed_with(job: ActionMailer::DeliveryJob, args: ['DelayedMailer', 'test_message', 'deliver_now', 1, 2, 3]) do + test "should enqueue the email with :deliver_now delivery method" do + assert_performed_with(job: ActionMailer::DeliveryJob, args: ["DelayedMailer", "test_message", "deliver_now", 1, 2, 3]) do @mail.deliver_later end end - test 'should enqueue the email with :deliver_now! delivery method' do - assert_performed_with(job: ActionMailer::DeliveryJob, args: ['DelayedMailer', 'test_message', 'deliver_now!', 1, 2, 3]) do + test "should enqueue the email with :deliver_now! delivery method" do + assert_performed_with(job: ActionMailer::DeliveryJob, args: ["DelayedMailer", "test_message", "deliver_now!", 1, 2, 3]) do @mail.deliver_later! end end - test 'should enqueue a delivery with a delay' do + test "should enqueue a delivery with a delay" do travel_to Time.new(2004, 11, 24, 01, 04, 44) do - assert_performed_with(job: ActionMailer::DeliveryJob, at: Time.current.to_f+600.seconds, args: ['DelayedMailer', 'test_message', 'deliver_now', 1, 2, 3]) do - @mail.deliver_later wait: 600.seconds + assert_performed_with(job: ActionMailer::DeliveryJob, at: Time.current + 10.minutes, args: ["DelayedMailer", "test_message", "deliver_now", 1, 2, 3]) do + @mail.deliver_later wait: 10.minutes end end end - test 'should enqueue a delivery at a specific time' do - later_time = Time.now.to_f + 3600 - assert_performed_with(job: ActionMailer::DeliveryJob, at: later_time, args: ['DelayedMailer', 'test_message', 'deliver_now', 1, 2, 3]) do + test "should enqueue a delivery at a specific time" do + later_time = Time.current + 1.hour + assert_performed_with(job: ActionMailer::DeliveryJob, at: later_time, args: ["DelayedMailer", "test_message", "deliver_now", 1, 2, 3]) do @mail.deliver_later wait_until: later_time end end - test 'should enqueue the job on the correct queue' do - assert_performed_with(job: ActionMailer::DeliveryJob, args: ['DelayedMailer', 'test_message', 'deliver_now', 1, 2, 3], queue: "test_queue") do + test "should enqueue the job on the correct queue" do + assert_performed_with(job: ActionMailer::DeliveryJob, args: ["DelayedMailer", "test_message", "deliver_now", 1, 2, 3], queue: "test_queue") do @mail.deliver_later end end - test 'can override the queue when enqueuing mail' do - assert_performed_with(job: ActionMailer::DeliveryJob, args: ['DelayedMailer', 'test_message', 'deliver_now', 1, 2, 3], queue: "another_queue") do + test "should enqueue the job with the correct delivery job" do + old_delivery_job = DelayedMailer.delivery_job + DelayedMailer.delivery_job = DummyJob + + assert_performed_with(job: DummyJob, args: ["DelayedMailer", "test_message", "deliver_now", 1, 2, 3]) do + @mail.deliver_later + end + + DelayedMailer.delivery_job = old_delivery_job + end + + class DummyJob < ActionMailer::DeliveryJob; end + + test "can override the queue when enqueuing mail" do + assert_performed_with(job: ActionMailer::DeliveryJob, args: ["DelayedMailer", "test_message", "deliver_now", 1, 2, 3], queue: "another_queue") do @mail.deliver_later(queue: :another_queue) end end + + test "deliver_later after accessing the message is disallowed" do + @mail.message # Load the message, which calls the mailer method. + + assert_raise RuntimeError do + @mail.deliver_later + end + end + + test "job delegates error handling to mailer" do + # Superclass not rescued by mailer's rescue_from RuntimeError + message = DelayedMailer.test_raise("StandardError") + assert_raise(StandardError) { message.deliver_later } + assert_nil DelayedMailer.last_error + assert_nil DelayedMailer.last_rescue_from_instance + + # Rescued by mailer's rescue_from RuntimeError + message = DelayedMailer.test_raise("DelayedMailerError") + assert_nothing_raised { message.deliver_later } + assert_equal "boom", DelayedMailer.last_error.message + assert_kind_of DelayedMailer, DelayedMailer.last_rescue_from_instance + end + + class DeserializationErrorFixture + include GlobalID::Identification + + def self.find(id) + raise "boom, missing find" + end + + attr_reader :id + def initialize(id = 1) + @id = id + end + + def to_global_id(options = {}) + super app: "foo" + end + end + + test "job delegates deserialization errors to mailer class" do + # Inject an argument that can't be deserialized. + message = DelayedMailer.test_message(DeserializationErrorFixture.new) + + # DeserializationError is raised, rescued, and delegated to the handler + # on the mailer class. + assert_nothing_raised { message.deliver_later } + assert_equal DelayedMailer, DelayedMailer.last_rescue_from_instance + assert_equal "Error while trying to deserialize arguments: boom, missing find", DelayedMailer.last_error.message + end end diff --git a/actionmailer/test/parameterized_test.rb b/actionmailer/test/parameterized_test.rb new file mode 100644 index 0000000000..d3b34fc3e3 --- /dev/null +++ b/actionmailer/test/parameterized_test.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +require "abstract_unit" +require "active_job" +require "mailers/params_mailer" + +class ParameterizedTest < ActiveSupport::TestCase + include ActiveJob::TestHelper + + setup do + @previous_logger = ActiveJob::Base.logger + ActiveJob::Base.logger = Logger.new(nil) + + @previous_delivery_method = ActionMailer::Base.delivery_method + ActionMailer::Base.delivery_method = :test + + @previous_deliver_later_queue_name = ActionMailer::Base.deliver_later_queue_name + ActionMailer::Base.deliver_later_queue_name = :test_queue + + @mail = ParamsMailer.with(inviter: "david@basecamp.com", invitee: "jason@basecamp.com").invitation + end + + teardown do + ActiveJob::Base.logger = @previous_logger + ParamsMailer.deliveries.clear + + ActionMailer::Base.delivery_method = @previous_delivery_method + ActionMailer::Base.deliver_later_queue_name = @previous_deliver_later_queue_name + end + + test "parameterized headers" do + assert_equal(["jason@basecamp.com"], @mail.to) + assert_equal(["david@basecamp.com"], @mail.from) + assert_equal("So says david@basecamp.com", @mail.body.encoded) + end + + test "enqueue the email with params" do + assert_performed_with(job: ActionMailer::Parameterized::DeliveryJob, args: ["ParamsMailer", "invitation", "deliver_now", { inviter: "david@basecamp.com", invitee: "jason@basecamp.com" } ]) do + @mail.deliver_later + end + end + + test "respond_to?" do + mailer = ParamsMailer.with(inviter: "david@basecamp.com", invitee: "jason@basecamp.com") + + assert_respond_to mailer, :invitation + assert_not_respond_to mailer, :anything + + invitation = mailer.method(:invitation) + assert_equal Method, invitation.class + + assert_raises(NameError) do + invitation = mailer.method(:anything) + end + end + + test "should enqueue a parameterized request with the correct delivery job" do + old_delivery_job = ParamsMailer.parameterized_delivery_job + ParamsMailer.parameterized_delivery_job = ParameterizedDummyJob + + assert_performed_with(job: ParameterizedDummyJob, args: ["ParamsMailer", "invitation", "deliver_now", { inviter: "david@basecamp.com", invitee: "jason@basecamp.com" } ]) do + @mail.deliver_later + end + + ParamsMailer.parameterized_delivery_job = old_delivery_job + end + + class ParameterizedDummyJob < ActionMailer::Parameterized::DeliveryJob; end +end diff --git a/actionmailer/test/test_case_test.rb b/actionmailer/test/test_case_test.rb index 86fd37bea6..7b9647d295 100644 --- a/actionmailer/test/test_case_test.rb +++ b/actionmailer/test/test_case_test.rb @@ -1,8 +1,48 @@ -require 'abstract_unit' +# frozen_string_literal: true + +require "abstract_unit" class TestTestMailer < ActionMailer::Base end +class ClearTestDeliveriesMixinTest < ActiveSupport::TestCase + include ActionMailer::TestCase::ClearTestDeliveries + + def before_setup + ActionMailer::Base.delivery_method, @original_delivery_method = :test, ActionMailer::Base.delivery_method + ActionMailer::Base.deliveries << "better clear me, setup" + super + end + + def after_teardown + super + assert_equal [], ActionMailer::Base.deliveries + ActionMailer::Base.delivery_method = @original_delivery_method + end + + def test_deliveries_are_cleared_on_setup_and_teardown + assert_equal [], ActionMailer::Base.deliveries + ActionMailer::Base.deliveries << "better clear me, teardown" + end +end + +class MailerDeliveriesClearingTest < ActionMailer::TestCase + def before_setup + ActionMailer::Base.deliveries << "better clear me, setup" + super + end + + def after_teardown + super + assert_equal [], ActionMailer::Base.deliveries + end + + def test_deliveries_are_cleared_on_setup_and_teardown + assert_equal [], ActionMailer::Base.deliveries + ActionMailer::Base.deliveries << "better clear me, teardown" + end +end + class CrazyNameMailerTest < ActionMailer::TestCase tests TestTestMailer @@ -20,7 +60,7 @@ class CrazySymbolNameMailerTest < ActionMailer::TestCase end class CrazyStringNameMailerTest < ActionMailer::TestCase - tests 'test_test_mailer' + tests "test_test_mailer" def test_set_mailer_class_manual_using_string assert_equal TestTestMailer, self.class.mailer_class diff --git a/actionmailer/test/test_helper_test.rb b/actionmailer/test/test_helper_test.rb index 0a4bc75d3e..d31170706b 100644 --- a/actionmailer/test/test_helper_test.rb +++ b/actionmailer/test/test_helper_test.rb @@ -1,5 +1,7 @@ -require 'abstract_unit' -require 'active_support/testing/stream' +# frozen_string_literal: true + +require "abstract_unit" +require "active_support/testing/stream" class TestHelperMailer < ActionMailer::Base def test @@ -8,6 +10,18 @@ class TestHelperMailer < ActionMailer::Base to: "test@example.com", from: "tester@example.com" end + + def test_args(recipient, name) + mail body: render(inline: "Hello, #{name}"), + to: recipient, + from: "tester@example.com" + end + + def test_parameter_args + mail body: render(inline: "All is #{params[:all]}"), + to: "test@example.com", + from: "tester@example.com" + end end class TestHelperMailerTest < ActionMailer::TestCase @@ -40,11 +54,11 @@ class TestHelperMailerTest < ActionMailer::TestCase end def test_encode - assert_equal '=?UTF-8?Q?This_is_=E3=81=82_string?=', encode('This is あ string') + assert_equal "This is あ string", Mail::Encodings.q_value_decode(encode("This is あ string")) end def test_read_fixture - assert_equal ['Welcome!'], read_fixture('welcome') + assert_equal ["Welcome!"], read_fixture("welcome") end def test_assert_emails @@ -55,6 +69,16 @@ class TestHelperMailerTest < ActionMailer::TestCase end end + def test_assert_emails_with_enqueued_emails + assert_nothing_raised do + assert_emails 1 do + silence_stream($stdout) do + TestHelperMailer.test.deliver_later + end + end + end + end + def test_repeated_assert_emails_calls assert_nothing_raised do assert_emails 1 do @@ -91,6 +115,18 @@ class TestHelperMailerTest < ActionMailer::TestCase end end + def test_assert_no_emails_with_enqueued_emails + error = assert_raise ActiveSupport::TestCase::Assertion do + assert_no_emails do + silence_stream($stdout) do + TestHelperMailer.test.deliver_later + end + end + end + + assert_match(/0 .* but 1/, error.message) + end + def test_assert_emails_too_few_sent error = assert_raise ActiveSupport::TestCase::Assertion do assert_emails 2 do @@ -143,6 +179,16 @@ class TestHelperMailerTest < ActionMailer::TestCase end end + def test_assert_enqueued_parameterized_emails + assert_nothing_raised do + assert_enqueued_emails 1 do + silence_stream($stdout) do + TestHelperMailer.with(a: 1).test.deliver_later + end + end + end + end + def test_assert_enqueued_emails_too_few_sent error = assert_raise ActiveSupport::TestCase::Assertion do assert_enqueued_emails 2 do @@ -176,6 +222,14 @@ class TestHelperMailerTest < ActionMailer::TestCase end end + def test_assert_no_enqueued_parameterized_emails + assert_nothing_raised do + assert_no_enqueued_emails do + TestHelperMailer.with(a: 1).test.deliver_now + end + end + end + def test_assert_no_enqueued_emails_failure error = assert_raise ActiveSupport::TestCase::Assertion do assert_no_enqueued_emails do @@ -187,6 +241,63 @@ class TestHelperMailerTest < ActionMailer::TestCase assert_match(/0 .* but 1/, error.message) end + + def test_assert_enqueued_email_with + assert_nothing_raised do + assert_enqueued_email_with TestHelperMailer, :test do + silence_stream($stdout) do + TestHelperMailer.test.deliver_later + end + end + end + end + + def test_assert_enqueued_email_with_with_no_block + assert_nothing_raised do + silence_stream($stdout) do + TestHelperMailer.test.deliver_later + assert_enqueued_email_with TestHelperMailer, :test + end + end + end + + def test_assert_enqueued_email_with_with_args + assert_nothing_raised do + assert_enqueued_email_with TestHelperMailer, :test_args, args: ["some_email", "some_name"] do + silence_stream($stdout) do + TestHelperMailer.test_args("some_email", "some_name").deliver_later + end + end + end + end + + def test_assert_enqueued_email_with_with_no_block_with_args + assert_nothing_raised do + silence_stream($stdout) do + TestHelperMailer.test_args("some_email", "some_name").deliver_later + assert_enqueued_email_with TestHelperMailer, :test_args, args: ["some_email", "some_name"] + end + end + end + + def test_assert_enqueued_email_with_with_parameterized_args + assert_nothing_raised do + assert_enqueued_email_with TestHelperMailer, :test_parameter_args, args: { all: "good" } do + silence_stream($stdout) do + TestHelperMailer.with(all: "good").test_parameter_args.deliver_later + end + end + end + end + + def test_assert_enqueued_email_with_with_no_block_with_parameterized_args + assert_nothing_raised do + silence_stream($stdout) do + TestHelperMailer.with(all: "good").test_parameter_args.deliver_later + assert_enqueued_email_with TestHelperMailer, :test_parameter_args, args: { all: "good" } + end + end + end end class AnotherTestHelperMailerTest < ActionMailer::TestCase @@ -198,6 +309,6 @@ class AnotherTestHelperMailerTest < ActionMailer::TestCase def test_setup_shouldnt_conflict_with_mailer_setup assert_kind_of Mail::Message, @expected - assert_equal 'a value', @test_var + assert_equal "a value", @test_var end end diff --git a/actionmailer/test/url_test.rb b/actionmailer/test/url_test.rb index 7928fe9542..a926663a9f 100644 --- a/actionmailer/test/url_test.rb +++ b/actionmailer/test/url_test.rb @@ -1,20 +1,27 @@ -require 'abstract_unit' -require 'action_controller' +# frozen_string_literal: true + +require "abstract_unit" +require "action_controller" class WelcomeController < ActionController::Base end AppRoutes = ActionDispatch::Routing::RouteSet.new -class ActionMailer::Base - include AppRoutes.url_helpers +AppRoutes.draw do + get "/welcome" => "foo#bar", as: "welcome" + get "/dummy_model" => "foo#baz", as: "dummy_model" + get "/welcome/greeting", to: "welcome#greeting" + get "/a/b(/:id)", to: "a#b" end class UrlTestMailer < ActionMailer::Base - default_url_options[:host] = 'www.basecamphq.com' + include AppRoutes.url_helpers + + default_url_options[:host] = "www.basecamphq.com" configure do |c| - c.assets_dir = '' # To get the tests to pass + c.assets_dir = "" # To get the tests to pass end def signed_up_with_url(recipient) @@ -27,14 +34,14 @@ class UrlTestMailer < ActionMailer::Base def exercise_url_for(options) @options = options @url = url_for(@options) - mail(from: 'from@example.com', to: 'to@example.com', subject: 'subject') + mail(from: "from@example.com", to: "to@example.com", subject: "subject") end end class ActionMailerUrlTest < ActionMailer::TestCase class DummyModel def self.model_name - OpenStruct.new(route_key: 'dummy_model') + OpenStruct.new(route_key: "dummy_model") end def persisted? @@ -50,11 +57,11 @@ class ActionMailerUrlTest < ActionMailer::TestCase end end - def encode( text, charset="UTF-8" ) - quoted_printable( text, charset ) + def encode(text, charset = "UTF-8") + quoted_printable(text, charset) end - def new_mail( charset="UTF-8" ) + def new_mail(charset = "UTF-8") mail = Mail.new mail.mime_version = "1.0" if charset @@ -64,7 +71,7 @@ class ActionMailerUrlTest < ActionMailer::TestCase end def assert_url_for(expected, options, relative = false) - expected = "http://www.basecamphq.com#{expected}" if expected.start_with?('/') && !relative + expected = "http://www.basecamphq.com#{expected}" if expected.start_with?("/") && !relative urls = UrlTestMailer.exercise_url_for(options).body.to_s.chomp.split assert_equal expected, urls.first @@ -72,50 +79,39 @@ class ActionMailerUrlTest < ActionMailer::TestCase end def setup - @recipient = 'test@localhost' + @recipient = "test@localhost" end def test_url_for UrlTestMailer.delivery_method = :test - AppRoutes.draw do - get ':controller(/:action(/:id))' - get '/welcome' => 'foo#bar', as: 'welcome' - get '/dummy_model' => 'foo#baz', as: 'dummy_model' - end - # string - assert_url_for 'http://foo/', 'http://foo/' + assert_url_for "http://foo/", "http://foo/" # symbol - assert_url_for '/welcome', :welcome + assert_url_for "/welcome", :welcome # hash - assert_url_for '/a/b/c', controller: 'a', action: 'b', id: 'c' - assert_url_for '/a/b/c', {controller: 'a', action: 'b', id: 'c', only_path: true}, true + assert_url_for "/a/b/c", controller: "a", action: "b", id: "c" + assert_url_for "/a/b/c", { controller: "a", action: "b", id: "c", only_path: true }, true # model - assert_url_for '/dummy_model', DummyModel.new + assert_url_for "/dummy_model", DummyModel.new # class - assert_url_for '/dummy_model', DummyModel + assert_url_for "/dummy_model", DummyModel # array - assert_url_for '/dummy_model' , [DummyModel] + assert_url_for "/dummy_model", [DummyModel] end def test_signed_up_with_url UrlTestMailer.delivery_method = :test - AppRoutes.draw do - get ':controller(/:action(/:id))' - get '/welcome' => "foo#bar", as: "welcome" - end - expected = new_mail expected.to = @recipient expected.subject = "[Signed up] Welcome #{@recipient}" - expected.body = "Hello there,\n\nMr. #{@recipient}. Please see our greeting at http://example.com/welcome/greeting http://www.basecamphq.com/welcome\n\n<img alt=\"Somelogo\" src=\"/images/somelogo.png\" />" + expected.body = "Hello there,\n\nMr. #{@recipient}. Please see our greeting at http://example.com/welcome/greeting http://www.basecamphq.com/welcome\n\n<img src=\"/images/somelogo.png\" />" expected.from = "system@loudthinking.com" expected.date = Time.local(2004, 12, 12) expected.content_type = "text/html" @@ -124,15 +120,15 @@ class ActionMailerUrlTest < ActionMailer::TestCase assert_nothing_raised { created = UrlTestMailer.signed_up_with_url(@recipient) } assert_not_nil created - expected.message_id = '<123@456>' - created.message_id = '<123@456>' + expected.message_id = "<123@456>" + created.message_id = "<123@456>" assert_dom_equal expected.encoded, created.encoded assert_nothing_raised { UrlTestMailer.signed_up_with_url(@recipient).deliver_now } assert_not_nil ActionMailer::Base.deliveries.first delivered = ActionMailer::Base.deliveries.first - delivered.message_id = '<123@456>' + delivered.message_id = "<123@456>" assert_dom_equal expected.encoded, delivered.encoded end end |