aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGeorge Claghorn <george.claghorn@gmail.com>2018-11-05 14:21:27 -0500
committerGitHub <noreply@github.com>2018-11-05 14:21:27 -0500
commit152a442b1902050265ddcef56f5506b3bfbb12e4 (patch)
treebd365d76299e978a6e974a64f5ff3d47f0614514
parentc474daefb18e9bab96f6f0bb0bb30dfc00058cb3 (diff)
parentac7fd0e56886eb134554789e014d2736b95d7042 (diff)
downloadrails-152a442b1902050265ddcef56f5506b3bfbb12e4.tar.gz
rails-152a442b1902050265ddcef56f5506b3bfbb12e4.tar.bz2
rails-152a442b1902050265ddcef56f5506b3bfbb12e4.zip
Merge pull request #1 from basecamp/ingresses
Accept inbound emails from a variety of ingresses
-rw-r--r--Gemfile2
-rw-r--r--Gemfile.lock31
-rw-r--r--actionmailbox.gemspec1
-rw-r--r--app/controllers/action_mailbox/base_controller.rb42
-rw-r--r--app/controllers/action_mailbox/inbound_emails_controller.rb17
-rw-r--r--app/controllers/action_mailbox/ingresses/amazon/inbound_emails_controller.rb21
-rw-r--r--app/controllers/action_mailbox/ingresses/mailgun/inbound_emails_controller.rb58
-rw-r--r--app/controllers/action_mailbox/ingresses/mandrill/inbound_emails_controller.rb65
-rw-r--r--app/controllers/action_mailbox/ingresses/postfix/inbound_emails_controller.rb14
-rw-r--r--app/controllers/action_mailbox/ingresses/sendgrid/inbound_emails_controller.rb7
-rw-r--r--app/models/action_mailbox/inbound_email/message_id.rb12
-rw-r--r--config/routes.rb15
-rw-r--r--lib/action_mailbox.rb1
-rw-r--r--lib/action_mailbox/engine.rb12
-rw-r--r--lib/action_mailbox/test_helper.rb11
-rw-r--r--lib/tasks/ingress.rake41
-rw-r--r--test/controllers/ingresses/amazon/inbound_emails_controller_test.rb20
-rw-r--r--test/controllers/ingresses/mailgun/inbound_emails_controller_test.rb89
-rw-r--r--test/controllers/ingresses/mandrill/inbound_emails_controller_test.rb58
-rw-r--r--test/controllers/ingresses/postfix/inbound_emails_controller_test.rb54
-rw-r--r--test/controllers/ingresses/sendgrid/inbound_emails_controller_test.rb44
-rw-r--r--test/fixtures/files/text.txt1
-rw-r--r--test/test_helper.rb19
-rw-r--r--test/unit/controller_test.rb27
-rw-r--r--test/unit/inbound_email/message_id_test.rb4
25 files changed, 599 insertions, 67 deletions
diff --git a/Gemfile b/Gemfile
index 65f1a6f190..bb5cba25a7 100644
--- a/Gemfile
+++ b/Gemfile
@@ -4,3 +4,5 @@ git_source(:github) { |repo_path| "https://github.com/#{repo_path}.git" }
gemspec
gem "rails", github: "rails/rails"
+
+gem "aws-sdk-sns"
diff --git a/Gemfile.lock b/Gemfile.lock
index d01cb7bd76..402a6e8a10 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -66,20 +66,46 @@ PATH
remote: .
specs:
actionmailbox (0.1.0)
+ http (>= 4.0.0)
rails (>= 5.2.0)
GEM
remote: https://rubygems.org/
specs:
+ addressable (2.5.2)
+ public_suffix (>= 2.0.2, < 4.0)
+ aws-eventstream (1.0.1)
+ aws-partitions (1.105.0)
+ aws-sdk-core (3.30.0)
+ aws-eventstream (~> 1.0)
+ aws-partitions (~> 1.0)
+ aws-sigv4 (~> 1.0)
+ jmespath (~> 1.0)
+ aws-sdk-sns (1.5.0)
+ aws-sdk-core (~> 3, >= 3.26.0)
+ aws-sigv4 (~> 1.0)
+ aws-sigv4 (1.0.3)
builder (3.2.3)
byebug (10.0.2)
concurrent-ruby (1.0.5)
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)
i18n (1.1.0)
concurrent-ruby (~> 1.0)
+ jmespath (1.4.0)
loofah (2.2.2)
crass (~> 1.0.2)
nokogiri (>= 1.5.9)
@@ -95,6 +121,7 @@ GEM
nio4r (2.3.1)
nokogiri (1.8.5)
mini_portile2 (~> 2.3.0)
+ public_suffix (3.0.3)
rack (2.0.5)
rack-test (1.1.0)
rack (>= 1.0, < 3)
@@ -116,6 +143,9 @@ 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)
websocket-driver (0.7.0)
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.3)
@@ -125,6 +155,7 @@ PLATFORMS
DEPENDENCIES
actionmailbox!
+ aws-sdk-sns
bundler (~> 1.15)
byebug
rails!
diff --git a/actionmailbox.gemspec b/actionmailbox.gemspec
index e413ac57f6..e3890ab574 100644
--- a/actionmailbox.gemspec
+++ b/actionmailbox.gemspec
@@ -16,6 +16,7 @@ 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"
diff --git a/app/controllers/action_mailbox/base_controller.rb b/app/controllers/action_mailbox/base_controller.rb
new file mode 100644
index 0000000000..c234ecd250
--- /dev/null
+++ b/app/controllers/action_mailbox/base_controller.rb
@@ -0,0 +1,42 @@
+class ActionMailbox::BaseController < ActionController::Base
+ skip_forgery_protection
+
+ def self.prepare
+ # Override in concrete controllers to run code on load.
+ end
+
+ before_action :ensure_configured
+
+ private
+ def ensure_configured
+ unless ActionMailbox.ingress == ingress_name
+ head :not_found
+ end
+ end
+
+ def ingress_name
+ self.class.name[/^ActionMailbox::Ingresses::(.*?)::/, 1].underscore.to_sym
+ end
+
+
+ def authenticate_by_password
+ if password.present?
+ http_basic_authenticate_or_request_with username: "actionmailbox", password: password, realm: "Action Mailbox"
+ else
+ raise ArgumentError, "Missing required ingress credentials"
+ end
+ end
+
+ def password
+ Rails.application.credentials.dig(:action_mailbox, :ingress_password) || ENV["RAILS_INBOUND_EMAIL_PASSWORD"]
+ end
+
+
+ # TODO: Extract to ActionController::HttpAuthentication
+ def http_basic_authenticate_or_request_with(username:, password:, realm: nil)
+ authenticate_or_request_with_http_basic(realm || "Application") do |given_username, given_password|
+ ActiveSupport::SecurityUtils.secure_compare(given_username, username) &
+ ActiveSupport::SecurityUtils.secure_compare(given_password, password)
+ end
+ end
+end
diff --git a/app/controllers/action_mailbox/inbound_emails_controller.rb b/app/controllers/action_mailbox/inbound_emails_controller.rb
deleted file mode 100644
index ec9bd6f229..0000000000
--- a/app/controllers/action_mailbox/inbound_emails_controller.rb
+++ /dev/null
@@ -1,17 +0,0 @@
-# TODO: Add access protection using basic auth with verified tokens. Maybe coming from credentials by default?
-# TODO: Spam/malware catching?
-# TODO: Specific bounces for SMTP good citizenship: 200/404/400
-class ActionMailbox::InboundEmailsController < ActionController::Base
- skip_forgery_protection
- before_action :require_rfc822_message, only: :create
-
- def create
- ActionMailbox::InboundEmail.create_and_extract_message_id!(params[:message])
- head :created
- end
-
- private
- def require_rfc822_message
- head :unsupported_media_type unless params.require(:message).content_type == 'message/rfc822'
- end
-end
diff --git a/app/controllers/action_mailbox/ingresses/amazon/inbound_emails_controller.rb b/app/controllers/action_mailbox/ingresses/amazon/inbound_emails_controller.rb
new file mode 100644
index 0000000000..d3998be2d4
--- /dev/null
+++ b/app/controllers/action_mailbox/ingresses/amazon/inbound_emails_controller.rb
@@ -0,0 +1,21 @@
+class ActionMailbox::Ingresses::Amazon::InboundEmailsController < ActionMailbox::BaseController
+ before_action :authenticate
+
+ cattr_accessor :verifier
+
+ def self.prepare
+ self.verifier ||= begin
+ require "aws-sdk-sns/message_verifier"
+ Aws::SNS::MessageVerifier.new
+ end
+ end
+
+ def create
+ ActionMailbox::InboundEmail.create_and_extract_message_id! params.require(:content)
+ end
+
+ private
+ def authenticate
+ head :unauthorized unless verifier.authentic?(request.body)
+ end
+end
diff --git a/app/controllers/action_mailbox/ingresses/mailgun/inbound_emails_controller.rb b/app/controllers/action_mailbox/ingresses/mailgun/inbound_emails_controller.rb
new file mode 100644
index 0000000000..e878192603
--- /dev/null
+++ b/app/controllers/action_mailbox/ingresses/mailgun/inbound_emails_controller.rb
@@ -0,0 +1,58 @@
+class ActionMailbox::Ingresses::Mailgun::InboundEmailsController < ActionMailbox::BaseController
+ before_action :authenticate
+
+ def create
+ ActionMailbox::InboundEmail.create_and_extract_message_id! params.require("body-mime")
+ end
+
+ private
+ def authenticate
+ head :unauthorized unless authenticated?
+ end
+
+ def authenticated?
+ if key.present?
+ Authenticator.new(
+ key: key,
+ timestamp: params.require(:timestamp),
+ token: params.require(:token),
+ signature: params.require(:signature)
+ ).authenticated?
+ else
+ raise ArgumentError, <<~MESSAGE.squish
+ Missing required Mailgun API key. Set action_mailbox.mailgun_api_key in your application's
+ encrypted credentials or provide the MAILGUN_INGRESS_API_KEY environment variable.
+ MESSAGE
+ end
+ end
+
+ def key
+ Rails.application.credentials.dig(:action_mailbox, :mailgun_api_key) || ENV["MAILGUN_INGRESS_API_KEY"]
+ end
+
+ class Authenticator
+ attr_reader :key, :timestamp, :token, :signature
+
+ def initialize(key:, timestamp:, token:, signature:)
+ @key, @timestamp, @token, @signature = key, Integer(timestamp), token, signature
+ end
+
+ def authenticated?
+ signed? && recent?
+ end
+
+ private
+ def signed?
+ ActiveSupport::SecurityUtils.secure_compare signature, expected_signature
+ end
+
+ # Allow for 2 minutes of drift between Mailgun time and local server time.
+ def recent?
+ Time.at(timestamp) >= 2.minutes.ago
+ end
+
+ def expected_signature
+ OpenSSL::HMAC.hexdigest OpenSSL::Digest::SHA256.new, key, "#{timestamp}#{token}"
+ end
+ end
+end
diff --git a/app/controllers/action_mailbox/ingresses/mandrill/inbound_emails_controller.rb b/app/controllers/action_mailbox/ingresses/mandrill/inbound_emails_controller.rb
new file mode 100644
index 0000000000..b32b254076
--- /dev/null
+++ b/app/controllers/action_mailbox/ingresses/mandrill/inbound_emails_controller.rb
@@ -0,0 +1,65 @@
+class ActionMailbox::Ingresses::Mandrill::InboundEmailsController < ActionMailbox::BaseController
+ before_action :authenticate
+
+ def create
+ raw_emails.each { |raw_email| ActionMailbox::InboundEmail.create_and_extract_message_id! raw_email }
+ head :ok
+ rescue JSON::ParserError => error
+ logger.error error.message
+ head :unprocessable_entity
+ end
+
+ private
+ def raw_emails
+ events.select { |event| event["event"] == "inbound" }.collect { |event| event.dig("msg", "raw_msg") }
+ end
+
+ def events
+ JSON.parse params.require(:mandrill_events)
+ end
+
+
+ def authenticate
+ head :unauthorized unless authenticated?
+ end
+
+ def authenticated?
+ if key.present?
+ Authenticator.new(request, key).authenticated?
+ else
+ raise ArgumentError, <<~MESSAGE.squish
+ Missing required Mandrill API key. Set action_mailbox.mandrill_api_key in your application's
+ encrypted credentials or provide the MANDRILL_INGRESS_API_KEY environment variable.
+ MESSAGE
+ end
+ end
+
+ def key
+ Rails.application.credentials.dig(:action_mailbox, :mandrill_api_key) || ENV["MANDRILL_INGRESS_API_KEY"]
+ end
+
+ class Authenticator
+ attr_reader :request, :key
+
+ def initialize(request, key)
+ @request, @key = request, key
+ end
+
+ def authenticated?
+ ActiveSupport::SecurityUtils.secure_compare given_signature, expected_signature
+ end
+
+ private
+ def given_signature
+ request.headers["X-Mandrill-Signature"]
+ end
+
+ def expected_signature
+ Base64.strict_encode64 OpenSSL::HMAC.digest(OpenSSL::Digest::SHA1.new, key, message)
+ end
+
+ def message
+ request.url + request.POST.sort.flatten.join
+ end
+ end
+end
diff --git a/app/controllers/action_mailbox/ingresses/postfix/inbound_emails_controller.rb b/app/controllers/action_mailbox/ingresses/postfix/inbound_emails_controller.rb
new file mode 100644
index 0000000000..133accf651
--- /dev/null
+++ b/app/controllers/action_mailbox/ingresses/postfix/inbound_emails_controller.rb
@@ -0,0 +1,14 @@
+class ActionMailbox::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
diff --git a/app/controllers/action_mailbox/ingresses/sendgrid/inbound_emails_controller.rb b/app/controllers/action_mailbox/ingresses/sendgrid/inbound_emails_controller.rb
new file mode 100644
index 0000000000..b856eb5b94
--- /dev/null
+++ b/app/controllers/action_mailbox/ingresses/sendgrid/inbound_emails_controller.rb
@@ -0,0 +1,7 @@
+class ActionMailbox::Ingresses::Sendgrid::InboundEmailsController < ActionMailbox::BaseController
+ before_action :authenticate_by_password
+
+ def create
+ ActionMailbox::InboundEmail.create_and_extract_message_id! params.require(:email)
+ end
+end
diff --git a/app/models/action_mailbox/inbound_email/message_id.rb b/app/models/action_mailbox/inbound_email/message_id.rb
index a1ec5c0437..5cfcadaba1 100644
--- a/app/models/action_mailbox/inbound_email/message_id.rb
+++ b/app/models/action_mailbox/inbound_email/message_id.rb
@@ -6,14 +6,16 @@ module ActionMailbox::InboundEmail::MessageId
end
module ClassMethods
- def create_and_extract_message_id!(raw_email, **options)
- create! raw_email: raw_email, message_id: extract_message_id(raw_email), **options
+ def create_and_extract_message_id!(source, **options)
+ create! message_id: extract_message_id(source), **options do |inbound_email|
+ inbound_email.raw_email.attach io: StringIO.new(source), filename: "message.eml", content_type: "message/rfc822"
+ end
end
private
- def extract_message_id(raw_email)
- mail_from_source(raw_email.read).message_id
- rescue
+ def extract_message_id(source)
+ mail_from_source(source).message_id
+ rescue => e
# FIXME: Add logging with "Couldn't extract Message ID, so will generating a new random ID instead"
end
end
diff --git a/config/routes.rb b/config/routes.rb
index 733f137262..99a15d1d32 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -1,11 +1,20 @@
# frozen_string_literal: true
Rails.application.routes.draw do
- post "/rails/action_mailbox/inbound_emails" => "action_mailbox/inbound_emails#create", as: :rails_inbound_emails
+ 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 "/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.
+ post "/mailgun/inbound_emails/mime" => "mailgun/inbound_emails#create", as: :rails_mailgun_inbound_emails
+ end
# TODO: Should these be mounted within the engine only?
scope "rails/conductor/action_mailbox/", module: "rails/conductor/action_mailbox" do
- resources :inbound_emails, as: :rails_conductor_inbound_emails
- post ":inbound_email_id/reroute" => "reroutes#create", as: :rails_conductor_inbound_email_reroute
+ resources :inbound_emails, as: :rails_conductor_inbound_emails do
+ post "reroute" => "reroutes#create"
+ end
end
end
diff --git a/lib/action_mailbox.rb b/lib/action_mailbox.rb
index ae37cb84ed..fbc8122d9d 100644
--- a/lib/action_mailbox.rb
+++ b/lib/action_mailbox.rb
@@ -6,6 +6,7 @@ module ActionMailbox
autoload :Base
autoload :Router
+ mattr_accessor :ingress
mattr_accessor :logger
mattr_accessor :incinerate_after, default: 30.days
end
diff --git a/lib/action_mailbox/engine.rb b/lib/action_mailbox/engine.rb
index b4758aacb5..cf438d8f24 100644
--- a/lib/action_mailbox/engine.rb
+++ b/lib/action_mailbox/engine.rb
@@ -14,5 +14,17 @@ module ActionMailbox
ActionMailbox.incinerate_after = app.config.action_mailbox.incinerate_after || 30.days
end
end
+
+ initializer "action_mailbox.ingress" do
+ config.after_initialize do |app|
+ if ActionMailbox.ingress = app.config.action_mailbox.ingress.presence
+ config.to_prepare do
+ if ingress_controller_class = "ActionMailbox::Ingresses::#{ActionMailbox.ingress.to_s.classify}::InboundEmailsController".safe_constantize
+ ingress_controller_class.prepare
+ end
+ end
+ end
+ end
+ end
end
end
diff --git a/lib/action_mailbox/test_helper.rb b/lib/action_mailbox/test_helper.rb
index a74ea8ef57..23b2bb02ca 100644
--- a/lib/action_mailbox/test_helper.rb
+++ b/lib/action_mailbox/test_helper.rb
@@ -5,18 +5,15 @@ module ActionMailbox
# Create an InboundEmail record using an eml fixture in the format of message/rfc822
# referenced with +fixture_name+ located in +test/fixtures/files/fixture_name+.
def create_inbound_email_from_fixture(fixture_name, status: :processing)
- create_inbound_email file_fixture(fixture_name), filename: fixture_name, status: status
+ create_inbound_email file_fixture(fixture_name).read, status: status
end
def create_inbound_email_from_mail(status: :processing, **mail_options)
- raw_email = Tempfile.new.tap { |io| io.write Mail.new(mail_options).to_s }
- create_inbound_email(raw_email, status: status)
+ create_inbound_email Mail.new(mail_options).to_s, status: status
end
- def create_inbound_email(io, filename: 'mail.eml', status: :processing)
- ActionMailbox::InboundEmail.create_and_extract_message_id! \
- ActionDispatch::Http::UploadedFile.new(tempfile: io, filename: filename, type: 'message/rfc822'),
- status: status
+ def create_inbound_email(source, status: :processing)
+ ActionMailbox::InboundEmail.create_and_extract_message_id! source, status: status
end
def receive_inbound_email_from_fixture(*args)
diff --git a/lib/tasks/ingress.rake b/lib/tasks/ingress.rake
new file mode 100644
index 0000000000..510951aa2a
--- /dev/null
+++ b/lib/tasks/ingress.rake
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+namespace :action_mailbox do
+ namespace :ingress do
+ desc "Pipe an inbound email from STDIN to the Postfix ingress at the given URL"
+ task :postfix do
+ require "active_support"
+ require "active_support/core_ext/object/blank"
+ require "http"
+
+ unless url = ENV["URL"].presence
+ abort "5.3.5 URL is required"
+ end
+
+ unless password = ENV["INGRESS_PASSWORD"].presence
+ abort "5.3.5 INGRESS_PASSWORD is required"
+ end
+
+ begin
+ response = HTTP.basic_auth(user: "actionmailbox", pass: password)
+ .timeout(connect: 1, write: 10, read: 10)
+ .post(url, headers: { "Content-Type" => "message/rfc822", "User-Agent" => ENV.fetch("USER_AGENT", "Postfix") }, body: STDIN)
+
+ case
+ when response.status.success?
+ puts "2.0.0 HTTP #{response.status}"
+ when response.status.unauthorized?
+ abort "4.7.0 HTTP #{response.status}"
+ when response.status.unsupported_media_type?
+ abort "5.6.1 HTTP #{response.status}"
+ else
+ abort "4.0.0 HTTP #{response.status}"
+ end
+ rescue HTTP::ConnectionError => error
+ abort "4.4.2 Error connecting to the Postfix ingress: #{error.message}"
+ rescue HTTP::TimeoutError
+ abort "4.4.7 Timed out piping to the Postfix ingress"
+ end
+ end
+ end
+end
diff --git a/test/controllers/ingresses/amazon/inbound_emails_controller_test.rb b/test/controllers/ingresses/amazon/inbound_emails_controller_test.rb
new file mode 100644
index 0000000000..c36c500cbe
--- /dev/null
+++ b/test/controllers/ingresses/amazon/inbound_emails_controller_test.rb
@@ -0,0 +1,20 @@
+require "test_helper"
+
+ActionMailbox::Ingresses::Amazon::InboundEmailsController.verifier =
+ Module.new { def self.authentic?(message); true; end }
+
+class ActionMailbox::Ingresses::Amazon::InboundEmailsControllerTest < ActionDispatch::IntegrationTest
+ setup { ActionMailbox.ingress = :amazon }
+
+ test "receiving an inbound email from Amazon" do
+ assert_difference -> { ActionMailbox::InboundEmail.count }, +1 do
+ post rails_amazon_inbound_emails_url, params: { content: file_fixture("../files/welcome.eml").read }, as: :json
+ 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
+end
diff --git a/test/controllers/ingresses/mailgun/inbound_emails_controller_test.rb b/test/controllers/ingresses/mailgun/inbound_emails_controller_test.rb
new file mode 100644
index 0000000000..c5ec71013e
--- /dev/null
+++ b/test/controllers/ingresses/mailgun/inbound_emails_controller_test.rb
@@ -0,0 +1,89 @@
+require "test_helper"
+
+ENV["MAILGUN_INGRESS_API_KEY"] = "tbsy84uSV1Kt3ZJZELY2TmShPRs91E3yL4tzf96297vBCkDWgL"
+
+class ActionMailbox::Ingresses::Mailgun::InboundEmailsControllerTest < ActionDispatch::IntegrationTest
+ setup { ActionMailbox.ingress = :mailgun }
+
+ test "receiving an inbound email from Mailgun" do
+ assert_difference -> { ActionMailbox::InboundEmail.count }, +1 do
+ travel_to "2018-10-09 15:15:00 EDT"
+ post rails_mailgun_inbound_emails_url, params: {
+ timestamp: 1539112500,
+ token: "7VwW7k6Ak7zcTwoSoNm7aTtbk1g67MKAnsYLfUB7PdszbgR5Xi",
+ signature: "ef24c5225322217bb065b80bb54eb4f9206d764e3e16abab07f0a64d1cf477cc",
+ "body-mime" => 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 a delayed inbound email from Mailgun" do
+ assert_no_difference -> { ActionMailbox::InboundEmail.count } do
+ travel_to "2018-10-09 15:26:00 EDT"
+ post rails_mailgun_inbound_emails_url, params: {
+ timestamp: 1539112500,
+ token: "7VwW7k6Ak7zcTwoSoNm7aTtbk1g67MKAnsYLfUB7PdszbgR5Xi",
+ signature: "ef24c5225322217bb065b80bb54eb4f9206d764e3e16abab07f0a64d1cf477cc",
+ "body-mime" => file_fixture("../files/welcome.eml").read
+ }
+ end
+
+ assert_response :unauthorized
+ end
+
+ test "rejecting a forged inbound email from Mailgun" do
+ assert_no_difference -> { ActionMailbox::InboundEmail.count } do
+ travel_to "2018-10-09 15:15:00 EDT"
+ post rails_mailgun_inbound_emails_url, params: {
+ timestamp: 1539112500,
+ token: "Zx8mJBiGmiiyyfWnho3zKyjCg2pxLARoCuBM7X9AKCioShGiMX",
+ signature: "ef24c5225322217bb065b80bb54eb4f9206d764e3e16abab07f0a64d1cf477cc",
+ "body-mime" => file_fixture("../files/welcome.eml").read
+ }
+ end
+
+ assert_response :unauthorized
+ end
+
+ test "raising when the configured Mailgun API key is nil" do
+ switch_key_to nil do
+ assert_raises ArgumentError do
+ travel_to "2018-10-09 15:15:00 EDT"
+ post rails_mailgun_inbound_emails_url, params: {
+ timestamp: 1539112500,
+ token: "7VwW7k6Ak7zcTwoSoNm7aTtbk1g67MKAnsYLfUB7PdszbgR5Xi",
+ signature: "ef24c5225322217bb065b80bb54eb4f9206d764e3e16abab07f0a64d1cf477cc",
+ "body-mime" => file_fixture("../files/welcome.eml").read
+ }
+ end
+ end
+ end
+
+ test "raising when the configured Mailgun API key is blank" do
+ switch_key_to "" do
+ assert_raises ArgumentError do
+ travel_to "2018-10-09 15:15:00 EDT"
+ post rails_mailgun_inbound_emails_url, params: {
+ timestamp: 1539112500,
+ token: "7VwW7k6Ak7zcTwoSoNm7aTtbk1g67MKAnsYLfUB7PdszbgR5Xi",
+ signature: "ef24c5225322217bb065b80bb54eb4f9206d764e3e16abab07f0a64d1cf477cc",
+ "body-mime" => file_fixture("../files/welcome.eml").read
+ }
+ end
+ end
+ end
+
+ private
+ def switch_key_to(new_key)
+ previous_key, ENV["MAILGUN_INGRESS_API_KEY"] = ENV["MAILGUN_INGRESS_API_KEY"], new_key
+ yield
+ ensure
+ ENV["MAILGUN_INGRESS_API_KEY"] = previous_key
+ end
+end
diff --git a/test/controllers/ingresses/mandrill/inbound_emails_controller_test.rb b/test/controllers/ingresses/mandrill/inbound_emails_controller_test.rb
new file mode 100644
index 0000000000..c8a8e731d6
--- /dev/null
+++ b/test/controllers/ingresses/mandrill/inbound_emails_controller_test.rb
@@ -0,0 +1,58 @@
+require "test_helper"
+
+ENV["MANDRILL_INGRESS_API_KEY"] = "1l9Qf7lutEf7h73VXfBwhw"
+
+class ActionMailbox::Ingresses::Mandrill::InboundEmailsControllerTest < ActionDispatch::IntegrationTest
+ setup do
+ ActionMailbox.ingress = :mandrill
+ @events = JSON.generate([{ event: "inbound", msg: { raw_msg: file_fixture("../files/welcome.eml").read } }])
+ end
+
+ test "receiving an inbound email from Mandrill" do
+ assert_difference -> { ActionMailbox::InboundEmail.count }, +1 do
+ post rails_mandrill_inbound_emails_url,
+ headers: { "X-Mandrill-Signature" => "gldscd2tAb/G+DmpiLcwukkLrC4=" }, params: { mandrill_events: @events }
+ end
+
+ assert_response :ok
+
+ 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 a forged inbound email from Mandrill" do
+ assert_no_difference -> { ActionMailbox::InboundEmail.count } do
+ post rails_mandrill_inbound_emails_url,
+ headers: { "X-Mandrill-Signature" => "forged" }, params: { mandrill_events: @events }
+ end
+
+ assert_response :unauthorized
+ end
+
+ test "raising when Mandrill API key is nil" do
+ switch_key_to nil do
+ assert_raises ArgumentError do
+ post rails_mandrill_inbound_emails_url,
+ headers: { "X-Mandrill-Signature" => "gldscd2tAb/G+DmpiLcwukkLrC4=" }, params: { mandrill_events: @events }
+ end
+ end
+ end
+
+ test "raising when Mandrill API key is blank" do
+ switch_key_to "" do
+ assert_raises ArgumentError do
+ post rails_mandrill_inbound_emails_url,
+ headers: { "X-Mandrill-Signature" => "gldscd2tAb/G+DmpiLcwukkLrC4=" }, params: { mandrill_events: @events }
+ end
+ end
+ end
+
+ private
+ def switch_key_to(new_key)
+ previous_key, ENV["MANDRILL_INGRESS_API_KEY"] = ENV["MANDRILL_INGRESS_API_KEY"], new_key
+ yield
+ ensure
+ ENV["MANDRILL_INGRESS_API_KEY"] = previous_key
+ end
+end
diff --git a/test/controllers/ingresses/postfix/inbound_emails_controller_test.rb b/test/controllers/ingresses/postfix/inbound_emails_controller_test.rb
new file mode 100644
index 0000000000..5e0777aa30
--- /dev/null
+++ b/test/controllers/ingresses/postfix/inbound_emails_controller_test.rb
@@ -0,0 +1,54 @@
+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/test/controllers/ingresses/sendgrid/inbound_emails_controller_test.rb b/test/controllers/ingresses/sendgrid/inbound_emails_controller_test.rb
new file mode 100644
index 0000000000..0c7d0d6846
--- /dev/null
+++ b/test/controllers/ingresses/sendgrid/inbound_emails_controller_test.rb
@@ -0,0 +1,44 @@
+require "test_helper"
+
+class ActionMailbox::Ingresses::Sendgrid::InboundEmailsControllerTest < ActionDispatch::IntegrationTest
+ setup { ActionMailbox.ingress = :sendgrid }
+
+ test "receiving an inbound email from Sendgrid" do
+ assert_difference -> { ActionMailbox::InboundEmail.count }, +1 do
+ post rails_sendgrid_inbound_emails_url,
+ headers: { authorization: credentials }, params: { email: 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 Sendgrid" do
+ assert_no_difference -> { ActionMailbox::InboundEmail.count } do
+ post rails_sendgrid_inbound_emails_url, params: { email: file_fixture("../files/welcome.eml").read }
+ end
+
+ assert_response :unauthorized
+ end
+
+ test "raising when the configured password is nil" do
+ switch_password_to nil do
+ assert_raises ArgumentError do
+ post rails_sendgrid_inbound_emails_url,
+ headers: { authorization: credentials }, params: { email: 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_sendgrid_inbound_emails_url,
+ headers: { authorization: credentials }, params: { email: file_fixture("../files/welcome.eml").read }
+ end
+ end
+ end
+end
diff --git a/test/fixtures/files/text.txt b/test/fixtures/files/text.txt
deleted file mode 100644
index 84c3f1cf21..0000000000
--- a/test/fixtures/files/text.txt
+++ /dev/null
@@ -1 +0,0 @@
-This Is Not An Email!
diff --git a/test/test_helper.rb b/test/test_helper.rb
index 264f9e8482..b4459f3feb 100644
--- a/test/test_helper.rb
+++ b/test/test_helper.rb
@@ -1,5 +1,5 @@
-# Configure Rails Environment
ENV["RAILS_ENV"] = "test"
+ENV["RAILS_INBOUND_EMAIL_PASSWORD"] = "tbsy84uSV1Kt3ZJZELY2TmShPRs91E3yL4tzf96297vBCkDWgL"
require_relative "../test/dummy/config/environment"
ActiveRecord::Migrator.migrations_paths = [File.expand_path("../test/dummy/db/migrate", __dir__)]
@@ -7,14 +7,11 @@ require "rails/test_help"
require "byebug"
-# Filter out Minitest backtrace while allowing backtrace from other libraries
-# to be shown.
Minitest.backtrace_filter = Minitest::BacktraceFilter.new
require "rails/test_unit/reporter"
Rails::TestUnitReporter.executable = 'bin/test'
-# Load fixtures from the engine
if ActiveSupport::TestCase.respond_to?(:fixture_path=)
ActiveSupport::TestCase.fixture_path = File.expand_path("fixtures", __dir__)
ActionDispatch::IntegrationTest.fixture_path = ActiveSupport::TestCase.fixture_path
@@ -28,6 +25,20 @@ class ActiveSupport::TestCase
include ActionMailbox::TestHelper, ActiveJob::TestHelper
end
+class ActionDispatch::IntegrationTest
+ private
+ def credentials
+ ActionController::HttpAuthentication::Basic.encode_credentials "actionmailbox", ENV["RAILS_INBOUND_EMAIL_PASSWORD"]
+ end
+
+ def switch_password_to(new_password)
+ previous_password, ENV["RAILS_INBOUND_EMAIL_PASSWORD"] = ENV["RAILS_INBOUND_EMAIL_PASSWORD"], new_password
+ yield
+ ensure
+ ENV["RAILS_INBOUND_EMAIL_PASSWORD"] = previous_password
+ end
+end
+
if ARGV.include?("-v")
ActiveRecord::Base.logger = Logger.new(STDOUT)
ActiveJob::Base.logger = Logger.new(STDOUT)
diff --git a/test/unit/controller_test.rb b/test/unit/controller_test.rb
deleted file mode 100644
index 508e561244..0000000000
--- a/test/unit/controller_test.rb
+++ /dev/null
@@ -1,27 +0,0 @@
-require_relative '../test_helper'
-
-class ActionMailbox::InboundEmailsControllerTest < ActionDispatch::IntegrationTest
- test "receiving a valid RFC 822 message" do
- assert_difference -> { ActionMailbox::InboundEmail.count }, +1 do
- post_inbound_email "welcome.eml"
- end
-
- assert_response :created
-
- inbound_email = ActionMailbox::InboundEmail.last
- assert_equal file_fixture('../files/welcome.eml').read, inbound_email.raw_email.download
- end
-
- test "rejecting a message of an unsupported type" do
- assert_no_difference -> { ActionMailbox::InboundEmail.count } do
- post rails_inbound_emails_url, params: { message: fixture_file_upload('files/text.txt', 'text/plain') }
- end
-
- assert_response :unsupported_media_type
- end
-
- private
- def post_inbound_email(fixture_name)
- post rails_inbound_emails_url, params: { message: fixture_file_upload("files/#{fixture_name}", 'message/rfc822') }
- end
-end
diff --git a/test/unit/inbound_email/message_id_test.rb b/test/unit/inbound_email/message_id_test.rb
index aa9ce90b4c..c744a5bf99 100644
--- a/test/unit/inbound_email/message_id_test.rb
+++ b/test/unit/inbound_email/message_id_test.rb
@@ -7,9 +7,7 @@ class ActionMailbox::InboundEmail::MessageIdTest < ActiveSupport::TestCase
end
test "message id is generated if its missing" do
- source_without_message_id = "Date: Fri, 28 Sep 2018 11:08:55 -0700\r\nTo: a@example.com\r\nMime-Version: 1.0\r\nContent-Type: text/plain\r\nContent-Transfer-Encoding: 7bit\r\n\r\nHello!"
- inbound_email = create_inbound_email Tempfile.new.tap { |raw_email| raw_email.write source_without_message_id }
-
+ inbound_email = create_inbound_email "Date: Fri, 28 Sep 2018 11:08:55 -0700\r\nTo: a@example.com\r\nMime-Version: 1.0\r\nContent-Type: text/plain\r\nContent-Transfer-Encoding: 7bit\r\n\r\nHello!"
assert_not_nil inbound_email.message_id
end
end