actionmailbox 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +2 -0
  3. data/Gemfile +8 -0
  4. data/Gemfile.lock +159 -0
  5. data/LICENSE +21 -0
  6. data/README.md +278 -0
  7. data/Rakefile +27 -0
  8. data/actionmailbox.gemspec +27 -0
  9. data/app/controllers/action_mailbox/base_controller.rb +43 -0
  10. data/app/controllers/action_mailbox/ingresses/amazon/inbound_emails_controller.rb +50 -0
  11. data/app/controllers/action_mailbox/ingresses/mailgun/inbound_emails_controller.rb +99 -0
  12. data/app/controllers/action_mailbox/ingresses/mandrill/inbound_emails_controller.rb +78 -0
  13. data/app/controllers/action_mailbox/ingresses/postfix/inbound_emails_controller.rb +55 -0
  14. data/app/controllers/action_mailbox/ingresses/sendgrid/inbound_emails_controller.rb +50 -0
  15. data/app/controllers/rails/conductor/action_mailbox/inbound_emails_controller.rb +27 -0
  16. data/app/controllers/rails/conductor/action_mailbox/reroutes_controller.rb +15 -0
  17. data/app/controllers/rails/conductor/base_controller.rb +10 -0
  18. data/app/jobs/action_mailbox/incineration_job.rb +18 -0
  19. data/app/jobs/action_mailbox/routing_job.rb +9 -0
  20. data/app/models/action_mailbox/inbound_email.rb +43 -0
  21. data/app/models/action_mailbox/inbound_email/incineratable.rb +18 -0
  22. data/app/models/action_mailbox/inbound_email/incineratable/incineration.rb +22 -0
  23. data/app/models/action_mailbox/inbound_email/message_id.rb +36 -0
  24. data/app/models/action_mailbox/inbound_email/routable.rb +22 -0
  25. data/app/views/layouts/rails/conductor.html.erb +7 -0
  26. data/app/views/rails/conductor/action_mailbox/inbound_emails/index.html.erb +15 -0
  27. data/app/views/rails/conductor/action_mailbox/inbound_emails/new.html.erb +42 -0
  28. data/app/views/rails/conductor/action_mailbox/inbound_emails/show.html.erb +15 -0
  29. data/bin/test +5 -0
  30. data/config/routes.rb +19 -0
  31. data/db/migrate/20180917164000_create_action_mailbox_tables.rb +11 -0
  32. data/lib/action_mailbox.rb +15 -0
  33. data/lib/action_mailbox/base.rb +111 -0
  34. data/lib/action_mailbox/callbacks.rb +32 -0
  35. data/lib/action_mailbox/engine.rb +34 -0
  36. data/lib/action_mailbox/mail_ext.rb +4 -0
  37. data/lib/action_mailbox/mail_ext/address_equality.rb +5 -0
  38. data/lib/action_mailbox/mail_ext/address_wrapping.rb +5 -0
  39. data/lib/action_mailbox/mail_ext/addresses.rb +25 -0
  40. data/lib/action_mailbox/mail_ext/from_source.rb +5 -0
  41. data/lib/action_mailbox/mail_ext/recipients.rb +5 -0
  42. data/lib/action_mailbox/postfix_relayer.rb +67 -0
  43. data/lib/action_mailbox/router.rb +38 -0
  44. data/lib/action_mailbox/router/route.rb +38 -0
  45. data/lib/action_mailbox/routing.rb +20 -0
  46. data/lib/action_mailbox/test_case.rb +8 -0
  47. data/lib/action_mailbox/test_helper.rb +42 -0
  48. data/lib/action_mailbox/version.rb +3 -0
  49. data/lib/tasks/ingress.rake +24 -0
  50. data/lib/tasks/install.rake +20 -0
  51. data/lib/templates/installer.rb +4 -0
  52. data/lib/templates/mailboxes/application_mailbox.rb +3 -0
  53. data/test/controllers/ingresses/amazon/inbound_emails_controller_test.rb +20 -0
  54. data/test/controllers/ingresses/mailgun/inbound_emails_controller_test.rb +89 -0
  55. data/test/controllers/ingresses/mandrill/inbound_emails_controller_test.rb +58 -0
  56. data/test/controllers/ingresses/postfix/inbound_emails_controller_test.rb +54 -0
  57. data/test/controllers/ingresses/sendgrid/inbound_emails_controller_test.rb +44 -0
  58. data/test/dummy/.babelrc +18 -0
  59. data/test/dummy/.gitignore +3 -0
  60. data/test/dummy/.postcssrc.yml +3 -0
  61. data/test/dummy/Rakefile +6 -0
  62. data/test/dummy/app/assets/config/manifest.js +3 -0
  63. data/test/dummy/app/assets/images/.keep +0 -0
  64. data/test/dummy/app/assets/stylesheets/application.css +15 -0
  65. data/test/dummy/app/assets/stylesheets/scaffold.css +80 -0
  66. data/test/dummy/app/channels/application_cable/channel.rb +4 -0
  67. data/test/dummy/app/channels/application_cable/connection.rb +4 -0
  68. data/test/dummy/app/controllers/application_controller.rb +2 -0
  69. data/test/dummy/app/controllers/concerns/.keep +0 -0
  70. data/test/dummy/app/helpers/application_helper.rb +2 -0
  71. data/test/dummy/app/javascript/packs/application.js +0 -0
  72. data/test/dummy/app/jobs/application_job.rb +2 -0
  73. data/test/dummy/app/mailboxes/application_mailbox.rb +2 -0
  74. data/test/dummy/app/mailboxes/messages_mailbox.rb +4 -0
  75. data/test/dummy/app/mailers/application_mailer.rb +4 -0
  76. data/test/dummy/app/models/application_record.rb +3 -0
  77. data/test/dummy/app/models/concerns/.keep +0 -0
  78. data/test/dummy/app/views/layouts/application.html.erb +14 -0
  79. data/test/dummy/app/views/layouts/mailer.html.erb +13 -0
  80. data/test/dummy/app/views/layouts/mailer.text.erb +1 -0
  81. data/test/dummy/bin/bundle +3 -0
  82. data/test/dummy/bin/rails +4 -0
  83. data/test/dummy/bin/rake +4 -0
  84. data/test/dummy/bin/setup +36 -0
  85. data/test/dummy/bin/update +31 -0
  86. data/test/dummy/bin/yarn +11 -0
  87. data/test/dummy/config.ru +5 -0
  88. data/test/dummy/config/application.rb +19 -0
  89. data/test/dummy/config/boot.rb +5 -0
  90. data/test/dummy/config/cable.yml +10 -0
  91. data/test/dummy/config/database.yml +25 -0
  92. data/test/dummy/config/environment.rb +5 -0
  93. data/test/dummy/config/environments/development.rb +63 -0
  94. data/test/dummy/config/environments/production.rb +96 -0
  95. data/test/dummy/config/environments/test.rb +46 -0
  96. data/test/dummy/config/initializers/application_controller_renderer.rb +8 -0
  97. data/test/dummy/config/initializers/assets.rb +14 -0
  98. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  99. data/test/dummy/config/initializers/content_security_policy.rb +22 -0
  100. data/test/dummy/config/initializers/cookies_serializer.rb +5 -0
  101. data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  102. data/test/dummy/config/initializers/inflections.rb +16 -0
  103. data/test/dummy/config/initializers/mime_types.rb +4 -0
  104. data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
  105. data/test/dummy/config/locales/en.yml +33 -0
  106. data/test/dummy/config/puma.rb +34 -0
  107. data/test/dummy/config/routes.rb +4 -0
  108. data/test/dummy/config/spring.rb +6 -0
  109. data/test/dummy/config/storage.yml +35 -0
  110. data/test/dummy/config/webpack/development.js +3 -0
  111. data/test/dummy/config/webpack/environment.js +3 -0
  112. data/test/dummy/config/webpack/production.js +3 -0
  113. data/test/dummy/config/webpack/test.js +3 -0
  114. data/test/dummy/config/webpacker.yml +65 -0
  115. data/test/dummy/db/migrate/20180208205311_create_action_mailroom_tables.rb +11 -0
  116. data/test/dummy/db/migrate/20180212164506_create_active_storage_tables.active_storage.rb +26 -0
  117. data/test/dummy/db/schema.rb +43 -0
  118. data/test/dummy/lib/assets/.keep +0 -0
  119. data/test/dummy/log/.keep +0 -0
  120. data/test/dummy/package.json +11 -0
  121. data/test/dummy/public/404.html +67 -0
  122. data/test/dummy/public/422.html +67 -0
  123. data/test/dummy/public/500.html +66 -0
  124. data/test/dummy/public/apple-touch-icon-precomposed.png +0 -0
  125. data/test/dummy/public/apple-touch-icon.png +0 -0
  126. data/test/dummy/public/favicon.ico +0 -0
  127. data/test/dummy/storage/.keep +0 -0
  128. data/test/dummy/yarn.lock +6071 -0
  129. data/test/fixtures/files/welcome.eml +631 -0
  130. data/test/jobs/incineration_job_test.rb +17 -0
  131. data/test/test_helper.rb +54 -0
  132. data/test/unit/inbound_email/incineration_test.rb +45 -0
  133. data/test/unit/inbound_email/message_id_test.rb +13 -0
  134. data/test/unit/inbound_email_test.rb +13 -0
  135. data/test/unit/mail_ext/address_equality_test.rb +9 -0
  136. data/test/unit/mail_ext/address_wrapping_test.rb +11 -0
  137. data/test/unit/mail_ext/recipients_test.rb +33 -0
  138. data/test/unit/mailbox/bouncing_test.rb +29 -0
  139. data/test/unit/mailbox/callbacks_test.rb +75 -0
  140. data/test/unit/mailbox/routing_test.rb +30 -0
  141. data/test/unit/mailbox/state_test.rb +49 -0
  142. data/test/unit/postfix_relayer_test.rb +90 -0
  143. data/test/unit/router_test.rb +137 -0
  144. metadata +355 -0
@@ -0,0 +1,15 @@
1
+ # Rerouting will run routing and processing on an email that has already been, or attempted to be, processed.
2
+ class Rails::Conductor::ActionMailbox::ReroutesController < Rails::Conductor::BaseController
3
+ def create
4
+ inbound_email = ActionMailbox::InboundEmail.find(params[:inbound_email_id])
5
+ reroute inbound_email
6
+
7
+ redirect_to main_app.rails_conductor_inbound_email_url(inbound_email)
8
+ end
9
+
10
+ private
11
+ def reroute(inbound_email)
12
+ inbound_email.pending!
13
+ inbound_email.route_later
14
+ end
15
+ end
@@ -0,0 +1,10 @@
1
+ # TODO: Move this to Rails::Conductor gem
2
+ class Rails::Conductor::BaseController < ActionController::Base
3
+ layout "rails/conductor"
4
+ before_action :ensure_development_env
5
+
6
+ private
7
+ def ensure_development_env
8
+ head :forbidden unless Rails.env.development?
9
+ end
10
+ end
@@ -0,0 +1,18 @@
1
+ # You can configure when this `IncinerationJob` will be run as a time-after-processing using the
2
+ # `config.action_mailbox.incinerate_after` or `ActionMailbox.incinerate_after` setting.
3
+ #
4
+ # Since this incineration is set for the future, it'll automatically ignore any `InboundEmail`s
5
+ # that have already been deleted and discard itself if so.
6
+ class ActionMailbox::IncinerationJob < ActiveJob::Base
7
+ queue_as { ActionMailbox.queues[:incineration] }
8
+
9
+ discard_on ActiveRecord::RecordNotFound
10
+
11
+ def self.schedule(inbound_email)
12
+ set(wait: ActionMailbox.incinerate_after).perform_later(inbound_email)
13
+ end
14
+
15
+ def perform(inbound_email)
16
+ inbound_email.incinerate
17
+ end
18
+ end
@@ -0,0 +1,9 @@
1
+ # Routing a new InboundEmail is an asynchronous operation, which allows the ingress controllers to quickly
2
+ # accept new incoming emails without being burdened to hang while they're actually being processed.
3
+ class ActionMailbox::RoutingJob < ActiveJob::Base
4
+ queue_as { ActionMailbox.queues[:routing] }
5
+
6
+ def perform(inbound_email)
7
+ inbound_email.route
8
+ end
9
+ end
@@ -0,0 +1,43 @@
1
+ require "mail"
2
+
3
+ # The `InboundEmail` is an Active Record that keeps a reference to the raw email stored in Active Storage
4
+ # and tracks the status of processing. By default, incoming emails will go through the following lifecycle:
5
+ #
6
+ # * Pending: Just received by one of the ingress controllers and scheduled for routing.
7
+ # * Processing: During active processing, while a specific mailbox is running its #process method.
8
+ # * Delivered: Successfully processed by the specific mailbox.
9
+ # * Failed: An exception was raised during the specific mailbox's execution of the `#process` method.
10
+ # * Bounced: Rejected processing by the specific mailbox and bounced to sender.
11
+ #
12
+ # Once the `InboundEmail` has reached the status of being either `delivered`, `failed`, or `bounced`,
13
+ # it'll count as having been `#processed?`. Once processed, the `InboundEmail` will be scheduled for
14
+ # automatic incineration at a later point.
15
+ #
16
+ # When working with an `InboundEmail`, you'll usually interact with the parsed version of the source,
17
+ # which is available as a `Mail` object from `#mail`. But you can also access the raw source directly
18
+ # using the `#source` method.
19
+ #
20
+ # Examples:
21
+ #
22
+ # inbound_email.mail.from # => 'david@loudthinking.com'
23
+ # inbound_email.source # Returns the full rfc822 source of the email as text
24
+ class ActionMailbox::InboundEmail < ActiveRecord::Base
25
+ self.table_name = "action_mailbox_inbound_emails"
26
+
27
+ include Incineratable, MessageId, Routable
28
+
29
+ has_one_attached :raw_email
30
+ enum status: %i[ pending processing delivered failed bounced ]
31
+
32
+ def mail
33
+ @mail ||= Mail.from_source(source)
34
+ end
35
+
36
+ def source
37
+ @source ||= raw_email.download
38
+ end
39
+
40
+ def processed?
41
+ delivered? || failed? || bounced?
42
+ end
43
+ end
@@ -0,0 +1,18 @@
1
+ # Ensure that the `InboundEmail` is automatically scheduled for later incineration if the status has been
2
+ # changed to `processed`. The later incineration will be invoked at the time specified by the
3
+ # `ActionMailbox.incinerate_after` time using the `IncinerationJob`.
4
+ module ActionMailbox::InboundEmail::Incineratable
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ after_update_commit :incinerate_later, if: -> { status_previously_changed? && processed? }
9
+ end
10
+
11
+ def incinerate_later
12
+ ActionMailbox::IncinerationJob.schedule self
13
+ end
14
+
15
+ def incinerate
16
+ Incineration.new(self).run
17
+ end
18
+ end
@@ -0,0 +1,22 @@
1
+ # Command class for carrying out the actual incineration of the `InboundMail` that's been scheduled
2
+ # for removal. Before the incineration – which really is just a call to `#destroy!` – is run, we verify
3
+ # that it's both eligible (by virtue of having already been processed) and time to do so (that is,
4
+ # the `InboundEmail` was processed after the `incinerate_after` time).
5
+ class ActionMailbox::InboundEmail::Incineratable::Incineration
6
+ def initialize(inbound_email)
7
+ @inbound_email = inbound_email
8
+ end
9
+
10
+ def run
11
+ @inbound_email.destroy! if due? && processed?
12
+ end
13
+
14
+ private
15
+ def due?
16
+ @inbound_email.updated_at < ActionMailbox.incinerate_after.ago.end_of_day
17
+ end
18
+
19
+ def processed?
20
+ @inbound_email.processed?
21
+ end
22
+ end
@@ -0,0 +1,36 @@
1
+ # The `Message-ID` as specified by rfc822 is supposed to be a unique identifier for that individual email.
2
+ # That makes it an ideal tracking token for debugging and forensics, just like `X-Request-Id` does for
3
+ # web request.
4
+ #
5
+ # If an inbound email does not, against the rfc822 mandate, specify a Message-ID, one will be generated
6
+ # using the approach from `Mail::MessageIdField`.
7
+ module ActionMailbox::InboundEmail::MessageId
8
+ extend ActiveSupport::Concern
9
+
10
+ included do
11
+ before_save :generate_missing_message_id
12
+ end
13
+
14
+ module ClassMethods
15
+ # Create a new `InboundEmail` from the raw `source` of the email, which be uploaded as a Active Storage
16
+ # attachment called `raw_email`. Before the upload, extract the Message-ID from the `source` and set
17
+ # it as an attribute on the new `InboundEmail`.
18
+ def create_and_extract_message_id!(source, **options)
19
+ create! message_id: extract_message_id(source), **options do |inbound_email|
20
+ inbound_email.raw_email.attach io: StringIO.new(source), filename: "message.eml", content_type: "message/rfc822"
21
+ end
22
+ end
23
+
24
+ private
25
+ def extract_message_id(source)
26
+ Mail.from_source(source).message_id
27
+ rescue => e
28
+ # FIXME: Add logging with "Couldn't extract Message ID, so will generating a new random ID instead"
29
+ end
30
+ end
31
+
32
+ private
33
+ def generate_missing_message_id
34
+ self.message_id ||= Mail::MessageIdField.new.message_id
35
+ end
36
+ end
@@ -0,0 +1,22 @@
1
+ # A newly received `InboundEmail` will not be routed synchronously as part of ingress controller's receival.
2
+ # Instead, the routing will be done asynchronously, using a `RoutingJob`, to ensure maximum parallel capacity.
3
+ #
4
+ # By default, all newly created `InboundEmail` records that have the status of `pending`, which is the default,
5
+ # will be scheduled for automatic, deferred routing.
6
+ module ActionMailbox::InboundEmail::Routable
7
+ extend ActiveSupport::Concern
8
+
9
+ included do
10
+ after_create_commit :route_later, if: :pending?
11
+ end
12
+
13
+ # Enqueue a `RoutingJob` for this `InboundEmail`.
14
+ def route_later
15
+ ActionMailbox::RoutingJob.perform_later self
16
+ end
17
+
18
+ # Route this `InboundEmail` using the routing rules declared on the `ApplicationMailbox`.
19
+ def route
20
+ ApplicationMailbox.route self
21
+ end
22
+ end
@@ -0,0 +1,7 @@
1
+ <html>
2
+ <head>
3
+ <title>Rails Conductor: <%= yield :title %></title>
4
+ </head>
5
+ <body>
6
+ <%= yield %>
7
+ </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,42 @@
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
+ <%= form.submit "Deliver inbound email" %>
42
+ <% 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,5 @@
1
+ #!/usr/bin/env ruby
2
+ $: << File.expand_path("../test", __dir__)
3
+
4
+ require "bundler/setup"
5
+ require "rails/plugin/test"
@@ -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 "/amazon/inbound_emails" => "amazon/inbound_emails#create", as: :rails_amazon_inbound_emails
6
+ post "/mandrill/inbound_emails" => "mandrill/inbound_emails#create", as: :rails_mandrill_inbound_emails
7
+ post "/postfix/inbound_emails" => "postfix/inbound_emails#create", as: :rails_postfix_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,11 @@
1
+ class CreateActionMailboxTables < ActiveRecord::Migration[5.2]
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
6
+
7
+ t.datetime :created_at, precision: 6
8
+ t.datetime :updated_at, precision: 6
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,15 @@
1
+ require "action_mailbox/engine"
2
+ require "action_mailbox/mail_ext"
3
+
4
+ module ActionMailbox
5
+ extend ActiveSupport::Autoload
6
+
7
+ autoload :Base
8
+ autoload :Router
9
+ autoload :TestCase
10
+
11
+ mattr_accessor :ingress
12
+ mattr_accessor :logger
13
+ mattr_accessor :incinerate_after, default: 30.days
14
+ mattr_accessor :queues, default: {}
15
+ end
@@ -0,0 +1,111 @@
1
+ require "active_support/rescuable"
2
+
3
+ require "action_mailbox/callbacks"
4
+ require "action_mailbox/routing"
5
+
6
+ # The base class for all application mailboxes. Not intended to be inherited from directly. Inherit from
7
+ # `ApplicationMailbox` instead, as that's where the app-specific routing is configured. This routing
8
+ # is specified in the following ways:
9
+ #
10
+ # class ApplicationMailbox < ActionMailbox::Base
11
+ # # Any of the recipients of the mail (whether to, cc, bcc) are matched against the regexp.
12
+ # route /^replies@/i => :replies
13
+ #
14
+ # # Any of the recipients of the mail (whether to, cc, bcc) needs to be an exact match for the string.
15
+ # route "help@example.com" => :help
16
+ #
17
+ # # Any callable (proc, lambda, etc) object is passed the inbound_email record and is a match if true.
18
+ # route ->(inbound_email) { inbound_email.mail.to.size > 2 } => :multiple_recipients
19
+ #
20
+ # # Any object responding to #match? is called with the inbound_email record as an argument. Match if true.
21
+ # route CustomAddress.new => :custom
22
+ #
23
+ # # Any inbound_email that has not been already matched will be sent to the BackstopMailbox.
24
+ # route :all => :backstop
25
+ # end
26
+ #
27
+ # Application mailboxes need to overwrite the `#process` method, which is invoked by the framework after
28
+ # callbacks have been run. The callbacks available are: `before_processing`, `after_processing`, and
29
+ # `around_processing`. The primary use case is ensure certain preconditions to processing are fulfilled
30
+ # using `before_processing` callbacks.
31
+ #
32
+ # If a precondition fails to be met, you can halt the processing using the `#bounced!` method,
33
+ # which will silently prevent any further processing, but not actually send out any bounce notice. You
34
+ # can also pair this behavior with the invocation of an Action Mailer class responsible for sending out
35
+ # an actual bounce email. This is done using the `#bounce_with` method, which takes the mail object returned
36
+ # by an Action Mailer method, like so:
37
+ #
38
+ # class ForwardsMailbox < ApplicationMailbox
39
+ # before_processing :ensure_sender_is_a_user
40
+ #
41
+ # private
42
+ # def ensure_sender_is_a_user
43
+ # unless User.exist?(email_address: mail.from)
44
+ # bounce_with UserRequiredMailer.missing(inbound_email)
45
+ # end
46
+ # end
47
+ # end
48
+ #
49
+ # During the processing of the inbound email, the status will be tracked. Before processing begins,
50
+ # the email will normally have the `pending` status. Once processing begins, just before callbacks
51
+ # and the `#process` method is called, the status is changed to `processing`. If processing is allowed to
52
+ # complete, the status is changed to `delivered`. If a bounce is triggered, then `bounced`. If an unhandled
53
+ # exception is bubbled up, then `failed`.
54
+ #
55
+ # Exceptions can be handled at the class level using the familiar `Rescuable` approach:
56
+ #
57
+ # class ForwardsMailbox < ApplicationMailbox
58
+ # rescue_from(ApplicationSpecificVerificationError) { bounced! }
59
+ # end
60
+ class ActionMailbox::Base
61
+ include ActiveSupport::Rescuable
62
+ include ActionMailbox::Callbacks, ActionMailbox::Routing
63
+
64
+ attr_reader :inbound_email
65
+ delegate :mail, :delivered!, :bounced!, to: :inbound_email
66
+
67
+ delegate :logger, to: ActionMailbox
68
+
69
+ def self.receive(inbound_email)
70
+ new(inbound_email).perform_processing
71
+ end
72
+
73
+ def initialize(inbound_email)
74
+ @inbound_email = inbound_email
75
+ end
76
+
77
+ def perform_processing
78
+ track_status_of_inbound_email do
79
+ run_callbacks :process do
80
+ process
81
+ end
82
+ end
83
+ rescue => exception
84
+ # TODO: Include a reference to the inbound_email in the exception raised so error handling becomes easier
85
+ rescue_with_handler(exception) || raise
86
+ end
87
+
88
+ def process
89
+ # Overwrite in subclasses
90
+ end
91
+
92
+ def finished_processing?
93
+ inbound_email.delivered? || inbound_email.bounced?
94
+ end
95
+
96
+
97
+ def bounce_with(message)
98
+ inbound_email.bounced!
99
+ message.deliver_later
100
+ end
101
+
102
+ private
103
+ def track_status_of_inbound_email
104
+ inbound_email.processing!
105
+ yield
106
+ inbound_email.delivered! unless inbound_email.bounced?
107
+ rescue
108
+ inbound_email.failed!
109
+ raise
110
+ end
111
+ end