# frozen_string_literal: true # rubocop:disable Layout/LineLength module ActionMailbox # Ingests inbound emails from Amazon SES/SNS and confirms subscriptions. # # Subscription requests must provide the following parameters in a JSON body: # - +Message+: Notification content # - +MessagId+: Notification unique identifier # - +Timestamp+: iso8601 timestamp # - +TopicArn+: Topic identifier # - +Type+: Type of event ("Subscription") # # Inbound email events must provide the following parameters in a JSON body: # - +Message+: Notification content # - +MessagId+: Notification unique identifier # - +Timestamp+: iso8601 timestamp # - +SubscribeURL+: Topic identifier # - +TopicArn+: Topic identifier # - +Type+: Type of event ("SubscriptionConfirmation") # # All requests are authenticated by validating the provided AWS signature. # # Returns: # # - 204 No Content if a request is successfully processed # - 401 Unauthorized if a request does not contain a valid signature # - 404 Not Found if the Amazon ingress has not been configured # - 422 Unprocessable Entity if a request provides invalid parameters # # == Usage # # 1. Tell Action Mailbox to accept emails from Amazon SES: # # # config/environments/production.rb # config.action_mailbox.ingress = :amazon # # 2. Configure which SNS topics will be accepted: # # config.action_mailbox.amazon.subscribed_topics = %w( # arn:aws:sns:eu-west-1:123456789001:example-topic-1 # arn:aws:sns:us-east-1:123456789002:example-topic-2 # ) # # 3. {Configure SES}[https://docs.aws.amazon.com/ses/latest/DeveloperGuide/receiving-email-notifications.html] # to route emails through SNS. # # Configure SNS to send emails to +/rails/action_mailbox/amazon/inbound_emails+. # # If your application is found at https://example.com you would # specify the fully-qualified URL https://example.com/rails/action_mailbox/amazon/inbound_emails. # # rubocop:enable Layout/LineLength module Ingresses module Amazon class InboundEmailsController < ActionMailbox::BaseController before_action :verify_authenticity before_action :validate_topic before_action :confirm_subscription def create head :bad_request unless mail.present? ActionMailbox::InboundEmail.create_and_extract_message_id!(mail) head :no_content end private def verify_authenticity head :bad_request unless notification.present? head :unauthorized unless verified? end def confirm_subscription return unless notification['Type'] == 'SubscriptionConfirmation' return head :ok if confirmation_response_code&.start_with?('2') Rails.logger.error('SNS subscription confirmation request rejected.') head :unprocessable_entity end def validate_topic return if valid_topics.include?(topic) Rails.logger.warn("Ignoring unknown topic: #{topic}") head :unauthorized end def confirmation_response_code @confirmation_response_code ||= begin Net::HTTP.get_response(URI(notification['SubscribeURL'])).code end end def notification @notification ||= JSON.parse(request.body.read) rescue JSON::ParserError => e Rails.logger.warn("Unable to parse SNS notification: #{e}") nil end def verified? verifier.authentic?(@notification.to_json) end def verifier Aws::SNS::MessageVerifier.new end def message @message ||= JSON.parse(notification['Message']) end def mail return nil unless notification['Type'] == 'Notification' return nil unless message['notificationType'] == 'Received' message['content'] end def topic return nil unless notification.present? notification['TopicArn'] end def valid_topics ::Rails.configuration.action_mailbox.amazon.subscribed_topics end end end end end