aboutsummaryrefslogtreecommitdiffstats
path: root/app
diff options
context:
space:
mode:
authorGeorge Claghorn <george@basecamp.com>2018-10-06 22:02:08 -0400
committerGeorge Claghorn <george@basecamp.com>2018-10-11 12:51:13 -0400
commit6b7eac5c51cbef4acd1ab7f48884e7b614f71678 (patch)
tree8eb6b0ebee4ad6073f6cb48b3effc60d4262d2f2 /app
parent47445511862a4c9979fb46889011edf585391091 (diff)
downloadrails-6b7eac5c51cbef4acd1ab7f48884e7b614f71678.tar.gz
rails-6b7eac5c51cbef4acd1ab7f48884e7b614f71678.tar.bz2
rails-6b7eac5c51cbef4acd1ab7f48884e7b614f71678.zip
Accept inbound emails from a variety of ingresses
Diffstat (limited to 'app')
-rw-r--r--app/controllers/action_mailbox/base_controller.rb11
-rw-r--r--app/controllers/action_mailbox/inbound_emails_controller.rb17
-rw-r--r--app/controllers/action_mailbox/ingresses/amazon/inbound_emails_controller.rb26
-rw-r--r--app/controllers/action_mailbox/ingresses/mailgun/inbound_emails_controller.rb61
-rw-r--r--app/controllers/action_mailbox/ingresses/postfix/inbound_emails_controller.rb11
-rw-r--r--app/controllers/action_mailbox/ingresses/sendgrid/inbound_emails_controller.rb16
-rw-r--r--app/models/action_mailbox/inbound_email/message_id.rb9
7 files changed, 133 insertions, 18 deletions
diff --git a/app/controllers/action_mailbox/base_controller.rb b/app/controllers/action_mailbox/base_controller.rb
new file mode 100644
index 0000000000..680c6a9615
--- /dev/null
+++ b/app/controllers/action_mailbox/base_controller.rb
@@ -0,0 +1,11 @@
+class ActionMailbox::BaseController < ActionController::Base
+ skip_forgery_protection
+
+ private
+ def authenticate
+ authenticate_or_request_with_http_basic("Action Mailbox") 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..7412928b56
--- /dev/null
+++ b/app/controllers/action_mailbox/ingresses/amazon/inbound_emails_controller.rb
@@ -0,0 +1,26 @@
+class ActionMailbox::Ingresses::Amazon::InboundEmailsController < ActionMailbox::BaseController
+ before_action :ensure_verified
+
+ # TODO: Lazy-load the AWS SDK
+ require "aws-sdk-sns/message_verifier"
+ cattr_accessor :verifier, default: Aws::SNS::MessageVerifier.new
+
+ def create
+ ActionMailbox::InboundEmail.create_and_extract_message_id! raw_email
+ head :no_content
+ end
+
+ private
+ def raw_email
+ StringIO.new params.require(:content)
+ end
+
+
+ def ensure_verified
+ head :unauthorized unless verified?
+ end
+
+ def verified?
+ 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..4d194a3e00
--- /dev/null
+++ b/app/controllers/action_mailbox/ingresses/mailgun/inbound_emails_controller.rb
@@ -0,0 +1,61 @@
+class ActionMailbox::Ingresses::Mailgun::InboundEmailsController < ActionMailbox::BaseController
+ before_action :ensure_authenticated
+
+ def create
+ ActionMailbox::InboundEmail.create_and_extract_message_id! raw_email
+ head :ok
+ end
+
+ private
+ def raw_email
+ StringIO.new params.require("body-mime")
+ end
+
+
+ def ensure_authenticated
+ head :unauthorized unless authenticated?
+ end
+
+ def authenticated?
+ Authenticator.new(authentication_params).authenticated?
+ rescue ArgumentError
+ false
+ end
+
+ def authentication_params
+ params.permit(:timestamp, :token, :signature).to_h.symbolize_keys
+ end
+
+
+ class Authenticator
+ cattr_accessor :key
+
+ attr_reader :timestamp, :token, :signature
+
+ def initialize(timestamp:, token:, signature:)
+ @timestamp, @token, @signature = timestamp, token, signature
+ end
+
+ def authenticated?
+ signed? && recent?
+ end
+
+ private
+ def signed?
+ ActiveSupport::SecurityUtils.secure_compare signature, expected_signature
+ end
+
+ # Allow for 10 minutes of drift between Mailgun time and local server time.
+ def recent?
+ time >= 10.minutes.ago
+ end
+
+ def expected_signature
+ OpenSSL::HMAC.hexdigest OpenSSL::Digest::SHA256.new, key, "#{timestamp}#{token}"
+ end
+
+ def time
+ Time.at Integer(timestamp)
+ 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..2631302606
--- /dev/null
+++ b/app/controllers/action_mailbox/ingresses/postfix/inbound_emails_controller.rb
@@ -0,0 +1,11 @@
+class ActionMailbox::Ingresses::Postfix::InboundEmailsController < ActionMailbox::BaseController
+ cattr_accessor :username, default: "actionmailbox"
+ cattr_accessor :password
+
+ before_action :authenticate
+
+ def create
+ ActionMailbox::InboundEmail.create_and_extract_message_id! params.require(:message)
+ head :no_content
+ 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..0b9e2e1866
--- /dev/null
+++ b/app/controllers/action_mailbox/ingresses/sendgrid/inbound_emails_controller.rb
@@ -0,0 +1,16 @@
+class ActionMailbox::Ingresses::Sendgrid::InboundEmailsController < ActionMailbox::BaseController
+ cattr_accessor :username, default: "actionmailbox"
+ cattr_accessor :password
+
+ before_action :authenticate
+
+ def create
+ ActionMailbox::InboundEmail.create_and_extract_message_id! raw_email
+ head :no_content
+ end
+
+ private
+ def raw_email
+ StringIO.new 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 590dbfc4d7..601c5f1a7e 100644
--- a/app/models/action_mailbox/inbound_email/message_id.rb
+++ b/app/models/action_mailbox/inbound_email/message_id.rb
@@ -7,7 +7,14 @@ module ActionMailbox::InboundEmail::MessageId
module ClassMethods
def create_and_extract_message_id!(raw_email, **options)
- create! raw_email: raw_email, message_id: extract_message_id(raw_email), **options
+ create! message_id: extract_message_id(raw_email), **options do |inbound_email|
+ case raw_email
+ when ActionDispatch::Http::UploadedFile
+ inbound_email.raw_email.attach raw_email
+ else
+ inbound_email.raw_email.attach io: raw_email.tap(&:rewind), filename: "message.eml", content_type: "message/rfc822"
+ end
+ end
end
private