diff options
Diffstat (limited to 'app')
8 files changed, 214 insertions, 22 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..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 |