+
+
+<%= link_to "Back to all inbound emails", main_app.rails_conductor_inbound_emails_path %>
\ No newline at end of file
diff --git a/actionmailbox/app/views/rails/conductor/action_mailbox/inbound_emails/sources/new.html.erb b/actionmailbox/app/views/rails/conductor/action_mailbox/inbound_emails/sources/new.html.erb
new file mode 100644
index 0000000000000..82b6ea8dd1bb9
--- /dev/null
+++ b/actionmailbox/app/views/rails/conductor/action_mailbox/inbound_emails/sources/new.html.erb
@@ -0,0 +1,12 @@
+<% provide :title, "Deliver new inbound email by source" %>
+
+
Deliver new inbound email by source
+
+<%= form_with(url: main_app.rails_conductor_inbound_email_sources_path, local: true) do |form| %>
+
+
+ <%= form.submit "Deliver inbound email" %>
+<% end %>
diff --git a/actionmailbox/bin/test b/actionmailbox/bin/test
new file mode 100755
index 0000000000000..c53377cc970f4
--- /dev/null
+++ b/actionmailbox/bin/test
@@ -0,0 +1,5 @@
+#!/usr/bin/env ruby
+# frozen_string_literal: true
+
+COMPONENT_ROOT = File.expand_path("..", __dir__)
+require_relative "../../tools/test"
diff --git a/actionmailbox/config/routes.rb b/actionmailbox/config/routes.rb
new file mode 100644
index 0000000000000..e3d5737a631f3
--- /dev/null
+++ b/actionmailbox/config/routes.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+Rails.application.routes.draw do
+ scope "/rails/action_mailbox", module: "action_mailbox/ingresses" do
+ post "/postmark/inbound_emails" => "postmark/inbound_emails#create", as: :rails_postmark_inbound_emails
+ post "/relay/inbound_emails" => "relay/inbound_emails#create", as: :rails_relay_inbound_emails
+ post "/sendgrid/inbound_emails" => "sendgrid/inbound_emails#create", as: :rails_sendgrid_inbound_emails
+
+ # Mandrill checks for the existence of a URL with a HEAD request before it will create the webhook.
+ get "/mandrill/inbound_emails" => "mandrill/inbound_emails#health_check", as: :rails_mandrill_inbound_health_check
+ post "/mandrill/inbound_emails" => "mandrill/inbound_emails#create", as: :rails_mandrill_inbound_emails
+
+ # Mailgun requires that a webhook's URL end in 'mime' for it to receive the raw contents of emails.
+ post "/mailgun/inbound_emails/mime" => "mailgun/inbound_emails#create", as: :rails_mailgun_inbound_emails
+ end
+
+ # TODO: Should these be mounted within the engine only?
+ scope "rails/conductor/action_mailbox/", module: "rails/conductor/action_mailbox" do
+ resources :inbound_emails, as: :rails_conductor_inbound_emails, only: %i[index new show create]
+ get "inbound_emails/sources/new", to: "inbound_emails/sources#new", as: :new_rails_conductor_inbound_email_source
+ post "inbound_emails/sources", to: "inbound_emails/sources#create", as: :rails_conductor_inbound_email_sources
+
+ post ":inbound_email_id/reroute" => "reroutes#create", as: :rails_conductor_inbound_email_reroute
+ post ":inbound_email_id/incinerate" => "incinerates#create", as: :rails_conductor_inbound_email_incinerate
+ end
+end
diff --git a/actionmailbox/db/migrate/20180917164000_create_action_mailbox_tables.rb b/actionmailbox/db/migrate/20180917164000_create_action_mailbox_tables.rb
new file mode 100644
index 0000000000000..2a1e2f7a8e2e1
--- /dev/null
+++ b/actionmailbox/db/migrate/20180917164000_create_action_mailbox_tables.rb
@@ -0,0 +1,19 @@
+class CreateActionMailboxTables < ActiveRecord::Migration[6.0]
+ def change
+ create_table :action_mailbox_inbound_emails, id: primary_key_type do |t|
+ t.integer :status, default: 0, null: false
+ t.string :message_id, null: false
+ t.string :message_checksum, null: false
+
+ t.timestamps
+
+ t.index [ :message_id, :message_checksum ], name: "index_action_mailbox_inbound_emails_uniqueness", unique: true
+ end
+ end
+
+ private
+ def primary_key_type
+ config = Rails.configuration.generators
+ config.options[config.orm][:primary_key_type] || :primary_key
+ end
+end
diff --git a/actionmailbox/lib/action_mailbox.rb b/actionmailbox/lib/action_mailbox.rb
new file mode 100644
index 0000000000000..d0e6b1600d840
--- /dev/null
+++ b/actionmailbox/lib/action_mailbox.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+require "active_support"
+require "active_support/rails"
+require "active_support/core_ext/numeric/time"
+
+require "action_mailbox/version"
+require "action_mailbox/deprecator"
+require "action_mailbox/mail_ext"
+
+# :markup: markdown
+# :include: ../README.md
+module ActionMailbox
+ extend ActiveSupport::Autoload
+
+ autoload :Base
+ autoload :Router
+ autoload :TestCase
+
+ mattr_accessor :ingress
+ mattr_accessor :logger
+ mattr_accessor :incinerate, default: true
+ mattr_accessor :incinerate_after, default: 30.days
+ mattr_accessor :queues, default: {}
+ mattr_accessor :storage_service
+end
diff --git a/actionmailbox/lib/action_mailbox/base.rb b/actionmailbox/lib/action_mailbox/base.rb
new file mode 100644
index 0000000000000..d6ff9caf404bb
--- /dev/null
+++ b/actionmailbox/lib/action_mailbox/base.rb
@@ -0,0 +1,135 @@
+# frozen_string_literal: true
+
+require "active_support/rescuable"
+
+require "action_mailbox/callbacks"
+require "action_mailbox/routing"
+
+module ActionMailbox
+ # = Action Mailbox \Base
+ #
+ # The base class for all application mailboxes. Not intended to be inherited from directly. Inherit from
+ # +ApplicationMailbox+ instead, as that's where the app-specific routing is configured. This routing
+ # is specified in the following ways:
+ #
+ # class ApplicationMailbox < ActionMailbox::Base
+ # # Any of the recipients of the mail (whether to, cc, bcc) are matched against the regexp.
+ # routing /^replies@/i => :replies
+ #
+ # # Any of the recipients of the mail (whether to, cc, bcc) needs to be an exact match for the string.
+ # routing "help@example.com" => :help
+ #
+ # # Any callable (proc, lambda, etc) object is passed the inbound_email record and is a match if true.
+ # routing ->(inbound_email) { inbound_email.mail.to.size > 2 } => :multiple_recipients
+ #
+ # # Any object responding to #match? is called with the inbound_email record as an argument. Match if true.
+ # routing CustomAddress.new => :custom
+ #
+ # # Any inbound_email that has not been already matched will be sent to the BackstopMailbox.
+ # routing :all => :backstop
+ # end
+ #
+ # Application mailboxes need to override the #process method, which is invoked by the framework after
+ # callbacks have been run. The callbacks available are: +before_processing+, +after_processing+, and
+ # +around_processing+. The primary use case is to ensure that certain preconditions to processing are fulfilled
+ # using +before_processing+ callbacks.
+ #
+ # If a precondition fails to be met, you can halt the processing using the +#bounced!+ method,
+ # which will silently prevent any further processing, but not actually send out any bounce notice. You
+ # can also pair this behavior with the invocation of an Action Mailer class responsible for sending out
+ # an actual bounce email. This is done using the #bounce_with method, which takes the mail object returned
+ # by an Action Mailer method, like so:
+ #
+ # class ForwardsMailbox < ApplicationMailbox
+ # before_processing :ensure_sender_is_a_user
+ #
+ # private
+ # def ensure_sender_is_a_user
+ # unless User.exist?(email_address: mail.from)
+ # bounce_with UserRequiredMailer.missing(inbound_email)
+ # end
+ # end
+ # end
+ #
+ # During the processing of the inbound email, the status will be tracked. Before processing begins,
+ # the email will normally have the +pending+ status. Once processing begins, just before callbacks
+ # and the #process method is called, the status is changed to +processing+. If processing is allowed to
+ # complete, the status is changed to +delivered+. If a bounce is triggered, then +bounced+. If an unhandled
+ # exception is bubbled up, then +failed+.
+ #
+ # Exceptions can be handled at the class level using the familiar
+ # ActiveSupport::Rescuable approach:
+ #
+ # class ForwardsMailbox < ApplicationMailbox
+ # rescue_from(ApplicationSpecificVerificationError) { bounced! }
+ # end
+ class Base
+ include ActiveSupport::Rescuable
+ include ActionMailbox::Callbacks, ActionMailbox::Routing
+
+ attr_reader :inbound_email
+ delegate :mail, :delivered!, :bounced!, to: :inbound_email
+
+ delegate :logger, to: ActionMailbox
+
+ def self.receive(inbound_email)
+ new(inbound_email).perform_processing
+ end
+
+ def initialize(inbound_email)
+ @inbound_email = inbound_email
+ end
+
+ def perform_processing # :nodoc:
+ ActiveSupport::Notifications.instrument "process.action_mailbox", instrumentation_payload do
+ track_status_of_inbound_email do
+ run_callbacks :process do
+ process
+ end
+ end
+ rescue => exception
+ # TODO: Include a reference to the inbound_email in the exception raised so error handling becomes easier
+ rescue_with_handler(exception) || raise
+ end
+ end
+
+ def process
+ # Override in subclasses
+ end
+
+ def finished_processing? # :nodoc:
+ inbound_email.delivered? || inbound_email.bounced?
+ end
+
+ # Enqueues the given +message+ for delivery and changes the inbound email's status to +:bounced+.
+ def bounce_with(message)
+ inbound_email.bounced!
+ message.deliver_later
+ end
+
+ # Immediately sends the given +message+ and changes the inbound email's status to +:bounced+.
+ def bounce_now_with(message)
+ inbound_email.bounced!
+ message.deliver_now
+ end
+
+ private
+ def instrumentation_payload
+ {
+ mailbox: self,
+ inbound_email: inbound_email.instrumentation_payload
+ }
+ end
+
+ def track_status_of_inbound_email
+ inbound_email.processing!
+ yield
+ inbound_email.delivered! unless inbound_email.bounced?
+ rescue
+ inbound_email.failed!
+ raise
+ end
+ end
+end
+
+ActiveSupport.run_load_hooks :action_mailbox, ActionMailbox::Base
diff --git a/actionmailbox/lib/action_mailbox/callbacks.rb b/actionmailbox/lib/action_mailbox/callbacks.rb
new file mode 100644
index 0000000000000..cc801083dd091
--- /dev/null
+++ b/actionmailbox/lib/action_mailbox/callbacks.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+require "active_support/callbacks"
+
+module ActionMailbox
+ # = Action Mailbox \Callbacks
+ #
+ # Defines the callbacks related to processing.
+ module Callbacks
+ extend ActiveSupport::Concern
+ include ActiveSupport::Callbacks
+
+ TERMINATOR = ->(mailbox, chain) do
+ chain.call
+ mailbox.finished_processing?
+ end
+
+ included do
+ define_callbacks :process, terminator: TERMINATOR, skip_after_callbacks_if_terminated: true
+ end
+
+ class_methods do
+ def before_processing(*methods, &block)
+ set_callback(:process, :before, *methods, &block)
+ end
+
+ def after_processing(*methods, &block)
+ set_callback(:process, :after, *methods, &block)
+ end
+
+ def around_processing(*methods, &block)
+ set_callback(:process, :around, *methods, &block)
+ end
+ end
+ end
+end
diff --git a/actionmailbox/lib/action_mailbox/deprecator.rb b/actionmailbox/lib/action_mailbox/deprecator.rb
new file mode 100644
index 0000000000000..675becfc42921
--- /dev/null
+++ b/actionmailbox/lib/action_mailbox/deprecator.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+module ActionMailbox
+ def self.deprecator # :nodoc:
+ @deprecator ||= ActiveSupport::Deprecation.new
+ end
+end
diff --git a/actionmailbox/lib/action_mailbox/engine.rb b/actionmailbox/lib/action_mailbox/engine.rb
new file mode 100644
index 0000000000000..d0dc5c8ce29a7
--- /dev/null
+++ b/actionmailbox/lib/action_mailbox/engine.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+require "rails"
+require "action_controller/railtie"
+require "active_job/railtie"
+require "active_record/railtie"
+require "active_storage/engine"
+
+require "action_mailbox"
+
+module ActionMailbox
+ class Engine < Rails::Engine
+ isolate_namespace ActionMailbox
+ config.eager_load_namespaces << ActionMailbox
+
+ config.action_mailbox = ActiveSupport::OrderedOptions.new
+ config.action_mailbox.incinerate = true
+ config.action_mailbox.incinerate_after = 30.days
+
+ config.action_mailbox.queues = ActiveSupport::InheritableOptions.new \
+ incineration: :action_mailbox_incineration, routing: :action_mailbox_routing
+
+ config.action_mailbox.storage_service = nil
+
+ initializer "action_mailbox.deprecator", before: :load_environment_config do |app|
+ app.deprecators[:action_mailbox] = ActionMailbox.deprecator
+ end
+
+ initializer "action_mailbox.config" do
+ config.after_initialize do |app|
+ ActionMailbox.logger = app.config.action_mailbox.logger || Rails.logger
+ ActionMailbox.incinerate = app.config.action_mailbox.incinerate.nil? ? true : app.config.action_mailbox.incinerate
+ ActionMailbox.incinerate_after = app.config.action_mailbox.incinerate_after || 30.days
+ ActionMailbox.queues = app.config.action_mailbox.queues || {}
+ ActionMailbox.ingress = app.config.action_mailbox.ingress
+ ActionMailbox.storage_service = app.config.action_mailbox.storage_service
+ end
+ end
+ end
+end
diff --git a/actionmailbox/lib/action_mailbox/gem_version.rb b/actionmailbox/lib/action_mailbox/gem_version.rb
new file mode 100644
index 0000000000000..d351800d34bbb
--- /dev/null
+++ b/actionmailbox/lib/action_mailbox/gem_version.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module ActionMailbox
+ # Returns the currently loaded version of Action Mailbox as a +Gem::Version+.
+ def self.gem_version
+ Gem::Version.new VERSION::STRING
+ end
+
+ module VERSION
+ MAJOR = 8
+ MINOR = 1
+ TINY = 0
+ PRE = "alpha"
+
+ STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
+ end
+end
diff --git a/actionmailbox/lib/action_mailbox/mail_ext.rb b/actionmailbox/lib/action_mailbox/mail_ext.rb
new file mode 100644
index 0000000000000..c4d277a1f9d7a
--- /dev/null
+++ b/actionmailbox/lib/action_mailbox/mail_ext.rb
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+
+require "mail"
+
+# The hope is to upstream most of these basic additions to the Mail gem's Mail object. But until then, here they lay!
+Dir["#{File.expand_path(File.dirname(__FILE__))}/mail_ext/*"].each { |path| require "action_mailbox/mail_ext/#{File.basename(path)}" }
diff --git a/actionmailbox/lib/action_mailbox/mail_ext/address_equality.rb b/actionmailbox/lib/action_mailbox/mail_ext/address_equality.rb
new file mode 100644
index 0000000000000..39a43b3468601
--- /dev/null
+++ b/actionmailbox/lib/action_mailbox/mail_ext/address_equality.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module Mail
+ class Address
+ def ==(other_address)
+ other_address.is_a?(Mail::Address) && to_s == other_address.to_s
+ end
+ end
+end
diff --git a/actionmailbox/lib/action_mailbox/mail_ext/address_wrapping.rb b/actionmailbox/lib/action_mailbox/mail_ext/address_wrapping.rb
new file mode 100644
index 0000000000000..19eb624c1c0b0
--- /dev/null
+++ b/actionmailbox/lib/action_mailbox/mail_ext/address_wrapping.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module Mail
+ class Address
+ def self.wrap(address)
+ address.is_a?(Mail::Address) ? address : Mail::Address.new(address)
+ end
+ end
+end
diff --git a/actionmailbox/lib/action_mailbox/mail_ext/addresses.rb b/actionmailbox/lib/action_mailbox/mail_ext/addresses.rb
new file mode 100644
index 0000000000000..5961088c781c0
--- /dev/null
+++ b/actionmailbox/lib/action_mailbox/mail_ext/addresses.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+module Mail
+ class Message
+ def from_address
+ address_list(header[:from])&.addresses&.first
+ end
+
+ def reply_to_address
+ address_list(header[:reply_to])&.addresses&.first
+ end
+
+ def recipients_addresses
+ to_addresses + cc_addresses + bcc_addresses + x_original_to_addresses + x_forwarded_to_addresses
+ end
+
+ def to_addresses
+ Array(address_list(header[:to])&.addresses)
+ end
+
+ def cc_addresses
+ Array(address_list(header[:cc])&.addresses)
+ end
+
+ def bcc_addresses
+ Array(address_list(header[:bcc])&.addresses)
+ end
+
+ def x_original_to_addresses
+ Array(header[:x_original_to]).collect { |header| Mail::Address.new header.to_s }
+ end
+
+ def x_forwarded_to_addresses
+ Array(header[:x_forwarded_to]).collect { |header| Mail::Address.new header.to_s }
+ end
+
+ private
+ def address_list(obj)
+ if obj.respond_to?(:element)
+ # Mail 2.8+
+ obj.element
+ else
+ # Mail <= 2.7.x
+ obj&.address_list
+ end
+ end
+ end
+end
diff --git a/actionmailbox/lib/action_mailbox/mail_ext/from_source.rb b/actionmailbox/lib/action_mailbox/mail_ext/from_source.rb
new file mode 100644
index 0000000000000..17b7fc80ad536
--- /dev/null
+++ b/actionmailbox/lib/action_mailbox/mail_ext/from_source.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+module Mail
+ def self.from_source(source)
+ Mail.new Mail::Utilities.binary_unsafe_to_crlf(source.to_s)
+ end
+end
diff --git a/actionmailbox/lib/action_mailbox/mail_ext/recipients.rb b/actionmailbox/lib/action_mailbox/mail_ext/recipients.rb
new file mode 100644
index 0000000000000..102ec83debe4b
--- /dev/null
+++ b/actionmailbox/lib/action_mailbox/mail_ext/recipients.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+module Mail
+ class Message
+ def recipients
+ Array(to) + Array(cc) + Array(bcc) + Array(header[:x_original_to]).map(&:to_s) +
+ Array(header[:x_forwarded_to]).map(&:to_s)
+ end
+ end
+end
diff --git a/actionmailbox/lib/action_mailbox/relayer.rb b/actionmailbox/lib/action_mailbox/relayer.rb
new file mode 100644
index 0000000000000..e2890acb608ab
--- /dev/null
+++ b/actionmailbox/lib/action_mailbox/relayer.rb
@@ -0,0 +1,75 @@
+# frozen_string_literal: true
+
+require "action_mailbox/version"
+require "net/http"
+require "uri"
+
+module ActionMailbox
+ class Relayer
+ class Result < Struct.new(:status_code, :message)
+ def success?
+ !failure?
+ end
+
+ def failure?
+ transient_failure? || permanent_failure?
+ end
+
+ def transient_failure?
+ status_code.start_with?("4.")
+ end
+
+ def permanent_failure?
+ status_code.start_with?("5.")
+ end
+ end
+
+ CONTENT_TYPE = "message/rfc822"
+ USER_AGENT = "Action Mailbox relayer v#{ActionMailbox.version}"
+
+ attr_reader :uri, :username, :password
+
+ def initialize(url:, username: "actionmailbox", password:)
+ @uri, @username, @password = URI(url), username, password
+ end
+
+ def relay(source)
+ case response = post(source)
+ when Net::HTTPSuccess
+ Result.new "2.0.0", "Successfully relayed message to ingress"
+ when Net::HTTPUnauthorized
+ Result.new "4.7.0", "Invalid credentials for ingress"
+ else
+ Result.new "4.0.0", "HTTP #{response.code}"
+ end
+ rescue IOError, SocketError, SystemCallError => error
+ Result.new "4.4.2", "Network error relaying to ingress: #{error.message}"
+ rescue Timeout::Error
+ Result.new "4.4.2", "Timed out relaying to ingress"
+ rescue => error
+ Result.new "4.0.0", "Error relaying to ingress: #{error.message}"
+ end
+
+ private
+ def post(source)
+ client.post uri, source,
+ "Content-Type" => CONTENT_TYPE,
+ "User-Agent" => USER_AGENT,
+ "Authorization" => "Basic #{Base64.strict_encode64(username + ":" + password)}"
+ end
+
+ def client
+ @client ||= Net::HTTP.new(uri.host, uri.port).tap do |connection|
+ if uri.scheme == "https"
+ require "openssl"
+
+ connection.use_ssl = true
+ connection.verify_mode = OpenSSL::SSL::VERIFY_PEER
+ end
+
+ connection.open_timeout = 1
+ connection.read_timeout = 10
+ end
+ end
+ end
+end
diff --git a/actionmailbox/lib/action_mailbox/router.rb b/actionmailbox/lib/action_mailbox/router.rb
new file mode 100644
index 0000000000000..0b67266af5c83
--- /dev/null
+++ b/actionmailbox/lib/action_mailbox/router.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+module ActionMailbox
+ # = Action Mailbox \Router
+ #
+ # Encapsulates the routes that live on the ApplicationMailbox and performs the actual routing when
+ # an inbound_email is received.
+ class Router
+ class RoutingError < StandardError; end
+
+ def initialize
+ @routes = []
+ end
+
+ def add_routes(routes)
+ routes.each do |(address, mailbox_name)|
+ add_route address, to: mailbox_name
+ end
+ end
+
+ def add_route(address, to:)
+ routes.append Route.new(address, to: to)
+ end
+
+ def route(inbound_email)
+ if mailbox = mailbox_for(inbound_email)
+ mailbox.receive(inbound_email)
+ else
+ inbound_email.bounced!
+
+ raise RoutingError
+ end
+ end
+
+ def mailbox_for(inbound_email)
+ routes.detect { |route| route.match?(inbound_email) }&.mailbox_class
+ end
+
+ private
+ attr_reader :routes
+ end
+end
+
+require "action_mailbox/router/route"
diff --git a/actionmailbox/lib/action_mailbox/router/route.rb b/actionmailbox/lib/action_mailbox/router/route.rb
new file mode 100644
index 0000000000000..7c67c775900f7
--- /dev/null
+++ b/actionmailbox/lib/action_mailbox/router/route.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+module ActionMailbox
+ # Encapsulates a route, which can then be matched against an inbound_email and provide a lookup of the matching
+ # mailbox class. See examples for the different route addresses and how to use them in the ActionMailbox::Base
+ # documentation.
+ class Router::Route
+ attr_reader :address, :mailbox_name
+
+ def initialize(address, to:)
+ @address, @mailbox_name = address, to
+
+ ensure_valid_address
+ end
+
+ def match?(inbound_email)
+ case address
+ when :all
+ true
+ when String
+ inbound_email.mail.recipients.any? { |recipient| address.casecmp?(recipient) }
+ when Regexp
+ inbound_email.mail.recipients.any? { |recipient| address.match?(recipient) }
+ when Proc
+ address.call(inbound_email)
+ else
+ address.match?(inbound_email)
+ end
+ end
+
+ def mailbox_class
+ "#{mailbox_name.to_s.camelize}Mailbox".constantize
+ end
+
+ private
+ def ensure_valid_address
+ unless [ Symbol, String, Regexp, Proc ].any? { |klass| address.is_a?(klass) } || address.respond_to?(:match?)
+ raise ArgumentError, "Expected a Symbol, String, Regexp, Proc, or matchable, got #{address.inspect}"
+ end
+ end
+ end
+end
diff --git a/actionmailbox/lib/action_mailbox/routing.rb b/actionmailbox/lib/action_mailbox/routing.rb
new file mode 100644
index 0000000000000..4e98d4ee0b29f
--- /dev/null
+++ b/actionmailbox/lib/action_mailbox/routing.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+module ActionMailbox
+ # See ActionMailbox::Base for how to specify routing.
+ module Routing
+ extend ActiveSupport::Concern
+
+ included do
+ cattr_accessor :router, default: ActionMailbox::Router.new
+ end
+
+ class_methods do
+ def routing(routes)
+ router.add_routes(routes)
+ end
+
+ def route(inbound_email)
+ router.route(inbound_email)
+ end
+
+ def mailbox_for(inbound_email)
+ router.mailbox_for(inbound_email)
+ end
+ end
+ end
+end
diff --git a/actionmailbox/lib/action_mailbox/test_case.rb b/actionmailbox/lib/action_mailbox/test_case.rb
new file mode 100644
index 0000000000000..5e78e428d3309
--- /dev/null
+++ b/actionmailbox/lib/action_mailbox/test_case.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+require "action_mailbox/test_helper"
+require "active_support/test_case"
+
+module ActionMailbox
+ class TestCase < ActiveSupport::TestCase
+ include ActionMailbox::TestHelper
+ end
+end
+
+ActiveSupport.run_load_hooks :action_mailbox_test_case, ActionMailbox::TestCase
diff --git a/actionmailbox/lib/action_mailbox/test_helper.rb b/actionmailbox/lib/action_mailbox/test_helper.rb
new file mode 100644
index 0000000000000..ea50afd7f0a61
--- /dev/null
+++ b/actionmailbox/lib/action_mailbox/test_helper.rb
@@ -0,0 +1,95 @@
+# frozen_string_literal: true
+
+require "mail"
+
+module ActionMailbox
+ module TestHelper
+ # Create an InboundEmail record using an eml fixture in the format of message/rfc822
+ # referenced with +fixture_name+ located in +test/fixtures/files/fixture_name+.
+ def create_inbound_email_from_fixture(fixture_name, status: :processing)
+ create_inbound_email_from_source file_fixture(fixture_name).read, status: status
+ end
+
+ # Creates an InboundEmail by specifying through options or a block.
+ #
+ # ==== Options
+ #
+ # * :status - The +status+ to set for the created InboundEmail.
+ # For possible statuses, see its documentation.
+ #
+ # ==== Creating a simple email
+ #
+ # When you only need to set basic fields like +from+, +to+, +subject+, and
+ # +body+, you can pass them directly as options.
+ #
+ # create_inbound_email_from_mail(from: "david@loudthinking.com", subject: "Hello!")
+ #
+ # ==== Creating a multi-part email
+ #
+ # When you need to create a more intricate email, like a multi-part email
+ # that contains both a plaintext version and an HTML version, you can pass a
+ # block.
+ #
+ # create_inbound_email_from_mail do
+ # to "David Heinemeier Hansson "
+ # from "Bilbo Baggins "
+ # subject "Come down to the Shire!"
+ #
+ # text_part do
+ # body "Please join us for a party at Bag End"
+ # end
+ #
+ # html_part do
+ # body "
Please join us for a party at Bag End
"
+ # end
+ # end
+ #
+ # As with +Mail.new+, you can also use a block parameter to define the parts
+ # of the message:
+ #
+ # create_inbound_email_from_mail do |mail|
+ # mail.to "David Heinemeier Hansson "
+ # mail.from "Bilbo Baggins "
+ # mail.subject "Come down to the Shire!"
+ #
+ # mail.text_part do |part|
+ # part.body "Please join us for a party at Bag End"
+ # end
+ #
+ # mail.html_part do |part|
+ # part.body "
Please join us for a party at Bag End
"
+ # end
+ # end
+ def create_inbound_email_from_mail(status: :processing, **mail_options, &block)
+ mail = Mail.new(mail_options, &block)
+ # Bcc header is not encoded by default
+ mail[:bcc].include_in_headers = true if mail[:bcc]
+
+ create_inbound_email_from_source mail.to_s, status: status
+ end
+
+ # Create an InboundEmail using the raw rfc822 +source+ as text.
+ def create_inbound_email_from_source(source, status: :processing)
+ ActionMailbox::InboundEmail.create_and_extract_message_id! source, status: status
+ end
+
+
+ # Create an InboundEmail from fixture using the same arguments as create_inbound_email_from_fixture
+ # and immediately route it to processing.
+ def receive_inbound_email_from_fixture(*args)
+ create_inbound_email_from_fixture(*args).tap(&:route)
+ end
+
+ # Create an InboundEmail using the same options or block as
+ # create_inbound_email_from_mail, then immediately route it for processing.
+ def receive_inbound_email_from_mail(**kwargs, &block)
+ create_inbound_email_from_mail(**kwargs, &block).tap(&:route)
+ end
+
+ # Create an InboundEmail using the same arguments as create_inbound_email_from_source and immediately route it
+ # to processing.
+ def receive_inbound_email_from_source(*args)
+ create_inbound_email_from_source(*args).tap(&:route)
+ end
+ end
+end
diff --git a/actionmailbox/lib/action_mailbox/version.rb b/actionmailbox/lib/action_mailbox/version.rb
new file mode 100644
index 0000000000000..10c4cee6adcf7
--- /dev/null
+++ b/actionmailbox/lib/action_mailbox/version.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+require_relative "gem_version"
+
+module ActionMailbox
+ # Returns the currently loaded version of Action Mailbox as a +Gem::Version+.
+ def self.version
+ gem_version
+ end
+end
diff --git a/actionmailbox/lib/generators/action_mailbox/install/install_generator.rb b/actionmailbox/lib/generators/action_mailbox/install/install_generator.rb
new file mode 100644
index 0000000000000..05066d6071d2c
--- /dev/null
+++ b/actionmailbox/lib/generators/action_mailbox/install/install_generator.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+require "rails/generators/mailbox/mailbox_generator"
+
+module ActionMailbox
+ module Generators
+ class InstallGenerator < ::Rails::Generators::Base
+ source_root Rails::Generators::MailboxGenerator.source_root
+
+ def create_action_mailbox_files
+ say "Copying application_mailbox.rb to app/mailboxes", :green
+ template "application_mailbox.rb", "app/mailboxes/application_mailbox.rb"
+ end
+
+ def add_action_mailbox_production_environment_config
+ environment <<~end_of_config, env: "production"
+ # Prepare the ingress controller used to receive mail
+ # config.action_mailbox.ingress = :relay
+
+ end_of_config
+ end
+
+ def create_migrations
+ rails_command "railties:install:migrations FROM=active_storage,action_mailbox", inline: true
+ end
+ end
+ end
+end
diff --git a/actionmailbox/lib/rails/generators/mailbox/USAGE b/actionmailbox/lib/rails/generators/mailbox/USAGE
new file mode 100644
index 0000000000000..371fc2af69119
--- /dev/null
+++ b/actionmailbox/lib/rails/generators/mailbox/USAGE
@@ -0,0 +1,10 @@
+Description:
+ Generates a new mailbox class in app/mailboxes and invokes your template
+ engine and test framework generators.
+
+Example:
+ `bin/rails generate mailbox inbox`
+
+ creates an InboxMailbox class and test:
+ Mailbox: app/mailboxes/inbox_mailbox.rb
+ Test: test/mailboxes/inbox_mailbox_test.rb
diff --git a/actionmailbox/lib/rails/generators/mailbox/mailbox_generator.rb b/actionmailbox/lib/rails/generators/mailbox/mailbox_generator.rb
new file mode 100644
index 0000000000000..c2c403b8f6e94
--- /dev/null
+++ b/actionmailbox/lib/rails/generators/mailbox/mailbox_generator.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+module Rails
+ module Generators
+ class MailboxGenerator < NamedBase
+ source_root File.expand_path("templates", __dir__)
+
+ check_class_collision suffix: "Mailbox"
+
+ def create_mailbox_file
+ template "mailbox.rb", File.join("app/mailboxes", class_path, "#{file_name}_mailbox.rb")
+
+ in_root do
+ if behavior == :invoke && !File.exist?(application_mailbox_file_name)
+ template "application_mailbox.rb", application_mailbox_file_name
+ end
+ end
+ end
+
+ hook_for :test_framework
+
+ private
+ def file_name # :doc:
+ @_file_name ||= super.sub(/_mailbox\z/i, "")
+ end
+
+ def application_mailbox_file_name
+ "app/mailboxes/application_mailbox.rb"
+ end
+ end
+ end
+end
diff --git a/actionmailbox/lib/rails/generators/mailbox/templates/application_mailbox.rb.tt b/actionmailbox/lib/rails/generators/mailbox/templates/application_mailbox.rb.tt
new file mode 100644
index 0000000000000..ac22d03cd29ac
--- /dev/null
+++ b/actionmailbox/lib/rails/generators/mailbox/templates/application_mailbox.rb.tt
@@ -0,0 +1,3 @@
+class ApplicationMailbox < ActionMailbox::Base
+ # routing /something/i => :somewhere
+end
diff --git a/actionmailbox/lib/rails/generators/mailbox/templates/mailbox.rb.tt b/actionmailbox/lib/rails/generators/mailbox/templates/mailbox.rb.tt
new file mode 100644
index 0000000000000..110b3b9d7e17d
--- /dev/null
+++ b/actionmailbox/lib/rails/generators/mailbox/templates/mailbox.rb.tt
@@ -0,0 +1,4 @@
+class <%= class_name %>Mailbox < ApplicationMailbox
+ def process
+ end
+end
diff --git a/actionmailbox/lib/rails/generators/test_unit/mailbox_generator.rb b/actionmailbox/lib/rails/generators/test_unit/mailbox_generator.rb
new file mode 100644
index 0000000000000..2ec7d11a2fa07
--- /dev/null
+++ b/actionmailbox/lib/rails/generators/test_unit/mailbox_generator.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module TestUnit
+ module Generators
+ class MailboxGenerator < ::Rails::Generators::NamedBase
+ source_root File.expand_path("templates", __dir__)
+
+ check_class_collision suffix: "MailboxTest"
+
+ def create_test_files
+ template "mailbox_test.rb", File.join("test/mailboxes", class_path, "#{file_name}_mailbox_test.rb")
+ end
+
+ private
+ def file_name # :doc:
+ @_file_name ||= super.sub(/_mailbox\z/i, "")
+ end
+ end
+ end
+end
diff --git a/actionmailbox/lib/rails/generators/test_unit/templates/mailbox_test.rb.tt b/actionmailbox/lib/rails/generators/test_unit/templates/mailbox_test.rb.tt
new file mode 100644
index 0000000000000..3e215b4d0bed7
--- /dev/null
+++ b/actionmailbox/lib/rails/generators/test_unit/templates/mailbox_test.rb.tt
@@ -0,0 +1,11 @@
+require "test_helper"
+
+class <%= class_name %>MailboxTest < ActionMailbox::TestCase
+ # test "receive mail" do
+ # receive_inbound_email_from_mail \
+ # to: '"someone" ',
+ # from: '"else" ',
+ # subject: "Hello world!",
+ # body: "Hello?"
+ # end
+end
diff --git a/actionmailbox/lib/tasks/ingress.rake b/actionmailbox/lib/tasks/ingress.rake
new file mode 100644
index 0000000000000..43b613ea12d7d
--- /dev/null
+++ b/actionmailbox/lib/tasks/ingress.rake
@@ -0,0 +1,72 @@
+# frozen_string_literal: true
+
+namespace :action_mailbox do
+ namespace :ingress do
+ task :environment do
+ require "active_support"
+ require "active_support/core_ext/object/blank"
+ require "action_mailbox/relayer"
+ end
+
+ desc "Relay an inbound email from Exim to Action Mailbox (URL and INGRESS_PASSWORD required)"
+ task exim: "action_mailbox:ingress:environment" do
+ url, password = ENV.values_at("URL", "INGRESS_PASSWORD")
+
+ if url.blank? || password.blank?
+ print "URL and INGRESS_PASSWORD are required"
+ exit 64 # EX_USAGE
+ end
+
+ ActionMailbox::Relayer.new(url: url, password: password).relay(STDIN.read).tap do |result|
+ print result.message
+
+ case
+ when result.success?
+ exit 0
+ when result.transient_failure?
+ exit 75 # EX_TEMPFAIL
+ else
+ exit 69 # EX_UNAVAILABLE
+ end
+ end
+ end
+
+ desc "Relay an inbound email from Postfix to Action Mailbox (URL and INGRESS_PASSWORD required)"
+ task postfix: "action_mailbox:ingress:environment" do
+ url, password = ENV.values_at("URL", "INGRESS_PASSWORD")
+
+ if url.blank? || password.blank?
+ print "4.3.5 URL and INGRESS_PASSWORD are required"
+ exit 1
+ end
+
+ ActionMailbox::Relayer.new(url: url, password: password).relay(STDIN.read).tap do |result|
+ print "#{result.status_code} #{result.message}"
+ exit result.success?
+ end
+ end
+
+ desc "Relay an inbound email from Qmail to Action Mailbox (URL and INGRESS_PASSWORD required)"
+ task qmail: "action_mailbox:ingress:environment" do
+ url, password = ENV.values_at("URL", "INGRESS_PASSWORD")
+
+ if url.blank? || password.blank?
+ print "URL and INGRESS_PASSWORD are required"
+ exit 111
+ end
+
+ ActionMailbox::Relayer.new(url: url, password: password).relay(STDIN.read).tap do |result|
+ print result.message
+
+ case
+ when result.success?
+ exit 0
+ when result.transient_failure?
+ exit 111
+ else
+ exit 100
+ end
+ end
+ end
+ end
+end
diff --git a/actionmailbox/lib/tasks/install.rake b/actionmailbox/lib/tasks/install.rake
new file mode 100644
index 0000000000000..cffa6c9c0f431
--- /dev/null
+++ b/actionmailbox/lib/tasks/install.rake
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+
+desc "Install Action Mailbox and its dependencies"
+task "action_mailbox:install" do
+ Rails::Command.invoke :generate, ["action_mailbox:install"]
+end
diff --git a/actionmailbox/test/controllers/ingresses/mailgun/inbound_emails_controller_test.rb b/actionmailbox/test/controllers/ingresses/mailgun/inbound_emails_controller_test.rb
new file mode 100644
index 0000000000000..dfed9fa63c465
--- /dev/null
+++ b/actionmailbox/test/controllers/ingresses/mailgun/inbound_emails_controller_test.rb
@@ -0,0 +1,128 @@
+# frozen_string_literal: true
+
+require "test_helper"
+
+ENV["MAILGUN_INGRESS_SIGNING_KEY"] = "tbsy84uSV1Kt3ZJZELY2TmShPRs91E3yL4tzf96297vBCkDWgL"
+
+class ActionMailbox::Ingresses::Mailgun::InboundEmailsControllerTest < ActionDispatch::IntegrationTest
+ setup { ActionMailbox.ingress = :mailgun }
+
+ test "receiving an inbound email from Mailgun" do
+ assert_difference -> { ActionMailbox::InboundEmail.count }, +1 do
+ travel_to "2018-10-09 15:15:00 EDT"
+ post rails_mailgun_inbound_emails_url, params: {
+ timestamp: 1539112500,
+ token: "7VwW7k6Ak7zcTwoSoNm7aTtbk1g67MKAnsYLfUB7PdszbgR5Xi",
+ signature: "ef24c5225322217bb065b80bb54eb4f9206d764e3e16abab07f0a64d1cf477cc",
+ "body-mime" => file_fixture("../files/welcome.eml").read
+ }
+ end
+
+ assert_response :no_content
+
+ inbound_email = ActionMailbox::InboundEmail.last
+ assert_equal file_fixture("../files/welcome.eml").read, inbound_email.raw_email.download
+ assert_equal "0CB459E0-0336-41DA-BC88-E6E28C697DDB@37signals.com", inbound_email.message_id
+ end
+
+ test "receiving an inbound email from Mailgun with non UTF-8 characters" do
+ assert_difference -> { ActionMailbox::InboundEmail.count }, +1 do
+ travel_to "2018-10-09 15:15:00 EDT"
+ post rails_mailgun_inbound_emails_url, params: {
+ timestamp: 1539112500,
+ token: "7VwW7k6Ak7zcTwoSoNm7aTtbk1g67MKAnsYLfUB7PdszbgR5Xi",
+ signature: "ef24c5225322217bb065b80bb54eb4f9206d764e3e16abab07f0a64d1cf477cc",
+ "body-mime" => file_fixture("../files/invalid_utf.eml").read
+ }
+ end
+
+ assert_response :no_content
+
+ inbound_email = ActionMailbox::InboundEmail.last
+ assert_equal file_fixture("../files/invalid_utf.eml").binread, inbound_email.raw_email.download
+ assert_equal "05988AA6EC0D44318855A5E39E3B6F9E@jansterba.com", inbound_email.message_id
+ end
+
+ test "add X-Original-To to email from Mailgun" do
+ assert_difference -> { ActionMailbox::InboundEmail.count }, +1 do
+ travel_to "2018-10-09 15:15:00 EDT"
+ post rails_mailgun_inbound_emails_url, params: {
+ timestamp: 1539112500,
+ token: "7VwW7k6Ak7zcTwoSoNm7aTtbk1g67MKAnsYLfUB7PdszbgR5Xi",
+ signature: "ef24c5225322217bb065b80bb54eb4f9206d764e3e16abab07f0a64d1cf477cc",
+ "body-mime" => file_fixture("../files/welcome.eml").read,
+ recipient: "replies@example.com"
+ }
+ end
+
+ assert_response :no_content
+
+ inbound_email = ActionMailbox::InboundEmail.last
+ mail = Mail.from_source(inbound_email.raw_email.download)
+ assert_equal "replies@example.com", mail.header["X-Original-To"].decoded
+ end
+
+ test "rejecting a delayed inbound email from Mailgun" do
+ assert_no_difference -> { ActionMailbox::InboundEmail.count } do
+ travel_to "2018-10-09 15:26:00 EDT"
+ post rails_mailgun_inbound_emails_url, params: {
+ timestamp: 1539112500,
+ token: "7VwW7k6Ak7zcTwoSoNm7aTtbk1g67MKAnsYLfUB7PdszbgR5Xi",
+ signature: "ef24c5225322217bb065b80bb54eb4f9206d764e3e16abab07f0a64d1cf477cc",
+ "body-mime" => file_fixture("../files/welcome.eml").read
+ }
+ end
+
+ assert_response :unauthorized
+ end
+
+ test "rejecting a forged inbound email from Mailgun" do
+ assert_no_difference -> { ActionMailbox::InboundEmail.count } do
+ travel_to "2018-10-09 15:15:00 EDT"
+ post rails_mailgun_inbound_emails_url, params: {
+ timestamp: 1539112500,
+ token: "Zx8mJBiGmiiyyfWnho3zKyjCg2pxLARoCuBM7X9AKCioShGiMX",
+ signature: "ef24c5225322217bb065b80bb54eb4f9206d764e3e16abab07f0a64d1cf477cc",
+ "body-mime" => file_fixture("../files/welcome.eml").read
+ }
+ end
+
+ assert_response :unauthorized
+ end
+
+ test "raising when the configured Mailgun Signing key is nil" do
+ switch_key_to nil do
+ assert_raises ArgumentError do
+ travel_to "2018-10-09 15:15:00 EDT"
+ post rails_mailgun_inbound_emails_url, params: {
+ timestamp: 1539112500,
+ token: "7VwW7k6Ak7zcTwoSoNm7aTtbk1g67MKAnsYLfUB7PdszbgR5Xi",
+ signature: "ef24c5225322217bb065b80bb54eb4f9206d764e3e16abab07f0a64d1cf477cc",
+ "body-mime" => file_fixture("../files/welcome.eml").read
+ }
+ end
+ end
+ end
+
+ test "raising when the configured Mailgun Signing key is blank" do
+ switch_key_to "" do
+ assert_raises ArgumentError do
+ travel_to "2018-10-09 15:15:00 EDT"
+ post rails_mailgun_inbound_emails_url, params: {
+ timestamp: 1539112500,
+ token: "7VwW7k6Ak7zcTwoSoNm7aTtbk1g67MKAnsYLfUB7PdszbgR5Xi",
+ signature: "ef24c5225322217bb065b80bb54eb4f9206d764e3e16abab07f0a64d1cf477cc",
+ "body-mime" => file_fixture("../files/welcome.eml").read
+ }
+ end
+ end
+ end
+
+ private
+ def switch_key_to(new_key)
+ previous_key, ENV["MAILGUN_INGRESS_SIGNING_KEY"] = ENV["MAILGUN_INGRESS_SIGNING_KEY"], new_key
+ yield
+ ensure
+ ENV["MAILGUN_INGRESS_SIGNING_KEY"] = previous_key
+ end
+end
diff --git a/actionmailbox/test/controllers/ingresses/mandrill/inbound_emails_controller_test.rb b/actionmailbox/test/controllers/ingresses/mandrill/inbound_emails_controller_test.rb
new file mode 100644
index 0000000000000..7dc900aa68c46
--- /dev/null
+++ b/actionmailbox/test/controllers/ingresses/mandrill/inbound_emails_controller_test.rb
@@ -0,0 +1,66 @@
+# frozen_string_literal: true
+
+require "test_helper"
+
+ENV["MANDRILL_INGRESS_API_KEY"] = "1l9Qf7lutEf7h73VXfBwhw"
+
+class ActionMailbox::Ingresses::Mandrill::InboundEmailsControllerTest < ActionDispatch::IntegrationTest
+ setup do
+ ActionMailbox.ingress = :mandrill
+ @events = JSON.generate([{ event: "inbound", msg: { raw_msg: file_fixture("../files/welcome.eml").read } }])
+ end
+
+ test "verifying existence of Mandrill inbound route" do
+ # Mandrill uses a HEAD request to verify if a URL exists before creating the ingress webhook
+ head rails_mandrill_inbound_health_check_url
+ assert_response :ok
+ end
+
+ test "receiving an inbound email from Mandrill" do
+ assert_difference -> { ActionMailbox::InboundEmail.count }, +1 do
+ post rails_mandrill_inbound_emails_url,
+ headers: { "X-Mandrill-Signature" => "1bNbyqkMFL4VYIT5+RQCrPs/mRY=" }, params: { mandrill_events: @events }
+ end
+
+ assert_response :ok
+
+ inbound_email = ActionMailbox::InboundEmail.last
+ assert_equal file_fixture("../files/welcome.eml").read, inbound_email.raw_email.download
+ assert_equal "0CB459E0-0336-41DA-BC88-E6E28C697DDB@37signals.com", inbound_email.message_id
+ end
+
+ test "rejecting a forged inbound email from Mandrill" do
+ assert_no_difference -> { ActionMailbox::InboundEmail.count } do
+ post rails_mandrill_inbound_emails_url,
+ headers: { "X-Mandrill-Signature" => "forged" }, params: { mandrill_events: @events }
+ end
+
+ assert_response :unauthorized
+ end
+
+ test "raising when Mandrill API key is nil" do
+ switch_key_to nil do
+ assert_raises ArgumentError do
+ post rails_mandrill_inbound_emails_url,
+ headers: { "X-Mandrill-Signature" => "gldscd2tAb/G+DmpiLcwukkLrC4=" }, params: { mandrill_events: @events }
+ end
+ end
+ end
+
+ test "raising when Mandrill API key is blank" do
+ switch_key_to "" do
+ assert_raises ArgumentError do
+ post rails_mandrill_inbound_emails_url,
+ headers: { "X-Mandrill-Signature" => "gldscd2tAb/G+DmpiLcwukkLrC4=" }, params: { mandrill_events: @events }
+ end
+ end
+ end
+
+ private
+ def switch_key_to(new_key)
+ previous_key, ENV["MANDRILL_INGRESS_API_KEY"] = ENV["MANDRILL_INGRESS_API_KEY"], new_key
+ yield
+ ensure
+ ENV["MANDRILL_INGRESS_API_KEY"] = previous_key
+ end
+end
diff --git a/actionmailbox/test/controllers/ingresses/postmark/inbound_emails_controller_test.rb b/actionmailbox/test/controllers/ingresses/postmark/inbound_emails_controller_test.rb
new file mode 100644
index 0000000000000..c371ad0cbae80
--- /dev/null
+++ b/actionmailbox/test/controllers/ingresses/postmark/inbound_emails_controller_test.rb
@@ -0,0 +1,84 @@
+# frozen_string_literal: true
+
+require "test_helper"
+
+class ActionMailbox::Ingresses::Postmark::InboundEmailsControllerTest < ActionDispatch::IntegrationTest
+ setup { ActionMailbox.ingress = :postmark }
+
+ test "receiving an inbound email from Postmark" do
+ assert_difference -> { ActionMailbox::InboundEmail.count }, +1 do
+ post rails_postmark_inbound_emails_url,
+ headers: { authorization: credentials }, params: { RawEmail: file_fixture("../files/welcome.eml").read }
+ end
+
+ assert_response :no_content
+
+ inbound_email = ActionMailbox::InboundEmail.last
+ assert_equal file_fixture("../files/welcome.eml").read, inbound_email.raw_email.download
+ assert_equal "0CB459E0-0336-41DA-BC88-E6E28C697DDB@37signals.com", inbound_email.message_id
+ end
+
+ test "receiving an inbound email from Postmark with non UTF-8 characters" do
+ assert_difference -> { ActionMailbox::InboundEmail.count }, +1 do
+ post rails_postmark_inbound_emails_url,
+ headers: { authorization: credentials }, params: { RawEmail: file_fixture("../files/invalid_utf.eml").read }
+ end
+
+ assert_response :no_content
+
+ inbound_email = ActionMailbox::InboundEmail.last
+ assert_equal file_fixture("../files/invalid_utf.eml").binread, inbound_email.raw_email.download
+ assert_equal "05988AA6EC0D44318855A5E39E3B6F9E@jansterba.com", inbound_email.message_id
+ end
+
+ test "add X-Original-To to email from Postmark" do
+ assert_difference -> { ActionMailbox::InboundEmail.count }, +1 do
+ post rails_postmark_inbound_emails_url,
+ headers: { authorization: credentials }, params: {
+ RawEmail: file_fixture("../files/welcome.eml").read,
+ OriginalRecipient: "thisguy@domain.abcd",
+ }
+ end
+
+ assert_response :no_content
+
+ inbound_email = ActionMailbox::InboundEmail.last
+ mail = Mail.from_source(inbound_email.raw_email.download)
+ assert_equal "thisguy@domain.abcd", mail.header["X-Original-To"].decoded
+ end
+
+ test "rejecting when RawEmail param is missing" do
+ assert_no_difference -> { ActionMailbox::InboundEmail.count } do
+ post rails_postmark_inbound_emails_url,
+ headers: { authorization: credentials }, params: { From: "someone@example.com" }
+ end
+
+ assert_response :unprocessable_entity
+ end
+
+ test "rejecting an unauthorized inbound email from Postmark" do
+ assert_no_difference -> { ActionMailbox::InboundEmail.count } do
+ post rails_postmark_inbound_emails_url, params: { RawEmail: file_fixture("../files/welcome.eml").read }
+ end
+
+ assert_response :unauthorized
+ end
+
+ test "raising when the configured password is nil" do
+ switch_password_to nil do
+ assert_raises ArgumentError do
+ post rails_postmark_inbound_emails_url,
+ headers: { authorization: credentials }, params: { RawEmail: file_fixture("../files/welcome.eml").read }
+ end
+ end
+ end
+
+ test "raising when the configured password is blank" do
+ switch_password_to "" do
+ assert_raises ArgumentError do
+ post rails_postmark_inbound_emails_url,
+ headers: { authorization: credentials }, params: { RawEmail: file_fixture("../files/welcome.eml").read }
+ end
+ end
+ end
+end
diff --git a/actionmailbox/test/controllers/ingresses/relay/inbound_emails_controller_test.rb b/actionmailbox/test/controllers/ingresses/relay/inbound_emails_controller_test.rb
new file mode 100644
index 0000000000000..ac0a6fab75093
--- /dev/null
+++ b/actionmailbox/test/controllers/ingresses/relay/inbound_emails_controller_test.rb
@@ -0,0 +1,78 @@
+# frozen_string_literal: true
+
+require "test_helper"
+
+class ActionMailbox::Ingresses::Relay::InboundEmailsControllerTest < ActionDispatch::IntegrationTest
+ setup { ActionMailbox.ingress = :relay }
+
+ test "receiving an inbound email relayed from an SMTP server" do
+ assert_difference -> { ActionMailbox::InboundEmail.count }, +1 do
+ post rails_relay_inbound_emails_url, headers: { "Authorization" => credentials, "Content-Type" => "message/rfc822" },
+ params: file_fixture("../files/welcome.eml").read
+ end
+
+ assert_response :no_content
+
+ inbound_email = ActionMailbox::InboundEmail.last
+ assert_equal file_fixture("../files/welcome.eml").read, inbound_email.raw_email.download
+ assert_equal "0CB459E0-0336-41DA-BC88-E6E28C697DDB@37signals.com", inbound_email.message_id
+ end
+
+ test "receiving an inbound email relayed from an SMTP server with non UTF-8 characters" do
+ assert_difference -> { ActionMailbox::InboundEmail.count }, +1 do
+ post rails_relay_inbound_emails_url, headers: { "Authorization" => credentials, "Content-Type" => "message/rfc822" },
+ params: file_fixture("../files/invalid_utf.eml").read
+ end
+
+ assert_response :no_content
+
+ inbound_email = ActionMailbox::InboundEmail.last
+ assert_equal file_fixture("../files/invalid_utf.eml").binread, inbound_email.raw_email.download
+ assert_equal "05988AA6EC0D44318855A5E39E3B6F9E@jansterba.com", inbound_email.message_id
+ end
+
+ test "rejecting a request with no body" do
+ assert_no_difference -> { ActionMailbox::InboundEmail.count } do
+ post rails_relay_inbound_emails_url, headers: { "Authorization" => credentials, "Content-Type" => "message/rfc822" },
+ env: { "rack.input" => nil }
+ end
+
+ assert_response :unprocessable_entity
+ end
+
+ test "rejecting an unauthorized inbound email" do
+ assert_no_difference -> { ActionMailbox::InboundEmail.count } do
+ post rails_relay_inbound_emails_url, headers: { "Content-Type" => "message/rfc822" },
+ params: file_fixture("../files/welcome.eml").read
+ end
+
+ assert_response :unauthorized
+ end
+
+ test "rejecting an inbound email of an unsupported media type" do
+ assert_no_difference -> { ActionMailbox::InboundEmail.count } do
+ post rails_relay_inbound_emails_url, headers: { "Authorization" => credentials, "Content-Type" => "text/plain" },
+ params: file_fixture("../files/welcome.eml").read
+ end
+
+ assert_response :unsupported_media_type
+ end
+
+ test "raising when the configured password is nil" do
+ switch_password_to nil do
+ assert_raises ArgumentError do
+ post rails_relay_inbound_emails_url, headers: { "Authorization" => credentials, "Content-Type" => "message/rfc822" },
+ params: file_fixture("../files/welcome.eml").read
+ end
+ end
+ end
+
+ test "raising when the configured password is blank" do
+ switch_password_to "" do
+ assert_raises ArgumentError do
+ post rails_relay_inbound_emails_url, headers: { "Authorization" => credentials, "Content-Type" => "message/rfc822" },
+ params: file_fixture("../files/welcome.eml").read
+ end
+ end
+ end
+end
diff --git a/actionmailbox/test/controllers/ingresses/sendgrid/inbound_emails_controller_test.rb b/actionmailbox/test/controllers/ingresses/sendgrid/inbound_emails_controller_test.rb
new file mode 100644
index 0000000000000..3454585088e91
--- /dev/null
+++ b/actionmailbox/test/controllers/ingresses/sendgrid/inbound_emails_controller_test.rb
@@ -0,0 +1,75 @@
+# frozen_string_literal: true
+
+require "test_helper"
+
+class ActionMailbox::Ingresses::Sendgrid::InboundEmailsControllerTest < ActionDispatch::IntegrationTest
+ setup { ActionMailbox.ingress = :sendgrid }
+
+ test "receiving an inbound email from Sendgrid" do
+ assert_difference -> { ActionMailbox::InboundEmail.count }, +1 do
+ post rails_sendgrid_inbound_emails_url,
+ headers: { authorization: credentials }, params: { email: file_fixture("../files/welcome.eml").read }
+ end
+
+ assert_response :no_content
+
+ inbound_email = ActionMailbox::InboundEmail.last
+ assert_equal file_fixture("../files/welcome.eml").read, inbound_email.raw_email.download
+ assert_equal "0CB459E0-0336-41DA-BC88-E6E28C697DDB@37signals.com", inbound_email.message_id
+ end
+
+ test "receiving an inbound email from Sendgrid with non UTF-8 characters" do
+ assert_difference -> { ActionMailbox::InboundEmail.count }, +1 do
+ post rails_sendgrid_inbound_emails_url,
+ headers: { authorization: credentials }, params: { email: file_fixture("../files/invalid_utf.eml").read }
+ end
+
+ assert_response :no_content
+
+ inbound_email = ActionMailbox::InboundEmail.last
+ assert_equal file_fixture("../files/invalid_utf.eml").binread, inbound_email.raw_email.download
+ assert_equal "05988AA6EC0D44318855A5E39E3B6F9E@jansterba.com", inbound_email.message_id
+ end
+
+ test "add X-Original-To to email from Sendgrid" do
+ assert_difference -> { ActionMailbox::InboundEmail.count }, +1 do
+ post rails_sendgrid_inbound_emails_url,
+ headers: { authorization: credentials }, params: {
+ email: file_fixture("../files/welcome.eml").read,
+ envelope: "{\"to\":[\"replies@example.com\"],\"from\":\"jason@37signals.com\"}",
+ }
+ end
+
+ assert_response :no_content
+
+ inbound_email = ActionMailbox::InboundEmail.last
+ mail = Mail.from_source(inbound_email.raw_email.download)
+ assert_equal "replies@example.com", mail.header["X-Original-To"].decoded
+ end
+
+ test "rejecting an unauthorized inbound email from Sendgrid" do
+ assert_no_difference -> { ActionMailbox::InboundEmail.count } do
+ post rails_sendgrid_inbound_emails_url, params: { email: file_fixture("../files/welcome.eml").read }
+ end
+
+ assert_response :unauthorized
+ end
+
+ test "raising when the configured password is nil" do
+ switch_password_to nil do
+ assert_raises ArgumentError do
+ post rails_sendgrid_inbound_emails_url,
+ headers: { authorization: credentials }, params: { email: file_fixture("../files/welcome.eml").read }
+ end
+ end
+ end
+
+ test "raising when the configured password is blank" do
+ switch_password_to "" do
+ assert_raises ArgumentError do
+ post rails_sendgrid_inbound_emails_url,
+ headers: { authorization: credentials }, params: { email: file_fixture("../files/welcome.eml").read }
+ end
+ end
+ end
+end
diff --git a/actionmailbox/test/controllers/rails/action_mailbox/inbound_emails_controller_test.rb b/actionmailbox/test/controllers/rails/action_mailbox/inbound_emails_controller_test.rb
new file mode 100644
index 0000000000000..83c9e90dd5671
--- /dev/null
+++ b/actionmailbox/test/controllers/rails/action_mailbox/inbound_emails_controller_test.rb
@@ -0,0 +1,106 @@
+# frozen_string_literal: true
+
+require "test_helper"
+
+class Rails::Conductor::ActionMailbox::InboundEmailsControllerTest < ActionDispatch::IntegrationTest
+ test "create inbound email" do
+ with_rails_env("development") do
+ assert_difference -> { ActionMailbox::InboundEmail.count }, +1 do
+ post rails_conductor_inbound_emails_path, params: {
+ mail: {
+ from: "Jason Fried ",
+ to: "Replies ",
+ cc: "CC ",
+ bcc: "Bcc ",
+ in_reply_to: "<4e6e35f5a38b4_479f13bb90078178@small-app-01.mail>",
+ subject: "Hey there",
+ body: "How's it going?"
+ }
+ }
+ end
+
+ mail = ActionMailbox::InboundEmail.last.mail
+ assert_equal %w[ jason@37signals.com ], mail.from
+ assert_equal %w[ replies@example.com ], mail.to
+ assert_equal %w[ cc@example.com ], mail.cc
+ assert_equal %w[ bcc@example.com ], mail.bcc
+ assert_equal "4e6e35f5a38b4_479f13bb90078178@small-app-01.mail", mail.in_reply_to
+ assert_equal "Hey there", mail.subject
+ assert_equal "How's it going?", mail.body.decoded
+ end
+ end
+
+ test "create inbound email with bcc" do
+ with_rails_env("development") do
+ assert_difference -> { ActionMailbox::InboundEmail.count }, +1 do
+ post rails_conductor_inbound_emails_path, params: {
+ mail: {
+ from: "Jason Fried ",
+ bcc: "Replies ",
+ subject: "Hey there",
+ body: "How's it going?"
+ }
+ }
+ end
+
+ mail = ActionMailbox::InboundEmail.last.mail
+ assert_equal %w[ jason@37signals.com ], mail.from
+ assert_equal %w[ replies@example.com ], mail.bcc
+ assert_equal "Hey there", mail.subject
+ assert_equal "How's it going?", mail.body.decoded
+ end
+ end
+
+ test "create inbound email with attachments" do
+ with_rails_env("development") do
+ assert_difference -> { ActionMailbox::InboundEmail.count }, +1 do
+ post rails_conductor_inbound_emails_path, params: {
+ mail: {
+ from: "Jason Fried ",
+ to: "Replies ",
+ subject: "Let's debate some attachments",
+ body: "Let's talk about these images:",
+ attachments: [ fixture_file_upload("avatar1.jpeg"), fixture_file_upload("avatar2.jpeg") ]
+ }
+ }
+ end
+
+ mail = ActionMailbox::InboundEmail.last.mail
+ assert_equal "Let's talk about these images:", mail.text_part.decoded
+ assert_equal 2, mail.attachments.count
+ assert_equal %w[ avatar1.jpeg avatar2.jpeg ], mail.attachments.collect(&:filename)
+ end
+ end
+
+ test "create inbound email with empty attachment" do
+ with_rails_env("development") do
+ assert_difference -> { ActionMailbox::InboundEmail.count }, +1 do
+ post rails_conductor_inbound_emails_path, params: {
+ mail: {
+ from: "",
+ to: "",
+ cc: "",
+ bcc: "",
+ x_original_to: "",
+ subject: "",
+ in_reply_to: "",
+ body: "",
+ attachments: [ "" ],
+ }
+ }
+ end
+
+ mail = ActionMailbox::InboundEmail.last.mail
+ assert_equal 0, mail.attachments.count
+ end
+ end
+
+ private
+ def with_rails_env(env)
+ old_rails_env = Rails.env
+ Rails.env = env
+ yield
+ ensure
+ Rails.env = old_rails_env
+ end
+end
diff --git a/actionmailbox/test/dummy/Rakefile b/actionmailbox/test/dummy/Rakefile
new file mode 100644
index 0000000000000..9a5ea7383aa83
--- /dev/null
+++ b/actionmailbox/test/dummy/Rakefile
@@ -0,0 +1,6 @@
+# Add your own tasks in files placed in lib/tasks ending in .rake,
+# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
+
+require_relative "config/application"
+
+Rails.application.load_tasks
diff --git a/actionmailbox/test/dummy/app/assets/config/manifest.js b/actionmailbox/test/dummy/app/assets/config/manifest.js
new file mode 100644
index 0000000000000..591819335f0b2
--- /dev/null
+++ b/actionmailbox/test/dummy/app/assets/config/manifest.js
@@ -0,0 +1,2 @@
+//= link_tree ../images
+//= link_directory ../stylesheets .css
diff --git a/actionpack/test/fixtures/layout_tests/alt/layouts/alt.erb b/actionmailbox/test/dummy/app/assets/images/.keep
similarity index 100%
rename from actionpack/test/fixtures/layout_tests/alt/layouts/alt.erb
rename to actionmailbox/test/dummy/app/assets/images/.keep
diff --git a/actionmailbox/test/dummy/app/assets/stylesheets/application.css b/actionmailbox/test/dummy/app/assets/stylesheets/application.css
new file mode 100644
index 0000000000000..0ebd7fe8299eb
--- /dev/null
+++ b/actionmailbox/test/dummy/app/assets/stylesheets/application.css
@@ -0,0 +1,15 @@
+/*
+ * This is a manifest file that'll be compiled into application.css, which will include all the files
+ * listed below.
+ *
+ * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
+ * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
+ *
+ * You're free to add application-wide styles to this file and they'll appear at the bottom of the
+ * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
+ * files in this directory. Styles in this file should be added after the last require_* statement.
+ * It is generally better to create a new file per style scope.
+ *
+ *= require_tree .
+ *= require_self
+ */
diff --git a/actionmailbox/test/dummy/app/assets/stylesheets/scaffold.css b/actionmailbox/test/dummy/app/assets/stylesheets/scaffold.css
new file mode 100644
index 0000000000000..cd4f3de38d1f0
--- /dev/null
+++ b/actionmailbox/test/dummy/app/assets/stylesheets/scaffold.css
@@ -0,0 +1,80 @@
+body {
+ background-color: #fff;
+ color: #333;
+ margin: 33px;
+}
+
+body, p, ol, ul, td {
+ font-family: verdana, arial, helvetica, sans-serif;
+ font-size: 13px;
+ line-height: 18px;
+}
+
+pre {
+ background-color: #eee;
+ padding: 10px;
+ font-size: 11px;
+}
+
+a {
+ color: #000;
+}
+
+a:visited {
+ color: #666;
+}
+
+a:hover {
+ color: #fff;
+ background-color: #000;
+}
+
+th {
+ padding-bottom: 5px;
+}
+
+td {
+ padding: 0 5px 7px;
+}
+
+div.field,
+div.actions {
+ margin-bottom: 10px;
+}
+
+#notice {
+ color: green;
+}
+
+.field_with_errors {
+ padding: 2px;
+ background-color: red;
+ display: table;
+}
+
+#error_explanation {
+ width: 450px;
+ border: 2px solid red;
+ padding: 7px 7px 0;
+ margin-bottom: 20px;
+ background-color: #f0f0f0;
+}
+
+#error_explanation h2 {
+ text-align: left;
+ font-weight: bold;
+ padding: 5px 5px 5px 15px;
+ font-size: 12px;
+ margin: -7px -7px 0;
+ background-color: #c00;
+ color: #fff;
+}
+
+#error_explanation ul li {
+ font-size: 12px;
+ list-style: square;
+}
+
+label {
+ display: block;
+}
diff --git a/actionmailbox/test/dummy/app/channels/application_cable/channel.rb b/actionmailbox/test/dummy/app/channels/application_cable/channel.rb
new file mode 100644
index 0000000000000..d67269728300b
--- /dev/null
+++ b/actionmailbox/test/dummy/app/channels/application_cable/channel.rb
@@ -0,0 +1,4 @@
+module ApplicationCable
+ class Channel < ActionCable::Channel::Base
+ end
+end
diff --git a/actionmailbox/test/dummy/app/channels/application_cable/connection.rb b/actionmailbox/test/dummy/app/channels/application_cable/connection.rb
new file mode 100644
index 0000000000000..0ff5442f476f9
--- /dev/null
+++ b/actionmailbox/test/dummy/app/channels/application_cable/connection.rb
@@ -0,0 +1,4 @@
+module ApplicationCable
+ class Connection < ActionCable::Connection::Base
+ end
+end
diff --git a/actionmailbox/test/dummy/app/controllers/application_controller.rb b/actionmailbox/test/dummy/app/controllers/application_controller.rb
new file mode 100644
index 0000000000000..09705d12ab4df
--- /dev/null
+++ b/actionmailbox/test/dummy/app/controllers/application_controller.rb
@@ -0,0 +1,2 @@
+class ApplicationController < ActionController::Base
+end
diff --git a/actionpack/test/fixtures/sprockets/app/fonts/dir/font.ttf b/actionmailbox/test/dummy/app/controllers/concerns/.keep
similarity index 100%
rename from actionpack/test/fixtures/sprockets/app/fonts/dir/font.ttf
rename to actionmailbox/test/dummy/app/controllers/concerns/.keep
diff --git a/railties/guides/code/getting_started/app/helpers/application_helper.rb b/actionmailbox/test/dummy/app/helpers/application_helper.rb
similarity index 100%
rename from railties/guides/code/getting_started/app/helpers/application_helper.rb
rename to actionmailbox/test/dummy/app/helpers/application_helper.rb
diff --git a/actionpack/test/fixtures/sprockets/app/fonts/font.ttf b/actionmailbox/test/dummy/app/javascript/packs/application.js
similarity index 100%
rename from actionpack/test/fixtures/sprockets/app/fonts/font.ttf
rename to actionmailbox/test/dummy/app/javascript/packs/application.js
diff --git a/actionmailbox/test/dummy/app/jobs/application_job.rb b/actionmailbox/test/dummy/app/jobs/application_job.rb
new file mode 100644
index 0000000000000..d394c3d106230
--- /dev/null
+++ b/actionmailbox/test/dummy/app/jobs/application_job.rb
@@ -0,0 +1,7 @@
+class ApplicationJob < ActiveJob::Base
+ # Automatically retry jobs that encountered a deadlock
+ # retry_on ActiveRecord::Deadlocked
+
+ # Most jobs are safe to ignore if the underlying records are no longer available
+ # discard_on ActiveJob::DeserializationError
+end
diff --git a/actionmailbox/test/dummy/app/mailboxes/application_mailbox.rb b/actionmailbox/test/dummy/app/mailboxes/application_mailbox.rb
new file mode 100644
index 0000000000000..be51eb363999d
--- /dev/null
+++ b/actionmailbox/test/dummy/app/mailboxes/application_mailbox.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+class ApplicationMailbox < ActionMailbox::Base
+ # routing /something/i => :somewhere
+end
diff --git a/actionmailbox/test/dummy/app/mailboxes/messages_mailbox.rb b/actionmailbox/test/dummy/app/mailboxes/messages_mailbox.rb
new file mode 100644
index 0000000000000..1a046ee13d959
--- /dev/null
+++ b/actionmailbox/test/dummy/app/mailboxes/messages_mailbox.rb
@@ -0,0 +1,4 @@
+class MessagesMailbox < ApplicationMailbox
+ def process
+ end
+end
diff --git a/actionmailbox/test/dummy/app/mailers/application_mailer.rb b/actionmailbox/test/dummy/app/mailers/application_mailer.rb
new file mode 100644
index 0000000000000..3c34c8148f105
--- /dev/null
+++ b/actionmailbox/test/dummy/app/mailers/application_mailer.rb
@@ -0,0 +1,4 @@
+class ApplicationMailer < ActionMailer::Base
+ default from: "from@example.com"
+ layout "mailer"
+end
diff --git a/actionmailbox/test/dummy/app/models/application_record.rb b/actionmailbox/test/dummy/app/models/application_record.rb
new file mode 100644
index 0000000000000..b63caeb8a5c4a
--- /dev/null
+++ b/actionmailbox/test/dummy/app/models/application_record.rb
@@ -0,0 +1,3 @@
+class ApplicationRecord < ActiveRecord::Base
+ primary_abstract_class
+end
diff --git a/actionpack/test/fixtures/sprockets/app/javascripts/dir/xmlhr.js b/actionmailbox/test/dummy/app/models/concerns/.keep
similarity index 100%
rename from actionpack/test/fixtures/sprockets/app/javascripts/dir/xmlhr.js
rename to actionmailbox/test/dummy/app/models/concerns/.keep
diff --git a/actionmailbox/test/dummy/app/views/layouts/application.html.erb b/actionmailbox/test/dummy/app/views/layouts/application.html.erb
new file mode 100644
index 0000000000000..f72b4ef0e7316
--- /dev/null
+++ b/actionmailbox/test/dummy/app/views/layouts/application.html.erb
@@ -0,0 +1,15 @@
+
+
+
+ Dummy
+
+ <%= csrf_meta_tags %>
+ <%= csp_meta_tag %>
+
+ <%= stylesheet_link_tag "application" %>
+
+
+
+ <%= yield %>
+
+
diff --git a/actionmailbox/test/dummy/app/views/layouts/mailer.html.erb b/actionmailbox/test/dummy/app/views/layouts/mailer.html.erb
new file mode 100644
index 0000000000000..3aac9002edca7
--- /dev/null
+++ b/actionmailbox/test/dummy/app/views/layouts/mailer.html.erb
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+ <%= yield %>
+
+
diff --git a/actionpack/test/fixtures/layouts/_yield_only.erb b/actionmailbox/test/dummy/app/views/layouts/mailer.text.erb
similarity index 100%
rename from actionpack/test/fixtures/layouts/_yield_only.erb
rename to actionmailbox/test/dummy/app/views/layouts/mailer.text.erb
diff --git a/actionmailbox/test/dummy/bin/rails b/actionmailbox/test/dummy/bin/rails
new file mode 100755
index 0000000000000..efc0377492f7e
--- /dev/null
+++ b/actionmailbox/test/dummy/bin/rails
@@ -0,0 +1,4 @@
+#!/usr/bin/env ruby
+APP_PATH = File.expand_path("../config/application", __dir__)
+require_relative "../config/boot"
+require "rails/commands"
diff --git a/actionmailbox/test/dummy/bin/rake b/actionmailbox/test/dummy/bin/rake
new file mode 100755
index 0000000000000..4fbf10b960ef7
--- /dev/null
+++ b/actionmailbox/test/dummy/bin/rake
@@ -0,0 +1,4 @@
+#!/usr/bin/env ruby
+require_relative "../config/boot"
+require "rake"
+Rake.application.run
diff --git a/actionmailbox/test/dummy/bin/setup b/actionmailbox/test/dummy/bin/setup
new file mode 100755
index 0000000000000..3cd5a9d7801ca
--- /dev/null
+++ b/actionmailbox/test/dummy/bin/setup
@@ -0,0 +1,33 @@
+#!/usr/bin/env ruby
+require "fileutils"
+
+# path to your application root.
+APP_ROOT = File.expand_path("..", __dir__)
+
+def system!(*args)
+ system(*args, exception: true)
+end
+
+FileUtils.chdir APP_ROOT do
+ # This script is a way to set up or update your development environment automatically.
+ # This script is idempotent, so that you can run it at any time and get an expectable outcome.
+ # Add necessary setup steps to this file.
+
+ puts "== Installing dependencies =="
+ system! "gem install bundler --conservative"
+ system("bundle check") || system!("bundle install")
+
+ # puts "\n== Copying sample files =="
+ # unless File.exist?("config/database.yml")
+ # FileUtils.cp "config/database.yml.sample", "config/database.yml"
+ # end
+
+ puts "\n== Preparing database =="
+ system! "bin/rails db:prepare"
+
+ puts "\n== Removing old logs and tempfiles =="
+ system! "bin/rails log:clear tmp:clear"
+
+ puts "\n== Restarting application server =="
+ system! "bin/rails restart"
+end
diff --git a/actionmailbox/test/dummy/config.ru b/actionmailbox/test/dummy/config.ru
new file mode 100644
index 0000000000000..4a3c09a6889a9
--- /dev/null
+++ b/actionmailbox/test/dummy/config.ru
@@ -0,0 +1,6 @@
+# This file is used by Rack-based servers to start the application.
+
+require_relative "config/environment"
+
+run Rails.application
+Rails.application.load_server
diff --git a/actionmailbox/test/dummy/config/application.rb b/actionmailbox/test/dummy/config/application.rb
new file mode 100644
index 0000000000000..8154f557052d9
--- /dev/null
+++ b/actionmailbox/test/dummy/config/application.rb
@@ -0,0 +1,27 @@
+require_relative "boot"
+
+require "rails/all"
+
+# Require the gems listed in Gemfile, including any gems
+# you've limited to :test, :development, or :production.
+Bundler.require(*Rails.groups)
+
+module Dummy
+ class Application < Rails::Application
+ config.load_defaults Rails::VERSION::STRING.to_f
+
+ # For compatibility with applications that use this config
+ config.action_controller.include_all_helpers = false
+
+ config.active_record.table_name_prefix = 'prefix_'
+ config.active_record.table_name_suffix = '_suffix'
+
+ # Configuration for the application, engines, and railties goes here.
+ #
+ # These settings can be overridden in specific environments using the files
+ # in config/environments, which are processed later.
+ #
+ # config.time_zone = "Central Time (US & Canada)"
+ # config.eager_load_paths << Rails.root.join("extras")
+ end
+end
diff --git a/actionmailbox/test/dummy/config/boot.rb b/actionmailbox/test/dummy/config/boot.rb
new file mode 100644
index 0000000000000..d5e0f0fdc80b8
--- /dev/null
+++ b/actionmailbox/test/dummy/config/boot.rb
@@ -0,0 +1,5 @@
+# Set up gems listed in the Gemfile.
+ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../../../Gemfile", __dir__)
+
+require "bundler/setup" if File.exist?(ENV["BUNDLE_GEMFILE"])
+$LOAD_PATH.unshift File.expand_path("../../../lib", __dir__)
diff --git a/actionmailbox/test/dummy/config/cable.yml b/actionmailbox/test/dummy/config/cable.yml
new file mode 100644
index 0000000000000..98367f8954247
--- /dev/null
+++ b/actionmailbox/test/dummy/config/cable.yml
@@ -0,0 +1,10 @@
+development:
+ adapter: async
+
+test:
+ adapter: test
+
+production:
+ adapter: redis
+ url: <%= ENV.fetch("http://github.com/REDIS_URL") { "redis://localhost:6379/1" } %>
+ channel_prefix: dummy_production
diff --git a/actionmailbox/test/dummy/config/database.yml b/actionmailbox/test/dummy/config/database.yml
new file mode 100644
index 0000000000000..796466ba23eed
--- /dev/null
+++ b/actionmailbox/test/dummy/config/database.yml
@@ -0,0 +1,25 @@
+# SQLite. Versions 3.8.0 and up are supported.
+# gem install sqlite3
+#
+# Ensure the SQLite 3 gem is defined in your Gemfile
+# gem "sqlite3"
+#
+default: &default
+ adapter: sqlite3
+ pool: <%= ENV.fetch("http://github.com/RAILS_MAX_THREADS") { 5 } %>
+ timeout: 5000
+
+development:
+ <<: *default
+ database: storage/development.sqlite3
+
+# Warning: The database defined as "test" will be erased and
+# re-generated from your development database when you run "rake".
+# Do not set this db to the same as development or production.
+test:
+ <<: *default
+ database: storage/test.sqlite3
+
+production:
+ <<: *default
+ database: storage/production.sqlite3
diff --git a/actionmailbox/test/dummy/config/environment.rb b/actionmailbox/test/dummy/config/environment.rb
new file mode 100644
index 0000000000000..cac5315775258
--- /dev/null
+++ b/actionmailbox/test/dummy/config/environment.rb
@@ -0,0 +1,5 @@
+# Load the Rails application.
+require_relative "application"
+
+# Initialize the Rails application.
+Rails.application.initialize!
diff --git a/actionmailbox/test/dummy/config/environments/development.rb b/actionmailbox/test/dummy/config/environments/development.rb
new file mode 100644
index 0000000000000..4d134bb530348
--- /dev/null
+++ b/actionmailbox/test/dummy/config/environments/development.rb
@@ -0,0 +1,73 @@
+require "active_support/core_ext/integer/time"
+
+Rails.application.configure do
+ # Settings specified here will take precedence over those in config/application.rb.
+
+ # In the development environment your application's code is reloaded any time
+ # it changes. This slows down response time but is perfect for development
+ # since you don't have to restart the web server when you make code changes.
+ config.enable_reloading = true
+
+ # Do not eager load code on boot.
+ config.eager_load = false
+
+ # Show full error reports.
+ config.consider_all_requests_local = true
+
+ # Enable server timing
+ config.server_timing = true
+
+ # Enable/disable caching. By default caching is disabled.
+ # Run rails dev:cache to toggle caching.
+ if Rails.root.join("tmp/caching-dev.txt").exist?
+ config.action_controller.perform_caching = true
+ config.action_controller.enable_fragment_cache_logging = true
+
+ config.cache_store = :memory_store
+ config.public_file_server.headers = {
+ "Cache-Control" => "public, max-age=#{2.days.to_i}"
+ }
+ else
+ config.action_controller.perform_caching = false
+
+ config.cache_store = :null_store
+ end
+
+ # Store uploaded files on the local file system (see config/storage.yml for options).
+ config.active_storage.service = :local
+
+ # Don't care if the mailer can't send.
+ config.action_mailer.raise_delivery_errors = false
+
+ config.action_mailer.perform_caching = false
+
+ # Print deprecation notices to the Rails logger.
+ config.active_support.deprecation = :log
+
+ # Raise exceptions for disallowed deprecations.
+ config.active_support.disallowed_deprecation = :raise
+
+ # Tell Active Support which deprecation messages to disallow.
+ config.active_support.disallowed_deprecation_warnings = []
+
+ # Raise an error on page load if there are pending migrations.
+ config.active_record.migration_error = :page_load
+
+ # Highlight code that triggered database queries in logs.
+ config.active_record.verbose_query_logs = true
+
+ # Suppress logger output for asset requests.
+ config.assets.quiet = true
+
+ # Raises error for missing translations.
+ # config.i18n.raise_on_missing_translations = true
+
+ # Annotate rendered view with file names.
+ # config.action_view.annotate_rendered_view_with_filenames = true
+
+ # Uncomment if you wish to allow Action Cable access from any origin.
+ # config.action_cable.disable_request_forgery_protection = true
+
+ # Raise error when a before_action's only/except options reference missing actions
+ config.action_controller.raise_on_missing_callback_actions = true
+end
diff --git a/actionmailbox/test/dummy/config/environments/production.rb b/actionmailbox/test/dummy/config/environments/production.rb
new file mode 100644
index 0000000000000..999e332a7e2be
--- /dev/null
+++ b/actionmailbox/test/dummy/config/environments/production.rb
@@ -0,0 +1,89 @@
+require "active_support/core_ext/integer/time"
+
+Rails.application.configure do
+ # Settings specified here will take precedence over those in config/application.rb.
+
+ # Code is not reloaded between requests.
+ config.enable_reloading = false
+
+ # Eager load code on boot. This eager loads most of Rails and
+ # your application in memory, allowing both threaded web servers
+ # and those relying on copy on write to perform better.
+ # Rake tasks automatically ignore this option for performance.
+ config.eager_load = true
+
+ # Full error reports are disabled and caching is turned on.
+ config.consider_all_requests_local = false
+ config.action_controller.perform_caching = true
+
+ # Ensures that a master key has been made available in either ENV["RAILS_MASTER_KEY"]
+ # or in config/master.key. This key is used to decrypt credentials (and other encrypted files).
+ # config.require_master_key = true
+
+ # Disable serving static files from `public/`, relying on NGINX/Apache to do so instead.
+ # config.public_file_server.enabled = false
+
+ # Compress CSS using a preprocessor.
+ # config.assets.css_compressor = :sass
+
+ # Do not fall back to assets pipeline if a precompiled asset is missed.
+ config.assets.compile = false
+
+ # Enable serving of images, stylesheets, and JavaScripts from an asset server.
+ # config.asset_host = "http://assets.example.com/"
+
+ # Specifies the header that your server uses for sending files.
+ # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for Apache
+ # config.action_dispatch.x_sendfile_header = "X-Accel-Redirect" # for NGINX
+
+ # Store uploaded files on the local file system (see config/storage.yml for options).
+ config.active_storage.service = :local
+
+ # Mount Action Cable outside main process or domain.
+ # config.action_cable.mount_path = nil
+ # config.action_cable.url = "wss://example.com/cable"
+ # config.action_cable.allowed_request_origins = [ "http://example.com/", /http:\/\/example.*/ ]
+
+ # Assume all access to the app is happening through a SSL-terminating reverse proxy.
+ # Can be used together with config.force_ssl for Strict-Transport-Security and secure cookies.
+ # config.assume_ssl = true
+
+ # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
+ # config.force_ssl = true
+
+ # Log to STDOUT by default
+ config.logger = ActiveSupport::Logger.new(STDOUT)
+ .tap { |logger| logger.formatter = ::Logger::Formatter.new }
+ .then { |logger| ActiveSupport::TaggedLogging.new(logger) }
+
+ # Prepend all log lines with the following tags.
+ config.log_tags = [ :request_id ]
+
+ # "info" includes generic and useful information about system operation, but avoids logging too much
+ # information to avoid inadvertent exposure of personally identifiable information (PII). Use "debug"
+ # for everything.
+ config.log_level = ENV.fetch("http://github.com/RAILS_LOG_LEVEL") { "info" }
+
+ # Use a different cache store in production.
+ # config.cache_store = :mem_cache_store
+
+ # Use a real queuing backend for Active Job (and separate queues per environment).
+ # config.active_job.queue_adapter = :resque
+ # config.active_job.queue_name_prefix = "dummy_production"
+
+ config.action_mailer.perform_caching = false
+
+ # Ignore bad email addresses and do not raise email delivery errors.
+ # Set this to true and configure the email server for immediate delivery to raise delivery errors.
+ # config.action_mailer.raise_delivery_errors = false
+
+ # Enable locale fallbacks for I18n (makes lookups for any locale fall back to
+ # the I18n.default_locale when a translation cannot be found).
+ config.i18n.fallbacks = true
+
+ # Don't log any deprecations.
+ config.active_support.report_deprecations = false
+
+ # Do not dump schema after migrations.
+ config.active_record.dump_schema_after_migration = false
+end
diff --git a/actionmailbox/test/dummy/config/environments/test.rb b/actionmailbox/test/dummy/config/environments/test.rb
new file mode 100644
index 0000000000000..5b1b89421f004
--- /dev/null
+++ b/actionmailbox/test/dummy/config/environments/test.rb
@@ -0,0 +1,63 @@
+require "active_support/core_ext/integer/time"
+
+# The test environment is used exclusively to run your application's
+# test suite. You never need to work with it otherwise. Remember that
+# your test database is "scratch space" for the test suite and is wiped
+# and recreated between test runs. Don't rely on the data there!
+
+Rails.application.configure do
+ # Settings specified here will take precedence over those in config/application.rb.
+
+ # While tests run files are not watched, reloading is not necessary.
+ config.enable_reloading = false
+
+ # Eager loading loads your entire application. When running a single test locally,
+ # this is usually not necessary, and can slow down your test suite. However, it's
+ # recommended that you enable it in continuous integration systems to ensure eager
+ # loading is working properly before deploying your code.
+ config.eager_load = ENV["CI"].present?
+
+ # Configure public file server for tests with Cache-Control for performance.
+ config.public_file_server.headers = {
+ "Cache-Control" => "public, max-age=#{1.hour.to_i}"
+ }
+
+ # Show full error reports and disable caching.
+ config.consider_all_requests_local = true
+ config.action_controller.perform_caching = false
+ config.cache_store = :null_store
+
+ # Raise exceptions instead of rendering exception templates.
+ config.action_dispatch.show_exceptions = :rescuable
+
+ # Disable request forgery protection in test environment.
+ config.action_controller.allow_forgery_protection = false
+
+ # Store uploaded files on the local file system in a temporary directory.
+ config.active_storage.service = :test
+
+ config.action_mailer.perform_caching = false
+
+ # Tell Action Mailer not to deliver emails to the real world.
+ # The :test delivery method accumulates sent emails in the
+ # ActionMailer::Base.deliveries array.
+ config.action_mailer.delivery_method = :test
+
+ # Print deprecation notices to the stderr.
+ config.active_support.deprecation = :stderr
+
+ # Raise exceptions for disallowed deprecations.
+ config.active_support.disallowed_deprecation = :raise
+
+ # Tell Active Support which deprecation messages to disallow.
+ config.active_support.disallowed_deprecation_warnings = []
+
+ # Raises error for missing translations.
+ # config.i18n.raise_on_missing_translations = true
+
+ # Annotate rendered view with file names.
+ # config.action_view.annotate_rendered_view_with_filenames = true
+
+ # Raise error when a before_action's only/except options reference missing actions
+ config.action_controller.raise_on_missing_callback_actions = true
+end
diff --git a/actionmailbox/test/dummy/config/initializers/assets.rb b/actionmailbox/test/dummy/config/initializers/assets.rb
new file mode 100644
index 0000000000000..2eeef966fe872
--- /dev/null
+++ b/actionmailbox/test/dummy/config/initializers/assets.rb
@@ -0,0 +1,12 @@
+# Be sure to restart your server when you modify this file.
+
+# Version of your assets, change this if you want to expire all your assets.
+Rails.application.config.assets.version = "1.0"
+
+# Add additional assets to the asset load path.
+# Rails.application.config.assets.paths << Emoji.images_path
+
+# Precompile additional assets.
+# application.js, application.css, and all non-JS/CSS in the app/assets
+# folder are already added.
+# Rails.application.config.assets.precompile += %w( admin.js admin.css )
diff --git a/actionmailbox/test/dummy/config/initializers/content_security_policy.rb b/actionmailbox/test/dummy/config/initializers/content_security_policy.rb
new file mode 100644
index 0000000000000..b3076b38fe143
--- /dev/null
+++ b/actionmailbox/test/dummy/config/initializers/content_security_policy.rb
@@ -0,0 +1,25 @@
+# Be sure to restart your server when you modify this file.
+
+# Define an application-wide content security policy.
+# See the Securing Rails Applications Guide for more information:
+# https://guides.rubyonrails.org/security.html#content-security-policy-header
+
+# Rails.application.configure do
+# config.content_security_policy do |policy|
+# policy.default_src :self, :https
+# policy.font_src :self, :https, :data
+# policy.img_src :self, :https, :data
+# policy.object_src :none
+# policy.script_src :self, :https
+# policy.style_src :self, :https
+# # Specify URI for violation reports
+# # policy.report_uri "/csp-violation-report-endpoint"
+# end
+#
+# # Generate session nonces for permitted importmap, inline scripts, and inline styles.
+# config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s }
+# config.content_security_policy_nonce_directives = %w(script-src style-src)
+#
+# # Report violations without enforcing the policy.
+# # config.content_security_policy_report_only = true
+# end
diff --git a/actionmailbox/test/dummy/config/initializers/filter_parameter_logging.rb b/actionmailbox/test/dummy/config/initializers/filter_parameter_logging.rb
new file mode 100644
index 0000000000000..adc6568ce8372
--- /dev/null
+++ b/actionmailbox/test/dummy/config/initializers/filter_parameter_logging.rb
@@ -0,0 +1,8 @@
+# Be sure to restart your server when you modify this file.
+
+# Configure parameters to be filtered from the log file. Use this to limit dissemination of
+# sensitive information. See the ActiveSupport::ParameterFilter documentation for supported
+# notations and behaviors.
+Rails.application.config.filter_parameters += [
+ :passw, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn
+]
diff --git a/actionmailbox/test/dummy/config/initializers/inflections.rb b/actionmailbox/test/dummy/config/initializers/inflections.rb
new file mode 100644
index 0000000000000..3860f659ead02
--- /dev/null
+++ b/actionmailbox/test/dummy/config/initializers/inflections.rb
@@ -0,0 +1,16 @@
+# Be sure to restart your server when you modify this file.
+
+# Add new inflection rules using the following format. Inflections
+# are locale specific, and you may define rules for as many different
+# locales as you wish. All of these examples are active by default:
+# ActiveSupport::Inflector.inflections(:en) do |inflect|
+# inflect.plural /^(ox)$/i, "\\1en"
+# inflect.singular /^(ox)en/i, "\\1"
+# inflect.irregular "person", "people"
+# inflect.uncountable %w( fish sheep )
+# end
+
+# These inflection rules are supported but not enabled by default:
+# ActiveSupport::Inflector.inflections(:en) do |inflect|
+# inflect.acronym "RESTful"
+# end
diff --git a/actionmailbox/test/dummy/config/locales/en.yml b/actionmailbox/test/dummy/config/locales/en.yml
new file mode 100644
index 0000000000000..6c349ae5e3743
--- /dev/null
+++ b/actionmailbox/test/dummy/config/locales/en.yml
@@ -0,0 +1,31 @@
+# Files in the config/locales directory are used for internationalization and
+# are automatically loaded by Rails. If you want to use locales other than
+# English, add the necessary files in this directory.
+#
+# To use the locales, use `I18n.t`:
+#
+# I18n.t "hello"
+#
+# In views, this is aliased to just `t`:
+#
+# <%= t("hello") %>
+#
+# To use a different locale, set it with `I18n.locale`:
+#
+# I18n.locale = :es
+#
+# This would use the information in config/locales/es.yml.
+#
+# To learn more about the API, please read the Rails Internationalization guide
+# at https://guides.rubyonrails.org/i18n.html.
+#
+# Be aware that YAML interprets the following case-insensitive strings as
+# booleans: `true`, `false`, `on`, `off`, `yes`, `no`. Therefore, these strings
+# must be quoted to be interpreted as strings. For example:
+#
+# en:
+# "yes": yup
+# enabled: "ON"
+
+en:
+ hello: "Hello world"
diff --git a/actionmailbox/test/dummy/config/puma.rb b/actionmailbox/test/dummy/config/puma.rb
new file mode 100644
index 0000000000000..09a5c4b7865e7
--- /dev/null
+++ b/actionmailbox/test/dummy/config/puma.rb
@@ -0,0 +1,38 @@
+# This configuration file will be evaluated by Puma. The top-level methods that
+# are invoked here are part of Puma's configuration DSL. For more information
+# about methods provided by the DSL, see https://puma.io/puma/Puma/DSL.html.
+
+# Puma can serve each request in a thread from an internal thread pool.
+# The `threads` method setting takes two numbers: a minimum and maximum.
+# Any libraries that use thread pools should be configured to match
+# the maximum value specified for Puma. Default is set to 5 threads for minimum
+# and maximum; this matches the default thread size of Active Record.
+max_threads_count = ENV.fetch("http://github.com/RAILS_MAX_THREADS") { 5 }
+min_threads_count = ENV.fetch("http://github.com/RAILS_MIN_THREADS") { max_threads_count }
+threads min_threads_count, max_threads_count
+
+# Specifies that the worker count should equal the number of processors in production.
+if ENV["RAILS_ENV"] == "production"
+ worker_count = Integer(ENV.fetch("http://github.com/WEB_CONCURRENCY") { Concurrent.physical_processor_count })
+ workers worker_count if worker_count > 1
+end
+
+# Specifies the `worker_timeout` threshold that Puma will use to wait before
+# terminating a worker in development environments.
+worker_timeout 3600 if ENV.fetch("http://github.com/RAILS_ENV", "development") == "development"
+
+# Specifies the `port` that Puma will listen on to receive requests; default is 3000.
+port ENV.fetch("http://github.com/PORT") { 3000 }
+
+# Specifies the `environment` that Puma will run in.
+environment ENV.fetch("http://github.com/RAILS_ENV") { "development" }
+
+# Specifies the `pidfile` that Puma will use.
+if ENV["PIDFILE"]
+ pidfile ENV["PIDFILE"]
+else
+ pidfile "tmp/pids/server.pid" if ENV.fetch("http://github.com/RAILS_ENV", "development") == "development"
+end
+
+# Allow puma to be restarted by `bin/rails restart` command.
+plugin :tmp_restart
diff --git a/actionmailbox/test/dummy/config/routes.rb b/actionmailbox/test/dummy/config/routes.rb
new file mode 100644
index 0000000000000..1daf9a4121a8b
--- /dev/null
+++ b/actionmailbox/test/dummy/config/routes.rb
@@ -0,0 +1,2 @@
+Rails.application.routes.draw do
+end
diff --git a/actionmailbox/test/dummy/config/storage.yml b/actionmailbox/test/dummy/config/storage.yml
new file mode 100644
index 0000000000000..c26dd89d229af
--- /dev/null
+++ b/actionmailbox/test/dummy/config/storage.yml
@@ -0,0 +1,38 @@
+test:
+ service: Disk
+ root: <%= Rails.root.join("tmp/storage") %>
+
+local:
+ service: Disk
+ root: <%= Rails.root.join("storage") %>
+
+test_email:
+ service: Disk
+ root: <%= Rails.root.join("tmp/storage_email") %>
+
+# Use bin/rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key)
+# amazon:
+# service: S3
+# access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %>
+# secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %>
+# region: us-east-1
+# bucket: your_own_bucket-<%= Rails.env %>
+
+# Remember not to checkin your GCS keyfile to a repository
+# google:
+# service: GCS
+# project: your_project
+# credentials: <%= Rails.root.join("path/to/gcs.keyfile") %>
+# bucket: your_own_bucket-<%= Rails.env %>
+
+# Use bin/rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key)
+# microsoft:
+# service: AzureStorage
+# storage_account_name: your_account_name
+# storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %>
+# container: your_container_name-<%= Rails.env %>
+
+# mirror:
+# service: Mirror
+# primary: local
+# mirrors: [ amazon, google, microsoft ]
diff --git a/actionmailbox/test/dummy/db/migrate/20180208205311_create_action_mailbox_tables.rb b/actionmailbox/test/dummy/db/migrate/20180208205311_create_action_mailbox_tables.rb
new file mode 100644
index 0000000000000..2a1e2f7a8e2e1
--- /dev/null
+++ b/actionmailbox/test/dummy/db/migrate/20180208205311_create_action_mailbox_tables.rb
@@ -0,0 +1,19 @@
+class CreateActionMailboxTables < ActiveRecord::Migration[6.0]
+ def change
+ create_table :action_mailbox_inbound_emails, id: primary_key_type do |t|
+ t.integer :status, default: 0, null: false
+ t.string :message_id, null: false
+ t.string :message_checksum, null: false
+
+ t.timestamps
+
+ t.index [ :message_id, :message_checksum ], name: "index_action_mailbox_inbound_emails_uniqueness", unique: true
+ end
+ end
+
+ private
+ def primary_key_type
+ config = Rails.configuration.generators
+ config.options[config.orm][:primary_key_type] || :primary_key
+ end
+end
diff --git a/actionmailbox/test/dummy/db/migrate/20180212164506_create_active_storage_tables.active_storage.rb b/actionmailbox/test/dummy/db/migrate/20180212164506_create_active_storage_tables.active_storage.rb
new file mode 100644
index 0000000000000..87798267b4764
--- /dev/null
+++ b/actionmailbox/test/dummy/db/migrate/20180212164506_create_active_storage_tables.active_storage.rb
@@ -0,0 +1,36 @@
+# This migration comes from active_storage (originally 20170806125915)
+class CreateActiveStorageTables < ActiveRecord::Migration[5.2]
+ def change
+ create_table :active_storage_blobs do |t|
+ t.string :key, null: false
+ t.string :filename, null: false
+ t.string :content_type
+ t.text :metadata
+ t.string :service_name, null: false
+ t.bigint :byte_size, null: false
+ t.string :checksum, null: false
+ t.datetime :created_at, null: false
+
+ t.index [ :key ], unique: true
+ end
+
+ create_table :active_storage_attachments do |t|
+ t.string :name, null: false
+ t.references :record, null: false, polymorphic: true, index: false
+ t.references :blob, null: false
+
+ t.datetime :created_at, null: false
+
+ t.index [ :record_type, :record_id, :name, :blob_id ], name: "index_active_storage_attachments_uniqueness", unique: true
+ t.foreign_key :active_storage_blobs, column: :blob_id
+ end
+
+ create_table :active_storage_variant_records do |t|
+ t.belongs_to :blob, null: false, index: false
+ t.string :variation_digest, null: false
+
+ t.index %i[ blob_id variation_digest ], name: "index_active_storage_variant_records_uniqueness", unique: true
+ t.foreign_key :active_storage_blobs, column: :blob_id
+ end
+ end
+end
diff --git a/actionmailbox/test/dummy/db/schema.rb b/actionmailbox/test/dummy/db/schema.rb
new file mode 100644
index 0000000000000..acbc0de9d3715
--- /dev/null
+++ b/actionmailbox/test/dummy/db/schema.rb
@@ -0,0 +1,53 @@
+# This file is auto-generated from the current state of the database. Instead
+# of editing this file, please use the migrations feature of Active Record to
+# incrementally modify your database, and then regenerate this schema definition.
+#
+# This file is the source Rails uses to define your schema when running `bin/rails
+# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to
+# be faster and is potentially less error prone than running all of your
+# migrations from scratch. Old migrations may fail to apply correctly if those
+# migrations use external dependencies or application code.
+#
+# It's strongly recommended that you check this file into your version control system.
+
+ActiveRecord::Schema[8.1].define(version: 2018_02_12_164506) do
+ create_table "action_mailbox_inbound_emails", force: :cascade do |t|
+ t.integer "status", default: 0, null: false
+ t.string "message_id", null: false
+ t.string "message_checksum", null: false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.index ["message_id", "message_checksum"], name: "index_action_mailbox_inbound_emails_uniqueness", unique: true
+ end
+
+ create_table "active_storage_attachments", force: :cascade do |t|
+ t.string "name", null: false
+ t.string "record_type", null: false
+ t.integer "record_id", null: false
+ t.integer "blob_id", null: false
+ t.datetime "created_at", precision: nil, null: false
+ t.index ["blob_id"], name: "index_active_storage_attachments_on_blob_id"
+ t.index ["record_type", "record_id", "name", "blob_id"], name: "index_active_storage_attachments_uniqueness", unique: true
+ end
+
+ create_table "active_storage_blobs", force: :cascade do |t|
+ t.string "key", null: false
+ t.string "filename", null: false
+ t.string "content_type"
+ t.text "metadata"
+ t.string "service_name", null: false
+ t.bigint "byte_size", null: false
+ t.string "checksum", null: false
+ t.datetime "created_at", precision: nil, null: false
+ t.index ["key"], name: "index_active_storage_blobs_on_key", unique: true
+ end
+
+ create_table "active_storage_variant_records", force: :cascade do |t|
+ t.integer "blob_id", null: false
+ t.string "variation_digest", null: false
+ t.index ["blob_id", "variation_digest"], name: "index_active_storage_variant_records_uniqueness", unique: true
+ end
+
+ add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id"
+ add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id"
+end
diff --git a/actionpack/test/fixtures/sprockets/app/javascripts/extra.js b/actionmailbox/test/dummy/lib/assets/.keep
similarity index 100%
rename from actionpack/test/fixtures/sprockets/app/javascripts/extra.js
rename to actionmailbox/test/dummy/lib/assets/.keep
diff --git a/actionpack/test/fixtures/sprockets/app/javascripts/xmlhr.js b/actionmailbox/test/dummy/log/.keep
similarity index 100%
rename from actionpack/test/fixtures/sprockets/app/javascripts/xmlhr.js
rename to actionmailbox/test/dummy/log/.keep
diff --git a/actionmailbox/test/dummy/public/400.html b/actionmailbox/test/dummy/public/400.html
new file mode 100644
index 0000000000000..f59c79ab82f05
--- /dev/null
+++ b/actionmailbox/test/dummy/public/400.html
@@ -0,0 +1,114 @@
+
+
+
+
+
+
+ The server cannot process the request due to a client error (400 Bad Request)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
The server cannot process the request due to a client error. Please check the request and try again. If you're the application owner check the logs for more information.
+
+
+
+
+
+
diff --git a/actionmailbox/test/dummy/public/404.html b/actionmailbox/test/dummy/public/404.html
new file mode 100644
index 0000000000000..26d16027c6a4c
--- /dev/null
+++ b/actionmailbox/test/dummy/public/404.html
@@ -0,0 +1,114 @@
+
+
+
+
+
+
+ The page you were looking for doesn't exist (404 Not found)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
The page you were looking for doesn't exist. You may have mistyped the address or the page may have moved. If you're the application owner check the logs for more information.
The change you wanted was rejected. Maybe you tried to change something you didn't have access to. If you're the application owner check the logs for more information.
We're sorry, but something went wrong. If you're the application owner check the logs for more information.
+
+
+
+
+
+
diff --git a/actionpack/test/fixtures/sprockets/app/stylesheets/dir/style.css b/actionmailbox/test/dummy/public/apple-touch-icon-precomposed.png
similarity index 100%
rename from actionpack/test/fixtures/sprockets/app/stylesheets/dir/style.css
rename to actionmailbox/test/dummy/public/apple-touch-icon-precomposed.png
diff --git a/actionpack/test/fixtures/sprockets/app/stylesheets/extra.css b/actionmailbox/test/dummy/public/apple-touch-icon.png
similarity index 100%
rename from actionpack/test/fixtures/sprockets/app/stylesheets/extra.css
rename to actionmailbox/test/dummy/public/apple-touch-icon.png
diff --git a/railties/guides/code/getting_started/public/favicon.ico b/actionmailbox/test/dummy/public/favicon.ico
similarity index 100%
rename from railties/guides/code/getting_started/public/favicon.ico
rename to actionmailbox/test/dummy/public/favicon.ico
diff --git a/actionpack/test/fixtures/sprockets/app/stylesheets/style.css b/actionmailbox/test/dummy/storage/.keep
similarity index 100%
rename from actionpack/test/fixtures/sprockets/app/stylesheets/style.css
rename to actionmailbox/test/dummy/storage/.keep
diff --git a/actionmailbox/test/fixtures/files/avatar1.jpeg b/actionmailbox/test/fixtures/files/avatar1.jpeg
new file mode 100644
index 0000000000000..31111c3bc9cf2
Binary files /dev/null and b/actionmailbox/test/fixtures/files/avatar1.jpeg differ
diff --git a/actionmailbox/test/fixtures/files/avatar2.jpeg b/actionmailbox/test/fixtures/files/avatar2.jpeg
new file mode 100644
index 0000000000000..e844e7f3c61d6
Binary files /dev/null and b/actionmailbox/test/fixtures/files/avatar2.jpeg differ
diff --git a/actionmailbox/test/fixtures/files/invalid_utf.eml b/actionmailbox/test/fixtures/files/invalid_utf.eml
new file mode 100644
index 0000000000000..c5c03d572b643
--- /dev/null
+++ b/actionmailbox/test/fixtures/files/invalid_utf.eml
@@ -0,0 +1,39 @@
+thread-index: Adjkg/rniynGRZvvRu2Ftd4zu7/YrA==
+Thread-Topic: =?iso-8859-2?Q?Informace_o_skladov=FDch_z=E1sob=E1ch_Copmany?=
+From:
+To: ,
+Message-ID: <05988AA6EC0D44318855A5E39E3B6F9E@jansterba.com>
+MIME-Version: 1.0
+Content-Type: multipart/mixed;
+ boundary="----=_NextPart_000_168F_01D8E494.BE7019A0"
+Content-Class: urn:content-classes:message
+Importance: normal
+Priority: normal
+X-MimeOLE: Produced By Microsoft MimeOLE V6.3.9600.20564
+X-EOPAttributedMessage: 0
+X-Spam-IndexStatus: 0
+
+This is a multi-part message in MIME format.
+
+------=_NextPart_000_168F_01D8E494.BE7019A0
+Content-Type: multipart/alternative;
+ boundary="----=_NextPart_001_1690_01D8E494.BE7019A0"
+
+------=_NextPart_001_1690_01D8E494.BE7019A0
+Content-Type: text/plain;
+ charset="iso-8859-2"
+Content-Transfer-Encoding: quoted-printable
+
+V=E1=BEen=FD z=E1kazn=EDku,
+
+v p=F8=EDloze zas=EDl=E1me aktu=E1ln=ED informace o skladov=FDch =
+z=E1sob=E1ch.
+
+------=_NextPart_001_1690_01D8E494.BE7019A0
+Content-Type: text/html;
+ charset="iso-8859-2"
+Content-Transfer-Encoding: 8bit
+
+V�en� z�kazn�ku,
v p��loze zas�l�me aktu�ln� informace o skladov�ch z�sob�ch.
+
+------=_NextPart_000_168F_01D8E494.BE7019A0--
diff --git a/actionmailbox/test/fixtures/files/welcome.eml b/actionmailbox/test/fixtures/files/welcome.eml
new file mode 100644
index 0000000000000..27fd51c58a586
--- /dev/null
+++ b/actionmailbox/test/fixtures/files/welcome.eml
@@ -0,0 +1,631 @@
+From: Jason Fried
+Mime-Version: 1.0 (Apple Message framework v1244.3)
+Content-Type: multipart/alternative; boundary="Apple-Mail=_33A037C7-4BB3-4772-AE52-FCF2D7535F74"
+Subject: Discussion: Let's debate these attachments
+Date: Tue, 13 Sep 2011 15:19:37 -0400
+In-Reply-To: <4e6e35f5a38b4_479f13bb90078178@small-app-01.mail>
+To: "Replies"
+References: <4e6e35f5a38b4_479f13bb90078178@small-app-01.mail>
+Message-Id: <0CB459E0-0336-41DA-BC88-E6E28C697DDB@37signals.com>
+X-Mailer: Apple Mail (2.1244.3)
+
+--Apple-Mail=_33A037C7-4BB3-4772-AE52-FCF2D7535F74
+Content-Transfer-Encoding: quoted-printable
+Content-Type: text/plain;
+ charset=utf-8
+
+Let's talk about these images:
+
+
+--Apple-Mail=_33A037C7-4BB3-4772-AE52-FCF2D7535F74
+Content-Type: multipart/related;
+ type="text/html";
+ boundary="Apple-Mail=_83444AF4-343C-4F75-AF8F-14E1E7434FC1"
+
+
+--Apple-Mail=_83444AF4-343C-4F75-AF8F-14E1E7434FC1
+Content-Transfer-Encoding: base64
+Content-Disposition: inline;
+ filename=avatar1.jpeg
+Content-Type: image/jpeg;
+ name="avatar1.jpeg"
+Content-Id: <7AAEB353-2341-4D46-A054-5CA5CB2363B7>
+
+/9j/4AAQSkZJRgABAQAAAQABAAD/4gxYSUNDX1BST0ZJTEUAAQEAAAxITGlubwIQAABtbnRyUkdC
+IFhZWiAHzgACAAkABgAxAABhY3NwTVNGVAAAAABJRUMgc1JHQgAAAAAAAAAAAAAAAAAA9tYAAQAA
+AADTLUhQICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABFj
+cHJ0AAABUAAAADNkZXNjAAABhAAAAGx3dHB0AAAB8AAAABRia3B0AAACBAAAABRyWFlaAAACGAAA
+ABRnWFlaAAACLAAAABRiWFlaAAACQAAAABRkbW5kAAACVAAAAHBkbWRkAAACxAAAAIh2dWVkAAAD
+TAAAAIZ2aWV3AAAD1AAAACRsdW1pAAAD+AAAABRtZWFzAAAEDAAAACR0ZWNoAAAEMAAAAAxyVFJD
+AAAEPAAACAxnVFJDAAAEPAAACAxiVFJDAAAEPAAACAx0ZXh0AAAAAENvcHlyaWdodCAoYykgMTk5
+OCBIZXdsZXR0LVBhY2thcmQgQ29tcGFueQAAZGVzYwAAAAAAAAASc1JHQiBJRUM2MTk2Ni0yLjEA
+AAAAAAAAAAAAABJzUkdCIElFQzYxOTY2LTIuMQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAWFlaIAAAAAAAAPNRAAEAAAABFsxYWVogAAAAAAAAAAAAAAAA
+AAAAAFhZWiAAAAAAAABvogAAOPUAAAOQWFlaIAAAAAAAAGKZAAC3hQAAGNpYWVogAAAAAAAAJKAA
+AA+EAAC2z2Rlc2MAAAAAAAAAFklFQyBodHRwOi8vd3d3LmllYy5jaAAAAAAAAAAAAAAAFklFQyBo
+dHRwOi8vd3d3LmllYy5jaAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAABkZXNjAAAAAAAAAC5JRUMgNjE5NjYtMi4xIERlZmF1bHQgUkdCIGNvbG91ciBzcGFjZSAt
+IHNSR0IAAAAAAAAAAAAAAC5JRUMgNjE5NjYtMi4xIERlZmF1bHQgUkdCIGNvbG91ciBzcGFjZSAt
+IHNSR0IAAAAAAAAAAAAAAAAAAAAAAAAAAAAAZGVzYwAAAAAAAAAsUmVmZXJlbmNlIFZpZXdpbmcg
+Q29uZGl0aW9uIGluIElFQzYxOTY2LTIuMQAAAAAAAAAAAAAALFJlZmVyZW5jZSBWaWV3aW5nIENv
+bmRpdGlvbiBpbiBJRUM2MTk2Ni0yLjEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHZpZXcAAAAA
+ABOk/gAUXy4AEM8UAAPtzAAEEwsAA1yeAAAAAVhZWiAAAAAAAEwJVgBQAAAAVx/nbWVhcwAAAAAA
+AAABAAAAAAAAAAAAAAAAAAAAAAAAAo8AAAACc2lnIAAAAABDUlQgY3VydgAAAAAAAAQAAAAABQAK
+AA8AFAAZAB4AIwAoAC0AMgA3ADsAQABFAEoATwBUAFkAXgBjAGgAbQByAHcAfACBAIYAiwCQAJUA
+mgCfAKQAqQCuALIAtwC8AMEAxgDLANAA1QDbAOAA5QDrAPAA9gD7AQEBBwENARMBGQEfASUBKwEy
+ATgBPgFFAUwBUgFZAWABZwFuAXUBfAGDAYsBkgGaAaEBqQGxAbkBwQHJAdEB2QHhAekB8gH6AgMC
+DAIUAh0CJgIvAjgCQQJLAlQCXQJnAnECegKEAo4CmAKiAqwCtgLBAssC1QLgAusC9QMAAwsDFgMh
+Ay0DOANDA08DWgNmA3IDfgOKA5YDogOuA7oDxwPTA+AD7AP5BAYEEwQgBC0EOwRIBFUEYwRxBH4E
+jASaBKgEtgTEBNME4QTwBP4FDQUcBSsFOgVJBVgFZwV3BYYFlgWmBbUFxQXVBeUF9gYGBhYGJwY3
+BkgGWQZqBnsGjAadBq8GwAbRBuMG9QcHBxkHKwc9B08HYQd0B4YHmQesB78H0gflB/gICwgfCDII
+RghaCG4IggiWCKoIvgjSCOcI+wkQCSUJOglPCWQJeQmPCaQJugnPCeUJ+woRCicKPQpUCmoKgQqY
+Cq4KxQrcCvMLCwsiCzkLUQtpC4ALmAuwC8gL4Qv5DBIMKgxDDFwMdQyODKcMwAzZDPMNDQ0mDUAN
+Wg10DY4NqQ3DDd4N+A4TDi4OSQ5kDn8Omw62DtIO7g8JDyUPQQ9eD3oPlg+zD88P7BAJECYQQxBh
+EH4QmxC5ENcQ9RETETERTxFtEYwRqhHJEegSBxImEkUSZBKEEqMSwxLjEwMTIxNDE2MTgxOkE8UT
+5RQGFCcUSRRqFIsUrRTOFPAVEhU0FVYVeBWbFb0V4BYDFiYWSRZsFo8WshbWFvoXHRdBF2UXiReu
+F9IX9xgbGEAYZRiKGK8Y1Rj6GSAZRRlrGZEZtxndGgQaKhpRGncanhrFGuwbFBs7G2MbihuyG9oc
+AhwqHFIcexyjHMwc9R0eHUcdcB2ZHcMd7B4WHkAeah6UHr4e6R8THz4faR+UH78f6iAVIEEgbCCY
+IMQg8CEcIUghdSGhIc4h+yInIlUigiKvIt0jCiM4I2YjlCPCI/AkHyRNJHwkqyTaJQklOCVoJZcl
+xyX3JicmVyaHJrcm6CcYJ0kneierJ9woDSg/KHEooijUKQYpOClrKZ0p0CoCKjUqaCqbKs8rAis2
+K2krnSvRLAUsOSxuLKIs1y0MLUEtdi2rLeEuFi5MLoIuty7uLyQvWi+RL8cv/jA1MGwwpDDbMRIx
+SjGCMbox8jIqMmMymzLUMw0zRjN/M7gz8TQrNGU0njTYNRM1TTWHNcI1/TY3NnI2rjbpNyQ3YDec
+N9c4FDhQOIw4yDkFOUI5fzm8Ofk6Njp0OrI67zstO2s7qjvoPCc8ZTykPOM9Ij1hPaE94D4gPmA+
+oD7gPyE/YT+iP+JAI0BkQKZA50EpQWpBrEHuQjBCckK1QvdDOkN9Q8BEA0RHRIpEzkUSRVVFmkXe
+RiJGZ0arRvBHNUd7R8BIBUhLSJFI10kdSWNJqUnwSjdKfUrESwxLU0uaS+JMKkxyTLpNAk1KTZNN
+3E4lTm5Ot08AT0lPk0/dUCdQcVC7UQZRUFGbUeZSMVJ8UsdTE1NfU6pT9lRCVI9U21UoVXVVwlYP
+VlxWqVb3V0RXklfgWC9YfVjLWRpZaVm4WgdaVlqmWvVbRVuVW+VcNVyGXNZdJ114XcleGl5sXr1f
+D19hX7NgBWBXYKpg/GFPYaJh9WJJYpxi8GNDY5dj62RAZJRk6WU9ZZJl52Y9ZpJm6Gc9Z5Nn6Wg/
+aJZo7GlDaZpp8WpIap9q92tPa6dr/2xXbK9tCG1gbbluEm5rbsRvHm94b9FwK3CGcOBxOnGVcfBy
+S3KmcwFzXXO4dBR0cHTMdSh1hXXhdj52m3b4d1Z3s3gReG54zHkqeYl553pGeqV7BHtje8J8IXyB
+fOF9QX2hfgF+Yn7CfyN/hH/lgEeAqIEKgWuBzYIwgpKC9INXg7qEHYSAhOOFR4Wrhg6GcobXhzuH
+n4gEiGmIzokziZmJ/opkisqLMIuWi/yMY4zKjTGNmI3/jmaOzo82j56QBpBukNaRP5GokhGSepLj
+k02TtpQglIqU9JVflcmWNJaflwqXdZfgmEyYuJkkmZCZ/JpomtWbQpuvnByciZz3nWSd0p5Anq6f
+HZ+Ln/qgaaDYoUehtqImopajBqN2o+akVqTHpTilqaYapoum/adup+CoUqjEqTepqaocqo+rAqt1
+q+msXKzQrUStuK4trqGvFq+LsACwdbDqsWCx1rJLssKzOLOutCW0nLUTtYq2AbZ5tvC3aLfguFm4
+0blKucK6O7q1uy67p7whvJu9Fb2Pvgq+hL7/v3q/9cBwwOzBZ8Hjwl/C28NYw9TEUcTOxUvFyMZG
+xsPHQce/yD3IvMk6ybnKOMq3yzbLtsw1zLXNNc21zjbOts83z7jQOdC60TzRvtI/0sHTRNPG1EnU
+y9VO1dHWVdbY11zX4Nhk2OjZbNnx2nba+9uA3AXcit0Q3ZbeHN6i3ynfr+A24L3hROHM4lPi2+Nj
+4+vkc+T85YTmDeaW5x/nqegy6LzpRunQ6lvq5etw6/vshu0R7ZzuKO6070DvzPBY8OXxcvH/8ozz
+GfOn9DT0wvVQ9d72bfb794r4Gfio+Tj5x/pX+uf7d/wH/Jj9Kf26/kv+3P9t////2wBDAAICAgIC
+AQICAgICAgIDAwYEAwMDAwcFBQQGCAcICAgHCAgJCg0LCQkMCggICw8LDA0ODg4OCQsQEQ8OEQ0O
+Dg7/2wBDAQICAgMDAwYEBAYOCQgJDg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4O
+Dg4ODg4ODg4ODg4ODg7/wAARCADwAPADASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAEC
+AwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0Kx
+wRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1
+dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ
+2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QA
+tREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYk
+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
+mk8VWW5WRGJGAOaAP//Z
+
+--Apple-Mail=_83444AF4-343C-4F75-AF8F-14E1E7434FC1
+Content-Transfer-Encoding: base64
+Content-Disposition: inline;
+ filename=avatar2.jpg
+Content-Type: image/jpeg;
+ x-unix-mode=0700;
+ name="avatar2.jpg"
+Content-Id: <4594E827-6E69-4329-8691-6BC35E3E73A0>
+
+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
+
+--Apple-Mail=_83444AF4-343C-4F75-AF8F-14E1E7434FC1--
+
+--Apple-Mail=_33A037C7-4BB3-4772-AE52-FCF2D7535F74--
diff --git a/actionmailbox/test/generators/mailbox_generator_test.rb b/actionmailbox/test/generators/mailbox_generator_test.rb
new file mode 100644
index 0000000000000..f9677ba591270
--- /dev/null
+++ b/actionmailbox/test/generators/mailbox_generator_test.rb
@@ -0,0 +1,85 @@
+# frozen_string_literal: true
+
+require "test_helper"
+require "rails/generators/mailbox/mailbox_generator"
+
+class MailboxGeneratorTest < Rails::Generators::TestCase
+ destination File.expand_path("../../tmp", __dir__)
+ setup :prepare_destination
+ tests Rails::Generators::MailboxGenerator
+
+ arguments ["inbox"]
+
+ def test_mailbox_skeleton_is_created
+ run_generator
+
+ assert_file "app/mailboxes/inbox_mailbox.rb" do |mailbox|
+ assert_match(/class InboxMailbox < ApplicationMailbox/, mailbox)
+ assert_match(/def process/, mailbox)
+ assert_no_match(%r{# routing /something/i => :somewhere}, mailbox)
+ end
+
+ assert_file "app/mailboxes/application_mailbox.rb" do |mailbox|
+ assert_match(/class ApplicationMailbox < ActionMailbox::Base/, mailbox)
+ assert_match(%r{# routing /something/i => :somewhere}, mailbox)
+ assert_no_match(/def process/, mailbox)
+ end
+ end
+
+ def test_mailbox_skeleton_is_created_with_namespace
+ run_generator %w(inceptions/inbox -t=test_unit)
+
+ assert_file "app/mailboxes/inceptions/inbox_mailbox.rb" do |mailbox|
+ assert_match(/class Inceptions::InboxMailbox < ApplicationMailbox/, mailbox)
+ assert_match(/def process/, mailbox)
+ assert_no_match(%r{# routing /something/i => :somewhere}, mailbox)
+ end
+
+ assert_file "test/mailboxes/inceptions/inbox_mailbox_test.rb" do |mailbox|
+ assert_match(/class Inceptions::InboxMailboxTest < ActionMailbox::TestCase/, mailbox)
+ assert_match(/# test "receive mail" do/, mailbox)
+ assert_match(/# to: '"someone" ',/, mailbox)
+ end
+
+ assert_file "app/mailboxes/application_mailbox.rb" do |mailbox|
+ assert_match(/class ApplicationMailbox < ActionMailbox::Base/, mailbox)
+ assert_match(%r{# routing /something/i => :somewhere}, mailbox)
+ assert_no_match(/def process/, mailbox)
+ end
+ end
+
+ def test_check_class_collision
+ Object.const_set :InboxMailbox, Class.new
+ content = capture(:stderr) { run_generator }
+ assert_match(/The name 'InboxMailbox' is either already used in your application or reserved/, content)
+ ensure
+ Object.send :remove_const, :InboxMailbox
+ end
+
+ def test_invokes_default_test_framework
+ run_generator %w(inbox -t=test_unit)
+
+ assert_file "test/mailboxes/inbox_mailbox_test.rb" do |test|
+ assert_match(/class InboxMailboxTest < ActionMailbox::TestCase/, test)
+ assert_match(/# test "receive mail" do/, test)
+ assert_match(/# to: '"someone" ',/, test)
+ end
+ end
+
+ def test_mailbox_on_revoke
+ run_generator
+ run_generator ["inbox"], behavior: :revoke
+
+ assert_no_file "app/mailboxes/inbox_mailbox.rb"
+ end
+
+ def test_mailbox_suffix_is_not_duplicated
+ run_generator %w(inbox_mailbox -t=test_unit)
+
+ assert_no_file "app/mailboxes/inbox_mailbox_mailbox.rb"
+ assert_file "app/mailboxes/inbox_mailbox.rb"
+
+ assert_no_file "test/mailboxes/inbox_mailbox_mailbox_test.rb"
+ assert_file "test/mailboxes/inbox_mailbox_test.rb"
+ end
+end
diff --git a/actionmailbox/test/jobs/incineration_job_test.rb b/actionmailbox/test/jobs/incineration_job_test.rb
new file mode 100644
index 0000000000000..03948a32bf93f
--- /dev/null
+++ b/actionmailbox/test/jobs/incineration_job_test.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+require "test_helper"
+
+class ActionMailbox::IncinerationJobTest < ActiveJob::TestCase
+ setup { @inbound_email = create_inbound_email_from_fixture("welcome.eml") }
+
+ test "ignoring a missing inbound email" do
+ @inbound_email.destroy!
+
+ perform_enqueued_jobs do
+ assert_nothing_raised do
+ ActionMailbox::IncinerationJob.perform_later @inbound_email
+ end
+ end
+ end
+end
diff --git a/actionmailbox/test/migrations_test.rb b/actionmailbox/test/migrations_test.rb
new file mode 100644
index 0000000000000..eb18781e03081
--- /dev/null
+++ b/actionmailbox/test/migrations_test.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+require "test_helper"
+require ActionMailbox::Engine.root.join("db/migrate/20180917164000_create_action_mailbox_tables.rb").to_s
+
+class ActionMailbox::MigrationsTest < ActiveSupport::TestCase
+ setup do
+ @original_verbose = ActiveRecord::Migration.verbose
+ ActiveRecord::Migration.verbose = false
+
+ @connection = ActiveRecord::Base.lease_connection
+ @original_options = Rails.configuration.generators.options.deep_dup
+ end
+
+ teardown do
+ Rails.configuration.generators.options = @original_options
+ rerun_migration
+ ActiveRecord::Migration.verbose = @original_verbose
+ end
+
+ test "migration creates tables with default primary key type" do
+ action_mailbox_tables.each do |table|
+ assert_equal :integer, primary_key(table).type
+ end
+ end
+
+ test "migration creates tables with configured primary key type" do
+ Rails.configuration.generators do |g|
+ g.orm :active_record, primary_key_type: :string
+ end
+
+ rerun_migration
+
+ action_mailbox_tables.each do |table|
+ assert_equal :string, primary_key(table).type
+ end
+ end
+
+ private
+ def rerun_migration
+ CreateActionMailboxTables.migrate(:down)
+ CreateActionMailboxTables.migrate(:up)
+ end
+
+ def action_mailbox_tables
+ @action_mailbox_tables ||= ActionMailbox::Record.descendants.map { |klass| klass.table_name.to_sym }
+ end
+
+ def primary_key(table)
+ @connection.columns(table).find { |c| c.name == "id" }
+ end
+end
diff --git a/actionmailbox/test/models/table_name_test.rb b/actionmailbox/test/models/table_name_test.rb
new file mode 100644
index 0000000000000..0b082d1bd4867
--- /dev/null
+++ b/actionmailbox/test/models/table_name_test.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+require "test_helper"
+
+class ActionMailbox::TableNameTest < ActiveSupport::TestCase
+ setup do
+ @old_prefix = ActiveRecord::Base.table_name_prefix
+ @old_suffix = ActiveRecord::Base.table_name_suffix
+
+ ActiveRecord::Base.table_name_prefix = @prefix = "abc_"
+ ActiveRecord::Base.table_name_suffix = @suffix = "_xyz"
+
+ @models = [ActionMailbox::InboundEmail]
+ @models.map(&:reset_table_name)
+ end
+
+ teardown do
+ ActiveRecord::Base.table_name_prefix = @old_prefix
+ ActiveRecord::Base.table_name_suffix = @old_suffix
+
+ @models.map(&:reset_table_name)
+ end
+
+ test "prefix and suffix are added to the Action Mailbox tables' name" do
+ assert_equal(
+ "#{@prefix}action_mailbox_inbound_emails#{@suffix}",
+ ActionMailbox::InboundEmail.table_name
+ )
+ end
+end
diff --git a/actionmailbox/test/test_helper.rb b/actionmailbox/test/test_helper.rb
new file mode 100644
index 0000000000000..2f4edb8282d49
--- /dev/null
+++ b/actionmailbox/test/test_helper.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+require_relative "../../tools/strict_warnings"
+
+ENV["RAILS_ENV"] = "test"
+ENV["RAILS_INBOUND_EMAIL_PASSWORD"] = "tbsy84uSV1Kt3ZJZELY2TmShPRs91E3yL4tzf96297vBCkDWgL"
+
+require_relative "../test/dummy/config/environment"
+ActiveRecord::Migrator.migrations_paths = [ File.expand_path("../test/dummy/db/migrate", __dir__) ]
+require "rails/test_help"
+
+require "webmock/minitest"
+
+require "rails/test_unit/reporter"
+Rails::TestUnitReporter.executable = "bin/test"
+
+if ActiveSupport::TestCase.respond_to?(:fixture_paths=)
+ ActiveSupport::TestCase.fixture_paths = [File.expand_path("fixtures", __dir__)]
+ ActionDispatch::IntegrationTest.fixture_paths = ActiveSupport::TestCase.fixture_paths
+ ActiveSupport::TestCase.file_fixture_path = File.expand_path("fixtures", __dir__) + "/files"
+ ActiveSupport::TestCase.fixtures :all
+end
+
+require "action_mailbox/test_helper"
+
+class ActiveSupport::TestCase
+ include ActionMailbox::TestHelper, ActiveJob::TestHelper
+end
+
+class ActionDispatch::IntegrationTest
+ private
+ def credentials
+ ActionController::HttpAuthentication::Basic.encode_credentials "actionmailbox", ENV["RAILS_INBOUND_EMAIL_PASSWORD"]
+ end
+
+ def switch_password_to(new_password)
+ previous_password, ENV["RAILS_INBOUND_EMAIL_PASSWORD"] = ENV["RAILS_INBOUND_EMAIL_PASSWORD"], new_password
+ yield
+ ensure
+ ENV["RAILS_INBOUND_EMAIL_PASSWORD"] = previous_password
+ end
+end
+
+if ARGV.include?("-v")
+ ActiveRecord::Base.logger = Logger.new(STDOUT)
+ ActiveJob::Base.logger = Logger.new(STDOUT)
+end
+
+class BounceMailer < ActionMailer::Base
+ def bounce(to:)
+ mail from: "receiver@example.com", to: to, subject: "Your email was not delivered" do |format|
+ format.html { render plain: "Sorry!" }
+ end
+ end
+end
+
+require_relative "../../tools/test_common"
diff --git a/actionmailbox/test/unit/inbound_email/incineration_test.rb b/actionmailbox/test/unit/inbound_email/incineration_test.rb
new file mode 100644
index 0000000000000..54488349fda0a
--- /dev/null
+++ b/actionmailbox/test/unit/inbound_email/incineration_test.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+require_relative "../../test_helper"
+
+class ActionMailbox::InboundEmail::IncinerationTest < ActiveSupport::TestCase
+ test "incinerating 30 days after delivery" do
+ freeze_time
+
+ assert_enqueued_with job: ActionMailbox::IncinerationJob, at: 30.days.from_now do
+ create_inbound_email_from_fixture("welcome.eml").delivered!
+ end
+
+ travel 30.days
+
+ assert_difference -> { ActionMailbox::InboundEmail.count }, -1 do
+ perform_enqueued_jobs only: ActionMailbox::IncinerationJob
+ end
+ end
+
+ test "incinerating 30 days after bounce" do
+ freeze_time
+
+ assert_enqueued_with job: ActionMailbox::IncinerationJob, at: 30.days.from_now do
+ create_inbound_email_from_fixture("welcome.eml").bounced!
+ end
+
+ travel 30.days
+
+ assert_difference -> { ActionMailbox::InboundEmail.count }, -1 do
+ perform_enqueued_jobs only: ActionMailbox::IncinerationJob
+ end
+ end
+
+ test "incinerating 30 days after failure" do
+ freeze_time
+
+ assert_enqueued_with job: ActionMailbox::IncinerationJob, at: 30.days.from_now do
+ create_inbound_email_from_fixture("welcome.eml").failed!
+ end
+
+ travel 30.days
+
+ assert_difference -> { ActionMailbox::InboundEmail.count }, -1 do
+ perform_enqueued_jobs only: ActionMailbox::IncinerationJob
+ end
+ end
+
+ test "skipping incineration" do
+ original, ActionMailbox.incinerate = ActionMailbox.incinerate, false
+
+ assert_no_enqueued_jobs only: ActionMailbox::IncinerationJob do
+ create_inbound_email_from_fixture("welcome.eml").delivered!
+ end
+ ensure
+ ActionMailbox.incinerate = original
+ end
+end
diff --git a/actionmailbox/test/unit/inbound_email/message_id_test.rb b/actionmailbox/test/unit/inbound_email/message_id_test.rb
new file mode 100644
index 0000000000000..af467a8d45466
--- /dev/null
+++ b/actionmailbox/test/unit/inbound_email/message_id_test.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require_relative "../../test_helper"
+
+class ActionMailbox::InboundEmail::MessageIdTest < ActiveSupport::TestCase
+ test "message id is extracted from raw email" do
+ inbound_email = create_inbound_email_from_fixture("welcome.eml")
+ assert_equal "0CB459E0-0336-41DA-BC88-E6E28C697DDB@37signals.com", inbound_email.message_id
+ end
+
+ test "message id is generated if its missing" do
+ inbound_email = create_inbound_email_from_source "Date: Fri, 28 Sep 2018 11:08:55 -0700\r\nTo: a@example.com\r\nMime-Version: 1.0\r\nContent-Type: text/plain\r\nContent-Transfer-Encoding: 7bit\r\n\r\nHello!"
+ assert_not_nil inbound_email.message_id
+ end
+end
diff --git a/actionmailbox/test/unit/inbound_email_test.rb b/actionmailbox/test/unit/inbound_email_test.rb
new file mode 100644
index 0000000000000..264209c118192
--- /dev/null
+++ b/actionmailbox/test/unit/inbound_email_test.rb
@@ -0,0 +1,84 @@
+# frozen_string_literal: true
+
+require_relative "../test_helper"
+require "minitest/mock"
+
+module ActionMailbox
+ class InboundEmailTest < ActiveSupport::TestCase
+ test "mail provides the parsed source" do
+ assert_equal "Discussion: Let's debate these attachments", create_inbound_email_from_fixture("welcome.eml").mail.subject
+ end
+
+ test "source returns the contents of the raw email" do
+ assert_equal file_fixture("welcome.eml").read, create_inbound_email_from_fixture("welcome.eml").source
+ end
+
+ test "email with message id is processed only once when received multiple times" do
+ mail = Mail.from_source(file_fixture("welcome.eml").read)
+ assert mail.message_id
+
+ inbound_email_1 = create_inbound_email_from_source(mail.to_s)
+ assert inbound_email_1
+
+ inbound_email_2 = create_inbound_email_from_source(mail.to_s)
+ assert_nil inbound_email_2
+ end
+
+ test "email with missing message id is processed only once when received multiple times" do
+ mail = Mail.from_source("Date: Fri, 28 Sep 2018 11:08:55 -0700\r\nTo: a@example.com\r\nMime-Version: 1.0\r\nContent-Type: text/plain\r\nContent-Transfer-Encoding: 7bit\r\n\r\nHello!")
+ assert_nil mail.message_id
+
+ inbound_email_1 = create_inbound_email_from_source(mail.to_s)
+ assert inbound_email_1
+
+ inbound_email_2 = create_inbound_email_from_source(mail.to_s)
+ assert_nil inbound_email_2
+ end
+
+ test "error on upload doesn't leave behind a pending inbound email" do
+ ActiveStorage::Blob.service.stub(:upload, -> { raise "Boom!" }) do
+ assert_no_difference -> { ActionMailbox::InboundEmail.count } do
+ assert_raises do
+ create_inbound_email_from_fixture "welcome.eml"
+ end
+ end
+ end
+ end
+
+ test "email gets saved to the configured storage service" do
+ ActionMailbox.storage_service = :test_email
+
+ assert_equal(:test_email, ActionMailbox.storage_service)
+
+ email = create_inbound_email_from_fixture("welcome.eml")
+
+ storage_service = ActiveStorage::Blob.services.fetch(ActionMailbox.storage_service)
+ raw = email.raw_email_blob
+
+ # Not present in the main storage
+ assert_not(ActiveStorage::Blob.service.exist?(raw.key))
+ # Present in the email storage
+ assert(storage_service.exist?(raw.key))
+ ensure
+ ActionMailbox.storage_service = nil
+ end
+
+ test "email gets saved to the default storage service, even if it gets changed" do
+ default_service = ActiveStorage::Blob.service
+ ActiveStorage::Blob.service = ActiveStorage::Blob.services.fetch(:test_email)
+
+ # Doesn't change ActionMailbox.storage_service
+ assert_nil(ActionMailbox.storage_service)
+
+ email = create_inbound_email_from_fixture("welcome.eml")
+ raw = email.raw_email_blob
+
+ # Not present in the (previously) default storage
+ assert_not(default_service.exist?(raw.key))
+ # Present in the current default storage (email)
+ assert(ActiveStorage::Blob.service.exist?(raw.key))
+ ensure
+ ActiveStorage::Blob.service = default_service
+ end
+ end
+end
diff --git a/actionmailbox/test/unit/mail_ext/address_equality_test.rb b/actionmailbox/test/unit/mail_ext/address_equality_test.rb
new file mode 100644
index 0000000000000..e4426aeae9594
--- /dev/null
+++ b/actionmailbox/test/unit/mail_ext/address_equality_test.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+require_relative "../../test_helper"
+
+module MailExt
+ class AddressEqualityTest < ActiveSupport::TestCase
+ test "two addresses with the same address are equal" do
+ assert_equal Mail::Address.new("david@basecamp.com"), Mail::Address.new("david@basecamp.com")
+ end
+ end
+end
diff --git a/actionmailbox/test/unit/mail_ext/address_wrapping_test.rb b/actionmailbox/test/unit/mail_ext/address_wrapping_test.rb
new file mode 100644
index 0000000000000..c4eb1328efa69
--- /dev/null
+++ b/actionmailbox/test/unit/mail_ext/address_wrapping_test.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require_relative "../../test_helper"
+
+module MailExt
+ class AddressWrappingTest < ActiveSupport::TestCase
+ test "wrap" do
+ needing_wrapping = Mail::Address.wrap("david@basecamp.com")
+ wrapping_not_needed = Mail::Address.wrap(Mail::Address.new("david@basecamp.com"))
+ assert_equal needing_wrapping.address, wrapping_not_needed.address
+ end
+ end
+end
diff --git a/actionmailbox/test/unit/mail_ext/addresses_test.rb b/actionmailbox/test/unit/mail_ext/addresses_test.rb
new file mode 100644
index 0000000000000..92ea75a071ccf
--- /dev/null
+++ b/actionmailbox/test/unit/mail_ext/addresses_test.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+require_relative "../../test_helper"
+
+module MailExt
+ class AddressesTest < ActiveSupport::TestCase
+ setup do
+ @mail = Mail.new \
+ from: "sally@example.com",
+ reply_to: "sarah@example.com",
+ to: "david@basecamp.com",
+ cc: "jason@basecamp.com",
+ bcc: "andrea@basecamp.com",
+ x_original_to: "ryan@basecamp.com",
+ x_forwarded_to: "jane@example.com"
+ end
+
+ test "from address uses address object" do
+ assert_equal "example.com", @mail.from_address.domain
+ end
+
+ test "reply to address uses address object" do
+ assert_equal "example.com", @mail.reply_to_address.domain
+ end
+
+ test "recipients include everyone from to, cc, bcc, x-original-to, and x-forwarded-to" do
+ assert_equal %w[ david@basecamp.com jason@basecamp.com andrea@basecamp.com ryan@basecamp.com jane@example.com ], @mail.recipients
+ end
+
+ test "recipients addresses use address objects" do
+ assert_equal "basecamp.com", @mail.recipients_addresses.first.domain
+ end
+
+ test "to addresses use address objects" do
+ assert_equal "basecamp.com", @mail.to_addresses.first.domain
+ end
+
+ test "cc addresses use address objects" do
+ assert_equal "basecamp.com", @mail.cc_addresses.first.domain
+ end
+
+ test "bcc addresses use address objects" do
+ assert_equal "basecamp.com", @mail.bcc_addresses.first.domain
+ end
+
+ test "x_original_to addresses use address objects" do
+ assert_equal "basecamp.com", @mail.x_original_to_addresses.first.domain
+ end
+
+ test "x_forwarded_to addresses use address objects" do
+ assert_equal "example.com", @mail.x_forwarded_to_addresses.first.domain
+ end
+ end
+end
diff --git a/actionmailbox/test/unit/mailbox/bouncing_test.rb b/actionmailbox/test/unit/mailbox/bouncing_test.rb
new file mode 100644
index 0000000000000..667ec62c04c31
--- /dev/null
+++ b/actionmailbox/test/unit/mailbox/bouncing_test.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+require_relative "../../test_helper"
+
+class BouncingWithReplyMailbox < ActionMailbox::Base
+ def process
+ bounce_with BounceMailer.bounce(to: mail.from)
+ end
+end
+
+class BouncingWithImmediateReplyMailbox < ActionMailbox::Base
+ def process
+ bounce_now_with BounceMailer.bounce(to: mail.from)
+ end
+end
+
+class ActionMailbox::Base::BouncingTest < ActiveSupport::TestCase
+ include ActionMailer::TestHelper
+
+ setup do
+ @inbound_email = create_inbound_email_from_mail \
+ from: "sender@example.com", to: "replies@example.com", subject: "Bounce me"
+ end
+
+ teardown do
+ ActionMailer::Base.deliveries.clear
+ end
+
+ test "bouncing with a reply" do
+ perform_enqueued_jobs only: ActionMailer::MailDeliveryJob do
+ BouncingWithReplyMailbox.receive @inbound_email
+ end
+
+ assert_predicate @inbound_email, :bounced?
+ assert_emails 1
+
+ mail = ActionMailer::Base.deliveries.last
+ assert_equal %w[ sender@example.com ], mail.to
+ assert_equal "Your email was not delivered", mail.subject
+ end
+
+ test "bouncing now with a reply" do
+ assert_no_enqueued_emails do
+ BouncingWithImmediateReplyMailbox.receive @inbound_email
+ end
+
+ assert_predicate @inbound_email, :bounced?
+ assert_emails 1
+
+ mail = ActionMailer::Base.deliveries.last
+ assert_equal %w[ sender@example.com ], mail.to
+ assert_equal "Your email was not delivered", mail.subject
+ end
+end
diff --git a/actionmailbox/test/unit/mailbox/callbacks_test.rb b/actionmailbox/test/unit/mailbox/callbacks_test.rb
new file mode 100644
index 0000000000000..1917aa3250da0
--- /dev/null
+++ b/actionmailbox/test/unit/mailbox/callbacks_test.rb
@@ -0,0 +1,77 @@
+# frozen_string_literal: true
+
+require_relative "../../test_helper"
+
+class CallbackMailbox < ActionMailbox::Base
+ before_processing { $before_processing = "Ran that!" }
+ after_processing { $after_processing = "Ran that too!" }
+ around_processing ->(r, block) { block.call; $around_processing = "Ran that as well!" }
+
+ def process
+ $processed = mail.subject
+ end
+end
+
+class BouncingCallbackMailbox < ActionMailbox::Base
+ before_processing { $before_processing = [ "Pre-bounce" ] }
+
+ before_processing do
+ bounce_with BounceMailer.bounce(to: mail.from)
+ $before_processing << "Bounce"
+ end
+
+ before_processing { $before_processing << "Post-bounce" }
+
+ after_processing { $after_processing = true }
+
+ def process
+ $processed = true
+ end
+end
+
+class DiscardingCallbackMailbox < ActionMailbox::Base
+ before_processing { $before_processing = [ "Pre-discard" ] }
+
+ before_processing do
+ delivered!
+ $before_processing << "Discard"
+ end
+
+ before_processing { $before_processing << "Post-discard" }
+
+ after_processing { $after_processing = true }
+
+ def process
+ $processed = true
+ end
+end
+
+class ActionMailbox::Base::CallbacksTest < ActiveSupport::TestCase
+ setup do
+ $before_processing = $after_processing = $around_processing = $processed = false
+ @inbound_email = create_inbound_email_from_fixture("welcome.eml")
+ end
+
+ test "all callback types" do
+ CallbackMailbox.receive @inbound_email
+ assert_equal "Ran that!", $before_processing
+ assert_equal "Ran that too!", $after_processing
+ assert_equal "Ran that as well!", $around_processing
+ end
+
+ test "bouncing in a callback terminates processing" do
+ BouncingCallbackMailbox.receive @inbound_email
+ assert_predicate @inbound_email, :bounced?
+ assert_equal [ "Pre-bounce", "Bounce" ], $before_processing
+ assert_not $processed
+ assert_not $after_processing
+ end
+
+ test "marking the inbound email as delivered in a callback terminates processing" do
+ DiscardingCallbackMailbox.receive @inbound_email
+ assert_predicate @inbound_email, :delivered?
+ assert_equal [ "Pre-discard", "Discard" ], $before_processing
+ assert_not $processed
+ assert_not $after_processing
+ end
+end
diff --git a/actionmailbox/test/unit/mailbox/notifications_test.rb b/actionmailbox/test/unit/mailbox/notifications_test.rb
new file mode 100644
index 0000000000000..655ed4560258f
--- /dev/null
+++ b/actionmailbox/test/unit/mailbox/notifications_test.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+require_relative "../../test_helper"
+
+class RepliesMailbox < ActionMailbox::Base
+end
+
+class ActionMailbox::Base::NotificationsTest < ActiveSupport::TestCase
+ test "instruments processing" do
+ mailbox = RepliesMailbox.new(create_inbound_email_from_fixture("welcome.eml"))
+ expected_payload = {
+ mailbox:,
+ inbound_email: {
+ id: 1,
+ message_id: "0CB459E0-0336-41DA-BC88-E6E28C697DDB@37signals.com",
+ status: "processing"
+ }
+ }
+
+ assert_notifications_count("process.action_mailbox", 1) do
+ assert_notification("process.action_mailbox", expected_payload) do
+ mailbox.perform_processing
+ end
+ end
+ end
+end
diff --git a/actionmailbox/test/unit/mailbox/routing_test.rb b/actionmailbox/test/unit/mailbox/routing_test.rb
new file mode 100644
index 0000000000000..8302b1d5ccce6
--- /dev/null
+++ b/actionmailbox/test/unit/mailbox/routing_test.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+require_relative "../../test_helper"
+
+class ApplicationMailbox < ActionMailbox::Base
+ routing "replies@example.com" => :replies
+end
+
+class RepliesMailbox < ActionMailbox::Base
+ def process
+ $processed = mail.subject
+ end
+end
+
+class ActionMailbox::Base::RoutingTest < ActiveSupport::TestCase
+ setup do
+ $processed = false
+ end
+
+ test "string routing" do
+ ApplicationMailbox.route create_inbound_email_from_fixture("welcome.eml")
+ assert_equal "Discussion: Let's debate these attachments", $processed
+ end
+
+ test "delayed routing" do
+ perform_enqueued_jobs only: ActionMailbox::RoutingJob do
+ create_inbound_email_from_fixture "welcome.eml", status: :pending
+ assert_equal "Discussion: Let's debate these attachments", $processed
+ end
+ end
+
+ test "mailbox_for" do
+ inbound_email = create_inbound_email_from_fixture "welcome.eml", status: :pending
+ assert_equal RepliesMailbox, ApplicationMailbox.mailbox_for(inbound_email)
+ end
+end
diff --git a/actionmailbox/test/unit/mailbox/state_test.rb b/actionmailbox/test/unit/mailbox/state_test.rb
new file mode 100644
index 0000000000000..cf1fd5441ea72
--- /dev/null
+++ b/actionmailbox/test/unit/mailbox/state_test.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+require_relative "../../test_helper"
+
+class SuccessfulMailbox < ActionMailbox::Base
+ def process
+ $processed = mail.subject
+ end
+end
+
+class UnsuccessfulMailbox < ActionMailbox::Base
+ rescue_from(RuntimeError) { $processed = :failure }
+
+ def process
+ raise "No way!"
+ end
+end
+
+class BouncingMailbox < ActionMailbox::Base
+ def process
+ $processed = :bounced
+ bounced!
+ end
+end
+
+
+class ActionMailbox::Base::StateTest < ActiveSupport::TestCase
+ setup do
+ $processed = false
+ @inbound_email = create_inbound_email_from_mail \
+ to: "replies@example.com", subject: "I was processed"
+ end
+
+ test "successful mailbox processing leaves inbound email in delivered state" do
+ SuccessfulMailbox.receive @inbound_email
+ assert_predicate @inbound_email, :delivered?
+ assert_equal "I was processed", $processed
+ end
+
+ test "unsuccessful mailbox processing leaves inbound email in failed state" do
+ UnsuccessfulMailbox.receive @inbound_email
+ assert_predicate @inbound_email, :failed?
+ assert_equal :failure, $processed
+ end
+
+ test "bounced inbound emails are not delivered" do
+ BouncingMailbox.receive @inbound_email
+ assert_predicate @inbound_email, :bounced?
+ assert_equal :bounced, $processed
+ end
+end
diff --git a/actionmailbox/test/unit/relayer_test.rb b/actionmailbox/test/unit/relayer_test.rb
new file mode 100644
index 0000000000000..89701d0baa111
--- /dev/null
+++ b/actionmailbox/test/unit/relayer_test.rb
@@ -0,0 +1,100 @@
+# frozen_string_literal: true
+
+require_relative "../test_helper"
+
+require "action_mailbox/relayer"
+
+module ActionMailbox
+ class RelayerTest < ActiveSupport::TestCase
+ URL = "https://example.com/rails/action_mailbox/relay/inbound_emails"
+ INGRESS_PASSWORD = "secret"
+
+ setup do
+ @relayer = ActionMailbox::Relayer.new(url: URL, password: INGRESS_PASSWORD)
+ end
+
+ test "successfully relaying an email" do
+ stub_request(:post, URL).to_return status: 204
+
+ result = @relayer.relay(file_fixture("welcome.eml").read)
+ assert_equal "2.0.0", result.status_code
+ assert_equal "Successfully relayed message to ingress", result.message
+ assert_predicate result, :success?
+ assert_not result.failure?
+
+ assert_requested :post, URL, body: file_fixture("welcome.eml").read,
+ basic_auth: [ "actionmailbox", INGRESS_PASSWORD ],
+ headers: { "Content-Type" => "message/rfc822", "User-Agent" => /\AAction Mailbox relayer v\d+\./ }
+ end
+
+ test "unsuccessfully relaying with invalid credentials" do
+ stub_request(:post, URL).to_return status: 401
+
+ result = @relayer.relay(file_fixture("welcome.eml").read)
+ assert_equal "4.7.0", result.status_code
+ assert_equal "Invalid credentials for ingress", result.message
+ assert_not result.success?
+ assert_predicate result, :failure?
+ end
+
+ test "unsuccessfully relaying due to an unspecified server error" do
+ stub_request(:post, URL).to_return status: 500
+
+ result = @relayer.relay(file_fixture("welcome.eml").read)
+ assert_equal "4.0.0", result.status_code
+ assert_equal "HTTP 500", result.message
+ assert_not result.success?
+ assert_predicate result, :failure?
+ end
+
+ test "unsuccessfully relaying due to a gateway timeout" do
+ stub_request(:post, URL).to_return status: 504
+
+ result = @relayer.relay(file_fixture("welcome.eml").read)
+ assert_equal "4.0.0", result.status_code
+ assert_equal "HTTP 504", result.message
+ assert_not result.success?
+ assert_predicate result, :failure?
+ end
+
+ test "unsuccessfully relaying due to ECONNRESET" do
+ stub_request(:post, URL).to_raise Errno::ECONNRESET.new
+
+ result = @relayer.relay(file_fixture("welcome.eml").read)
+ assert_equal "4.4.2", result.status_code
+ assert_equal "Network error relaying to ingress: Connection reset by peer", result.message
+ assert_not result.success?
+ assert_predicate result, :failure?
+ end
+
+ test "unsuccessfully relaying due to connection failure" do
+ stub_request(:post, URL).to_raise SocketError.new("Failed to open TCP connection to example.com:443")
+
+ result = @relayer.relay(file_fixture("welcome.eml").read)
+ assert_equal "4.4.2", result.status_code
+ assert_equal "Network error relaying to ingress: Failed to open TCP connection to example.com:443", result.message
+ assert_not result.success?
+ assert_predicate result, :failure?
+ end
+
+ test "unsuccessfully relaying due to client-side timeout" do
+ stub_request(:post, URL).to_timeout
+
+ result = @relayer.relay(file_fixture("welcome.eml").read)
+ assert_equal "4.4.2", result.status_code
+ assert_equal "Timed out relaying to ingress", result.message
+ assert_not result.success?
+ assert_predicate result, :failure?
+ end
+
+ test "unsuccessfully relaying due to an unhandled exception" do
+ stub_request(:post, URL).to_raise StandardError.new("Something went wrong")
+
+ result = @relayer.relay(file_fixture("welcome.eml").read)
+ assert_equal "4.0.0", result.status_code
+ assert_equal "Error relaying to ingress: Something went wrong", result.message
+ assert_not result.success?
+ assert_predicate result, :failure?
+ end
+ end
+end
diff --git a/actionmailbox/test/unit/router_test.rb b/actionmailbox/test/unit/router_test.rb
new file mode 100644
index 0000000000000..d1f46c18c8bdd
--- /dev/null
+++ b/actionmailbox/test/unit/router_test.rb
@@ -0,0 +1,163 @@
+# frozen_string_literal: true
+
+require_relative "../test_helper"
+
+class RootMailbox < ActionMailbox::Base
+ def process
+ $processed_by = self.class.to_s
+ $processed_mail = mail
+ end
+end
+
+class FirstMailbox < RootMailbox
+end
+
+class SecondMailbox < RootMailbox
+end
+
+module Nested
+ class FirstMailbox < RootMailbox
+ end
+end
+
+class FirstMailboxAddress
+ def match?(inbound_email)
+ inbound_email.mail.to.include?("replies-class@example.com")
+ end
+end
+
+module ActionMailbox
+ class RouterTest < ActiveSupport::TestCase
+ setup do
+ @router = ActionMailbox::Router.new
+ $processed_by = $processed_mail = nil
+ end
+
+ test "single string route" do
+ @router.add_routes("first@example.com" => :first)
+
+ inbound_email = create_inbound_email_from_mail(to: "first@example.com", subject: "This is a reply")
+ @router.route inbound_email
+ assert_equal "FirstMailbox", $processed_by
+ assert_equal inbound_email.mail, $processed_mail
+ end
+
+ test "single string routing on cc" do
+ @router.add_routes("first@example.com" => :first)
+
+ inbound_email = create_inbound_email_from_mail(to: "someone@example.com", cc: "first@example.com", subject: "This is a reply")
+ @router.route inbound_email
+ assert_equal "FirstMailbox", $processed_by
+ assert_equal inbound_email.mail, $processed_mail
+ end
+
+ test "single string routing on bcc" do
+ @router.add_routes("first@example.com" => :first)
+
+ inbound_email = create_inbound_email_from_mail(to: "someone@example.com", bcc: "first@example.com", subject: "This is a reply")
+ @router.route inbound_email
+ assert_equal "FirstMailbox", $processed_by
+ assert_equal inbound_email.mail, $processed_mail
+ end
+
+ test "single string routing case-insensitively" do
+ @router.add_routes("first@example.com" => :first)
+
+ inbound_email = create_inbound_email_from_mail(to: "FIRST@example.com", subject: "This is a reply")
+ @router.route inbound_email
+ assert_equal "FirstMailbox", $processed_by
+ assert_equal inbound_email.mail, $processed_mail
+ end
+
+ test "multiple string routes" do
+ @router.add_routes("first@example.com" => :first, "second@example.com" => :second)
+
+ inbound_email = create_inbound_email_from_mail(to: "first@example.com", subject: "This is a reply")
+ @router.route inbound_email
+ assert_equal "FirstMailbox", $processed_by
+ assert_equal inbound_email.mail, $processed_mail
+
+ inbound_email = create_inbound_email_from_mail(to: "second@example.com", subject: "This is a reply")
+ @router.route inbound_email
+ assert_equal "SecondMailbox", $processed_by
+ assert_equal inbound_email.mail, $processed_mail
+ end
+
+ test "single regexp route" do
+ @router.add_routes(/replies-\w+@example.com/ => :first, "replies-nowhere@example.com" => :second)
+
+ inbound_email = create_inbound_email_from_mail(to: "replies-okay@example.com", subject: "This is a reply")
+ @router.route inbound_email
+ assert_equal "FirstMailbox", $processed_by
+ end
+
+ test "single proc route" do
+ @router.add_route \
+ ->(inbound_email) { inbound_email.mail.to.include?("replies-proc@example.com") },
+ to: :second
+
+ @router.route create_inbound_email_from_mail(to: "replies-proc@example.com", subject: "This is a reply")
+ assert_equal "SecondMailbox", $processed_by
+ end
+
+ test "address class route" do
+ @router.add_route FirstMailboxAddress.new, to: :first
+ @router.route create_inbound_email_from_mail(to: "replies-class@example.com", subject: "This is a reply")
+ assert_equal "FirstMailbox", $processed_by
+ end
+
+ test "string route to nested mailbox" do
+ @router.add_route "first@example.com", to: "nested/first"
+
+ inbound_email = create_inbound_email_from_mail(to: "first@example.com", subject: "This is a reply")
+ @router.route inbound_email
+ assert_equal "Nested::FirstMailbox", $processed_by
+ end
+
+ test "all as the only route" do
+ @router.add_route :all, to: :first
+ @router.route create_inbound_email_from_mail(to: "replies-class@example.com", subject: "This is a reply")
+ assert_equal "FirstMailbox", $processed_by
+ end
+
+ test "all as the second route" do
+ @router.add_route FirstMailboxAddress.new, to: :first
+ @router.add_route :all, to: :second
+
+ @router.route create_inbound_email_from_mail(to: "replies-class@example.com", subject: "This is a reply")
+ assert_equal "FirstMailbox", $processed_by
+
+ @router.route create_inbound_email_from_mail(to: "elsewhere@example.com", subject: "This is a reply")
+ assert_equal "SecondMailbox", $processed_by
+ end
+
+ test "missing route" do
+ inbound_email = create_inbound_email_from_mail(to: "going-nowhere@example.com", subject: "This is a reply")
+ assert_raises(ActionMailbox::Router::RoutingError) do
+ @router.route inbound_email
+ end
+ assert_predicate inbound_email, :bounced?
+ end
+
+ test "invalid address" do
+ error = assert_raises(ArgumentError) do
+ @router.add_route Array.new, to: :first
+ end
+ assert_equal "Expected a Symbol, String, Regexp, Proc, or matchable, got []", error.message
+ end
+
+ test "single string mailbox_for" do
+ @router.add_routes("first@example.com" => :first)
+
+ inbound_email = create_inbound_email_from_mail(to: "first@example.com", subject: "This is a reply")
+ assert_equal FirstMailbox, @router.mailbox_for(inbound_email)
+ end
+
+ test "mailbox_for with no matches" do
+ @router.add_routes("first@example.com" => :first)
+
+ inbound_email = create_inbound_email_from_mail(to: "second@example.com", subject: "This is a reply")
+ assert_nil @router.mailbox_for(inbound_email)
+ end
+ end
+end
diff --git a/actionmailbox/test/unit/test_helper_test.rb b/actionmailbox/test/unit/test_helper_test.rb
new file mode 100644
index 0000000000000..d392c80133789
--- /dev/null
+++ b/actionmailbox/test/unit/test_helper_test.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+require_relative "../test_helper"
+
+module ActionMailbox
+ class TestHelperTest < ActiveSupport::TestCase
+ test "multi-part mail can be built in tests using a block" do
+ inbound_email = create_inbound_email_from_mail do
+ to "test@example.com"
+ from "hello@example.com"
+
+ text_part do
+ body "Hello, world"
+ end
+
+ html_part do
+ body "
Hello, world
"
+ end
+ end
+
+ mail = inbound_email.mail
+
+ expected_mail_text_part = <<~TEXT.chomp
+ Content-Type: text/plain;\r
+ charset=UTF-8\r
+ Content-Transfer-Encoding: 7bit\r
+ \r
+ Hello, world
+ TEXT
+
+ expected_mail_html_part = <<~HTML.chomp
+ Content-Type: text/html;\r
+ charset=UTF-8\r
+ Content-Transfer-Encoding: 7bit\r
+ \r
+
Hello, world
+ HTML
+
+ assert_equal 2, mail.parts.count
+ assert_equal expected_mail_text_part, mail.text_part.to_s
+ assert_equal expected_mail_html_part, mail.html_part.to_s
+ end
+ end
+end
diff --git a/actionmailer/CHANGELOG.md b/actionmailer/CHANGELOG.md
index da5d5c4086b2b..96299d1fedd08 100644
--- a/actionmailer/CHANGELOG.md
+++ b/actionmailer/CHANGELOG.md
@@ -1,501 +1,2 @@
-## Rails 4.0.0 (unreleased) ##
-* No changes
-
-
-## Rails 3.2.1 (January 26, 2012) ##
-
-* No changes.
-
-
-## Rails 3.2.0 (January 20, 2012) ##
-
-* Upgrade mail version to 2.4.0 *ML*
-
-* Remove Old ActionMailer API *Josh Kalderimis*
-
-
-## Rails 3.1.3 (November 20, 2011) ##
-
-* No changes
-
-
-## Rails 3.1.2 (November 18, 2011) ##
-
-* No changes
-
-
-## Rails 3.1.1 (October 7, 2011) ##
-
-* No changes
-
-
-## Rails 3.1.0 (August 30, 2011) ##
-
-* No changes
-
-
-## Rails 3.0.11 (November 18, 2011) ##
-
-* No changes.
-
-
-## Rails 3.0.10 (August 16, 2011) ##
-
-* No changes.
-
-
-## Rails 3.0.9 (June 16, 2011) ##
-
-* No changes.
-
-
-## Rails 3.0.8 (June 7, 2011) ##
-
-* Mail dependency increased to 2.2.19
-
-
-## Rails 3.0.7 (April 18, 2011) ##
-
-* remove AM delegating register_observer and register_interceptor to Mail *Josh Kalderimis*
-
-
-## Rails 3.0.6 (April 5, 2011) ##
-
-* Don't allow i18n to change the minor version, version now set to ~> 0.5.0 *Santiago Pastorino*
-
-
-## Rails 3.0.5 (February 26, 2011) ##
-
-* No changes.
-
-
-## Rails 3.0.4 (February 8, 2011) ##
-
-* No changes.
-
-
-## Rails 3.0.3 (November 16, 2010) ##
-
-* No changes.
-
-
-## Rails 3.0.2 (November 15, 2010) ##
-
-* No changes
-
-
-## Rails 3.0.1 (October 15, 2010) ##
-
-* No Changes, just a version bump.
-
-
-## Rails 3.0.0 (August 29, 2010) ##
-
-* subject is automatically looked up on I18n using mailer_name and action_name as scope as in t(".subject") *JK*
-
-* Changed encoding behaviour of mail, so updated tests in actionmailer and bumped mail version to 2.2.1 *ML*
-
-* Added ability to pass Proc objects to the defaults hash *ML*
-
-* Removed all quoting.rb type files from ActionMailer and put Mail 2.2.0 in instead *ML*
-
-* Lot of updates to various test cases that now work better with the new Mail and so have different expectations
-
-* Added interceptors and observers from Mail *ML*
-
- ActionMailer::Base.register_interceptor calls Mail.register_interceptor
- ActionMailer::Base.register_observer calls Mail.register_observer
-
-* Mail::Part now no longer has nil as a default charset, it is always set to something, and defaults to UTF-8
-
-* Added explict setting of charset in set_fields! method to make sure Mail has the user defined default
-
-* Removed quoting.rb and refactored for Mail to take responsibility of all quoting and auto encoding requirements for the header.
-
-* Fixed several tests which had incorrect encoding.
-
-* Changed all utf-8 to UTF-8 for consistency
-
-* Whole new API added with tests. See base.rb for full details. Old API is deprecated.
-
-* The Mail::Message class has helped methods for all the field types that return 'common' defaults for the common use case, so to get the subject, mail.subject will give you a string, mail.date will give you a DateTime object, mail.from will give you an array of address specs (mikel@test.lindsaar.net) etc. If you want to access the field object itself, call mail[:field_name] which will return the field object you want, which you can then chain, like mail[:from].formatted
-
-* Mail#content_type now returns the content_type field as a string. If you want the mime type of a mail, then you call Mail#mime_type (eg, text/plain), if you want the parameters of the content type field, you call Mail#content_type_parameters which gives you a hash, eg {'format' => 'flowed', 'charset' => 'utf-8'}
-
-* ActionMailer::Base :default_implicit_parts_order now is in the sequence of the order you want, no reversing of ordering takes place. The default order now is text/plain, then text/enriched, then text/html and then any other part that is not one of these three.
-
-* Mail does not have "quoted_body", "quoted_subject" etc. All of these are accessed via body.encoded, subject.encoded etc
-
-* Every object in a Mail object returns an object, never a string. So Mail.body returns a Mail::Body class object, need to call #encoded or #decoded to get the string you want.
-* Mail::Message#set_content_type does not exist, it is simply Mail::Message#content_type
-
-* Every mail message gets a unique message_id unless you specify one, had to change all the tests that check for equality with expected.encoded == actual.encoded to first replace their message_ids with control values
-
-* Mail now has a proper concept of parts, remove the ActionMailer::Part and ActionMailer::PartContainer classes
-
-* Calling #encoded on any object returns it as a string ready to go into the output stream of an email, this means it includes the \r\n at the end of the lines and the object is pre-wrapped with \r\n\t if it is a header field. Also, the "encoded" value includes the field name if it is a header field.
-
-* Attachments are only the actual attachment, with filename etc. A part contains an attachment. The part has the content_type etc. So attachments.last.content_type is invalid. But parts.last.content_type
-
-* There is no idea of a "sub_head" in Mail. A part is just a Message with some extra functionality, so it just has a "header" like a normal mail message
-
-
-## 2.3.2 Final (March 15, 2009) ##
-
-* Fixed that ActionMailer should send correctly formatted Return-Path in MAIL FROM for SMTP #1842 *Matt Jones*
-
-* Fixed RFC-2045 quoted-printable bug #1421 *squadette*
-
-* Fixed that no body charset would be set when there are attachments present #740 *Paweł Kondzior*
-
-
-## 2.2.1 RC2 (November 14th, 2008) ##
-
-* Turn on STARTTLS if it is available in Net::SMTP (added in Ruby 1.8.7) and the SMTP server supports it (This is required for Gmail's SMTP server) #1336 *Grant Hollingworth*
-
-
-## 2.2.0 RC1 (October 24th, 2008) ##
-
-* Add layout functionality to mailers *Pratik Naik*
-
- Mailer layouts behaves just like controller layouts, except layout names need to
- have '_mailer' postfix for them to be automatically picked up.
-
-
-## 2.1.0 (May 31st, 2008) ##
-
-* Fixed that a return-path header would be ignored #7572 *joost*
-
-* Less verbose mail logging: just recipients for :info log level; the whole email for :debug only. #8000 *iaddict, Tarmo Tänav*
-
-* Updated TMail to version 1.2.1 *Mikel Lindsaar*
-
-* Fixed that you don't have to call super in ActionMailer::TestCase#setup #10406 *jamesgolick*
-
-
-## 2.0.2 (December 16th, 2007) ##
-
-* Included in Rails 2.0.2
-
-
-## 2.0.1 (December 7th, 2007) ##
-
-* Update ActionMailer so it treats ActionView the same way that ActionController does. Closes #10244 *Rick Olson*
-
- * Pass the template_root as an array as ActionView's view_path
- * Request templates with the "#{mailer_name}/#{action}" as opposed to just "#{action}"
-
-* Fixed that partials would be broken when using text.plain.erb as the extension #10130 *java*
-
-* Update README to use new smtp settings configuration API. Closes #10060 *psq*
-
-* Allow ActionMailer subclasses to individually set their delivery method (so two subclasses can have different delivery methods) #10033 *Zach Dennis*
-
-* Update TMail to v1.1.0. Use an updated version of TMail if available. *Mikel Lindsaar*
-
-* Introduce a new base test class for testing Mailers. ActionMailer::TestCase *Michael Koziarski*
-
-* Fix silent failure of rxml templates. #9879 *jstewart*
-
-* Fix attachment decoding when using the TMail C extension. #7861 *orangechicken*
-
-* Increase mail delivery test coverage. #8692 *Kamal Fariz Mahyuddin*
-
-* Register alternative template engines using ActionMailer::Base.register_template_extension('haml'). #7534 *cwd, Josh Peek*
-
-* Only load ActionController::UrlWriter if ActionController is present *Rick Olson*
-
-* Make sure parsed emails recognized attachments nested inside multipart parts. #6714 *Jamis Buck*
-
-* Allow mailer actions named send by using __send__ internally. #6467 *iGEL*
-
-* Add assert_emails and assert_no_emails to test the number of emails delivered. #6479 *Jonathan Viney*
- # Assert total number of emails delivered:
- assert_emails 0
- ContactMailer.deliver_contact
- assert_emails 1
-
- # Assert number of emails delivered within a block:
- assert_emails 1 do
- post :signup, :name => 'Jonathan'
- end
-
-
-## 1.3.3 (March 12th, 2007) ##
-
-* Depend on Action Pack 1.13.3
-
-
-## 1.3.2 (February 5th, 2007) ##
-
-* Deprecate server_settings renaming it to smtp_settings, add sendmail_settings to allow you to override the arguments to and location of the sendmail executable. *Michael Koziarski*
-
-
-## 1.3.1 (January 16th, 2007) ##
-
-* Depend on Action Pack 1.13.1
-
-
-## 1.3.0 (January 16th, 2007) ##
-
-* Make mime version default to 1.0. closes #2323 *ror@andreas-s.net*
-
-* Make sure quoted-printable text is decoded correctly when only portions of the text are encoded. closes #3154. *jon@siliconcircus.com*
-
-* Make sure DOS newlines in quoted-printable text are normalized to unix newlines before unquoting. closes #4166 and #4452. *Jamis Buck*
-
-* Fixed that iconv decoding should catch InvalidEncoding #3153 *jon@siliconcircus.com*
-
-* Tighten rescue clauses. #5985 *james@grayproductions.net*
-
-* Automatically included ActionController::UrlWriter, such that URL generation can happen within ActionMailer controllers. *David Heinemeier Hansson*
-
-* Replace Reloadable with Reloadable::Deprecated. *Nicholas Seckar*
-
-* Mailer template root applies to a class and its subclasses rather than acting globally. #5555 *somekool@gmail.com*
-
-* Resolve action naming collision. #5520 *ssinghi@kreeti.com*
-
-* ActionMailer::Base documentation rewrite. Closes #4991 *Kevin Clark, Marcel Molina Jr.*
-
-* Replace alias method chaining with Module#alias_method_chain. *Marcel Molina Jr.*
-
-* Replace Ruby's deprecated append_features in favor of included. *Marcel Molina Jr.*
-
-* Correct spurious documentation example code which results in a SyntaxError. *Marcel Molina Jr.*
-
-
-## 1.2.1 (April 6th, 2006) ##
-
-* Be part of Rails 1.1.1
-
-
-## 1.2.0 (March 27th, 2006) ##
-
-* Nil charset caused subject line to be improperly quoted in implicitly multipart messages #2662 *ehalvorsen+rails@runbox.com*
-
-* Parse content-type apart before using it so that sub-parts of the header can be set correctly #2918 *Jamis Buck*
-
-* Make custom headers work in subparts #4034 *elan@bluemandrill.com*
-
-* Template paths with dot chars in them no longer mess up implicit template selection for multipart messages #3332 *Chad Fowler*
-
-* Make sure anything with content-disposition of "attachment" is passed to the attachment presenter when parsing an email body *Jamis Buck*
-
-* Make sure TMail#attachments includes anything with content-disposition of "attachment", regardless of content-type *Jamis Buck*
-
-
-## 1.1.5 (December 13th, 2005) ##
-
-* Become part of Rails 1.0
-
-
-## 1.1.4 (December 7th, 2005) ##
-
-* Rename Version constant to VERSION. #2802 *Marcel Molina Jr.*
-
-* Stricter matching for implicitly multipart filenames excludes files ending in unsupported extensions (such as foo.rhtml.bak) and without a two-part content type (such as foo.text.rhtml or foo.text.really.plain.rhtml). #2398 *Dave Burt , Jeremy Kemper*
-
-
-## 1.1.3 (November 7th, 2005) ##
-
-* Allow Mailers to have custom initialize methods that set default instance variables for all mail actions #2563 *mrj@bigpond.net.au*
-
-
-## 1.1.2 (October 26th, 2005) ##
-
-* Upgraded to Action Pack 1.10.2
-
-
-## 1.1.1 (October 19th, 2005) ##
-
-* Upgraded to Action Pack 1.10.1
-
-
-## 1.1.0 (October 16th, 2005) ##
-
-* Update and extend documentation (rdoc)
-
-* Minero Aoki made TMail available to Rails/ActionMailer under the MIT license (instead of LGPL) *RubyConf '05*
-
-* Austin Ziegler made Text::Simple available to Rails/ActionMailer under a MIT-like licens *See rails ML, subject "Text::Format Licence Exception" on Oct 15, 2005*
-
-* Fix vendor require paths to prevent files being required twice
-
-* Don't add charset to content-type header for a part that contains subparts (for AOL compatibility) #2013 *John Long*
-
-* Preserve underscores when unquoting message bodies #1930
-
-* Encode multibyte characters correctly #1894
-
-* Multipart messages specify a MIME-Version header automatically #2003 *John Long*
-
-* Add a unified render method to ActionMailer (delegates to ActionView::Base#render)
-
-* Move mailer initialization to a separate (overridable) method, so that subclasses may alter the various defaults #1727
-
-* Look at content-location header (if available) to determine filename of attachments #1670
-
-* ActionMailer::Base.deliver(email) had been accidentally removed, but was documented in the Rails book #1849
-
-* Fix problem with sendmail delivery where headers should be delimited by \n characters instead of \r\n, which confuses some mail readers #1742 *Kent Sibilev*
-
-
-## 1.0.1 (11 July, 2005) ##
-
-* Bind to Action Pack 1.9.1
-
-
-## 1.0.0 (6 July, 2005) ##
-
-* Avoid adding nil header values #1392
-
-* Better multipart support with implicit multipart/alternative and sorting of subparts *John Long*
-
-* Allow for nested parts in multipart mails #1570 *Flurin Egger*
-
-* Normalize line endings in outgoing mail bodies to "\n" #1536 *John Long*
-
-* Allow template to be explicitly specified #1448 *tuxie@dekadance.se*
-
-* Allow specific "multipart/xxx" content-type to be set on multipart messages #1412 *Flurin Egger*
-
-* Unquoted @ characters in headers are now accepted in spite of RFC 822 #1206
-
-* Helper support (borrowed from ActionPack)
-
-* Silently ignore Errno::EINVAL errors when converting text.
-
-* Don't cause an error when parsing an encoded attachment name #1340 *lon@speedymac.com*
-
-* Nested multipart message parts are correctly processed in TMail::Mail#body
-
-* BCC headers are removed when sending via SMTP #1402
-
-* Added 'content_type' accessor, to allow content type to be set on a per-message basis. content_type defaults to "text/plain".
-
-* Silently ignore Iconv::IllegalSequence errors when converting text #1341 *lon@speedymac.com*
-
-* Support attachments and multipart messages.
-
-* Added new accessors for the various mail properties.
-
-* Fix to only perform the charset conversion if a 'from' and a 'to' charset are given (make no assumptions about what the charset was) #1276 *Jamis Buck*
-
-* Fix attachments and content-type problems #1276 *Jamis Buck*
-
-* Fixed the TMail#body method to look at the content-transfer-encoding header and unquote the body according to the rules it specifies #1265 *Jamis Buck*
-
-* Added unquoting even if the iconv lib can't be loaded--in that case, only the charset conversion is skipped #1265 *Jamis Buck*
-
-* Added automatic decoding of base64 bodies #1214 *Jamis Buck*
-
-* Added that delivery errors are caught in a way so the mail is still returned whether the delivery was successful or not
-
-* Fixed that email address like "Jamis Buck, M.D." would cause the quoter to generate emails resulting in "bad address" errors from the mail server #1220 *Jamis Buck*
-
-
-## 0.9.1 (20th April, 2005) ##
-
-* Depend on Action Pack 1.8.1
-
-
-## 0.9.0 (19th April, 2005) ##
-
-* Added that deliver_* will now return the email that was sent
-
-* Added that quoting to UTF-8 only happens if the characters used are in that range #955 *Jamis Buck*
-
-* Fixed quoting for all address headers, not just to #955 *Jamis Buck*
-
-* Fixed unquoting of emails that doesn't have an explicit charset #1036 *wolfgang@stufenlos.net*
-
-
-## 0.8.1 (27th March, 2005) ##
-
-* Fixed that if charset was found that the end of a mime part declaration TMail would throw an error #919 *lon@speedymac.com*
-
-* Fixed that TMail::Unquoter would fail to recognize quoting method if it was in lowercase #919 *lon@speedymac.com*
-
-* Fixed that TMail::Encoder would fail when it attempts to parse e-mail addresses which are encoded using something other than the messages encoding method #919 *lon@speedymac.com*
-
-* Added rescue for missing iconv library and throws warnings if subject/body is called on a TMail object without it instead
-
-
-## 0.8.0 (22th March, 2005) ##
-
-* Added framework support for processing incoming emails with an Action Mailer class. See example in README.
-
-
-## 0.7.1 (7th March, 2005) ##
-
-* Bind to newest Action Pack (1.5.1)
-
-
-## 0.7.0 (24th February, 2005) ##
-
-* Added support for charsets for both subject and body. The default charset is now UTF-8 #673 [Jamis Buck]. Examples:
-
- def iso_charset(recipient)
- @recipients = recipient
- @subject = "testing iso charsets"
- @from = "system@loudthinking.com"
- @body = "Nothing to see here."
- @charset = "iso-8859-1"
- end
-
- def unencoded_subject(recipient)
- @recipients = recipient
- @subject = "testing unencoded subject"
- @from = "system@loudthinking.com"
- @body = "Nothing to see here."
- @encode_subject = false
- @charset = "iso-8859-1"
- end
-
-
-## 0.6.1 (January 18th, 2005) ##
-
-* Fixed sending of emails to use Tmail#from not the deprecated Tmail#from_address
-
-
-## 0.6 (January 17th, 2005) ##
-
-* Fixed that bcc and cc should be settable through @bcc and @cc -- not just @headers["Bcc"] and @headers["Cc"] #453 *Eric Hodel*
-
-* Fixed Action Mailer to be "warnings safe" so you can run with ruby -w and not get framework warnings #453 *Eric Hodel*
-
-
-## 0.5 ##
-
-* Added access to custom headers, like cc, bcc, and reply-to #268 [Andreas Schwarz]. Example:
-
- def post_notification(recipients, post)
- @recipients = recipients
- @from = post.author.email_address_with_name
- @headers["bcc"] = SYSTEM_ADMINISTRATOR_EMAIL
- @headers["reply-to"] = "notifications@example.com"
- @subject = "[#{post.account.name} #{post.title}]"
- @body["post"] = post
- end
-
-## 0.4 (5) ##
-
-* Consolidated the server configuration options into Base#server_settings= and expanded that with controls for authentication and more *Marten*
- NOTE: This is an API change that could potentially break your application if you used the old application form. Please do change!
-
-* Added Base#deliveries as an accessor for an array of emails sent out through that ActionMailer class when using the :test delivery option. *Jeremy Kemper*
-
-* Added Base#perform_deliveries= which can be set to false to turn off the actual delivery of the email through smtp or sendmail.
- This is especially useful for functional testing that shouldn't send off real emails, but still trigger delivery_* methods.
-
-* Added option to specify delivery method with Base#delivery_method=. Default is :smtp and :sendmail is currently the only other option.
- Sendmail is assumed to be present at "/usr/sbin/sendmail" if that option is used. *Kent Sibilev*
-
-* Dropped "include TMail" as it added to much baggage into the default namespace (like Version) *Chad Fowler*
-
-
-## 0.3 ##
-
-* First release
+Please check [8-0-stable](https://github.com/rails/rails/blob/8-0-stable/actionmailer/CHANGELOG.md) for previous changes.
diff --git a/actionmailer/MIT-LICENSE b/actionmailer/MIT-LICENSE
index 810daf856c435..7be9ac633faf0 100644
--- a/actionmailer/MIT-LICENSE
+++ b/actionmailer/MIT-LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2004-2012 David Heinemeier Hansson
+Copyright (c) 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 755717cfba945..d26365048b4f7 100644
--- a/actionmailer/README.rdoc
+++ b/actionmailer/README.rdoc
@@ -1,6 +1,6 @@
= Action Mailer -- Easy email delivery and testing
-Action Mailer is a framework for designing email-service layers. These layers
+Action Mailer is a framework for designing email service layers. These layers
are used to consolidate code for sending out forgotten passwords, welcome
wishes on signup, invoices for billing, and any other use case that requires
a written notification to either a person or another system.
@@ -13,6 +13,8 @@ Additionally, an Action Mailer class can be used to process incoming email,
such as allowing a blog to accept new posts from an email (which could even
have been sent from a phone).
+You can read more about Action Mailer in the {Action Mailer Basics}[https://guides.rubyonrails.org/action_mailer_basics.html] guide.
+
== Sending emails
The framework works by initializing any instance variables you want to be
@@ -22,12 +24,12 @@ the email.
This can be as simple as:
class Notifier < ActionMailer::Base
- delivers_from 'system@loudthinking.com'
+ default from: 'system@loudthinking.com'
def welcome(recipient)
@recipient = recipient
- mail(:to => recipient,
- :subject => "[Signed up] Welcome #{recipient}")
+ mail(to: recipient,
+ subject: "[Signed up] Welcome #{recipient}")
end
end
@@ -61,77 +63,48 @@ generated would look like this:
Thank you for signing up!
-In previous version of Rails you would call create_method_name and
-deliver_method_name. Rails 3.0 has a much simpler interface - you
-simply call the method and optionally call +deliver+ on the return value.
+In order to send mails, you simply call the method and then call +deliver_now+ on the return value.
Calling the method returns a Mail Message object:
- message = Notifier.welcome # => Returns a Mail::Message object
- message.deliver # => delivers the email
+ message = Notifier.welcome("david@loudthinking.com") # => Returns a Mail::Message object
+ message.deliver_now # => delivers the email
Or you can just chain the methods together like:
- Notifier.welcome.deliver # Creates the email and sends it immediately
+ Notifier.welcome("david@loudthinking.com").deliver_now # Creates the email and sends it immediately
== Setting defaults
-It is possible to set default values that will be used in every method in your Action Mailer class. To implement this functionality, you just call the public class method default which you get for free from ActionMailer::Base. This method accepts a Hash as the parameter. You can use any of the headers e-mail messages has, like :from as the key. You can also pass in a string as the key, like "Content-Type", but Action Mailer does this out of the box for you, so you won't need to worry about that. Finally, it is also possible to pass in a Proc that will get evaluated when it is needed.
+It is possible to set default values that will be used in every method in your
+Action Mailer class. To implement this functionality, you just call the public
+class method +default+ which you get for free from ActionMailer::Base.
+This method accepts a Hash as the parameter. You can use any of the headers,
+email messages have, like +:from+ as the key. You can also pass in a string as
+the key, like "Content-Type", but Action Mailer does this out of the box for you,
+so you won't need to worry about that. Finally, it is also possible to pass in a
+Proc that will get evaluated when it is needed.
-Note that every value you set with this method will get over written if you use the same key in your mailer method.
+Note that every value you set with this method will get overwritten if you use the
+same key in your mailer method.
Example:
class AuthenticationMailer < ActionMailer::Base
- default :from => "awesome@application.com", :subject => Proc.new { "E-mail was generated at #{Time.now}" }
+ default from: "awesome@application.com", subject: Proc.new { "E-mail was generated at #{Time.now}" }
.....
end
-== Receiving emails
-
-To receive emails, you need to implement a public instance method called receive that takes an
-email object as its single parameter. The Action Mailer framework has a corresponding class method,
-which is also called receive, that accepts a raw, unprocessed email as a string, which it then turns
-into the email object and calls the receive instance method.
-
-Example:
-
- class Mailman < ActionMailer::Base
- def receive(email)
- page = Page.find_by_address(email.to.first)
- page.emails.create(
- :subject => email.subject, :body => email.body
- )
-
- if email.has_attachments?
- email.attachments.each do |attachment|
- page.attachments.create({
- :file => attachment, :description => email.subject
- })
- end
- end
- end
- end
-
-This Mailman can be the target for Postfix or other MTAs. In Rails, you would use the runner in the
-trivial case like this:
-
- rails runner 'Mailman.receive(STDIN.read)'
-
-However, invoking Rails in the runner for each mail to be received is very resource intensive. A single
-instance of Rails should be run within a daemon, if it is going to be utilized to process more than just
-a limited number of email.
-
== Configuration
The Base class has the full list of configuration options. Here's an example:
ActionMailer::Base.smtp_settings = {
- :address => 'smtp.yourserver.com', # default: localhost
- :port => '25', # default: 25
- :user_name => 'user',
- :password => 'pass',
- :authentication => :plain # :plain, :login or :cram_md5
+ address: 'smtp.yourserver.com', # default: localhost
+ port: '25', # default: 25
+ user_name: 'user',
+ password: 'pass',
+ authentication: :plain # :plain, :login or :cram_md5
}
@@ -139,27 +112,30 @@ The Base class has the full list of configuration options. Here's an example:
The latest version of Action Mailer can be installed with RubyGems:
- % [sudo] gem install actionmailer
+ $ 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
+* https://github.com/rails/rails/tree/main/actionmailer
== License
Action Mailer is released under the MIT license:
-* http://www.opensource.org/licenses/MIT
+* https://opensource.org/licenses/MIT
== Support
API documentation is at
-* http://api.rubyonrails.org
+* https://api.rubyonrails.org
-Bug reports and feature requests can be filed with the rest 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
+Feature requests should be discussed on the rails-core mailing list here:
+
+* https://discuss.rubyonrails.org/c/rubyonrails-core
diff --git a/actionmailer/Rakefile b/actionmailer/Rakefile
old mode 100755
new mode 100644
index 8f5aeb960350f..8c6181e659337
--- a/actionmailer/Rakefile
+++ b/actionmailer/Rakefile
@@ -1,37 +1,26 @@
-#!/usr/bin/env rake
-require 'rake/testtask'
-require 'rake/packagetask'
-require 'rubygems/package_task'
+# frozen_string_literal: true
+
+require "rake/testtask"
desc "Default Task"
-task :default => [ :test ]
+task default: [ :test ]
+
+ENV["RAILS_MINITEST_PLUGIN"] = "true"
# 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.options = "--profile" if ENV["CI"]
+ t.ruby_opts = ["--dev"] if defined?(JRUBY_VERSION)
}
namespace :test do
task :isolated do
- ruby = File.join(*RbConfig::CONFIG.values_at('bindir', 'RUBY_INSTALL_NAME'))
Dir.glob("test/**/*_test.rb").all? do |file|
- sh(ruby, '-Ilib:test', file)
- end or raise "Failures"
+ sh(Gem.ruby, "-w", "-Ilib:test", file)
+ end || raise("Failures")
end
end
-
-spec = eval(File.read('actionmailer.gemspec'))
-
-Gem::PackageTask.new(spec) do |p|
- p.gem_spec = spec
-end
-
-desc "Release to gemcutter"
-task :release => :package do
- require 'rake/gemcutter'
- Rake::Gemcutter::Tasks.new(spec).define
- Rake::Task['gem:push'].invoke
-end
diff --git a/actionmailer/actionmailer.gemspec b/actionmailer/actionmailer.gemspec
index c605f1ff04df5..88a9be08c1cf7 100644
--- a/actionmailer/actionmailer.gemspec
+++ b/actionmailer/actionmailer.gemspec
@@ -1,21 +1,43 @@
-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.required_ruby_version = '>= 1.9.3'
+ s.summary = "Email composition and delivery framework (part of Rails)."
+ s.description = "Email on Rails. Compose, deliver, and test emails using the familiar controller/view pattern. First-class support for multipart email and attachments."
+
+ s.required_ruby_version = ">= 3.2.0"
+
+ s.license = "MIT"
+
+ s.author = "David Heinemeier Hansson"
+ s.email = "david@loudthinking.com"
+ s.homepage = "https://rubyonrails.org/"
+
+ s.files = Dir["CHANGELOG.md", "README.rdoc", "MIT-LICENSE", "lib/**/*"]
+ s.require_path = "lib"
+ s.requirements << "none"
+
+ s.metadata = {
+ "bug_tracker_uri" => "https://github.com/rails/rails/issues",
+ "changelog_uri" => "https://github.com/rails/rails/blob/v#{version}/actionmailer/CHANGELOG.md",
+ "documentation_uri" => "https://api.rubyonrails.org/v#{version}/",
+ "mailing_list_uri" => "https://discuss.rubyonrails.org/c/rubyonrails-talk",
+ "source_code_uri" => "https://github.com/rails/rails/tree/v#{version}/actionmailer",
+ "rubygems_mfa_required" => "true",
+ }
- s.author = 'David Heinemeier Hansson'
- s.email = 'david@loudthinking.com'
- s.homepage = 'http://www.rubyonrails.org/'
+ # NOTE: Please read our dependency guidelines before updating versions:
+ # https://edgeguides.rubyonrails.org/security.html#dependency-management-and-cves
- s.files = Dir['CHANGELOG.md', 'README.rdoc', 'MIT-LICENSE', 'lib/**/*']
- s.require_path = 'lib'
- s.requirements << 'none'
+ s.add_dependency "activesupport", version
+ s.add_dependency "actionpack", version
+ s.add_dependency "actionview", version
+ s.add_dependency "activejob", version
- s.add_dependency('actionpack', version)
- s.add_dependency('mail', '~> 2.4.1')
+ s.add_dependency "mail", ">= 2.8.0"
+ s.add_dependency "rails-dom-testing", "~> 2.2"
end
diff --git a/actionmailer/bin/test b/actionmailer/bin/test
new file mode 100755
index 0000000000000..c53377cc970f4
--- /dev/null
+++ b/actionmailer/bin/test
@@ -0,0 +1,5 @@
+#!/usr/bin/env ruby
+# 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 1045dd58ef1d5..f58a858094143 100644
--- a/actionmailer/lib/action_mailer.rb
+++ b/actionmailer/lib/action_mailer.rb
@@ -1,5 +1,7 @@
+# frozen_string_literal: true
+
#--
-# Copyright (c) 2004-2012 David Heinemeier Hansson
+# Copyright (c) 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,28 +23,57 @@
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#++
-actionpack_path = File.expand_path('../../../actionpack/lib', __FILE__)
-$:.unshift(actionpack_path) if File.directory?(actionpack_path) && !$:.include?(actionpack_path)
-
-require 'abstract_controller'
-require 'action_view'
-require 'action_mailer/version'
+require "abstract_controller"
+require "action_mailer/version"
+require "action_mailer/deprecator"
# Common Active Support usage in Action Mailer
-require 'active_support/core_ext/class'
-require 'active_support/core_ext/object/blank'
-require 'active_support/core_ext/module/attr_internal'
-require 'active_support/core_ext/module/delegation'
-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"
+# :include: ../README.rdoc
module ActionMailer
extend ::ActiveSupport::Autoload
- autoload :Collector
+ eager_autoload do
+ autoload :Collector
+ end
+
autoload :Base
+ autoload :Callbacks
autoload :DeliveryMethods
+ autoload :InlinePreviewInterceptor
autoload :MailHelper
+ autoload :Parameterized
+ autoload :Preview
+ autoload :Previews, "action_mailer/preview"
autoload :TestCase
autoload :TestHelper
+ autoload :MessageDelivery
+ autoload :MailDeliveryJob
+ autoload :QueuedDelivery
+ autoload :FormBuilder
+
+ def self.eager_load!
+ super
+
+ require "mail"
+ Mail.eager_autoload!
+
+ Base.descendants.each do |mailer|
+ mailer.eager_load! unless mailer.abstract?
+ end
+ end
+end
+
+autoload :Mime, "action_dispatch/http/mime_type"
+
+ActiveSupport.on_load(:action_view) do
+ ActionView::Base.default_formats ||= Mime::SET.symbols
+ ActionView::Template.mime_types_implementation = Mime
+ ActionView::LookupContext::DetailsKey.clear
end
diff --git a/actionmailer/lib/action_mailer/base.rb b/actionmailer/lib/action_mailer/base.rb
index 1800ff5839d8f..bc3e8f306ba52 100644
--- a/actionmailer/lib/action_mailer/base.rb
+++ b/actionmailer/lib/action_mailer/base.rb
@@ -1,33 +1,43 @@
-require 'mail'
-require 'action_mailer/collector'
-require 'active_support/core_ext/object/blank'
-require 'active_support/core_ext/string/inflections'
-require 'active_support/core_ext/hash/except'
-require 'action_mailer/log_subscriber'
-
-module ActionMailer #:nodoc:
+# frozen_string_literal: true
+
+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 \Base
+ #
# Action Mailer allows you to send email from your application using a mailer model and views.
#
- # = Mailer Models
+ # == Mailer Models
#
# To use Action Mailer, you need to create a mailer model.
#
- # $ rails generate mailer Notifier
+ # $ bin/rails generate mailer Notifier
#
- # The generated model inherits from ActionMailer::Base. Emails are defined by creating methods
- # within the model which are then used to set variables to be used in the mail template, to
- # change options on the mail, or to add attachments.
+ # The generated model inherits from ApplicationMailer which in turn
+ # inherits from +ActionMailer::Base+. A mailer model defines methods
+ # used to generate an email message. In these methods, you can set up variables to be used in
+ # the mailer views, options on the mail itself such as the :from address, and attachments.
#
- # Examples:
+ # class ApplicationMailer < ActionMailer::Base
+ # default from: 'from@example.com'
+ # layout 'mailer'
+ # end
#
- # class Notifier < ActionMailer::Base
- # default :from => 'no-reply@example.com',
- # :return_path => 'system@example.com'
+ # class NotifierMailer < ApplicationMailer
+ # default from: 'no-reply@example.com',
+ # return_path: 'system@example.com'
#
# def welcome(recipient)
# @account = recipient
- # mail(:to => recipient.email_address_with_name,
- # :bcc => ["bcc@example.com", "Order Watcher "])
+ # mail(to: recipient.email_address_with_name,
+ # bcc: ["bcc@example.com", "Order Watcher "])
# end
# end
#
@@ -40,73 +50,71 @@ module ActionMailer #:nodoc:
# in the same manner as attachments[]=
#
# * headers[]= - Allows you to specify any header field in your email such
- # as headers['X-No-Spam'] = 'True'. Note, while most fields like To:
- # From: can only appear once in an email header, other fields like X-Anything
- # can appear multiple times. If you want to change a field that can appear multiple times,
- # you need to set it to nil first so that Mail knows you are replacing it and not adding
- # another field of the same name.
+ # as headers['X-No-Spam'] = 'True'. Note that declaring a header multiple times
+ # will add many fields of the same name. Read #headers doc for more information.
#
# * headers(hash) - Allows you to specify multiple headers in your email such
# as headers({'X-No-Spam' => 'True', 'In-Reply-To' => '1234@message.id'})
#
# * mail - Allows you to specify email to be sent.
#
- # The hash passed to the mail method allows you to specify any header that a Mail::Message
- # will accept (any valid Email header including optional fields).
+ # The hash passed to the mail method allows you to specify any header that a +Mail::Message+
+ # 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.text.html.erb+ view file in a +multipart/alternative+ email.
+ # file as well as the +welcome.html.erb+ view file in a +multipart/alternative+ email.
#
# If you want to explicitly render only certain templates, pass a block:
#
- # mail(:to => user.email) do |format|
+ # mail(to: user.email) do |format|
# format.text
# format.html
# end
#
# The block syntax is also useful in providing information specific to a part:
#
- # mail(:to => user.email) do |format|
- # format.text(:content_transfer_encoding => "base64")
+ # mail(to: user.email) do |format|
+ # format.text(content_transfer_encoding: "base64")
# format.html
# end
#
# Or even to render a special view:
#
- # mail(:to => user.email) do |format|
+ # mail(to: user.email) do |format|
# format.text
# format.html { render "some_other_template" }
# end
#
- # = Mailer views
+ # == Mailer views
#
# 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 .erb file with the same
+ # To define a template to be used with a mailer, create an .erb file with the same
# name as the method in your mailer model. For example, in the mailer defined above, the template at
- # app/views/notifier/welcome.text.erb would be used to generate the email.
+ # app/views/notifier_mailer/welcome.text.erb would be used to generate the email.
#
- # Variables defined in the model are accessible as instance variables in the view.
+ # Variables defined in the methods of your mailer model are accessible as instance variables in their
+ # corresponding view.
#
# Emails by default are sent in plain text, so a sample view for our model example might look like this:
#
# Hi <%= @account.name %>,
# Thanks for joining our service! Please check back often.
#
- # You can even use Action Pack helpers in these views. For example:
+ # You can even use Action View helpers in these views. For example:
#
# You got a new note!
- # <%= truncate(@note.body, :length => 25) %>
+ # <%= truncate(@note.body, length: 25) %>
#
- # If you need to access the subject, from or the recipients in the view, you can do that through message object:
+ # If you need to access the subject, from, or the recipients in the view, you can do that through message object:
#
# You got a new note from <%= message.from %>!
- # <%= truncate(@note.body, :length => 25) %>
+ # <%= truncate(@note.body, length: 25) %>
#
#
- # = Generating URLs
+ # == Generating URLs
#
# URLs can be generated in mailer views using url_for or named routes. Unlike controllers from
# Action Pack, the mailer instance doesn't have any context about the incoming request, so you'll need
@@ -114,11 +122,11 @@ module ActionMailer #:nodoc:
#
# When using url_for you'll need to provide the :host, :controller, and :action:
#
- # <%= url_for(:host => "example.com", :controller => "welcome", :action => "greeting") %>
+ # <%= url_for(host: "example.com", controller: "welcome", action: "greeting") %>
#
# When using named routes you only need to supply the :host:
#
- # <%= users_url(http://github.com/:host%20=%3E%20%22example.com") %>
+ # <%= users_url(/service/host: "example.com") %>
#
# You should use the named_route_url style (which generates absolute URLs) and avoid using the
# named_route_path style (which generates relative URLs), since clients reading the mail will
@@ -127,35 +135,49 @@ module ActionMailer #:nodoc:
# It is also possible to set a default host that will be used in all mailers by setting the :host
# option as a configuration option in config/application.rb:
#
- # config.action_mailer.default_url_options = { :host => "example.com" }
+ # config.action_mailer.default_url_options = { host: "example.com" }
+ #
+ # You can also define a default_url_options method on individual mailers to override these
+ # default settings per-mailer.
+ #
+ # By default when config.force_ssl is +true+, URLs generated for hosts will use the HTTPS protocol.
+ #
+ # == Sending mail
+ #
+ # Once a mailer action and template are defined, you can deliver your message or defer its creation and
+ # delivery for later:
+ #
+ # NotifierMailer.welcome(User.first).deliver_now # sends the email
+ # mail = NotifierMailer.welcome(User.first) # => an ActionMailer::MessageDelivery object
+ # mail.deliver_now # generates and sends the email now
#
- # When you decide to set a default :host for your mailers, then you need to make sure to use the
- # :only_path => false option when using url_for. Since the url_for view helper
- # will generate relative URLs by default when a :host option isn't explicitly provided, passing
- # :only_path => false will ensure that absolute URLs are generated.
+ # The ActionMailer::MessageDelivery class is a wrapper around a delegate that will call
+ # your method to generate the mail. If you want direct access to the delegator, or +Mail::Message+,
+ # you can call the message method on the ActionMailer::MessageDelivery object.
#
- # = Sending mail
+ # NotifierMailer.welcome(User.first).message # => a Mail::Message object
#
- # Once a mailer action and template are defined, you can deliver your message or create it and save it
- # for delivery later:
+ # Action Mailer is nicely integrated with Active Job so you can generate and send emails in the background
+ # (example: outside of the request-response cycle, so the user doesn't have to wait on it):
#
- # Notifier.welcome(david).deliver # sends the email
- # mail = Notifier.welcome(david) # => a Mail::Message object
- # mail.deliver # sends the email
+ # NotifierMailer.welcome(User.first).deliver_later # enqueue the email sending to Active Job
+ #
+ # Note that deliver_later will execute your method from the background job.
#
# You never instantiate your mailer class. Rather, you just call the method you defined on the class itself.
+ # All instance methods are expected to return a message object to be sent.
#
- # = Multipart Emails
+ # == Multipart Emails
#
# 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
- # * signup_notification.text.html.erb
- # * signup_notification.text.xml.builder
- # * signup_notification.text.yaml.erb
+ # * signup_notification.html.erb
+ # * signup_notification.xml.builder
+ # * signup_notification.yml.erb
#
# Each would be rendered and added as a separate part to the message, with the corresponding content
# type. The content type for the entire message is automatically set to multipart/alternative,
@@ -166,32 +188,55 @@ module ActionMailer #:nodoc:
# This means that you'll have to manually add each part to the email and set the content type of the email
# to multipart/alternative.
#
- # = Attachments
+ # == Attachments
#
# Sending attachment in emails is easy:
#
- # class ApplicationMailer < ActionMailer::Base
+ # class NotifierMailer < ApplicationMailer
# def welcome(recipient)
# attachments['free_book.pdf'] = File.read('path/to/file.pdf')
- # mail(:to => recipient, :subject => "New account information")
+ # mail(to: recipient, subject: "New account information")
# end
# end
#
- # Which will (if it had both a welcome.text.erb and welcome.text.html.erb
+ # Which will (if it had both a welcome.text.erb and welcome.html.erb
# template in the view directory), send a complete multipart/mixed email with two parts,
# the first part being a multipart/alternative with the text and HTML email parts inside,
# and the second being a application/pdf with a Base64 encoded copy of the file.pdf book
# with the filename +free_book.pdf+.
#
- # = Inline Attachments
+ # If you need to send attachments with no content, you need to create an empty view for it,
+ # or add an empty body parameter 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", body: "")
+ # 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: "Hello there")
+ # end
+ # end
+ #
+ # == Inline Attachments
#
# You can also specify that a file should be displayed inline with other HTML. This is useful
# if you want to display a corporate logo or a photo.
#
- # class ApplicationMailer < ActionMailer::Base
+ # class NotifierMailer < ApplicationMailer
# def welcome(recipient)
# attachments.inline['photo.png'] = File.read('path/to/photo.png')
- # mail(:to => recipient, :subject => "Here is what we look like")
+ # mail(to: recipient, subject: "Here is what we look like")
# end
# end
#
@@ -207,9 +252,9 @@ module ActionMailer #:nodoc:
#
#
Please Don't Cringe
#
- # <%= image_tag attachments['photo.png'].url, :alt => 'Our Photo', :class => 'photo' -%>
+ # <%= image_tag attachments['photo.png'].url, alt: 'Our Photo', class: 'photo' -%>
#
- # = Observing and Intercepting Mails
+ # == Observing and Intercepting Mails
#
# Action Mailer provides hooks into the Mail observer and interceptor methods. These allow you to
# register classes that are called during the mail delivery life cycle.
@@ -220,63 +265,161 @@ module ActionMailer #:nodoc:
# An interceptor class must implement the :delivering_email(message) method which will be
# called before the email is sent, allowing you to make modifications to the email before it hits
# the delivery agents. Your class should make any needed modifications directly to the passed
- # in Mail::Message instance.
+ # in +Mail::Message+ instance.
#
- # = Default Hash
+ # == Default \Hash
#
# Action Mailer provides some intelligent defaults for your emails, these are usually specified in a
# default method inside the class definition:
#
- # class Notifier < ActionMailer::Base
- # default :sender => 'system@example.com'
+ # class NotifierMailer < ApplicationMailer
+ # default sender: 'system@example.com'
# end
#
- # You can pass in any header value that a Mail::Message accepts. Out of the box,
- # ActionMailer::Base sets the following:
+ # You can pass in any header value that a +Mail::Message+ accepts. Out of the box,
+ # +ActionMailer::Base+ sets the following:
#
- # * :mime_version => "1.0"
- # * :charset => "UTF-8",
- # * :content_type => "text/plain",
- # * :parts_order => [ "text/plain", "text/enriched", "text/html" ]
+ # * mime_version: "1.0"
+ # * charset: "UTF-8"
+ # * content_type: "text/plain"
+ # * parts_order: [ "text/plain", "text/enriched", "text/html" ]
#
- # parts_order and charset are not actually valid Mail::Message header fields,
+ # parts_order and charset are not actually valid +Mail::Message+ header fields,
# but Action Mailer translates them appropriately and sets the correct values.
#
# As you can pass in any header, you need to either quote the header as a string, or pass it in as
# an underscored symbol, so the following will work:
#
- # class Notifier < ActionMailer::Base
+ # class NotifierMailer < ApplicationMailer
# default 'Content-Transfer-Encoding' => '7bit',
- # :content_description => 'This is a description'
+ # content_description: 'This is a description'
# end
#
- # Finally, Action Mailer also supports passing Proc objects into the default hash, so you
- # can define methods that evaluate as the message is being generated:
+ # Finally, Action Mailer also supports passing Proc and Lambda objects into the default hash,
+ # so you can define methods that evaluate as the message is being generated:
#
- # class Notifier < ActionMailer::Base
- # default 'X-Special-Header' => Proc.new { my_method }
+ # class NotifierMailer < ApplicationMailer
+ # 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 defaults 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 default_options= configuration in config/application.rb:
+ #
+ # config.action_mailer.default_options = { from: "no-reply@example.org" }
+ #
+ # == \Callbacks
+ #
+ # You can specify callbacks using before_action and after_action for configuring your messages,
+ # and using before_deliver and after_deliver for wrapping the delivery process.
+ # For example, when you want to add default inline attachments and log delivery for all messages
+ # sent out by a certain mailer class:
+ #
+ # class NotifierMailer < ApplicationMailer
+ # before_action :add_inline_attachment!
+ # after_deliver :log_delivery
+ #
+ # def welcome
+ # mail
+ # end
+ #
+ # private
+ # def add_inline_attachment!
+ # attachments.inline["footer.jpg"] = File.read('/path/to/filename.jpg')
+ # end
+ #
+ # def log_delivery
+ # Rails.logger.info "Sent email with message id '#{message.message_id}' at #{Time.current}."
+ # end
+ # end
+ #
+ # Action callbacks in Action Mailer are implemented using
+ # AbstractController::Callbacks, so you can define and configure
+ # callbacks in the same manner that you would use callbacks in classes that
+ # inherit from ActionController::Base.
+ #
+ # Note that unless you have a specific reason to do so, you should prefer
+ # using before_action rather than after_action in your
+ # Action Mailer classes so that headers are parsed properly.
+ #
+ # == Rescuing Errors
#
- # = Configuration options
+ # +rescue+ blocks inside of a mailer method cannot rescue errors that occur
+ # outside of rendering -- for example, record deserialization errors in a
+ # background job, or errors from a third-party mail delivery service.
+ #
+ # To rescue errors that occur during any part of the mailing process, use
+ # {rescue_from}[rdoc-ref:ActiveSupport::Rescuable::ClassMethods#rescue_from]:
+ #
+ # class NotifierMailer < ApplicationMailer
+ # rescue_from ActiveJob::DeserializationError do
+ # # ...
+ # end
+ #
+ # rescue_from "SomeThirdPartyService::ApiError" do
+ # # ...
+ # end
+ #
+ # def notify(recipient)
+ # mail(to: recipient, subject: "Notification")
+ # end
+ # end
+ #
+ # == Previewing emails
+ #
+ # You can preview your email templates visually by adding a mailer preview file to the
+ # ActionMailer::Base.preview_paths. Since most emails do something interesting
+ # with database data, you'll need to write some scenarios to load messages with fake data:
+ #
+ # class NotifierMailerPreview < ActionMailer::Preview
+ # def welcome
+ # NotifierMailer.welcome(User.first)
+ # end
+ # end
+ #
+ # Methods must return a +Mail::Message+ object which can be generated by calling the mailer
+ # method without the additional deliver_now / deliver_later. The location of the
+ # mailer preview directories can be configured using the preview_paths option which has a default
+ # of test/mailers/previews:
+ #
+ # config.action_mailer.preview_paths << "#{Rails.root}/lib/mailer_previews"
+ #
+ # An overview of all previews is accessible at http://localhost:3000/rails/mailers
+ # on a running development server instance.
+ #
+ # Previews can also be intercepted in a similar manner as deliveries can be by registering
+ # a preview interceptor that has a previewing_email method:
+ #
+ # class CssInlineStyler
+ # def self.previewing_email(message)
+ # # inline CSS styles
+ # end
+ # end
+ #
+ # config.action_mailer.preview_interceptors :css_inline_styler
+ #
+ # Note that interceptors need to be registered both with register_interceptor
+ # and register_preview_interceptor if they should operate on both sending and
+ # previewing emails.
+ #
+ # == Configuration options
#
# These options are specified on the class level, like
# ActionMailer::Base.raise_delivery_errors = true
#
- # * default - You can pass this in at a class level as well as within the class itself as
+ # * default_options - You can pass this in at a class level as well as within the class itself as
# per the above section.
#
# * logger - the logger is used for generating information on the mailing run if available.
- # Can be set to nil for no logging. Compatible with both Ruby's own Logger and Log4r loggers.
+ # Can be set to +nil+ for no logging. Compatible with both Ruby's own +Logger+ and Log4r loggers.
#
# * smtp_settings - Allows detailed configuration for :smtp delivery method:
# * :address - Allows you to use a remote mail server. Just change it from its default
@@ -287,19 +430,24 @@ module ActionMailer #:nodoc:
# * :password - If your mail server requires authentication, set the password in this setting.
# * :authentication - If your mail server requires authentication, you need to specify the
# authentication type here.
- # This is a symbol and one of :plain (will send the password in the clear), :login (will
- # send password Base64 encoded) or :cram_md5 (combines a Challenge/Response mechanism to exchange
+ # This is a symbol and one of :plain (will send the password Base64 encoded), :login (will
+ # send the password Base64 encoded) or :cram_md5 (combines a Challenge/Response mechanism to exchange
# information and a cryptographic Message Digest 5 algorithm to hash important information)
- # * :enable_starttls_auto - When set to true, detects if STARTTLS is enabled in your SMTP server
- # and starts to use it.
+ # * :enable_starttls - Use STARTTLS when connecting to your SMTP server and fail if unsupported. Defaults
+ # to false. Requires at least version 2.7 of the Mail gem.
+ # * :enable_starttls_auto - Detects if STARTTLS is enabled in your SMTP server and starts
+ # to use it. Defaults to true.
# * :openssl_verify_mode - 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 ('none', 'peer', 'client_once','fail_if_no_peer_cert') or directly the
- # constant (OpenSSL::SSL::VERIFY_NONE, OpenSSL::SSL::VERIFY_PEER,...).
+ # of an OpenSSL verify constant ('none' or 'peer') or directly the constant
+ # (+OpenSSL::SSL::VERIFY_NONE+ or +OpenSSL::SSL::VERIFY_PEER+).
+ # * :ssl/:tls Enables the SMTP connection to use SMTP/TLS (SMTPS: SMTP over direct TLS connection)
+ # * :open_timeout Number of seconds to wait while attempting to open a connection.
+ # * :read_timeout Number of seconds to wait until timing-out a read(2) call.
#
# * sendmail_settings - Allows you to override options for the :sendmail delivery method.
# * :location - The location of the sendmail executable. Defaults to /usr/sbin/sendmail.
- # * :arguments - The command line arguments. Defaults to -i -t with -f sender@address
+ # * :arguments - The command line arguments. Defaults to %w[ -i ] with -f sender@address
# added automatically before the message is sent.
#
# * file_settings - Allows you to override options for the :file delivery method.
@@ -310,39 +458,52 @@ module ActionMailer #:nodoc:
#
# * delivery_method - Defines a delivery method. Possible values are :smtp (default),
# :sendmail, :test, and :file. Or you may provide a custom delivery method
- # object eg. MyOwnDeliveryMethodClass.new. See the Mail gem documentation on the interface you need to
+ # object e.g. +MyOwnDeliveryMethodClass+. See the Mail gem documentation on the interface you need to
# implement for a custom delivery agent.
#
# * perform_deliveries - Determines whether emails are actually sent from Action Mailer when you
- # call .deliver on an mail message or on an Action Mailer method. This is on by default but can
+ # call .deliver on an email message or on an Action Mailer method. This is on by default but can
# be turned off to aid in functional testing.
#
# * deliveries - Keeps an array of all the emails sent out through the Action Mailer with
# delivery_method :test. Most useful for unit and functional testing.
#
+ # * delivery_job - The job class used with deliver_later. Mailers can set this to use a
+ # custom delivery job. Defaults to +ActionMailer::MailDeliveryJob+.
+ #
+ # * deliver_later_queue_name - The queue name used by deliver_later with the default
+ # delivery_job. Mailers can set this to use a custom queue name.
class Base < AbstractController::Base
+ include Callbacks
include DeliveryMethods
+ include QueuedDelivery
+ include Rescuable
+ include Parameterized
+ include Previews
+ include FormBuilder
+
abstract!
- include AbstractController::Logger
include AbstractController::Rendering
- include AbstractController::Layouts
+
+ include AbstractController::Logger
include AbstractController::Helpers
include AbstractController::Translation
include AbstractController::AssetPaths
+ include AbstractController::Callbacks
+ include AbstractController::Caching
- self.protected_instance_variables = [:@_action_has_layout]
+ include ActionView::Layouts
- helper ActionMailer::MailHelper
+ PROTECTED_IVARS = AbstractController::Rendering::DEFAULT_PROTECTED_INSTANCE_VARIABLES + [:@_action_has_layout]
- private_class_method :new #:nodoc:
+ helper ActionMailer::MailHelper
- class_attribute :default_params
- self.default_params = {
- :mime_version => "1.0",
- :charset => "UTF-8",
- :content_type => "text/plain",
- :parts_order => [ "text/plain", "text/enriched", "text/html" ]
+ class_attribute :default_params, default: {
+ mime_version: "1.0",
+ charset: "UTF-8",
+ content_type: "text/plain",
+ parts_order: [ "text/plain", "text/enriched", "text/html" ]
}.freeze
class << self
@@ -351,128 +512,209 @@ def register_observers(*observers)
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 or a string can be passed in as the Observer. If a string is passed in
- # it will be constantized.
+ # 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 register_observer(observer)
- delivery_observer = (observer.is_a?(String) ? observer.constantize : observer)
- Mail.register_observer(delivery_observer)
+ 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 or a string can be passed in as the Interceptor. If a string is passed in
- # it will be constantized.
+ # 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 register_interceptor(interceptor)
- delivery_interceptor = (interceptor.is_a?(String) ? interceptor.constantize : interceptor)
- Mail.register_interceptor(delivery_interceptor)
+ 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
+ value.to_s.camelize.constantize
+ else
+ value
+ end
end
+ private :observer_class_for
+ # 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 ||= name.underscore
+ @mailer_name ||= anonymous? ? "anonymous" : name.underscore
end
+ # Allows to set the name of current mailer.
attr_writer :mailer_name
alias :controller_path :mailer_name
+ # Allows to set defaults through app configuration:
+ #
+ # config.action_mailer.default_options = { from: "no-reply@example.org" }
def default(value = nil)
self.default_params = default_params.merge(value).freeze if value
default_params
end
+ alias :default_options= :default
- # Receives a raw email, parses it into an email object, decodes it,
- # instantiates a new mailer, and passes the email object to the mailer
- # object's +receive+ method. If you want your mailer to be able to
- # process incoming messages, you'll need to implement a +receive+
- # method that accepts the raw email string as a parameter:
+ # Wraps an email delivery inside of ActiveSupport::Notifications instrumentation.
#
- # class MyMailer < ActionMailer::Base
- # def receive(mail)
- # ...
- # end
- # end
- def receive(raw_mail)
- ActiveSupport::Notifications.instrument("receive.action_mailer") do |payload|
- mail = Mail.new(raw_mail)
- set_payload_for_mail(payload, mail)
- new.receive(mail)
- end
- end
-
- # Wraps an email delivery inside of Active Support Notifications instrumentation. This
- # method is actually called by the Mail::Message object itself through a callback
- # when you call :deliver on the Mail::Message, calling +deliver_mail+ directly
- # and passing a Mail::Message will do nothing except tell the logger you sent the email.
- def deliver_mail(mail) #:nodoc:
+ # This method is actually called by the +Mail::Message+ object itself
+ # through a callback when you call :deliver on the +Mail::Message+,
+ # calling +deliver_mail+ directly and passing a +Mail::Message+ will do
+ # nothing except tell the logger you sent the email.
+ def deliver_mail(mail) # :nodoc:
ActiveSupport::Notifications.instrument("deliver.action_mailer") do |payload|
set_payload_for_mail(payload, mail)
yield # Let Mail do the delivery actions
end
end
- def respond_to?(method, include_private = false) #:nodoc:
- super || action_methods.include?(method.to_s)
+ # Returns an email in the format "Name ".
+ #
+ # If the name is a blank string, it returns just the address.
+ def email_address_with_name(address, name)
+ Mail::Address.new.tap do |builder|
+ builder.address = address
+ builder.display_name = name.presence
+ end.to_s
end
- protected
+ private
+ def set_payload_for_mail(payload, mail)
+ payload[:mail] = mail.encoded
+ 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[:perform_deliveries] = mail.perform_deliveries
+ end
- 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 method_missing(method_name, ...)
+ if action_methods.include?(method_name.name)
+ MessageDelivery.new(self, method_name, ...)
+ else
+ super
+ end
end
- def method_missing(method, *args) #:nodoc:
- return super unless respond_to?(method)
- new(method, *args).message
+ def respond_to_missing?(method, include_all = false)
+ action_methods.include?(method.name) || 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(method_name=nil, *args)
+ def initialize
super()
+ @_mail_was_called = false
@_message = Mail.new
- process(method_name, *args) if method_name
end
- def process(*args) #:nodoc:
- lookup_context.skip_default_locale!
- super
+ def process(method_name, *args) # :nodoc:
+ payload = {
+ mailer: self.class.name,
+ action: method_name,
+ args: args
+ }
+
+ ActiveSupport::Notifications.instrument("process.action_mailer", payload) do
+ super
+ @_message = NullMail.new unless @_mail_was_called
+ end
end
+ ruby2_keywords(:process)
+ class NullMail # :nodoc:
+ def body; "" end
+ def header; {} end
+
+ def respond_to?(string, include_all = false)
+ true
+ end
+
+ def method_missing(...)
+ nil
+ end
+ end
+
+ # Returns the name of the mailer object.
def mailer_name
self.class.mailer_name
end
- # Allows you to pass random and unusual headers to the new +Mail::Message+ object
- # which will add them to itself.
+ # Returns an email in the format "Name ".
+ #
+ # If the name is a blank string, it returns just the address.
+ def email_address_with_name(address, name)
+ self.class.email_address_with_name(address, name)
+ end
+
+ # Allows you to pass random and unusual headers to the new +Mail::Message+
+ # object which will add them to itself.
#
# headers['X-Special-Domain-Specific-Header'] = "SecretValue"
#
- # You can also pass a hash into headers of header field names and values, which
- # will then be set on the Mail::Message object:
+ # You can also pass a hash into headers of header field names and values,
+ # which will then be set on the +Mail::Message+ object:
#
# headers 'X-Special-Domain-Specific-Header' => "SecretValue",
# 'In-Reply-To' => incoming.message_id
#
- # The resulting Mail::Message will have the following in it's header:
+ # The resulting +Mail::Message+ will have the following in its header:
#
# X-Special-Domain-Specific-Header: SecretValue
- def headers(args=nil)
+ #
+ # Note about replacing already defined headers:
+ #
+ # * +subject+
+ # * +sender+
+ # * +from+
+ # * +to+
+ # * +cc+
+ # * +bcc+
+ # * +reply-to+
+ # * +orig-date+
+ # * +message-id+
+ # * +references+
+ #
+ # Fields can only appear once in email headers while other fields such as
+ # X-Anything can appear multiple times.
+ #
+ # If you want to replace any header which already exists, first set it to
+ # +nil+ in order to reset the value otherwise another field will be added
+ # for the same header.
+ def headers(args = nil)
if args
@_message.headers(args)
else
@@ -484,23 +726,23 @@ def headers(args=nil)
#
# 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+, and +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',
- # :content => File.read('/path/to/filename.jpg')}
+ # 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',
- # :encoding => 'SpecialEncoding',
- # :content => file_content }
+ # mail.attachments['filename.jpg'] = {mime_type: 'application/gzip',
+ # encoding: 'SpecialEncoding',
+ # content: file_content }
#
# You can also search for specific attachments:
#
@@ -511,222 +753,319 @@ def headers(args=nil)
# mail.attachments[0] # => Mail::Part (first attachment)
#
def attachments
- @_message.attachments
+ if @_mail_was_called
+ LateAttachmentsProxy.new(@_message.attachments)
+ else
+ @_message.attachments
+ end
+ end
+
+ class LateAttachmentsProxy < SimpleDelegator
+ def inline; self end
+ def []=(_name, _content); _raise_error end
+
+ private
+ def _raise_error
+ raise RuntimeError, "Can't add attachments after `mail` was called.\n" \
+ "Make sure to use `attachments[]=` before calling `mail`."
+ end
end
# The main method that creates the message and renders the email templates. There are
# two ways to call this method, with a block, or without a block.
#
- # Both methods accept a headers hash. This hash allows you to specify the most used headers
- # in an email message, these are:
+ # It accepts a headers hash. This hash allows you to specify
+ # the most used headers in an email message, these are:
#
- # * :subject - The subject of the message, if this is omitted, Action Mailer will
- # ask the Rails I18n class for a translated :subject in the scope of
+ # * +:subject+ - The subject of the message, if this is omitted, Action Mailer will
+ # ask the \Rails I18n class for a translated +:subject+ in the scope of
# [mailer_scope, action_name] or if this is missing, will translate the
- # humanized version of the action_name
- # * :to - Who the message is destined for, can be a string of addresses, or an array
+ # humanized version of the +action_name+
+ # * +:to+ - Who the message is destined for, can be a string of addresses, or an array
# of addresses.
- # * :from - Who the message is from
- # * :cc - Who you would like to Carbon-Copy on this email, can be a string of addresses,
+ # * +:from+ - Who the message is from
+ # * +:cc+ - Who you would like to Carbon-Copy on this email, can be a string of addresses,
# or an array of addresses.
- # * :bcc - Who you would like to Blind-Carbon-Copy on this email, can be a string of
+ # * +:bcc+ - Who you would like to Blind-Carbon-Copy on this email, can be a string of
# addresses, or an array of addresses.
- # * :reply_to - Who to set the Reply-To header of the email to.
- # * :date - The date to say the email was sent on.
+ # * +:reply_to+ - Who to set the +Reply-To+ header of the email to.
+ # * +:date+ - The date to say the email was sent on.
#
- # You can set default values for any of the above headers (except :date) by using the default
- # class method:
+ # You can set default values for any of the above headers (except +:date+)
+ # by using the ::default class method:
#
# class Notifier < ActionMailer::Base
- # self.default :from => 'no-reply@test.lindsaar.net',
- # :bcc => 'email_logger@test.lindsaar.net',
- # :reply_to => 'bounces@test.lindsaar.net'
+ # default from: 'no-reply@test.lindsaar.net',
+ # bcc: 'email_logger@test.lindsaar.net',
+ # reply_to: 'bounces@test.lindsaar.net'
# end
#
# If you need other headers not listed above, you can either pass them in
# as part of the headers hash or use the headers['name'] = value
# method.
#
- # When a :return_path is specified as header, that value will be used as the 'envelope from'
- # address for the Mail message. Setting this is useful when you want delivery notifications
- # sent to a different address than the one in :from. Mail will actually use the
- # :return_path in preference to the :sender in preference to the :from
- # field for the 'envelope from' value.
+ # When a +:return_path+ is specified as header, that value will be used as
+ # the 'envelope from' address for the Mail message. Setting this is useful
+ # when you want delivery notifications sent to a different address than the
+ # one in +:from+. Mail will actually use the +:return_path+ in preference
+ # to the +:sender+ in preference to the +:from+ field for the 'envelope
+ # from' value.
#
- # If you do not pass a block to the +mail+ method, it will find all templates in the
- # view paths using by default the mailer name and the method name that it is being
- # called from, it will then create parts for each of these templates intelligently,
- # making educated guesses on correct content type and sequence, and return a fully
- # prepared Mail::Message ready to call :deliver on to send.
+ # If you do not pass a block to the +mail+ method, it will find all
+ # templates in the view paths using by default the mailer name and the
+ # method name that it is being called from, it will then create parts for
+ # each of these templates intelligently, making educated guesses on correct
+ # content type and sequence, and return a fully prepared +Mail::Message+
+ # ready to call :deliver on to send.
#
# For example:
#
# class Notifier < ActionMailer::Base
- # default :from => 'no-reply@test.lindsaar.net',
+ # default from: 'no-reply@test.lindsaar.net'
#
# def welcome
- # mail(:to => 'mikel@test.lindsaar.net')
+ # mail(to: 'mikel@test.lindsaar.net')
# end
# end
#
- # Will look for all templates at "app/views/notifier" with name "welcome". However, those
- # can be customized:
+ # Will look for all templates at "app/views/notifier" with name "welcome".
+ # If no welcome template exists, it will raise an ActionView::MissingTemplate error.
+ #
+ # However, those can be customized:
#
- # mail(:template_path => 'notifications', :template_name => 'another')
+ # mail(template_path: 'notifications', template_name: 'another')
#
# And now it will look for all templates at "app/views/notifications" with name "another".
#
# If you do pass a block, you can render specific templates of your choice:
#
- # mail(:to => 'mikel@test.lindsaar.net') do |format|
+ # mail(to: 'mikel@test.lindsaar.net') do |format|
# format.text
# format.html
# end
#
- # You can even render text directly without using a template:
+ # You can even render plain text directly without using a template:
#
- # mail(:to => 'mikel@test.lindsaar.net') do |format|
- # format.text { render :text => "Hello Mikel!" }
- # format.html { render :text => "
".html_safe }
# end
#
- # Which will render a multipart/alternative email with text/plain and
- # text/html parts.
+ # Which will render a +multipart/alternative+ email with +text/plain+ and
+ # +text/html+ parts.
#
# The block syntax also allows you to customize the part headers if desired:
#
- # mail(:to => 'mikel@test.lindsaar.net') do |format|
- # format.text(:content_transfer_encoding => "base64")
+ # mail(to: 'mikel@test.lindsaar.net') do |format|
+ # format.text(content_transfer_encoding: "base64")
# format.html
# end
#
- def mail(headers={}, &block)
- m = @_message
+ def mail(headers = {}, &block)
+ return message if @_mail_was_called && headers.blank? && !block
- # At the beginning, do not consider class default for parts order neither content_type
+ # At the beginning, do not consider class default for content_type
content_type = headers[:content_type]
- parts_order = headers[:parts_order]
-
- # Call all the procs (if any)
- class_default = self.class.default
- default_values = class_default.merge(class_default) do |k,v|
- v.respond_to?(:to_proc) ? instance_eval(&v) : v
- end
- # Handle defaults
- headers = headers.reverse_merge(default_values)
- headers[:subject] ||= default_i18n_subject
+ headers = apply_defaults(headers)
# Apply charset at the beginning so all fields are properly quoted
- m.charset = charset = headers[:charset]
+ message.charset = charset = headers[:charset]
# Set configure delivery behavior
- wrap_delivery_behavior!(headers.delete(:delivery_method))
+ wrap_delivery_behavior!(headers[:delivery_method], headers[:delivery_method_options])
- # Assign all headers except parts_order, content_type and body
- assignable = headers.except(:parts_order, :content_type, :body, :template_name, :template_path)
- assignable.each { |k, v| m[k] = v }
+ assign_headers_to_message(message, headers)
# Render the templates and blocks
- responses, explicit_order = collect_responses_and_parts_order(headers, &block)
- create_parts_from_responses(m, responses)
+ responses = collect_responses(headers, &block)
+ @_mail_was_called = true
- # Setup content type, reapply charset and handle parts order
- m.content_type = set_content_type(m, content_type, headers[:content_type])
- m.charset = charset
+ create_parts_from_responses(message, responses)
+ wrap_inline_attachments(message)
- if m.multipart?
- parts_order ||= explicit_order || headers[:parts_order]
- m.body.set_sort_order(parts_order)
- m.body.sort_parts!
+ # Set up content type, reapply charset and handle parts order
+ message.content_type = set_content_type(message, content_type, headers[:content_type])
+ message.charset = charset
+
+ if message.multipart?
+ message.body.set_sort_order(headers[:parts_order])
+ message.body.sort_parts!
end
- m
+ message
end
- protected
+ 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.all?(&:inline?)
+ ["multipart", "related", params]
+ else
+ ["multipart", "mixed", params]
+ end
+ when m.multipart?
+ ["multipart", "alternative", params]
+ else
+ m.content_type || class_default
+ end
+ end
+
+ # Translates the +subject+ using \Rails I18n class under [mailer_scope, action_name] scope.
+ # If it does not find a translation for the +subject+ under the specified scope it will default to a
+ # humanized version of the action_name.
+ # 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, scope: [mailer_scope, action_name], default: action_name.humanize)
+ end
+
+ # Emails do not support relative path links.
+ def self.supports_path? # :doc:
+ false
+ end
- 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 { |a| a.inline? }
- ["multipart", "related", params]
+ def apply_defaults(headers)
+ default_values = self.class.default.except(*headers.keys).transform_values do |value|
+ compute_default(value)
+ end
+
+ 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)
+
+ if value.arity == 1
+ instance_exec(self, &value)
else
- ["multipart", "mixed", params]
+ instance_exec(&value)
end
- when m.multipart?
- ["multipart", "alternative", params]
- else
- m.content_type || class_default
end
- end
- # Translates the +subject+ using Rails I18n class under [mailer_scope, action_name] scope.
- # If it does not find a translation for the +subject+ under the specified scope it will default to a
- # humanized version of the action_name.
- def default_i18n_subject #:nodoc:
- mailer_scope = self.class.mailer_name.gsub('/', '.')
- I18n.t(:subject, :scope => [mailer_scope, action_name], :default => action_name.humanize)
- 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_and_parts_order(headers) #:nodoc:
- responses, parts_order = [], nil
+ def collect_responses(headers, &block)
+ if block_given?
+ collect_responses_from_block(headers, &block)
+ elsif headers[:body]
+ collect_responses_from_text(headers)
+ else
+ collect_responses_from_templates(headers)
+ end
+ end
- if block_given?
- collector = ActionMailer::Collector.new(lookup_context) { render(action_name) }
+ def collect_responses_from_block(headers)
+ templates_name = headers[:template_name] || action_name
+ collector = ActionMailer::Collector.new(lookup_context) { render(templates_name) }
yield(collector)
- parts_order = collector.responses.map { |r| r[:content_type] }
- responses = collector.responses
- elsif headers[:body]
- responses << {
- :body => headers.delete(:body),
- :content_type => self.class.default[:content_type] || "text/plain"
- }
- else
- templates_path = headers.delete(:template_path) || self.class.mailer_name
- templates_name = headers.delete(:template_name) || action_name
+ collector.responses
+ end
- each_template(templates_path, templates_name) do |template|
- self.formats = template.formats
+ def collect_responses_from_text(headers)
+ [{
+ body: headers.delete(:body),
+ content_type: headers[:content_type] || "text/plain"
+ }]
+ end
+
+ def collect_responses_from_templates(headers)
+ templates_path = headers[:template_path] || self.class.mailer_name
+ templates_name = headers[:template_name] || action_name
- responses << {
- :body => render(:template => template),
- :content_type => template.mime_type.to_s
+ each_template(Array(templates_path), templates_name).map do |template|
+ format = template.format || self.formats.first
+ {
+ body: render(template: template, formats: [format]),
+ content_type: Mime[format].to_s
}
end
end
- [responses, parts_order]
- 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(&:format).each(&block)
+ end
+ end
- def each_template(paths, name, &block) #:nodoc:
- templates = lookup_context.find_all(name, Array(paths))
- templates.uniq { |t| t.formats }.each(&block)
- end
+ def wrap_inline_attachments(message)
+ # If we have both types of attachment, wrap all the inline attachments
+ # in multipart/related, but not the actual attachments
+ if message.attachments.detect(&:inline?) && message.attachments.detect { |a| !a.inline? }
+ related = Mail::Part.new
+ related.content_type = "multipart/related"
+ mixed = [ related ]
- def create_parts_from_responses(m, responses) #:nodoc:
- 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) }
+ message.parts.each do |p|
+ if p.attachment? && !p.inline?
+ mixed << p
+ else
+ related.add_part(p)
+ end
+ end
+
+ message.parts.clear
+ mixed.each { |c| message.add_part(c) }
+ end
end
- end
- def insert_part(container, response, charset) #:nodoc:
- response[:charset] ||= charset
- part = Mail::Part.new(response)
- container.add_part(part)
- 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) }
+ end
+ end
- ActiveSupport.run_load_hooks(:action_mailer, self)
+ def insert_part(container, response, charset)
+ response[:charset] ||= charset
+ part = Mail::Part.new(response)
+ container.add_part(part)
+ end
+
+ # This and #instrument_name is for caching instrument
+ def instrument_payload(key)
+ {
+ mailer: mailer_name,
+ key: key
+ }
+ end
+
+ def instrument_name
+ "action_mailer"
+ end
+
+ def _protected_ivars
+ PROTECTED_IVARS
+ end
+
+ ActiveSupport.run_load_hooks(:action_mailer, self)
end
end
-
diff --git a/actionmailer/lib/action_mailer/callbacks.rb b/actionmailer/lib/action_mailer/callbacks.rb
new file mode 100644
index 0000000000000..fa71f0b4f1b2e
--- /dev/null
+++ b/actionmailer/lib/action_mailer/callbacks.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module ActionMailer
+ module Callbacks
+ extend ActiveSupport::Concern
+
+ included do
+ include ActiveSupport::Callbacks
+ define_callbacks :deliver, skip_after_callbacks_if_terminated: true
+ end
+
+ module ClassMethods
+ # Defines a callback that will get called right before the
+ # message is sent to the delivery method.
+ def before_deliver(*filters, &blk)
+ set_callback(:deliver, :before, *filters, &blk)
+ end
+
+ # Defines a callback that will get called right after the
+ # message's delivery method is finished.
+ def after_deliver(*filters, &blk)
+ set_callback(:deliver, :after, *filters, &blk)
+ end
+
+ # Defines a callback that will get called around the message's deliver method.
+ def around_deliver(*filters, &blk)
+ set_callback(:deliver, :around, *filters, &blk)
+ end
+ end
+ end
+end
diff --git a/actionmailer/lib/action_mailer/collector.rb b/actionmailer/lib/action_mailer/collector.rb
index 17b22aea2af9d..888410fa755ca 100644
--- a/actionmailer/lib/action_mailer/collector.rb
+++ b/actionmailer/lib/action_mailer/collector.rb
@@ -1,8 +1,10 @@
-require 'abstract_controller/collector'
-require 'active_support/core_ext/hash/reverse_merge'
-require 'active_support/core_ext/array/extract_options'
+# frozen_string_literal: true
-module ActionMailer #:nodoc:
+require "abstract_controller/collector"
+require "active_support/core_ext/hash/reverse_merge"
+require "active_support/core_ext/array/extract_options"
+
+module ActionMailer
class Collector
include AbstractController::Collector
attr_reader :responses
@@ -15,13 +17,13 @@ def initialize(context, &block)
def any(*args, &block)
options = args.extract_options!
- raise "You have to supply at least one format" if args.empty?
+ raise ArgumentError, "You have to supply at least one format" if args.empty?
args.each { |type| send(type, options.dup, &block) }
end
alias :all :any
- def custom(mime, options={})
- options.reverse_merge!(:content_type => mime.to_s)
+ def custom(mime, options = {})
+ options.reverse_merge!(content_type: mime.to_s)
@context.formats = [mime.to_sym]
options[:body] = block_given? ? yield : @default_render.call
@responses << options
diff --git a/actionmailer/lib/action_mailer/delivery_methods.rb b/actionmailer/lib/action_mailer/delivery_methods.rb
index d1467fb5261bc..df6e90db7adcc 100644
--- a/actionmailer/lib/action_mailer/delivery_methods.rb
+++ b/actionmailer/lib/action_mailer/delivery_methods.rb
@@ -1,72 +1,69 @@
-require 'tmpdir'
+# frozen_string_literal: true
+
+require "tmpdir"
module ActionMailer
- # This module handles everything related to mail delivery, from registering new
- # delivery methods to configuring the mail object to be sent.
+ # = Action Mailer \DeliveryMethods
+ #
+ # This module handles everything related to mail delivery, from registering
+ # new delivery methods to configuring the mail object to be sent.
module DeliveryMethods
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 :raise_delivery_errors, default: true
+ cattr_accessor :perform_deliveries, default: true
- 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',
- :user_name => nil,
- :password => nil,
- :authentication => nil,
- :enable_starttls_auto => true
+ address: "localhost",
+ port: 25,
+ domain: "localhost.localdomain",
+ user_name: nil,
+ password: nil,
+ authentication: nil,
+ enable_starttls_auto: true
add_delivery_method :file, Mail::FileDelivery,
- :location => defined?(Rails.root) ? "#{Rails.root}/tmp/mails" : "#{Dir.tmpdir}/mails"
+ 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: %w[-i]
add_delivery_method :test, Mail::TestMailer
end
+ # Helpers for creating and wrapping delivery behavior, used by DeliveryMethods.
module ClassMethods
# Provides a list of emails that have been delivered by Mail::TestMailer
- delegate :deliveries, :deliveries=, :to => Mail::TestMailer
+ delegate :deliveries, :deliveries=, to: Mail::TestMailer
- # Adds a new delivery method through the given class using the given symbol
- # as alias and the default options supplied:
- #
- # Example:
+ # Adds a new delivery method through the given class using the given
+ # symbol as alias and the default options supplied.
#
# add_delivery_method :sendmail, Mail::Sendmail,
- # :location => '/usr/sbin/sendmail',
- # :arguments => '-i -t'
- #
- def add_delivery_method(symbol, klass, default_options={})
+ # location: '/usr/sbin/sendmail',
+ # arguments: %w[ -i ]
+ def add_delivery_method(symbol, klass, default_options = {})
class_attribute(:"#{symbol}_settings") unless respond_to?(:"#{symbol}_settings")
- send(:"#{symbol}_settings=", default_options)
+ public_send(:"#{symbol}_settings=", default_options)
self.delivery_methods = delivery_methods.merge(symbol.to_sym => klass).freeze
end
- def wrap_delivery_behavior(mail, method=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
when NilClass
raise "Delivery method cannot be nil"
when Symbol
- if klass = delivery_methods[method.to_sym]
- mail.delivery_method(klass, send(:"#{method}_settings"))
+ if klass = delivery_methods[method]
+ mail.delivery_method(klass, (send(:"#{method}_settings") || {}).merge(options || {}))
else
raise "Invalid delivery method #{method.inspect}"
end
@@ -79,7 +76,7 @@ def wrap_delivery_behavior(mail, method=nil) #:nodoc:
end
end
- def wrap_delivery_behavior!(*args) #:nodoc:
+ def wrap_delivery_behavior!(*args) # :nodoc:
self.class.wrap_delivery_behavior(message, *args)
end
end
diff --git a/actionmailer/lib/action_mailer/deprecator.rb b/actionmailer/lib/action_mailer/deprecator.rb
new file mode 100644
index 0000000000000..26fde3a857ece
--- /dev/null
+++ b/actionmailer/lib/action_mailer/deprecator.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+module ActionMailer
+ def self.deprecator # :nodoc:
+ @deprecator ||= ActiveSupport::Deprecation.new
+ end
+end
diff --git a/actionmailer/lib/action_mailer/form_builder.rb b/actionmailer/lib/action_mailer/form_builder.rb
new file mode 100644
index 0000000000000..048ea337822fc
--- /dev/null
+++ b/actionmailer/lib/action_mailer/form_builder.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+module ActionMailer
+ # = Action Mailer Form Builder
+ #
+ # Override the default form builder for all views rendered by this
+ # mailer and any of its descendants. Accepts a subclass of
+ # ActionView::Helpers::FormBuilder.
+ #
+ # While emails typically will not include forms, this can be used
+ # by views that are shared between controllers and mailers.
+ #
+ # For more information, see +ActionController::FormBuilder+.
+ module FormBuilder
+ extend ActiveSupport::Concern
+
+ included do
+ class_attribute :_default_form_builder, instance_accessor: false
+ end
+
+ module ClassMethods
+ # Set the form builder to be used as the default for all forms
+ # in the views rendered by this mailer and its subclasses.
+ #
+ # ==== Parameters
+ # * builder - Default form builder. Accepts a subclass of ActionView::Helpers::FormBuilder
+ def default_form_builder(builder)
+ self._default_form_builder = builder
+ end
+ end
+
+ # Default form builder for the mailer
+ def default_form_builder
+ self.class._default_form_builder
+ end
+ end
+end
diff --git a/actionmailer/lib/action_mailer/gem_version.rb b/actionmailer/lib/action_mailer/gem_version.rb
new file mode 100644
index 0000000000000..0317b4ebdf600
--- /dev/null
+++ b/actionmailer/lib/action_mailer/gem_version.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module ActionMailer
+ # Returns the currently loaded version of Action Mailer as a +Gem::Version+.
+ def self.gem_version
+ Gem::Version.new VERSION::STRING
+ end
+
+ module VERSION
+ MAJOR = 8
+ MINOR = 1
+ TINY = 0
+ PRE = "alpha"
+
+ STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
+ end
+end
diff --git a/actionmailer/lib/action_mailer/inline_preview_interceptor.rb b/actionmailer/lib/action_mailer/inline_preview_interceptor.rb
new file mode 100644
index 0000000000000..6b47f1b961137
--- /dev/null
+++ b/actionmailer/lib/action_mailer/inline_preview_interceptor.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+
+require "base64"
+
+module ActionMailer
+ # = Action Mailer \InlinePreviewInterceptor
+ #
+ # 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 an HTML email in a web browser.
+ #
+ # This interceptor is enabled by default. To disable it, delete it from the
+ # ActionMailer::Base.preview_interceptors array:
+ #
+ # ActionMailer::Base.preview_interceptors.delete(ActionMailer::InlinePreviewInterceptor)
+ #
+ class InlinePreviewInterceptor
+ PATTERN = /src=(?:"cid:[^"]+"|'cid:[^']+')/i
+
+ include Base64
+
+ def self.previewing_email(message) # :nodoc:
+ new(message).transform!
+ end
+
+ def initialize(message) # :nodoc:
+ @message = message
+ end
+
+ def transform! # :nodoc:
+ return message if html_part.blank?
+
+ html_part.body = html_part.decoded.gsub(PATTERN) do |match|
+ if part = find_part(match[9..-2])
+ %[src="#{data_url(http://github.com/part)}"]
+ else
+ match
+ end
+ end
+
+ message
+ end
+
+ private
+ attr_reader :message
+
+ def html_part
+ @html_part ||= message.html_part
+ end
+
+ def data_url(http://github.com/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 }
+ end
+ end
+end
diff --git a/actionmailer/lib/action_mailer/log_subscriber.rb b/actionmailer/lib/action_mailer/log_subscriber.rb
index a6c163832e3ca..130d5d83e62df 100644
--- a/actionmailer/lib/action_mailer/log_subscriber.rb
+++ b/actionmailer/lib/action_mailer/log_subscriber.rb
@@ -1,16 +1,40 @@
+# frozen_string_literal: true
+
+require "active_support/log_subscriber"
+
module ActionMailer
+ # = Action Mailer \LogSubscriber
+ #
+ # Implements the ActiveSupport::LogSubscriber for logging notifications when
+ # email is delivered or received.
class LogSubscriber < ActiveSupport::LogSubscriber
+ # An email was delivered.
def deliver(event)
- recipients = Array(event.payload[:to]).join(', ')
- info("\nSent mail to #{recipients} (%1.fms)" % event.duration)
- debug(event.payload[:mail])
+ info do
+ if exception = event.payload[:exception_object]
+ "Failed delivery of mail #{event.payload[:message_id]} error_class=#{exception.class} error_message=#{exception.message.inspect}"
+ elsif event.payload[:perform_deliveries]
+ "Delivered mail #{event.payload[:message_id]} (#{event.duration.round(1)}ms)"
+ else
+ "Skipped delivery of mail #{event.payload[:message_id]} as `perform_deliveries` is false"
+ end
+ end
+
+ debug { event.payload[:mail] }
end
+ subscribe_log_level :deliver, :debug
- def receive(event)
- info("\nReceived mail (%.1fms)" % event.duration)
- debug(event.payload[:mail])
+ # An email was generated.
+ def process(event)
+ debug do
+ mailer = event.payload[:mailer]
+ action = event.payload[:action]
+ "#{mailer}##{action}: processed outbound mail in #{event.duration.round(1)}ms"
+ end
end
+ subscribe_log_level :process, :debug
+ # Use the logger configured for ActionMailer::Base.
def logger
ActionMailer::Base.logger
end
diff --git a/actionmailer/lib/action_mailer/mail_delivery_job.rb b/actionmailer/lib/action_mailer/mail_delivery_job.rb
new file mode 100644
index 0000000000000..d76a7cf1155c8
--- /dev/null
+++ b/actionmailer/lib/action_mailer/mail_delivery_job.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+require "active_job"
+
+module ActionMailer
+ # = Action Mailer \MailDeliveryJob
+ #
+ # The +ActionMailer::MailDeliveryJob+ class is used when you
+ # want to send emails outside of the request-response cycle. It supports
+ # sending either parameterized or normal mail.
+ #
+ # Exceptions are rescued and handled by the mailer class.
+ class MailDeliveryJob < ActiveJob::Base # :nodoc:
+ queue_as do
+ mailer_class = arguments.first.constantize
+ mailer_class.deliver_later_queue_name
+ end
+
+ rescue_from StandardError, with: :handle_exception_with_mailer_class
+
+ def perform(mailer, mail_method, delivery_method, args:, kwargs: nil, params: nil)
+ mailer_class = params ? mailer.constantize.with(params) : mailer.constantize
+ message = if kwargs
+ mailer_class.public_send(mail_method, *args, **kwargs)
+ else
+ mailer_class.public_send(mail_method, *args)
+ end
+ message.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/mail_helper.rb b/actionmailer/lib/action_mailer/mail_helper.rb
index 2036883b2249d..13c8571dc45f7 100644
--- a/actionmailer/lib/action_mailer/mail_helper.rb
+++ b/actionmailer/lib/action_mailer/mail_helper.rb
@@ -1,16 +1,42 @@
+# frozen_string_literal: true
+
module ActionMailer
+ # = Action Mailer \MailHelper
+ #
+ # Provides helper methods for ActionMailer::Base that can be used for easily
+ # formatting messages, accessing mailer or message instances, and the
+ # attachments list.
module MailHelper
- # Take the text and format it, indented two spaces for each line, and wrapped at 72 columns.
+ # Take the text and format it, indented two spaces for each line, and
+ # wrapped at 72 columns:
+ #
+ # text = <<-TEXT
+ # This is
+ # the paragraph.
+ #
+ # * item1 * item2
+ # TEXT
+ #
+ # block_format text
+ # # => " This is the paragraph.\n\n * item1\n * item2\n"
def block_format(text)
formatted = text.split(/\n\r?\n/).collect { |paragraph|
format_paragraph(paragraph)
}.join("\n\n")
# Make list points stand on their own line
- formatted.gsub!(/[ ]*([*]+) ([^*]*)/) { |s| " #{$1} #{$2.strip}\n" }
- formatted.gsub!(/[ ]*([#]+) ([^#]*)/) { |s| " #{$1} #{$2.strip}\n" }
+ output = +""
+ splits = formatted.split(/(\*+|\#+)/)
+ while line = splits.shift
+ if line.start_with?("*", "#") && splits.first&.start_with?(" ")
+ output.chomp!(" ") while output.end_with?(" ")
+ output << " #{line} #{splits.shift.strip}\n"
+ else
+ output << line
+ end
+ end
- formatted
+ output
end
# Access the mailer instance.
@@ -25,14 +51,14 @@ def message
# Access the message attachments list.
def attachments
- @_message.attachments
+ mailer.attachments
end
# Returns +text+ wrapped at +len+ columns and indented +indent+ spaces.
+ # By default column length +len+ equals 72 characters and indent
+ # +indent+ equal two spaces.
#
- # === Examples
- #
- # my_text = "Here is a sample text with more than 40 characters"
+ # my_text = 'Here is a sample text with more than 40 characters'
#
# format_paragraph(my_text, 25, 4)
# # => " Here is a sample text with\n more than 40 characters"
@@ -40,15 +66,16 @@ def format_paragraph(text, len = 72, indent = 2)
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
end
end
- sentences.map { |sentence|
- "#{" " * indent}#{sentence.join(' ')}"
+ indentation = " " * indent
+ sentences.map! { |sentence|
+ "#{indentation}#{sentence.join(' ')}"
}.join "\n"
end
end
diff --git a/actionmailer/lib/action_mailer/message_delivery.rb b/actionmailer/lib/action_mailer/message_delivery.rb
new file mode 100644
index 0000000000000..504bb25312765
--- /dev/null
+++ b/actionmailer/lib/action_mailer/message_delivery.rb
@@ -0,0 +1,156 @@
+# frozen_string_literal: true
+
+require "delegate"
+
+module ActionMailer
+ # = Action Mailer \MessageDelivery
+ #
+ # The +ActionMailer::MessageDelivery+ class is used by
+ # ActionMailer::Base when creating a new mailer.
+ # MessageDelivery is a wrapper (+Delegator+ subclass) around a lazy
+ # created +Mail::Message+. You can get direct access to the
+ # +Mail::Message+, deliver the email or schedule the email to be sent
+ # through Active Job.
+ #
+ # Notifier.welcome(User.first) # an ActionMailer::MessageDelivery object
+ # Notifier.welcome(User.first).deliver_now # sends the email
+ # 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_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
+ ruby2_keywords(:initialize)
+
+ # Method calls are delegated to the Mail::Message that's ready to deliver.
+ def __getobj__ # :nodoc:
+ @mail_message ||= processed_mailer.message
+ end
+
+ # Unused except for delegator internals (dup, marshalling).
+ def __setobj__(mail_message) # :nodoc:
+ @mail_message = mail_message
+ end
+
+ # 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+
+ # and +raise_delivery_errors+, so use with caution.
+ #
+ # Notifier.welcome(User.first).deliver_later!
+ # Notifier.welcome(User.first).deliver_later!(wait: 1.hour)
+ # Notifier.welcome(User.first).deliver_later!(wait_until: 10.hours.from_now)
+ # Notifier.welcome(User.first).deliver_later!(priority: 10)
+ #
+ # Options:
+ #
+ # * :wait - Enqueue the email to be delivered with a delay
+ # * :wait_until - Enqueue the email to be delivered at (after) a specific date / time
+ # * :queue - Enqueue the email on the specified queue
+ # * :priority - Enqueues the email with the specified priority
+ #
+ # By default, the email will be enqueued using ActionMailer::MailDeliveryJob on
+ # the default queue. Mailer classes can customize the queue name used for the default
+ # job by assigning a +deliver_later_queue_name+ class variable, or provide a custom job
+ # by assigning a +delivery_job+. When a custom job is used, it controls the queue name.
+ #
+ # class AccountRegistrationMailer < ApplicationMailer
+ # self.delivery_job = RegistrationDeliveryJob
+ # end
+ def deliver_later!(options = {})
+ enqueue_delivery :deliver_now!, options
+ end
+
+ # Enqueues the email to be delivered through Active Job. When the
+ # job runs it will send the email using +deliver_now+.
+ #
+ # Notifier.welcome(User.first).deliver_later
+ # Notifier.welcome(User.first).deliver_later(wait: 1.hour)
+ # Notifier.welcome(User.first).deliver_later(wait_until: 10.hours.from_now)
+ # Notifier.welcome(User.first).deliver_later(priority: 10)
+ #
+ # Options:
+ #
+ # * :wait - Enqueue the email to be delivered with a delay.
+ # * :wait_until - Enqueue the email to be delivered at (after) a specific date / time.
+ # * :queue - Enqueue the email on the specified queue.
+ # * :priority - Enqueues the email with the specified priority
+ #
+ # By default, the email will be enqueued using ActionMailer::MailDeliveryJob on
+ # the default queue. Mailer classes can customize the queue name used for the default
+ # job by assigning a +deliver_later_queue_name+ class variable, or provide a custom job
+ # by assigning a +delivery_job+. When a custom job is used, it controls the queue name.
+ #
+ # class AccountRegistrationMailer < ApplicationMailer
+ # self.delivery_job = RegistrationDeliveryJob
+ # end
+ def deliver_later(options = {})
+ enqueue_delivery :deliver_now, options
+ end
+
+ # Delivers an email without checking +perform_deliveries+ and +raise_delivery_errors+,
+ # so use with caution.
+ #
+ # Notifier.welcome(User.first).deliver_now!
+ #
+ def deliver_now!
+ processed_mailer.handle_exceptions do
+ processed_mailer.run_callbacks(:deliver) do
+ message.deliver!
+ end
+ end
+ end
+
+ # Delivers an email:
+ #
+ # Notifier.welcome(User.first).deliver_now
+ #
+ def deliver_now
+ processed_mailer.handle_exceptions do
+ processed_mailer.run_callbacks(:deliver) do
+ message.deliver
+ end
+ end
+ end
+
+ private
+ # Returns the processed Mailer instance. We keep this instance
+ # on hand so we can run callbacks and 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 = {})
+ 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
+ @mailer_class.delivery_job.set(options).perform_later(
+ @mailer_class.name, @action.to_s, delivery_method.to_s, args: @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 0000000000000..cbfba5f1deedc
--- /dev/null
+++ b/actionmailer/lib/action_mailer/parameterized.rb
@@ -0,0 +1,154 @@
+# frozen_string_literal: true
+
+module ActionMailer
+ # = Action Mailer \Parameterized
+ #
+ # 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_writer :params
+
+ def params
+ @params ||= {}
+ end
+ 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, ...)
+ if @mailer.action_methods.include?(method_name.name)
+ ActionMailer::Parameterized::MessageDelivery.new(@mailer, method_name, @params, ...)
+ 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, ...)
+ super(mailer_class, action, ...)
+ @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
+ @mailer_class.delivery_job.set(options).perform_later(
+ @mailer_class.name, @action.to_s, delivery_method.to_s, params: @params, args: @args)
+ end
+ end
+ end
+ end
+end
diff --git a/actionmailer/lib/action_mailer/preview.rb b/actionmailer/lib/action_mailer/preview.rb
new file mode 100644
index 0000000000000..814f256cef1d2
--- /dev/null
+++ b/actionmailer/lib/action_mailer/preview.rb
@@ -0,0 +1,142 @@
+# frozen_string_literal: true
+
+require "active_support/descendants_tracker"
+
+module ActionMailer
+ module Previews # :nodoc:
+ extend ActiveSupport::Concern
+
+ included do
+ # Add the location of mailer previews through app configuration:
+ #
+ # config.action_mailer.preview_paths << "#{Rails.root}/lib/mailer_previews"
+ #
+ mattr_accessor :preview_paths, instance_writer: false, default: []
+
+ # Enable or disable mailer previews through app configuration:
+ #
+ # config.action_mailer.show_previews = true
+ #
+ # Defaults to +true+ for development environment
+ #
+ mattr_accessor :show_previews, instance_writer: false
+
+ # :nodoc:
+ mattr_accessor :preview_interceptors, instance_writer: false, default: [ActionMailer::InlinePreviewInterceptor]
+ end
+
+ module ClassMethods
+ # Register one or more Interceptors which will be called before mail is previewed.
+ def register_preview_interceptors(*interceptors)
+ 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 constantized.
+ def register_preview_interceptor(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
+ 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
+ load_previews if descendants.empty?
+ descendants.sort_by { |mailer| mailer.name.titleize }
+ end
+
+ # 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, params = {})
+ preview = new(params)
+ message = preview.public_send(email)
+ inform_preview_interceptors(message)
+ message
+ end
+
+ # Returns all of the available email previews.
+ def emails
+ public_instance_methods(false).map(&:to_s).sort
+ end
+
+ # Returns +true+ if the email exists.
+ def email_exists?(email)
+ emails.include?(email)
+ end
+
+ # Returns +true+ if the preview exists.
+ def exists?(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 }
+ end
+
+ # Returns the underscored name of the mailer preview without the suffix.
+ def preview_name
+ name.delete_suffix("Preview").underscore
+ end
+
+ private
+ def load_previews
+ preview_paths.each do |preview_path|
+ Dir["#{preview_path}/**/*_preview.rb"].sort.each { |file| require file }
+ end
+ end
+
+ def preview_paths
+ Base.preview_paths
+ end
+
+ def show_previews
+ Base.show_previews
+ end
+
+ def inform_preview_interceptors(message)
+ Base.preview_interceptors.each do |interceptor|
+ interceptor.previewing_email(message)
+ end
+ end
+ end
+ end
+end
diff --git a/actionmailer/lib/action_mailer/queued_delivery.rb b/actionmailer/lib/action_mailer/queued_delivery.rb
new file mode 100644
index 0000000000000..1624b62aa373d
--- /dev/null
+++ b/actionmailer/lib/action_mailer/queued_delivery.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+module ActionMailer
+ module QueuedDelivery
+ extend ActiveSupport::Concern
+
+ included do
+ class_attribute :delivery_job, default: ::ActionMailer::MailDeliveryJob
+ class_attribute :deliver_later_queue_name, default: :mailers
+ end
+ end
+end
diff --git a/actionmailer/lib/action_mailer/railtie.rb b/actionmailer/lib/action_mailer/railtie.rb
index 5c03a29f0fc5b..1f1a41e1ce3ed 100644
--- a/actionmailer/lib/action_mailer/railtie.rb
+++ b/actionmailer/lib/action_mailer/railtie.rb
@@ -1,10 +1,19 @@
+# frozen_string_literal: true
+
+require "active_job/railtie"
require "action_mailer"
require "rails"
require "abstract_controller/railties/routes_helpers"
module ActionMailer
- class Railtie < Rails::Railtie
+ class Railtie < Rails::Railtie # :nodoc:
config.action_mailer = ActiveSupport::OrderedOptions.new
+ config.action_mailer.preview_paths = []
+ config.eager_load_namespaces << ActionMailer
+
+ initializer "action_mailer.deprecator", before: :load_environment_config do |app|
+ app.deprecators[:action_mailer] = ActionMailer.deprecator
+ end
initializer "action_mailer.logger" do
ActiveSupport.on_load(:action_mailer) { self.logger ||= Rails.logger }
@@ -17,27 +26,62 @@ class Railtie < Rails::Railtie
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
+ options.preview_paths |= ["#{Rails.root}/test/mailers/previews"]
# make sure readers methods get compiled
- options.asset_path ||= app.config.asset_path
options.asset_host ||= app.config.asset_host
options.relative_url_root ||= app.config.relative_url_root
ActiveSupport.on_load(:action_mailer) do
include AbstractController::UrlFor
- extend ::AbstractController::Railties::RoutesHelpers.with(app.routes)
+ extend ::AbstractController::Railties::RoutesHelpers.with(app.routes, false)
include app.routes.mounted_helpers
register_interceptors(options.delete(:interceptors))
+ register_preview_interceptors(options.delete(:preview_interceptors))
register_observers(options.delete(:observers))
+ self.preview_paths |= options[:preview_paths]
+
+ if delivery_job = options.delete(:delivery_job)
+ self.delivery_job = delivery_job.constantize
+ end
- options.each { |k,v| send("#{k}=", v) }
+ if options.smtp_settings
+ self.smtp_settings = options.smtp_settings
+ end
+
+ smtp_timeout = options.delete(:smtp_timeout)
+
+ if self.smtp_settings && smtp_timeout
+ self.smtp_settings[:open_timeout] ||= smtp_timeout
+ self.smtp_settings[:read_timeout] ||= smtp_timeout
+ end
+
+ options.each { |k, v| send("#{k}=", v) }
+ end
+
+ ActiveSupport.on_load(:action_dispatch_integration_test) do
+ include ActionMailer::TestHelper
+ include ActionMailer::TestCase::ClearTestDeliveries
end
end
- initializer "action_mailer.compile_config_methods" do
- ActiveSupport.on_load(:action_mailer) do
- config.compile_methods! if config.respond_to?(:compile_methods!)
+ initializer "action_mailer.set_autoload_paths", before: :set_autoload_paths do |app|
+ options = app.config.action_mailer
+ app.config.paths["test/mailers/previews"].concat(options.preview_paths)
+ 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/download/*path" => "rails/mailers#download", internal: true
+ get "/rails/mailers/*path" => "rails/mailers#preview", internal: true
+ 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 0000000000000..2c881505ae27b
--- /dev/null
+++ b/actionmailer/lib/action_mailer/rescuable.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+module ActionMailer # :nodoc:
+ # = Action Mailer \Rescuable
+ #
+ # Provides
+ # {rescue_from}[rdoc-ref:ActiveSupport::Rescuable::ClassMethods#rescue_from]
+ # for mailers. Wraps mailer action processing, mail job processing, and mail
+ # delivery to handle configured errors.
+ 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 529140dfad90c..1a07d8e79242e 100644
--- a/actionmailer/lib/action_mailer/test_case.rb
+++ b/actionmailer/lib/action_mailer/test_case.rb
@@ -1,25 +1,48 @@
-require 'active_support/test_case'
-require 'active_support/core_ext/class/attribute'
+# 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
+ include ActiveSupport::Testing::ConstantLookup
include TestHelper
+ include Rails::Dom::Testing::Assertions::SelectorAssertions
+ include Rails::Dom::Testing::Assertions::DomAssertions
included do
class_attribute :_mailer_class
setup :initialize_test_deliveries
setup :set_expected_mail
+ teardown :restore_test_deliveries
+ ActiveSupport.run_load_hooks(:action_mailer_test_case, self)
end
module ClassMethods
@@ -35,7 +58,7 @@ def tests(mailer)
end
def mailer_class
- if mailer = self._mailer_class
+ if mailer = _mailer_class
mailer
else
tests determine_default_mailer(name)
@@ -43,28 +66,52 @@ def mailer_class
end
def determine_default_mailer(name)
- name.sub(/Test$/, '').constantize
- rescue NameError
- raise NonInferrableMailerError.new(name)
+ mailer = determine_constant_from_test_name(name) do |constant|
+ Class === constant && constant < ActionMailer::Base
+ end
+ raise NonInferrableMailerError.new(name) if mailer.nil?
+ mailer
end
end
- protected
+ # Reads the fixture file for the given mailer.
+ #
+ # This is useful when testing mailers by being able to write the body of
+ # an email inside a fixture. See the testing guide for a concrete example:
+ # https://guides.rubyonrails.org/testing.html#revenge-of-the-fixtures
+ def read_fixture(action)
+ IO.readlines(File.join(Rails.root, "test", "fixtures", self.class.mailer_class.name.underscore, action))
+ end
+ private
def initialize_test_deliveries
- ActionMailer::Base.delivery_method = :test
+ 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
+ restore_delivery_method
+ ActionMailer::Base.perform_deliveries = @old_perform_deliveries
+ end
+
+ def set_delivery_method(method)
+ @old_delivery_method = ActionMailer::Base.delivery_method
+ ActionMailer::Base.delivery_method = method
+ end
+
+ def restore_delivery_method
+ ActionMailer::Base.deliveries.clear
+ ActionMailer::Base.delivery_method = @old_delivery_method
+ end
+
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
@@ -72,10 +119,6 @@ def charset
def encode(subject)
Mail::Encodings.q_value_encode(subject, charset)
end
-
- def read_fixture(action)
- IO.readlines(File.join(Rails.root, 'test', 'fixtures', self.class.mailer_class.name.underscore, action))
- end
end
include Behavior
diff --git a/actionmailer/lib/action_mailer/test_helper.rb b/actionmailer/lib/action_mailer/test_helper.rb
index 7204822395ebc..32fcb66873834 100644
--- a/actionmailer/lib/action_mailer/test_helper.rb
+++ b/actionmailer/lib/action_mailer/test_helper.rb
@@ -1,43 +1,51 @@
+# frozen_string_literal: true
+
+require "active_support/core_ext/array/extract_options"
+require "active_job"
+
module ActionMailer
+ # Provides helper methods for testing Action Mailer, including #assert_emails
+ # and #assert_no_emails.
module TestHelper
+ include ActiveJob::TestHelper
+
# Asserts that the number of emails sent matches the given number.
#
# def test_emails
# assert_emails 0
- # ContactMailer.deliver_contact
+ # ContactMailer.welcome.deliver_now
# assert_emails 1
- # ContactMailer.deliver_contact
+ # ContactMailer.welcome.deliver_now
# assert_emails 2
# end
#
- # If a block is passed, that block should cause the specified number of emails to be sent.
+ # If a block is passed, that block should cause the specified number of
+ # emails to be sent.
#
# def test_emails_again
# assert_emails 1 do
- # ContactMailer.deliver_contact
+ # ContactMailer.welcome.deliver_now
# end
#
# assert_emails 2 do
- # ContactMailer.deliver_contact
- # ContactMailer.deliver_contact
+ # 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
- new_count = ActionMailer::Base.deliveries.size
- assert_equal original_count + number, new_count, "#{number} emails expected, but #{new_count - original_count} were sent"
+ diff = capture_emails(&block).length
+ assert_equal number, diff, "#{number} emails expected, but #{diff} were sent"
else
assert_equal number, ActionMailer::Base.deliveries.size
end
end
- # Assert that no emails have been sent.
+ # Asserts that no emails have been sent.
#
# def test_emails
# assert_no_emails
- # ContactMailer.deliver_contact
+ # ContactMailer.welcome.deliver_now
# assert_emails 1
# end
#
@@ -51,9 +59,206 @@ def assert_emails(number)
#
# 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
+
+ # Asserts that the number of emails enqueued for later delivery matches
+ # the given number.
+ #
+ # def test_emails
+ # assert_enqueued_emails 0
+ # ContactMailer.welcome.deliver_later
+ # assert_enqueued_emails 1
+ # ContactMailer.welcome.deliver_later
+ # assert_enqueued_emails 2
+ # end
+ #
+ # If a block is passed, that block should cause the specified number of
+ # emails to be enqueued.
+ #
+ # def test_emails_again
+ # assert_enqueued_emails 1 do
+ # ContactMailer.welcome.deliver_later
+ # end
+ #
+ # assert_enqueued_emails 2 do
+ # ContactMailer.welcome.deliver_later
+ # ContactMailer.welcome.deliver_later
+ # end
+ # end
+ def assert_enqueued_emails(number, &block)
+ assert_enqueued_jobs(number, only: ->(job) { delivery_job_filter(job) }, &block)
+ end
+
+ # Asserts that a specific email has been enqueued, optionally
+ # matching arguments and/or params.
+ #
+ # def test_email
+ # ContactMailer.welcome.deliver_later
+ # assert_enqueued_email_with ContactMailer, :welcome
+ # end
+ #
+ # def test_email_with_parameters
+ # ContactMailer.with(greeting: "Hello").welcome.deliver_later
+ # assert_enqueued_email_with ContactMailer, :welcome, args: { greeting: "Hello" }
+ # end
+ #
+ # def test_email_with_arguments
+ # ContactMailer.welcome("Hello", "Goodbye").deliver_later
+ # assert_enqueued_email_with ContactMailer, :welcome, args: ["Hello", "Goodbye"]
+ # end
+ #
+ # def test_email_with_named_arguments
+ # ContactMailer.welcome(greeting: "Hello", farewell: "Goodbye").deliver_later
+ # assert_enqueued_email_with ContactMailer, :welcome, args: [{ greeting: "Hello", farewell: "Goodbye" }]
+ # end
+ #
+ # def test_email_with_parameters_and_arguments
+ # ContactMailer.with(greeting: "Hello").welcome("Cheers", "Goodbye").deliver_later
+ # assert_enqueued_email_with ContactMailer, :welcome, params: { greeting: "Hello" }, args: ["Cheers", "Goodbye"]
+ # end
+ #
+ # def test_email_with_parameters_and_named_arguments
+ # ContactMailer.with(greeting: "Hello").welcome(farewell: "Goodbye").deliver_later
+ # assert_enqueued_email_with ContactMailer, :welcome, params: { greeting: "Hello" }, args: [{farewell: "Goodbye"}]
+ # end
+ #
+ # def test_email_with_parameterized_mailer
+ # ContactMailer.with(greeting: "Hello").welcome.deliver_later
+ # assert_enqueued_email_with ContactMailer.with(greeting: "Hello"), :welcome
+ # end
+ #
+ # def test_email_with_matchers
+ # ContactMailer.with(greeting: "Hello").welcome("Cheers", "Goodbye").deliver_later
+ # assert_enqueued_email_with ContactMailer, :welcome,
+ # params: ->(params) { /hello/i.match?(params[:greeting]) },
+ # args: ->(args) { /cheers/i.match?(args[0]) }
+ # 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, params: nil, args: nil, queue: nil, &block)
+ if mailer.is_a? ActionMailer::Parameterized::Mailer
+ params = mailer.instance_variable_get(:@params)
+ mailer = mailer.instance_variable_get(:@mailer)
+ end
+
+ args = Array(args) unless args.is_a?(Proc)
+ queue ||= mailer.deliver_later_queue_name || ActiveJob::Base.default_queue_name
+
+ expected = ->(job_args) do
+ job_kwargs = job_args.extract_options!
+
+ [mailer.to_s, method.to_s, "deliver_now"] == job_args &&
+ params === job_kwargs[:params] && args === job_kwargs[:args]
+ end
+
+ assert_enqueued_with(job: mailer.delivery_job, args: expected, queue: queue.to_s, &block)
+ end
+
+ # Asserts that no emails are enqueued for later delivery.
+ #
+ # def test_no_emails
+ # assert_no_enqueued_emails
+ # ContactMailer.welcome.deliver_later
+ # assert_enqueued_emails 1
+ # end
+ #
+ # If a block is provided, it should not cause any emails to be enqueued.
+ #
+ # def test_no_emails
+ # assert_no_enqueued_emails do
+ # # No emails should be enqueued from this block
+ # end
+ # end
+ def assert_no_enqueued_emails(&block)
+ assert_enqueued_emails 0, &block
+ end
+
+ # Delivers all enqueued emails. If a block is given, delivers all of the emails
+ # that were enqueued throughout the duration of the block. If a block is
+ # not given, delivers all the enqueued emails up to this point in the test.
+ #
+ # def test_deliver_enqueued_emails
+ # deliver_enqueued_emails do
+ # ContactMailer.welcome.deliver_later
+ # end
+ #
+ # assert_emails 1
+ # end
+ #
+ # def test_deliver_enqueued_emails_without_block
+ # ContactMailer.welcome.deliver_later
+ #
+ # deliver_enqueued_emails
+ #
+ # assert_emails 1
+ # end
+ #
+ # If the +:queue+ option is specified,
+ # then only the emails(s) enqueued to a specific queue will be performed.
+ #
+ # def test_deliver_enqueued_emails_with_queue
+ # deliver_enqueued_emails queue: :external_mailers do
+ # CustomerMailer.deliver_later_queue_name = :external_mailers
+ # CustomerMailer.welcome.deliver_later # will be performed
+ # EmployeeMailer.deliver_later_queue_name = :internal_mailers
+ # EmployeeMailer.welcome.deliver_later # will not be performed
+ # end
+ #
+ # assert_emails 1
+ # end
+ #
+ # If the +:at+ option is specified, then only delivers emails enqueued to deliver
+ # immediately or before the given time.
+ def deliver_enqueued_emails(queue: nil, at: nil, &block)
+ perform_enqueued_jobs(only: ->(job) { delivery_job_filter(job) }, queue: queue, at: at, &block)
+ end
+
+ # Returns any emails that are sent in the block.
+ #
+ # def test_emails
+ # emails = capture_emails do
+ # ContactMailer.welcome.deliver_now
+ # end
+ # assert_equal "Hi there", emails.first.subject
+ #
+ # emails = capture_emails do
+ # ContactMailer.welcome.deliver_now
+ # ContactMailer.welcome.deliver_later
+ # end
+ # assert_equal "Hi there", emails.first.subject
+ # end
+ def capture_emails(&block)
+ original_count = ActionMailer::Base.deliveries.size
+ deliver_enqueued_emails(&block)
+ new_count = ActionMailer::Base.deliveries.size
+ diff = new_count - original_count
+ ActionMailer::Base.deliveries.last(diff)
+ end
+
+ private
+ def delivery_job_filter(job)
+ job_class = job.is_a?(Hash) ? job.fetch(:job) : job.class
+
+ Base.descendants.map(&:delivery_job).include?(job_class)
+ end
end
end
diff --git a/actionmailer/lib/action_mailer/version.rb b/actionmailer/lib/action_mailer/version.rb
index 0cb5dc6d6465c..9be6e66e07bb0 100644
--- a/actionmailer/lib/action_mailer/version.rb
+++ b/actionmailer/lib/action_mailer/version.rb
@@ -1,10 +1,11 @@
-module ActionMailer
- module VERSION #:nodoc:
- MAJOR = 4
- MINOR = 0
- TINY = 0
- PRE = "beta"
+# frozen_string_literal: true
+
+require_relative "gem_version"
- STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.')
+module ActionMailer
+ # Returns the currently loaded version of Action Mailer as a
+ # +Gem::Version+.
+ def self.version
+ gem_version
end
end
diff --git a/actionmailer/lib/rails/generators/mailer/USAGE b/actionmailer/lib/rails/generators/mailer/USAGE
index 9f1d6b182e06f..6768e5c2a2ac2 100644
--- a/actionmailer/lib/rails/generators/mailer/USAGE
+++ b/actionmailer/lib/rails/generators/mailer/USAGE
@@ -1,18 +1,20 @@
Description:
-============
- Stubs out a new mailer and its views. Passes the mailer name, either
+ Generates a new mailer and its views. Passes the mailer name, either
CamelCased or under_scored, and an optional list of emails as arguments.
This generates a mailer class in app/mailers and invokes your template
engine and test framework generators.
-Example:
-========
- rails generate mailer Notifications signup forgot_password invoice
+Examples:
+ `bin/rails generate mailer sign_up`
+
+ creates a sign up mailer class, views, and test:
+ Mailer: app/mailers/sign_up_mailer.rb
+ Views: app/views/sign_up_mailer/signup.text.erb [...]
+ Test: test/mailers/sign_up_mailer_test.rb
+
+ `bin/rails generate mailer notifications sign_up forgot_password invoice`
+
+ creates a notifications mailer with sign_up, forgot_password, and invoice actions.
- creates a Notifications mailer class, views, test, and fixtures:
- Mailer: app/mailers/notifications.rb
- Views: app/views/notifications/signup.erb [...]
- Test: test/functional/notifications_test.rb
- Fixtures: test/fixtures/notifications/signup [...]
diff --git a/actionmailer/lib/rails/generators/mailer/mailer_generator.rb b/actionmailer/lib/rails/generators/mailer/mailer_generator.rb
index dd7fa640c9ded..c37a74c76232a 100644
--- a/actionmailer/lib/rails/generators/mailer/mailer_generator.rb
+++ b/actionmailer/lib/rails/generators/mailer/mailer_generator.rb
@@ -1,16 +1,38 @@
+# 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"
- argument :actions, :type => :array, :default => [], :banner => "method method"
- check_class_collision
+ check_class_collision suffix: "Mailer"
def create_mailer_file
- template "mailer.rb", File.join('app/mailers', class_path, "#{file_name}.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
+
+ 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
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 0000000000000..00fb9bd48fa22
--- /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
deleted file mode 100644
index aaa1f79732f8f..0000000000000
--- a/actionmailer/lib/rails/generators/mailer/templates/mailer.rb
+++ /dev/null
@@ -1,18 +0,0 @@
-<% module_namespacing do -%>
-class <%= class_name %> < ActionMailer::Base
- default <%= key_value :from, '"from@example.com"' %>
-<% actions.each do |action| -%>
-
- # Subject can be set in your I18n file at config/locales/en.yml
- # with the following lookup:
- #
- # en.<%= file_path.gsub("/",".") %>.<%= action %>.subject
- #
- def <%= action %>
- @greeting = "Hi"
-
- mail <%= key_value :to, '"to@example.org"' %>
- end
-<% end -%>
-end
-<% end -%>
diff --git a/actionmailer/lib/rails/generators/mailer/templates/mailer.rb.tt b/actionmailer/lib/rails/generators/mailer/templates/mailer.rb.tt
new file mode 100644
index 0000000000000..333a9f59b8144
--- /dev/null
+++ b/actionmailer/lib/rails/generators/mailer/templates/mailer.rb.tt
@@ -0,0 +1,19 @@
+<% module_namespacing do -%>
+class <%= class_name %>Mailer < ApplicationMailer
+<% actions.each_with_index do |action, index| -%>
+<% if index != 0 -%>
+
+<% end -%>
+ # Subject can be set in your I18n file at config/locales/en.yml
+ # with the following lookup:
+ #
+ # en.<%= file_path.tr("/",".") %>_mailer.<%= action %>.subject
+ #
+ def <%= action %>
+ @greeting = "Hi"
+
+ mail to: "to@example.org"
+ end
+<% end -%>
+end
+<% end -%>
diff --git a/actionmailer/test/abstract_unit.rb b/actionmailer/test/abstract_unit.rb
index 3a519253f978a..d795e466c3003 100644
--- a/actionmailer/test/abstract_unit.rb
+++ b/actionmailer/test/abstract_unit.rb
@@ -1,78 +1,43 @@
-# Pathname has a warning, so require it first while silencing
-# warnings to shut it up.
-#
-# Also, in 1.9, Bundler creates warnings due to overriding
-# Rubygems methods
-begin
- old, $VERBOSE = $VERBOSE, nil
- require 'pathname'
- require File.expand_path('../../../load_paths', __FILE__)
-ensure
- $VERBOSE = old
-end
+# frozen_string_literal: true
-require 'active_support/core_ext/kernel/reporting'
+require_relative "../../tools/strict_warnings"
+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
-lib = File.expand_path("#{File.dirname(__FILE__)}/../lib")
-$:.unshift(lib) unless $:.include?('lib') || $:.include?(lib)
+module Rails
+ def self.root
+ File.expand_path("..", __dir__)
+ end
+end
-require 'minitest/autorun'
-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"
-silence_warnings do
- # These external dependencies have warnings :/
- require 'mail'
-end
+# Emulate AV railtie
+require "action_view"
+ActionMailer::Base.include(ActionView::Layouts)
# Show backtraces for deprecated behavior for quicker cleanup.
-ActiveSupport::Deprecation.debug = true
+ActionMailer.deprecator.debug = true
-# Bogus template processors
-ActionView::Template.register_template_handler :haml, lambda { |template| "Look its HAML!".inspect }
-ActionView::Template.register_template_handler :bak, lambda { |template| "Lame backup".inspect }
+# 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
-class MockSMTP
- def self.deliveries
- @@deliveries
- end
-
- def initialize
- @@deliveries = []
- end
-
- def sendmail(mail, from, to)
- @@deliveries << [mail, from, to]
- end
-
- def start(*args)
- yield self
- end
-end
-
-class Net::SMTP
- def self.new(*args)
- MockSMTP.new
- end
-end
-
-def set_delivery_method(method)
- @old_delivery_method = ActionMailer::Base.delivery_method
- ActionMailer::Base.delivery_method = method
-end
+ActionMailer::Base.delivery_job = ActionMailer::MailDeliveryJob
-def restore_delivery_method
- ActionMailer::Base.delivery_method = @old_delivery_method
+class ActiveSupport::TestCase
+ include ActiveSupport::Testing::MethodCallAssertions
end
-ActiveSupport::Deprecation.silenced = true
+require_relative "../../tools/test_common"
diff --git a/actionmailer/test/assert_select_email_test.rb b/actionmailer/test/assert_select_email_test.rb
new file mode 100644
index 0000000000000..9699fe4000ded
--- /dev/null
+++ b/actionmailer/test/assert_select_email_test.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+
+class AssertSelectEmailTest < ActionMailer::TestCase
+ class AssertSelectMailer < ActionMailer::Base
+ def test(html)
+ mail body: html, content_type: "text/html",
+ subject: "Test e-mail", from: "test@test.host", to: "test "
+ end
+ end
+
+ class AssertMultipartSelectMailer < ActionMailer::Base
+ def test(options)
+ mail subject: "Test e-mail", from: "test@test.host", to: "test " do |format|
+ format.text { render plain: options[:text] }
+ format.html { render plain: options[:html] }
+ end
+ end
+ end
+
+ #
+ # Test assert_select_email
+ #
+
+ def test_assert_select_email
+ assert_raise ActiveSupport::TestCase::Assertion do
+ assert_select_email { }
+ end
+
+ AssertSelectMailer.test("
foo
bar
").deliver_now
+ assert_select_email do
+ assert_select "div:root" do
+ assert_select "p:first-child", "foo"
+ assert_select "p:last-child", "bar"
+ end
+ end
+ end
+
+ def test_assert_select_email_multipart
+ AssertMultipartSelectMailer.test(html: "
foo
bar
", text: "foo bar").deliver_now
+ assert_select_email do
+ assert_select "div:root" do
+ assert_select "p:first-child", "foo"
+ assert_select "p:last-child", "bar"
+ end
+ end
+ end
+end
diff --git a/actionmailer/test/asset_host_test.rb b/actionmailer/test/asset_host_test.rb
index 696a9f11749f6..7e0d7e7cc599b 100644
--- a/actionmailer/test/asset_host_test.rb
+++ b/actionmailer/test/asset_host_test.rb
@@ -1,22 +1,20 @@
-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
-class AssetHostTest < ActiveSupport::TestCase
+class AssetHostTest < ActionMailer::TestCase
def setup
- set_delivery_method :test
- ActionMailer::Base.perform_deliveries = true
- ActionMailer::Base.deliveries.clear
AssetHostMailer.configure do |c|
c.asset_host = "http://www.example.com/"
- c.assets_dir = ''
end
end
@@ -26,31 +24,16 @@ def teardown
def test_asset_host_as_string
mail = AssetHostMailer.email_with_asset
- assert_equal %Q{}, mail.body.to_s.strip
+ assert_dom_equal '', 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')
+ if source.start_with?("/images")
"http://images.example.com/"
- else
- "http://assets.example.com/"
end
}
mail = AssetHostMailer.email_with_asset
- assert_equal %Q{}, mail.body.to_s.strip
- end
-
- def test_asset_host_as_two_argument_proc
- ActionController::Base.config.asset_host = Proc.new {|source,request|
- if request && request.ssl?
- "https://www.example.com/"
- else
- "http://www.example.com/"
- end
- }
- mail = nil
- assert_nothing_raised { mail = AssetHostMailer.email_with_asset }
- assert_equal %Q{}, mail.body.to_s.strip
+ assert_dom_equal '', mail.body.to_s.strip
end
end
diff --git a/actionmailer/test/base_test.rb b/actionmailer/test/base_test.rb
index 65550ab5056fe..49e0cba9fb61a 100644
--- a/actionmailer/test/base_test.rb
+++ b/actionmailer/test/base_test.rb
@@ -1,15 +1,29 @@
-# encoding: utf-8
-require 'abstract_unit'
-require 'active_support/time'
+# frozen_string_literal: true
-require 'mailers/base_mailer'
-require 'mailers/proc_mailer'
-require 'mailers/asset_mailer'
+require "abstract_unit"
+
+require "action_dispatch"
+require "active_support/time"
+
+require "mailers/base_mailer"
+require "mailers/proc_mailer"
+require "mailers/asset_mailer"
class BaseTest < ActiveSupport::TestCase
- def teardown
- ActionMailer::Base.asset_host = nil
- ActionMailer::Base.assets_dir = nil
+ include Rails::Dom::Testing::Assertions::DomAssertions
+
+ setup do
+ @original_delivery_method = ActionMailer::Base.delivery_method
+ ActionMailer::Base.delivery_method = :test
+ @original_asset_host = ActionMailer::Base.asset_host
+ @original_assets_dir = ActionMailer::Base.assets_dir
+ end
+
+ teardown do
+ ActionMailer::Base.asset_host = @original_asset_host
+ ActionMailer::Base.assets_dir = @original_assets_dir
+ BaseMailer.deliveries.clear
+ ActionMailer::Base.delivery_method = @original_delivery_method
end
test "method call to mail does not raise error" do
@@ -19,33 +33,34 @@ def teardown
# 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(["mikel@test.lindsaar.net"], email.reply_to)
+ 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',
- :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)
+ 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(time, email.date)
end
@@ -54,70 +69,89 @@ def teardown
assert_equal("Welcome", email.body.encoded)
end
+ test "mail() doesn't set the mailer as a controller in the execution context" do
+ ActiveSupport::ExecutionContext.clear
+ assert_nil ActiveSupport::ExecutionContext.to_h[:controller]
+ BaseMailer.welcome(from: "someone@example.com", to: "another@example.org").to
+ assert_nil ActiveSupport::ExecutionContext.to_h[:controller]
+ end
+
test "can pass in :body to the mail method hash" do
- email = BaseMailer.welcome(:body => "Hello there")
+ email = BaseMailer.welcome(body: "Hello there")
assert_equal("text/plain", email.mime_type)
assert_equal("Hello there", email.body.encoded)
end
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
+
+ test "mail() using email_address_with_name" do
+ email = BaseMailer.with_name
+ assert_equal("Sunny ", email["To"].value)
+ assert_equal("Mikel ", email["Reply-To"].value)
+ end
+
+ test "mail() using email_address_with_name with blank string as name" do
+ email = BaseMailer.with_blank_name
+ assert_equal("sunny@example.com", email["To"].value)
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("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
@@ -126,24 +160,29 @@ def teardown
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
- email = BaseMailer.attachment_with_content(:body => "I'm the eggman")
+ email = BaseMailer.attachment_with_content(body: "I'm the eggman")
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
@@ -158,33 +197,47 @@ def teardown
assert_equal("logo.png", email.parts[1].filename)
end
- # Defaults values
+ test "can embed an inline attachment and other attachments" do
+ email = BaseMailer.inline_and_other_attachments
+ # Need to call #encoded to force the JIT sort on parts
+ email.encoded
+ assert_equal(2, email.parts.length)
+ assert_equal("multipart/mixed", email.mime_type)
+ assert_equal("multipart/related", email.parts[0].mime_type)
+ assert_equal("multipart/alternative", email.parts[0].parts[0].mime_type)
+ assert_equal("text/plain", email.parts[0].parts[0].parts[0].mime_type)
+ assert_equal("text/html", email.parts[0].parts[0].parts[1].mime_type)
+ assert_equal("logo.png", email.parts[0].parts[1].filename)
+ assert_equal("certificate.pdf", email.parts[1].filename)
+ end
+
+ # Default values
test "uses default charset from class" do
- with_default BaseMailer, :charset => "US-ASCII" do
+ with_default BaseMailer, charset: "US-ASCII" do
email = BaseMailer.welcome
assert_equal("US-ASCII", email.charset)
- email = BaseMailer.welcome(:charset => "iso-8559-1")
+ email = BaseMailer.welcome(charset: "iso-8559-1")
assert_equal("iso-8559-1", email.charset)
end
end
test "uses default content type from class" do
- with_default BaseMailer, :content_type => "text/html" do
+ with_default BaseMailer, content_type: "text/html" do
email = BaseMailer.welcome
assert_equal("text/html", email.mime_type)
- email = BaseMailer.welcome(:content_type => "text/plain")
+ email = BaseMailer.welcome(content_type: "text/plain")
assert_equal("text/plain", email.mime_type)
end
end
test "uses default mime version from class" do
- with_default BaseMailer, :mime_version => "2.0" do
+ with_default BaseMailer, mime_version: "2.0" do
email = BaseMailer.welcome
assert_equal("2.0", email.mime_version)
- email = BaseMailer.welcome(:mime_version => "1.0")
+ email = BaseMailer.welcome(mime_version: "1.0")
assert_equal("1.0", email.mime_version)
end
end
@@ -197,19 +250,93 @@ def teardown
end
test "subject gets default from I18n" do
- BaseMailer.default :subject => nil
- email = BaseMailer.welcome(:subject => nil)
- assert_equal "Welcome", email.subject
+ with_default BaseMailer, subject: nil do
+ email = BaseMailer.welcome(subject: nil)
+ assert_equal "Welcome", email.subject
- I18n.backend.store_translations('en', :base_mailer => {:welcome => {:subject => "New Subject!"}})
- email = BaseMailer.welcome(:subject => nil)
- assert_equal "New Subject!", email.subject
+ 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
+ email = BaseMailer.with_subject_interpolations
+ assert_equal "Will the real Slim Shady please stand up?", email.subject
+ end
end
test "translations are scoped properly" do
- I18n.backend.store_translations('en', :base_mailer => {:email_with_translations => {:greet_user => "Hello %{name}!"}})
- email = BaseMailer.email_with_translations
- assert_equal 'Hello lifo!', email.body.encoded
+ 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
+ end
+ end
+
+ test "adding attachments after mail was called raises exception" do
+ 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"
+ end
+ end
+
+ e = assert_raises(RuntimeError) { LateAttachmentMailer.welcome.message }
+ assert_match(/Can't add attachments after `mail` was called./, e.message)
+ end
+
+ test "adding inline attachments after mail was called raises exception" do
+ 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"
+ end
+ end
+
+ e = assert_raises(RuntimeError) { LateInlineAttachmentMailer.welcome.message }
+ assert_match(/Can't add attachments after `mail` was called./, e.message)
+ end
+
+ test "accessing inline attachments after mail was called works" do
+ class LateInlineAttachmentAccessorMailer < ActionMailer::Base
+ def welcome
+ mail body: "yay", from: "welcome@example.com", to: "to@example.com"
+ attachments.inline["invoice.pdf"]
+ end
+ end
+
+ assert_nothing_raised { LateInlineAttachmentAccessorMailer.welcome.message }
+ end
+
+ test "adding inline attachments while rendering mail works" do
+ class LateInlineAttachmentMailer < ActionMailer::Base
+ def on_render
+ mail from: "welcome@example.com", to: "to@example.com"
+ end
+ end
+
+ mail = LateInlineAttachmentMailer.on_render
+ 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 }
+ end
+
+ test "accessing attachments works after mail was called" do
+ class LateAttachmentAccessorMailer < ActionMailer::Base
+ def welcome
+ 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"]
+ flunk("Should allow access to attachments")
+ end
+ end
+ end
+
+ assert_nothing_raised { LateAttachmentAccessorMailer.welcome.message }
end
# Implicit multipart
@@ -223,37 +350,46 @@ def teardown
assert_equal("HTML Implicit Multipart", email.parts[1].body.encoded)
end
+ test "implicit multipart formats" do
+ email = BaseMailer.implicit_multipart_formats
+ assert_equal(2, email.parts.size)
+ assert_equal("multipart/alternative", email.mime_type)
+ assert_equal("text/plain", email.parts[0].mime_type)
+ assert_equal("Implicit Multipart [:text]", email.parts[0].body.encoded)
+ assert_equal("text/html", email.parts[1].mime_type)
+ assert_equal("Implicit Multipart [:html]", email.parts[1].body.encoded)
+ end
+
test "implicit multipart with sort order" do
order = ["text/html", "text/plain"]
- with_default BaseMailer, :parts_order => order do
+ with_default BaseMailer, parts_order: order do
email = BaseMailer.implicit_multipart
assert_equal("text/html", email.parts[0].mime_type)
assert_equal("text/plain", email.parts[1].mime_type)
- email = BaseMailer.implicit_multipart(:parts_order => order.reverse)
+ email = BaseMailer.implicit_multipart(parts_order: order.reverse)
assert_equal("text/plain", email.parts[0].mime_type)
assert_equal("text/html", email.parts[1].mime_type)
end
end
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)
+ email = BaseMailer.implicit_multipart(attachments: true)
+ 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)
+ with_default BaseMailer, parts_order: order do
+ email = BaseMailer.implicit_multipart(attachments: true)
+ 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
@@ -268,14 +404,38 @@ def teardown
end
test "implicit multipart with other locale" do
- swap I18n, :locale => :pl do
+ swap I18n, locale: :pl do
email = BaseMailer.implicit_with_locale
assert_equal(2, email.parts.size)
assert_equal("multipart/alternative", email.mime_type)
assert_equal("text/plain", email.parts[0].mime_type)
assert_equal("Implicit with locale PL TEXT", email.parts[0].body.encoded)
assert_equal("text/html", email.parts[1].mime_type)
- assert_equal("Implicit with locale HTML", email.parts[1].body.encoded)
+ assert_equal("Implicit with locale EN HTML", email.parts[1].body.encoded)
+ end
+ end
+
+ test "implicit multipart with fallback locale" do
+ fallback_backend = Class.new(I18n::Backend::Simple) do
+ include I18n::Backend::Fallbacks
+ end
+
+ begin
+ backend = I18n.backend
+ I18n.backend = fallback_backend.new
+ I18n.fallbacks[:"de-AT"] = [:de]
+
+ swap I18n, locale: "de-AT" do
+ email = BaseMailer.implicit_with_locale
+ assert_equal(2, email.parts.size)
+ assert_equal("multipart/alternative", email.mime_type)
+ assert_equal("text/plain", email.parts[0].mime_type)
+ assert_equal("Implicit with locale DE-AT TEXT", email.parts[0].body.encoded)
+ assert_equal("text/html", email.parts[1].mime_type)
+ assert_equal("Implicit with locale DE HTML", email.parts[1].body.encoded)
+ end
+ ensure
+ I18n.backend = backend
end
end
@@ -317,37 +477,24 @@ def teardown
assert_not_nil(mail.content_type_parameters[:boundary])
end
- test "explicit multipart does not sort order" do
- order = ["text/html", "text/plain"]
- with_default BaseMailer, :parts_order => order do
- email = BaseMailer.explicit_multipart
- assert_equal("text/plain", email.parts[0].mime_type)
- assert_equal("text/html", email.parts[1].mime_type)
-
- email = BaseMailer.explicit_multipart(:parts_order => order.reverse)
- assert_equal("text/plain", email.parts[0].mime_type)
- assert_equal("text/html", email.parts[1].mime_type)
- end
- end
-
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)
+ email = BaseMailer.explicit_multipart(attachments: true)
+ 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
email = BaseMailer.explicit_multipart_templates
assert_equal(2, email.parts.size)
assert_equal("multipart/alternative", email.mime_type)
- assert_equal("text/html", email.parts[0].mime_type)
- assert_equal("HTML Explicit Multipart Templates", email.parts[0].body.encoded)
- assert_equal("text/plain", email.parts[1].mime_type)
- assert_equal("TEXT Explicit Multipart Templates", email.parts[1].body.encoded)
+ assert_equal("text/plain", email.parts[0].mime_type)
+ assert_equal("TEXT Explicit Multipart Templates", email.parts[0].body.encoded)
+ assert_equal("text/html", email.parts[1].mime_type)
+ assert_equal("HTML Explicit Multipart Templates", email.parts[1].body.encoded)
end
test "explicit multipart with format.any" do
@@ -360,6 +507,13 @@ def teardown
assert_equal("Format with any!", email.parts[1].body.encoded)
end
+ test "explicit without specifying format with format.any" do
+ error = assert_raises(ArgumentError) do
+ BaseMailer.explicit_without_specifying_format_with_any.parts
+ end
+ assert_equal "You have to supply at least one format", error.message
+ end
+
test "explicit multipart with format(Hash)" do
email = BaseMailer.explicit_multipart_with_options(true)
email.ready_to_send!
@@ -382,72 +536,97 @@ def teardown
email = BaseMailer.explicit_multipart_with_one_template
assert_equal(2, email.parts.size)
assert_equal("multipart/alternative", email.mime_type)
- assert_equal("text/html", email.parts[0].mime_type)
- assert_equal("[:html]", email.parts[0].body.encoded)
- assert_equal("text/plain", email.parts[1].mime_type)
- assert_equal("[:text]", email.parts[1].body.encoded)
+ assert_equal("text/plain", email.parts[0].mime_type)
+ assert_equal("[:text]", email.parts[0].body.encoded)
+ assert_equal("text/html", email.parts[1].mime_type)
+ assert_equal("[:html]", email.parts[1].body.encoded)
+ end
+
+ test "explicit multipart with sort order" do
+ order = ["text/html", "text/plain"]
+ with_default BaseMailer, parts_order: order do
+ email = BaseMailer.explicit_multipart
+ assert_equal("text/html", email.parts[0].mime_type)
+ assert_equal("text/plain", email.parts[1].mime_type)
+
+ email = BaseMailer.explicit_multipart(parts_order: order.reverse)
+ assert_equal("text/plain", email.parts[0].mime_type)
+ assert_equal("text/html", email.parts[1].mime_type)
+ end
end
# Class level API with method missing
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
- BaseMailer.deliveries.clear
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
- BaseMailer.deliveries.clear
- BaseMailer.expects(:deliver_mail).once
- mail = BaseMailer.welcome.deliver
- assert_instance_of Mail::Message, mail
+ assert_called(BaseMailer, :deliver_mail) do
+ mail = BaseMailer.welcome.deliver_now
+ assert_equal "The first email on new API!", mail.subject
+ end
end
test "calling deliver on the action should increment the deliveries collection if using the test mailer" do
- BaseMailer.delivery_method = :test
- BaseMailer.deliveries.clear
- BaseMailer.welcome.deliver
+ BaseMailer.welcome.deliver_now
assert_equal(1, BaseMailer.deliveries.length)
end
test "calling deliver, ActionMailer should yield back to mail to let it call :do_delivery on itself" do
mail = Mail::Message.new
- mail.expects(:do_delivery).once
- BaseMailer.expects(:welcome).returns(mail)
- BaseMailer.welcome.deliver
+ assert_called(mail, :do_delivery) do
+ assert_called(BaseMailer, :welcome, returns: mail) do
+ BaseMailer.welcome.deliver
+ end
+ end
end
# Rendering
test "you can specify a different template for implicit render" do
- mail = BaseMailer.implicit_different_template('implicit_multipart').deliver
+ 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
+ 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
+ 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
+ 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
+ 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
+ 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
+ mail = BaseMailer.welcome_from_another_path(["unknown/invalid", "another.path/base_mailer"]).deliver_now
assert_equal("Welcome from another path", mail.body.encoded)
end
@@ -457,18 +636,33 @@ def teardown
mail = AssetMailer.welcome
- assert_equal(%{}, mail.body.to_s.strip)
+ assert_dom_equal(%{}, mail.body.to_s.strip)
end
test "assets tags should use a Mailer's asset_host settings when available" do
- ActionMailer::Base.config.asset_host = "global.com"
+ ActionMailer::Base.config.asset_host = "http://global.com/"
ActionMailer::Base.config.assets_dir = "global/"
- AssetMailer.asset_host = "http://local.com/"
+ TempAssetMailer = Class.new(AssetMailer) do
+ self.mailer_name = "asset_mailer"
+ self.asset_host = "http://local.com/"
+ end
- mail = AssetMailer.welcome
+ mail = TempAssetMailer.welcome
+
+ assert_dom_equal(%{}, mail.body.to_s.strip)
+ end
+
+ test "the view is not rendered when mail was never called" do
+ mail = BaseMailer.without_mail_call
+ assert_equal("", mail.body.to_s.strip)
+ mail.deliver_now
+ end
- assert_equal(%{}, mail.body.to_s.strip)
+ 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)
+ mail.deliver_now
end
# Before and After hooks
@@ -483,66 +677,166 @@ def self.delivered_email(mail)
end
end
- test "you can register an observer to the mail object that gets informed on email delivery" do
- ActionMailer::Base.register_observer(MyObserver)
- mail = BaseMailer.welcome
- MyObserver.expects(:delivered_email).with(mail)
- mail.deliver
+ 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
- ActionMailer::Base.register_observer("BaseTest::MyObserver")
- mail = BaseMailer.welcome
- MyObserver.expects(:delivered_email).with(mail)
- mail.deliver
+ 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 multiple observers to the mail object that both get informed on email delivery" do
- ActionMailer::Base.register_observers("BaseTest::MyObserver", MySecondObserver)
- mail = BaseMailer.welcome
- MyObserver.expects(:delivered_email).with(mail)
- MySecondObserver.expects(:delivered_email).with(mail)
- mail.deliver
+ 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
- class MyInterceptor
- def self.delivering_email(mail)
+ 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
+ assert_called_with(MyObserver, :delivered_email, [mail]) do
+ assert_called_with(MySecondObserver, :delivered_email, [mail]) do
+ 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
+ class MyInterceptor
+ def self.delivering_email(mail); end
+ def self.previewing_email(mail); end
+ end
+
class MySecondInterceptor
- def self.delivering_email(mail)
+ def self.delivering_email(mail); end
+ def self.previewing_email(mail); end
+ end
+
+ 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 to the mail object that gets passed the mail object before delivery" do
- ActionMailer::Base.register_interceptor(MyInterceptor)
- mail = BaseMailer.welcome
- MyInterceptor.expects(:delivering_email).with(mail)
- mail.deliver
+ 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 stringified name to the mail object that gets passed the mail object before delivery" do
- ActionMailer::Base.register_interceptor("BaseTest::MyInterceptor")
- mail = BaseMailer.welcome
- MyInterceptor.expects(:delivering_email).with(mail)
- mail.deliver
+ 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
- ActionMailer::Base.register_interceptors("BaseTest::MyInterceptor", MySecondInterceptor)
- mail = BaseMailer.welcome
- MyInterceptor.expects(:delivering_email).with(mail)
- MySecondInterceptor.expects(:delivering_email).with(mail)
- mail.deliver
+ 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
+ assert_called_with(MyInterceptor, :delivering_email, [mail]) do
+ assert_called_with(MySecondInterceptor, :delivering_email, [mail]) do
+ 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
+ mail1 = ProcMailer.welcome["X-Proc-Method"]
yesterday = 1.day.ago
- Time.stubs(:now).returns(yesterday)
- mail2 = ProcMailer.welcome
- assert(mail1['X-Proc-Method'].to_s.to_i > mail2['X-Proc-Method'].to_s.to_i)
+ Time.stub(:now, yesterday) do
+ 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")
+ end
+
+ test "proc default values can have arity of 1 where arg is a mailer instance" do
+ assert_equal("complex_value", ProcMailer.welcome["X-Lambda-Arity-1-arg"].to_s)
+ assert_equal("complex_value", ProcMailer.welcome["X-Lambda-Arity-1-self"].to_s)
+ 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
@@ -550,32 +844,157 @@ def self.delivering_email(mail)
assert_equal("Thanks for signing up this afternoon", mail.subject)
end
+ test "proc default values are not evaluated when overridden" do
+ with_default BaseMailer, from: -> { flunk }, to: -> { flunk } do
+ email = BaseMailer.welcome(from: "overridden-from@example.com", to: "overridden-to@example.com")
+ assert_equal ["overridden-from@example.com"], email.from
+ assert_equal ["overridden-to@example.com"], email.to
+ end
+ end
+
+ test "modifying the mail message with a before_action" do
+ class BeforeActionMailer < ActionMailer::Base
+ before_action :add_special_header!
+
+ def welcome ; mail ; end
+
+ private
+ 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)
+ end
+
+ test "modifying the mail message with an after_action" do
+ class AfterActionMailer < ActionMailer::Base
+ after_action :add_special_header!
+
+ def welcome ; mail ; end
+
+ private
+ def add_special_header!
+ headers("X-Special-Header" => "Testing")
+ end
+ end
+
+ assert_equal("Testing", AfterActionMailer.welcome["X-Special-Header"].to_s)
+ end
+
+ test "adding an inline attachment using a before_action" do
+ class DefaultInlineAttachmentMailer < ActionMailer::Base
+ before_action :add_inline_attachment!
+
+ def welcome ; mail ; end
+
+ private
+ 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)
+ 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
end
- assert_equal ["notify"], FooMailer.action_methods
+ assert_equal Set.new(["notify"]), FooMailer.action_methods
+ end
+
+ test "mailer can be anonymous" do
+ mailer = Class.new(ActionMailer::Base) do
+ def welcome
+ mail
+ end
+ end
+
+ assert_equal "anonymous", mailer.mailer_name
+
+ assert_equal "Welcome", mailer.welcome.subject
+ assert_equal "Anonymous mailer body", mailer.welcome.body.encoded.strip
+ end
+
+ test "email_address_with_name escapes" do
+ address = BaseMailer.email_address_with_name("test@example.org", 'I "<3" email')
+ assert_equal '"I \"<3\" email" ', address
+ end
+
+ 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" }
+
+ def welcome
+ mail(subject: "subject", body: "hello world")
+ end
+ end
+
+ assert_equal ["robert.pankowecki@gmail.com"], DefaultFromMailer.welcome.from
+ end
+
+ test "mail() without arguments serves as getter for the current mail message" do
+ class MailerWithCallback < ActionMailer::Base
+ after_action :a_callback
+
+ def welcome
+ headers("X-Special-Header" => "special indeed!")
+ mail subject: "subject", body: "hello world", to: ["joe@example.com"]
+ end
+
+ def a_callback
+ mail.to << "jane@example.com"
+ end
+ end
+
+ mail = MailerWithCallback.welcome
+ assert_equal "subject", mail.subject
+ assert_equal ["joe@example.com", "jane@example.com"], mail.to
+ assert_equal "hello world", mail.body.encoded.strip
+ assert_equal "special indeed!", mail["X-Special-Header"].to_s
+ end
+
+ test "notification for process" do
+ expected_payload = { mailer: "BaseMailer", action: :welcome, args: [{ body: "Hello there" }] }
+
+ assert_notifications_count("process.action_mailer", 1) do
+ assert_notification("process.action_mailer", expected_payload) do
+ BaseMailer.welcome(body: "Hello there").deliver_now
+ end
+ end
end
- protected
+ test "notification for deliver" do
+ assert_notifications_count("deliver.action_mailer", 1) do
+ notification = assert_notification("deliver.action_mailer") do
+ BaseMailer.welcome(body: "Hello there").deliver_now
+ end
+ assert_not_nil notification.payload[:message_id]
+ end
+ end
+
+ private
# Execute the block setting the given values and restoring old values after
# the block is executed.
def swap(klass, new_values)
old_values = {}
new_values.each do |key, value|
- old_values[key] = klass.send key
- klass.send :"#{key}=", value
+ old_values[key] = klass.public_send key
+ klass.public_send :"#{key}=", value
end
yield
ensure
old_values.each do |key, value|
- klass.send :"#{key}=", value
+ klass.public_send :"#{key}=", value
end
end
@@ -586,4 +1005,148 @@ def with_default(klass, new_values)
ensure
klass.default_params = old
end
+
+ def mail_side_effects
+ old_observers = Mail.class_variable_get(:@@delivery_notification_observers)
+ old_delivery_interceptors = Mail.class_variable_get(:@@delivery_interceptors)
+ yield
+ ensure
+ Mail.class_variable_set(:@@delivery_notification_observers, old_observers)
+ Mail.class_variable_set(:@@delivery_interceptors, old_delivery_interceptors)
+ end
+
+ def with_translation(locale, data)
+ I18n.backend.store_translations(locale, data)
+ yield
+ ensure
+ I18n.backend.reload!
+ end
+end
+
+class BasePreviewInterceptorsTest < ActiveSupport::TestCase
+ teardown do
+ ActionMailer::Base.preview_interceptors.clear
+ end
+
+ class BaseMailerPreview < ActionMailer::Preview
+ def welcome
+ BaseMailer.welcome
+ end
+ end
+
+ class MyInterceptor
+ def self.delivering_email(mail); end
+ def self.previewing_email(mail); end
+ end
+
+ class MySecondInterceptor
+ def self.delivering_email(mail); end
+ def self.previewing_email(mail); end
+ end
+
+ 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|
+ instance.stub(:welcome, mail) do
+ assert_called_with(MyInterceptor, :previewing_email, [mail]) do
+ BaseMailerPreview.call(:welcome)
+ 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 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|
+ instance.stub(:welcome, mail) do
+ assert_called_with(MyInterceptor, :previewing_email, [mail]) do
+ BaseMailerPreview.call(:welcome)
+ 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 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|
+ instance.stub(:welcome, mail) do
+ assert_called_with(MyInterceptor, :previewing_email, [mail]) do
+ BaseMailerPreview.call(:welcome)
+ 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 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|
+ instance.stub(:welcome, mail) do
+ assert_called_with(MyInterceptor, :previewing_email, [mail]) do
+ assert_called_with(MySecondInterceptor, :previewing_email, [mail]) do
+ BaseMailerPreview.call(:welcome)
+ end
+ 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 PreviewTest < ActiveSupport::TestCase
+ class A < ActionMailer::Preview; end
+
+ module B
+ class A < ActionMailer::Preview; end
+ class C < ActionMailer::Preview; end
+ end
+
+ class C < ActionMailer::Preview; end
+
+ test "all() returns mailers in alphabetical order" do
+ ActionMailer::Preview.stub(:descendants, [C, A, B::C, B::A]) do
+ mailers = ActionMailer::Preview.all
+ assert_equal [A, B::A, B::C, C], mailers
+ 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 0000000000000..1a495159b2c45
--- /dev/null
+++ b/actionmailer/test/caching_test.rb
@@ -0,0 +1,243 @@
+# 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", "html")}/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", "html")}/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
+
+ expected_payload = {
+ mailer: "caching_mailer",
+ key: [:views, "caching_mailer/fragment_cache:#{template_digest("caching_mailer/fragment_cache", "html")}", :caching]
+ }
+
+ assert_notification("read_fragment.action_mailer", expected_payload) do
+ @mailer.fragment_cache
+ end
+ ensure
+ @mailer.enable_fragment_cache_logging = true
+ end
+
+ private
+ def template_digest(name, format)
+ ActionView::Digestor.digest(name: name, format: format, 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_nothing_raised do
+ cache_helper.send :fragment_for, "Test fragment name", "Test fragment", &Proc.new { nil }
+ 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/callbacks_test.rb b/actionmailer/test/callbacks_test.rb
new file mode 100644
index 0000000000000..3a2cf8f31822c
--- /dev/null
+++ b/actionmailer/test/callbacks_test.rb
@@ -0,0 +1,86 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "mailers/callback_mailer"
+require "active_support/testing/stream"
+
+class ActionMailerCallbacksTest < ActiveSupport::TestCase
+ include ActiveJob::TestHelper
+ include ActiveSupport::Testing::Stream
+
+ setup do
+ @previous_delivery_method = ActionMailer::Base.delivery_method
+ ActionMailer::Base.delivery_method = :test
+ CallbackMailer.rescue_from_error = nil
+ CallbackMailer.after_deliver_instance = nil
+ CallbackMailer.around_deliver_instance = nil
+ CallbackMailer.abort_before_deliver = nil
+ CallbackMailer.around_handles_error = nil
+ end
+
+ teardown do
+ ActionMailer::Base.deliveries.clear
+ ActionMailer::Base.delivery_method = @previous_delivery_method
+ CallbackMailer.rescue_from_error = nil
+ CallbackMailer.after_deliver_instance = nil
+ CallbackMailer.around_deliver_instance = nil
+ CallbackMailer.abort_before_deliver = nil
+ CallbackMailer.around_handles_error = nil
+ end
+
+ test "deliver_now should call after_deliver callback and can access sent message" do
+ mail_delivery = CallbackMailer.test_message
+ mail_delivery.deliver_now
+
+ assert_kind_of CallbackMailer, CallbackMailer.after_deliver_instance
+ assert_not_empty CallbackMailer.after_deliver_instance.message.message_id
+ assert_equal mail_delivery.message_id, CallbackMailer.after_deliver_instance.message.message_id
+ assert_equal "test-receiver@test.com", CallbackMailer.after_deliver_instance.message.to.first
+ end
+
+ test "deliver_now! should call after_deliver callback" do
+ CallbackMailer.test_message.deliver_now!
+
+ assert_kind_of CallbackMailer, CallbackMailer.after_deliver_instance
+ end
+
+ test "before_deliver can abort the delivery and not run after_deliver callbacks" do
+ CallbackMailer.abort_before_deliver = true
+
+ mail_delivery = CallbackMailer.test_message
+ mail_delivery.deliver_now
+
+ assert_nil mail_delivery.message_id
+ assert_nil CallbackMailer.after_deliver_instance
+ end
+
+ test "deliver_later should call after_deliver callback and can access sent message" do
+ perform_enqueued_jobs do
+ silence_stream($stdout) do
+ CallbackMailer.test_message.deliver_later
+ end
+ end
+ assert_kind_of CallbackMailer, CallbackMailer.after_deliver_instance
+ assert_not_empty CallbackMailer.after_deliver_instance.message.message_id
+ end
+
+ test "around_deliver is called after rescue_from on action processing exceptions" do
+ CallbackMailer.around_handles_error = true
+
+ CallbackMailer.test_raise_action.deliver_now
+ assert CallbackMailer.rescue_from_error
+ end
+
+ test "around_deliver is called before rescue_from on deliver! exceptions" do
+ CallbackMailer.around_handles_error = true
+
+ stub_any_instance(Mail::TestMailer, instance: Mail::TestMailer.new({})) do |instance|
+ instance.stub(:deliver!, proc { raise "boom deliver exception" }) do
+ CallbackMailer.test_message.deliver_now
+ end
+ end
+
+ assert_kind_of CallbackMailer, CallbackMailer.after_deliver_instance
+ assert_nil CallbackMailer.rescue_from_error
+ end
+end
diff --git a/actionmailer/test/delivery_methods_test.rb b/actionmailer/test/delivery_methods_test.rb
index 08f84dbf3b40e..a39eb37eaa907 100644
--- a/actionmailer/test/delivery_methods_test.rb
+++ b/actionmailer/test/delivery_methods_test.rb
@@ -1,9 +1,17 @@
-require 'abstract_unit'
-require 'mail'
+# frozen_string_literal: true
+
+require "abstract_unit"
class MyCustomDelivery
end
+class MyOptionedDelivery
+ attr_reader :options
+ def initialize(options)
+ @options = options
+ end
+end
+
class BogusDelivery
def initialize(*)
end
@@ -15,35 +23,37 @@ def deliver!(mail)
class DefaultsDeliveryMethodsTest < ActiveSupport::TestCase
test "default smtp settings" do
- settings = { :address => "localhost",
- :port => 25,
- :domain => 'localhost.localdomain',
- :user_name => nil,
- :password => nil,
- :authentication => nil,
- :enable_starttls_auto => true }
+ settings = { address: "localhost",
+ port: 25,
+ domain: "localhost.localdomain",
+ user_name: nil,
+ password: nil,
+ authentication: nil,
+ enable_starttls_auto: true }
assert_equal settings, ActionMailer::Base.smtp_settings
end
- test "default file delivery settings" do
- settings = {:location => "#{Dir.tmpdir}/mails"}
+ test "default file delivery settings (with Rails.root)" do
+ 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'}
+ settings = {
+ location: "/usr/sbin/sendmail",
+ arguments: %w[ -i ]
+ }
assert_equal settings, ActionMailer::Base.sendmail_settings
end
end
class CustomDeliveryMethodsTest < ActiveSupport::TestCase
- def setup
+ setup do
@old_delivery_method = ActionMailer::Base.delivery_method
ActionMailer::Base.add_delivery_method :custom, MyCustomDelivery
end
- def teardown
+ teardown do
ActionMailer::Base.delivery_method = @old_delivery_method
new = ActionMailer::Base.delivery_methods.dup
new.delete(:custom)
@@ -56,8 +66,8 @@ def teardown
end
test "allow to customize custom settings" do
- ActionMailer::Base.custom_settings = { :foo => :bar }
- assert_equal Hash[:foo => :bar], ActionMailer::Base.custom_settings
+ ActionMailer::Base.custom_settings = { foo: :bar }
+ assert_equal Hash[foo: :bar], ActionMailer::Base.custom_settings
end
test "respond to custom settings" do
@@ -75,98 +85,164 @@ def teardown
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
- def setup
- ActionMailer::Base.delivery_method = :smtp
+ setup do
+ @old_delivery_method = DeliveryMailer.delivery_method
end
- def teardown
- DeliveryMailer.delivery_method = :smtp
- DeliveryMailer.perform_deliveries = true
- DeliveryMailer.raise_delivery_errors = true
+ teardown do
+ DeliveryMailer.delivery_method = @old_delivery_method
+ DeliveryMailer.deliveries.clear
end
test "ActionMailer should be told when Mail gets delivered" do
- DeliveryMailer.deliveries.clear
- DeliveryMailer.expects(:deliver_mail).once
- DeliveryMailer.welcome.deliver
+ DeliveryMailer.delivery_method = :test
+ assert_called(DeliveryMailer, :deliver_mail) do
+ DeliveryMailer.welcome.deliver_now
+ end
end
test "delivery method can be customized per instance" do
- email = DeliveryMailer.welcome.deliver
- assert_instance_of Mail::SMTP, email.delivery_method
- email = DeliveryMailer.welcome(:delivery_method => :test).deliver
- assert_instance_of Mail::TestMailer, email.delivery_method
+ stub_any_instance(Mail::SMTP, instance: Mail::SMTP.new({})) do |instance|
+ assert_called(instance, :deliver!) do
+ email = DeliveryMailer.welcome.deliver_now
+ assert_instance_of Mail::SMTP, email.delivery_method
+ email = DeliveryMailer.welcome(delivery_method: :test).deliver_now
+ assert_instance_of Mail::TestMailer, email.delivery_method
+ end
+ end
end
test "delivery method can be customized in subclasses not changing the parent" do
DeliveryMailer.delivery_method = :test
assert_equal :smtp, ActionMailer::Base.delivery_method
- $BREAK = true
- email = DeliveryMailer.welcome.deliver
+ email = DeliveryMailer.welcome.deliver_now
assert_instance_of Mail::TestMailer, email.delivery_method
end
+ test "delivery method options default to class level options" do
+ 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" }
+ ActionMailer::Base.add_delivery_method :optioned, MyOptionedDelivery, default_options
+ 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
+
+ test "default delivery options can be overridden per mail instance" do
+ settings = {
+ address: "localhost",
+ port: 25,
+ 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" }
+ 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]
+ assert_equal "somethingobtuse", delivery_method_instance.settings[:password]
+ assert_equal delivery_method_instance.settings.merge(overridden_options), delivery_method_instance.settings
+
+ # make sure that overriding delivery method options per mail instance doesn't affect the Base setting
+ assert_equal settings, ActionMailer::Base.smtp_settings
+ end
+
test "non registered delivery methods raises errors" do
DeliveryMailer.delivery_method = :unknown
- assert_raise RuntimeError do
- DeliveryMailer.welcome.deliver
+ error = assert_raise RuntimeError do
+ DeliveryMailer.welcome.deliver_now
end
+ assert_equal "Invalid delivery method :unknown", error.message
+ end
+
+ test "undefined delivery methods raises errors" do
+ DeliveryMailer.delivery_method = nil
+ error = assert_raise RuntimeError do
+ DeliveryMailer.welcome.deliver_now
+ end
+ assert_equal "Delivery method cannot be nil", error.message
end
test "does not perform deliveries if requested" do
- DeliveryMailer.perform_deliveries = false
- DeliveryMailer.deliveries.clear
- Mail::Message.any_instance.expects(:deliver!).never
- DeliveryMailer.welcome.deliver
+ old_perform_deliveries = DeliveryMailer.perform_deliveries
+ begin
+ DeliveryMailer.perform_deliveries = false
+ stub_any_instance(Mail::Message) do |instance|
+ assert_not_called(instance, :deliver!) do
+ DeliveryMailer.welcome.deliver_now
+ end
+ end
+ ensure
+ DeliveryMailer.perform_deliveries = old_perform_deliveries
+ end
end
test "does not append the deliveries collection if told not to perform the delivery" do
- DeliveryMailer.perform_deliveries = false
- DeliveryMailer.deliveries.clear
- DeliveryMailer.welcome.deliver
- assert_equal(0, DeliveryMailer.deliveries.length)
+ old_perform_deliveries = DeliveryMailer.perform_deliveries
+ begin
+ DeliveryMailer.perform_deliveries = false
+ DeliveryMailer.welcome.deliver_now
+ assert_equal [], DeliveryMailer.deliveries
+ ensure
+ DeliveryMailer.perform_deliveries = old_perform_deliveries
+ end
end
test "raise errors on bogus deliveries" do
DeliveryMailer.delivery_method = BogusDelivery
- DeliveryMailer.deliveries.clear
assert_raise RuntimeError do
- DeliveryMailer.welcome.deliver
+ DeliveryMailer.welcome.deliver_now
end
end
test "does not increment the deliveries collection on error" do
DeliveryMailer.delivery_method = BogusDelivery
- DeliveryMailer.deliveries.clear
assert_raise RuntimeError do
- DeliveryMailer.welcome.deliver
+ DeliveryMailer.welcome.deliver_now
end
- assert_equal(0, DeliveryMailer.deliveries.length)
+ assert_equal [], DeliveryMailer.deliveries
end
test "does not raise errors on bogus deliveries if set" do
- DeliveryMailer.delivery_method = BogusDelivery
- DeliveryMailer.raise_delivery_errors = false
- assert_nothing_raised do
- DeliveryMailer.welcome.deliver
+ old_raise_delivery_errors = DeliveryMailer.raise_delivery_errors
+ begin
+ DeliveryMailer.delivery_method = BogusDelivery
+ DeliveryMailer.raise_delivery_errors = false
+ assert_nothing_raised do
+ DeliveryMailer.welcome.deliver_now
+ end
+ ensure
+ DeliveryMailer.raise_delivery_errors = old_raise_delivery_errors
end
end
test "does not increment the deliveries collection on bogus deliveries" do
- DeliveryMailer.delivery_method = BogusDelivery
- DeliveryMailer.raise_delivery_errors = false
- DeliveryMailer.deliveries.clear
- DeliveryMailer.welcome.deliver
- assert_equal(0, DeliveryMailer.deliveries.length)
+ old_raise_delivery_errors = DeliveryMailer.raise_delivery_errors
+ begin
+ DeliveryMailer.delivery_method = BogusDelivery
+ DeliveryMailer.raise_delivery_errors = false
+ DeliveryMailer.welcome.deliver_now
+ assert_equal [], DeliveryMailer.deliveries
+ ensure
+ DeliveryMailer.raise_delivery_errors = old_raise_delivery_errors
+ end
end
-
end
diff --git a/actionmailer/test/fixtures/anonymous/welcome.erb b/actionmailer/test/fixtures/anonymous/welcome.erb
new file mode 100644
index 0000000000000..8361da62c4e5d
--- /dev/null
+++ b/actionmailer/test/fixtures/anonymous/welcome.erb
@@ -0,0 +1 @@
+Anonymous mailer body
diff --git a/actionmailer/test/fixtures/attachments/foo.jpg b/actionmailer/test/fixtures/attachments/foo.jpg
deleted file mode 100644
index b976fe5e002bf..0000000000000
Binary files a/actionmailer/test/fixtures/attachments/foo.jpg and /dev/null differ
diff --git a/actionmailer/test/fixtures/attachments/test.jpg b/actionmailer/test/fixtures/attachments/test.jpg
deleted file mode 100644
index b976fe5e002bf..0000000000000
Binary files a/actionmailer/test/fixtures/attachments/test.jpg and /dev/null differ
diff --git a/actionpack/test/tmp/.gitignore b/actionmailer/test/fixtures/base_mailer/attachment_with_hash.html.erb
similarity index 100%
rename from actionpack/test/tmp/.gitignore
rename to actionmailer/test/fixtures/base_mailer/attachment_with_hash.html.erb
diff --git a/activerecord/test/migrations/empty/.gitkeep b/actionmailer/test/fixtures/base_mailer/attachment_with_hash_default_encoding.html.erb
similarity index 100%
rename from activerecord/test/migrations/empty/.gitkeep
rename to actionmailer/test/fixtures/base_mailer/attachment_with_hash_default_encoding.html.erb
diff --git a/actionmailer/test/fixtures/base_mailer/email_custom_layout.text.html.erb b/actionmailer/test/fixtures/base_mailer/email_custom_layout.text.html.erb
deleted file mode 100644
index a2187308b6dfa..0000000000000
--- a/actionmailer/test/fixtures/base_mailer/email_custom_layout.text.html.erb
+++ /dev/null
@@ -1 +0,0 @@
-body_text
\ No newline at end of file
diff --git a/actionmailer/test/fixtures/base_mailer/email_with_translations.html.erb b/actionmailer/test/fixtures/base_mailer/email_with_translations.html.erb
index 30466dd005391..d676a6d2da03d 100644
--- a/actionmailer/test/fixtures/base_mailer/email_with_translations.html.erb
+++ b/actionmailer/test/fixtures/base_mailer/email_with_translations.html.erb
@@ -1 +1 @@
-<%= t('.greet_user', :name => 'lifo') %>
\ No newline at end of file
+<%= t('.greet_user', name: 'lifo') %>
\ No newline at end of file
diff --git a/actionmailer/test/fixtures/base_mailer/implicit_multipart_formats.html.erb b/actionmailer/test/fixtures/base_mailer/implicit_multipart_formats.html.erb
new file mode 100644
index 0000000000000..0179b070b8a65
--- /dev/null
+++ b/actionmailer/test/fixtures/base_mailer/implicit_multipart_formats.html.erb
@@ -0,0 +1 @@
+Implicit Multipart <%= formats.inspect %>
\ No newline at end of file
diff --git a/actionmailer/test/fixtures/base_mailer/implicit_multipart_formats.text.erb b/actionmailer/test/fixtures/base_mailer/implicit_multipart_formats.text.erb
new file mode 100644
index 0000000000000..0179b070b8a65
--- /dev/null
+++ b/actionmailer/test/fixtures/base_mailer/implicit_multipart_formats.text.erb
@@ -0,0 +1 @@
+Implicit Multipart <%= formats.inspect %>
\ No newline at end of file
diff --git a/actionmailer/test/fixtures/base_mailer/implicit_with_locale.de-AT.text.erb b/actionmailer/test/fixtures/base_mailer/implicit_with_locale.de-AT.text.erb
new file mode 100644
index 0000000000000..e97505fad9bad
--- /dev/null
+++ b/actionmailer/test/fixtures/base_mailer/implicit_with_locale.de-AT.text.erb
@@ -0,0 +1 @@
+Implicit with locale DE-AT TEXT
\ No newline at end of file
diff --git a/actionmailer/test/fixtures/base_mailer/implicit_with_locale.de.html.erb b/actionmailer/test/fixtures/base_mailer/implicit_with_locale.de.html.erb
new file mode 100644
index 0000000000000..0536b5d3e2b9f
--- /dev/null
+++ b/actionmailer/test/fixtures/base_mailer/implicit_with_locale.de.html.erb
@@ -0,0 +1 @@
+Implicit with locale DE HTML
\ No newline at end of file
diff --git a/actionmailer/test/fixtures/base_mailer/inline_and_other_attachments.html.erb b/actionmailer/test/fixtures/base_mailer/inline_and_other_attachments.html.erb
new file mode 100644
index 0000000000000..e20087812748f
--- /dev/null
+++ b/actionmailer/test/fixtures/base_mailer/inline_and_other_attachments.html.erb
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/actionmailer/test/fixtures/base_mailer/inline_and_other_attachments.text.erb b/actionmailer/test/fixtures/base_mailer/inline_and_other_attachments.text.erb
new file mode 100644
index 0000000000000..e161d244d27eb
--- /dev/null
+++ b/actionmailer/test/fixtures/base_mailer/inline_and_other_attachments.text.erb
@@ -0,0 +1,4 @@
+Inline Image
+
+No image for you
+
diff --git a/railties/guides/code/getting_started/app/mailers/.gitkeep b/actionmailer/test/fixtures/base_mailer/welcome_with_headers.html.erb
similarity index 100%
rename from railties/guides/code/getting_started/app/mailers/.gitkeep
rename to actionmailer/test/fixtures/base_mailer/welcome_with_headers.html.erb
diff --git a/actionmailer/test/fixtures/base_mailer/without_mail_call.erb b/actionmailer/test/fixtures/base_mailer/without_mail_call.erb
new file mode 100644
index 0000000000000..290379d5fb454
--- /dev/null
+++ b/actionmailer/test/fixtures/base_mailer/without_mail_call.erb
@@ -0,0 +1 @@
+<% raise 'the template should not be rendered' %>
\ No newline at end of file
diff --git a/railties/guides/code/getting_started/app/models/.gitkeep b/actionmailer/test/fixtures/base_test/after_action_mailer/welcome.html.erb
similarity index 100%
rename from railties/guides/code/getting_started/app/models/.gitkeep
rename to actionmailer/test/fixtures/base_test/after_action_mailer/welcome.html.erb
diff --git a/railties/guides/code/getting_started/lib/assets/.gitkeep b/actionmailer/test/fixtures/base_test/before_action_mailer/welcome.html.erb
similarity index 100%
rename from railties/guides/code/getting_started/lib/assets/.gitkeep
rename to actionmailer/test/fixtures/base_test/before_action_mailer/welcome.html.erb
diff --git a/railties/guides/code/getting_started/lib/tasks/.gitkeep b/actionmailer/test/fixtures/base_test/default_inline_attachment_mailer/welcome.html.erb
similarity index 100%
rename from railties/guides/code/getting_started/lib/tasks/.gitkeep
rename to actionmailer/test/fixtures/base_test/default_inline_attachment_mailer/welcome.html.erb
diff --git a/actionmailer/test/fixtures/base_test/late_inline_attachment_mailer/on_render.erb b/actionmailer/test/fixtures/base_test/late_inline_attachment_mailer/on_render.erb
new file mode 100644
index 0000000000000..6decd3bb311cd
--- /dev/null
+++ b/actionmailer/test/fixtures/base_test/late_inline_attachment_mailer/on_render.erb
@@ -0,0 +1,7 @@
+
Adding an inline image while rendering
+
+<% controller.attachments.inline["controller_attachments.jpg"] = 'via controller.attachments.inline' %>
+<%= image_tag attachments['controller_attachments.jpg'].url %>
+
+<% attachments.inline["attachments.jpg"] = 'via attachments.inline' %>
+<%= image_tag attachments['attachments.jpg'].url %>
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 0000000000000..8e965f52b4712
--- /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 0000000000000..90189627daf2a
--- /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 0000000000000..2957d083e8492
--- /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 0000000000000..0541ac321b7a1
--- /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 0000000000000..0d26baa2d7440
--- /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 0000000000000..ef97326e03eff
--- /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 0000000000000..0d52429a81151
--- /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/fixtures/first_mailer/share.erb b/actionmailer/test/fixtures/first_mailer/share.erb
deleted file mode 100644
index da43638ceb4ed..0000000000000
--- a/actionmailer/test/fixtures/first_mailer/share.erb
+++ /dev/null
@@ -1 +0,0 @@
-first mail
diff --git a/actionmailer/test/fixtures/form_builder_mailer/welcome.html.erb b/actionmailer/test/fixtures/form_builder_mailer/welcome.html.erb
new file mode 100644
index 0000000000000..180a827089c00
--- /dev/null
+++ b/actionmailer/test/fixtures/form_builder_mailer/welcome.html.erb
@@ -0,0 +1,3 @@
+<%= form_with(url: "/") do |f| %>
+ <%= f.message %>
+<% end %>
diff --git a/railties/guides/code/getting_started/test/fixtures/.gitkeep b/actionmailer/test/fixtures/mail_delivery_test/delivery_mailer/welcome.html.erb
similarity index 100%
rename from railties/guides/code/getting_started/test/fixtures/.gitkeep
rename to actionmailer/test/fixtures/mail_delivery_test/delivery_mailer/welcome.html.erb
diff --git a/actionmailer/test/fixtures/path.with.dots/funky_path_mailer/multipart_with_template_path_with_dots.erb b/actionmailer/test/fixtures/path.with.dots/funky_path_mailer/multipart_with_template_path_with_dots.erb
deleted file mode 100644
index 2d0cd5c124c75..0000000000000
--- a/actionmailer/test/fixtures/path.with.dots/funky_path_mailer/multipart_with_template_path_with_dots.erb
+++ /dev/null
@@ -1 +0,0 @@
-Have some dots. Enjoy!
\ No newline at end of file
diff --git a/railties/guides/code/getting_started/test/functional/.gitkeep b/actionmailer/test/fixtures/proc_mailer/welcome.html.erb
similarity index 100%
rename from railties/guides/code/getting_started/test/functional/.gitkeep
rename to actionmailer/test/fixtures/proc_mailer/welcome.html.erb
diff --git a/actionmailer/test/fixtures/raw_email b/actionmailer/test/fixtures/raw_email
deleted file mode 100644
index 43f7a59cee03f..0000000000000
--- a/actionmailer/test/fixtures/raw_email
+++ /dev/null
@@ -1,14 +0,0 @@
-From jamis_buck@byu.edu Mon May 2 16:07:05 2005
-Mime-Version: 1.0 (Apple Message framework v622)
-Content-Transfer-Encoding: base64
-Message-Id:
-Content-Type: text/plain;
- charset=EUC-KR;
- format=flowed
-To: willard15georgina@jamis.backpackit.com
-From: Jamis Buck
-Subject: =?EUC-KR?Q?NOTE:_=C7=D1=B1=B9=B8=BB=B7=CE_=C7=CF=B4=C2_=B0=CD?=
-Date: Mon, 2 May 2005 16:07:05 -0600
-
-tOu6zrrQwMcguLbC+bChwfa3ziwgv+y4rrTCIMfPs6q01MC7ILnPvcC0z7TZLg0KDQrBpiDAzLin
-wLogSmFtaXPA1LTPtNku
diff --git a/actionmailer/test/fixtures/raw_email10 b/actionmailer/test/fixtures/raw_email10
deleted file mode 100644
index edad5ccff1ca8..0000000000000
--- a/actionmailer/test/fixtures/raw_email10
+++ /dev/null
@@ -1,20 +0,0 @@
-Return-Path:
-Received: from xxx.xxxx.xxx by xxx.xxxx.xxx with ESMTP id C1B953B4CB6 for ; Tue, 10 May 2005 15:27:05 -0500
-Received: from SMS-GTYxxx.xxxx.xxx by xxx.xxxx.xxx with ESMTP id ca for ; Tue, 10 May 2005 15:27:04 -0500
-Received: from xxx.xxxx.xxx by SMS-GTYxxx.xxxx.xxx with ESMTP id j4AKR3r23323 for ; Tue, 10 May 2005 15:27:03 -0500
-Date: Tue, 10 May 2005 15:27:03 -0500
-From: xxx@xxxx.xxx
-Sender: xxx@xxxx.xxx
-To: xxxxxxxxxxx@xxxx.xxxx.xxx
-Message-Id:
-X-Original-To: xxxxxxxxxxx@xxxx.xxxx.xxx
-Delivered-To: xxx@xxxx.xxx
-Importance: normal
-Content-Type: text/plain; charset=X-UNKNOWN
-
-Test test. Hi. Waving. m
-
-----------------------------------------------------------------
-Sent via Bell Mobility's Text Messaging service.
-Envoyé par le service de messagerie texte de Bell Mobilité.
-----------------------------------------------------------------
diff --git a/actionmailer/test/fixtures/raw_email12 b/actionmailer/test/fixtures/raw_email12
deleted file mode 100644
index 2cd31720d3b61..0000000000000
--- a/actionmailer/test/fixtures/raw_email12
+++ /dev/null
@@ -1,32 +0,0 @@
-Mime-Version: 1.0 (Apple Message framework v730)
-Content-Type: multipart/mixed; boundary=Apple-Mail-13-196941151
-Message-Id: <9169D984-4E0B-45EF-82D4-8F5E53AD7012@example.com>
-From: foo@example.com
-Subject: testing
-Date: Mon, 6 Jun 2005 22:21:22 +0200
-To: blah@example.com
-
-
---Apple-Mail-13-196941151
-Content-Transfer-Encoding: quoted-printable
-Content-Type: text/plain;
- charset=ISO-8859-1;
- delsp=yes;
- format=flowed
-
-This is the first part.
-
---Apple-Mail-13-196941151
-Content-Type: image/jpeg
-Content-Transfer-Encoding: base64
-Content-Location: Photo25.jpg
-Content-ID:
-Content-Disposition: inline
-
-jamisSqGSIb3DQEHAqCAMIjamisxCzAJBgUrDgMCGgUAMIAGCSqGSjamisEHAQAAoIIFSjCCBUYw
-ggQujamisQICBD++ukQwDQYJKojamisNAQEFBQAwMTELMAkGA1UEBhMCRjamisAKBgNVBAoTA1RE
-QzEUMBIGjamisxMLVERDIE9DRVMgQ0jamisNMDQwMjI5MTE1OTAxWhcNMDYwMjamisIyOTAxWjCB
-gDELMAkGA1UEjamisEsxKTAnBgNVBAoTIEjamisuIG9yZ2FuaXNhdG9yaXNrIHRpbjamisRuaW5=
-
---Apple-Mail-13-196941151--
-
diff --git a/actionmailer/test/fixtures/raw_email13 b/actionmailer/test/fixtures/raw_email13
deleted file mode 100644
index 7d9314e36a75d..0000000000000
--- a/actionmailer/test/fixtures/raw_email13
+++ /dev/null
@@ -1,29 +0,0 @@
-Mime-Version: 1.0 (Apple Message framework v730)
-Content-Type: multipart/mixed; boundary=Apple-Mail-13-196941151
-Message-Id: <9169D984-4E0B-45EF-82D4-8F5E53AD7012@example.com>
-From: foo@example.com
-Subject: testing
-Date: Mon, 6 Jun 2005 22:21:22 +0200
-To: blah@example.com
-
-
---Apple-Mail-13-196941151
-Content-Transfer-Encoding: quoted-printable
-Content-Type: text/plain;
- charset=ISO-8859-1;
- delsp=yes;
- format=flowed
-
-This is the first part.
-
---Apple-Mail-13-196941151
-Content-Type: text/x-ruby-script; name="hello.rb"
-Content-Transfer-Encoding: 7bit
-Content-Disposition: attachment;
- filename="api.rb"
-
-puts "Hello, world!"
-gets
-
---Apple-Mail-13-196941151--
-
diff --git a/actionmailer/test/fixtures/raw_email2 b/actionmailer/test/fixtures/raw_email2
deleted file mode 100644
index 9f87bb2a98cf1..0000000000000
--- a/actionmailer/test/fixtures/raw_email2
+++ /dev/null
@@ -1,114 +0,0 @@
-From xxxxxxxxx.xxxxxxx@gmail.com Sun May 8 19:07:09 2005
-Return-Path:
-X-Original-To: xxxxx@xxxxx.xxxxxxxxx.com
-Delivered-To: xxxxx@xxxxx.xxxxxxxxx.com
-Received: from localhost (localhost [127.0.0.1])
- by xxxxx.xxxxxxxxx.com (Postfix) with ESMTP id 06C9DA98D
- for ; Sun, 8 May 2005 19:09:13 +0000 (GMT)
-Received: from xxxxx.xxxxxxxxx.com ([127.0.0.1])
- by localhost (xxxxx.xxxxxxxxx.com [127.0.0.1]) (amavisd-new, port 10024)
- with LMTP id 88783-08 for ;
- Sun, 8 May 2005 19:09:12 +0000 (GMT)
-Received: from xxxxxxx.xxxxxxxxx.com (xxxxxxx.xxxxxxxxx.com [69.36.39.150])
- by xxxxx.xxxxxxxxx.com (Postfix) with ESMTP id 10D8BA960
- for ; Sun, 8 May 2005 19:09:12 +0000 (GMT)
-Received: from zproxy.gmail.com (zproxy.gmail.com [64.233.162.199])
- by xxxxxxx.xxxxxxxxx.com (Postfix) with ESMTP id 9EBC4148EAB
- for ; Sun, 8 May 2005 14:09:11 -0500 (CDT)
-Received: by zproxy.gmail.com with SMTP id 13so1233405nzp
- for ; Sun, 08 May 2005 12:09:11 -0700 (PDT)
-DomainKey-Signature: a=rsa-sha1; q=dns; c=nofws;
- s=beta; d=gmail.com;
- h=received:message-id:date:from:reply-to:to:subject:in-reply-to:mime-version:content-type:references;
- b=cid1mzGEFa3gtRa06oSrrEYfKca2CTKu9sLMkWxjbvCsWMtp9RGEILjUz0L5RySdH5iO661LyNUoHRFQIa57bylAbXM3g2DTEIIKmuASDG3x3rIQ4sHAKpNxP7Pul+mgTaOKBv+spcH7af++QEJ36gHFXD2O/kx9RePs3JNf/K8=
-Received: by 10.36.10.16 with SMTP id 16mr1012493nzj;
- Sun, 08 May 2005 12:09:11 -0700 (PDT)
-Received: by 10.36.5.10 with HTTP; Sun, 8 May 2005 12:09:11 -0700 (PDT)
-Message-ID:
-Date: Sun, 8 May 2005 14:09:11 -0500
-From: xxxxxxxxx xxxxxxx
-Reply-To: xxxxxxxxx xxxxxxx
-To: xxxxx xxxx
-Subject: Fwd: Signed email causes file attachments
-In-Reply-To:
-Mime-Version: 1.0
-Content-Type: multipart/mixed;
- boundary="----=_Part_5028_7368284.1115579351471"
-References:
-
-------=_Part_5028_7368284.1115579351471
-Content-Type: text/plain; charset=ISO-8859-1
-Content-Transfer-Encoding: quoted-printable
-Content-Disposition: inline
-
-We should not include these files or vcards as attachments.
-
----------- Forwarded message ----------
-From: xxxxx xxxxxx
-Date: May 8, 2005 1:17 PM
-Subject: Signed email causes file attachments
-To: xxxxxxx@xxxxxxxxxx.com
-
-
-Hi,
-
-Just started to use my xxxxxxxx account (to set-up a GTD system,
-natch) and noticed that when I send content via email the signature/
-certificate from my email account gets added as a file (e.g.
-"smime.p7s").
-
-Obviously I can uncheck the signature option in the Mail compose
-window but how often will I remember to do that?
-
-Is there any way these kind of files could be ignored, e.g. via some
-sort of exclusions list?
-
-------=_Part_5028_7368284.1115579351471
-Content-Type: application/pkcs7-signature; name=smime.p7s
-Content-Transfer-Encoding: base64
-Content-Disposition: attachment; filename="smime.p7s"
-
-MIAGCSqGSIb3DQEHAqCAMIACAQExCzAJBgUrDgMCGgUAMIAGCSqGSIb3DQEHAQAAoIIGFDCCAs0w
-ggI2oAMCAQICAw5c+TANBgkqhkiG9w0BAQQFADBiMQswCQYDVQQGEwJaQTElMCMGA1UEChMcVGhh
-d3RlIENvbnN1bHRpbmcgKFB0eSkgTHRkLjEsMCoGA1UEAxMjVGhhd3RlIFBlcnNvbmFsIEZyZWVt
-YWlsIElzc3VpbmcgQ0EwHhcNMDUwMzI5MDkzOTEwWhcNMDYwMzI5MDkzOTEwWjBCMR8wHQYDVQQD
-ExZUaGF3dGUgRnJlZW1haWwgTWVtYmVyMR8wHQYJKoZIhvcNAQkBFhBzbWhhdW5jaEBtYWMuY29t
-MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAn90dPsYS3LjfMY211OSYrDQLzwNYPlAL
-7+/0XA+kdy8/rRnyEHFGwhNCDmg0B6pxC7z3xxJD/8GfCd+IYUUNUQV5m9MkxfP9pTVXZVIYLaBw
-o8xS3A0a1LXealcmlEbJibmKkEaoXci3MhryLgpaa+Kk/sH02SNatDO1vS28bPsibZpcc6deFrla
-hSYnL+PW54mDTGHIcCN2fbx/Y6qspzqmtKaXrv75NBtuy9cB6KzU4j2xXbTkAwz3pRSghJJaAwdp
-+yIivAD3vr0kJE3p+Ez34HMh33EXEpFoWcN+MCEQZD9WnmFViMrvfvMXLGVFQfAAcC060eGFSRJ1
-ZQ9UVQIDAQABoy0wKzAbBgNVHREEFDASgRBzbWhhdW5jaEBtYWMuY29tMAwGA1UdEwEB/wQCMAAw
-DQYJKoZIhvcNAQEEBQADgYEAQMrg1n2pXVWteP7BBj+Pk3UfYtbuHb42uHcLJjfjnRlH7AxnSwrd
-L3HED205w3Cq8T7tzVxIjRRLO/ljq0GedSCFBky7eYo1PrXhztGHCTSBhsiWdiyLWxKlOxGAwJc/
-lMMnwqLOdrQcoF/YgbjeaUFOQbUh94w9VDNpWZYCZwcwggM/MIICqKADAgECAgENMA0GCSqGSIb3
-DQEBBQUAMIHRMQswCQYDVQQGEwJaQTEVMBMGA1UECBMMV2VzdGVybiBDYXBlMRIwEAYDVQQHEwlD
-YXBlIFRvd24xGjAYBgNVBAoTEVRoYXd0ZSBDb25zdWx0aW5nMSgwJgYDVQQLEx9DZXJ0aWZpY2F0
-aW9uIFNlcnZpY2VzIERpdmlzaW9uMSQwIgYDVQQDExtUaGF3dGUgUGVyc29uYWwgRnJlZW1haWwg
-Q0ExKzApBgkqhkiG9w0BCQEWHHBlcnNvbmFsLWZyZWVtYWlsQHRoYXd0ZS5jb20wHhcNMDMwNzE3
-MDAwMDAwWhcNMTMwNzE2MjM1OTU5WjBiMQswCQYDVQQGEwJaQTElMCMGA1UEChMcVGhhd3RlIENv
-bnN1bHRpbmcgKFB0eSkgTHRkLjEsMCoGA1UEAxMjVGhhd3RlIFBlcnNvbmFsIEZyZWVtYWlsIElz
-c3VpbmcgQ0EwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAMSmPFVzVftOucqZWh5owHUEcJ3f
-6f+jHuy9zfVb8hp2vX8MOmHyv1HOAdTlUAow1wJjWiyJFXCO3cnwK4Vaqj9xVsuvPAsH5/EfkTYk
-KhPPK9Xzgnc9A74r/rsYPge/QIACZNenprufZdHFKlSFD0gEf6e20TxhBEAeZBlyYLf7AgMBAAGj
-gZQwgZEwEgYDVR0TAQH/BAgwBgEB/wIBADBDBgNVHR8EPDA6MDigNqA0hjJodHRwOi8vY3JsLnRo
-YXd0ZS5jb20vVGhhd3RlUGVyc29uYWxGcmVlbWFpbENBLmNybDALBgNVHQ8EBAMCAQYwKQYDVR0R
-BCIwIKQeMBwxGjAYBgNVBAMTEVByaXZhdGVMYWJlbDItMTM4MA0GCSqGSIb3DQEBBQUAA4GBAEiM
-0VCD6gsuzA2jZqxnD3+vrL7CF6FDlpSdf0whuPg2H6otnzYvwPQcUCCTcDz9reFhYsPZOhl+hLGZ
-GwDFGguCdJ4lUJRix9sncVcljd2pnDmOjCBPZV+V2vf3h9bGCE6u9uo05RAaWzVNd+NWIXiC3CEZ
-Nd4ksdMdRv9dX2VPMYIC5zCCAuMCAQEwaTBiMQswCQYDVQQGEwJaQTElMCMGA1UEChMcVGhhd3Rl
-IENvbnN1bHRpbmcgKFB0eSkgTHRkLjEsMCoGA1UEAxMjVGhhd3RlIFBlcnNvbmFsIEZyZWVtYWls
-IElzc3VpbmcgQ0ECAw5c+TAJBgUrDgMCGgUAoIIBUzAYBgkqhkiG9w0BCQMxCwYJKoZIhvcNAQcB
-MBwGCSqGSIb3DQEJBTEPFw0wNTA1MDgxODE3NDZaMCMGCSqGSIb3DQEJBDEWBBQSkG9j6+hB0pKp
-fV9tCi/iP59sNTB4BgkrBgEEAYI3EAQxazBpMGIxCzAJBgNVBAYTAlpBMSUwIwYDVQQKExxUaGF3
-dGUgQ29uc3VsdGluZyAoUHR5KSBMdGQuMSwwKgYDVQQDEyNUaGF3dGUgUGVyc29uYWwgRnJlZW1h
-aWwgSXNzdWluZyBDQQIDDlz5MHoGCyqGSIb3DQEJEAILMWugaTBiMQswCQYDVQQGEwJaQTElMCMG
-A1UEChMcVGhhd3RlIENvbnN1bHRpbmcgKFB0eSkgTHRkLjEsMCoGA1UEAxMjVGhhd3RlIFBlcnNv
-bmFsIEZyZWVtYWlsIElzc3VpbmcgQ0ECAw5c+TANBgkqhkiG9w0BAQEFAASCAQAm1GeF7dWfMvrW
-8yMPjkhE+R8D1DsiCoWSCp+5gAQm7lcK7V3KrZh5howfpI3TmCZUbbaMxOH+7aKRKpFemxoBY5Q8
-rnCkbpg/++/+MI01T69hF/rgMmrGcrv2fIYy8EaARLG0xUVFSZHSP+NQSYz0TTmh4cAESHMzY3JA
-nHOoUkuPyl8RXrimY1zn0lceMXlweZRouiPGuPNl1hQKw8P+GhOC5oLlM71UtStnrlk3P9gqX5v7
-Tj7Hx057oVfY8FMevjxGwU3EK5TczHezHbWWgTyum9l2ZQbUQsDJxSniD3BM46C1VcbDLPaotAZ0
-fTYLZizQfm5hcWEbfYVzkSzLAAAAAAAA
-------=_Part_5028_7368284.1115579351471--
-
diff --git a/actionmailer/test/fixtures/raw_email3 b/actionmailer/test/fixtures/raw_email3
deleted file mode 100644
index 3a0927490ae1f..0000000000000
--- a/actionmailer/test/fixtures/raw_email3
+++ /dev/null
@@ -1,70 +0,0 @@
-From xxxx@xxxx.com Tue May 10 11:28:07 2005
-Return-Path:
-X-Original-To: xxxx@xxxx.com
-Delivered-To: xxxx@xxxx.com
-Received: from localhost (localhost [127.0.0.1])
- by xxx.xxxxx.com (Postfix) with ESMTP id 50FD3A96F
- for ; Tue, 10 May 2005 17:26:50 +0000 (GMT)
-Received: from xxx.xxxxx.com ([127.0.0.1])
- by localhost (xxx.xxxxx.com [127.0.0.1]) (amavisd-new, port 10024)
- with LMTP id 70060-03 for ;
- Tue, 10 May 2005 17:26:49 +0000 (GMT)
-Received: from xxx.xxxxx.com (xxx.xxxxx.com [69.36.39.150])
- by xxx.xxxxx.com (Postfix) with ESMTP id 8B957A94B
- for ; Tue, 10 May 2005 17:26:48 +0000 (GMT)
-Received: from xxx.xxxxx.com (xxx.xxxxx.com [64.233.184.203])
- by xxx.xxxxx.com (Postfix) with ESMTP id 9972514824C
- for ; Tue, 10 May 2005 12:26:40 -0500 (CDT)
-Received: by xxx.xxxxx.com with SMTP id 68so1694448wri
- for ; Tue, 10 May 2005 10:26:40 -0700 (PDT)
-DomainKey-Signature: a=rsa-sha1; q=dns; c=nofws;
- s=beta; d=xxxxx.com;
- h=received:message-id:date:from:reply-to:to:subject:mime-version:content-type;
- b=g8ZO5ttS6GPEMAz9WxrRk9+9IXBUfQIYsZLL6T88+ECbsXqGIgfGtzJJFn6o9CE3/HMrrIGkN5AisxVFTGXWxWci5YA/7PTVWwPOhJff5BRYQDVNgRKqMl/SMttNrrRElsGJjnD1UyQ/5kQmcBxq2PuZI5Zc47u6CILcuoBcM+A=
-Received: by 10.54.96.19 with SMTP id t19mr621017wrb;
- Tue, 10 May 2005 10:26:39 -0700 (PDT)
-Received: by 10.54.110.5 with HTTP; Tue, 10 May 2005 10:26:39 -0700 (PDT)
-Message-ID:
-Date: Tue, 10 May 2005 11:26:39 -0600
-From: Test Tester
-Reply-To: Test Tester
-To: xxxx@xxxx.com, xxxx@xxxx.com
-Subject: Another PDF
-Mime-Version: 1.0
-Content-Type: multipart/mixed;
- boundary="----=_Part_2192_32400445.1115745999735"
-X-Virus-Scanned: amavisd-new at textdrive.com
-
-------=_Part_2192_32400445.1115745999735
-Content-Type: text/plain; charset=ISO-8859-1
-Content-Transfer-Encoding: quoted-printable
-Content-Disposition: inline
-
-Just attaching another PDF, here, to see what the message looks like,
-and to see if I can figure out what is going wrong here.
-
-------=_Part_2192_32400445.1115745999735
-Content-Type: application/pdf; name="broken.pdf"
-Content-Transfer-Encoding: base64
-Content-Disposition: attachment; filename="broken.pdf"
-
-JVBERi0xLjQNCiXk9tzfDQoxIDAgb2JqDQo8PCAvTGVuZ3RoIDIgMCBSDQogICAvRmlsdGVyIC9G
-bGF0ZURlY29kZQ0KPj4NCnN0cmVhbQ0KeJy9Wt2KJbkNvm/od6jrhZxYln9hWEh2p+8HBvICySaE
-ycLuTV4/1ifJ9qnq09NpSBimu76yLUuy/qzqcPz7+em3Ixx/CDc6CsXxs3b5+fvfjr/8cPz6/BRu
-rbfAx/n3739/fuJylJ5u5fjX81OuDr4deK4Bz3z/aDP+8fz0yw8g0Ofq7ktr1Mn+u28rvhy/jVeD
-QSa+9YNKHP/pxjvDNfVAx/m3MFz54FhvTbaseaxiDoN2LeMVMw+yA7RbHSCDzxZuaYB2E1Yay7QU
-x89vz0+tyFDKMlAHK5yqLmnjF+c4RjEiQIUeKwblXMe+AsZjN1J5yGQL5DHpDHksurM81rF6PKab
-gK6zAarIDzIiUY23rJsN9iorAE816aIu6lsgAdQFsuhhkHOUFgVjp2GjMqSewITXNQ27jrMeamkg
-1rPI3iLWG2CIaSBB+V1245YVRICGbbpYKHc2USFDl6M09acQVQYhlwIrkBNLISvXhGlF1wi5FHCw
-wxZkoGNJlVeJCEsqKA+3YAV5AMb6KkeaqEJQmFKKQU8T1pRi2ihE1Y4CDrqoYFFXYjJJOatsyzuI
-8SIlykuxKTMibWK8H1PgEvqYgs4GmQSrEjJAalgGirIhik+p4ZQN9E3ETFPAHE1b8pp1l/0Rc1gl
-fQs0ABWvyoZZzU8VnPXwVVcO9BEsyjEJaO6eBoZRyKGlrKoYoOygA8BGIzgwN3RQ15ouigG5idZQ
-fx2U4Db2CqiLO0WHAZoylGiCAqhniNQjFjQPSkmjwfNTgQ6M1Ih+eWo36wFmjIxDJZiGUBiWsAyR
-xX3EekGOizkGI96Ol9zVZTAivikURhRsHh2E3JhWMpSTZCnnonrLhMCodgrNcgo4uyJUJc6qnVss
-nrGd1Ptr0YwisCOYyIbUwVjV4xBUNLbguSO2YHujonAMJkMdSI7bIw91Akq2AUlMUWGFTMAOamjU
-OvZQCxIkY2pCpMFo/IwLdVLHs6nddwTRrgoVbvLU9eB0G4EMndV0TNoxHbt3JBWwK6hhv3iHfDtF
-yokB302IpEBTnWICde4uYc/1khDbSIkQopO6lcqamGBu1OSE3N5IPSsZX00CkSHRiiyx6HQIShsS
-HSVNswdVsaOUSAWq9aYhDtGDaoG5a3lBGkYt/lFlBFt1UqrYnzVtUpUQnLiZeouKgf1KhRBViRRk
-ExepJCzTwEmFDalIRbLEGtw0gfpESOpIAF/NnpPzcVCG86s0g2DuSyd41uhNGbEgaSrWEXORErbw
-------=_Part_2192_32400445.1115745999735--
-
diff --git a/actionmailer/test/fixtures/raw_email4 b/actionmailer/test/fixtures/raw_email4
deleted file mode 100644
index 639ad40e495f0..0000000000000
--- a/actionmailer/test/fixtures/raw_email4
+++ /dev/null
@@ -1,59 +0,0 @@
-Return-Path:
-Received: from xxx.xxxx.xxx by xxx.xxxx.xxx with ESMTP id 6AAEE3B4D23 for ; Sun, 8 May 2005 12:30:23 -0500
-Received: from xxx.xxxx.xxx by xxx.xxxx.xxx with ESMTP id j48HUC213279 for ; Sun, 8 May 2005 12:30:13 -0500
-Received: from conversion-xxx.xxxx.xxx.net by xxx.xxxx.xxx id <0IG600901LQ64I@xxx.xxxx.xxx> for ; Sun, 8 May 2005 12:30:12 -0500
-Received: from agw1 by xxx.xxxx.xxx with ESMTP id <0IG600JFYLYCAxxx@xxxx.xxx> for ; Sun, 8 May 2005 12:30:12 -0500
-Date: Sun, 8 May 2005 12:30:08 -0500
-From: xxx@xxxx.xxx
-To: xxx@xxxx.xxx
-Message-Id: <7864245.1115573412626.JavaMxxx@xxxx.xxx>
-Subject: Filth
-Mime-Version: 1.0
-Content-Type: multipart/mixed; boundary=mimepart_427e4cb4ca329_133ae40413c81ef
-X-Mms-Priority: 1
-X-Mms-Transaction-Id: 3198421808-0
-X-Mms-Message-Type: 0
-X-Mms-Sender-Visibility: 1
-X-Mms-Read-Reply: 1
-X-Original-To: xxx@xxxx.xxx
-X-Mms-Message-Class: 0
-X-Mms-Delivery-Report: 0
-X-Mms-Mms-Version: 16
-Delivered-To: xxx@xxxx.xxx
-X-Nokia-Ag-Version: 2.0
-
-This is a multi-part message in MIME format.
-
---mimepart_427e4cb4ca329_133ae40413c81ef
-Content-Type: multipart/mixed; boundary=mimepart_427e4cb4cbd97_133ae40413c8217
-
-
-
---mimepart_427e4cb4cbd97_133ae40413c8217
-Content-Type: text/plain; charset=utf-8
-Content-Transfer-Encoding: 7bit
-Content-Disposition: inline
-Content-Location: text.txt
-
-Some text
-
---mimepart_427e4cb4cbd97_133ae40413c8217--
-
---mimepart_427e4cb4ca329_133ae40413c81ef
-Content-Type: text/plain; charset=us-ascii
-Content-Transfer-Encoding: 7bit
-
-
---
-This Orange Multi Media Message was sent wirefree from an Orange
-MMS phone. If you would like to reply, please text or phone the
-sender directly by using the phone number listed in the sender's
-address. To learn more about Orange's Multi Media Messaging
-Service, find us on the Web at xxx.xxxx.xxx.uk/mms
-
-
---mimepart_427e4cb4ca329_133ae40413c81ef
-
-
---mimepart_427e4cb4ca329_133ae40413c81ef-
-
diff --git a/actionmailer/test/fixtures/raw_email5 b/actionmailer/test/fixtures/raw_email5
deleted file mode 100644
index bbe31bcdc5ef7..0000000000000
--- a/actionmailer/test/fixtures/raw_email5
+++ /dev/null
@@ -1,19 +0,0 @@
-Return-Path:
-Received: from xxx.xxxx.xxx by xxx.xxxx.xxx with ESMTP id C1B953B4CB6 for ; Tue, 10 May 2005 15:27:05 -0500
-Received: from SMS-GTYxxx.xxxx.xxx by xxx.xxxx.xxx with ESMTP id ca for ; Tue, 10 May 2005 15:27:04 -0500
-Received: from xxx.xxxx.xxx by SMS-GTYxxx.xxxx.xxx with ESMTP id j4AKR3r23323 for ; Tue, 10 May 2005 15:27:03 -0500
-Date: Tue, 10 May 2005 15:27:03 -0500
-From: xxx@xxxx.xxx
-Sender: xxx@xxxx.xxx
-To: xxxxxxxxxxx@xxxx.xxxx.xxx
-Message-Id:
-X-Original-To: xxxxxxxxxxx@xxxx.xxxx.xxx
-Delivered-To: xxx@xxxx.xxx
-Importance: normal
-
-Test test. Hi. Waving. m
-
-----------------------------------------------------------------
-Sent via Bell Mobility's Text Messaging service.
-Envoyé par le service de messagerie texte de Bell Mobilité.
-----------------------------------------------------------------
diff --git a/actionmailer/test/fixtures/raw_email6 b/actionmailer/test/fixtures/raw_email6
deleted file mode 100644
index 8e37bd73921a5..0000000000000
--- a/actionmailer/test/fixtures/raw_email6
+++ /dev/null
@@ -1,20 +0,0 @@
-Return-Path:
-Received: from xxx.xxxx.xxx by xxx.xxxx.xxx with ESMTP id C1B953B4CB6 for ; Tue, 10 May 2005 15:27:05 -0500
-Received: from SMS-GTYxxx.xxxx.xxx by xxx.xxxx.xxx with ESMTP id ca for ; Tue, 10 May 2005 15:27:04 -0500
-Received: from xxx.xxxx.xxx by SMS-GTYxxx.xxxx.xxx with ESMTP id j4AKR3r23323 for ; Tue, 10 May 2005 15:27:03 -0500
-Date: Tue, 10 May 2005 15:27:03 -0500
-From: xxx@xxxx.xxx
-Sender: xxx@xxxx.xxx
-To: xxxxxxxxxxx@xxxx.xxxx.xxx
-Message-Id:
-X-Original-To: xxxxxxxxxxx@xxxx.xxxx.xxx
-Delivered-To: xxx@xxxx.xxx
-Importance: normal
-Content-Type: text/plain; charset=us-ascii
-
-Test test. Hi. Waving. m
-
-----------------------------------------------------------------
-Sent via Bell Mobility's Text Messaging service.
-Envoyé par le service de messagerie texte de Bell Mobilité.
-----------------------------------------------------------------
diff --git a/actionmailer/test/fixtures/raw_email7 b/actionmailer/test/fixtures/raw_email7
deleted file mode 100644
index da64ada8a50cf..0000000000000
--- a/actionmailer/test/fixtures/raw_email7
+++ /dev/null
@@ -1,66 +0,0 @@
-Mime-Version: 1.0 (Apple Message framework v730)
-Content-Type: multipart/mixed; boundary=Apple-Mail-13-196941151
-Message-Id: <9169D984-4E0B-45EF-82D4-8F5E53AD7012@example.com>
-From: foo@example.com
-Subject: testing
-Date: Mon, 6 Jun 2005 22:21:22 +0200
-To: blah@example.com
-
-
---Apple-Mail-13-196941151
-Content-Type: multipart/mixed;
- boundary=Apple-Mail-12-196940926
-
-
---Apple-Mail-12-196940926
-Content-Transfer-Encoding: quoted-printable
-Content-Type: text/plain;
- charset=ISO-8859-1;
- delsp=yes;
- format=flowed
-
-This is the first part.
-
---Apple-Mail-12-196940926
-Content-Transfer-Encoding: 7bit
-Content-Type: text/x-ruby-script;
- x-unix-mode=0666;
- name="test.rb"
-Content-Disposition: attachment;
- filename=test.rb
-
-puts "testing, testing"
-
---Apple-Mail-12-196940926
-Content-Transfer-Encoding: base64
-Content-Type: application/pdf;
- x-unix-mode=0666;
- name="test.pdf"
-Content-Disposition: inline;
- filename=test.pdf
-
-YmxhaCBibGFoIGJsYWg=
-
---Apple-Mail-12-196940926
-Content-Transfer-Encoding: 7bit
-Content-Type: text/plain;
- charset=US-ASCII;
- format=flowed
-
-
-
---Apple-Mail-12-196940926--
-
---Apple-Mail-13-196941151
-Content-Transfer-Encoding: base64
-Content-Type: application/pkcs7-signature;
- name=smime.p7s
-Content-Disposition: attachment;
- filename=smime.p7s
-
-jamisSqGSIb3DQEHAqCAMIjamisxCzAJBgUrDgMCGgUAMIAGCSqGSjamisEHAQAAoIIFSjCCBUYw
-ggQujamisQICBD++ukQwDQYJKojamisNAQEFBQAwMTELMAkGA1UEBhMCRjamisAKBgNVBAoTA1RE
-QzEUMBIGjamisxMLVERDIE9DRVMgQ0jamisNMDQwMjI5MTE1OTAxWhcNMDYwMjamisIyOTAxWjCB
-gDELMAkGA1UEjamisEsxKTAnBgNVBAoTIEjamisuIG9yZ2FuaXNhdG9yaXNrIHRpbjamisRuaW5=
-
---Apple-Mail-13-196941151--
diff --git a/actionmailer/test/fixtures/raw_email8 b/actionmailer/test/fixtures/raw_email8
deleted file mode 100644
index 79996365b3d0e..0000000000000
--- a/actionmailer/test/fixtures/raw_email8
+++ /dev/null
@@ -1,47 +0,0 @@
-From xxxxxxxxx.xxxxxxx@gmail.com Sun May 8 19:07:09 2005
-Return-Path:
-Message-ID:
-Date: Sun, 8 May 2005 14:09:11 -0500
-From: xxxxxxxxx xxxxxxx
-Reply-To: xxxxxxxxx xxxxxxx
-To: xxxxx xxxx
-Subject: Fwd: Signed email causes file attachments
-In-Reply-To:
-Mime-Version: 1.0
-Content-Type: multipart/mixed;
- boundary="----=_Part_5028_7368284.1115579351471"
-References:
-
-------=_Part_5028_7368284.1115579351471
-Content-Type: text/plain; charset=ISO-8859-1
-Content-Transfer-Encoding: quoted-printable
-Content-Disposition: inline
-
-We should not include these files or vcards as attachments.
-
----------- Forwarded message ----------
-From: xxxxx xxxxxx
-Date: May 8, 2005 1:17 PM
-Subject: Signed email causes file attachments
-To: xxxxxxx@xxxxxxxxxx.com
-
-
-Hi,
-
-Test attachments oddly encoded with japanese charset.
-
-
-------=_Part_5028_7368284.1115579351471
-Content-Type: application/octet-stream; name*=iso-2022-jp'ja'01%20Quien%20Te%20Dij%8aat.%20Pitbull.mp3
-Content-Transfer-Encoding: base64
-Content-Disposition: attachment
-
-MIAGCSqGSIb3DQEHAqCAMIACAQExCzAJBgUrDgMCGgUAMIAGCSqGSIb3DQEHAQAAoIIGFDCCAs0w
-ggI2oAMCAQICAw5c+TANBgkqhkiG9w0BAQQFADBiMQswCQYDVQQGEwJaQTElMCMGA1UEChMcVGhh
-d3RlIENvbnN1bHRpbmcgKFB0eSkgTHRkLjEsMCoGA1UEAxMjVGhhd3RlIFBlcnNvbmFsIEZyZWVt
-YWlsIElzc3VpbmcgQ0EwHhcNMDUwMzI5MDkzOTEwWhcNMDYwMzI5MDkzOTEwWjBCMR8wHQYDVQQD
-ExZUaGF3dGUgRnJlZW1haWwgTWVtYmVyMR8wHQYJKoZIhvcNAQkBFhBzbWhhdW5jaEBtYWMuY29t
-MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAn90dPsYS3LjfMY211OSYrDQLzwNYPlAL
-7+/0XA+kdy8/rRnyEHFGwhNCDmg0B6pxC7z3xxJD/8GfCd+IYUUNUQV5m9MkxfP9pTVXZVIYLaBw
-------=_Part_5028_7368284.1115579351471--
-
diff --git a/actionmailer/test/fixtures/raw_email9 b/actionmailer/test/fixtures/raw_email9
deleted file mode 100644
index 02ea0b05c530a..0000000000000
--- a/actionmailer/test/fixtures/raw_email9
+++ /dev/null
@@ -1,28 +0,0 @@
-Received: from xxx.xxx.xxx ([xxx.xxx.xxx.xxx] verified)
- by xxx.com (CommuniGate Pro SMTP 4.2.8)
- with SMTP id 2532598 for xxx@xxx.com; Wed, 23 Feb 2005 17:51:49 -0500
-Received-SPF: softfail
- receiver=xxx.com; client-ip=xxx.xxx.xxx.xxx; envelope-from=xxx@xxx.xxx
-quite Delivered-To: xxx@xxx.xxx
-Received: by xxx.xxx.xxx (Wostfix, from userid xxx)
- id 0F87F333; Wed, 23 Feb 2005 16:16:17 -0600
-Date: Wed, 23 Feb 2005 18:20:17 -0400
-From: "xxx xxx"
-Message-ID: <4D6AA7EB.6490534@xxx.xxx>
-To: xxx@xxx.com
-Subject: Stop adware/spyware once and for all.
-X-Scanned-By: MIMEDefang 2.11 (www dot roaringpenguin dot com slash mimedefang)
-
-You are infected with:
-Ad Ware and Spy Ware
-
-Get your free scan and removal download now,
-before it gets any worse.
-
-http://xxx.xxx.info?aid=3D13&?stat=3D4327kdzt
-
-
-
-
-no more? (you will still be infected)
-http://xxx.xxx.info/discon/?xxx@xxx.com
diff --git a/actionmailer/test/fixtures/raw_email_quoted_with_0d0a b/actionmailer/test/fixtures/raw_email_quoted_with_0d0a
deleted file mode 100644
index 8a2c25a5ddfcd..0000000000000
--- a/actionmailer/test/fixtures/raw_email_quoted_with_0d0a
+++ /dev/null
@@ -1,14 +0,0 @@
-Mime-Version: 1.0 (Apple Message framework v730)
-Message-Id: <9169D984-4E0B-45EF-82D4-8F5E53AD7012@example.com>
-From: foo@example.com
-Subject: testing
-Date: Mon, 6 Jun 2005 22:21:22 +0200
-To: blah@example.com
-Content-Transfer-Encoding: quoted-printable
-Content-Type: text/plain
-
-A fax has arrived from remote ID ''.=0D=0A-----------------------=
--------------------------------------=0D=0ATime: 3/9/2006 3:50:52=
- PM=0D=0AReceived from remote ID: =0D=0AInbound user ID XXXXXXXXXX, r=
-outing code XXXXXXXXX=0D=0AResult: (0/352;0/0) Successful Send=0D=0AP=
-age record: 1 - 1=0D=0AElapsed time: 00:58 on channel 11=0D=0A
diff --git a/actionmailer/test/fixtures/raw_email_with_invalid_characters_in_content_type b/actionmailer/test/fixtures/raw_email_with_invalid_characters_in_content_type
deleted file mode 100644
index a8ff7ed4cb476..0000000000000
--- a/actionmailer/test/fixtures/raw_email_with_invalid_characters_in_content_type
+++ /dev/null
@@ -1,104 +0,0 @@
-Return-Path:
-Received: from some.isp.com by baci with ESMTP id 632BD5758 for ; Sun, 21 Oct 2007 19:38:21 +1000
-Date: Sun, 21 Oct 2007 19:38:13 +1000
-From: Mikel Lindsaar
-Reply-To: Mikel Lindsaar
-To: mikel.lindsaar@baci
-Message-Id: <009601c813c6$19df3510$0437d30a@mikel091a>
-Subject: Testing outlook
-Mime-Version: 1.0
-Content-Type: multipart/alternative; boundary=----=_NextPart_000_0093_01C81419.EB75E850
-Delivered-To: mikel.lindsaar@baci
-X-Mimeole: Produced By Microsoft MimeOLE V6.00.2900.3138
-X-Msmail-Priority: Normal
-
-This is a multi-part message in MIME format.
-
-
-------=_NextPart_000_0093_01C81419.EB75E850
-Content-Type: text/plain; charset=iso-8859-1
-Content-Transfer-Encoding: Quoted-printable
-
-Hello
-This is an outlook test
-
-So there.
-
-Me.
-
-------=_NextPart_000_0093_01C81419.EB75E850
-Content-Type: text/html; charset=iso-8859-1
-Content-Transfer-Encoding: Quoted-printable
-
-
-
-
-
-
-
-
-
Hello
-
This is an outlook=20
-test
-
-
So there.
-
-
Me.
-
-
-------=_NextPart_000_0093_01C81419.EB75E850--
-
-
-Return-Path:
-Received: from some.isp.com by baci with ESMTP id 632BD5758 for ; Sun, 21 Oct 2007 19:38:21 +1000
-Date: Sun, 21 Oct 2007 19:38:13 +1000
-From: Mikel Lindsaar
-Reply-To: Mikel Lindsaar
-To: mikel.lindsaar@baci
-Message-Id: <009601c813c6$19df3510$0437d30a@mikel091a>
-Subject: Testing outlook
-Mime-Version: 1.0
-Content-Type: multipart/alternative; boundary=----=_NextPart_000_0093_01C81419.EB75E850
-Delivered-To: mikel.lindsaar@baci
-X-Mimeole: Produced By Microsoft MimeOLE V6.00.2900.3138
-X-Msmail-Priority: Normal
-
-This is a multi-part message in MIME format.
-
-
-------=_NextPart_000_0093_01C81419.EB75E850
-Content-Type: text/plain; charset=iso-8859-1
-Content-Transfer-Encoding: Quoted-printable
-
-Hello
-This is an outlook test
-
-So there.
-
-Me.
-
-------=_NextPart_000_0093_01C81419.EB75E850
-Content-Type: text/html; charset=iso-8859-1
-Content-Transfer-Encoding: Quoted-printable
-
-
-
-
-
-
-
-
-
Hello
-
This is an outlook=20
-test
-
-
So there.
-
-
Me.
-
-
-------=_NextPart_000_0093_01C81419.EB75E850--
-
-
diff --git a/actionmailer/test/fixtures/raw_email_with_nested_attachment b/actionmailer/test/fixtures/raw_email_with_nested_attachment
deleted file mode 100644
index 429c408c5d2ef..0000000000000
--- a/actionmailer/test/fixtures/raw_email_with_nested_attachment
+++ /dev/null
@@ -1,100 +0,0 @@
-From jamis@37signals.com Thu Feb 22 11:20:31 2007
-Mime-Version: 1.0 (Apple Message framework v752.3)
-Message-Id: <2CCE0408-10C7-4045-9B16-A1C11C31469B@37signals.com>
-Content-Type: multipart/signed;
- micalg=sha1;
- boundary=Apple-Mail-42-587703407;
- protocol="application/pkcs7-signature"
-To: Jamis Buck
-Subject: Testing attachments
-From: Jamis Buck
-Date: Thu, 22 Feb 2007 11:20:31 -0700
-
-
---Apple-Mail-42-587703407
-Content-Type: multipart/mixed;
- boundary=Apple-Mail-41-587703287
-
-
---Apple-Mail-41-587703287
-Content-Transfer-Encoding: 7bit
-Content-Type: text/plain;
- charset=US-ASCII;
- format=flowed
-
-Here is a test of an attachment via email.
-
-- Jamis
-
-
---Apple-Mail-41-587703287
-Content-Transfer-Encoding: base64
-Content-Type: image/png;
- x-unix-mode=0644;
- name=byo-ror-cover.png
-Content-Disposition: inline;
- filename=truncated.png
-
-iVBORw0KGgoAAAANSUhEUgAAAKUAAADXCAYAAAB7wZEQAAAABHNCSVQICAgIfAhkiAAAAAlwSFlz
-AAALEgAACxIB0t1+/AAAABd0RVh0Q3JlYXRpb24gVGltZQAxLzI1LzIwMDeD9CJVAAAAGHRFWHRT
-b2Z0d2FyZQBBZG9iZSBGaXJld29ya3NPsx9OAAAyBWlUWHRYTUw6Y29tLmFkb2JlLnhtcDw/eHBh
-Y2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+Cjx4OnhtcG1l
-dGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDQuMS1j
-MDIwIDEuMjU1NzE2LCBUdWUgT2N0IDEwIDIwMDYgMjM6MTY6MzQiPgogICA8cmRmOlJERiB4bWxu
-czpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPgogICAg
-ICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgICAgICAgICB4bWxuczp4YXA9Imh0
-dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iPgogICAgICAgICA8eGFwOkNyZWF0b3JUb29sPkFk
-b2JlIEZpcmV3b3JrcyBDUzM8L3hhcDpDcmVhdG9yVG9vbD4KICAgICAgICAgPHhhcDpDcmVhdGVE
-YXRlPjIwMDctMDEtMjVUMDU6Mjg6MjFaPC94YXA6Q3JlYXRlRGF0ZT4KICAgICAgICAgPHhhcDpN
-b2RpZnlEYXRlPjIwMDctMDEtMjVUMDU6Mjg6MjFaPC94YXA6TW9kaWZ5RGF0ZT4KICAgICAgPC9y
-ZGY6RGVzY3JpcHRpb24+CiAgICAgIDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiCiAgICAg
-ICAgICAgIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyI+CiAgICAg
-ICAgIDxkYzpmb3JtYXQ+aW1hZ2UvcG5nPC9kYzpmb3JtYXQ+CiAgICAgIDwvcmRmOkRlc2NyaXB0
-hhojpmnJMfaYFmSkXWg5PGCmHXVj/c9At0hSK2xGdd8F3muk0VFjb4f5Ue0ksQ8qAcq0delaXhdb
-DjKNnF+3B3t9kObZYmk7AZgWYqO9anpR3wpM9sQ5XslB9a+kWyTtNb0fOmudzGHfPFBQDKesyycm
-DBL7Cw5bXjIEuci+SSOm/LYnXDZu6iuPEj8lYBb+OU8xx1f9m+e5rhJiYKqjo5vHfiZp+VUkW9xc
-Ufd6JHNWc47PkQqb9ie3SLEZB/ZqyAssiqURY+G35iOMZUrHbasHnb80QAPv9FHtAbJIyro7bi5b
-ai2TEAKen5+LJNWrglZjm3UbZvt7KryA2J5b5J1jZF8kL6GzvG1Zqx54Y1y7J7n20wMOt9frG2sW
-uwGP07kNz3732vf6bfvAvLldfS+9fts2euXY37D+R29FGZdlnhzV4TTFmPJduBP2RbNNua4rTqcT
-Qt7Xy1KUB0AHSdP5AZQYvHZg7WD1XvYeMO1A9HhZPqMX5KXbMBrn2efxns/ee21674efxz4Tp/fq
-2HZ648dgYaC1i3Vq1IbNPq3PvDTPezY9FaRISjvnzWqdgcWN8EJgjnNq+Z7ktOm9l2Nfth28EZi4
-bG/we5JwxM+Tql47/D/X6b38I8/RyxvxPJrX6zvQbo3h9jyJx+C0ALX327QETHl5eYlaYCT5rPTb
-+5/rAq26t3lKIxV/p88hq6ptngdgCzoPjJqndiLfc/6y5A14WeDFGNPct4iUsJBV2bYzLEV7m83s
-6Rp63VPhHKC/g/LzaU9qexJRr56043JWinqAtfZqsSm1sjoznthl54dtCqv+uL4nIY+oYWuc3+nH
-kGfn8b0HQpvOYLQAZUDanbJs3jQhITZEgdarZK+cO6ySlL13rut5nFaN23s7u3Snz6eRPTkCoc2/
-Vp1zHfZVFpZ87FiMVLV1iqyK5rlzfji2GzjfDsodlD+Weo5UD4h6PwKqzQMqID0tq2VjjFVSMpis
-ZLRAs7sePZBZAHI+gIanB8I7MD+femAceeUe2Kxa5jS950kZ1p5eNEdeX1+jFmSpZ+1EdWCsDcne
-NPNgUHNw3aYpnzv9PGTX0uo94EtN9qq1rOdxe3kc79T8ukeHJJ8Fnxej6qlylbLLsjQLOy6Xy2a1
-kefs/N+nM7+S7IG5/E5Yc7F003pWErLjbH0O5cGadiMptSB/DZ5U5DI9yeg5MFYyMj8lC/Y7/Xjq
-OZlWcnpg9aQfXz2HRq+Wn5xOp6gN8tWq8R44e2pfyzLYemEgprst+XXk2Zj2nXlbsG05BprndTMv
-C3QRaXczshhVsHnMgfYn80Y2g5JureA6wBasPeP7LkE/jvZMJAaf/g/U2RelHsisvan5FqweIAHg
-Pwc7L68GxvVDAAAAAElFTkSuQmCC
-
---Apple-Mail-41-587703287--
-
---Apple-Mail-42-587703407
-Content-Transfer-Encoding: base64
-Content-Type: application/pkcs7-signature;
- name=smime.p7s
-Content-Disposition: attachment;
- filename=smime.p7s
-
-MIAGCSqGSIb3DQEHAqCAMIACAQExCzAJBgUrDgMCGgUAMIAGCSqGSIb3DQEHAQAAoIIGJzCCAuAw
-ggJJoAMCAQICEFjnFNYXwDEZRWY5EkfzopUwDQYJKoZIhvcNAQEFBQAwYjELMAkGA1UEBhMCWkEx
-JTAjBgNVBAoTHFRoYXd0ZSBDb25zdWx0aW5nIChQdHkpIEx0ZC4xLDAqBgNVBAMTI1RoYXd0ZSBQ
-ZXJzb25hbCBGcmVlbWFpbCBJc3N1aW5nIENBMB4XDTA2MDkxMjE3MDExMloXDTA3MDkxMjE3MDEx
-MlowRTEfMB0GA1UEAxMWVGhhd3RlIEZyZWVtYWlsIE1lbWJlcjEiMCAGCSqGSIb3DQEJARYTamFt
-aXNAMzdzaWduYWxzLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAO2A9JeOFIFJ
-G6z8pTcAldrZ2nMe+Xb1tNrbHgoVzN/QhHXM4qst2Ml93cmFLjMmwG7P9RJeU4oNx+jTqVoBB7NV
-Ne1/o56Do0KhfMZ9iUDQdPLbkZMq4EEpFMdm6PyM3muRKwPhj66iAWe/osCb8DowUK2f66vaRx0Z
-Y0MQHIIrXE02Ta4IfAhIfPqBLkZ4WgTYBHN9vMdYea1jF0GO4gqGk1wqwb3yxv2QMYMbwJ6SI+k/
-ZjkSR/OilTCBhwYLKoZIhvcNAQkQAgsxeKB2MGIxCzAJBgNVBAYTAlpBMSUwIwYDVQQKExxUaGF3
-dGUgQ29uc3VsdGluZyAoUHR5KSBMdGQuMSwwKgYDVQQDEyNUaGF3dGUgUGVyc29uYWwgRnJlZW1h
-aWwgSXNzdWluZyBDQQIQWOcU1hfAMRlFZjkSR/OilTANBgkqhkiG9w0BAQEFAASCAQCfwQiC3v6/
-yleRDGv3bJ4nQYQ+c3mz3+mn3Xi6uU35n3piwxZZaWRdmLyiXPvU+QReHpSf3l2qsEZM3sdE0XF9
-eRul/+QTFJcDNXOEAxG1zC2Gpz+6c6RrX4Ou12Pwkp+pNrZWTSY/mZgdqcArupOBcZi7qBjoWcy5
-wb54dfvSSjrjmqLbkH/E8ww/6gGQuU/xXpAUZgUrTmQHrNKeIdSh5oDkOxFaFWvnmb8Z/2ixKqW/
-Ux6WqamyvBtTs/5YBEtnpZOk+uVoscYEUBhU+DVJ2OSvTdXSivMtBdXmGTsG22k+P1NGUHi/A7ev
-xPaO0uk4V8xyjNlN4HPuGpkrlXwPAAAAAAAA
-
---Apple-Mail-42-587703407--
diff --git a/actionmailer/test/fixtures/raw_email_with_partially_quoted_subject b/actionmailer/test/fixtures/raw_email_with_partially_quoted_subject
deleted file mode 100644
index e86108da1ea54..0000000000000
--- a/actionmailer/test/fixtures/raw_email_with_partially_quoted_subject
+++ /dev/null
@@ -1,14 +0,0 @@
-From jamis@37signals.com Mon May 2 16:07:05 2005
-Mime-Version: 1.0 (Apple Message framework v622)
-Content-Transfer-Encoding: base64
-Message-Id:
-Content-Type: text/plain;
- charset=EUC-KR;
- format=flowed
-To: jamis@37signals.com
-From: Jamis Buck
-Subject: Re: Test: =?UTF-8?B?Iua8ouWtlyI=?= mid =?UTF-8?B?Iua8ouWtlyI=?= tail
-Date: Mon, 2 May 2005 16:07:05 -0600
-
-tOu6zrrQwMcguLbC+bChwfa3ziwgv+y4rrTCIMfPs6q01MC7ILnPvcC0z7TZLg0KDQrBpiDAzLin
-wLogSmFtaXPA1LTPtNku
diff --git a/actionmailer/test/fixtures/second_mailer/share.erb b/actionmailer/test/fixtures/second_mailer/share.erb
deleted file mode 100644
index 9a540106720f1..0000000000000
--- a/actionmailer/test/fixtures/second_mailer/share.erb
+++ /dev/null
@@ -1 +0,0 @@
-second mail
diff --git a/actionmailer/test/fixtures/test_helper_mailer/welcome b/actionmailer/test/fixtures/test_helper_mailer/welcome
new file mode 100644
index 0000000000000..61ce70d578709
--- /dev/null
+++ b/actionmailer/test/fixtures/test_helper_mailer/welcome
@@ -0,0 +1 @@
+Welcome!
\ No newline at end of file
diff --git a/actionmailer/test/fixtures/test_mailer/_subtemplate.text.erb b/actionmailer/test/fixtures/test_mailer/_subtemplate.text.erb
deleted file mode 100644
index 3b4ba35f20488..0000000000000
--- a/actionmailer/test/fixtures/test_mailer/_subtemplate.text.erb
+++ /dev/null
@@ -1 +0,0 @@
-let's go!
\ No newline at end of file
diff --git a/actionmailer/test/fixtures/test_mailer/custom_templating_extension.html.haml b/actionmailer/test/fixtures/test_mailer/custom_templating_extension.html.haml
deleted file mode 100644
index 8dcf9746ccf27..0000000000000
--- a/actionmailer/test/fixtures/test_mailer/custom_templating_extension.html.haml
+++ /dev/null
@@ -1,6 +0,0 @@
-%p Hello there,
-
-%p
- Mr.
- = @recipient
- from haml
\ No newline at end of file
diff --git a/actionmailer/test/fixtures/test_mailer/custom_templating_extension.text.haml b/actionmailer/test/fixtures/test_mailer/custom_templating_extension.text.haml
deleted file mode 100644
index 8dcf9746ccf27..0000000000000
--- a/actionmailer/test/fixtures/test_mailer/custom_templating_extension.text.haml
+++ /dev/null
@@ -1,6 +0,0 @@
-%p Hello there,
-
-%p
- Mr.
- = @recipient
- from haml
\ No newline at end of file
diff --git a/actionmailer/test/fixtures/test_mailer/implicitly_multipart_example.html.erb b/actionmailer/test/fixtures/test_mailer/implicitly_multipart_example.html.erb
deleted file mode 100644
index 946d99ede50e3..0000000000000
--- a/actionmailer/test/fixtures/test_mailer/implicitly_multipart_example.html.erb
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
- HTML formatted message to <%= @recipient %>.
-
-
-
-
- HTML formatted message to <%= @recipient %>.
-
-
diff --git a/actionmailer/test/fixtures/test_mailer/implicitly_multipart_example.html.erb~ b/actionmailer/test/fixtures/test_mailer/implicitly_multipart_example.html.erb~
deleted file mode 100644
index 946d99ede50e3..0000000000000
--- a/actionmailer/test/fixtures/test_mailer/implicitly_multipart_example.html.erb~
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
- HTML formatted message to <%= @recipient %>.
-
-
-
-
- HTML formatted message to <%= @recipient %>.
-
-
diff --git a/actionmailer/test/fixtures/test_mailer/implicitly_multipart_example.ignored.erb b/actionmailer/test/fixtures/test_mailer/implicitly_multipart_example.ignored.erb
deleted file mode 100644
index 6940419d47a1e..0000000000000
--- a/actionmailer/test/fixtures/test_mailer/implicitly_multipart_example.ignored.erb
+++ /dev/null
@@ -1 +0,0 @@
-Ignored when searching for implicitly multipart parts.
diff --git a/actionmailer/test/fixtures/test_mailer/implicitly_multipart_example.rhtml.bak b/actionmailer/test/fixtures/test_mailer/implicitly_multipart_example.rhtml.bak
deleted file mode 100644
index 6940419d47a1e..0000000000000
--- a/actionmailer/test/fixtures/test_mailer/implicitly_multipart_example.rhtml.bak
+++ /dev/null
@@ -1 +0,0 @@
-Ignored when searching for implicitly multipart parts.
diff --git a/actionmailer/test/fixtures/test_mailer/implicitly_multipart_example.text.erb b/actionmailer/test/fixtures/test_mailer/implicitly_multipart_example.text.erb
deleted file mode 100644
index a6c8d54cf9faa..0000000000000
--- a/actionmailer/test/fixtures/test_mailer/implicitly_multipart_example.text.erb
+++ /dev/null
@@ -1,2 +0,0 @@
-Plain text to <%= @recipient %>.
-Plain text to <%= @recipient %>.
diff --git a/actionmailer/test/fixtures/test_mailer/implicitly_multipart_example.yaml.erb b/actionmailer/test/fixtures/test_mailer/implicitly_multipart_example.yaml.erb
deleted file mode 100644
index c14348c7707fd..0000000000000
--- a/actionmailer/test/fixtures/test_mailer/implicitly_multipart_example.yaml.erb
+++ /dev/null
@@ -1 +0,0 @@
-yaml to: <%= @recipient %>
\ No newline at end of file
diff --git a/actionmailer/test/fixtures/test_mailer/included_subtemplate.text.erb b/actionmailer/test/fixtures/test_mailer/included_subtemplate.text.erb
deleted file mode 100644
index a93c30ea1a732..0000000000000
--- a/actionmailer/test/fixtures/test_mailer/included_subtemplate.text.erb
+++ /dev/null
@@ -1 +0,0 @@
-Hey Ho, <%= render :partial => "subtemplate" %>
\ No newline at end of file
diff --git a/actionmailer/test/fixtures/test_mailer/multipart_alternative.html.erb b/actionmailer/test/fixtures/test_mailer/multipart_alternative.html.erb
deleted file mode 100644
index 73ea14f82f73e..0000000000000
--- a/actionmailer/test/fixtures/test_mailer/multipart_alternative.html.erb
+++ /dev/null
@@ -1 +0,0 @@
-foo <%= @foo %>
\ No newline at end of file
diff --git a/actionmailer/test/fixtures/test_mailer/multipart_alternative.plain.erb b/actionmailer/test/fixtures/test_mailer/multipart_alternative.plain.erb
deleted file mode 100644
index 779fe4c1eaa96..0000000000000
--- a/actionmailer/test/fixtures/test_mailer/multipart_alternative.plain.erb
+++ /dev/null
@@ -1 +0,0 @@
-foo: <%= @foo %>
\ No newline at end of file
diff --git a/actionmailer/test/fixtures/test_mailer/rxml_template.rxml b/actionmailer/test/fixtures/test_mailer/rxml_template.rxml
deleted file mode 100644
index d566bd8d7cbf4..0000000000000
--- a/actionmailer/test/fixtures/test_mailer/rxml_template.rxml
+++ /dev/null
@@ -1,2 +0,0 @@
-xml.instruct!
-xml.test
\ No newline at end of file
diff --git a/actionmailer/test/fixtures/test_mailer/signed_up.html.erb b/actionmailer/test/fixtures/test_mailer/signed_up.html.erb
deleted file mode 100644
index 7afe1f651c63b..0000000000000
--- a/actionmailer/test/fixtures/test_mailer/signed_up.html.erb
+++ /dev/null
@@ -1,3 +0,0 @@
-Hello there,
-
-Mr. <%= @recipient %>
\ No newline at end of file
diff --git a/actionmailer/test/fixtures/url_test_mailer/exercise_url_for.erb b/actionmailer/test/fixtures/url_test_mailer/exercise_url_for.erb
new file mode 100644
index 0000000000000..0322c1191eaa0
--- /dev/null
+++ b/actionmailer/test/fixtures/url_test_mailer/exercise_url_for.erb
@@ -0,0 +1 @@
+<%= url_for(@options) %> <%= @url %>
diff --git a/actionmailer/test/form_builder_test.rb b/actionmailer/test/form_builder_test.rb
new file mode 100644
index 0000000000000..460acd04be822
--- /dev/null
+++ b/actionmailer/test/form_builder_test.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "mailers/form_builder_mailer"
+
+class MailerFormBuilderTest < ActiveSupport::TestCase
+ def test_default_form_builder_assigned
+ email = FormBuilderMailer.welcome
+ assert_includes(email.body.encoded, "hi from SpecializedFormBuilder")
+ end
+end
diff --git a/actionmailer/test/i18n_with_controller_test.rb b/actionmailer/test/i18n_with_controller_test.rb
index 7040ae6f8d148..5e5be4ff2b2a3 100644
--- a/actionmailer/test/i18n_with_controller_test.rb
+++ b/actionmailer/test/i18n_with_controller_test.rb
@@ -1,46 +1,76 @@
-require 'abstract_unit'
-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)
@recipient = recipient
I18n.locale = :de
- mail(:to => recipient, :subject => "#{I18n.t :email_subject} #{recipient}",
- :from => "system@loudthinking.com", :date => Time.local(2004, 12, 12))
+ mail(to: recipient, subject: I18n.t(:email_subject),
+ from: "system@loudthinking.com", date: Time.local(2004, 12, 12))
end
end
class TestController < ActionController::Base
def send_mail
- I18nTestMailer.mail_with_i18n_subject("test@localhost").deliver
- render :text => 'Mail sent'
+ email = I18nTestMailer.mail_with_i18n_subject("test@localhost").deliver_now
+ render plain: "Mail sent - Subject: #{email.subject}"
end
end
class ActionMailerI18nWithControllerTest < ActionDispatch::IntegrationTest
Routes = ActionDispatch::Routing::RouteSet.new
Routes.draw do
- match ':controller(/:action(/:id))'
+ ActionDispatch.deprecator.silence do
+ get ":controller(/:action(/:id))"
+ end
end
- def app
- Routes
+ class RoutedRackApp
+ attr_reader :routes
+
+ def initialize(routes, &blk)
+ @routes = routes
+ @stack = ActionDispatch::MiddlewareStack.new(&blk).build(@routes)
+ end
+
+ def call(env)
+ @stack.call(env)
+ end
end
- def setup
- I18n.backend.store_translations('de', :email_subject => '[Signed up] Welcome')
+ APP = RoutedRackApp.new(Routes)
+
+ def app
+ APP
end
- def teardown
- I18n.locale = :en
+ teardown do
+ I18n.locale = I18n.default_locale
end
def test_send_mail
- get '/test/send_mail'
- assert_equal "Mail sent", @response.body
+ stub_any_instance(Mail::SMTP, instance: Mail::SMTP.new({})) do |instance|
+ assert_called(instance, :deliver!) do
+ 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
+
+ private
+ 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 5f52a1bd697bf..4272430a9bfd2 100644
--- a/actionmailer/test/log_subscriber_test.rb
+++ b/actionmailer/test/log_subscriber_test.rb
@@ -1,5 +1,7 @@
+# frozen_string_literal: true
+
require "abstract_unit"
-require 'mailers/base_mailer'
+require "mailers/base_mailer"
require "active_support/log_subscriber/test_helper"
require "action_mailer/log_subscriber"
@@ -11,9 +13,12 @@ def setup
ActionMailer::LogSubscriber.attach_to :action_mailer
end
- class TestMailer < ActionMailer::Base
- def receive(mail)
- # Do nothing
+ class BogusDelivery
+ def initialize(*)
+ end
+
+ def deliver!(mail)
+ raise "failed"
end
end
@@ -22,21 +27,43 @@ def set_logger(logger)
end
def test_deliver_is_notified
- BaseMailer.welcome.deliver
+ BaseMailer.welcome(message_id: "123@abc").deliver_now
wait
+
assert_equal(1, @logger.logged(:info).size)
- assert_match(/Sent mail to system@test.lindsaar.net/, @logger.logged(:info).first)
- assert_equal(1, @logger.logged(:debug).size)
- assert_match(/Welcome/, @logger.logged(:debug).first)
+ assert_match(/Delivered mail 123@abc/, @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)
+ 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")
- TestMailer.receive(fixture)
+ def test_deliver_message_when_perform_deliveries_is_false
+ BaseMailer.welcome_without_deliveries(message_id: "123@abc").deliver_now
wait
+
+ assert_equal(1, @logger.logged(:info).size)
+ assert_match("Skipped delivery of mail 123@abc 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_deliver_message_when_exception_happened
+ previous_delivery_method = BaseMailer.delivery_method
+ BaseMailer.delivery_method = BogusDelivery
+
+ assert_raises(RuntimeError) { BaseMailer.welcome(message_id: "123@abc").deliver_now }
+ wait
+
assert_equal(1, @logger.logged(:info).size)
- assert_match(/Received mail/, @logger.logged(:info).first)
- assert_equal(1, @logger.logged(:debug).size)
- assert_match(/Jamis/, @logger.logged(:debug).first)
+ assert_equal('Failed delivery of mail 123@abc error_class=RuntimeError error_message="failed"', @logger.logged(:info).first)
+ ensure
+ BaseMailer.delivery_method = previous_delivery_method
end
-end
\ No newline at end of file
+end
diff --git a/actionmailer/test/mail_helper_test.rb b/actionmailer/test/mail_helper_test.rb
index d8a73e6c468d3..06729826f8480 100644
--- a/actionmailer/test/mail_helper_test.rb
+++ b/actionmailer/test/mail_helper_test.rb
@@ -1,16 +1,18 @@
-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|
- format.html { render(:inline => "<%= block_format @text %>") }
+ format.html { render(inline: "<%= block_format @text %>") }
end
end
@@ -18,7 +20,7 @@ def use_format_paragraph
@text = "But soft! What light through yonder window breaks?"
mail_with_defaults do |format|
- format.html { render(:inline => "<%= format_paragraph @text, 15, 1 %>") }
+ format.html { render(inline: "<%= format_paragraph @text, 15, 1 %>") }
end
end
@@ -26,19 +28,19 @@ def use_format_paragraph_with_long_first_word
@text = "Antidisestablishmentarianism is very long."
mail_with_defaults do |format|
- format.html { render(:inline => "<%= format_paragraph @text, 10, 1 %>") }
+ format.html { render(inline: "<%= format_paragraph @text, 10, 1 %>") }
end
end
def use_mailer
mail_with_defaults do |format|
- format.html { render(:inline => "<%= mailer.message.subject %>") }
+ format.html { render(inline: "<%= mailer.message.subject %>") }
end
end
def use_message
mail_with_defaults do |format|
- format.html { render(:inline => "<%= message.subject %>") }
+ format.html { render(inline: "<%= message.subject %>") }
end
end
@@ -55,16 +57,21 @@ def use_block_format
TEXT
mail_with_defaults do |format|
- format.html { render(:inline => "<%= block_format @text %>") }
+ format.html { render(inline: "<%= block_format @text %>") }
end
end
- protected
-
- def mail_with_defaults(&block)
- mail(:to => "test@localhost", :from => "tester@example.com",
- :subject => "using helpers", &block)
+ def use_cache
+ mail_with_defaults do |format|
+ format.html { render(inline: "<% cache(:foo) do %>Greetings from a cache helper block<% end %>") }
+ end
end
+
+ private
+ def mail_with_defaults(&block)
+ mail(to: "test@localhost", from: "tester@example.com",
+ subject: "using helpers", &block)
+ end
end
class MailerHelperTest < ActionMailer::TestCase
@@ -107,5 +114,24 @@ def test_use_block_format
TEXT
assert_equal expected.gsub("\n", "\r\n"), mail.body.encoded
end
-end
+ def test_use_cache
+ assert_nothing_raised do
+ mail = HelperMailer.use_cache
+ assert_equal "Greetings from a cache helper block", mail.body.encoded
+ end
+ end
+
+ def helper
+ Object.new.extend(ActionMailer::MailHelper)
+ end
+
+ def test_block_format
+ assert_equal " * foo\n", helper.block_format(" * foo")
+ assert_equal " * foo\n", helper.block_format(" * foo")
+ assert_equal " * foo\n", helper.block_format("* foo")
+ assert_equal " * foo\n*bar", helper.block_format("* foo*bar")
+ assert_equal " * foo\n * bar\n", helper.block_format("* foo * bar")
+ assert_equal " *", helper.block_format("* ")
+ end
+end
diff --git a/actionmailer/test/mail_layout_test.rb b/actionmailer/test/mail_layout_test.rb
index 71e93c29f180b..16d77ed61dc0f 100644
--- a/actionmailer/test/mail_layout_test.rb
+++ b/actionmailer/test/mail_layout_test.rb
@@ -1,9 +1,11 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
class AutoLayoutMailer < ActionMailer::Base
- default :to => 'test@localhost',
- :subject => "You have a mail",
- :from => "tester@example.com"
+ default to: "test@localhost",
+ subject: "You have a mail",
+ from: "tester@example.com"
def hello
mail()
@@ -11,16 +13,16 @@ def hello
def spam
@world = "Earth"
- mail(:body => render(:inline => "Hello, <%= @world %>", :layout => 'spam'))
+ mail(body: render(inline: "Hello, <%= @world %>", layout: "spam"))
end
def nolayout
@world = "Earth"
- mail(:body => render(:inline => "Hello, <%= @world %>", :layout => false))
+ mail(body: render(inline: "Hello, <%= @world %>", layout: false))
end
def multipart(type = nil)
- mail(:content_type => type) do |format|
+ mail(content_type: type) do |format|
format.text { render }
format.html { render }
end
@@ -28,11 +30,11 @@ def multipart(type = nil)
end
class ExplicitLayoutMailer < ActionMailer::Base
- layout 'spam', :except => [:logout]
+ layout "spam", except: [:logout]
- default :to => 'test@localhost',
- :subject => "You have a mail",
- :from => "tester@example.com"
+ default to: "test@localhost",
+ subject: "You have a mail",
+ from: "tester@example.com"
def signup
mail()
@@ -44,16 +46,6 @@ def logout
end
class LayoutMailerTest < ActiveSupport::TestCase
- def setup
- set_delivery_method :test
- ActionMailer::Base.perform_deliveries = true
- ActionMailer::Base.deliveries.clear
- end
-
- def teardown
- restore_delivery_method
- end
-
def test_should_pickup_default_layout
mail = AutoLayoutMailer.hello
assert_equal "Hello from layout Inside", mail.body.to_s.strip
@@ -64,10 +56,10 @@ def test_should_pickup_multipart_layout
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
@@ -76,10 +68,10 @@ def test_should_pickup_multipartmixed_layout
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 f54a50d00d937..7a9aba2629cf9 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 e55d72fdb43f9..4a73d40bafb87 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: email_address_with_name("mikel@test.lindsaar.net", "Mikel")
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 = {})
@@ -16,7 +18,22 @@ def welcome_with_headers(hash = {})
end
def welcome_from_another_path(path)
- mail(:template_name => "welcome", :template_path => path)
+ mail(template_name: "welcome", template_path: path)
+ end
+
+ def welcome_without_deliveries(hash = {})
+ mail({ template_name: "welcome" }.merge!(hash))
+ mail.perform_deliveries = false
+ end
+
+ def with_name
+ to = email_address_with_name("sunny@example.com", "Sunny")
+ mail(template_name: "welcome", to: to)
+ end
+
+ def with_blank_name
+ to = email_address_with_name("sunny@example.com", "")
+ mail(template_name: "welcome", to: to)
end
def html_only(hash = {})
@@ -28,30 +45,40 @@ def plain_text_only(hash = {})
end
def inline_attachment
- attachments.inline['logo.png'] = "\312\213\254\232"
+ attachments.inline["logo.png"] = "\312\213\254\232"
+ mail
+ end
+
+ def inline_and_other_attachments
+ attachments.inline["logo.png"] = "\312\213\254\232"
+ attachments["certificate.pdf"] = "This is test File content"
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 => "\312\213\254\232)b",
- :mime_type => "image/x-jpg",
- :transfer_encoding => "base64" }
+ 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",
- :mime_type => "image/x-jpg" }
+ 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
+
+ def implicit_multipart_formats(hash = {})
mail(hash)
end
@@ -60,10 +87,10 @@ def implicit_with_locale(hash = {})
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,14 +103,20 @@ def explicit_multipart_templates(hash = {})
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
+
+ def explicit_without_specifying_format_with_any(hash = {})
+ mail(hash) do |format|
+ format.any
end
end
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
@@ -94,25 +127,44 @@ def explicit_multipart_with_one_template(hash = {})
end
end
- def implicit_different_template(template_name='')
- mail(:template_name => template_name)
+ def implicit_different_template(template_name = "")
+ mail(template_name: template_name)
+ end
+
+ 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}" }
+ 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 }
+ format.text { render layout: layout_name }
+ format.html { render layout: layout_name }
end
end
def email_with_translations
- mail :body => render("email_with_translations", :formats => [:html])
+ mail body: render("email_with_translations", formats: [:html])
+ end
+
+ def without_mail_call
+ end
+
+ def with_nil_as_return_value
+ mail(template_name: "welcome")
+ nil
+ end
+
+ def with_subject_interpolations
+ 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 0000000000000..02f0c6c103b56
--- /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/callback_mailer.rb b/actionmailer/test/mailers/callback_mailer.rb
new file mode 100644
index 0000000000000..86e6c005d4f56
--- /dev/null
+++ b/actionmailer/test/mailers/callback_mailer.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+CallbackMailerError = Class.new(StandardError)
+class CallbackMailer < ActionMailer::Base
+ cattr_accessor :rescue_from_error
+ cattr_accessor :after_deliver_instance
+ cattr_accessor :around_deliver_instance
+ cattr_accessor :abort_before_deliver
+ cattr_accessor :around_handles_error
+
+ rescue_from CallbackMailerError do |error|
+ @@rescue_from_error = error
+ end
+
+ before_deliver do
+ throw :abort if @@abort_before_deliver
+ end
+
+ after_deliver do
+ @@after_deliver_instance = self
+ end
+
+ around_deliver do |mailer, block|
+ @@around_deliver_instance = self
+ block.call
+ rescue StandardError
+ raise unless @@around_handles_error
+ end
+
+ def test_message(*)
+ mail(from: "test-sender@test.com", to: "test-receiver@test.com", subject: "Test Subject", body: "Test Body")
+ end
+
+ def test_raise_action
+ raise CallbackMailerError, "boom action processing"
+ end
+end
diff --git a/actionmailer/test/mailers/delayed_mailer.rb b/actionmailer/test/mailers/delayed_mailer.rb
new file mode 100644
index 0000000000000..37c7590d97bb2
--- /dev/null
+++ b/actionmailer/test/mailers/delayed_mailer.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+class DelayedMailerError < StandardError; end
+
+class DelayedMailer < ActionMailer::Base
+ self.deliver_later_queue_name = :delayed_mailers
+
+ 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")
+ end
+
+ def test_kwargs(argument:)
+ 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/form_builder_mailer.rb b/actionmailer/test/mailers/form_builder_mailer.rb
new file mode 100644
index 0000000000000..8e2f00a01b20f
--- /dev/null
+++ b/actionmailer/test/mailers/form_builder_mailer.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class FormBuilderMailer < ActionMailer::Base
+ class SpecializedFormBuilder < ActionView::Helpers::FormBuilder
+ def message
+ "hi from SpecializedFormBuilder"
+ end
+ end
+
+ default_form_builder SpecializedFormBuilder
+
+ def welcome
+ mail(to: "email@example.com")
+ end
+end
diff --git a/actionmailer/test/mailers/params_mailer.rb b/actionmailer/test/mailers/params_mailer.rb
new file mode 100644
index 0000000000000..84aa336311313
--- /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 43916e1421449..75ee5dd9b61c5 100644
--- a/actionmailer/test/mailers/proc_mailer.rb
+++ b/actionmailer/test/mailers/proc_mailer.rb
@@ -1,16 +1,24 @@
+# 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 },
- :subject => Proc.new { give_a_greeting }
+ 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-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
new file mode 100644
index 0000000000000..81061ff25b5d7
--- /dev/null
+++ b/actionmailer/test/message_delivery_test.rb
@@ -0,0 +1,187 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_job"
+require "mailers/delayed_mailer"
+
+class MessageDeliveryTest < ActiveSupport::TestCase
+ include ActiveJob::TestHelper
+
+ setup do
+ @previous_logger = ActiveJob::Base.logger
+ @previous_delivery_method = ActionMailer::Base.delivery_method
+
+ ActiveJob::Base.logger = Logger.new(nil)
+ ActionMailer::Base.delivery_method = :test
+
+ 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
+
+ DelayedMailer.last_error = nil
+ DelayedMailer.last_rescue_from_instance = nil
+ end
+
+ 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
+ end
+
+ test "should respond to .deliver_later" do
+ assert_respond_to @mail, :deliver_later
+ end
+
+ test "should respond to .deliver_later!" do
+ assert_respond_to @mail, :deliver_later!
+ end
+
+ test "should respond to .deliver_now" do
+ assert_respond_to @mail, :deliver_now
+ end
+
+ test "should respond to .deliver_now!" do
+ assert_respond_to @mail, :deliver_now!
+ end
+
+ test "should enqueue the email with :deliver_now delivery method" do
+ assert_performed_with(job: ActionMailer::MailDeliveryJob, args: ["DelayedMailer", "test_message", "deliver_now", args: [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::MailDeliveryJob, args: ["DelayedMailer", "test_message", "deliver_now!", args: [1, 2, 3]]) do
+ @mail.deliver_later!
+ end
+ end
+
+ test "should enqueue delivery with a delay" do
+ travel_to Time.new(2004, 11, 24, 1, 4, 44) do
+ assert_performed_with(job: ActionMailer::MailDeliveryJob, at: Time.current + 10.minutes, args: ["DelayedMailer", "test_message", "deliver_now", args: [1, 2, 3]]) do
+ @mail.deliver_later wait: 10.minutes
+ end
+ end
+ end
+
+ test "should enqueue delivery with a priority" do
+ job = @mail.deliver_later priority: 10
+ assert_equal 10, job.priority
+ end
+
+ test "should enqueue delivery at a specific time" do
+ later_time = Time.current + 1.hour
+ assert_performed_with(job: ActionMailer::MailDeliveryJob, at: later_time, args: ["DelayedMailer", "test_message", "deliver_now", args: [1, 2, 3]]) do
+ @mail.deliver_later wait_until: later_time
+ end
+ end
+
+ test "should enqueue delivery on the correct queue" do
+ assert_performed_with(job: ActionMailer::MailDeliveryJob, args: ["DelayedMailer", "test_message", "deliver_now", args: [1, 2, 3]], queue: "delayed_mailers") do
+ @mail.deliver_later
+ end
+ end
+
+ test "should enqueue delivery with the correct job" do
+ old_delivery_job = DelayedMailer.delivery_job
+ DelayedMailer.delivery_job = DummyJob
+
+ assert_performed_with(job: DummyJob, args: ["DelayedMailer", "test_message", "deliver_now", args: [1, 2, 3]]) do
+ @mail.deliver_later
+ end
+
+ DelayedMailer.delivery_job = old_delivery_job
+ end
+
+ class DummyJob < ActionMailer::MailDeliveryJob; end
+
+ test "delivery queue can be overridden when enqueuing mail" do
+ assert_performed_with(job: ActionMailer::MailDeliveryJob, args: ["DelayedMailer", "test_message", "deliver_now", args: [1, 2, 3]], queue: "another_queue") do
+ @mail.deliver_later(queue: :another_queue)
+ end
+ end
+
+ test "delivery queue can be overridden in subclasses" do
+ previous_queue_name = DelayedMailer.deliver_later_queue_name
+ DelayedMailer.deliver_later_queue_name = :throttled_mailers
+
+ assert_equal :throttled_mailers, DelayedMailer.deliver_later_queue_name
+ assert_equal :mailers, ActionMailer::Base.deliver_later_queue_name
+
+ assert_performed_with(job: ActionMailer::MailDeliveryJob, args: ["DelayedMailer", "test_message", "deliver_now", args: []], queue: "throttled_mailers") do
+ DelayedMailer.test_message.deliver_later
+ end
+ ensure
+ DelayedMailer.deliver_later_queue_name = previous_queue_name
+ 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
+
+ test "allows for keyword arguments" do
+ assert_performed_with(job: ActionMailer::MailDeliveryJob, args: ["DelayedMailer", "test_kwargs", "deliver_now", args: [argument: 1]]) do
+ message = DelayedMailer.test_kwargs(argument: 1)
+ message.deliver_later
+ end
+ end
+end
diff --git a/actionmailer/test/parameterized_test.rb b/actionmailer/test/parameterized_test.rb
new file mode 100644
index 0000000000000..366327ee76be1
--- /dev/null
+++ b/actionmailer/test/parameterized_test.rb
@@ -0,0 +1,93 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_job"
+require "mailers/params_mailer"
+
+class ParameterizedTest < ActiveSupport::TestCase
+ include ActiveJob::TestHelper
+
+ class DummyDeliveryJob < ActionMailer::MailDeliveryJob
+ end
+
+ 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
+
+ @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
+ 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 "degrade gracefully when .with is not called" do
+ @mail = ParamsMailer.invitation
+
+ assert_nil(@mail.to)
+ assert_nil(@mail.from)
+ end
+
+ test "enqueue the email with params" do
+ args = [
+ "ParamsMailer",
+ "invitation",
+ "deliver_now",
+ params: { inviter: "david@basecamp.com", invitee: "jason@basecamp.com" },
+ args: [],
+ ]
+ assert_performed_with(job: ActionMailer::MailDeliveryJob, args: args) 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
+ args = [
+ "ParamsMailer",
+ "invitation",
+ "deliver_now",
+ params: { inviter: "david@basecamp.com", invitee: "jason@basecamp.com" },
+ args: [],
+ ]
+
+ with_delivery_job DummyDeliveryJob do
+ assert_performed_with(job: DummyDeliveryJob, args: args) do
+ @mail.deliver_later
+ end
+ end
+ end
+
+ private
+ def with_delivery_job(job)
+ old_delivery_job = ParamsMailer.delivery_job
+ ParamsMailer.delivery_job = job
+ yield
+ ensure
+ ParamsMailer.delivery_job = old_delivery_job
+ end
+end
diff --git a/actionmailer/test/test_case_test.rb b/actionmailer/test/test_case_test.rb
new file mode 100644
index 0000000000000..9897f3891bb1e
--- /dev/null
+++ b/actionmailer/test/test_case_test.rb
@@ -0,0 +1,68 @@
+# 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 ManuallySetNameMailerTest < ActionMailer::TestCase
+ tests TestTestMailer
+
+ def test_set_mailer_class_manual
+ assert_equal TestTestMailer, self.class.mailer_class
+ end
+end
+
+class ManuallySetSymbolNameMailerTest < ActionMailer::TestCase
+ tests :test_test_mailer
+
+ def test_set_mailer_class_manual_using_symbol
+ assert_equal TestTestMailer, self.class.mailer_class
+ end
+end
+
+class ManuallySetStringNameMailerTest < ActionMailer::TestCase
+ tests "test_test_mailer"
+
+ def test_set_mailer_class_manual_using_string
+ assert_equal TestTestMailer, self.class.mailer_class
+ end
+end
diff --git a/actionmailer/test/test_helper_test.rb b/actionmailer/test/test_helper_test.rb
index dd62164176de8..de1c3861a96fe 100644
--- a/actionmailer/test/test_helper_test.rb
+++ b/actionmailer/test/test_helper_test.rb
@@ -1,15 +1,58 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/testing/stream"
class TestHelperMailer < ActionMailer::Base
def test
@world = "Earth"
- mail :body => render(:inline => "Hello, <%= @world %>"),
- :to => "test@example.com",
- :from => "tester@example.com"
+ mail body: render(inline: "Hello, <%= @world %>"),
+ subject: "Hi!",
+ 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_named_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 CustomDeliveryJob < ActionMailer::MailDeliveryJob
+end
+
+class CustomDeliveryMailer < TestHelperMailer
+ self.delivery_job = CustomDeliveryJob
+end
+
+class CustomQueueMailer < TestHelperMailer
+ self.deliver_later_queue_name = :custom_queue
end
class TestHelperMailerTest < ActionMailer::TestCase
+ include ActiveSupport::Testing::Stream
+
+ setup do
+ @previous_deliver_later_queue_name = ActionMailer::Base.deliver_later_queue_name
+ end
+
+ teardown do
+ ActionMailer::Base.deliver_later_queue_name = @previous_deliver_later_queue_name
+ end
+
def test_setup_sets_right_action_mailer_options
assert_equal :test, ActionMailer::Base.delivery_method
assert ActionMailer::Base.perform_deliveries
@@ -36,10 +79,68 @@ def test_charset_is_utf_8
assert_equal "UTF-8", charset
end
+ def test_encode
+ 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")
+ end
+
def test_assert_emails
assert_nothing_raised do
assert_emails 1 do
- TestHelperMailer.test.deliver
+ TestHelperMailer.test.deliver_now
+ end
+ end
+ end
+
+ def test_capture_emails
+ assert_nothing_raised do
+ emails = capture_emails do
+ TestHelperMailer.test.deliver_now
+ end
+ email = emails.first
+ assert_instance_of Mail::Message, email
+ assert_equal "Hello, Earth", email.body.to_s
+ assert_equal "Hi!", email.subject
+
+ emails = capture_emails do
+ TestHelperMailer.test.deliver_now
+ TestHelperMailer.test.deliver_now
+ end
+ assert_instance_of Array, emails
+ assert_instance_of Mail::Message, emails.first
+ assert_instance_of Mail::Message, emails.second
+ end
+ end
+
+ def test_assert_emails_with_custom_delivery_job
+ assert_nothing_raised do
+ assert_emails(1) do
+ silence_stream($stdout) do
+ CustomDeliveryMailer.test.deliver_later
+ end
+ end
+ end
+ end
+
+ def test_assert_emails_with_custom_parameterized_delivery_job
+ assert_nothing_raised do
+ assert_emails(1) do
+ silence_stream($stdout) do
+ CustomDeliveryMailer.with(foo: "bar").test_parameter_args.deliver_later
+ end
+ end
+ 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
@@ -47,27 +148,27 @@ def test_assert_emails
def test_repeated_assert_emails_calls
assert_nothing_raised do
assert_emails 1 do
- TestHelperMailer.test.deliver
+ TestHelperMailer.test.deliver_now
end
end
assert_nothing_raised do
assert_emails 2 do
- TestHelperMailer.test.deliver
- TestHelperMailer.test.deliver
+ TestHelperMailer.test.deliver_now
+ TestHelperMailer.test.deliver_now
end
end
end
def test_assert_emails_with_no_block
assert_nothing_raised do
- TestHelperMailer.test.deliver
+ TestHelperMailer.test.deliver_now
assert_emails 1
end
assert_nothing_raised do
- TestHelperMailer.test.deliver
- TestHelperMailer.test.deliver
+ TestHelperMailer.test.deliver_now
+ TestHelperMailer.test.deliver_now
assert_emails 3
end
end
@@ -80,10 +181,22 @@ def test_assert_no_emails
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
- TestHelperMailer.test.deliver
+ TestHelperMailer.test.deliver_now
end
end
@@ -93,23 +206,406 @@ def test_assert_emails_too_few_sent
def test_assert_emails_too_many_sent
error = assert_raise ActiveSupport::TestCase::Assertion do
assert_emails 1 do
- TestHelperMailer.test.deliver
- TestHelperMailer.test.deliver
+ TestHelperMailer.test.deliver_now
+ TestHelperMailer.test.deliver_now
end
end
assert_match(/1 .* but 2/, error.message)
end
+ def test_assert_emails_message
+ TestHelperMailer.test.deliver_now
+ error = assert_raise ActiveSupport::TestCase::Assertion do
+ assert_emails 2 do
+ TestHelperMailer.test.deliver_now
+ end
+ end
+ assert_match "Expected: 2", error.message
+ assert_match "Actual: 1", error.message
+ end
+
def test_assert_no_emails_failure
error = assert_raise ActiveSupport::TestCase::Assertion do
assert_no_emails do
- TestHelperMailer.test.deliver
+ TestHelperMailer.test.deliver_now
end
end
assert_match(/0 .* but 1/, error.message)
end
+
+ def test_assert_enqueued_emails
+ assert_nothing_raised do
+ assert_enqueued_emails 1 do
+ silence_stream($stdout) do
+ TestHelperMailer.test.deliver_later
+ end
+ end
+ 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
+ silence_stream($stdout) do
+ TestHelperMailer.test.deliver_later
+ end
+ end
+ end
+
+ assert_match(/2 .* but 1/, error.message)
+ end
+
+ def test_assert_enqueued_emails_with_custom_delivery_job
+ assert_nothing_raised do
+ assert_enqueued_emails(1) do
+ silence_stream($stdout) do
+ CustomDeliveryMailer.test.deliver_later
+ end
+ end
+ end
+ end
+
+ def test_assert_enqueued_emails_too_many_sent
+ error = assert_raise ActiveSupport::TestCase::Assertion do
+ assert_enqueued_emails 1 do
+ silence_stream($stdout) do
+ TestHelperMailer.test.deliver_later
+ TestHelperMailer.test.deliver_later
+ end
+ end
+ end
+
+ assert_match(/1 .* but 2/, error.message)
+ end
+
+ def test_assert_no_enqueued_emails
+ assert_nothing_raised do
+ assert_no_enqueued_emails do
+ TestHelperMailer.test.deliver_now
+ end
+ 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
+ silence_stream($stdout) do
+ TestHelperMailer.test.deliver_later
+ end
+ end
+ end
+
+ 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_when_deliver_later_queue_name_is_nil
+ ActionMailer::Base.deliver_later_queue_name = nil
+
+ 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_when_deliver_later_queue_name_with_non_default_name
+ ActionMailer::Base.deliver_later_queue_name = "sample_mailer_queue_name"
+
+ 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_when_deliver_later_queue_name_is_symbol
+ ActionMailer::Base.deliver_later_queue_name = :mailers
+
+ 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_when_queue_arg_is_symbol
+ ActionMailer::Base.deliver_later_queue_name = "mailers"
+
+ assert_nothing_raised do
+ assert_enqueued_email_with TestHelperMailer, :test, queue: :mailers do
+ silence_stream($stdout) do
+ TestHelperMailer.test.deliver_later
+ end
+ end
+ end
+ end
+
+ def test_assert_enqueued_email_with_when_mailer_has_custom_deliver_later_queue
+ assert_nothing_raised do
+ assert_enqueued_email_with CustomQueueMailer, :test do
+ silence_stream($stdout) do
+ CustomQueueMailer.test.deliver_later
+ end
+ end
+
+ assert_enqueued_email_with CustomQueueMailer, :test, queue: :custom_queue do
+ silence_stream($stdout) do
+ CustomQueueMailer.test.deliver_later
+ end
+ end
+ end
+ end
+
+ def test_assert_enqueued_email_with_when_mailer_has_custom_delivery_job
+ assert_nothing_raised do
+ assert_enqueued_email_with CustomDeliveryMailer, :test do
+ silence_stream($stdout) do
+ CustomDeliveryMailer.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, params: { 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_parameterized_mailer
+ assert_nothing_raised do
+ assert_enqueued_email_with TestHelperMailer.with(all: "good"), :test_parameter_args 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_named_args
+ assert_nothing_raised do
+ assert_enqueued_email_with TestHelperMailer, :test_named_args, args: [{ email: "some_email", name: "some_name" }] do
+ silence_stream($stdout) do
+ TestHelperMailer.test_named_args(email: "some_email", name: "some_name").deliver_later
+ end
+ end
+ end
+ end
+
+ def test_assert_enqueued_email_with_with_params_and_args
+ assert_nothing_raised do
+ assert_enqueued_email_with TestHelperMailer, :test_args, params: { all: "good" }, args: ["some_email", "some_name"] do
+ silence_stream($stdout) do
+ TestHelperMailer.with(all: "good").test_args("some_email", "some_name").deliver_later
+ end
+ end
+ end
+ end
+
+ def test_assert_enqueued_email_with_with_params_and_named_args
+ assert_nothing_raised do
+ assert_enqueued_email_with TestHelperMailer, :test_named_args, params: { all: "good" }, args: [{ email: "some_email", name: "some_name" }] do
+ silence_stream($stdout) do
+ TestHelperMailer.with(all: "good").test_named_args(email: "some_email", name: "some_name").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
+ end
+ assert_enqueued_email_with TestHelperMailer, :test_parameter_args, params: { all: "good" }
+ end
+ end
+
+ def test_assert_enqueued_email_with_supports_params_matcher_proc
+ mail_params = { all: "good" }
+
+ silence_stream($stdout) do
+ TestHelperMailer.with(mail_params).test_parameter_args.deliver_later
+ end
+
+ matcher_params = nil
+
+ assert_nothing_raised do
+ assert_enqueued_email_with TestHelperMailer, :test_parameter_args, params: ->(params) { matcher_params = params }
+ end
+
+ assert_equal mail_params, matcher_params
+
+ assert_raises ActiveSupport::TestCase::Assertion do
+ assert_enqueued_email_with TestHelperMailer, :test_parameter_args, params: ->(_) { false }
+ end
+ end
+
+ def test_assert_enqueued_email_with_supports_args_matcher_proc
+ mail_args = ["some_email", "some_name"]
+
+ silence_stream($stdout) do
+ TestHelperMailer.test_args(*mail_args).deliver_later
+ end
+
+ matcher_args = nil
+
+ assert_nothing_raised do
+ assert_enqueued_email_with TestHelperMailer, :test_args, args: ->(args) { matcher_args = args }
+ end
+
+ assert_equal mail_args, matcher_args
+
+ assert_raises ActiveSupport::TestCase::Assertion do
+ assert_enqueued_email_with TestHelperMailer, :test_args, args: ->(_) { false }
+ end
+ end
+
+ def test_assert_enqueued_email_with_supports_named_args_matcher_proc
+ mail_args = [{ email: "some_email", name: "some_name" }]
+
+ silence_stream($stdout) do
+ TestHelperMailer.test_named_args(**mail_args[0]).deliver_later
+ end
+
+ matcher_args = nil
+
+ assert_nothing_raised do
+ assert_enqueued_email_with TestHelperMailer, :test_named_args, args: ->(args) { matcher_args = args }
+ end
+
+ assert_equal mail_args, matcher_args
+ end
+
+ def test_deliver_enqueued_emails_with_no_block
+ assert_nothing_raised do
+ silence_stream($stdout) do
+ TestHelperMailer.test.deliver_later
+ deliver_enqueued_emails
+ end
+ end
+
+ assert_emails(1)
+ end
+
+ def test_deliver_enqueued_emails_with_a_block
+ assert_nothing_raised do
+ deliver_enqueued_emails do
+ silence_stream($stdout) do
+ TestHelperMailer.test.deliver_later
+ end
+ end
+ end
+
+ assert_emails(1)
+ end
+
+ def test_deliver_enqueued_emails_with_custom_delivery_job
+ assert_nothing_raised do
+ deliver_enqueued_emails do
+ silence_stream($stdout) do
+ CustomDeliveryMailer.test.deliver_later
+ end
+ end
+ end
+
+ assert_emails(1)
+ end
+
+ def test_deliver_enqueued_emails_with_custom_queue
+ assert_nothing_raised do
+ deliver_enqueued_emails(queue: CustomQueueMailer.deliver_later_queue_name) do
+ silence_stream($stdout) do
+ TestHelperMailer.test.deliver_later
+ CustomQueueMailer.test.deliver_later
+ end
+ end
+ end
+
+ assert_emails(1)
+ assert_enqueued_email_with(TestHelperMailer, :test)
+ end
+
+ def test_deliver_enqueued_emails_with_at
+ assert_nothing_raised do
+ deliver_enqueued_emails(at: 1.hour.from_now) do
+ silence_stream($stdout) do
+ TestHelperMailer.test.deliver_later
+ TestHelperMailer.test.deliver_later(wait: 2.hours)
+ end
+ end
+ end
+
+ assert_emails(1)
+ end
end
class AnotherTestHelperMailerTest < ActionMailer::TestCase
@@ -121,6 +617,20 @@ def setup
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
+
+class AdapterIsNotTestAdapterTest < ActionMailer::TestCase
+ def queue_adapter_for_test
+ ActiveJob::QueueAdapters::InlineAdapter.new
+ end
+
+ def test_can_send_email_using_any_active_job_adapter
+ assert_nothing_raised do
+ assert_emails 1 do
+ TestHelperMailer.test.deliver_now
+ end
+ end
end
end
diff --git a/actionmailer/test/test_test.rb b/actionmailer/test/test_test.rb
deleted file mode 100644
index 86fd37bea6efd..0000000000000
--- a/actionmailer/test/test_test.rb
+++ /dev/null
@@ -1,28 +0,0 @@
-require 'abstract_unit'
-
-class TestTestMailer < ActionMailer::Base
-end
-
-class CrazyNameMailerTest < ActionMailer::TestCase
- tests TestTestMailer
-
- def test_set_mailer_class_manual
- assert_equal TestTestMailer, self.class.mailer_class
- end
-end
-
-class CrazySymbolNameMailerTest < ActionMailer::TestCase
- tests :test_test_mailer
-
- def test_set_mailer_class_manual_using_symbol
- assert_equal TestTestMailer, self.class.mailer_class
- end
-end
-
-class CrazyStringNameMailerTest < ActionMailer::TestCase
- tests 'test_test_mailer'
-
- def test_set_mailer_class_manual_using_string
- assert_equal TestTestMailer, self.class.mailer_class
- end
-end
diff --git a/actionmailer/test/url_test.rb b/actionmailer/test/url_test.rb
index 0536e830984c5..123c194aacc2d 100644
--- a/actionmailer/test/url_test.rb
+++ b/actionmailer/test/url_test.rb
@@ -1,37 +1,63 @@
-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(http://github.com/recipient)
@recipient = recipient
- @welcome_url = url_for :host => "example.com", :controller => "welcome", :action => "greeting"
- mail(:to => recipient, :subject => "[Signed up] Welcome #{recipient}",
- :from => "system@loudthinking.com", :date => Time.local(2004, 12, 12))
+ @welcome_url = url_for host: "example.com", controller: "welcome", action: "greeting"
+ mail(to: recipient, subject: "[Signed up] Welcome #{recipient}",
+ from: "system@loudthinking.com", date: Time.local(2004, 12, 12))
+ end
+
+ def exercise_url_for(options)
+ @options = options
+ @url = url_for(@options)
+ mail(from: "from@example.com", to: "to@example.com", subject: "subject")
end
end
class ActionMailerUrlTest < ActionMailer::TestCase
+ class DummyModel
+ def self.model_name
+ Struct.new(:route_key, :name).new("dummy_model", nil)
+ end
- def encode( text, charset="UTF-8" )
- quoted_printable( text, charset )
+ def persisted?
+ false
+ end
+
+ def model_name
+ self.class.model_name
+ end
+
+ def to_model
+ self
+ end
end
- def new_mail( charset="UTF-8" )
+ def new_mail(charset = "UTF-8")
mail = Mail.new
mail.mime_version = "1.0"
if charset
@@ -40,31 +66,48 @@ def new_mail( charset="UTF-8" )
mail
end
- def setup
- set_delivery_method :test
- ActionMailer::Base.perform_deliveries = true
- ActionMailer::Base.deliveries.clear
- ActiveSupport::Deprecation.silenced = false
+ def assert_url_for(expected, options, relative = false)
+ expected = "http://www.basecamphq.com/#{expected}" if expected.start_with?("/") && !relative
+ urls = UrlTestMailer.exercise_url_for(options).body.to_s.chomp.split
- @recipient = 'test@localhost'
+ assert_equal expected, urls.first
+ assert_equal expected, urls.second
end
- def teardown
- restore_delivery_method
+ def setup
+ @recipient = "test@localhost"
end
- def test_signed_up_with_url
+ def test_url_for
UrlTestMailer.delivery_method = :test
- AppRoutes.draw do
- match ':controller(/:action(/:id))'
- match '/welcome' => "foo#bar", :as => "welcome"
- end
+ # string
+ assert_url_for "http://foo/", "http://foo/"
+
+ # symbol
+ 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
+
+ # model
+ assert_url_for "/dummy_model", DummyModel.new
+
+ # class
+ assert_url_for "/dummy_model", DummyModel
+
+ # array
+ assert_url_for "/dummy_model", [DummyModel]
+ end
+
+ def test_signed_up_with_url
+ UrlTestMailer.delivery_method = :test
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"
+ expected.body = "Hello there,\n\nMr. #{@recipient}. Please see our greeting at http://example.com/welcome/greeting http://www.basecamphq.com/welcome\n\n"
expected.from = "system@loudthinking.com"
expected.date = Time.local(2004, 12, 12)
expected.content_type = "text/html"
@@ -73,15 +116,15 @@ def test_signed_up_with_url
assert_nothing_raised { created = UrlTestMailer.signed_up_with_url(http://github.com/@recipient) }
assert_not_nil created
- expected.message_id = '<123@456>'
- created.message_id = '<123@456>'
- assert_equal expected.encoded, created.encoded
+ 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(http://github.com/@recipient).deliver }
+ assert_nothing_raised { UrlTestMailer.signed_up_with_url(http://github.com/@recipient).deliver_now }
assert_not_nil ActionMailer::Base.deliveries.first
delivered = ActionMailer::Base.deliveries.first
- delivered.message_id = '<123@456>'
- assert_equal expected.encoded, delivered.encoded
+ delivered.message_id = "<123@456>"
+ assert_dom_equal expected.encoded, delivered.encoded
end
end
diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md
index 96ad3a155c657..708812d618810 100644
--- a/actionpack/CHANGELOG.md
+++ b/actionpack/CHANGELOG.md
@@ -1,6065 +1,189 @@
-## Rails 4.0.0 (unreleased) ##
+* Always return empty body for HEAD requests in `PublicExceptions` and
+ `DebugExceptions`.
-* Forms of persisted records use always PATCH (via the `_method` hack). *fxn*
+ This is required by `Rack::Lint` (per RFC9110).
-* For resources, both PATCH and PUT are routed to the `update` action. *fxn*
+ *Hartley McGuire*
-* Don't ignore `force_ssl` in development. This is a change of behavior - use a `:if` condition to recreate the old behavior.
+* Add comprehensive support for HTTP Cache-Control request directives according to RFC 9111.
- class AccountsController < ApplicationController
- force_ssl :if => :ssl_configured?
+ Provides a `request.cache_control_directives` object that gives access to request cache directives:
- def ssl_configured?
- !Rails.env.development?
- end
- end
-
- *Pat Allan*
-
-* Adds support for the PATCH verb:
- * Request objects respond to `patch?`.
- * Routes have a new `patch` method, and understand `:patch` in the
- existing places where a verb is configured, like `:via`.
- * New method `patch` available in functional tests.
- * If `:patch` is the default verb for updates, edits are
- tunneled as PATCH rather than as PUT, and routing acts accordingly.
- * New method `patch_via_redirect` available in integration tests.
-
- *dlee*
-
-* Integration tests support the `OPTIONS` method. *Jeremy Kemper*
-
-* `expires_in` accepts a `must_revalidate` flag. If true, "must-revalidate"
- is added to the Cache-Control header. *fxn*
-
-* Add `date_field` and `date_field_tag` helpers which render an `input[type="date"]` tag *Olek Janiszewski*
-
-* Adds `image_url`, `javascript_url`, `stylesheet_url`, `audio_url`, `video_url`, and `font_url`
- to assets tag helper. These URL helpers will return the full path to your assets. This is useful
- when you are going to reference this asset from external host. *Prem Sichanugrist*
-
-* Default responder will now always use your overridden block in `respond_with` to render your response. *Prem Sichanugrist*
-
-* Allow `value_method` and `text_method` arguments from `collection_select` and
- `options_from_collection_for_select` to receive an object that responds to `:call`,
- such as a `proc`, to evaluate the option in the current element context. This works
- the same way with `collection_radio_buttons` and `collection_check_boxes`.
-
- *Carlos Antonio da Silva + Rafael Mendonça França*
-
-* Add `collection_check_boxes` form helper, similar to `collection_select`:
- Example:
-
- collection_check_boxes :post, :author_ids, Author.all, :id, :name
- # Outputs something like:
-
-
-
-
-
-
- The label/check_box pairs can be customized with a block.
-
- *Carlos Antonio da Silva + Rafael Mendonça França*
-
-* Add `collection_radio_buttons` form helper, similar to `collection_select`:
- Example:
-
- collection_radio_buttons :post, :author_id, Author.all, :id, :name
- # Outputs something like:
-
-
-
-
-
- The label/radio_button pairs can be customized with a block.
-
- *Carlos Antonio da Silva + Rafael Mendonça França*
-
-* check_box with `:form` html5 attribute will now replicate the `:form`
- attribute to the hidden field as well. *Carlos Antonio da Silva*
-
-* `label` form helper accepts :for => nil to not generate the attribute. *Carlos Antonio da Silva*
-
-* Add `:format` option to number_to_percentage *Rodrigo Flores*
-
-* Add `config.action_view.logger` to configure logger for ActionView. *Rafael Mendonça França*
-
-* Deprecated ActionController::Integration in favour of ActionDispatch::Integration
-
-* Deprecated ActionController::IntegrationTest in favour of ActionDispatch::IntegrationTest
-
-* Deprecated ActionController::PerformanceTest in favour of ActionDispatch::PerformanceTest
-
-* Deprecated ActionController::AbstractRequest in favour of ActionDispatch::Request
-
-* Deprecated ActionController::Request in favour of ActionDispatch::Request
-
-* Deprecated ActionController::AbstractResponse in favour of ActionDispatch::Response
-
-* Deprecated ActionController::Response in favour of ActionDispatch::Response
-
-* Deprecated ActionController::Routing in favour of ActionDispatch::Routing
-
-* check_box helper with :disabled => true will generate a disabled hidden field to conform with the HTML convention where disabled fields are not submitted with the form.
- This is a behavior change, previously the hidden tag had a value of the disabled checkbox.
- *Tadas Tamosauskas*
-
-* `favicon_link_tag` helper will now use the favicon in app/assets by default. *Lucas Caton*
-
-* `ActionView::Helpers::TextHelper#highlight` now defaults to the
- HTML5 `mark` element. *Brian Cardarella*
-
-
-## Rails 3.2.2 (March 1, 2012) ##
-
-* Format lookup for partials is derived from the format in which the template is being rendered. Closes #5025 part 2 *Santiago Pastorino*
-
-* Use the right format when a partial is missing. Closes #5025. *Santiago Pastorino*
-
-* Default responder will now always use your overridden block in `respond_with` to render your response. *Prem Sichanugrist*
-
-* check_box helper with :disabled => true will generate a disabled hidden field to conform with the HTML convention where disabled fields are not submitted with the form.
- This is a behavior change, previously the hidden tag had a value of the disabled checkbox.
- *Tadas Tamosauskas*
-
-
-## Rails 3.2.1 (January 26, 2012) ##
-
-* Documentation improvements.
-
-* Allow `form.select` to accept ranges (regression). *Jeremy Walker*
-
-* `datetime_select` works with -/+ infinity dates. *Joe Van Dyk*
-
-
-## Rails 3.2.0 (January 20, 2012) ##
-
-* Add `config.action_dispatch.default_charset` to configure default charset for ActionDispatch::Response. *Carlos Antonio da Silva*
-
-* Deprecate setting default charset at controller level, use the new `config.action_dispatch.default_charset` instead. *Carlos Antonio da Silva*
-
-* Deprecate ActionController::UnknownAction in favour of AbstractController::ActionNotFound. *Carlos Antonio da Silva*
-
-* Deprecate ActionController::DoubleRenderError in favour of AbstractController::DoubleRenderError. *Carlos Antonio da Silva*
-
-* Deprecate method_missing handling for not found actions, use action_missing instead. *Carlos Antonio da Silva*
-
-* Deprecate ActionController#rescue_action, ActionController#initialize_template_class, and ActionController#assign_shortcuts.
- These methods were not being used internally anymore and are going to be removed in Rails 4. *Carlos Antonio da Silva*
-
-* Use a BodyProxy instead of including a Module that responds to
- close. Closes #4441 if Active Record is disabled assets are delivered
- correctly *Santiago Pastorino*
-
-* Rails initialization with initialize_on_precompile = false should set assets_dir *Santiago Pastorino*
-
-* Add font_path helper method *Santiago Pastorino*
-
-* Depends on rack ~> 1.4.0 *Santiago Pastorino*
-
-* Add :gzip option to `caches_page`. The default option can be configured globally using `page_cache_compression` *Andrey Sitnik*
-
-* The ShowExceptions middleware now accepts a exceptions application that is responsible to render an exception when the application fails. The application is invoked with a copy of the exception in `env["action_dispatch.exception"]` and with the PATH_INFO rewritten to the status code. *José Valim*
-
-* Add `button_tag` support to ActionView::Helpers::FormBuilder.
-
- This support mimics the default behavior of `submit_tag`.
-
- Example:
-
- <%= form_for @post do |f| %>
- <%= f.button %>
- <% end %>
-
-* Date helpers accept a new option, `:use_two_digit_numbers = true`, that renders select boxes for months and days with a leading zero without changing the respective values.
- For example, this is useful for displaying ISO8601-style dates such as '2011-08-01'. *Lennart Fridén and Kim Persson*
-
-* Make ActiveSupport::Benchmarkable a default module for ActionController::Base, so the #benchmark method is once again available in the controller context like it used to be *DHH*
-
-* Deprecated implied layout lookup in controllers whose parent had a explicit layout set:
-
- class ApplicationController
- layout "application"
- end
-
- class PostsController < ApplicationController
- end
-
- In the example above, Posts controller will no longer automatically look up for a posts layout.
-
- If you need this functionality you could either remove `layout "application"` from ApplicationController or explicitly set it to nil in PostsController. *José Valim*
-
-* Rails will now use your default layout (such as "layouts/application") when you specify a layout with `:only` and `:except` condition, and those conditions fail. *Prem Sichanugrist*
-
- For example, consider this snippet:
-
- class CarsController
- layout 'single_car', :only => :show
- end
-
- Rails will use 'layouts/single_car' when a request comes in `:show` action, and use 'layouts/application' (or 'layouts/cars', if exists) when a request comes in for any other actions.
-
-* form_for with +:as+ option uses "#{action}_#{as}" as css class and id:
-
- Before:
-
- form_for(@user, :as => 'client') # => "' from now on. *Rick Olson*
-
-* Added block-usage to PrototypeHelper#form_remote_tag, document block-usage of FormTagHelper#form_tag *Rick Olson*
-
-* Add a 0 margin/padding div around the hidden _method input tag that form_tag outputs. *Rick Olson*
-
-* Added block-usage to TagHelper#content_tag *DHH*. Example:
-
- <% content_tag :div, :class => "strong" %>
- Hello world!
- <% end %>
-
- Will output:
-
Hello world!
-
-* Deprecated UrlHelper#link_to_image and UrlHelper#link_to :post => true #6409 *Bob Silva*
-
-* Upgraded NumberHelper with number_to_phone support international formats to comply with ITU E.123 by supporting area codes with less than 3 digits, added precision argument to number_to_human_size (defaults to 1) #6421 *Bob Silva*
-
-* Fixed that setting RAILS_ASSET_ID to "" should not add a trailing slash after assets #6454 *Bob Silva/chrismear*
-
-* Force *_url named routes to show the host in ActionView *Rick Olson*
-
- <%= url_for ... %> # no host
- <%= foo_path %> # no host
- <%= foo_url %> # host!
-
-* Add support for converting blocks into function arguments to JavaScriptGenerator#call and JavaScriptProxy#call. *Sam Stephenson*
-
-* Add JavaScriptGenerator#literal for wrapping a string in an object whose #to_json is the string itself. *Sam Stephenson*
-
-* Add <%= escape_once html %> to escape html while leaving any currently escaped entities alone. Fix button_to double-escaping issue. *Rick Olson*
-
-* Fix double-escaped entities, such as &, {, etc. *Rick Olson*
-
-* Fix routing to correctly determine when generation fails. Closes #6300. *psross*.
-
-* Fix broken assert_generates when extra keys are being checked. *Jamis Buck*
-
-* Replace KCODE checks with String#chars for truncate. Closes #6385 *Manfred Stienstra*
-
-* Make page caching respect the format of the resource that is being requested even if the current route is the default route so that, e.g. posts.rss is not transformed by url_for to '/' and subsequently cached as '/index.html' when it should be cached as '/posts.rss'. *Marcel Molina Jr.*
-
-* Use String#chars in TextHelper::excerpt. Closes #6386 *Manfred Stienstra*
-
-* Fix relative URL root matching problems. *Mark Imbriaco*
-
-* Fix filter skipping in controller subclasses. #5949, #6297, #6299 *Martin Emde*
-
-* render_text may optionally append to the response body. render_javascript appends by default. This allows you to chain multiple render :update calls by setting @performed_render = false between them (awaiting a better public API). *Jeremy Kemper*
-
-* Rename test assertion to prevent shadowing. Closes #6306. *psross*
-
-* Fixed that NumberHelper#number_to_delimiter should respect precision of higher than two digits #6231 *Philip Hallstrom*
-
-* Fixed that FormHelper#radio_button didn't respect an :id being passed in #6266 *evansj*
-
-* Added an html_options hash parameter to javascript_tag() and update_page_tag() helpers #6311 *tzaharia*. Example:
-
- update_page_tag :defer => 'true' { |page| ... }
-
- Gives:
-
-
-
- Which is needed for dealing with the IE6 DOM when it's not yet fully loaded.
-
-* Fixed that rescue template path shouldn't be hardcoded, then it's easier to hook in your own #6295 *Mike Naberezny*
-
-* Fixed escaping of backslashes in JavaScriptHelper#escape_javascript #6302 *sven@c3d2.de*
-
-* Fixed that some 500 rescues would cause 500's themselves because the response had not yet been generated #6329 *cmselmer*
-
-* respond_to :html doesn't assume .rhtml. #6281 *Hampton Catlin*
-
-* Fixed some deprecation warnings in ActionPack *Rick Olson*
-
-* assert_select_rjs decodes escaped unicode chars since the Javascript generators encode them. #6240 *japgolly*
-
-* Deprecation: @cookies, @headers, @request, @response will be removed after 1.2. Use the corresponding method instead. *Jeremy Kemper*
-
-* Make the :status parameter expand to the default message for that status code if it is an integer. Also support symbol statuses. *Jamis Buck*. Examples:
-
- head :status => 404 # expands to "404 Not Found"
- head :status => :not_found # expands to "404 Not Found"
- head :status => :created # expands to "201 Created"
-
-* Add head(options = {}) for responses that have no body. *Jamis Buck*. Examples:
-
- head :status => 404 # return an empty response with a 404 status
- head :location => person_path(@person), :status => 201
-
-* Fix bug that kept any before_filter except the first one from being able to halt the before_filter chain. *Rick Olson*
-
-* strip_links is case-insensitive. #6285 *tagoh, Bob Silva*
-
-* Clear the cache of possible controllers whenever Routes are reloaded. *Nicholas Seckar*
-
-* Filters overhaul including meantime filter support using around filters + blocks. #5949 *Martin Emde, Roman Le Negrate, Stefan Kaes, Jeremy Kemper*
-
-* Update CGI process to allow sessions to contain namespaced models. Closes #4638. *dfelstead@site5.com*
-
-* Fix routing to respect user provided requirements and defaults when assigning default routing options (such as :action => 'index'). Closes #5950. *Nicholas Seckar*
-
-* Rescue Errno::ECONNRESET to handle an unexpectedly closed socket connection. Improves SCGI reliability. #3368, #6226 *sdsykes, fhanshaw@vesaria.com*
-
-* Added that respond_to blocks will automatically set the content type to be the same as is requested *DHH*. Examples:
-
- respond_to do |format|
- format.html { render :text => "I'm being sent as text/html" }
- format.rss { render :text => "I'm being sent as application/rss+xml" }
- format.atom { render :text => "I'm being sent as application/xml", :content_type => Mime::XML }
- end
-
-* Added utf-8 as the default charset for all renders. You can change this default using ActionController::Base.default_charset=(encoding) *David Heinemeier Hansson*
-
-* Added proper getters and setters for content type and charset *DHH*. Example of what we used to do:
-
- response.headers["Content-Type"] = "application/atom+xml; charset=utf-8"
-
- ...now:
-
- response.content_type = Mime::ATOM
- response.charset = "utf-8"
-
-* Declare file extensions exempt from layouts. #6219 *brandon*
- Example: ActionController::Base.exempt_from_layout 'rpdf'
-
-* Add chained replace/update support for assert_select_rjs *Rick Olson*
-
- Given RJS like...
-
- page['test1'].replace "
foo
"
- page['test2'].replace_html "
foo
"
-
- Test it with...
-
- assert_select_rjs :chained_replace
- assert_select_rjs :chained_replace, "test1"
-
- assert_select_rjs :chained_replace_html
- assert_select_rjs :chained_replace_html, "test2"
-
-* Load helpers in alphabetical order for consistency. Resolve cyclic javascript_helper dependency. #6132, #6178 *choonkeat@gmail.com*
-
-* Skip params with empty names, such as the &=Save query string from . #2569 *Manfred Stienstra, raphinou@yahoo.com*
-
-* Fix assert_tag so that :content => "foo" does not match substrings, but only exact strings. Use :content => /foo/ to match substrings. #2799 *Eric Hodel*
-
-* Update JavaScriptGenerator#show/hide/toggle/remove to new Prototype syntax for multiple ids, #6068 *petermichaux@gmail.com*
-
-* Update UrlWriter to support :only_path. *Nicholas Seckar, Dave Thomas*
-
-* Fixed JavaScriptHelper#link_to_function and JavaScriptHelper#button_to_function to have the script argument be optional *DHH*. So what used to require a nil, like this:
-
- link_to("Hider", nil, :class => "hider_link") { |p| p[:something].hide }
-
- ...can be written like this:
-
- link_to("Hider", :class => "hider_link") { |p| p[:something].hide }
-
-* Added access to nested attributes in RJS #4548 *richcollins@gmail.com*. Examples:
-
- page['foo']['style'] # => $('foo').style;
- page['foo']['style']['color'] # => $('blank_slate').style.color;
- page['foo']['style']['color'] = 'red' # => $('blank_slate').style.color = 'red';
- page['foo']['style'].color = 'red' # => $('blank_slate').style.color = 'red';
-
-* Fixed that AssetTagHelper#image_tag and others using compute_public_path should not modify the incoming source argument (closes #5102) *eule@space.ch*
-
-* Deprecated the auto-appending of .png to AssetTagHelper#image_tag calls that doesn't have an extension *David Heinemeier Hansson*
-
-* Fixed FormOptionsHelper#select to respect :selected value #5813
-
-* Fixed TextHelper#simple_format to deal with multiple single returns within a single paragraph #5835 *moriq@moriq.com*
-
-* Fixed TextHelper#pluralize to handle 1 as a string #5909 *rails@bencurtis.com*
-
-* Improved resolution of DateHelper#distance_of_time_in_words for better precision #5994 *Bob Silva*
-
-* Changed that uncaught exceptions raised any where in the application will cause RAILS_ROOT/public/500.html to be read and shown instead of just the static "Application error (Rails)" *David Heinemeier Hansson*
-
-* Added deprecation language for pagination which will become a plugin by Rails 2.0 *David Heinemeier Hansson*
-
-* Added deprecation language for in_place_editor and auto_complete_field that both pieces will become plugins by Rails 2.0 *David Heinemeier Hansson*
-
-* Deprecated all of ActionController::Dependencies. All dependency loading is now handled from Active Support *David Heinemeier Hansson*
-
-* Added assert_select* for CSS selector-based testing (deprecates assert_tag) #5936 *assaf.arkin@gmail.com*
-
-* radio_button_tag generates unique id attributes. #3353 *Bob Silva, somekool@gmail.com*
-
-* strip_tags passes through blank args such as nil or "". #2229, #6702 *duncan@whomwah.com, dharana*
-
-* Cleanup assert_tag :children counting. #2181 *jamie@bravenet.com*
-
-* button_to accepts :method so you can PUT and DELETE with it. #6005 *Dan Webb*
-
-* Update sanitize text helper to strip plaintext tags, and . *Rick Olson*
-
-* Add routing tests to assert that RoutingError is raised when conditions aren't met. Closes #6016 *Nathan Witmer*
-
-* Make auto_link parse a greater subset of valid url formats. *Jamis Buck*
-
-* Integration tests: headers beginning with X aren't excluded from the HTTP_ prefix, so X-Requested-With becomes HTTP_X_REQUESTED_WITH as expected. *Mike Clark*
-
-* Switch to using FormEncodedPairParser for parsing request parameters. *Nicholas Seckar, David Heinemeier Hansson*
-
-* respond_to .html now always renders #{action_name}.rhtml so that registered custom template handlers do not override it in priority. Custom mime types require a block and throw proper error now. *Tobias Lütke*
-
-* Deprecation: test deprecated instance vars in partials. *Jeremy Kemper*
-
-* Add UrlWriter to allow writing urls from Mailers and scripts. *Nicholas Seckar*
-
-* Relax Routing's anchor pattern warning; it was preventing use of [^/] inside restrictions. *Nicholas Seckar*
-
-* Add controller_paths variable to Routing. *Nicholas Seckar*
-
-* Fix assert_redirected_to issue with named routes for module controllers. *Rick Olson*
-
-* Tweak RoutingError message to show option diffs, not just missing named route significant keys. *Rick Olson*
-
-* Invoke method_missing directly on hidden actions. Closes #3030. *Nicholas Seckar*
-
-* Add RoutingError exception when RouteSet fails to generate a path from a Named Route. *Rick Olson*
-
-* Replace Reloadable with Reloadable::Deprecated. *Nicholas Seckar*
-
-* Deprecation: check whether instance variables have been monkeyed with before assigning them to deprecation proxies. Raises a RuntimeError if so. *Jeremy Kemper*
-
-* Add support for the param_name parameter to the auto_complete_field helper. #5026 *david.a.williams@gmail.com*
-
-* Deprecation! @params, @session, @flash will be removed after 1.2. Use the corresponding instance methods instead. You'll get printed warnings during tests and logged warnings in dev mode when you access either instance variable directly. *Jeremy Kemper*
-
-* Make Routing noisy when an anchor regexp is assigned to a segment. #5674 *François Beausoleil*
-
-* Added months and years to the resolution of DateHelper#distance_of_time_in_words, such that "60 days ago" becomes "2 months ago" #5611 *pjhyett@gmail.com*
-
-* Make controller_path available as an instance method. #5724 *jmckible@gmail.com*
-
-* Update query parser to support adjacent hashes. *Nicholas Seckar*
-
-* Make action caching aware of different formats for the same action so that, e.g. foo.xml is cached separately from foo.html. Implicitly set content type when reading in cached content with mime revealing extensions so the entire onous isn't on the webserver. *Marcel Molina Jr.*
-
-* Restrict Request Method hacking with ?_method to POST requests. *Rick Olson*
-
-* Fixed the new_#{resource}_url route and added named route tests for Simply Restful. *Rick Olson*
-
-* Added map.resources from the Simply Restful plugin *DHH*. Examples (the API has changed to use plurals!):
-
- map.resources :messages
- map.resources :messages, :comments
- map.resources :messages, :new => { :preview => :post }
-
-* Fixed that integration simulation of XHRs should set Accept header as well *Edward Frederick*
-
-* TestRequest#reset_session should restore a TestSession, not a hash *Michael Koziarski*
-
-* Don't search a load-path of '.' for controller files *Jamis Buck*
-
-* Update integration.rb to require test_process explicitly instead of via Dependencies. *Nicholas Seckar*
-
-* Fixed that you can still access the flash after the flash has been reset in reset_session. Closes #5584 *lmarlow*
-
-* Allow form_for and fields_for to work with indexed form inputs. *Jeremy Kemper, Matt Lyon*
-
- <% form_for 'post[]', @post do |f| -%>
- <% end -%>
-
-* Remove leak in development mode by replacing define_method with module_eval. *Nicholas Seckar*
-
-* Provide support for decimal columns to form helpers. Closes #5672. *Dave Thomas*
-
-* Pass :id => nil or :class => nil to error_messages_for to supress that html attribute. #3586 *olivier_ansaldi@yahoo.com*
-
-* Reset @html_document between requests so assert_tag works. #4810 *Jarkko Laine, easleydp@gmail.com*
-
-* Integration tests behave well with render_component. #4632 *edward.frederick@revolution.com, dev.rubyonrails@maxdunn.com*
-
-* Added exception handling of missing layouts #5373 *chris@ozmm.org*
-
-* Fixed that real files and symlinks should be treated the same when compiling templates #5438 *zachary@panandscan.com*
-
-* Fixed that the flash should be reset when reset_session is called #5584 *Shugo Maeda*
-
-* Added special case for "1 Byte" in NumberHelper#number_to_human_size #5593 *murpyh@rubychan.de*
-
-* Fixed proper form-encoded parameter parsing for requests with "Content-Type: application/x-www-form-urlencoded; charset=utf-8" (note the presence of a charset directive) *David Heinemeier Hansson*
-
-* Add route_name_path method to generate only the path for a named routes. For example, map.person will add person_path. *Nicholas Seckar*
-
-* Avoid naming collision among compiled view methods. *Jeremy Kemper*
-
-* Fix CGI extensions when they expect string but get nil in Windows. Closes #5276 *Mislav Marohnić*
-
-* Determine the correct template_root for deeply nested components. #2841 *s.brink@web.de*
-
-* Fix that routes with *path segments in the recall can generate URLs. *Rick Olson*
-
-* Fix strip_links so that it doesn't hang on multiline tags *Jamis Buck*
-
-* Remove problematic control chars in rescue template. #5316 *Stefan Kaes*
-
-* Make sure passed routing options are not mutated by routing code. #5314 *Blair Zajac*
-
-* Make sure changing the controller from foo/bar to bing/bang does not change relative to foo. *Jamis Buck*
-
-* Escape the path before routing recognition. #3671
-
-* Make sure :id and friends are unescaped properly. #5275 *me@julik.nl*
-
-* Rewind readable CGI params so others may reread them (such as CGI::Session when passing the session id in a multipart form). #210 *mklame@atxeu.com, matthew@walker.wattle.id.au*
-
-* Added Mime::TEXT (text/plain) and Mime::ICS (text/calendar) as new default types *David Heinemeier Hansson*
-
-* Added Mime::Type.register(string, symbol, synonyms = []) for adding new custom mime types *DHH*. Example: Mime::Type.register("image/gif", :gif)
-
-* Added support for Mime objects in render :content_type option *DHH*. Example: render :text => some_atom, :content_type => Mime::ATOM
-
-* Add :status option to send_data and send_file. Defaults to '200 OK'. #5243 *Manfred Stienstra *
-
-* Routing rewrite. Simpler, faster, easier to understand. The published API for config/routes.rb is unchanged, but nearly everything else is different, so expect breakage in plugins and libs that try to fiddle with routes. *Nicholas Seckar, Jamis Buck*
-
- map.connect '/foo/:id', :controller => '...', :action => '...'
- map.connect '/foo/:id.:format', :controller => '...', :action => '...'
- map.connect '/foo/:id', ..., :conditions => { :method => :get }
-
-* Cope with missing content type and length headers. Parse parameters from multipart and urlencoded request bodies only. *Jeremy Kemper*
-
-* Accept multipart PUT parameters. #5235 *guy.naor@famundo.com*
-
-* Added interrogation of params[:format] to determine Accept type. If :format is specified and matches a declared extension, like "rss" or "xml", that mime type will be put in front of the accept handler. This means you can link to the same action from different extensions and use that fact to determine output *DHH*. Example:
-
- class WeblogController < ActionController::Base
- def index
- @posts = Post.find :all
-
- respond_to do |format|
- format.html
- format.xml { render :xml => @posts.to_xml }
- format.rss { render :action => "feed.rxml" }
- end
- end
- end
-
- \# returns HTML when requested by a browser, since the browser
- \# has the HTML mimetype at the top of its priority list
- Accept: text/html
- GET /weblog
-
- \# returns the XML
- Accept: application/xml
- GET /weblog
-
- \# returns the HTML
- Accept: application/xml
- GET /weblog.html
-
- \# returns the XML
- Accept: text/html
- GET /weblog.xml
-
- All this relies on the fact that you have a route that includes .:format.
-
-* Expanded :method option in FormTagHelper#form_tag, FormHelper#form_for, PrototypeHelper#remote_form_for, PrototypeHelper#remote_form_tag, and PrototypeHelper#link_to_remote to allow for verbs other than GET and POST by automatically creating a hidden form field named _method, which will simulate the other verbs over post *David Heinemeier Hansson*
-
-* Added :method option to UrlHelper#link_to, which allows for using other verbs than GET for the link. This replaces the :post option, which is now deprecated. Example: link_to "Destroy", person_url(http://github.com/:id%20=%3E%20person), :method => :delete *David Heinemeier Hansson*
-
-* follow_redirect doesn't complain about being redirected to the same controller. #5153 *dymo@mk.ukrtelecom.ua*
-
-* Add layout attribute to response object with the name of the layout that was rendered, or nil if none rendered. *Kevin Clark*
-
-* Fix NoMethodError when parsing params like &&. *Adam Greenfield*
-
-* form.text_area handles the :size option just like the original text_area (:size => '60x10' becomes cols="60" rows="10"). *Jeremy Kemper*
-
-* Excise ingrown code from FormOptionsHelper#options_for_select. #5008 *anonymous*
-
-* Small fix in routing to allow dynamic routes (broken after [4242]) *Rick Olson*
-
- map.connect '*path', :controller => 'files', :action => 'show'
-
-* Use #flush between switching from #write to #syswrite. Closes #4907. *Blair Zajac *
-
-* Allow error_messages_for to report errors for multiple objects, as well as support for customizing the name of the object in the error summary header. Closes #4186. *andrew@redlinesoftware.com, Marcel Molina Jr.*
-
- error_messages_for :account, :user, :subscription, :object_name => :account
-
-* Fix assert_redirected_to tests according to real-world usage. Also, don't fail if you add an extra :controller option: *Rick Olson*
-
- redirect_to :action => 'new'
- assert_redirected_to :controller => 'monkeys', :action => 'new'
-
-* Diff compared routing options. Allow #assert_recognizes to take a second arg as a hash to specify optional request method *Rick Olson*
-
- assert_recognizes({:controller => 'users', :action => 'index'}, 'users')
- assert_recognizes({:controller => 'users', :action => 'create'}, {:path => 'users', :method => :post})
-
-* Diff compared options with #assert_redirected_to *Rick Olson*
-
-* Add support in routes for semicolon delimited "subpaths", like /books/:id;:action *Jamis Buck*
-
-* Change link_to_function and button_to_function to (optionally) take an update_page block instead of a JavaScript string. Closes #4804. *zraii@comcast.net, Sam Stephenson*
-
-* Modify routing so that you can say :require => { :method => :post } for a route, and the route will never be selected unless the request method is POST. Only works for route recognition, not for route generation. *Jamis Buck*
-
-* Added :add_headers option to verify which merges a hash of name/value pairs into the response's headers hash if the prerequisites cannot be satisfied. *Sam Stephenson*
- ex. verify :only => :speak, :method => :post,
- :render => { :status => 405, :text => "Must be post" },
- :add_headers => { "Allow" => "POST" }
-
-
-## 1.12.5 (August 10th, 2006) ##
-
-* Updated security fix
-
-
-## 1.12.4 (August 8th, 2006) ##
-
-* Cache CgiRequest#request_parameters so that multiple calls don't re-parse multipart data. *Rick Olson*
-
-* Fixed that remote_form_for can leave out the object parameter and default to the instance variable of the object_name, just like form_for *David Heinemeier Hansson*
-
-* Added ActionController.filter_parameter_logging that makes it easy to remove passwords, credit card numbers, and other sensitive information from being logged when a request is handled. #1897 *jeremye@bsa.ca.gov*
-
-* Fixed that real files and symlinks should be treated the same when compiling templates. #5438 *zachary@panandscan.com*
-
-* Add :status option to send_data and send_file. Defaults to '200 OK'. #5243 *Manfred Stienstra *
-
-* Update documentation for erb trim syntax. #5651 *matt@mattmargolis.net*
-
-* Short documentation to mention use of Mime::Type.register. #5710 *choonkeat@gmail.com*
-
-
-## 1.12.3 (June 28th, 2006) ##
-
-* Fix broken traverse_to_controller. We now:
- Look for a _controller.rb file under RAILS_ROOT to load.
- If we find it, we require_dependency it and return the controller it defined. (If none was defined we stop looking.)
- If we don't find it, we look for a .rb file under RAILS_ROOT to load. If we find it, and it loads a constant we keep looking.
- Otherwise we check to see if a directory of the same name exists, and if it does we create a module for it.
-
-
-## 1.12.2 (June 27th, 2006) ##
-
-* Refinement to avoid exceptions in traverse_to_controller.
-
-* (Hackish) Fix loading of arbitrary files in Ruby's load path by traverse_to_controller. *Nicholas Seckar*
-
-
-## 1.12.1 (April 6th, 2006) ##
-
-* Fixed that template extensions would be cached development mode #4624 *Stefan Kaes*
-
-* Update to Prototype 1.5.0_rc0 *Sam Stephenson*
-
-* Honor skipping filters conditionally for only certain actions even when the parent class sets that filter to conditionally be executed only for the same actions. #4522 *Marcel Molina Jr.*
-
-* Delegate xml_http_request in integration tests to the session instance. *Jamis Buck*
-
-* Update the diagnostics template skip the useless '' text. *Nicholas Seckar*
-
-* CHANGED DEFAULT: Don't parse YAML input by default, but keep it available as an easy option *David Heinemeier Hansson*
-
-* Add additional autocompleter options *aballai, Thomas Fuchs*
-
-* Fixed fragment caching of binary data on Windows #4493 *bellis@deepthought.org*
-
-* Applied Prototype $() performance patches (#4465, #4477) and updated script.aculo.us *Sam Stephenson, Thomas Fuchs*
-
-* Added automated timestamping to AssetTagHelper methods for stylesheets, javascripts, and images when Action Controller is run under Rails *DHH*. Example:
-
- image_tag("rails.png") # => ''
-
- ...to avoid frequent stats (not a problem for most people), you can set RAILS_ASSET_ID in the ENV to avoid stats:
-
- ENV["RAILS_ASSET_ID"] = "2345"
- image_tag("rails.png") # => ''
-
- This can be used by deployment managers to set the asset id by application revision
-
-
-## 1.12.0 (March 27th, 2006) ##
-
-* Add documentation for respond_to. *Jamis Buck*
-
-* Fixed require of bluecloth and redcloth when gems haven't been loaded #4446 *murphy@cYcnus.de*
-
-* Update to Prototype 1.5.0_pre1 *Sam Stephenson*
-
-* Change #form_for and #fields_for so that the second argument is not required *Dave Thomas*
-
- <% form_for :post, @post, :url => { :action => 'create' } do |f| -%>
-
- becomes...
-
- <% form_for :post, :url => { :action => 'create' } do |f| -%>
-
-* Update to script.aculo.us 1.6 *Thomas Fuchs*
-
-* Enable application/x-yaml processing by default *Jamis Buck*
-
-* Fix double url escaping of remote_function. Add :escape => false option to ActionView's url_for. *Nicholas Seckar*
-
-* Add :script option to in_place_editor to support evalScripts (closes #4194) *Cody Fauser*
-
-* Fix mixed case enumerable methods in the JavaScript Collection Proxy (closes #4314) *Cody Fauser*
-
-* Undo accidental escaping for mail_to; add regression test. *Nicholas Seckar*
-
-* Added nicer message for assert_redirected_to (closes #4294) *court3nay*
-
- assert_redirected_to :action => 'other_host', :only_path => false
-
- when it was expecting...
-
- redirected_to :action => 'other_host', :only_path => true, :host => 'other.test.host'
-
- gives the error message...
-
- response is not a redirection to all of the options supplied (redirection is <{:only_path=>false, :host=>"other.test.host", :action=>"other_host"}>), difference: <{:only_path=>"true", :host=>"other.test.host"}>
-
-* Change url_for to escape the resulting URLs when called from a view. *Nicholas Seckar, coffee2code*
-
-* Added easy support for testing file uploads with fixture_file_upload #4105 *turnip@turnipspatch.com*. Example:
-
- # Looks in Test::Unit::TestCase.fixture_path + '/files/spongebob.png'
- post :change_avatar, :avatar => fixture_file_upload('/files/spongebob.png', 'image/png')
-
-* Fixed UrlHelper#current_page? to behave even when url-escaped entities are present #3929 *jeremy@planetargon.com*
-
-* Add ability for relative_url_root to be specified via an environment variable RAILS_RELATIVE_URL_ROOT. *isaac@reuben.com, Nicholas Seckar*
-
-* Fixed link_to "somewhere", :post => true to produce valid XHTML by using the parentnode instead of document.body for the instant form #3007 *Bob Silva*
-
-* Added :function option to PrototypeHelper#observe_field/observe_form that allows you to call a function instead of submitting an ajax call as the trigger #4268 *jonathan@daikini.com*
-
-* Make Mime::Type.parse consider q values (if any) *Jamis Buck*
-
-* XML-formatted requests are typecast according to "type" attributes for :xml_simple *Jamis Buck*
-
-* Added protection against proxy setups treating requests as local even when they're not #3898 *Steve Purcell*
-
-* Added TestRequest#raw_post that simulate raw_post from CgiRequest #3042 *François Beausoleil*
-
-* Underscore dasherized keys in formatted requests *Jamis Buck*
-
-* Add MimeResponds::Responder#any for managing multiple types with identical responses *Jamis Buck*
-
-* Make the xml_http_request testing method set the HTTP_ACCEPT header *Jamis Buck*
-
-* Add Verification to scaffolds. Prevent destructive actions using GET *Michael Koziarski*
-
-* Avoid hitting the filesystem when using layouts by using a File.directory? cache. *Stefan Kaes, Nicholas Seckar*
-
-* Simplify ActionController::Base#controller_path *Nicholas Seckar*
-
-* Added simple alert() notifications for RJS exceptions when config.action_view.debug_rjs = true. *Sam Stephenson*
-
-* Added :content_type option to render, so you can change the content type on the fly *DHH*. Example: render :action => "atom.rxml", :content_type => "application/atom+xml"
-
-* CHANGED DEFAULT: The default content type for .rxml is now application/xml instead of type/xml, see http://www.xml.com/pub/a/2004/07/21/dive.html for reason *David Heinemeier Hansson*
-
-* Added option to render action/template/file of a specific extension (and here by template type). This means you can have multiple templates with the same name but a different extension *DHH*. Example:
-
- class WeblogController < ActionController::Base
- def index
- @posts = Post.find :all
-
- respond_to do |type|
- type.html # using defaults, which will render weblog/index.rhtml
- type.xml { render :action => "index.rxml" }
- type.js { render :action => "index.rjs" }
- end
- end
- end
-
-* Added better support for using the same actions to output for different sources depending on the Accept header *DHH*. Example:
-
- class WeblogController < ActionController::Base
- def create
- @post = Post.create(params[:post])
-
- respond_to do |type|
- type.js { render } # renders create.rjs
- type.html { redirect_to :action => "index" }
- type.xml do
- headers["Location"] = url_for(:action => "show", :id => @post)
- render(:nothing, :status => "201 Created")
- end
- end
- end
- end
-
-* Added Base#render(:xml => xml) that works just like Base#render(:text => text), but sets the content-type to text/xml and the charset to UTF-8 *David Heinemeier Hansson*
-
-* Integration test's url_for now runs in the context of the last request (if any) so after post /products/show/1 url_for :action => 'new' will yield /product/new *Tobias Lütke*
-
-* Re-added mixed-in helper methods for the JavascriptGenerator. Moved JavascriptGenerators methods to a module that is mixed in after the helpers are added. Also fixed that variables set in the enumeration methods like #collect are set correctly. Documentation added for the enumeration methods *Rick Olson*. Examples:
-
- page.select('#items li').collect('items') do |element|
- element.hide
- end
- # => var items = $$('#items li').collect(function(value, index) { return value.hide(); });
-
-* Added plugin support for parameter parsers, which allows for better support for REST web services. By default, posts submitted with the application/xml content type is handled by creating a XmlSimple hash with the same name as the root element of the submitted xml. More handlers can easily be registered like this:
-
- # Assign a new param parser to a new content type
- ActionController::Base.param_parsers['application/atom+xml'] = Proc.new do |data|
- node = REXML::Document.new(post)
- { node.root.name => node.root }
- end
-
- # Assign the default XmlSimple to a new content type
- ActionController::Base.param_parsers['application/backpack+xml'] = :xml_simple
-
- Default YAML web services were retired, ActionController::Base.param_parsers carries an example which shows how to get this functionality back. As part of this new plugin support, request.[formatted_post?, xml_post?, yaml_post? and post_format] were all deprecated in favor of request.content_type *Tobias Lütke*
-* Fixed Effect.Appear in effects.js to work with floats in Safari #3524, #3813, #3044 *Thomas Fuchs*
-
-* Fixed that default image extension was not appended when using a full URL with AssetTagHelper#image_tag #4032, #3728 *rubyonrails@beautifulpixel.com*
-
-* Added that page caching will only happen if the response code is less than 400 #4033 *g.bucher@teti.ch*
-
-* Add ActionController::IntegrationTest to allow high-level testing of the way the controllers and routes all work together *Jamis Buck*
-
-* Added support to AssetTagHelper#javascript_include_tag for having :defaults appear anywhere in the list, so you can now make one call ala javascript_include_tag(:defaults, "my_scripts") or javascript_include_tag("my_scripts", :defaults) depending on how you want the load order #3506 *Bob Silva*
-
-* Added support for visual effects scoped queues to the visual_effect helper #3530 *Abdur-Rahman Advany*
-
-* Added .rxml (and any non-rhtml template, really) supportfor CaptureHelper#content_for and CaptureHelper#capture #3287 *Brian Takita*
-
-* Added script.aculo.us drag and drop helpers to RJS *Thomas Fuchs*. Examples:
-
- page.draggable 'product-1'
- page.drop_receiving 'wastebasket', :url => { :action => 'delete' }
- page.sortable 'todolist', :url => { action => 'change_order' }
-
-* Fixed that form elements would strip the trailing [] from the first parameter #3545 *ruby@bobsilva.com*
-
-* During controller resolution, update the NameError suppression to check for the expected constant. *Nicholas Seckar*
-
-* Update script.aculo.us to V1.5.3 *Thomas Fuchs*
-
-* Added various InPlaceEditor options, #3746, #3891, #3896, #3906 *Bill Burcham, ruairi, sl33p3r*
-
-* Added :count option to pagination that'll make it possible for the ActiveRecord::Base.count call to using something else than * for the count. Especially important for count queries using DISTINCT #3839 *Stefan Kaes*
-
-* Update script.aculo.us to V1.5.2 *Thomas Fuchs*
-
-* Added element and collection proxies to RJS *DHH*. Examples:
-
- page['blank_slate'] # => $('blank_slate');
- page['blank_slate'].show # => $('blank_slate').show();
- page['blank_slate'].show('first').up # => $('blank_slate').show('first').up();
-
- page.select('p') # => $$('p');
- page.select('p.welcome b').first # => $$('p.welcome b').first();
- page.select('p.welcome b').first.hide # => $$('p.welcome b').first().hide();
-
-* Add JavaScriptGenerator#replace for replacing an element's "outer HTML". #3246 *tom@craz8.com, Sam Stephenson*
-
-* Remove over-engineered form_for code for a leaner implementation. *Nicholas Seckar*
-
-* Document form_for's :html option. *Nicholas Seckar*
-
-* Major components cleanup and speedup. #3527 *Stefan Kaes*
-
-* Fix problems with pagination and :include. *Kevin Clark*
-
-* Add ActiveRecordTestCase for testing AR integration. *Kevin Clark*
-
-* Add Unit Tests for pagination *Kevin Clark*
-
-* Add :html option for specifying form tag options in form_for. *Sam Stephenson*
-
-* Replace dubious controller parent class in filter docs. #3655, #3722 *info@rhalff.com, eigentone@gmail.com*
-
-* Don't interpret the :value option on text_area as an html attribute. Set the text_area's value. #3752 *gabriel@gironda.org*
-
-* Fix remote_form_for creates a non-ajax form. *Rick Olson*
-
-* Don't let arbitrary classes match as controllers -- a potentially dangerous bug. *Nicholas Seckar*
-
-* Fix Routing tests. Fix routing where failing to match a controller would prevent the rest of routes from being attempted. *Nicholas Seckar*
-
-* Add :builder => option to form_for and friends. *Nicholas Seckar, Rick Olson*
-
-* Fix controller resolution to avoid accidentally inheriting a controller from a parent module. *Nicholas Seckar*
-
-* Set sweeper's @controller to nil after a request so that the controller may be collected between requests. *Nicholas Seckar*
-
-* Subclasses of ActionController::Caching::Sweeper should be Reloadable. *Rick Olson*
-
-* Document the :xhr option for verifications. #3666 *leeo*
-
-* Added :only and :except controls to skip_before/after_filter just like for when you add filters *David Heinemeier Hansson*
-
-* Ensure that the instance variables are copied to the template when performing render :update. *Nicholas Seckar*
-
-* Add the ability to call JavaScriptGenerator methods from helpers called in update blocks. *Sam Stephenson* Example:
- module ApplicationHelper
- def update_time
- page.replace_html 'time', Time.now.to_s(:db)
- page.visual_effect :highlight, 'time'
- end
- end
-
- class UserController < ApplicationController
- def poll
- render :update { |page| page.update_time }
- end
- end
-
-* Add render(:update) to ActionView::Base. *Sam Stephenson*
-
-* Fix render(:update) to not render layouts. *Sam Stephenson*
-
-* Fixed that SSL would not correctly be detected when running lighttpd/fcgi behind lighttpd w/mod_proxy #3548 *Steve Purcell*
-
-* Added the possibility to specify atomatic expiration for the memcachd session container #3571 *Stefan Kaes*
-
-* Change layout discovery to take into account the change in semantics with File.join and nil arguments. *Marcel Molina Jr.*
-
-* Raise a RedirectBackError if redirect_to :back is called when there's no HTTP_REFERER defined #3049 *Kevin Clark*
-
-* Treat timestamps like datetimes for scaffolding purposes #3388 *Maik Schmidt*
-
-* Fix IE bug with link_to "something", :post => true #3443 *Justin Palmer*
-
-* Extract Test::Unit::TestCase test process behavior into an ActionController::TestProcess module. *Sam Stephenson*
-
-* Pass along blocks from render_to_string to render. *Sam Stephenson*
-
-* Add render :update for inline RJS. *Sam Stephenson* Example:
- class UserController < ApplicationController
- def refresh
- render :update do |page|
- page.replace_html 'user_list', :partial => 'user', :collection => @users
- page.visual_effect :highlight, 'user_list'
- end
- end
- end
-
-* allow nil objects for error_messages_for *Michael Koziarski*
-
-* Refactor human_size to exclude decimal place if it is zero. *Marcel Molina Jr.*
-
-* Update to Prototype 1.5.0_pre0 *Sam Stephenson*
-
-* Automatically discover layouts when a controller is namespaced. #2199, #3424 *me@jonnii.com rails@jeffcole.net Marcel Molina Jr.*
-
-* Add support for multiple proxy servers to CgiRequest#host *gaetanot@comcast.net*
-
-* Documentation typo fix. #2367 *Blair Zajac*
-
-* Remove Upload Progress. #2871 *Sean Treadway*
-
-* Fix typo in function name mapping in auto_complete_field. #2929 #3446 *doppler@gmail.com phil.ross@gmail.com*
-
-* Allow auto-discovery of third party template library layouts. *Marcel Molina Jr.*
-
-* Have the form builder output radio button, not check box, when calling the radio button helper. #3331 *LouisStAmour@gmail.com*
-
-* Added assignment of the Autocompleter object created by JavaScriptMacroHelper#auto_complete_field to a local javascript variables *David Heinemeier Hansson*
-
-* Added :on option for PrototypeHelper#observe_field that allows you to specify a different callback hook to have the observer trigger on *David Heinemeier Hansson*
-
-* Added JavaScriptHelper#button_to_function that works just like JavaScriptHelper#link_to_function but uses a button instead of a href *David Heinemeier Hansson*
-
-* Added that JavaScriptHelper#link_to_function will honor existing :onclick definitions when adding the function call *David Heinemeier Hansson*
-
-* Added :disable_with option to FormTagHelper#submit_tag to allow for easily disabled submit buttons with different text *David Heinemeier Hansson*
-
-* Make auto_link handle nil by returning quickly if blank? *Scott Barron*
-
-* Make auto_link match urls with a port number specified. *Marcel Molina Jr.*
-
-* Added support for toggling visual effects to ScriptaculousHelper::visual_effect, #3323. *Thomas Fuchs*
-
-* Update to script.aculo.us to 1.5.0 rev. 3343 *Thomas Fuchs*
-
-* Added :select option for JavaScriptMacroHelper#auto_complete_field that makes it easier to only use part of the auto-complete suggestion as the value for insertion *Thomas Fuchs*
-
-* Added delayed execution of Javascript from within RJS #3264 *devslashnull@gmail.com*. Example:
-
- page.delay(20) do
- page.visual_effect :fade, 'notice'
- end
-
-* Add session ID to default logging, but remove the verbose description of every step *David Heinemeier Hansson*
-
-* Add the following RJS methods: *Sam Stephenson*
-
- * alert - Displays an alert() dialog
- * redirect_to - Changes window.location.href to simulate a browser redirect
- * call - Calls a JavaScript function
- * assign - Assigns to a JavaScript variable
- * << - Inserts an arbitrary JavaScript string
-
-* Fix incorrect documentation for form_for *Nicholas Seckar*
-
-* Don't include a layout when rendering an rjs template using render's :template option. *Marcel Molina Jr.*
-
-## 1.1.2 (December 13th, 2005) ##
-
-* Become part of Rails 1.0
-
-* Update to script.aculo.us 1.5.0 final (equals 1.5.0_rc6) *Thomas Fuchs*
-
-* Update to Prototype 1.4.0 final *Sam Stephenson*
-
-* Added form_remote_for (form_for meets form_remote_tag) *David Heinemeier Hansson*
-
-* Update to script.aculo.us 1.5.0_rc6
-
-* More robust relative url root discovery for SCGI compatibility. This solves the 'SCGI routes problem' -- you no longer need to prefix all your routes with the name of the SCGI mountpoint. #3070 *Dave Ringoen*
-
-* Fix docs for text_area_tag. #3083. *Christopher Cotton*
-
-* Change form_for and fields_for method signatures to take object name and object as separate arguments rather than as a Hash. *David Heinemeier Hansson*
-
-* Introduce :selected option to the select helper. Allows you to specify a selection other than the current value of object.method. Specify :selected => nil to leave all options unselected. #2991 *Jonathan Viney *
-
-* Initialize @optional in routing code to avoid warnings about uninitialized access to an instance variable. *Nicholas Seckar*
-
-* Make ActionController's render honor the :locals option when rendering a :file. #1665. *Emanuel Borsboom, Marcel Molina Jr.*
-
-* Allow assert_tag(:conditions) to match the empty string when a tag has no children. Closes #2959. *Jamis Buck*
-
-* Update html-scanner to handle CDATA sections better. Closes #2970. *Jamis Buck*
-
-* Don't put flash in session if sessions are disabled. *Jeremy Kemper*
-
-* Strip out trailing &_= for raw post bodies. Closes #2868. *Sam Stephenson*
-
-* Pass multiple arguments to Element.show and Element.hide in JavaScriptGenerator instead of using iterators. *Sam Stephenson*
-
-* Improve expire_fragment documentation. #2966 *court3nay*
-
-* Correct docs for automatic layout assignment. #2610. *Charles M. Gerungan*
-
-* Always create new AR sessions rather than trying too hard to avoid database traffic. #2731 *Jeremy Kemper*
-
-* Update to Prototype 1.4.0_rc4. Closes #2943 (old Array.prototype.reverse behavior can be obtained by passing false as an argument). *Sam Stephenson*
-
-* Use Element.update('id', 'html') instead of $('id').innerHTML = 'html' in JavaScriptGenerator#replace_html so that script tags are evaluated. *Sam Stephenson*
-
-* Make rjs templates always implicitly skip out on layouts. *Marcel Molina Jr.*
-
-* Correct length for the truncate text helper. #2913 *Stefan Kaes*
-
-* Update to Prototype 1.4.0_rc3. Closes #1893, #2505, #2550, #2748, #2783. *Sam Stephenson*
-
-* Add support for new rjs templates which wrap an update_page block. *Marcel Molina Jr.*
-
-* Rename Version constant to VERSION. #2802 *Marcel Molina Jr.*
-
-* Correct time_zone_options_for_select docs. #2892 *pudeyo@rpi.com*
-
-* Remove the unused, slow response_dump and session_dump variables from error pages. #1222 *lmarlow*
-
-* Performance tweaks: use Set instead of Array to speed up prototype helper include? calls. Avoid logging code if logger is nil. Inline commonly-called template presence checks. #2880, #2881, #2882, #2883 *Stefan Kaes*
-
-* MemCache store may be given multiple addresses. #2869 *Ryan Carver *
-
-* Handle cookie parsing irregularity for certain Nokia phones. #2530 *zaitzow@gmail.com*
-
-* Added PrototypeHelper::JavaScriptGenerator and PrototypeHelper#update_page for easily modifying multiple elements in an Ajax response. *Sam Stephenson* Example:
-
- update_page do |page|
- page.insert_html :bottom, 'list', '
Last item
'
- page.visual_effect :highlight, 'list'
- page.hide 'status-indicator', 'cancel-link'
- end
-
- generates the following JavaScript:
-
- new Insertion.Bottom("list", "
Last item
");
- new Effect.Highlight("list");
- ["status-indicator", "cancel-link"].each(Element.hide);
-
-* Refactored JavaScriptHelper into PrototypeHelper and ScriptaculousHelper *Sam Stephenson*
-
-* Update to latest script.aculo.us version (as of [3031])
-
-* Updated docs for in_place_editor, fixes a couple bugs and offers extended support for external controls *Justin Palmer*
-
-* Update documentation for render :file. #2858 *Tom Werner*
-
-* Only include builtin filters whose filenames match /^[a-z][a-z_]*_helper.rb$/ to avoid including operating system metadata such as ._foo_helper.rb. #2855 *court3nay*
-
-* Added FormHelper#form_for and FormHelper#fields_for that makes it easier to work with forms for single objects also if they don't reside in instance variables *DHH*. Examples:
-
- <% form_for :person, @person, :url => { :action => "update" } do |f| %>
- First name: <%= f.text_field :first_name %>
- Last name : <%= f.text_field :last_name %>
- Biography : <%= f.text_area :biography %>
- Admin? : <%= f.check_box :admin %>
- <% end %>
-
- <% form_for :person, person, :url => { :action => "update" } do |person_form| %>
- First name: <%= person_form.text_field :first_name %>
- Last name : <%= person_form.text_field :last_name %>
-
- <% fields_for :permission => person.permission do |permission_fields| %>
- Admin? : <%= permission_fields.check_box :admin %>
- <% end %>
- <% end %>
-
-* options_for_select allows any objects which respond_to? :first and :last rather than restricting to Array and Range. #2824 *Jacob Robbins , Jeremy Kemper*
-
-* The auto_link text helper accepts an optional block to format the link text for each url and email address. Example: auto_link(post.body) { |text| truncate(text, 10) } *Jeremy Kemper*
-
-* assert_tag uses exact matches for string conditions, instead of partial matches. Use regex to do partial matches. #2799 *Jamis Buck*
-
-* CGI::Session::ActiveRecordStore.data_column_name = 'foobar' to use a different session data column than the 'data' default. *nbpwie102@sneakemail.com*
-
-* Do not raise an exception when default helper is missing; log a debug message instead. It's nice to delete empty helpers. *Jeremy Kemper*
-
-* Controllers with acronyms in their names (e.g. PDFController) require the correct default helper (PDFHelper in file pdf_helper.rb). #2262 *jeff@opendbms.com*
-
-
-## 1.11.0 (November 7th, 2005) ##
-
-* Added request as instance method to views, so you can do <%= request.env["HTTP_REFERER"] %>, just like you can already access response, session, and the likes *David Heinemeier Hansson*
-
-* Fix conflict with assert_tag and Glue gem #2255 *david.felstead@gmail.com*
-
-* Add documentation to assert_tag indicating that it only works with well-formed XHTML #1937, #2570 *Jamis Buck*
-
-* Added action_pack.rb stub so that ActionPack::Version loads properly *Sam Stephenson*
-
-* Added short-hand to assert_tag so assert_tag :tag => "span" can be written as assert_tag "span" *David Heinemeier Hansson*
-
-* Added skip_before_filter/skip_after_filter for easier control of the filter chain in inheritance hierachies *DHH*. Example:
-
- class ApplicationController < ActionController::Base
- before_filter :authenticate
- end
-
- class WeblogController < ApplicationController
- # will run the :authenticate filter
- end
-
- class SignupController < ActionController::Base
- # will not run the :authenticate filter
- skip_before_filter :authenticate
- end
-
-* Added redirect_to :back as a short-hand for redirect_to(request.env["HTTP_REFERER"]) *David Heinemeier Hansson*
-
-* Change javascript_include_tag :defaults to not use script.aculo.us loader, which facilitates the use of plugins for future script.aculo.us and third party javascript extensions, and provide register_javascript_include_default for plugins to specify additional JavaScript files to load. Removed slider.js and builder.js from actionpack. *Thomas Fuchs*
-
-* Fix problem where redirecting components can cause an infinite loop *Rick Olson*
-
-* Added support for the queue option on visual_effect *Thomas Fuchs*
-
-* Update script.aculo.us to V1.5_rc4 *Thomas Fuchs*
-
-* Fix that render :text didn't interpolate instance variables #2629, #2626 *Stefan Kaes*
-
-* Fix line number detection and escape RAILS_ROOT in backtrace Regexp *Nicholas Seckar*
-
-* Fixed document.getElementsByClassName from Prototype to be speedy again *Sam Stephenson*
-
-* Recognize ./#{RAILS_ROOT} as RAILS_ROOT in error traces *Nicholas Seckar*
-
-* Remove ARStore session fingerprinting *Nicholas Seckar*
-
-* Fix obscure bug in ARStore *Nicholas Seckar*
-
-* Added TextHelper#strip_tags for removing HTML tags from a string (using HTMLTokenizer) #2229 *marcin@junkheap.net*
-
-* Added a reader for flash.now, so it's possible to do stuff like flash.now[:alert] ||= 'New if not set' #2422 *Caio Chassot*
-
-
-## 1.10.2 (October 26th, 2005) ##
-
-* Reset template variables after using render_to_string *Stefan Kaes*
-
-* Expose the session model backing CGI::Session
-
-* Abbreviate RAILS_ROOT in traces
-
-
-## 1.10.1 (October 19th, 2005) ##
-
-* Update error trace templates *Nicholas Seckar*
-
-* Stop showing generated routing code in application traces *Nicholas Seckar*
-
-
-## 1.10.0 (October 16th, 2005) ##
-
-* Make string-keys locals assigns optional. Add documentation describing depreciated state *Stefan Kaes*
-
-* Improve line number detection for template errors *Nicholas Seckar*
-
-* Update/clean up documentation (rdoc)
-
-* Upgrade to Prototype 1.4.0_rc0 *Sam Stephenson*
-
-* Added assert_vaild. Reports the proper AR error messages as fail message when the passed record is invalid *Tobias Lütke*
-
-* Add temporary support for passing locals to render using string keys *Nicholas Seckar*
-
-* Clean up error pages by providing better backtraces *Nicholas Seckar*
-
-* Raise an exception if an attempt is made to insert more session data into the ActiveRecordStore data column than the column can hold. #2234. *justin@textdrive.com*
-
-* Removed references to assertions.rb from actionpack assert's backtraces. Makes error reports in functional unit tests much less noisy. *Tobias Lütke*
-
-* Updated and clarified documentation for JavaScriptHelper to be more concise about the various options for including the JavaScript libs. *Thomas Fuchs*
-
-* Hide "Retry with Breakpoint" button on error pages until feature is functional. *David Heinemeier Hansson*
-
-* Fix Request#host_with_port to use the standard port when Rails is behind a proxy. *Nicholas Seckar*
-
-* Escape query strings in the href attribute of URLs created by url_helper. #2333 *Michael Schuerig *
-
-* Improved line number reporting for template errors *Nicholas Seckar*
-
-* Added :locals support for render :inline #2463 *mdabney@cavoksolutions.com*
-
-* Unset the X-Requested-With header when using the xhr wrapper in functional tests so that future requests aren't accidentally xhr'ed #2352 *me@julik.nl, Sam Stephenson*
-
-* Unescape paths before writing cache to file system. #1877. *Damien Pollet*
-
-* Wrap javascript_tag contents in a CDATA section and add a cdata_section method to TagHelper #1691 *Michael Schuerig, Sam Stephenson*
-
-* Misc doc fixes (typos/grammar/etc). #2445. *coffee2code*
-
-* Speed improvement for session_options. #2287. *Stefan Kaes*
-
-* Make cacheing binary files friendly with Windows. #1975. *Rich Olson*
-
-* Convert boolean form options form the tag_helper. #809. *Michael Schuerig *
-
-* Fixed that an instance variable with the same name as a partial should be implicitly passed as the partial :object #2269 *court3nay*
-
-* Update Prototype to V1.4.0_pre11, script.aculo.us to [2502] *Thomas Fuchs*
-
-* Make assert_tag :children count appropriately. Closes #2181. *jamie@bravenet.com*
-
-* Forced newer versions of RedCloth to use hard breaks *David Heinemeier Hansson*
-
-* Added new scriptaculous options for auto_complete_field #2343 *Manfred Stienstra*
-
-* Don't prepend the asset host if the string is already a fully-qualified URL
-
-* Updated to script.aculo.us V1.5.0_rc2 and Prototype to V1.4.0_pre7 *Thomas Fuchs*
-
-* Undo condition change made in [2345] to prevent normal parameters arriving as StringIO.
-
-* Tolerate consecutive delimiters in query parameters. #2295 *darashi@gmail.com*
-
-* Streamline render process, code cleaning. Closes #2294. *skae*
-
-* Keep flash after components are rendered. #2291 *Rick Olson, Scott*
-
-* Shorten IE file upload path to filename only to match other browsers. #1507 *court3nay*
-
-* Fix open/save dialog in IE not opening files send with send_file/send_data, #2279 *Thomas Fuchs*
-
-* Fixed that auto_discovery_link_tag couldn't take a string as the URL *David Heinemeier Hansson*
-
-* Fixed problem with send_file and WEBrick using stdout #1812 *David Heinemeier Hansson*
-
-* Optimized tag_options to not sort keys, which is no longer necessary when assert_dom_equal and friend is available #1995 *skae*
-
-* Added assert_dom_equal and assert_dom_not_equal to compare tags generated by the helpers in an order-indifferent manner #1995 *skae*
-
-* Fixed that Request#domain caused an exception if the domain header wasn't set in the original http request #1795 *Michael Koziarski*
-
-* Make the truncate() helper multi-byte safe (assuming $KCODE has been set to something other than "NONE") #2103
-
-* Add routing tests from #1945 *ben@groovie.org*
-
-* Add a routing test case covering #2101 *Nicholas Seckar*
-
-* Cache relative_url_root for all webservers, not just Apache #2193 *skae*
-
-* Speed up cookie use by decreasing string copying #2194 *skae*
-
-* Fixed access to "Host" header with requests made by crappy old HTTP/1.0 clients #2124 *Marcel Molina Jr.*
-
-* Added easy assignment of fragment cache store through use of symbols for included stores (old way still works too)
-
- Before:
- ActionController::Base.fragment_cache_store =
- ActionController::Base::Caching::Fragments::FileStore.new("/path/to/cache/directory")
-
- After:
- ActionController::Base.fragment_cache_store = :file_store, "/path/to/cache/directory"
-
-* Added ActionController::Base.session_store=, session_store, and session_options to make it easier to tweak the session options (instead of going straight to ActionController::CgiRequest::DEFAULT_SESSION_OPTIONS)
-
-* Added TextHelper#cycle to cycle over an array of values on each hit (useful for alternating row colors etc) #2154 *dave-ml@dribin.org*
-
-* Ensure that request.path never returns nil. Closes #1675 *Nicholas Seckar*
-
-* Add ability to specify Route Regexps for controllers. Closes #1917. *Sebastian Kanthak*
-
-* Provide Named Route's hash methods as helper methods. Closes #1744. *Nicholas Seckar, Steve Purcell*
-
-* Added :multipart option to ActiveRecordHelper#form to make it possible to add file input fields #2034 *jstirk@oobleyboo.com*
-
-* Moved auto-completion and in-place editing into the Macros module and their helper counterparts into JavaScriptMacrosHelper
-
-* Added in-place editing support in the spirit of auto complete with ActionController::Base.in_place_edit_for, JavascriptHelper#in_place_editor_field, and Javascript support from script.aculo.us #2038 *Jon Tirsen*
-
-* Added :disabled option to all data selects that'll make the elements inaccessible for change #2167, #253 *eigentone*
-
-* Fixed that TextHelper#auto_link_urls would include punctuation in the links #2166, #1671 *eigentone*
-
-* Fixed that number_to_currency(1000, {:precision => 0})) should return "$1,000", instead of "$1,000." #2122 *sd@notso.net*
-
-* Allow link_to_remote to use any DOM-element as the parent of the form elements to be submitted #2137 *erik@ruby-lang.nl*. Example:
-
-
-
-* Fixed that render :partial would fail when :object was a Hash (due to backwards compatibility issues) #2148 *Sam Stephenson*
-
-* Fixed JavascriptHelper#auto_complete_for to only include unique items #2153 *Thomas Fuchs*
-
-* Fixed all AssetHelper methods to work with relative paths, such that javascript_include_tag('stdlib/standard') will look in /javascripts/stdlib/standard instead of '/stdlib/standard/' #1963
-
-* Avoid extending view instance with helper modules each request. Closes #1979
-
-* Performance improvements to CGI methods. Closes #1980 *Stefan Kaes*
-
-* Added :post option to UrlHelper#link_to that makes it possible to do POST requests through normal ahref links using Javascript
-
-* Fixed overwrite_params
-
-* Added ActionController::Base.benchmark and ActionController::Base.silence to allow for easy benchmarking and turning off the log
-
-* Updated vendor copy of html-scanner to support better xml parsing
-
-* Added :popup option to UrlHelper#link_to #1996 *gabriel.gironda@gmail.com*. Examples:
-
- link_to "Help", { :action => "help" }, :popup => true
- link_to "Busy loop", { :action => "busy" }, :popup => ['new_window', 'height=300,width=600']
-
-* Drop trailing \000 if present on RAW_POST_DATA (works around bug in Safari Ajax implementation) #918
-
-* Fix observe_field to fall back to event-based observation if frequency <= 0 #1916 *Michael Schubert*
-
-* Allow use of the :with option for submit_to_remote #1936 *jon@instance-design.co.uk*
-
-* AbstractRequest#domain returns nil when host is an ip address #2012 *Kevin Clark*
-
-* ActionController documentation update #2051 *François Beausoleil*
-
-* Yield @content_for_ variables to templates #2058 *Sam Stephenson*
-
-* Make rendering an empty partial collection behave like :nothing => true #2080 *Sam Stephenson*
-
-* Add option to specify the singular name used by pagination.
-
-* Use string key to obtain action value. Allows indifferent hashes to be disabled.
-
-* Added ActionView::Base.cache_template_loading back.
-
-* Rewrote compiled templates to decrease code complexity. Removed template load caching in favour of compiled caching. Fixed template error messages. *Nicholas Seckar*
-
-* Fix Routing to handle :some_param => nil better. *Nicholas Seckar, Luminas*
-
-* Add support for :include with pagination (subject to existing constraints for :include with :limit and :offset) #1478 *Michael Schubert*
-
-* Prevent the benchmark module from blowing up if a non-HTTP/1.1 request is processed
-
-* Added :use_short_month option to select_month helper to show month names as abbreviations
-
-* Make link_to escape the javascript in the confirm option #1964 *nicolas.pouillard@gmail.com*
-
-* Make assert_redirected_to properly check URL's passed as strings #1910 *Scott Barron*
-
-* Make sure :layout => false is always used when rendering inside a layout
-
-* Use raise instead of assert_not_nil in Test::Unit::TestCase#process to ensure that the test variables (controller, request, response) have been set
-
-* Make sure assigns are built for every request when testing #1866
-
-* Allow remote_addr to be queried on TestRequest #1668
-
-* Fixed bug when a partial render was passing a local with the same name as the partial
-
-* Improved performance of test app req/sec with ~10% refactoring the render method #1823 *Stefan Kaes*
-
-* Improved performance of test app req/sec with 5-30% through a series of Action Pack optimizations #1811 *Stefan Kaes*
-
-* Changed caching/expiration/hit to report using the DEBUG log level and errors to use the ERROR log level instead of both using INFO
-
-* Added support for per-action session management #1763
-
-* Improved rendering speed on complicated templates by up to 100% (the more complex the templates, the higher the speedup) #1234 *Stefan Kaes*. This did necessasitate a change to the internals of ActionView#render_template that now has four parameters. Developers of custom view handlers (like Amrita) need to update for that.
-
-* Added options hash as third argument to FormHelper#input, so you can do input('person', 'zip', :size=>10) #1719 *jeremye@bsa.ca.gov*
-
-* Added Base#expires_in(seconds)/Base#expires_now to control HTTP content cache headers #1755 *Thomas Fuchs*
-
-* Fixed line number reporting for Builder template errors #1753 *piotr*
-
-* Fixed assert_routing so that testing controllers in modules works as expected *Nicholas Seckar, Rick Olson*
-
-* Fixed bug with :success/:failure callbacks for the JavaScriptHelper methods #1730 *court3nay/Thomas Fuchs*
-
-* Added named_route method to RouteSet instances so that RouteSet instance methods do not prevent certain names from being used. *Nicholas Seckar*
-
-* Fixed routes so that routes which do not specify :action in the path or in the requirements have a default of :action => 'index', In addition, fixed url generation so that :action => 'index' does not need to be provided for such urls. *Nicholas Seckar, Markjuh*
-
-* Worked around a Safari bug where it wouldn't pass headers through if the response was zero length by having render :nothing return ' ' instead of ''
-
-* Fixed Request#subdomains to handle "foo.foo.com" correctly
-
-
-## 1.9.1 (11 July, 2005) ##
-
-* Fixed that auto_complete_for didn't force the input string to lower case even as the db comparison was
-
-* Fixed that Action View should always use the included Builder, never attempt to require the gem, to ensure compatibility
-
-* Added that nil options are not included in tags, so tag("p", :ignore => nil) now returns not but that tag("p", :ignore => "") still includes it #1465 *Michael Schuerig*
-
-* Fixed that UrlHelper#link_to_unless/link_to_if used html_escape on the name if no link was to be applied. This is unnecessary and breaks its use with images #1649 *joergd@pobox.com*
-
-* Improved error message for DoubleRenderError
-
-* Fixed routing to allow for testing of *path components #1650 *Nicholas Seckar*
-
-* Added :handle as an option to sortable_element to restrict the drag handle to a given class #1642 *thejohnny*
-
-* Added a bunch of script.aculo.us features #1644, #1677, #1695 *Thomas Fuchs*
- * Effect.ScrollTo, to smoothly scroll the page to an element
- * Better Firefox flickering handling on SlideUp/SlideDown
- * Removed a possible memory leak in IE with draggables
- * Added support for cancelling dragging my hitting ESC
- * Added capability to remove draggables/droppables and redeclare sortables in dragdrop.js (this makes it possible to call sortable_element on the same element more than once, e.g. in AJAX returns that modify the sortable element. all current sortable 'stuff' on the element will be discarded and the sortable will be rebuilt)
- * Always reset background color on Effect.Highlight; this make change backwards-compatibility, to be sure include style="background-color:(target-color)" on your elements or else elements will fall back to their CSS rules (which is a good thing in most circumstances)
- * Removed circular references from element to prevent memory leaks (still not completely gone in IE)
- * Changes to class extension in effects.js
- * Make Effect.Highlight restore any previously set background color when finishing (makes effect work with CSS classes that set a background color)
- * Fixed myriads of memory leaks in IE and Gecko-based browsers *David Zülke*
- * Added incremental and local autocompleting and loads of documentation to controls.js *Ivan Krstic*
- * Extended the auto_complete_field helper to accept tokens option
- * Changed object extension mechanism to favor Object.extend to make script.aculo.us easily adaptable to support 3rd party libs like IE7.js *David Zülke*
-
-* Fixed that named routes didn't use the default values for action and possible other parameters #1534 *Nicholas Seckar*
-
-* Fixed JavascriptHelper#visual_effect to use camelize such that :blind_up will work #1639 *pelletierm@eastmedia.net*
-
-* Fixed that a SessionRestoreError was thrown if a model object was placed in the session that wasn't available to all controllers. This means that it's no longer necessary to use the 'model :post' work-around in ApplicationController to have a Post model in your session.
-
-
-## 1.9.0 (6 July, 2005) ##
-
-* Added logging of the request URI in the benchmark statement (makes it easy to grep for slow actions)
-
-* Added javascript_include_tag :defaults shortcut that'll include all the default javascripts included with Action Pack (prototype, effects, controls, dragdrop)
-
-* Cache several controller variables that are expensive to calculate #1229 *Stefan Kaes*
-
-* The session class backing CGI::Session::ActiveRecordStore may be replaced with any class that duck-types with a subset of Active Record. See docs for details #1238 *Stefan Kaes*
-
-* Fixed that hashes was not working properly when passed by GET to lighttpd #849 *Nicholas Seckar*
-
-* Fixed assert_template nil will be true when no template was rendered #1565 *maceywj@telus.net*
-
-* Added :prompt option to FormOptions#select (and the users of it, like FormOptions#select_country etc) to create "Please select" style descriptors #1181 *Michael Schuerig*
-
-* Added JavascriptHelper#update_element_function, which returns a Javascript function (or expression) that'll update a DOM element according to the options passed #933 *mortonda@dgrmm.net*. Examples:
-
- <%= update_element_function("products", :action => :insert, :position => :bottom, :content => "
- <% end %>
-
-* Added :field_name option to DateHelper#select_(year|month|day) to deviate from the year/month/day defaults #1266 *Marcel Molina Jr.*
-
-* Added JavascriptHelper#draggable_element and JavascriptHelper#drop_receiving_element to facilitate easy dragging and dropping through the script.aculo.us libraries #1578 *Thomas Fuchs*
-
-* Added that UrlHelper#mail_to will now also encode the default link title #749 *f.svehla@gmail.com*
-
-* Removed the default option of wrap=virtual on FormHelper#text_area to ensure XHTML compatibility #1300 *thomas@columbus.rr.com*
-
-* Adds the ability to include XML CDATA tags using Builder #1563 *Josh Knowles*. Example:
-
- xml.cdata! "some text" # =>
-
-* Added evaluation of SCRIPT blocks in content returned to Ajax calls #1577 *Thomas Fuchs/court3nay/Sean Treadway*
-
-* Directly generate paths with a leading slash instead of tacking it on later. #1543 *Nicholas Seckar*
-
-* Fixed errant parameter modification in functional tests. #1542 *Nicholas Seckar*
-
-* Routes fail with leading slash #1540 *Nicholas Seckar*
-
-* Added support for graceful error handling of Ajax calls #1217 *Jamis Buck/Thomas Fuchs*. Example:
-
- link_to_remote(
- "test",
- :url => { :action => "faulty" },
- :update => { :success => "good", :failure => "bad" },
- 403 => "alert('Forbidden- got ya!')",
- 404 => "alert('Nothing there...?')",
- :failure => "alert('Unkown error ' + request.status)")
-
-* Attempt to explicitly flush the output at the end of CgiProcess#out
-
-* Fixed assert_redirected_to to handle absolute controller paths properly #1472 *Rick Olson/Nicholas Seckar*
-
-* Added event-based observations when frequency is not set on observe_field/form #1474 *flash@vanklinkenbergsoftware.nl*
-
-* Added script.aculo.us Javascripts (controls.js, dragdrop.js, effects.js) (NEEDS MORE DESCRIPTION) #1509 *Thomas Fuchs*
-
-* Fixed prototype to consider all fields it doesn't know as text (such as Safari's search) just like the browser in its serialization #1497 *Sean Treadway*
-
-* Improved performance of Routes generation by a factor of 5 #1434 *Nicholas Seckar*
-
-* Added named routes (NEEDS BETTER DESCRIPTION) #1434 *Nicholas Seckar*
-
-* Improved AbstractRequest documentation #1483 *court3nay*
-
-* Added ActionController::Base.allow_concurrency to control whether the application is thread-safe, so multi-threaded servers like WEBrick knows whether to apply a mutex around the performance of each action. Turned off by default. EXPERIMENTAL FEATURE.
-
-* Added TextHelper#word_wrap(text, line_length = 80) #1449 *tuxie@dekadance.se*
-
-* Added a fall-through action for form_remote_tag that'll be used in case Javascript is unavailable #1459 *Scott Barron*. Example:
-
- form_remote_tag :html => { :action => url_for(:controller => "some", :action => "place") }
-
-* Added :xhr => true/false option to verify so you can ensure that a request is coming from an Ajax call or not #1464 *Thomas Fuchs*
-
-* Added tag_options as a third parameter to AssetHelper#auto_discovery_link_tag to control options like the title of the link #1430 *Kevin Clark*
-
-* Added option to pass in parameters to CaptureHelper#capture, so you can create more advanced view helper methods #1466 *duane.johnson@gmail.com*. Example:
-
- <% show_calendar(:year => 2005, :month => 6) do |day, options| %>
- <% options[:bgcolor] = '#dfd' if 10..15.include? day %>
- [<%= day %>]
- <% end %>
-
-* Changed the default name of the input tag generated by FormTagHelper#submit_tag from "submit" to "commit" so it doesn't clash with form.submit() calls in Javascript #1271
-
-* Fixed relative urls support for lighttpd #1048 *Nicholas Seckar/maznawak@nerim.net*
-
-* Correct distance_of_time_in_words for integer arguments and make the second arg optional, treating the first arg as a duration in seconds. #1458 *madrobby *
-
-* Fixed query parser to deal gracefully with equal signs inside keys and values #1345 *gorou*.
- Example: /?sig=abcdef=:foobar=&x=y will pass now.
-
-* Added Cuba to country list #1351 *todd*
-
-* Fixed radio_button to work with numeric values #1352 *demetrius*
-
-* Added :extension option to NumberHelper#number_to_phone #1361 *delynnb*
-
-* Added button_to as a form-based solution to deal with harmful actions that should be hidden behind POSTs. This makes it just as easy as link_to to create a safe trigger for actions like destroy, although it's limited by being a block element, the fixed look, and a no-no inside other forms. #1371 *tom@moertel.com*
-
-* Fixed image_tag so an exception is not thrown just because the image is missing and alt value can't be generated #1395 *Marcel Molina Jr.*
-
-* Added a third parameter to TextHelper#auto_link called href_options for specifying additional tag options on the links generated #1401 *tyler.kovacs@gmail.com*. Example: auto_link(text, :all, { :target => "_blank" }) to have all the generated links open in a new window.
-
-* Fixed TextHelper#highlight to return the text, not nil, if the phrase is blank #1409 *Patrick Lenz*
-
-* Fixed TagHelper such that :name and 'name' keys in the options doesn't result in two attributes #1455 *take_tk*
-
-* Ensure that helpers are only available to the controllers where they are defined and their subclasses. #1394 *kdole@tamu.edu*
-
-* render("foo/bar") works with a layout again
-
-* Fixed double-singularization on scaffolded pagination call (Address would be turned into Addres) #1216, #1404 *nilsga*
-
-* Removed the require hack used by functional testing to work around an earlier bug in rake.
-
-* Allow distance_of_time_in_words to work with any value that responds to #to_time (like dates) #969
-
-* Support :render option for :verify #1440 *Tobias Lütke*
-
-* Updated vendor copy of html-scanner lib to 0.5.2, for bug fixes and optimizations. The :content option may be used as expected--to find a tag whose textual content is a particular value--in assert_tag, now.
-
-* Changed test requests to come from 0.0.0.0 instead of 127.0.0.1 such that they don't trigger debugging screens on exceptions, but instead call rescue_action_in_public
-
-* Modernize scaffolding to match the generator: use the new render method and change style from the warty @params["id"] to the sleek params[:id]. #1367
-
-* Include :id in the action generated by the form helper method. Then, for example, the controller can do Model.find(params[:id]) for both edit and update actions. Updated scaffolding to take advantage. #1367
-
-* Add assertions with friendly messages to TestCase#process to ensure that @controller, @request, and @response are set. #1367
-
-* Arrays, hashes sent via multipart posts are converted to strings #1032 *dj@omelia.org, me@julik.nl*
-
-* render(:layout => true) is a synonym for render(:layout => nil)
-
-* Make sure the benchmarking render method always returns the output of the render.
-
-* render(:action), render(:template) and render() are the only three calls that default to using a layout. All other render calls assume :layout => false. This also fixes send_file, which was applying a layout if one existed for the current action.
-
-* verify with :redirect_to won't redirect if a redirect or render has already been performed #1350
-
-* render(:partial => true) is identical to the behavior of the deprecated render_partial()
-
-* Fixed render(:partial => "...") to use an empty Hash for the local assigns #1365
-
-* Fixed Caching::Fragments::FileStore.delete to not raise an exception if the delete fails.
-
-* Deprecated all render_* methods in favor of consolidating all rendering behavior in Base#render(options). This enables more natural use of combining options, such as layouts. Examples:
-
- +---------------------------------------------------------------+-------------------------------------------------------+
- | BEFORE | AFTER |
- +---------------------------------------------------------------+-------------------------------------------------------+
- | render_with_layout "weblog/show", "200 OK", "layouts/dialog" | render :action => "show", :layout => "dialog" |
- | render_without_layout "weblog/show" | render :action => "show", :layout => false |
- | render_action "error", "404 Not Found" | render :action => "error", :status => "404 Not Found" |
- | render_template "xml.div('stuff')", "200 OK", :rxml | render :inline => "xml.div('stuff')", :type => :rxml |
- | render_text "hello world!" | render :text => "hello world!" |
- | render_partial_collection "person", @people, nil, :a => 1 | render :partial => "person", :collection => @people, |
- | | :locals => { :a => 1 } |
- +---------------------------------------------------------------+-------------------------------------------------------+
-
-* Deprecated redirect_to_path and redirect_to_url in favor of letting redirect_to do the right thing when passed either a path or url.
-
-* Fixed use of an integer as return code for renders, so render_text "hello world", 404 now works #1327
-
-* Fixed assert_redirect_to to work with redirect_to_path #869 *Nicholas Seckar*
-
-* Fixed escaping of :method option in remote_form_tag #1218 *Rick Olson*
-
-* Added Serbia and Montenegro to the country_select #1239 *todd@robotcoop.com*
-
-* Fixed Request#remote_ip in testing #1251 *Jeremy Kemper*
-
-* Fixed that compute_public_path should recognize external URLs, so image_tag("http://www.example.com/images/icon.gif") is not prefixed with the relative url path #1254 *victor-ronr-trac@carotena.net*
-
-* Added support for descending year values in DateHelper#select_year, like select_year(Date.today, :start_year => 2005, :end_year => 1900), which would count down from 2005 to 1900 instead of the other way #1274 *nwoods@mail.com*
-
-* Fixed that FormHelper#checkbox should return a checked checkbox if the value is the same as checked_value #1286 *Florian Weber*
-
-* Fixed Form.disable in Prototype #1317 *Wintermute*
-
-* Added accessors to logger, params, response, session, flash, and headers from the view, so you can write <% logger.info "stuff" %> instead of <% @logger.info "others" %> -- more consistent with the preferred way of accessing these attributes and collections from the controller
-
-* Added support for POST data in form of YAML or XML, which is controller through the Content-Type header. Example request:
-
- Content-Type: application/xml
- HelloWorld
-
- ...is the same as:
-
- Content-Type: application/x-yaml
- ---
- item:
- content: HelloWorld
-
- ...is the same as:
-
- item[content]=HelloWorld
-
- Which in the end turns into { "item" => { "content" => "HelloWorld" } }. This makes it a lot easier to publish REST web services on top of your regular actions (as they won't care).
-
- Example Curl call:
-
- curl -H 'Content-Type: application/xml' -d 'KillMeMore' http://www.example.com/service
-
- You can query to find out whether a given request came through as one of these types with:
- - request.post_format? (:url_encoded, :xml or :yaml)
- - request.formatted_post? (for either xml or yaml)
- - request.xml_post?
- - request.yaml_post?
-
-* Added bundling of XmlSimple by Maik Schmidt
-
-* Fixed that render_partial_collection should always return a string (and not sometimes an array, despite <%= %> not caring)
-
-* Added TextHelper#sanitize that can will remove any Javascript handlers, blocks, and forms from an input of HTML. This allows for use of HTML on public sites, but still be free of XSS issues. #1277 *Jamis Buck*
-
-* Fixed the HTML scanner used by assert_tag where a infinite loop could be caused by a stray less-than sign in the input #1270 *Jamis Buck*
-
-* Added functionality to assert_tag, so you can now do tests on the siblings of a node, to assert that some element comes before or after the element in question, or just to assert that some element exists as a sibling #1226 *Jamis Buck*
-
-* Added better error handling for regexp caching expiration
-
-* Fixed handling of requests coming from unknown HTTP methods not to kill the server
-
-* Added that both AssetHelper#stylesheet_link_tag and AssetHelper#javascript_include_tag now accept an option hash as the last parameter, so you can do stuff like: stylesheet_link_tag "style", :media => "all"
-
-* Added FormTagHelper#image_submit_tag for making submit buttons that uses images
-
-* Added ActionController::Base.asset_host that will then be used by all the asset helpers. This enables you to easily offload static content like javascripts and images to a separate server tuned just for that.
-
-* Fixed action/fragment caching using the filestore when a directory and a file wanted to use the same name. Now there's a .cache prefix that sidesteps the conflict #1188 *imbcmdth@hotmail.com*
-
-* Fixed missing id uniqueness in FormTag#radio_button #1207 *Jarkko Laine*
-
-* Fixed assert_redirected_to to work with :only_path => false #1204 *Alisdair McDiarmid*
-
-* Fixed render_partial_collection to output an empty string instead of nil when handed an empty array #1202 *Ryan Carver*
-
-* Improved the speed of regular expression expirations for caching by a factor of 10 #1221 *Jamis Buck*
-
-* Removed dumping of template assigns on the rescue page as it would very easily include a ton of data making page loads take seconds (and the information was rarely helpful) #1222
-
-* Added BenchmarkHelper that can measure the execution time of a block in a template and reports the result to the log. Example:
-
- <% benchmark "Notes section" do %>
- <%= expensive_notes_operation %>
- <% end %>
-
- Will add something like "Notes section (0.345234)" to the log.
-
-* Added ActionController::Caching::Sweeper as an improved an easier to use sweeper. The new sweepers work on a single-step approach instead of two-steps like the old ones. Before
-
- def after_save(record)
- @list = record.is_a?(List) ? record : record.list
- end
-
- def filter(controller)
- controller.expire_page(:controller => "lists", :action => %w( show public feed ), :id => @list.id)
- controller.expire_action(:controller => "lists", :action => "all")
- @list.shares.each { |share| controller.expire_page(:controller => "lists", :action => "show", :id => share.url_key) }
- end
-
- ..after:
-
- def after_save(record)
- list = record.is_a?(List) ? record : record.list
- expire_page(:controller => "lists", :action => %w( show public feed ), :id => list.id)
- expire_action(:controller => "lists", :action => "all")
- list.shares.each { |share| expire_page(:controller => "lists", :action => "show", :id => share.url_key) }
- end
-
- The new sweepers can also observe on the actions themselves by implementing methods according to (before|after)_$controller_$action. Example of a callback that'll be called after PagesController#update_title has been performed:
-
- def after_pages_update_title
- expire_fragment(%r{pages/#{controller.assigns["page"].id}/.*})
- end
-
- Note that missing_method is delegated to the controller instance, which is assigned in a before filter. This means that you can call expire_fragment instead of @controller.expire_fragment.
-
-* Added that Fragments#expire_fragment now accepts as a regular expression as the name thereby deprecating expire_matched_fragments
-
-* Fixed that fragments shouldn't use the current host and the path as part of the key like pages does
-
-* Added conditions to around_filters just like before_filter and after_filter
-
-
-## 1.8.1 (20th April, 2005) ##
-
-* Added xml_http_request/xhr method for simulating XMLHttpRequest in functional tests #1151 *Sam Stephenson*. Example:
-
- xhr :post, :index
-
-* Fixed that Ajax.Base.options.asynchronous wasn't being respected in Ajax.Request (thanks Jon Casey)
-
-* Fixed that :get, :post, and the others should take a flash array as the third argument just like process #1144 *rails@cogentdude.com*
-
-* Fixed a problem with Flash.now
-
-* Fixed stringification on all assigned hashes. The sacrifice is that assigns[:person] won't work in testing. Instead assigns["person"] or assigns(:person) must be used. In other words, the keys of assigns stay strings but we've added a method-based accessor to appease the need for symbols.
-
-* Fixed that rendering a template would require a connection to the database #1146
-
-
-## 1.8.0 (19th April, 2005) ##
-
-* Added assert_tag and assert_no_tag as a much improved alternative to the deprecated assert_template_xpath_match #1126 *Jamis Buck*
-
-* Deprecated the majority of all the testing assertions and replaced them with a much smaller core and access to all the collections the old assertions relied on. That way the regular test/unit assertions can be used against these. Added documentation about how to use it all.
-
-* Added a wide range of new Javascript effects:
- * Effect.Puff zooms the element out and makes it smoothly transparent at the same time, giving a "puff" illusion #996 *thomas@fesch.at*
- After the animation is completed, the display property will be set to none.
- This effect will work on relative and absolute positioned elements.
-
- * Effect.Appear as the opposite of Effect.Fade #990 *thomas@fesch.at*
- You should return elements with style="display:none;" or a like class for this to work best and have no chance of flicker.
-
- * Effect.Squish for scaling down an element and making it disappear at the end #972 *thomas@fesch.at*
-
- * Effect.Scale for smoothly scaling images or text up and down #972 *thomas@fesch.at*
-
- * Effect.Fade which smoothly turns opacity from 100 to 0 and then hides the element #960 *thomas@fesch.at*
-
-* Added Request#xml_http_request? (and an alias xhr?) to that'll return true when the request came from one of the Javascript helper methods (Ajax). This can be used to give one behavior for modern browsers supporting Ajax, another to old browsers #1127 *Sam Stephenson*
-
-* Changed render_partial to take local assigns as the second parameter instead of an explicit object and then the assigns. So the API changes from:
-
- <%= render_partial "account", person, "rules" => regulations.rules %>
-
- ...to:
-
- <%= render_partial "account", :account => person, :rules => regulations.rules %>
-
- The old API will still work, though, and render_partial "account" will still assume :account => @account.
-
-* Added support for web servers that use PATH_INFO instead of REQUEST_URI like IIS #1014 *BradG/Nicholas Seckar*
-
-* Added graceful handling of PUT, DELETE, and OPTIONS requests for a complete coverage of REST functionality #1136 *Josh Knowles*
-
-* Fixed that you can now pass an alternative :href option to link_to_function/remote in order to point to somewhere other than # if the javascript fails or is turned off. You can do the same with form_remote_tag by passing in :action. #1113 *Sam Stephenson*
-
-* Fixed DateHelper to return values on the option tags such that they'll work properly in IE with form_remote_tag #1024 *Scott Raymond*
-
-* Fixed FormTagHelper#check_box to respect checked #1049 *DelynnB*
-
-* Added that render_partial called from a controller will use the action name as default #828 *Dan Peterson*
-
-* Added Element.toggle, Element.show, and Element.hide to the prototype javascript library. Toggle.display has been deprecated, but will still work #992 *Lucas Carlson*
-
-* Added that deleting a cookie should not just set it to an empty string but also instantly expire it #1118 *todd@robotcoop.com*
-
-* Added AssetTagHelper#image_path, AssetTagHelper#javascript_path, and AssetTagHelper#stylesheet_path #1110 *Larry Halff*
-
-* Fixed url_for(nil) in functional tests #1116 *Alisdair McDiarmid*
-
-* Fixed error handling of broken layouts #1115 *Michael Schubert*
-
-* Added submit_to_remote that allows you to trigger an Ajax form submition at the click of the submission button, which allows for multiple targets in a single form through the use of multiple submit buttons #930 *yrashk@gmail.com*
-
-* Fixed pagination to work with joins #1034 *scott@sigkill.org*
-
-* Fixed that *rest parameter in map.connect couldn't accept an empty list #1037 *Dee Zsombor*
-
-* Added :confirm option to link_to_remote just like link_to has #1082 *yrashk@fp.org.ua*
-
-* Added minute_step as an option to select_minute (and the helpers that use it) to jump in larger increments than just 1 minute. At 15, it would return 0, 15, 30, 45 options #1085 *ordwaye@evergreen.edu*
-
-* Fixed that an exception would be thrown when an empty form was submitted #1090 *jan@ulbrich-boerwang.de*
-
-* Moved TextHelper#human_size to NumberHelper#number_to_human_size, but kept an deprecated alias to the old method name
-
-* Fixed that the content-type for some browsers could include an additional \r which made wonky things happen #1067 *Thomas Fuchs*
-
-* Fixed that radio buttons shouldn't have a default size attribute #1074 *hendrik@mans.de*
-
-* Added ActionView::Helpers::InstanceTag::DEFAULT_RADIO_OPTIONS that contains a hash of default options for radio buttons #1074 *hendrik@mans.de*
-
-* Fixed that in some circumstances controllers outside of modules may have hidden ones inside modules. For example, admin/content might have been hidden by /content. #1075 *Nicholas Seckar*
-
-* Added JavascriptHelper#periodically_call_remote in order to create areas of a page that update automatically at a set interval #945 *Jon Tirsen*
-
-* Fixed Cache#expire_matched_fragments that couldn't recognize the difference between string and url_for options #1030 *Stefan Kaes*
-
-* Added simulation of @request.request_uri in functional tests #1038 *Jamis Buck*
-
-* Fixed autolinking to work better in more cases #1013 *Jamis Buck*
-
-* Added the possible of using symbols in form helpers that relate to instance variables like text_field :account, :name in addition to text_field "account", "name"'
-
-* Fixed javascript_include_tag to output type instead of language and conform to XHTML #1018 *Rick Olson*
-
-* Added NumberHelper for common string representations like phone number, currency, and percentage #1015 *DeLynn*
-
-* Added pagination for scaffolding (10 items per page) #964 *mortonda@dgrmm.net*
-
-* Added assert_no_cookie and fixed assert_cookie_equal to deal with non-existing cookies #979 *Jeremy Kemper*
-
-* Fixed :overwrite_param so it doesn't delete but reject elements from @request.parameters #982 *raphinou@yahoo.com*
-
-* Added :method option to verify for ensuring that either GET, POST, etc is allowed #984 *Jamis Buck*
-
-* Added options to set cc, bcc, subject, and body for UrlHelper#mail_to #966 *DeLynn*
-
-* Fixed include_blank for select_hour/minute/second #527 *edward@debian.org*
-
-* Improved the message display on the exception handler pages #963 *Johan Sorensen*
-
-* Fixed that on very rare occasions, webrick would raise a NoMethodError: private method 'split' called for nil #1001 *Flurin Egger*
-
-* Fixed problem with page caching #958 *Rick Olson*
-
-
-## 1.7.0 (27th March, 2005) ##
-
-* Added ActionController::Base.page_cache_extension for setting the page cache file extension (the default is .html) #903 *Andreas*
-
-* Fixed "bad environment variable value" exception caused by Safari, Apache, and Ajax calls #918
-
-* Fixed that pagination_helper would ignore :params #947 *Sebastian Kanthak*
-
-* Added :owerwrite_params back to url_for and friends -- it was AWL since the introduction of Routes #921 *raphinou*
-
-* Added :position option to link_to_remote/form_remote_tag that can be either :before, :top, :bottom, or :after and specifies where the return from the method should be inserted #952 *Matthew McCray/Sam Stephenson*
-
-* Added Effect.Highlight to prototype.js to do Yellow Fade Technique (of 37signals' fame) on any container #952 *Sam Stephenson/court3nay*
-
-* Added include_seconds option as the third parameter to distance_of_time_in_words which will render "less than a minute" in higher resolution ("less than 10 seconds" etc) #944 *thomas@fesch.at*
-
-* Added fourth option to process in test cases to specify the content of the flash #949 *Jamis Buck*
-
-* Added Verifications that allows you to specify preconditions to actions in form of statements like verify :only => :update_post, :params => "admin_privileges", :redirect_to => { :action => "settings" }, which ensure that the update_post action is only called if admin_privileges is available as a parameter -- otherwise the user is redirected to settings. #897 *Jamis Buck*
-
-* Fixed Form.Serialize for the JavascriptHelper to also seriliaze password fields #934 *dweitzman@gmail.com*
-
-* Added JavascriptHelper#escape_javascript as a public method (was private) and made it escape both single and double quotes and new lines #940 *mortonda@dgrmm.net*
-
-* Added trailing_slash option to url_for, so you can generate urls ending in a slash. Note that is currently not recommended unless you need it for special reasons since it breaks caching #937 *stian@grytoyr.net*
-
-* Added expire_matched_fragments(regular_expression) to clear out a lot of fragment caches at once #927 *Rick Olson*
-
-* Fixed the problems with : and ? in file names for fragment caches on Windows #927 *Rick Olson*
-
-* Added TextHelper#human_size for formatting file sizes, like human_size(1234567) => 1.2 MB #943 *thomas@fesch.at*
-
-* Fixed link_to :confirm #936 *Nicholas Seckar*
-
-* Improved error reporting especially around never shallowing exceptions. Debugging helpers should be much easier now #980 *Nicholas Seckar*
-
-* Fixed Toggle.display in prototype.js #902 *Lucas Carlson*
-
-
-## 1.6.0 (22th March, 2005) ##
-
-* Added a JavascriptHelper and accompanying prototype.js library that opens the world of Ajax to Action Pack with a large array of options for dynamically interacting with an application without reloading the page #884 *Sam Stephenson/David*
-
-* Added pagination support through both a controller and helper add-on #817 *Sam Stephenson*
-
-* Fixed routing and helpers to make Rails work on non-vhost setups #826 *Nicholas Seckar/Tobias Lütke*
-
-* Added a much improved Flash module that allows for finer-grained control on expiration and allows you to flash the current action #839 *Caio Chassot*. Example of flash.now:
-
- class SomethingController < ApplicationController
- def save
- ...
- if @something.save
- # will redirect, use flash
- flash[:message] = 'Save successful'
- redirect_to :action => 'list'
- else
- # no redirect, message is for current action, use flash.now
- flash.now[:message] = 'Save failed, review'
- render_action 'edit'
- end
- end
- end
-
-* Added to_param call for parameters when composing an url using url_for from something else than strings #812 *Sam Stephenson*. Example:
-
- class Page
- def initialize(number)
- @number = number
- end
- # ...
- def to_param
- @number.to_s
- end
- end
-
- You can now use instances of Page with url_for:
-
- class BarController < ApplicationController
- def baz
- page = Page.new(4)
- url = url_for :page => page # => "http://foo/bar/baz?page=4"
- end
- end
-
-* Fixed form helpers to query Model#id_before_type_cast instead of Model#id as a temporary workaround for Ruby 1.8.2 warnings #818 *DeLynn B*
-
-* Fixed TextHelper#markdown to use blank? instead of empty? so it can deal with nil strings passed #814 *Johan Sörensen*
-
-* Added TextHelper#simple_format as a non-dependency text presentation helper #814 *Johan Sörensen*
-
-* Added that the html options disabled, readonly, and multiple can all be treated as booleans. So specifying disabled => :true will give disabled="disabled". #809 *mindel*
-
-* Added path collection syntax for Routes that will gobble up the rest of the url and pass it on to the controller #830 *rayners*. Example:
-
- map.connect 'categories/*path_info', :controller => 'categories', :action => 'show'
-
- A request for /categories/top-level-cat, would give @params[:path_info] with "top-level-cat".
- A request for /categories/top-level-cat/level-1-cat, would give @params[:path_info] with "top-level-cat/level-1-cat" and so forth.
-
- The @params[:path_info] return is really an array, but where to_s has been overwritten to do join("/").
-
-* Fixed options_for_select on selected line issue #624 *Florian Weber*
-
-* Added CaptureHelper with CaptureHelper#capture and CaptureHelper#content_for. See documentation in helper #837 *Tobias Lütke*
-
-* Fixed :anchor use in url_for #821 *Nicholas Seckar*
-
-* Removed the reliance on PATH_INFO as it was causing problems for caching and inhibited the new non-vhost support #822 *Nicholas Seckar*
-
-* Added assigns shortcut for @response.template.assigns to controller test cases *Jeremy Kemper*. Example:
-
- Before:
-
- def test_list
- assert_equal 5, @response.template.assigns['recipes'].size
- assert_equal 8, @response.template.assigns['categories'].size
- end
-
- After:
-
- def test_list
- assert_equal 5, assigns(:recipes).size
- assert_equal 8, assigns(:categories).size
- end
-
-* Added TagHelper#image_tag and deprecated UrlHelper#link_image_to (recommended approach is to combine image_tag and link_to instead)
-
-* Fixed textilize to be resilient to getting nil parsed (by using Object#blank? instead of String#empty?)
-
-* Fixed that the :multipart option in FormTagHelper#form_tag would be ignored *Yonatan Feldman*
-
-
-## 1.5.1 (7th March, 2005) ##
-
-* Fixed that the routes.rb file wouldn't be found on symlinked setups due to File.expand_path #793 *piotr@t-p-l.com*
-
-* Changed ActiveRecordStore to use Marshal instead of YAML as the latter proved troublesome in persisting circular dependencies. Updating existing applications MUST clear their existing session table from data to start using this updated store #739 *Jamis Buck*
-
-* Added shortcut :id assignment to render_component and friends (before you had to go through :params) #784 *Lucas Carlson*
-
-* Fixed that map.connect should convert arguments to strings #780 *Nicholas Seckar*
-
-* Added UrlHelper#link_to_if/link_to_unless to enable other conditions that just link_to_unless_current #757 *mindel*
-
-* Fixed that single quote was not escaped in a UrlHelper#link_to javascript confirm #549 *Scott Barron*
-
-* Removed the default border on link_image_to (it broke xhtml strict) -- can be specified with :border => 0 #517 *?/caleb*
-
-* Fixed that form helpers would treat string and symbol keys differently in html_options (and possibly create duplicate entries) #112 *Jeremy Kemper*
-
-* Fixed that broken pipe errors (clients disconnecting in mid-request) could bring down a fcgi process
-
-* Added the original exception message to session recall errors (so you can see which class wasnt required)
-
-* Fixed that RAILS_ROOT might not be defined when AP was loaded, so do a late initialization of the ROUTE_FILE #761 *Scott Barron*
-
-* Fix request.path_info and clear up LoadingModule behavior #754 *Nicholas Seckar*
-
-* Fixed caching to be aware of extensions (so you can cache files like api.wsdl or logo.png) #734 *Nicholas Seckar*
-
-* Fixed that Routes would raise NameErrors if a controller component contains characters that are not valid constant names #733 *Nicholas Seckar*
-
-* Added PATH_INFO access from the request that allows urls like the following to be interpreted by rails: http://www.example.com/dispatcher.cgi/controller/action -- that makes it possible to use rails as a CGI under lighttpd and would also allow (for example) Rublog to be ported to rails without breaking existing links to Rublog-powered blogs. #728 *Jamis Buck*
-
-* Fixed that caching the root would result in .html not index.html #731, #734 *alisdair/Nicholas Seckar*
-
-
-## 1.5.0 (24th February, 2005) ##
-
-* Added Routing as a replacement for mod_rewrite pretty urls [Nicholas Seckar]. Read more in ActionController::Base.url_for and on http://manuals.rubyonrails.com/read/book/9
-
-* Added components that allows you to call other actions for their rendered response while execution another action. You can either delegate the entire response rendering or you can mix a partial response in with your other content. Read more on http://manuals.rubyonrails.com/read/book/14
-
-* Fixed that proxy IPs do not follow all RFC1918 nets #251 *caleb@aei-tech.com*
-
-* Added Base#render_to_string to parse a template and get the result back as a string #479
-
-* Fixed that send_file/data can work even if render* has been called before in action processing to render the content of a file to be send for example #601
-
-* Added FormOptionsHelper#time_zone_select and FormOptionsHelper#time_zone_options_for_select to work with the new value object TimeZone in Active Support #688 *Jamis Buck*
-
-* Added FormHelper#file_field and FormTagHelper#file_field_tag for creating file upload fields
-
-* Added :order option for date_select that allows control over the order in which the date dropdowns is used and which of them should be used #619 [Tim Bates]. Examples:
-
- date_select("post", "written_on", :order => [:day, :month, :year])
- date_select("user", "birthday", :order => [:month, :day])
-
-* Added ActionView::Base.register_template_handler for easy integration of an alternative template language to ERb and Builder. See test/controller/custom_handler_test.rb for a usage example #656 *Jamis Buck*
-
-* Added AssetTagHelper that provides methods for linking a HTML page together with other assets, such as javascripts, stylesheets, and feeds.
-
-* Added FormTagHelper that provides a number of methods for creating form tags that doesn't rely on conventions with an object assigned to the template like FormHelper does. With the FormTagHelper, you provide the names and values yourself.
-
-* Added Afghanistan, Iran, and Iraq to the countries list used by FormOptions#country_select and FormOptions#country_options_for_select
-
-* Renamed link_to_image to link_image_to (since thats what it actually does) -- kept alias for the old method name
-
-* Fixed textilize for RedCloth3 to keep doing hardbreaks
-
-* Fixed that assert_template_xpath_matches did not indicate when a path was not found #658 *Eric Hodel*
-
-* Added TextHelper#auto_link to turn email addresses and urls into ahrefs
-
-* Fixed that on validation errors, scaffold couldn't find template #654 *mindel*
-
-* Added Base#hide_action(*names) to hide public methods from a controller that would otherwise have been callable through the URL. For the majority of cases, its preferred just to make the methods you don't want to expose protected or private (so they'll automatically be hidden) -- but if you must have a public method, this is a way to make it uncallable. Base#hidden_actions retrieve the list of all hidden actions for the controller #644 *Nicholas Seckar*
-
-* Fixed that a bunch of methods from ActionController::Base was accessible as actions (callable through a URL) when they shouldn't have been #644 *Nicholas Seckar*
-
-* Added UrlHelper#current_page?(options) method to check if the url_for options passed corresponds to the current page
-
-* Fixed https handling on other ports than 443 *Alan Gano*
-
-* Added follow_redirect method for functional tests that'll get-request the redirect that was made. Example:
-
- def test_create_post
- post :create, "post" => { "title" => "Exciting!" }
- assert_redirected_to :action => "show"
-
- follow_redirect
- assert_rendered_file "post/show"
- end
-
- Limitation: Only works for redirects to other actions within the same controller.
-
-* Fixed double requiring of models with the same name as the controller
-
-* Fixed that query params could be forced to nil on a POST due to the raw post fix #562 *moriq@moriq.com*
-
-* Fixed that cookies shouldn't be frozen in TestRequest #571 *Eric Hodel*
-
-
-## 1.4.0 (January 25th, 2005) ##
-
-* Fixed problems with ActiveRecordStore under the development environment in Rails
-
-* Fixed the ordering of attributes in the xml-decleration of Builder #540 *woeye*
-
-* Added @request.raw_post as a convenience access to @request.env['RAW_POST_DATA'] #534 *Tobias Lütke*
-
-* Added support for automatic id-based indexing for lists of items #532 [dblack]. Example:
-
- <% @students.each do |@student| %>
- <%= text_field "student[]", "first_name", :size => "20" %>
- <%= text_field "student[]", "last_name" %>
- <%= text_field "student[]", "grade", :size => "5" %>
- <% end %>
-
- ...would produce, for say David Black with id 123 and a grace of C+:
-
-
-
-
-
-* Added :application_prefix to url_for and friends that makes it easier to setup Rails in non-vhost environments #516 *Jamis Buck*
-
-* Added :encode option to mail_to that'll allow you to masquarede the email address behind javascript or hex encoding #494 *Lucas Carlson*
-
-* Fixed that the content-header was being set to application/octet_stream instead of application/octet-stream on send_date/file *Alexey*
-
-* Removed the need for passing the binding when using CacheHelper#cache
-
-* Added TestResponse#binary_content that'll return as a string the data sent through send_data/send_file for testing #500 *Alexey*
-
-* Added @request.env['RAW_POST_DATA'] for people who need access to the data before Ruby's CGI has parsed it #505 *Jeremy Kemper*
-
-* Fixed that a default fragment store wan't being set to MemoryStore as intended.
-
-* Fixed that all redirect and render calls now return true, so you can use the pattern of "do and return". Example:
-
- def show
- redirect_to(:action => "login") and return unless @person.authenticated?
- render_text "I won't happen unless the person is authenticated"
- end
-
-* Added that renders and redirects called in before_filters will have the same effect as returning false: stopping the chain. Example:
-
- class WeblogController
- before_filter { |c| c.send(:redirect_to_url("http://www.farfaraway.com/")}) }
-
- def hello
- render_text "I will never be called"
- end
- end
-
-
-* Added that only one render or redirect can happen per action. The first call wins and subsequent calls are ignored. Example:
-
- def do_something
- redirect_to :action => "elsewhere"
- render_action "overthere"
- end
-
- Only the redirect happens. The rendering call is simply ignored.
-
-
-## 1.3.1 (January 18th, 2005) ##
-
-* Fixed a bug where cookies wouldn't be set if a symbol was used instead of a string as the key
-
-* Added assert_cookie_equal to assert the contents of a named cookie
-
-* Fixed bug in page caching that prevented it from working at all
-
-
-## 1.3.0 (January 17th, 2005) ##
-
-* Added an extensive caching module that offers three levels of granularity (page, action, fragment) and a variety of stores.
- Read more in ActionController::Caching.
-
-* Added the option of passing a block to ActiveRecordHelper#form in order to add more to the auto-generated form #469 *dom@sisna.com*
-
- form("entry", :action => "sign") do |form|
- form << content_tag("b", "Department")
- form << collection_select("department", "id", @departments, "id", "name")
- end
-
-* Added arrays as a value option for params in url_for and friends #467 [Eric Anderson]. Example:
-
- url_for(:controller => 'user', :action => 'delete', :params => { 'username' => %( paul john steve ) } )
- # => /user/delete?username[]=paul&username[]=john&username[]=steve
-
-* Fixed that controller tests can now assert on the use of cookies #466 *Alexey*
-
-* Fixed that send_file would "remember" all the files sent by adding to the headers again and again #458 *Jeremy Kemper*
-
-* Fixed url rewriter confusion when the controller or action name was a substring of the controller_prefix or action_prefix
-
-* Added conditional layouts like layout "weblog_standard", :except => :rss #452 *Marcel Molina Jr.*
-
-* Fixed that MemCacheStore wasn't included by default and added default MemCache object pointing to localhost #447 *Lucas Carlson*
-
-* Added fourth argument to render_collection_of_partials that allows you to specify local_assigns -- just like render_partial #432 *Ryan Davis*
-
-* Fixed that host would choke when cgi.host returned nil #432 *Tobias Lütke*
-
-* Added that form helpers now take an index option #448 *Tim Bates*
-
- Example:
- text_field "person", "name", "index" => 3
-
- Becomes:
-
-
-* Fixed three issues with retrying breakpoints #417 *Florian Gross*
-
- 1. Don't screw up pages that use multiple values for the same parameter (?foo=bar&foo=qux was converted to ?foo=barqux)
- 2. Don't screw up all forms when you click the "Retry with Breakpoint" link multiple times instead of reloading
- (This caused the parameters to be added multiple times for GET forms leading to trouble.)
- 3. Don't add ?BP-RETRY=1 multiple times
-
-* Added that all renders and redirects now return false, so they can be used as the last line in before_filters to stop execution.
-
- Before:
- def authenticate
- unless @session[:authenticated]
- redirect_to :controller => "account", :action => "login"
- return false
- end
- end
-
- After:
- def authenticate
- redirect_to(:controller => "account", :action => "login") unless @session[:authenticated]
- end
-
-* Added conditional filters #431 [Marcel Molina Jr.]. Example:
-
- class JournalController < ActionController::Base
- # only require authentication if the current action is edit or delete
- before_filter :authorize, :only_on => [ :edit, :delete ]
-
- private
- def authorize
- # redirect to login unless authenticated
- end
- end
-
-* Added Base#render_nothing as a cleaner way of doing render_text "" when you're not interested in returning anything but an empty response.
-
-* Added the possibility of passing nil to UrlHelper#link_to to use the link itself as the name
-
-
-## 1.2.0 (January 4th, 2005) ##
-
-* Added MemCacheStore for storing session data in Danga's MemCache system *Bob Cottrell*
- Depends on: MemCached server (http://www.danga.com/memcached/), MemCache client (http://raa.ruby-lang.org/project/memcache/)
-
-* Added thread-safety to the DRbStore #66, #389 *Ben Stiglitz*
-
-* Added DateHelper#select_time and DateHelper#select_second #373 *Scott Baron*
-
-* Added :host and :protocol options to url_for and friends to redirect to another host and protocol than the current.
-
-* Added class declaration for the MissingFile exception #388 *Kent Sibilev*
-
-* Added "short hypertext note with a hyperlink to the new URI(s)" to redirects to fulfill compliance with RFC 2616 (HTTP/1.1) section 10.3.3 #397 *Tim Bates*
-
-* Added second boolean parameter to Base.redirect_to_url and Response#redirect to control whether the redirect is permanent or not (301 vs 302) #375 *Hodel*
-
-* Fixed redirects when the controller and action is named the same. Still haven't fixed same controller, module, and action, though #201 *Josh Peek*
-
-* Fixed problems with running multiple functional tests in Rails under 1.8.2 by including hack for test/unit weirdness
-
-* Fixed that @request.remote_ip didn't work in the test environment #369 *Bruno Mattarollo*
-
-
-## 1.1.0 ##
-
-* Added search through session to clear out association caches at the end of each request. This makes it possible to place Active Record objects
- in the session without worrying about stale data in the associations (the main object is still subject to caching, naturally) #347 *Tobias Lütke*
-
-* Added more informative exception when using helper :some_helper and the helper requires another file that fails, you'll get an
- error message tells you what file actually failed to load, rather than falling back on assuming it was the helper file itself #346 *dblack*
-
-* Added use of *_before_type_cast for all input and text fields. This is helpful for getting "100,000" back on a integer-based
- validation where the value would normally be "100".
-
-* Added Request#port_string to get something like ":8080" back on 8080 and "" on 80 (or 443 with https).
-
-* Added Request#domain (returns string) and Request#subdomains (returns array).
-
-* Added POST support for the breakpoint retries, so form processing that raises an exception can be retried with the original request *Florian Gross*
-
-* Fixed regression with Base#reset_session that wouldn't use the DEFAULT_SESSION_OPTIONS *adam@the-kramers.net*
-
-* Fixed error rendering of rxml documents to not just swallow the exception and return 0 (still not guessing the right line, but hey)
-
-* Fixed that textilize and markdown would instantiate their engines even on empty strings. This also fixes #333 *Ulysses*
-
-* Fixed UrlHelper#link_to_unless so it doesn't care if the id is a string or fixnum *Ryan Davis*
-
-
-## 1.0.1 ##
-
-* Fixed a bug that would cause an ApplicationController to require itself three times and hence cause filters to be run three times *evl*
-
-
-## 1.0 ##
-
-* Added that controllers will now attempt to require a model dependency with their name and in a singular attempt for their name.
- So both PostController and PostsController will automatically have the post.rb model required. If no model is found, no error is raised,
- as it is then expected that no match is available and the programmer will have included his own models.
-
-* Fixed DateHelper#date_select so that you can pass include_blank as an option even if you don't use start_year and end_year #59 *what-a-day*
-
-* Added that controllers will now search for a layout in $template_root/layouts/$controller_name.r(html|xml), so PostsController will look
- for layouts/posts.rhtml or layouts/posts.rxml and automatically configure this layout if found #307 *Marcel Molina Jr.*
-
-* Added FormHelper#radio_button to work with radio buttons like its already possible with check boxes *Michael Koziarski*
-
-* Added TemplateError#backtrace that makes it much easier to debug template errors from unit and functional tests
-
-* Added display of error messages with scaffolded form pages
-
-* Added option to ERB templates to swallow newlines by using <% if something -%> instead of just <% if something %>. Example:
-
- class SomeController < ApplicationController
- <% if options[:scaffold] %>
- scaffold :<%= singular_name %>
- <% end %>
- helper :post
-
- ...produces this on post as singular_name:
-
- class SomeController < ApplicationController
-
- scaffold :post
-
- helper :post
-
- ...where as:
-
- class SomeController < ApplicationController
- <% if options[:scaffold] -%>
- scaffold :<%= singular_name %>
- <% end -%>
- helper :post
-
- ...produces:
-
- class SomeController < ApplicationController
- scaffold :post
- helper :post
-
- *This undocumented gem for ERb was uncovered by bitsweat*
-
-* Fixed CgiRequest so that it'll now accept session options with Symbols as keys (as the documentation points out) *Suggested by Andreas*
-
-* Added that render_partial will always by default include a counter with value 1 unless there is a counter passed in via the
- local_assigns hash that overrides it. As a result, render_collection_of_partials can still be written in terms of render_partial
- and partials that make use of a counter can be called without problems from both render_collection_of_partials as well as
- render_partial #295 *Marcel Molina Jr.*
-
-* Fixed CgiRequest#out to fall back to #write if $stdout doesn't have #syswrite *Jeremy Kemper*
-
-* Fixed all helpers so that they use XHTML compliant double quotes for values instead of single quotes *htonl/Jeremy Kemper*
-
-* Added link_to_image(src, options = {}, html_options = {}). Documentation:
-
- Creates a link tag to the image residing at the +src+ using an URL created by the set of +options+. See the valid options in
- link:classes/ActionController/Base.html#M000021. It's also possible to pass a string instead of an options hash to
- get a link tag that just points without consideration. The html_options works jointly for the image and ahref tag by
- letting the following special values enter the options on the image and the rest goes to the ahref:
-
- ::alt: If no alt text is given, the file name part of the +src+ is used (capitalized and without the extension)
- ::size: Supplied as "XxY", so "30x45" becomes width="30" and height="45"
- ::align: Sets the alignment, no special features
-
- The +src+ can be supplied as a...
- * full path, like "/my_images/image.gif"
- * file name, like "rss.gif", that gets expanded to "/images/rss.gif"
- * file name without extension, like "logo", that gets expanded to "/images/logo.png"
-
-* Fixed to_input_field_tag so it no longer explicitly uses InstanceTag.value if value was specified in the options hash *evl*
-
-* Added the possibility of having validate be protected for assert_(in)valid_column #263 *Tobias Lütke*
-
-* Added that ActiveRecordHelper#form now calls url_for on the :action option.
-
-* Added all the HTTP methods as alternatives to the generic "process" for functional testing #276 *Tobias Lütke*. Examples:
-
- # Calls Controller#miletone with a GET request
- process :milestone
-
- # Calls Controller#miletone with a POST request that has parameters
- post :milestone, { "name" => "David" }
-
- # Calls Controller#milestone with a HEAD request that has both parameters and session data
- head :milestone, { "id" => 1 }, { "user_id" => 23 }
-
- This is especially useful for testing idiomatic REST web services.
-
-* Added proper handling of HEAD requests, so that content isn't returned (Request#head? added as well) #277 *Eric Hodel*
-
-* Added indifference to whether @headers["Content-Type"], @headers["Content-type"], or @headers["content-type"] is used.
-
-* Added TestSession#session_id that returns an empty string to make it easier to functional test applications that doesn't use
- cookie-based sessions #275 *jcf*
-
-* Fixed that cached template loading would still check the file system to see if the file existed #258 *Andreas Schwarz*
-
-* Added options to tailor header tag, div id, and div class on ActiveRecordHelper#error_messages_for *Josh Peek*
-
-* Added graceful handling of non-alphanumeric names and misplaced brackets in input parameters *Jeremy Kemper*
-
-* Added a new container for cookies that makes them more intuative to use. The old methods of cookie and @cookies have been deprecated.
-
- Examples for writing:
-
- cookies["user_name"] = "david" # => Will set a simple session cookie
- cookies["login"] = { "value" => "XJ-122", "expires" => Time.now + 360} # => Will set a cookie that expires in 1 hour
-
- Examples for reading:
-
- cookies["user_name"] # => "david"
- cookies.size # => 2
-
- Read more in ActionController::Cookies
-
- NOTE: If you were using the old accessor (cookies instead of @cookies), this could potentially break your code -- if you expect a full cookie object!
-
-* Added the opportunity to defined method_missing on a controller which will handle all requests for actions not otherwise defined #223 *timb*
-
-* Fixed AbstractRequest#remote_ip for users going through proxies - Patch #228 *Eric Hodel*
-
-* Added Request#ssl? which is shorthand for @request.protocol == "https://"
-
-* Added the choice to call form_tag with no arguments (resulting in a form posting to current action) *Jeremy Kemper*
-
-* Upgraded to Builder 1.2.1
-
-* Added :module as an alias for :controller_prefix to url_for and friends, so you can do redirect_to(:module => "shop", :controller => "purchases")
- and go to /shop/purchases/
-
-* Added support for controllers in modules through @params["module"].
-
-* Added reloading for dependencies under cached environments like FastCGI and mod_ruby. This makes it possible to use those environments for development.
- This is turned on by default, but can be turned off with ActionController::Base.reload_dependencies = false in production environments.
-
- NOTE: This will only have an effect if you use the new model, service, and observer class methods to mark dependencies. All libraries loaded through
- require will be "forever" cached. You can, however, use ActionController::Base.load_or_require("library") to get this behavior outside of the new
- dependency style.
-
-* Added that controllers will automatically require their own helper if possible. So instead of doing:
-
- class MsgController < ApplicationController
- helper :msg
- end
-
- ...you can just do:
-
- class MsgController < ApplicationController
- end
-
-* Added dependencies_on(layer) to query the dependencies of a controller. Examples:
-
- MsgController.dependencies_on(:model) # => [ :post, :comment, :attachment ]
- MsgController.dependencies_on(:service) # => [ :notification_service ]
- MsgController.dependencies_on(:observer) # => [ :comment_observer ]
-
-* Added a new dependency model with the class methods model, service, and observer. Example:
-
- class MsgController < ApplicationController
- model :post, :comment, :attachment
- service :notification_service
- observer :comment_observer
- end
-
- These new "keywords" remove the need for explicitly calling 'require' in most cases. The observer method even instantiates the
- observer as well as requiring it.
-
-* Fixed that link_to would escape & in the url again after url_for already had done so
-
-
-## 0.9.5 (28) ##
-
-* Added helper_method to designate that a given private or protected method you should available as a helper in the view. *Jeremy Kemper*
-
-* Fixed assert_rendered_file so it actually verifies if that was the rendered file *htonl*
-
-* Added the option for sharing partial spacer templates just like partials themselves *radsaq*
-
-* Fixed that Russia was named twice in country_select *alexey*
-
-* Fixed request_origin to use remote_ip instead of remote_addr *Jeremy Kemper*
-
-* Fixed link_to breakage when nil was passed for html_options *alexey*
-
-* Fixed redirect_to on a virtual server setup with apache with a port other than the default where it would forget the port number *seanohalpin*
-
-* Fixed that auto-loading webrick on Windows would cause file uploads to fail *Jeremy Kemper*
-
-* Fixed issues with sending files on WEBrick by setting the proper binmode *Jeremy Kemper*
-
-* Added send_data as an alternative to send_file when the stream is not read off the filesystem but from a database or generated live *Jeremy Kemper*
-
-* Added a new way to include helpers that doesn't require the include hack and can go without the explicit require. *Jeremy Kemper*
-
- Before:
-
- module WeblogHelper
- def self.included(controller) #:nodoc:
- controller.ancestors.include?(ActionController::Base) ? controller.add_template_helper(self) : super
- end
- end
-
- require 'weblog_helper'
- class WeblogController < ActionController::Base
- include WeblogHelper
- end
-
- After:
-
- module WeblogHelper
- end
-
- class WeblogController < ActionController::Base
- helper :weblog
- end
-
-* Added a default content-type of "text/xml" to .rxml renders *Ryan Platte*
-
-* Fixed that when /controller/index was requested by the browser, url_for would generates wrong URLs *Ryan Platte*
-
-* Fixed a bug that would share cookies between users when using FastCGI and mod_ruby *The Robot Co-op*
-
-* Added an optional third hash parameter to the process method in functional tests that takes the session data to be used *alexey*
-
-* Added UrlHelper#mail_to to make it easier to create mailto: style ahrefs
-
-* Added better error messages for layouts declared with the .rhtml extension (which they shouldn't) *geech*
-
-* Added another case to DateHelper#distance_in_minutes to return "less than a minute" instead of "0 minutes" and "1 minute" instead of "1 minutes"
-
-* Added a hidden field to checkboxes generated with FormHelper#check_box that will make sure that the unchecked value (usually 0)
- is sent even if the checkbox is not checked. This relieves the controller from doing custom checking if the checkbox wasn't
- checked. BEWARE: This might conflict with your run-on-the-mill work-around code. *Tobias Lütke*
-
-* Fixed error_message_on to just use the first if more than one error had been added *Marcel Molina Jr.*
-
-* Fixed that URL rewriting with /controller/ was working but /controller was not and that you couldn't use :id on index *geech*
-
-* Fixed a bug with link_to where the :confirm option wouldn't be picked up if the link was a straight url instead of an option hash
-
-* Changed scaffolding of forms to use