diff options
author | George Claghorn <george@basecamp.com> | 2018-11-25 14:30:05 -0500 |
---|---|---|
committer | George Claghorn <george@basecamp.com> | 2018-11-25 15:36:08 -0500 |
commit | 148110e70c0a408ea418a8e36a6a99305fdd9c99 (patch) | |
tree | e8e1d865885370e4e5771a21be7156abca53023d | |
parent | 2e4df7cea354339cbae20252ae14106a0cce12b5 (diff) | |
download | rails-148110e70c0a408ea418a8e36a6a99305fdd9c99.tar.gz rails-148110e70c0a408ea418a8e36a6a99305fdd9c99.tar.bz2 rails-148110e70c0a408ea418a8e36a6a99305fdd9c99.zip |
Extract ActionMailbox::PostfixRelayer
-rw-r--r-- | Gemfile.lock | 24 | ||||
-rw-r--r-- | actionmailbox.gemspec | 2 | ||||
-rw-r--r-- | lib/action_mailbox/postfix_relayer.rb | 60 | ||||
-rw-r--r-- | lib/tasks/ingress.rake | 37 | ||||
-rw-r--r-- | test/test_helper.rb | 1 | ||||
-rw-r--r-- | test/unit/postfix_relayer_test.rb | 80 |
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 |