actionmailbox 6.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +41 -0
  3. data/MIT-LICENSE +21 -0
  4. data/README.md +13 -0
  5. data/app/controllers/action_mailbox/base_controller.rb +34 -0
  6. data/app/controllers/action_mailbox/ingresses/mailgun/inbound_emails_controller.rb +103 -0
  7. data/app/controllers/action_mailbox/ingresses/mandrill/inbound_emails_controller.rb +82 -0
  8. data/app/controllers/action_mailbox/ingresses/postmark/inbound_emails_controller.rb +62 -0
  9. data/app/controllers/action_mailbox/ingresses/relay/inbound_emails_controller.rb +65 -0
  10. data/app/controllers/action_mailbox/ingresses/sendgrid/inbound_emails_controller.rb +54 -0
  11. data/app/controllers/rails/conductor/action_mailbox/inbound_emails_controller.rb +35 -0
  12. data/app/controllers/rails/conductor/action_mailbox/reroutes_controller.rb +19 -0
  13. data/app/controllers/rails/conductor/base_controller.rb +14 -0
  14. data/app/jobs/action_mailbox/incineration_job.rb +25 -0
  15. data/app/jobs/action_mailbox/routing_job.rb +13 -0
  16. data/app/models/action_mailbox/inbound_email.rb +49 -0
  17. data/app/models/action_mailbox/inbound_email/incineratable.rb +20 -0
  18. data/app/models/action_mailbox/inbound_email/incineratable/incineration.rb +26 -0
  19. data/app/models/action_mailbox/inbound_email/message_id.rb +38 -0
  20. data/app/models/action_mailbox/inbound_email/routable.rb +24 -0
  21. data/app/views/layouts/rails/conductor.html.erb +8 -0
  22. data/app/views/rails/conductor/action_mailbox/inbound_emails/index.html.erb +15 -0
  23. data/app/views/rails/conductor/action_mailbox/inbound_emails/new.html.erb +47 -0
  24. data/app/views/rails/conductor/action_mailbox/inbound_emails/show.html.erb +15 -0
  25. data/config/routes.rb +19 -0
  26. data/db/migrate/20180917164000_create_action_mailbox_tables.rb +13 -0
  27. data/lib/action_mailbox.rb +17 -0
  28. data/lib/action_mailbox/base.rb +118 -0
  29. data/lib/action_mailbox/callbacks.rb +34 -0
  30. data/lib/action_mailbox/engine.rb +33 -0
  31. data/lib/action_mailbox/gem_version.rb +17 -0
  32. data/lib/action_mailbox/mail_ext.rb +6 -0
  33. data/lib/action_mailbox/mail_ext/address_equality.rb +9 -0
  34. data/lib/action_mailbox/mail_ext/address_wrapping.rb +9 -0
  35. data/lib/action_mailbox/mail_ext/addresses.rb +29 -0
  36. data/lib/action_mailbox/mail_ext/from_source.rb +7 -0
  37. data/lib/action_mailbox/mail_ext/recipients.rb +9 -0
  38. data/lib/action_mailbox/relayer.rb +75 -0
  39. data/lib/action_mailbox/router.rb +42 -0
  40. data/lib/action_mailbox/router/route.rb +42 -0
  41. data/lib/action_mailbox/routing.rb +22 -0
  42. data/lib/action_mailbox/test_case.rb +12 -0
  43. data/lib/action_mailbox/test_helper.rb +48 -0
  44. data/lib/action_mailbox/version.rb +10 -0
  45. data/lib/rails/generators/installer.rb +10 -0
  46. data/lib/rails/generators/mailbox/USAGE +12 -0
  47. data/lib/rails/generators/mailbox/mailbox_generator.rb +32 -0
  48. data/lib/rails/generators/mailbox/templates/application_mailbox.rb.tt +3 -0
  49. data/lib/rails/generators/mailbox/templates/mailbox.rb.tt +4 -0
  50. data/lib/rails/generators/test_unit/mailbox_generator.rb +20 -0
  51. data/lib/rails/generators/test_unit/templates/mailbox_test.rb.tt +11 -0
  52. data/lib/tasks/ingress.rake +72 -0
  53. data/lib/tasks/install.rake +20 -0
  54. metadata +186 -0
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rails
4
+ class Conductor::ActionMailbox::InboundEmailsController < Rails::Conductor::BaseController
5
+ def index
6
+ @inbound_emails = ActionMailbox::InboundEmail.order(created_at: :desc)
7
+ end
8
+
9
+ def new
10
+ end
11
+
12
+ def show
13
+ @inbound_email = ActionMailbox::InboundEmail.find(params[:id])
14
+ end
15
+
16
+ def create
17
+ inbound_email = create_inbound_email(new_mail)
18
+ redirect_to main_app.rails_conductor_inbound_email_url(inbound_email)
19
+ end
20
+
21
+ private
22
+ def new_mail
23
+ Mail.new(params.require(:mail).permit(:from, :to, :cc, :bcc, :in_reply_to, :subject, :body).to_h).tap do |mail|
24
+ mail[:bcc]&.include_in_headers = true
25
+ params[:mail][:attachments].to_a.each do |attachment|
26
+ mail.add_file(filename: attachment.original_filename, content: attachment.read)
27
+ end
28
+ end
29
+ end
30
+
31
+ def create_inbound_email(mail)
32
+ ActionMailbox::InboundEmail.create_and_extract_message_id!(mail.to_s)
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rails
4
+ # Rerouting will run routing and processing on an email that has already been, or attempted to be, processed.
5
+ class Conductor::ActionMailbox::ReroutesController < Rails::Conductor::BaseController
6
+ def create
7
+ inbound_email = ActionMailbox::InboundEmail.find(params[:inbound_email_id])
8
+ reroute inbound_email
9
+
10
+ redirect_to main_app.rails_conductor_inbound_email_url(inbound_email)
11
+ end
12
+
13
+ private
14
+ def reroute(inbound_email)
15
+ inbound_email.pending!
16
+ inbound_email.route_later
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rails
4
+ # TODO: Move this to Rails::Conductor gem
5
+ class Conductor::BaseController < ActionController::Base
6
+ layout "rails/conductor"
7
+ before_action :ensure_development_env
8
+
9
+ private
10
+ def ensure_development_env
11
+ head :forbidden unless Rails.env.development?
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionMailbox
4
+ # You can configure when this +IncinerationJob+ will be run as a time-after-processing using the
5
+ # +config.action_mailbox.incinerate_after+ or +ActionMailbox.incinerate_after+ setting.
6
+ #
7
+ # Since this incineration is set for the future, it'll automatically ignore any <tt>InboundEmail</tt>s
8
+ # that have already been deleted and discard itself if so.
9
+ #
10
+ # You can disable incinerating processed emails by setting +config.action_mailbox.incinerate+ or
11
+ # +ActionMailbox.incinerate+ to +false+.
12
+ class IncinerationJob < ActiveJob::Base
13
+ queue_as { ActionMailbox.queues[:incineration] }
14
+
15
+ discard_on ActiveRecord::RecordNotFound
16
+
17
+ def self.schedule(inbound_email)
18
+ set(wait: ActionMailbox.incinerate_after).perform_later(inbound_email)
19
+ end
20
+
21
+ def perform(inbound_email)
22
+ inbound_email.incinerate
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionMailbox
4
+ # Routing a new InboundEmail is an asynchronous operation, which allows the ingress controllers to quickly
5
+ # accept new incoming emails without being burdened to hang while they're actually being processed.
6
+ class RoutingJob < ActiveJob::Base
7
+ queue_as { ActionMailbox.queues[:routing] }
8
+
9
+ def perform(inbound_email)
10
+ inbound_email.route
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "mail"
4
+
5
+ module ActionMailbox
6
+ # The +InboundEmail+ is an Active Record that keeps a reference to the raw email stored in Active Storage
7
+ # and tracks the status of processing. By default, incoming emails will go through the following lifecycle:
8
+ #
9
+ # * Pending: Just received by one of the ingress controllers and scheduled for routing.
10
+ # * Processing: During active processing, while a specific mailbox is running its #process method.
11
+ # * Delivered: Successfully processed by the specific mailbox.
12
+ # * Failed: An exception was raised during the specific mailbox's execution of the +#process+ method.
13
+ # * Bounced: Rejected processing by the specific mailbox and bounced to sender.
14
+ #
15
+ # Once the +InboundEmail+ has reached the status of being either +delivered+, +failed+, or +bounced+,
16
+ # it'll count as having been +#processed?+. Once processed, the +InboundEmail+ will be scheduled for
17
+ # automatic incineration at a later point.
18
+ #
19
+ # When working with an +InboundEmail+, you'll usually interact with the parsed version of the source,
20
+ # which is available as a +Mail+ object from +#mail+. But you can also access the raw source directly
21
+ # using the +#source+ method.
22
+ #
23
+ # Examples:
24
+ #
25
+ # inbound_email.mail.from # => 'david@loudthinking.com'
26
+ # inbound_email.source # Returns the full rfc822 source of the email as text
27
+ class InboundEmail < ActiveRecord::Base
28
+ self.table_name = "action_mailbox_inbound_emails"
29
+
30
+ include Incineratable, MessageId, Routable
31
+
32
+ has_one_attached :raw_email
33
+ enum status: %i[ pending processing delivered failed bounced ]
34
+
35
+ def mail
36
+ @mail ||= Mail.from_source(source)
37
+ end
38
+
39
+ def source
40
+ @source ||= raw_email.download
41
+ end
42
+
43
+ def processed?
44
+ delivered? || failed? || bounced?
45
+ end
46
+ end
47
+ end
48
+
49
+ ActiveSupport.run_load_hooks :action_mailbox_inbound_email, ActionMailbox::InboundEmail
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Ensure that the +InboundEmail+ is automatically scheduled for later incineration if the status has been
4
+ # changed to +processed+. The later incineration will be invoked at the time specified by the
5
+ # +ActionMailbox.incinerate_after+ time using the +IncinerationJob+.
6
+ module ActionMailbox::InboundEmail::Incineratable
7
+ extend ActiveSupport::Concern
8
+
9
+ included do
10
+ after_update_commit :incinerate_later, if: -> { ActionMailbox.incinerate && status_previously_changed? && processed? }
11
+ end
12
+
13
+ def incinerate_later
14
+ ActionMailbox::IncinerationJob.schedule self
15
+ end
16
+
17
+ def incinerate
18
+ Incineration.new(self).run
19
+ end
20
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionMailbox
4
+ # Command class for carrying out the actual incineration of the +InboundMail+ that's been scheduled
5
+ # for removal. Before the incineration – which really is just a call to +#destroy!+ – is run, we verify
6
+ # that it's both eligible (by virtue of having already been processed) and time to do so (that is,
7
+ # the +InboundEmail+ was processed after the +incinerate_after+ time).
8
+ class InboundEmail::Incineratable::Incineration
9
+ def initialize(inbound_email)
10
+ @inbound_email = inbound_email
11
+ end
12
+
13
+ def run
14
+ @inbound_email.destroy! if due? && processed?
15
+ end
16
+
17
+ private
18
+ def due?
19
+ @inbound_email.updated_at < ActionMailbox.incinerate_after.ago.end_of_day
20
+ end
21
+
22
+ def processed?
23
+ @inbound_email.processed?
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The +Message-ID+ as specified by rfc822 is supposed to be a unique identifier for that individual email.
4
+ # That makes it an ideal tracking token for debugging and forensics, just like +X-Request-Id+ does for
5
+ # web request.
6
+ #
7
+ # If an inbound email does not, against the rfc822 mandate, specify a Message-ID, one will be generated
8
+ # using the approach from <tt>Mail::MessageIdField</tt>.
9
+ module ActionMailbox::InboundEmail::MessageId
10
+ extend ActiveSupport::Concern
11
+
12
+ class_methods do
13
+ # Create a new +InboundEmail+ from the raw +source+ of the email, which be uploaded as a Active Storage
14
+ # attachment called +raw_email+. Before the upload, extract the Message-ID from the +source+ and set
15
+ # it as an attribute on the new +InboundEmail+.
16
+ def create_and_extract_message_id!(source, **options)
17
+ message_checksum = Digest::SHA1.hexdigest(source)
18
+ message_id = extract_message_id(source) || generate_missing_message_id(message_checksum)
19
+
20
+ create! options.merge(message_id: message_id, message_checksum: message_checksum) do |inbound_email|
21
+ inbound_email.raw_email.attach io: StringIO.new(source), filename: "message.eml", content_type: "message/rfc822"
22
+ end
23
+ rescue ActiveRecord::RecordNotUnique
24
+ nil
25
+ end
26
+
27
+ private
28
+ def extract_message_id(source)
29
+ Mail.from_source(source).message_id rescue nil
30
+ end
31
+
32
+ def generate_missing_message_id(message_checksum)
33
+ Mail::MessageIdField.new("<#{message_checksum}@#{::Socket.gethostname}.mail>").message_id.tap do |message_id|
34
+ logger.warn "Message-ID couldn't be parsed or is missing. Generated a new Message-ID: #{message_id}"
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ # A newly received +InboundEmail+ will not be routed synchronously as part of ingress controller's receival.
4
+ # Instead, the routing will be done asynchronously, using a +RoutingJob+, to ensure maximum parallel capacity.
5
+ #
6
+ # By default, all newly created +InboundEmail+ records that have the status of +pending+, which is the default,
7
+ # will be scheduled for automatic, deferred routing.
8
+ module ActionMailbox::InboundEmail::Routable
9
+ extend ActiveSupport::Concern
10
+
11
+ included do
12
+ after_create_commit :route_later, if: :pending?
13
+ end
14
+
15
+ # Enqueue a +RoutingJob+ for this +InboundEmail+.
16
+ def route_later
17
+ ActionMailbox::RoutingJob.perform_later self
18
+ end
19
+
20
+ # Route this +InboundEmail+ using the routing rules declared on the +ApplicationMailbox+.
21
+ def route
22
+ ApplicationMailbox.route self
23
+ end
24
+ end
@@ -0,0 +1,8 @@
1
+ <html>
2
+ <head>
3
+ <title>Rails Conductor: <%= yield :title %></title>
4
+ </head>
5
+ <body>
6
+ <%= yield %>
7
+ </body>
8
+ </html>
@@ -0,0 +1,15 @@
1
+ <% provide :title, "Deliver new inbound email" %>
2
+
3
+ <h1>All inbound emails</h1>
4
+
5
+ <table>
6
+ <tr><th>Message ID</th><th>Status</th></tr>
7
+ <% @inbound_emails.each do |inbound_email| %>
8
+ <tr>
9
+ <td><%= link_to inbound_email.message_id, main_app.rails_conductor_inbound_email_path(inbound_email) %></td>
10
+ <td><%= inbound_email.status %></td>
11
+ </tr>
12
+ <% end %>
13
+ </table>
14
+
15
+ <%= link_to "Deliver new inbound email", main_app.new_rails_conductor_inbound_email_path %>
@@ -0,0 +1,47 @@
1
+ <% provide :title, "Deliver new inbound email" %>
2
+
3
+ <h1>Deliver new inbound email</h1>
4
+
5
+ <%= form_with(url: main_app.rails_conductor_inbound_emails_path, scope: :mail, local: true) do |form| %>
6
+ <div>
7
+ <%= form.label :from, "From" %><br>
8
+ <%= form.text_field :from %>
9
+ </div>
10
+
11
+ <div>
12
+ <%= form.label :to, "To" %><br>
13
+ <%= form.text_field :to %>
14
+ </div>
15
+
16
+ <div>
17
+ <%= form.label :cc, "CC" %><br>
18
+ <%= form.text_field :cc %>
19
+ </div>
20
+
21
+ <div>
22
+ <%= form.label :bcc, "BCC" %><br>
23
+ <%= form.text_field :bcc %>
24
+ </div>
25
+
26
+ <div>
27
+ <%= form.label :in_reply_to, "In-Reply-To" %><br>
28
+ <%= form.text_field :in_reply_to %>
29
+ </div>
30
+
31
+ <div>
32
+ <%= form.label :subject, "Subject" %><br>
33
+ <%= form.text_field :subject %>
34
+ </div>
35
+
36
+ <div>
37
+ <%= form.label :body, "Body" %><br>
38
+ <%= form.text_area :body, size: "40x20" %>
39
+ </div>
40
+
41
+ <div>
42
+ <%= form.label :attachments, "Attachments" %><br>
43
+ <%= form.file_field :attachments, multiple: true %>
44
+ </div>
45
+
46
+ <%= form.submit "Deliver inbound email" %>
47
+ <% end %>
@@ -0,0 +1,15 @@
1
+ <% provide :title, @inbound_email.message_id %>
2
+
3
+ <h1><%= @inbound_email.message_id %>: <%= @inbound_email.status %></h1>
4
+
5
+ <ul>
6
+ <li><%= button_to "Route again", main_app.rails_conductor_inbound_email_reroute_path(@inbound_email), method: :post %></li>
7
+ <li>Incinerate</li>
8
+ </ul>
9
+
10
+ <details>
11
+ <summary>Full email source</summary>
12
+ <pre><%= @inbound_email.source %></pre>
13
+ </details>
14
+
15
+ <%= link_to "Back to all inbound emails", main_app.rails_conductor_inbound_emails_path %>
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ Rails.application.routes.draw do
4
+ scope "/rails/action_mailbox", module: "action_mailbox/ingresses" do
5
+ post "/mandrill/inbound_emails" => "mandrill/inbound_emails#create", as: :rails_mandrill_inbound_emails
6
+ post "/postmark/inbound_emails" => "postmark/inbound_emails#create", as: :rails_postmark_inbound_emails
7
+ post "/relay/inbound_emails" => "relay/inbound_emails#create", as: :rails_relay_inbound_emails
8
+ post "/sendgrid/inbound_emails" => "sendgrid/inbound_emails#create", as: :rails_sendgrid_inbound_emails
9
+
10
+ # Mailgun requires that a webhook's URL end in 'mime' for it to receive the raw contents of emails.
11
+ post "/mailgun/inbound_emails/mime" => "mailgun/inbound_emails#create", as: :rails_mailgun_inbound_emails
12
+ end
13
+
14
+ # TODO: Should these be mounted within the engine only?
15
+ scope "rails/conductor/action_mailbox/", module: "rails/conductor/action_mailbox" do
16
+ resources :inbound_emails, as: :rails_conductor_inbound_emails
17
+ post ":inbound_email_id/reroute" => "reroutes#create", as: :rails_conductor_inbound_email_reroute
18
+ end
19
+ end
@@ -0,0 +1,13 @@
1
+ class CreateActionMailboxTables < ActiveRecord::Migration[6.0]
2
+ def change
3
+ create_table :action_mailbox_inbound_emails do |t|
4
+ t.integer :status, default: 0, null: false
5
+ t.string :message_id, null: false
6
+ t.string :message_checksum, null: false
7
+
8
+ t.timestamps
9
+
10
+ t.index [ :message_id, :message_checksum ], name: "index_action_mailbox_inbound_emails_uniqueness", unique: true
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "action_mailbox/mail_ext"
4
+
5
+ module ActionMailbox
6
+ extend ActiveSupport::Autoload
7
+
8
+ autoload :Base
9
+ autoload :Router
10
+ autoload :TestCase
11
+
12
+ mattr_accessor :ingress
13
+ mattr_accessor :logger
14
+ mattr_accessor :incinerate, default: true
15
+ mattr_accessor :incinerate_after, default: 30.days
16
+ mattr_accessor :queues, default: {}
17
+ end
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/rescuable"
4
+
5
+ require "action_mailbox/callbacks"
6
+ require "action_mailbox/routing"
7
+
8
+ module ActionMailbox
9
+ # The base class for all application mailboxes. Not intended to be inherited from directly. Inherit from
10
+ # +ApplicationMailbox+ instead, as that's where the app-specific routing is configured. This routing
11
+ # is specified in the following ways:
12
+ #
13
+ # class ApplicationMailbox < ActionMailbox::Base
14
+ # # Any of the recipients of the mail (whether to, cc, bcc) are matched against the regexp.
15
+ # routing /^replies@/i => :replies
16
+ #
17
+ # # Any of the recipients of the mail (whether to, cc, bcc) needs to be an exact match for the string.
18
+ # routing "help@example.com" => :help
19
+ #
20
+ # # Any callable (proc, lambda, etc) object is passed the inbound_email record and is a match if true.
21
+ # routing ->(inbound_email) { inbound_email.mail.to.size > 2 } => :multiple_recipients
22
+ #
23
+ # # Any object responding to #match? is called with the inbound_email record as an argument. Match if true.
24
+ # routing CustomAddress.new => :custom
25
+ #
26
+ # # Any inbound_email that has not been already matched will be sent to the BackstopMailbox.
27
+ # routing :all => :backstop
28
+ # end
29
+ #
30
+ # Application mailboxes need to overwrite the +#process+ method, which is invoked by the framework after
31
+ # callbacks have been run. The callbacks available are: +before_processing+, +after_processing+, and
32
+ # +around_processing+. The primary use case is ensure certain preconditions to processing are fulfilled
33
+ # using +before_processing+ callbacks.
34
+ #
35
+ # If a precondition fails to be met, you can halt the processing using the +#bounced!+ method,
36
+ # which will silently prevent any further processing, but not actually send out any bounce notice. You
37
+ # can also pair this behavior with the invocation of an Action Mailer class responsible for sending out
38
+ # an actual bounce email. This is done using the +#bounce_with+ method, which takes the mail object returned
39
+ # by an Action Mailer method, like so:
40
+ #
41
+ # class ForwardsMailbox < ApplicationMailbox
42
+ # before_processing :ensure_sender_is_a_user
43
+ #
44
+ # private
45
+ # def ensure_sender_is_a_user
46
+ # unless User.exist?(email_address: mail.from)
47
+ # bounce_with UserRequiredMailer.missing(inbound_email)
48
+ # end
49
+ # end
50
+ # end
51
+ #
52
+ # During the processing of the inbound email, the status will be tracked. Before processing begins,
53
+ # the email will normally have the +pending+ status. Once processing begins, just before callbacks
54
+ # and the +#process+ method is called, the status is changed to +processing+. If processing is allowed to
55
+ # complete, the status is changed to +delivered+. If a bounce is triggered, then +bounced+. If an unhandled
56
+ # exception is bubbled up, then +failed+.
57
+ #
58
+ # Exceptions can be handled at the class level using the familiar +Rescuable+ approach:
59
+ #
60
+ # class ForwardsMailbox < ApplicationMailbox
61
+ # rescue_from(ApplicationSpecificVerificationError) { bounced! }
62
+ # end
63
+ class Base
64
+ include ActiveSupport::Rescuable
65
+ include ActionMailbox::Callbacks, ActionMailbox::Routing
66
+
67
+ attr_reader :inbound_email
68
+ delegate :mail, :delivered!, :bounced!, to: :inbound_email
69
+
70
+ delegate :logger, to: ActionMailbox
71
+
72
+ def self.receive(inbound_email)
73
+ new(inbound_email).perform_processing
74
+ end
75
+
76
+ def initialize(inbound_email)
77
+ @inbound_email = inbound_email
78
+ end
79
+
80
+ def perform_processing #:nodoc:
81
+ track_status_of_inbound_email do
82
+ run_callbacks :process do
83
+ process
84
+ end
85
+ end
86
+ rescue => exception
87
+ # TODO: Include a reference to the inbound_email in the exception raised so error handling becomes easier
88
+ rescue_with_handler(exception) || raise
89
+ end
90
+
91
+ def process
92
+ # Overwrite in subclasses
93
+ end
94
+
95
+ def finished_processing? #:nodoc:
96
+ inbound_email.delivered? || inbound_email.bounced?
97
+ end
98
+
99
+
100
+ # Enqueues the given +message+ for delivery and changes the inbound email's status to +:bounced+.
101
+ def bounce_with(message)
102
+ inbound_email.bounced!
103
+ message.deliver_later
104
+ end
105
+
106
+ private
107
+ def track_status_of_inbound_email
108
+ inbound_email.processing!
109
+ yield
110
+ inbound_email.delivered! unless inbound_email.bounced?
111
+ rescue
112
+ inbound_email.failed!
113
+ raise
114
+ end
115
+ end
116
+ end
117
+
118
+ ActiveSupport.run_load_hooks :action_mailbox, ActionMailbox::Base