aboutsummaryrefslogtreecommitdiffstats
path: root/actionmailbox
diff options
context:
space:
mode:
authorGeorge Claghorn <george.claghorn@gmail.com>2019-01-12 21:38:26 -0500
committerGitHub <noreply@github.com>2019-01-12 21:38:26 -0500
commit512b5316dd33a8aa36821ee9b134d6652fd4a35f (patch)
tree81e3434a480765ff76f41f7994685e9391e28aee /actionmailbox
parentbb75d68fe2262199a16973c09a8b2749542c7590 (diff)
downloadrails-512b5316dd33a8aa36821ee9b134d6652fd4a35f.tar.gz
rails-512b5316dd33a8aa36821ee9b134d6652fd4a35f.tar.bz2
rails-512b5316dd33a8aa36821ee9b134d6652fd4a35f.zip
Add Exim and Qmail support to Action Mailbox
Diffstat (limited to 'actionmailbox')
-rw-r--r--actionmailbox/README.md2
-rw-r--r--actionmailbox/app/controllers/action_mailbox/ingresses/relay/inbound_emails_controller.rb (renamed from actionmailbox/app/controllers/action_mailbox/ingresses/postfix/inbound_emails_controller.rb)30
-rw-r--r--actionmailbox/config/routes.rb2
-rw-r--r--actionmailbox/lib/action_mailbox/relayer.rb (renamed from actionmailbox/lib/action_mailbox/postfix_relayer.rb)28
-rw-r--r--actionmailbox/lib/tasks/ingress.rake58
-rw-r--r--actionmailbox/test/controllers/ingresses/relay/inbound_emails_controller_test.rb (renamed from actionmailbox/test/controllers/ingresses/postfix/inbound_emails_controller_test.rb)20
-rw-r--r--actionmailbox/test/unit/relayer_test.rb (renamed from actionmailbox/test/unit/postfix_relayer_test.rb)34
7 files changed, 122 insertions, 52 deletions
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/relay/inbound_emails_controller.rb
index c0d5002a12..2d91c968c8 100644
--- a/actionmailbox/app/controllers/action_mailbox/ingresses/postfix/inbound_emails_controller.rb
+++ b/actionmailbox/app/controllers/action_mailbox/ingresses/relay/inbound_emails_controller.rb
@@ -1,31 +1,31 @@
# frozen_string_literal: true
module ActionMailbox
- # Ingests inbound emails relayed from Postfix.
+ # 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 Postfix ingress can learn its password. You should only use the Postfix ingress over HTTPS.
+ # the ingress can learn its password. You should only use this ingress over HTTPS.
#
# Returns:
#
# - <tt>204 No Content</tt> if an inbound email is successfully recorded and enqueued for routing to the appropriate mailbox
# - <tt>401 Unauthorized</tt> if the request could not be authenticated
- # - <tt>404 Not Found</tt> if Action Mailbox is not configured to accept inbound emails from Postfix
+ # - <tt>404 Not Found</tt> if Action Mailbox is not configured to accept inbound emails relayed from an SMTP server
# - <tt>415 Unsupported Media Type</tt> if the request does not contain an RFC 822 message
# - <tt>500 Server Error</tt> 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:
+ # 1. Tell Action Mailbox to accept emails from an SMTP relay:
#
# # config/environments/production.rb
- # config.action_mailbox.ingress = :postfix
+ # config.action_mailbox.ingress = :relay
#
- # 2. Generate a strong password that Action Mailbox can use to authenticate requests to the Postfix ingress.
+ # 2. Generate a strong password that Action Mailbox can use to authenticate requests to the ingress.
#
# Use <tt>rails credentials:edit</tt> to add the password to your application's encrypted credentials under
# +action_mailbox.ingress_password+, where Action Mailbox will automatically find it:
@@ -35,14 +35,20 @@ module ActionMailbox
#
# 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 <tt>bin/rails action_mailbox:ingress:postfix</tt>, providing the +URL+ of the Postfix
- # ingress and the +INGRESS_PASSWORD+ you previously generated.
+ # 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 lived at <tt>https://example.com</tt>, the full command would look like this:
+ # If your application lives at <tt>https://example.com</tt>, you would configure the Postfix SMTP server to pipe
+ # inbound emails to the following command:
#
- # URL=https://example.com/rails/action_mailbox/postfix/inbound_emails INGRESS_PASSWORD=... bin/rails action_mailbox:ingress:postfix
- class Ingresses::Postfix::InboundEmailsController < ActionMailbox::BaseController
+ # 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 (<tt>bin/rails action_mailbox:ingress:exim)
+ # - Postfix (<tt>bin/rails action_mailbox:ingress:postfix)
+ # - Qmail (<tt>bin/rails action_mailbox:ingress:qmail)
+ class Ingresses::Relay::InboundEmailsController < ActionMailbox::BaseController
before_action :authenticate_by_password, :require_valid_rfc822_message
def create
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/relayer.rb
index d43c56ed2b..e2890acb60 100644
--- a/actionmailbox/lib/action_mailbox/postfix_relayer.rb
+++ b/actionmailbox/lib/action_mailbox/relayer.rb
@@ -5,19 +5,27 @@ require "net/http"
require "uri"
module ActionMailbox
- class PostfixRelayer
- class Result < Struct.new(:output)
+ class Relayer
+ class Result < Struct.new(:status_code, :message)
def success?
!failure?
end
def failure?
- output.match?(/\A[45]\.\d{1,3}\.\d{1,3}(\s|\z)/)
+ 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 Postfix relayer v#{ActionMailbox.version}"
+ USER_AGENT = "Action Mailbox relayer v#{ActionMailbox.version}"
attr_reader :uri, :username, :password
@@ -28,18 +36,18 @@ module ActionMailbox
def relay(source)
case response = post(source)
when Net::HTTPSuccess
- Result.new "2.0.0 Successfully relayed message to Postfix ingress"
+ Result.new "2.0.0", "Successfully relayed message to ingress"
when Net::HTTPUnauthorized
- Result.new "4.7.0 Invalid credentials for Postfix ingress"
+ Result.new "4.7.0", "Invalid credentials for ingress"
else
- Result.new "4.0.0 HTTP #{response.code}"
+ 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}"
+ Result.new "4.4.2", "Network error relaying to ingress: #{error.message}"
rescue Timeout::Error
- Result.new "4.4.2 Timed out relaying to Postfix ingress"
+ Result.new "4.4.2", "Timed out relaying to ingress"
rescue => error
- Result.new "4.0.0 Error relaying to Postfix ingress: #{error.message}"
+ Result.new "4.0.0", "Error relaying to ingress: #{error.message}"
end
private
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/relay/inbound_emails_controller_test.rb
index d646f5e859..67c5993f7f 100644
--- a/actionmailbox/test/controllers/ingresses/postfix/inbound_emails_controller_test.rb
+++ b/actionmailbox/test/controllers/ingresses/relay/inbound_emails_controller_test.rb
@@ -2,12 +2,12 @@
require "test_helper"
-class ActionMailbox::Ingresses::Postfix::InboundEmailsControllerTest < ActionDispatch::IntegrationTest
- setup { ActionMailbox.ingress = :postfix }
+class ActionMailbox::Ingresses::Relay::InboundEmailsControllerTest < ActionDispatch::IntegrationTest
+ setup { ActionMailbox.ingress = :relay }
- test "receiving an inbound email from Postfix" do
+ test "receiving an inbound email relayed from an SMTP server" do
assert_difference -> { ActionMailbox::InboundEmail.count }, +1 do
- post rails_postfix_inbound_emails_url, headers: { "Authorization" => credentials, "Content-Type" => "message/rfc822" },
+ post rails_relay_inbound_emails_url, headers: { "Authorization" => credentials, "Content-Type" => "message/rfc822" },
params: file_fixture("../files/welcome.eml").read
end
@@ -18,18 +18,18 @@ class ActionMailbox::Ingresses::Postfix::InboundEmailsControllerTest < ActionDis
assert_equal "0CB459E0-0336-41DA-BC88-E6E28C697DDB@37signals.com", inbound_email.message_id
end
- test "rejecting an unauthorized inbound email from Postfix" do
+ test "rejecting an unauthorized inbound email" do
assert_no_difference -> { ActionMailbox::InboundEmail.count } do
- post rails_postfix_inbound_emails_url, headers: { "Content-Type" => "message/rfc822" },
+ 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 from Postfix" do
+ test "rejecting an inbound email of an unsupported media type" do
assert_no_difference -> { ActionMailbox::InboundEmail.count } do
- post rails_postfix_inbound_emails_url, headers: { "Authorization" => credentials, "Content-Type" => "text/plain" },
+ post rails_relay_inbound_emails_url, headers: { "Authorization" => credentials, "Content-Type" => "text/plain" },
params: file_fixture("../files/welcome.eml").read
end
@@ -39,7 +39,7 @@ class ActionMailbox::Ingresses::Postfix::InboundEmailsControllerTest < ActionDis
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" },
+ post rails_relay_inbound_emails_url, headers: { "Authorization" => credentials, "Content-Type" => "message/rfc822" },
params: file_fixture("../files/welcome.eml").read
end
end
@@ -48,7 +48,7 @@ class ActionMailbox::Ingresses::Postfix::InboundEmailsControllerTest < ActionDis
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" },
+ post rails_relay_inbound_emails_url, headers: { "Authorization" => credentials, "Content-Type" => "message/rfc822" },
params: file_fixture("../files/welcome.eml").read
end
end
diff --git a/actionmailbox/test/unit/postfix_relayer_test.rb b/actionmailbox/test/unit/relayer_test.rb
index 5f7496ec3f..fb2b48ea16 100644
--- a/actionmailbox/test/unit/postfix_relayer_test.rb
+++ b/actionmailbox/test/unit/relayer_test.rb
@@ -2,35 +2,37 @@
require_relative "../test_helper"
-require "action_mailbox/postfix_relayer"
+require "action_mailbox/relayer"
module ActionMailbox
- class PostfixRelayerTest < ActiveSupport::TestCase
- URL = "https://example.com/rails/action_mailbox/postfix/inbound_emails"
+ class RelayerTest < ActiveSupport::TestCase
+ URL = "https://example.com/rails/action_mailbox/relay/inbound_emails"
INGRESS_PASSWORD = "secret"
setup do
- @relayer = ActionMailbox::PostfixRelayer.new(url: URL, password: INGRESS_PASSWORD)
+ @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 Successfully relayed message to Postfix ingress", result.output
+ 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 Postfix relayer v\d+\./ }
+ 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 Invalid credentials for Postfix ingress", result.output
+ assert_equal "4.7.0", result.status_code
+ assert_equal "Invalid credentials for ingress", result.message
assert_not result.success?
assert result.failure?
end
@@ -39,7 +41,8 @@ module ActionMailbox
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_equal "4.0.0", result.status_code
+ assert_equal "HTTP 500", result.message
assert_not result.success?
assert result.failure?
end
@@ -48,7 +51,8 @@ module ActionMailbox
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_equal "4.0.0", result.status_code
+ assert_equal "HTTP 504", result.message
assert_not result.success?
assert result.failure?
end
@@ -57,7 +61,8 @@ module ActionMailbox
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_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
@@ -66,7 +71,8 @@ module ActionMailbox
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_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
@@ -75,7 +81,8 @@ module ActionMailbox
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_equal "4.4.2", result.status_code
+ assert_equal "Timed out relaying to ingress", result.message
assert_not result.success?
assert result.failure?
end
@@ -84,7 +91,8 @@ module ActionMailbox
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_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