aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGeorge Claghorn <george@basecamp.com>2018-11-25 14:30:05 -0500
committerGeorge Claghorn <george@basecamp.com>2018-11-25 15:36:08 -0500
commit148110e70c0a408ea418a8e36a6a99305fdd9c99 (patch)
treee8e1d865885370e4e5771a21be7156abca53023d
parent2e4df7cea354339cbae20252ae14106a0cce12b5 (diff)
downloadrails-148110e70c0a408ea418a8e36a6a99305fdd9c99.tar.gz
rails-148110e70c0a408ea418a8e36a6a99305fdd9c99.tar.bz2
rails-148110e70c0a408ea418a8e36a6a99305fdd9c99.zip
Extract ActionMailbox::PostfixRelayer
-rw-r--r--Gemfile.lock24
-rw-r--r--actionmailbox.gemspec2
-rw-r--r--lib/action_mailbox/postfix_relayer.rb60
-rw-r--r--lib/tasks/ingress.rake37
-rw-r--r--test/test_helper.rb1
-rw-r--r--test/unit/postfix_relayer_test.rb80
6 files changed, 159 insertions, 45 deletions
diff --git a/Gemfile.lock b/Gemfile.lock
index 402a6e8a10..6d8ef0e9f1 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -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
diff --git a/actionmailbox.gemspec b/actionmailbox.gemspec
index e3890ab574..e3dae277d4 100644
--- a/actionmailbox.gemspec
+++ b/actionmailbox.gemspec
@@ -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")
diff --git a/lib/action_mailbox/postfix_relayer.rb b/lib/action_mailbox/postfix_relayer.rb
new file mode 100644
index 0000000000..de4fdc06d0
--- /dev/null
+++ b/lib/action_mailbox/postfix_relayer.rb
@@ -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
diff --git a/lib/tasks/ingress.rake b/lib/tasks/ingress.rake
index 6620e1fe43..6d1c0797ff 100644
--- a/lib/tasks/ingress.rake
+++ b/lib/tasks/ingress.rake
@@ -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
diff --git a/test/test_helper.rb b/test/test_helper.rb
index b4459f3feb..31fc59bba2 100644
--- a/test/test_helper.rb
+++ b/test/test_helper.rb
@@ -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
diff --git a/test/unit/postfix_relayer_test.rb b/test/unit/postfix_relayer_test.rb
new file mode 100644
index 0000000000..9f5b78e216
--- /dev/null
+++ b/test/unit/postfix_relayer_test.rb
@@ -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