Extract ActionMailbox::PostfixRelayer

This commit is contained in:
George Claghorn 2018-11-25 14:30:05 -05:00
parent 2e4df7cea3
commit 148110e70c
6 changed files with 159 additions and 45 deletions

View File

@ -66,7 +66,6 @@ PATH
remote: .
specs:
actionmailbox (0.1.0)
http (>= 4.0.0)
rails (>= 5.2.0)
GEM
@ -88,21 +87,13 @@ GEM
builder (3.2.3)
byebug (10.0.2)
concurrent-ruby (1.0.5)
crack (0.4.3)
safe_yaml (~> 1.0.0)
crass (1.0.4)
domain_name (0.5.20180417)
unf (>= 0.0.5, < 1.0.0)
erubi (1.7.1)
globalid (0.4.1)
activesupport (>= 4.2.0)
http (4.0.0)
addressable (~> 2.3)
http-cookie (~> 1.0)
http-form_data (~> 2.0)
http_parser.rb (~> 0.6.0)
http-cookie (1.0.3)
domain_name (~> 0.5)
http-form_data (2.1.1)
http_parser.rb (0.6.0)
hashdiff (0.3.7)
i18n (1.1.0)
concurrent-ruby (~> 1.0)
jmespath (1.4.0)
@ -131,6 +122,7 @@ GEM
rails-html-sanitizer (1.0.4)
loofah (~> 2.2, >= 2.2.2)
rake (12.3.1)
safe_yaml (1.0.4)
sprockets (3.7.2)
concurrent-ruby (~> 1.0)
rack (> 1, < 3)
@ -143,9 +135,10 @@ GEM
thread_safe (0.3.6)
tzinfo (1.2.5)
thread_safe (~> 0.1)
unf (0.1.4)
unf_ext
unf_ext (0.0.7.5)
webmock (3.4.2)
addressable (>= 2.3.6)
crack (>= 0.3.2)
hashdiff
websocket-driver (0.7.0)
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.3)
@ -160,6 +153,7 @@ DEPENDENCIES
byebug
rails!
sqlite3
webmock
BUNDLED WITH
1.16.6

View File

@ -16,11 +16,11 @@ Gem::Specification.new do |s|
s.required_ruby_version = ">= 2.5.0"
s.add_dependency "rails", ">= 5.2.0"
s.add_dependency "http", ">= 4.0.0"
s.add_development_dependency "bundler", "~> 1.15"
s.add_development_dependency "sqlite3"
s.add_development_dependency "byebug"
s.add_development_dependency "webmock"
s.files = `git ls-files`.split("\n")
s.test_files = `git ls-files -- test/*`.split("\n")

View File

@ -0,0 +1,60 @@
# frozen_string_literal: true
require "net/http"
require "uri"
require "openssl"
module ActionMailbox
class PostfixRelayer
class Result < Struct.new(:output)
def success?
!failure?
end
def failure?
output.match?(/\A[45]\.\d\.\d /)
end
end
attr_reader :uri, :username, :password, :user_agent
def initialize(url:, username: "actionmailbox", password:, user_agent: nil)
@uri, @username, @password, @user_agent = URI(url), username, password, user_agent || "Postfix"
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.path, source,
"Content-Type" => "message/rfc822",
"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|
connection.use_ssl = uri.scheme == "https"
connection.verify_mode = OpenSSL::SSL::VERIFY_PEER
connection.open_timeout = 1
connection.read_timeout = 10
end
end
end
end

View File

@ -2,45 +2,24 @@
namespace :action_mailbox do
namespace :ingress do
desc "Pipe an inbound email from STDIN to the Postfix ingress at the given URL"
desc "Pipe an inbound email from STDIN to the Postfix ingress (URL and INGRESS_PASSWORD required)"
task :postfix do
require "active_support"
require "active_support/core_ext/object/blank"
require "http"
require "action_mailbox/ingresses/postfix/relayer"
url, password = ENV.values_at("URL", "INGRESS_PASSWORD")
url, password, user_agent = ENV.values_at("URL", "INGRESS_PASSWORD", "USER_AGENT")
if url.blank? || password.blank?
puts "4.3.5 URL and INGRESS_PASSWORD are required"
echo "4.3.5 URL and INGRESS_PASSWORD are required"
exit 1
end
begin
response = HTTP.basic_auth(user: "actionmailbox", pass: password)
.timeout(connect: 1, write: 10, read: 10)
.post(url, body: STDIN.read,
headers: { "Content-Type" => "message/rfc822", "User-Agent" => ENV.fetch("USER_AGENT", "Postfix") })
case
when response.status.success?
puts "2.0.0 HTTP #{response.status}"
when response.status.unauthorized?
puts "4.7.0 HTTP #{response.status}"
exit 1
when response.status.unsupported_media_type?
puts "5.6.1 HTTP #{response.status}"
exit 1
else
puts "4.0.0 HTTP #{response.status}"
exit 1
ActionMailbox::PostfixRelayer.new(url: url, password: password, user_agent: user_agent)
.relay(STDIN.read).tap do |result|
echo result.output
exit result.success? ? 0 : 1
end
rescue HTTP::ConnectionError => error
puts "4.4.2 Error connecting to the Postfix ingress: #{error.message}"
exit 1
rescue HTTP::TimeoutError
puts "4.4.7 Timed out piping to the Postfix ingress"
exit 1
end
end
end
end

View File

@ -6,6 +6,7 @@ ActiveRecord::Migrator.migrations_paths = [File.expand_path("../test/dummy/db/mi
require "rails/test_help"
require "byebug"
require "webmock/minitest"
Minitest.backtrace_filter = Minitest::BacktraceFilter.new

View File

@ -0,0 +1,80 @@
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" => "Postfix" }
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 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