From b77d2d9a032b64c7819bf950194c715a3404da37 Mon Sep 17 00:00:00 2001 From: Tomek Maszkowski Date: Mon, 31 Dec 2018 18:16:44 -0500 Subject: [PATCH] Added Postmark ingress support --- .../postmark/inbound_emails_controller.rb | 62 +++++++++++++++ actionmailbox/config/routes.rb | 1 + .../inbound_emails_controller_test.rb | 55 ++++++++++++++ guides/source/action_mailbox_basics.md | 36 +++++++++ railties/test/application/rake/routes_test.rb | 1 + railties/test/commands/routes_test.rb | 75 +++++++++++-------- 6 files changed, 197 insertions(+), 33 deletions(-) create mode 100644 actionmailbox/app/controllers/action_mailbox/ingresses/postmark/inbound_emails_controller.rb create mode 100644 actionmailbox/test/controllers/ingresses/postmark/inbound_emails_controller_test.rb diff --git a/actionmailbox/app/controllers/action_mailbox/ingresses/postmark/inbound_emails_controller.rb b/actionmailbox/app/controllers/action_mailbox/ingresses/postmark/inbound_emails_controller.rb new file mode 100644 index 0000000000..ec4e0da5ec --- /dev/null +++ b/actionmailbox/app/controllers/action_mailbox/ingresses/postmark/inbound_emails_controller.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +module ActionMailbox + # Ingests inbound emails from Postmark. Requires +RawEmail+ parameter containing a full RFC 822 message. + # + # Authenticates requests using HTTP basic access authentication. The username is always +actionmailbox+, and the + # password is read from the application's encrypted credentials or an environment variable. See the Usage section below. + # + # Note that basic authentication is insecure over unencrypted HTTP. An attacker that intercepts cleartext requests to + # the Postmark ingress can learn its password. You should only use the Postmark ingress over HTTPS. + # + # Returns: + # + # - 204 No Content if an inbound email is successfully recorded and enqueued for routing to the appropriate mailbox + # - 401 Unauthorized if the request's signature could not be validated + # - 404 Not Found if Action Mailbox is not configured to accept inbound emails from Postmark + # - 422 Unprocessable Entity if the request is missing the required +RawEmail+ parameter + # - 500 Server Error if the ingress password is not configured, or if one of the Active Record database, + # the Active Storage service, or the Active Job backend is misconfigured or unavailable + # + # == Usage + # + # 1. Tell Action Mailbox to accept emails from Postmark: + # + # # config/environments/production.rb + # config.action_mailbox.ingress = :postmark + # + # 2. Generate a strong password that Action Mailbox can use to authenticate requests to the Postmark ingress. + # + # Use rails credentials:edit to add the password to your application's encrypted credentials under + # +action_mailbox.ingress_password+, where Action Mailbox will automatically find it: + # + # action_mailbox: + # ingress_password: ... + # + # Alternatively, provide the password in the +RAILS_INBOUND_EMAIL_PASSWORD+ environment variable. + # + # 3. {Configure Postmark inbound webhook}[https://postmarkapp.com/manual#configure-your-inbound-webhook-url] + # to forward inbound emails to +/rails/action_mailbox/postmark/inbound_emails+ with the username +actionmailbox+ and + # the password you previously generated. If your application lived at https://example.com, you would + # configure Postmark with the following fully-qualified URL: + # + # https://actionmailbox:PASSWORD@example.com/rails/action_mailbox/postmark/inbound_emails + # + # *NOTE:* When configuring your Postmark inbound webhook, be sure to check the box labeled *"Include raw email + # content in JSON payload"*. Action Mailbox needs the raw email content to work. + class Ingresses::Postmark::InboundEmailsController < ActionMailbox::BaseController + before_action :authenticate_by_password + + def create + ActionMailbox::InboundEmail.create_and_extract_message_id! params.require("RawEmail") + rescue ActionController::ParameterMissing => error + logger.error <<~MESSAGE + #{error.message} + + When configuring your Postmark inbound webhook, be sure to check the box + labeled "Include raw email content in JSON payload". + MESSAGE + head :unprocessable_entity + end + end +end diff --git a/actionmailbox/config/routes.rb b/actionmailbox/config/routes.rb index f1bc9847f5..7aa230fdff 100644 --- a/actionmailbox/config/routes.rb +++ b/actionmailbox/config/routes.rb @@ -5,6 +5,7 @@ Rails.application.routes.draw do post "/amazon/inbound_emails" => "amazon/inbound_emails#create", as: :rails_amazon_inbound_emails post "/mandrill/inbound_emails" => "mandrill/inbound_emails#create", as: :rails_mandrill_inbound_emails post "/postfix/inbound_emails" => "postfix/inbound_emails#create", as: :rails_postfix_inbound_emails + post "/postmark/inbound_emails" => "postmark/inbound_emails#create", as: :rails_postmark_inbound_emails post "/sendgrid/inbound_emails" => "sendgrid/inbound_emails#create", as: :rails_sendgrid_inbound_emails # Mailgun requires that a webhook's URL end in 'mime' for it to receive the raw contents of emails. diff --git a/actionmailbox/test/controllers/ingresses/postmark/inbound_emails_controller_test.rb b/actionmailbox/test/controllers/ingresses/postmark/inbound_emails_controller_test.rb new file mode 100644 index 0000000000..11b579b39c --- /dev/null +++ b/actionmailbox/test/controllers/ingresses/postmark/inbound_emails_controller_test.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +require "test_helper" + +class ActionMailbox::Ingresses::Postmark::InboundEmailsControllerTest < ActionDispatch::IntegrationTest + setup { ActionMailbox.ingress = :postmark } + + test "receiving an inbound email from Postmark" do + assert_difference -> { ActionMailbox::InboundEmail.count }, +1 do + post rails_postmark_inbound_emails_url, + headers: { authorization: credentials }, params: { RawEmail: file_fixture("../files/welcome.eml").read } + end + + assert_response :no_content + + inbound_email = ActionMailbox::InboundEmail.last + assert_equal file_fixture("../files/welcome.eml").read, inbound_email.raw_email.download + assert_equal "0CB459E0-0336-41DA-BC88-E6E28C697DDB@37signals.com", inbound_email.message_id + end + + test "rejecting when RawEmail param is missing" do + assert_no_difference -> { ActionMailbox::InboundEmail.count } do + post rails_postmark_inbound_emails_url, + headers: { authorization: credentials }, params: { From: "someone@example.com" } + end + + assert_response :unprocessable_entity + end + + test "rejecting an unauthorized inbound email from Postmark" do + assert_no_difference -> { ActionMailbox::InboundEmail.count } do + post rails_postmark_inbound_emails_url, params: { RawEmail: file_fixture("../files/welcome.eml").read } + end + + assert_response :unauthorized + end + + test "raising when the configured password is nil" do + switch_password_to nil do + assert_raises ArgumentError do + post rails_postmark_inbound_emails_url, + headers: { authorization: credentials }, params: { RawEmail: file_fixture("../files/welcome.eml").read } + end + end + end + + test "raising when the configured password is blank" do + switch_password_to "" do + assert_raises ArgumentError do + post rails_postmark_inbound_emails_url, + headers: { authorization: credentials }, params: { RawEmail: file_fixture("../files/welcome.eml").read } + end + end + end +end diff --git a/guides/source/action_mailbox_basics.md b/guides/source/action_mailbox_basics.md index eb8a14b4d2..f2c8a0a6b7 100644 --- a/guides/source/action_mailbox_basics.md +++ b/guides/source/action_mailbox_basics.md @@ -155,6 +155,42 @@ would look like this: $ URL=https://example.com/rails/action_mailbox/postfix/inbound_emails INGRESS_PASSWORD=... rails action_mailbox:ingress:postfix ``` +### Postmark + +Tell Action Mailbox to accept emails from Postmark: + +```ruby +# config/environments/production.rb +config.action_mailbox.ingress = :postmark +``` + +Generate a strong password that Action Mailbox can use to authenticate +requests to the Postmark ingress. + +Use `rails credentials:edit` to add the password to your application's +encrypted credentials under `action_mailbox.ingress_password`, +where Action Mailbox will automatically find it: + +```yaml +action_mailbox: + ingress_password: ... +``` + +Alternatively, provide the password in the `RAILS_INBOUND_EMAIL_PASSWORD` +environment variable. + +[Configure Postmark inbound webhook](https://postmarkapp.com/manual#configure-your-inbound-webhook-url) +to forward inbound emails to `/rails/action_mailbox/postmark/inbound_emails` with the username `actionmailbox` +and the password you previously generated. If your application lived at `https://example.com`, you would +configure Postmark with the following fully-qualified URL: + +``` +https://actionmailbox:PASSWORD@example.com/rails/action_mailbox/postmark/inbound_emails +``` + +NOTE: When configuring your Postmark inbound webhook, be sure to check the box labeled **"Include raw email content in JSON payload"**. +Action Mailbox needs the raw email content to work. + ### SendGrid Tell Action Mailbox to accept emails from SendGrid: diff --git a/railties/test/application/rake/routes_test.rb b/railties/test/application/rake/routes_test.rb index 933c735078..bbcda7c258 100644 --- a/railties/test/application/rake/routes_test.rb +++ b/railties/test/application/rake/routes_test.rb @@ -23,6 +23,7 @@ module ApplicationTests rails_amazon_inbound_emails POST /rails/action_mailbox/amazon/inbound_emails(.:format) action_mailbox/ingresses/amazon/inbound_emails#create rails_mandrill_inbound_emails POST /rails/action_mailbox/mandrill/inbound_emails(.:format) action_mailbox/ingresses/mandrill/inbound_emails#create rails_postfix_inbound_emails POST /rails/action_mailbox/postfix/inbound_emails(.:format) action_mailbox/ingresses/postfix/inbound_emails#create + rails_postmark_inbound_emails POST /rails/action_mailbox/postmark/inbound_emails(.:format) action_mailbox/ingresses/postmark/inbound_emails#create rails_sendgrid_inbound_emails POST /rails/action_mailbox/sendgrid/inbound_emails(.:format) action_mailbox/ingresses/sendgrid/inbound_emails#create rails_mailgun_inbound_emails POST /rails/action_mailbox/mailgun/inbound_emails/mime(.:format) action_mailbox/ingresses/mailgun/inbound_emails#create rails_conductor_inbound_emails GET /rails/conductor/action_mailbox/inbound_emails(.:format) rails/conductor/action_mailbox/inbound_emails#index diff --git a/railties/test/commands/routes_test.rb b/railties/test/commands/routes_test.rb index a43a6d32b9..f77214ac2a 100644 --- a/railties/test/commands/routes_test.rb +++ b/railties/test/commands/routes_test.rb @@ -18,15 +18,16 @@ class Rails::Command::RoutesTest < ActiveSupport::TestCase RUBY assert_equal <<~OUTPUT, run_routes_command([ "-c", "PostController" ]) - Prefix Verb URI Pattern Controller#Action - new_post GET /post/new(.:format) posts#new - edit_post GET /post/edit(.:format) posts#edit - post GET /post(.:format) posts#show - PATCH /post(.:format) posts#update - PUT /post(.:format) posts#update - DELETE /post(.:format) posts#destroy - POST /post(.:format) posts#create - rails_postfix_inbound_emails POST /rails/action_mailbox/postfix/inbound_emails(.:format) action_mailbox/ingresses/postfix/inbound_emails#create + Prefix Verb URI Pattern Controller#Action + new_post GET /post/new(.:format) posts#new + edit_post GET /post/edit(.:format) posts#edit + post GET /post(.:format) posts#show + PATCH /post(.:format) posts#update + PUT /post(.:format) posts#update + DELETE /post(.:format) posts#destroy + POST /post(.:format) posts#create + rails_postfix_inbound_emails POST /rails/action_mailbox/postfix/inbound_emails(.:format) action_mailbox/ingresses/postfix/inbound_emails#create + rails_postmark_inbound_emails POST /rails/action_mailbox/postmark/inbound_emails(.:format) action_mailbox/ingresses/postmark/inbound_emails#create OUTPUT assert_equal <<~OUTPUT, run_routes_command([ "-c", "UserPermissionController" ]) @@ -65,6 +66,7 @@ class Rails::Command::RoutesTest < ActiveSupport::TestCase rails_amazon_inbound_emails POST /rails/action_mailbox/amazon/inbound_emails(.:format) action_mailbox/ingresses/amazon/inbound_emails#create rails_mandrill_inbound_emails POST /rails/action_mailbox/mandrill/inbound_emails(.:format) action_mailbox/ingresses/mandrill/inbound_emails#create rails_postfix_inbound_emails POST /rails/action_mailbox/postfix/inbound_emails(.:format) action_mailbox/ingresses/postfix/inbound_emails#create + rails_postmark_inbound_emails POST /rails/action_mailbox/postmark/inbound_emails(.:format) action_mailbox/ingresses/postmark/inbound_emails#create rails_sendgrid_inbound_emails POST /rails/action_mailbox/sendgrid/inbound_emails(.:format) action_mailbox/ingresses/sendgrid/inbound_emails#create rails_mailgun_inbound_emails POST /rails/action_mailbox/mailgun/inbound_emails/mime(.:format) action_mailbox/ingresses/mailgun/inbound_emails#create POST /rails/conductor/action_mailbox/inbound_emails(.:format) rails/conductor/action_mailbox/inbound_emails#create @@ -131,15 +133,16 @@ class Rails::Command::RoutesTest < ActiveSupport::TestCase OUTPUT assert_equal <<~OUTPUT, run_routes_command([ "-c", "PostController" ]) - Prefix Verb URI Pattern Controller#Action - new_admin_post GET /admin/post/new(.:format) admin/posts#new - edit_admin_post GET /admin/post/edit(.:format) admin/posts#edit - admin_post GET /admin/post(.:format) admin/posts#show - PATCH /admin/post(.:format) admin/posts#update - PUT /admin/post(.:format) admin/posts#update - DELETE /admin/post(.:format) admin/posts#destroy - POST /admin/post(.:format) admin/posts#create - rails_postfix_inbound_emails POST /rails/action_mailbox/postfix/inbound_emails(.:format) action_mailbox/ingresses/postfix/inbound_emails#create + Prefix Verb URI Pattern Controller#Action + new_admin_post GET /admin/post/new(.:format) admin/posts#new + edit_admin_post GET /admin/post/edit(.:format) admin/posts#edit + admin_post GET /admin/post(.:format) admin/posts#show + PATCH /admin/post(.:format) admin/posts#update + PUT /admin/post(.:format) admin/posts#update + DELETE /admin/post(.:format) admin/posts#destroy + POST /admin/post(.:format) admin/posts#create + rails_postfix_inbound_emails POST /rails/action_mailbox/postfix/inbound_emails(.:format) action_mailbox/ingresses/postfix/inbound_emails#create + rails_postmark_inbound_emails POST /rails/action_mailbox/postmark/inbound_emails(.:format) action_mailbox/ingresses/postmark/inbound_emails#create OUTPUT expected_permission_output = <<~OUTPUT @@ -168,6 +171,7 @@ class Rails::Command::RoutesTest < ActiveSupport::TestCase rails_amazon_inbound_emails POST /rails/action_mailbox/amazon/inbound_emails(.:format) action_mailbox/ingresses/amazon/inbound_emails#create rails_mandrill_inbound_emails POST /rails/action_mailbox/mandrill/inbound_emails(.:format) action_mailbox/ingresses/mandrill/inbound_emails#create rails_postfix_inbound_emails POST /rails/action_mailbox/postfix/inbound_emails(.:format) action_mailbox/ingresses/postfix/inbound_emails#create + rails_postmark_inbound_emails POST /rails/action_mailbox/postmark/inbound_emails(.:format) action_mailbox/ingresses/postmark/inbound_emails#create rails_sendgrid_inbound_emails POST /rails/action_mailbox/sendgrid/inbound_emails(.:format) action_mailbox/ingresses/sendgrid/inbound_emails#create rails_mailgun_inbound_emails POST /rails/action_mailbox/mailgun/inbound_emails/mime(.:format) action_mailbox/ingresses/mailgun/inbound_emails#create rails_conductor_inbound_emails GET /rails/conductor/action_mailbox/inbound_emails(.:format) rails/conductor/action_mailbox/inbound_emails#index @@ -220,81 +224,86 @@ class Rails::Command::RoutesTest < ActiveSupport::TestCase URI | /rails/action_mailbox/postfix/inbound_emails(.:format) Controller#Action | action_mailbox/ingresses/postfix/inbound_emails#create --[ Route 5 ]-------------- + Prefix | rails_postmark_inbound_emails + Verb | POST + URI | /rails/action_mailbox/postmark/inbound_emails(.:format) + Controller#Action | action_mailbox/ingresses/postmark/inbound_emails#create + --[ Route 6 ]-------------- Prefix | rails_sendgrid_inbound_emails Verb | POST URI | /rails/action_mailbox/sendgrid/inbound_emails(.:format) Controller#Action | action_mailbox/ingresses/sendgrid/inbound_emails#create - --[ Route 6 ]-------------- + --[ Route 7 ]-------------- Prefix | rails_mailgun_inbound_emails Verb | POST URI | /rails/action_mailbox/mailgun/inbound_emails/mime(.:format) Controller#Action | action_mailbox/ingresses/mailgun/inbound_emails#create - --[ Route 7 ]-------------- + --[ Route 8 ]-------------- Prefix | rails_conductor_inbound_emails Verb | GET URI | /rails/conductor/action_mailbox/inbound_emails(.:format) Controller#Action | rails/conductor/action_mailbox/inbound_emails#index - --[ Route 8 ]-------------- + --[ Route 9 ]-------------- Prefix | Verb | POST URI | /rails/conductor/action_mailbox/inbound_emails(.:format) Controller#Action | rails/conductor/action_mailbox/inbound_emails#create - --[ Route 9 ]-------------- + --[ Route 10 ]------------- Prefix | new_rails_conductor_inbound_email Verb | GET URI | /rails/conductor/action_mailbox/inbound_emails/new(.:format) Controller#Action | rails/conductor/action_mailbox/inbound_emails#new - --[ Route 10 ]------------- + --[ Route 11 ]------------- Prefix | edit_rails_conductor_inbound_email Verb | GET URI | /rails/conductor/action_mailbox/inbound_emails/:id/edit(.:format) Controller#Action | rails/conductor/action_mailbox/inbound_emails#edit - --[ Route 11 ]------------- + --[ Route 12 ]------------- Prefix | rails_conductor_inbound_email Verb | GET URI | /rails/conductor/action_mailbox/inbound_emails/:id(.:format) Controller#Action | rails/conductor/action_mailbox/inbound_emails#show - --[ Route 12 ]------------- + --[ Route 13 ]------------- Prefix | Verb | PATCH URI | /rails/conductor/action_mailbox/inbound_emails/:id(.:format) Controller#Action | rails/conductor/action_mailbox/inbound_emails#update - --[ Route 13 ]------------- + --[ Route 14 ]------------- Prefix | Verb | PUT URI | /rails/conductor/action_mailbox/inbound_emails/:id(.:format) Controller#Action | rails/conductor/action_mailbox/inbound_emails#update - --[ Route 14 ]------------- + --[ Route 15 ]------------- Prefix | Verb | DELETE URI | /rails/conductor/action_mailbox/inbound_emails/:id(.:format) Controller#Action | rails/conductor/action_mailbox/inbound_emails#destroy - --[ Route 15 ]------------- + --[ Route 16 ]------------- Prefix | rails_conductor_inbound_email_reroute Verb | POST URI | /rails/conductor/action_mailbox/:inbound_email_id/reroute(.:format) Controller#Action | rails/conductor/action_mailbox/reroutes#create - --[ Route 16 ]------------- + --[ Route 17 ]------------- Prefix | rails_service_blob Verb | GET URI | /rails/active_storage/blobs/:signed_id/*filename(.:format) Controller#Action | active_storage/blobs#show - --[ Route 17 ]------------- + --[ Route 18 ]------------- Prefix | rails_blob_representation Verb | GET URI | /rails/active_storage/representations/:signed_blob_id/:variation_key/*filename(.:format) Controller#Action | active_storage/representations#show - --[ Route 18 ]------------- + --[ Route 19 ]------------- Prefix | rails_disk_service Verb | GET URI | /rails/active_storage/disk/:encoded_key/*filename(.:format) Controller#Action | active_storage/disk#show - --[ Route 19 ]------------- + --[ Route 20 ]------------- Prefix | update_rails_disk_service Verb | PUT URI | /rails/active_storage/disk/:encoded_token(.:format) Controller#Action | active_storage/disk#update - --[ Route 20 ]------------- + --[ Route 21 ]------------- Prefix | rails_direct_uploads Verb | POST URI | /rails/active_storage/direct_uploads(.:format)