diff options
author | George Claghorn <george@basecamp.com> | 2018-10-06 22:02:08 -0400 |
---|---|---|
committer | George Claghorn <george@basecamp.com> | 2018-10-11 12:51:13 -0400 |
commit | 6b7eac5c51cbef4acd1ab7f48884e7b614f71678 (patch) | |
tree | 8eb6b0ebee4ad6073f6cb48b3effc60d4262d2f2 /app/controllers/action_mailbox | |
parent | 47445511862a4c9979fb46889011edf585391091 (diff) | |
download | rails-6b7eac5c51cbef4acd1ab7f48884e7b614f71678.tar.gz rails-6b7eac5c51cbef4acd1ab7f48884e7b614f71678.tar.bz2 rails-6b7eac5c51cbef4acd1ab7f48884e7b614f71678.zip |
Accept inbound emails from a variety of ingresses
Diffstat (limited to 'app/controllers/action_mailbox')
6 files changed, 125 insertions, 17 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 |