From 512b5316dd33a8aa36821ee9b134d6652fd4a35f Mon Sep 17 00:00:00 2001 From: George Claghorn Date: Sat, 12 Jan 2019 21:38:26 -0500 Subject: Add Exim and Qmail support to Action Mailbox --- actionmailbox/README.md | 2 +- .../ingresses/postfix/inbound_emails_controller.rb | 59 ------------ .../ingresses/relay/inbound_emails_controller.rb | 65 ++++++++++++++ actionmailbox/config/routes.rb | 2 +- .../lib/action_mailbox/postfix_relayer.rb | 67 -------------- actionmailbox/lib/action_mailbox/relayer.rb | 75 ++++++++++++++++ actionmailbox/lib/tasks/ingress.rake | 58 ++++++++++-- .../postfix/inbound_emails_controller_test.rb | 56 ------------ .../relay/inbound_emails_controller_test.rb | 56 ++++++++++++ actionmailbox/test/unit/postfix_relayer_test.rb | 92 ------------------- actionmailbox/test/unit/relayer_test.rb | 100 +++++++++++++++++++++ 11 files changed, 351 insertions(+), 281 deletions(-) delete mode 100644 actionmailbox/app/controllers/action_mailbox/ingresses/postfix/inbound_emails_controller.rb create mode 100644 actionmailbox/app/controllers/action_mailbox/ingresses/relay/inbound_emails_controller.rb delete mode 100644 actionmailbox/lib/action_mailbox/postfix_relayer.rb create mode 100644 actionmailbox/lib/action_mailbox/relayer.rb delete mode 100644 actionmailbox/test/controllers/ingresses/postfix/inbound_emails_controller_test.rb create mode 100644 actionmailbox/test/controllers/ingresses/relay/inbound_emails_controller_test.rb delete mode 100644 actionmailbox/test/unit/postfix_relayer_test.rb create mode 100644 actionmailbox/test/unit/relayer_test.rb (limited to 'actionmailbox') diff --git a/actionmailbox/README.md b/actionmailbox/README.md index c70f73b40f..9a47223d3b 100644 --- a/actionmailbox/README.md +++ b/actionmailbox/README.md @@ -1,6 +1,6 @@ # Action Mailbox -Action Mailbox routes incoming emails to controller-like mailboxes for processing in Rails. It ships with ingresses for Amazon SES, Mailgun, Mandrill, Postmark, and SendGrid. You can also handle inbound mails directly via the built-in Postfix ingress. +Action Mailbox routes incoming emails to controller-like mailboxes for processing in Rails. It ships with ingresses for Amazon SES, Mailgun, Mandrill, Postmark, and SendGrid. You can also handle inbound mails directly via the built-in Exim, Postfix, and Qmail ingresses. The inbound emails are turned into `InboundEmail` records using Active Record and feature lifecycle tracking, storage of the original email on cloud storage via Active Storage, and responsible data handling with on-by-default incineration. diff --git a/actionmailbox/app/controllers/action_mailbox/ingresses/postfix/inbound_emails_controller.rb b/actionmailbox/app/controllers/action_mailbox/ingresses/postfix/inbound_emails_controller.rb deleted file mode 100644 index c0d5002a12..0000000000 --- a/actionmailbox/app/controllers/action_mailbox/ingresses/postfix/inbound_emails_controller.rb +++ /dev/null @@ -1,59 +0,0 @@ -# frozen_string_literal: true - -module ActionMailbox - # Ingests inbound emails relayed from Postfix. - # - # 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 Postfix ingress can learn its password. You should only use the Postfix 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 could not be authenticated - # - 404 Not Found if Action Mailbox is not configured to accept inbound emails from Postfix - # - 415 Unsupported Media Type if the request does not contain an RFC 822 message - # - 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 Postfix: - # - # # config/environments/production.rb - # config.action_mailbox.ingress = :postfix - # - # 2. Generate a strong password that Action Mailbox can use to authenticate requests to the Postfix 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 Postfix}[https://serverfault.com/questions/258469/how-to-configure-postfix-to-pipe-all-incoming-email-to-a-script] - # to pipe inbound emails to bin/rails action_mailbox:ingress:postfix, providing the +URL+ of the Postfix - # ingress and the +INGRESS_PASSWORD+ you previously generated. - # - # If your application lived at https://example.com, the full command would look like this: - # - # URL=https://example.com/rails/action_mailbox/postfix/inbound_emails INGRESS_PASSWORD=... bin/rails action_mailbox:ingress:postfix - class Ingresses::Postfix::InboundEmailsController < ActionMailbox::BaseController - before_action :authenticate_by_password, :require_valid_rfc822_message - - def create - ActionMailbox::InboundEmail.create_and_extract_message_id! request.body.read - end - - private - def require_valid_rfc822_message - unless request.content_type == "message/rfc822" - head :unsupported_media_type - end - end - end -end diff --git a/actionmailbox/app/controllers/action_mailbox/ingresses/relay/inbound_emails_controller.rb b/actionmailbox/app/controllers/action_mailbox/ingresses/relay/inbound_emails_controller.rb new file mode 100644 index 0000000000..2d91c968c8 --- /dev/null +++ b/actionmailbox/app/controllers/action_mailbox/ingresses/relay/inbound_emails_controller.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +module ActionMailbox + # Ingests inbound emails relayed from an SMTP server. + # + # 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 ingress can learn its password. You should only use this 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 could not be authenticated + # - 404 Not Found if Action Mailbox is not configured to accept inbound emails relayed from an SMTP server + # - 415 Unsupported Media Type if the request does not contain an RFC 822 message + # - 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 an SMTP relay: + # + # # config/environments/production.rb + # config.action_mailbox.ingress = :relay + # + # 2. Generate a strong password that Action Mailbox can use to authenticate requests to the 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 your SMTP server to pipe inbound emails to the appropriate ingress command, providing the +URL+ of the + # relay ingress and the +INGRESS_PASSWORD+ you previously generated. + # + # If your application lives at https://example.com, you would configure the Postfix SMTP server to pipe + # inbound emails to the following command: + # + # bin/rails action_mailbox:ingress:postfix URL=https://example.com/rails/action_mailbox/postfix/inbound_emails INGRESS_PASSWORD=... + # + # Built-in ingress commands are available for these popular SMTP servers: + # + # - Exim (bin/rails action_mailbox:ingress:exim) + # - Postfix (bin/rails action_mailbox:ingress:postfix) + # - Qmail (bin/rails action_mailbox:ingress:qmail) + class Ingresses::Relay::InboundEmailsController < ActionMailbox::BaseController + before_action :authenticate_by_password, :require_valid_rfc822_message + + def create + ActionMailbox::InboundEmail.create_and_extract_message_id! request.body.read + end + + private + def require_valid_rfc822_message + unless request.content_type == "message/rfc822" + head :unsupported_media_type + end + end + end +end diff --git a/actionmailbox/config/routes.rb b/actionmailbox/config/routes.rb index 7aa230fdff..517d2835af 100644 --- a/actionmailbox/config/routes.rb +++ b/actionmailbox/config/routes.rb @@ -4,8 +4,8 @@ Rails.application.routes.draw do scope "/rails/action_mailbox", module: "action_mailbox/ingresses" 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 "/relay/inbound_emails" => "relay/inbound_emails#create", as: :rails_relay_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/lib/action_mailbox/postfix_relayer.rb b/actionmailbox/lib/action_mailbox/postfix_relayer.rb deleted file mode 100644 index d43c56ed2b..0000000000 --- a/actionmailbox/lib/action_mailbox/postfix_relayer.rb +++ /dev/null @@ -1,67 +0,0 @@ -# frozen_string_literal: true - -require "action_mailbox/version" -require "net/http" -require "uri" - -module ActionMailbox - class PostfixRelayer - class Result < Struct.new(:output) - def success? - !failure? - end - - def failure? - output.match?(/\A[45]\.\d{1,3}\.\d{1,3}(\s|\z)/) - end - end - - CONTENT_TYPE = "message/rfc822" - USER_AGENT = "Action Mailbox Postfix relayer v#{ActionMailbox.version}" - - attr_reader :uri, :username, :password - - def initialize(url:, username: "actionmailbox", password:) - @uri, @username, @password = URI(url), username, password - end - - def relay(source) - case response = post(source) - when Net::HTTPSuccess - Result.new "2.0.0 Successfully relayed message to Postfix ingress" - when Net::HTTPUnauthorized - Result.new "4.7.0 Invalid credentials for Postfix ingress" - else - Result.new "4.0.0 HTTP #{response.code}" - end - rescue IOError, SocketError, SystemCallError => error - Result.new "4.4.2 Network error relaying to Postfix ingress: #{error.message}" - rescue Timeout::Error - Result.new "4.4.2 Timed out relaying to Postfix ingress" - rescue => error - Result.new "4.0.0 Error relaying to Postfix ingress: #{error.message}" - end - - private - def post(source) - client.post uri, source, - "Content-Type" => CONTENT_TYPE, - "User-Agent" => USER_AGENT, - "Authorization" => "Basic #{Base64.strict_encode64(username + ":" + password)}" - end - - def client - @client ||= Net::HTTP.new(uri.host, uri.port).tap do |connection| - if uri.scheme == "https" - require "openssl" - - connection.use_ssl = true - connection.verify_mode = OpenSSL::SSL::VERIFY_PEER - end - - connection.open_timeout = 1 - connection.read_timeout = 10 - end - end - end -end diff --git a/actionmailbox/lib/action_mailbox/relayer.rb b/actionmailbox/lib/action_mailbox/relayer.rb new file mode 100644 index 0000000000..e2890acb60 --- /dev/null +++ b/actionmailbox/lib/action_mailbox/relayer.rb @@ -0,0 +1,75 @@ +# frozen_string_literal: true + +require "action_mailbox/version" +require "net/http" +require "uri" + +module ActionMailbox + class Relayer + class Result < Struct.new(:status_code, :message) + def success? + !failure? + end + + def failure? + transient_failure? || permanent_failure? + end + + def transient_failure? + status_code.start_with?("4.") + end + + def permanent_failure? + status_code.start_with?("5.") + end + end + + CONTENT_TYPE = "message/rfc822" + USER_AGENT = "Action Mailbox relayer v#{ActionMailbox.version}" + + attr_reader :uri, :username, :password + + def initialize(url:, username: "actionmailbox", password:) + @uri, @username, @password = URI(url), username, password + end + + def relay(source) + case response = post(source) + when Net::HTTPSuccess + Result.new "2.0.0", "Successfully relayed message to ingress" + when Net::HTTPUnauthorized + Result.new "4.7.0", "Invalid credentials for ingress" + else + Result.new "4.0.0", "HTTP #{response.code}" + end + rescue IOError, SocketError, SystemCallError => error + Result.new "4.4.2", "Network error relaying to ingress: #{error.message}" + rescue Timeout::Error + Result.new "4.4.2", "Timed out relaying to ingress" + rescue => error + Result.new "4.0.0", "Error relaying to ingress: #{error.message}" + end + + private + def post(source) + client.post uri, source, + "Content-Type" => CONTENT_TYPE, + "User-Agent" => USER_AGENT, + "Authorization" => "Basic #{Base64.strict_encode64(username + ":" + password)}" + end + + def client + @client ||= Net::HTTP.new(uri.host, uri.port).tap do |connection| + if uri.scheme == "https" + require "openssl" + + connection.use_ssl = true + connection.verify_mode = OpenSSL::SSL::VERIFY_PEER + end + + connection.open_timeout = 1 + connection.read_timeout = 10 + end + end + end +end diff --git a/actionmailbox/lib/tasks/ingress.rake b/actionmailbox/lib/tasks/ingress.rake index f775bbdfd7..43b613ea12 100644 --- a/actionmailbox/lib/tasks/ingress.rake +++ b/actionmailbox/lib/tasks/ingress.rake @@ -2,12 +2,37 @@ namespace :action_mailbox do namespace :ingress do - desc "Pipe an inbound email from STDIN to the Postfix ingress (URL and INGRESS_PASSWORD required)" - task :postfix do + task :environment do require "active_support" require "active_support/core_ext/object/blank" - require "action_mailbox/postfix_relayer" + require "action_mailbox/relayer" + end + + desc "Relay an inbound email from Exim to Action Mailbox (URL and INGRESS_PASSWORD required)" + task exim: "action_mailbox:ingress:environment" do + url, password = ENV.values_at("URL", "INGRESS_PASSWORD") + + if url.blank? || password.blank? + print "URL and INGRESS_PASSWORD are required" + exit 64 # EX_USAGE + end + + ActionMailbox::Relayer.new(url: url, password: password).relay(STDIN.read).tap do |result| + print result.message + + case + when result.success? + exit 0 + when result.transient_failure? + exit 75 # EX_TEMPFAIL + else + exit 69 # EX_UNAVAILABLE + end + end + end + desc "Relay an inbound email from Postfix to Action Mailbox (URL and INGRESS_PASSWORD required)" + task postfix: "action_mailbox:ingress:environment" do url, password = ENV.values_at("URL", "INGRESS_PASSWORD") if url.blank? || password.blank? @@ -15,10 +40,33 @@ namespace :action_mailbox do exit 1 end - ActionMailbox::PostfixRelayer.new(url: url, password: password).relay(STDIN.read).tap do |result| - print result.output + ActionMailbox::Relayer.new(url: url, password: password).relay(STDIN.read).tap do |result| + print "#{result.status_code} #{result.message}" exit result.success? end end + + desc "Relay an inbound email from Qmail to Action Mailbox (URL and INGRESS_PASSWORD required)" + task qmail: "action_mailbox:ingress:environment" do + url, password = ENV.values_at("URL", "INGRESS_PASSWORD") + + if url.blank? || password.blank? + print "URL and INGRESS_PASSWORD are required" + exit 111 + end + + ActionMailbox::Relayer.new(url: url, password: password).relay(STDIN.read).tap do |result| + print result.message + + case + when result.success? + exit 0 + when result.transient_failure? + exit 111 + else + exit 100 + end + end + end end end diff --git a/actionmailbox/test/controllers/ingresses/postfix/inbound_emails_controller_test.rb b/actionmailbox/test/controllers/ingresses/postfix/inbound_emails_controller_test.rb deleted file mode 100644 index d646f5e859..0000000000 --- a/actionmailbox/test/controllers/ingresses/postfix/inbound_emails_controller_test.rb +++ /dev/null @@ -1,56 +0,0 @@ -# frozen_string_literal: true - -require "test_helper" - -class ActionMailbox::Ingresses::Postfix::InboundEmailsControllerTest < ActionDispatch::IntegrationTest - setup { ActionMailbox.ingress = :postfix } - - test "receiving an inbound email from Postfix" do - assert_difference -> { ActionMailbox::InboundEmail.count }, +1 do - post rails_postfix_inbound_emails_url, headers: { "Authorization" => credentials, "Content-Type" => "message/rfc822" }, - params: 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 an unauthorized inbound email from Postfix" do - assert_no_difference -> { ActionMailbox::InboundEmail.count } do - post rails_postfix_inbound_emails_url, headers: { "Content-Type" => "message/rfc822" }, - params: file_fixture("../files/welcome.eml").read - end - - assert_response :unauthorized - end - - test "rejecting an inbound email of an unsupported media type from Postfix" do - assert_no_difference -> { ActionMailbox::InboundEmail.count } do - post rails_postfix_inbound_emails_url, headers: { "Authorization" => credentials, "Content-Type" => "text/plain" }, - params: file_fixture("../files/welcome.eml").read - end - - assert_response :unsupported_media_type - end - - test "raising when the configured password is nil" do - switch_password_to nil do - assert_raises ArgumentError do - post rails_postfix_inbound_emails_url, headers: { "Authorization" => credentials, "Content-Type" => "message/rfc822" }, - params: 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_postfix_inbound_emails_url, headers: { "Authorization" => credentials, "Content-Type" => "message/rfc822" }, - params: file_fixture("../files/welcome.eml").read - end - end - end -end diff --git a/actionmailbox/test/controllers/ingresses/relay/inbound_emails_controller_test.rb b/actionmailbox/test/controllers/ingresses/relay/inbound_emails_controller_test.rb new file mode 100644 index 0000000000..67c5993f7f --- /dev/null +++ b/actionmailbox/test/controllers/ingresses/relay/inbound_emails_controller_test.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +require "test_helper" + +class ActionMailbox::Ingresses::Relay::InboundEmailsControllerTest < ActionDispatch::IntegrationTest + setup { ActionMailbox.ingress = :relay } + + test "receiving an inbound email relayed from an SMTP server" do + assert_difference -> { ActionMailbox::InboundEmail.count }, +1 do + post rails_relay_inbound_emails_url, headers: { "Authorization" => credentials, "Content-Type" => "message/rfc822" }, + params: 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 an unauthorized inbound email" do + assert_no_difference -> { ActionMailbox::InboundEmail.count } do + post rails_relay_inbound_emails_url, headers: { "Content-Type" => "message/rfc822" }, + params: file_fixture("../files/welcome.eml").read + end + + assert_response :unauthorized + end + + test "rejecting an inbound email of an unsupported media type" do + assert_no_difference -> { ActionMailbox::InboundEmail.count } do + post rails_relay_inbound_emails_url, headers: { "Authorization" => credentials, "Content-Type" => "text/plain" }, + params: file_fixture("../files/welcome.eml").read + end + + assert_response :unsupported_media_type + end + + test "raising when the configured password is nil" do + switch_password_to nil do + assert_raises ArgumentError do + post rails_relay_inbound_emails_url, headers: { "Authorization" => credentials, "Content-Type" => "message/rfc822" }, + params: 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_relay_inbound_emails_url, headers: { "Authorization" => credentials, "Content-Type" => "message/rfc822" }, + params: file_fixture("../files/welcome.eml").read + end + end + end +end diff --git a/actionmailbox/test/unit/postfix_relayer_test.rb b/actionmailbox/test/unit/postfix_relayer_test.rb deleted file mode 100644 index 5f7496ec3f..0000000000 --- a/actionmailbox/test/unit/postfix_relayer_test.rb +++ /dev/null @@ -1,92 +0,0 @@ -# frozen_string_literal: true - -require_relative "../test_helper" - -require "action_mailbox/postfix_relayer" - -module ActionMailbox - class PostfixRelayerTest < ActiveSupport::TestCase - URL = "https://example.com/rails/action_mailbox/postfix/inbound_emails" - INGRESS_PASSWORD = "secret" - - setup do - @relayer = ActionMailbox::PostfixRelayer.new(url: URL, password: INGRESS_PASSWORD) - end - - test "successfully relaying an email" do - stub_request(:post, URL).to_return status: 204 - - result = @relayer.relay(file_fixture("welcome.eml").read) - assert_equal "2.0.0 Successfully relayed message to Postfix ingress", result.output - assert result.success? - assert_not result.failure? - - assert_requested :post, URL, body: file_fixture("welcome.eml").read, - basic_auth: [ "actionmailbox", INGRESS_PASSWORD ], - headers: { "Content-Type" => "message/rfc822", "User-Agent" => /\AAction Mailbox Postfix relayer v\d+\./ } - end - - test "unsuccessfully relaying with invalid credentials" do - stub_request(:post, URL).to_return status: 401 - - result = @relayer.relay(file_fixture("welcome.eml").read) - assert_equal "4.7.0 Invalid credentials for Postfix ingress", result.output - assert_not result.success? - assert result.failure? - end - - test "unsuccessfully relaying due to an unspecified server error" do - stub_request(:post, URL).to_return status: 500 - - result = @relayer.relay(file_fixture("welcome.eml").read) - assert_equal "4.0.0 HTTP 500", result.output - assert_not result.success? - assert result.failure? - end - - test "unsuccessfully relaying due to a gateway timeout" do - stub_request(:post, URL).to_return status: 504 - - result = @relayer.relay(file_fixture("welcome.eml").read) - assert_equal "4.0.0 HTTP 504", result.output - assert_not result.success? - assert result.failure? - end - - test "unsuccessfully relaying due to ECONNRESET" do - stub_request(:post, URL).to_raise Errno::ECONNRESET.new - - result = @relayer.relay(file_fixture("welcome.eml").read) - assert_equal "4.4.2 Network error relaying to Postfix ingress: Connection reset by peer", result.output - assert_not result.success? - assert result.failure? - end - - test "unsuccessfully relaying due to connection failure" do - stub_request(:post, URL).to_raise SocketError.new("Failed to open TCP connection to example.com:443") - - result = @relayer.relay(file_fixture("welcome.eml").read) - assert_equal "4.4.2 Network error relaying to Postfix ingress: Failed to open TCP connection to example.com:443", result.output - assert_not result.success? - assert result.failure? - end - - test "unsuccessfully relaying due to client-side timeout" do - stub_request(:post, URL).to_timeout - - result = @relayer.relay(file_fixture("welcome.eml").read) - assert_equal "4.4.2 Timed out relaying to Postfix ingress", result.output - assert_not result.success? - assert result.failure? - end - - test "unsuccessfully relaying due to an unhandled exception" do - stub_request(:post, URL).to_raise StandardError.new("Something went wrong") - - result = @relayer.relay(file_fixture("welcome.eml").read) - assert_equal "4.0.0 Error relaying to Postfix ingress: Something went wrong", result.output - assert_not result.success? - assert result.failure? - end - end -end diff --git a/actionmailbox/test/unit/relayer_test.rb b/actionmailbox/test/unit/relayer_test.rb new file mode 100644 index 0000000000..fb2b48ea16 --- /dev/null +++ b/actionmailbox/test/unit/relayer_test.rb @@ -0,0 +1,100 @@ +# frozen_string_literal: true + +require_relative "../test_helper" + +require "action_mailbox/relayer" + +module ActionMailbox + class RelayerTest < ActiveSupport::TestCase + URL = "https://example.com/rails/action_mailbox/relay/inbound_emails" + INGRESS_PASSWORD = "secret" + + setup do + @relayer = ActionMailbox::Relayer.new(url: URL, password: INGRESS_PASSWORD) + end + + test "successfully relaying an email" do + stub_request(:post, URL).to_return status: 204 + + result = @relayer.relay(file_fixture("welcome.eml").read) + assert_equal "2.0.0", result.status_code + assert_equal "Successfully relayed message to ingress", result.message + assert result.success? + assert_not result.failure? + + assert_requested :post, URL, body: file_fixture("welcome.eml").read, + basic_auth: [ "actionmailbox", INGRESS_PASSWORD ], + headers: { "Content-Type" => "message/rfc822", "User-Agent" => /\AAction Mailbox relayer v\d+\./ } + end + + test "unsuccessfully relaying with invalid credentials" do + stub_request(:post, URL).to_return status: 401 + + result = @relayer.relay(file_fixture("welcome.eml").read) + assert_equal "4.7.0", result.status_code + assert_equal "Invalid credentials for ingress", result.message + assert_not result.success? + assert result.failure? + end + + test "unsuccessfully relaying due to an unspecified server error" do + stub_request(:post, URL).to_return status: 500 + + result = @relayer.relay(file_fixture("welcome.eml").read) + assert_equal "4.0.0", result.status_code + assert_equal "HTTP 500", result.message + assert_not result.success? + assert result.failure? + end + + test "unsuccessfully relaying due to a gateway timeout" do + stub_request(:post, URL).to_return status: 504 + + result = @relayer.relay(file_fixture("welcome.eml").read) + assert_equal "4.0.0", result.status_code + assert_equal "HTTP 504", result.message + assert_not result.success? + assert result.failure? + end + + test "unsuccessfully relaying due to ECONNRESET" do + stub_request(:post, URL).to_raise Errno::ECONNRESET.new + + result = @relayer.relay(file_fixture("welcome.eml").read) + assert_equal "4.4.2", result.status_code + assert_equal "Network error relaying to ingress: Connection reset by peer", result.message + assert_not result.success? + assert result.failure? + end + + test "unsuccessfully relaying due to connection failure" do + stub_request(:post, URL).to_raise SocketError.new("Failed to open TCP connection to example.com:443") + + result = @relayer.relay(file_fixture("welcome.eml").read) + assert_equal "4.4.2", result.status_code + assert_equal "Network error relaying to ingress: Failed to open TCP connection to example.com:443", result.message + assert_not result.success? + assert result.failure? + end + + test "unsuccessfully relaying due to client-side timeout" do + stub_request(:post, URL).to_timeout + + result = @relayer.relay(file_fixture("welcome.eml").read) + assert_equal "4.4.2", result.status_code + assert_equal "Timed out relaying to ingress", result.message + assert_not result.success? + assert result.failure? + end + + test "unsuccessfully relaying due to an unhandled exception" do + stub_request(:post, URL).to_raise StandardError.new("Something went wrong") + + result = @relayer.relay(file_fixture("welcome.eml").read) + assert_equal "4.0.0", result.status_code + assert_equal "Error relaying to ingress: Something went wrong", result.message + assert_not result.success? + assert result.failure? + end + end +end -- cgit v1.2.3