From 855e08d22d36269eb2ea6615d0c2916e1376704e Mon Sep 17 00:00:00 2001 From: Yurii Rashkovskii Date: Mon, 28 Jun 2021 17:02:23 -0700 Subject: [PATCH] Problem: ActionMailbox uses default ActiveStorage service This is imperfect in situations when a separation between regular files (such as uploads) and emails is necessary (for the purposes of regulatory compliance, proper compartmentalization, etc.) Solution: allow configuring ActionMailbox's storage service --- actionmailbox/CHANGELOG.md | 16 +++++++++ .../models/action_mailbox/inbound_email.rb | 2 +- .../inbound_email/message_id.rb | 3 +- actionmailbox/lib/action_mailbox.rb | 1 + actionmailbox/lib/action_mailbox/engine.rb | 3 ++ actionmailbox/test/dummy/config/storage.yml | 4 +++ actionmailbox/test/unit/inbound_email_test.rb | 36 +++++++++++++++++++ guides/source/configuring.md | 2 ++ .../test/application/configuration_test.rb | 19 ++++++++++ 9 files changed, 84 insertions(+), 2 deletions(-) diff --git a/actionmailbox/CHANGELOG.md b/actionmailbox/CHANGELOG.md index a56d85338b..902a8a1801 100644 --- a/actionmailbox/CHANGELOG.md +++ b/actionmailbox/CHANGELOG.md @@ -1,3 +1,19 @@ +* Add ability to configure ActiveStorage service + for storing email raw source. + + ```yml + # config/storage.yml + incoming_emails: + service: Disk + root: /secure/dir/for/emails/only + ``` + + ```ruby + config.action_mailbox.storage_service = :incoming_emails + ``` + + *Yurii Rashkovskii* + * Add ability to incinerate an inbound message through the conductor interface. *Santiago Bartesaghi* diff --git a/actionmailbox/app/models/action_mailbox/inbound_email.rb b/actionmailbox/app/models/action_mailbox/inbound_email.rb index 7357d0083e..8d2976452a 100644 --- a/actionmailbox/app/models/action_mailbox/inbound_email.rb +++ b/actionmailbox/app/models/action_mailbox/inbound_email.rb @@ -29,7 +29,7 @@ module ActionMailbox include Incineratable, MessageId, Routable - has_one_attached :raw_email + has_one_attached :raw_email, service: ActionMailbox.storage_service enum status: %i[ pending processing delivered failed bounced ] def mail diff --git a/actionmailbox/app/models/action_mailbox/inbound_email/message_id.rb b/actionmailbox/app/models/action_mailbox/inbound_email/message_id.rb index d8f52c8dbf..3e786e9659 100644 --- a/actionmailbox/app/models/action_mailbox/inbound_email/message_id.rb +++ b/actionmailbox/app/models/action_mailbox/inbound_email/message_id.rb @@ -35,7 +35,8 @@ module ActionMailbox::InboundEmail::MessageId end def create_and_upload_raw_email!(source) - ActiveStorage::Blob.create_and_upload! io: StringIO.new(source), filename: "message.eml", content_type: "message/rfc822" + ActiveStorage::Blob.create_and_upload! io: StringIO.new(source), filename: "message.eml", content_type: "message/rfc822", + service_name: ActionMailbox.storage_service end end end diff --git a/actionmailbox/lib/action_mailbox.rb b/actionmailbox/lib/action_mailbox.rb index 772dbd6529..1350aee853 100644 --- a/actionmailbox/lib/action_mailbox.rb +++ b/actionmailbox/lib/action_mailbox.rb @@ -14,4 +14,5 @@ module ActionMailbox 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/engine.rb b/actionmailbox/lib/action_mailbox/engine.rb index bab3964d93..b6dadd0c01 100644 --- a/actionmailbox/lib/action_mailbox/engine.rb +++ b/actionmailbox/lib/action_mailbox/engine.rb @@ -20,6 +20,8 @@ module ActionMailbox config.action_mailbox.queues = ActiveSupport::InheritableOptions.new \ incineration: :action_mailbox_incineration, routing: :action_mailbox_routing + config.action_mailbox.storage_service = nil + initializer "action_mailbox.config" do config.after_initialize do |app| ActionMailbox.logger = app.config.action_mailbox.logger || Rails.logger @@ -27,6 +29,7 @@ module ActionMailbox 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 diff --git a/actionmailbox/test/dummy/config/storage.yml b/actionmailbox/test/dummy/config/storage.yml index d8e4267cc4..ab8ba89f40 100644 --- a/actionmailbox/test/dummy/config/storage.yml +++ b/actionmailbox/test/dummy/config/storage.yml @@ -6,6 +6,10 @@ 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 diff --git a/actionmailbox/test/unit/inbound_email_test.rb b/actionmailbox/test/unit/inbound_email_test.rb index 6a56bc7ebf..264209c118 100644 --- a/actionmailbox/test/unit/inbound_email_test.rb +++ b/actionmailbox/test/unit/inbound_email_test.rb @@ -44,5 +44,41 @@ module ActionMailbox 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/guides/source/configuring.md b/guides/source/configuring.md index 02ec06346b..0f4a94ce55 100644 --- a/guides/source/configuring.md +++ b/guides/source/configuring.md @@ -785,6 +785,8 @@ Defaults to `'signed cookie'`. * `config.action_mailbox.queues.routing` accepts a symbol indicating the Active Job queue to use for routing jobs. When this option is `nil`, routing jobs are sent to the default Active Job queue (see `config.active_job.default_queue_name`). +* `config.action_mailbox.storage_service` accepts a symbol indicating the Active Storage service to use for uploading emails. When this option is `nil`, emails are uploaded to the default Active Storage service (see `config.active_storage.service`). + ### Configuring Action Mailer There are a number of settings available on `config.action_mailer`: diff --git a/railties/test/application/configuration_test.rb b/railties/test/application/configuration_test.rb index 1aa900d52c..18aa6572e7 100644 --- a/railties/test/application/configuration_test.rb +++ b/railties/test/application/configuration_test.rb @@ -2946,6 +2946,25 @@ module ApplicationTests assert_equal :another_queue, ActionMailbox.queues[:routing] end + test "ActionMailbox.storage_service is nil by default (default service)" do + app "development" + assert_nil(ActionMailbox.storage_service) + end + + test "ActionMailbox.storage_service can be configured" do + add_to_config <<-RUBY + config.active_storage.service_configurations = { + email: { + root: "#{Dir.tmpdir}/email", + service: "Disk" + } + } + config.action_mailbox.storage_service = :email + RUBY + app "development" + assert_equal(:email, ActionMailbox.storage_service) + end + test "ActionMailer::Base.delivery_job is ActionMailer::MailDeliveryJob by default" do app "development"