actionmailbox 6.0.2
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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +46 -0
- data/MIT-LICENSE +21 -0
- data/README.md +13 -0
- data/app/controllers/action_mailbox/base_controller.rb +34 -0
- data/app/controllers/action_mailbox/ingresses/mailgun/inbound_emails_controller.rb +103 -0
- data/app/controllers/action_mailbox/ingresses/mandrill/inbound_emails_controller.rb +82 -0
- data/app/controllers/action_mailbox/ingresses/postmark/inbound_emails_controller.rb +62 -0
- data/app/controllers/action_mailbox/ingresses/relay/inbound_emails_controller.rb +65 -0
- data/app/controllers/action_mailbox/ingresses/sendgrid/inbound_emails_controller.rb +54 -0
- data/app/controllers/rails/conductor/action_mailbox/inbound_emails_controller.rb +35 -0
- data/app/controllers/rails/conductor/action_mailbox/reroutes_controller.rb +19 -0
- data/app/controllers/rails/conductor/base_controller.rb +14 -0
- data/app/jobs/action_mailbox/incineration_job.rb +25 -0
- data/app/jobs/action_mailbox/routing_job.rb +13 -0
- data/app/models/action_mailbox/inbound_email.rb +49 -0
- data/app/models/action_mailbox/inbound_email/incineratable.rb +20 -0
- data/app/models/action_mailbox/inbound_email/incineratable/incineration.rb +26 -0
- data/app/models/action_mailbox/inbound_email/message_id.rb +38 -0
- data/app/models/action_mailbox/inbound_email/routable.rb +24 -0
- data/app/views/layouts/rails/conductor.html.erb +8 -0
- data/app/views/rails/conductor/action_mailbox/inbound_emails/index.html.erb +15 -0
- data/app/views/rails/conductor/action_mailbox/inbound_emails/new.html.erb +47 -0
- data/app/views/rails/conductor/action_mailbox/inbound_emails/show.html.erb +15 -0
- data/config/routes.rb +19 -0
- data/db/migrate/20180917164000_create_action_mailbox_tables.rb +13 -0
- data/lib/action_mailbox.rb +17 -0
- data/lib/action_mailbox/base.rb +118 -0
- data/lib/action_mailbox/callbacks.rb +34 -0
- data/lib/action_mailbox/engine.rb +33 -0
- data/lib/action_mailbox/gem_version.rb +17 -0
- data/lib/action_mailbox/mail_ext.rb +6 -0
- data/lib/action_mailbox/mail_ext/address_equality.rb +9 -0
- data/lib/action_mailbox/mail_ext/address_wrapping.rb +9 -0
- data/lib/action_mailbox/mail_ext/addresses.rb +29 -0
- data/lib/action_mailbox/mail_ext/from_source.rb +7 -0
- data/lib/action_mailbox/mail_ext/recipients.rb +9 -0
- data/lib/action_mailbox/relayer.rb +75 -0
- data/lib/action_mailbox/router.rb +42 -0
- data/lib/action_mailbox/router/route.rb +42 -0
- data/lib/action_mailbox/routing.rb +22 -0
- data/lib/action_mailbox/test_case.rb +12 -0
- data/lib/action_mailbox/test_helper.rb +48 -0
- data/lib/action_mailbox/version.rb +10 -0
- data/lib/rails/generators/installer.rb +10 -0
- data/lib/rails/generators/mailbox/USAGE +12 -0
- data/lib/rails/generators/mailbox/mailbox_generator.rb +32 -0
- data/lib/rails/generators/mailbox/templates/application_mailbox.rb.tt +3 -0
- data/lib/rails/generators/mailbox/templates/mailbox.rb.tt +4 -0
- data/lib/rails/generators/test_unit/mailbox_generator.rb +20 -0
- data/lib/rails/generators/test_unit/templates/mailbox_test.rb.tt +11 -0
- data/lib/tasks/ingress.rake +72 -0
- data/lib/tasks/install.rake +20 -0
- 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,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 %>
|
data/config/routes.rb
ADDED
@@ -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
|