# frozen_string_literal: true
require "active_support/rescuable"
require "action_mailbox/callbacks"
require "action_mailbox/routing"
module ActionMailbox
# 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 overwrite 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 ensure 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 +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:
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
def process
# Overwrite 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
private
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