AWS SES / AWS Lambda mail receiver endpoint code?

@dltj here. I can’t help with Node/JavaScript, but I can offer some pseudo-code in the form of the Python that I’m using to make this happen:

""" AWS Lambda to receive message from SES and post it to Discourse"""
import json
import logging
import boto3
import requests
from base64 import b64decode

LOGGER = logging.getLogger(__name__)
LOGGER.setLevel(logging.INFO)

# pylint: disable=C0301
ENCRYPTED_DISCOURSE_KEY = "{cyper-text}"
DISCOURSE_KEY = boto3.client("kms").decrypt(
    CiphertextBlob=b64decode(ENCRYPTED_DISCOURSE_KEY)
)["Plaintext"]
DISCOURSE_SITE = "discuss.folio.org"
EMAIL_S3_BUCKET = "openlibraryfoundation-discourse-mail"

DISCOURSE_URL = f"https://{DISCOURSE_SITE}/admin/email/handle_mail"
DISCOURSE_API_HEADERS = {
    "Api-Key": DISCOURSE_KEY,
    "Api-Username": "system",
}


def log_lambda_context(context):
    """Log the attributes of the context object received from the Lambda invoker
    """
    LOGGER.info(
        "Time remaining (ms): %s.  Log stream: %s.  Log group: %s.  Mem. limits(MB): %s",
        context.get_remaining_time_in_millis(),
        context.log_stream_name,
        context.log_group_name,
        context.memory_limit_in_mb,
    )


def lambda_handler(event, context):
    """ Handle SES delivery event """
    log_lambda_context(context)
    LOGGER.info(json.dumps(event))

    processed = False

    for record in event["Records"]:
        mail = record["ses"]["mail"]
        mailMessageId = mail["commonHeaders"]["messageId"]
        sesMessageId = mail["messageId"]
        mailSender = mail["source"]

        LOGGER.info(
            "Processing an SES with mID %s (%s) from %s",
            mailMessageId,
            sesMessageId,
            mailSender,
        )

        deliveryRecipients = mail["destination"]
        for recipientEmailAddress in deliveryRecipients:
            LOGGER.info("Delivery recipient: %s", recipientEmailAddress)

            s3resource = boto3.resource("s3")
            bucket = s3resource.Bucket(EMAIL_S3_BUCKET)
            obj = bucket.Object(sesMessageId)
            LOGGER.info("Getting %s/%s", EMAIL_S3_BUCKET, sesMessageId)
            post_data = {"email": obj.get()["Body"].read()}

            LOGGER.info("Posting to %s", DISCOURSE_SITE)
            r = requests.post(
                DISCOURSE_URL, headers=DISCOURSE_API_HEADERS, data=post_data
            )
            if r.status_code == 200:
                LOGGER.info("Mail accepted by Discourse: %s", DISCOURSE_SITE)
                obj.delete()
                processed = True
            else:
                LOGGER.error(
                    "Mail rejected by %s (%s): %s",
                    DISCOURSE_SITE,
                    r.status_code,
                    r.content,
                )

    return processed

I’m using Serverless to manage the deployment and upkeep of the Lambda. In case you want to go a similar route, you’re welcome to start with this serverless.yml file:

service: ses-post-to-discourse

frameworkVersion: "=1.36.1"

provider:
  name: aws
  runtime: python3.7
  onError: arn:aws:sns:us-east-1:{AWS_ACCOUNT}:ses-discourse-DLQ
  region: us-east-1
  iamRoleStatements:
  - Effect: 'Allow'
    Action:
      - 'ses:SendBounce'
    Resource:
      - '*'
  - Effect: 'Allow'
    Action:
      - 's3:*'
    Resource:
      - 'arn:aws:s3:::openlibraryfoundation-discourse-mail/*'
  - Effect: 'Allow'
    Action:
      - 'kms:decrypt'
    Resource:
      - 'arn:aws:kms:us-east-1:{AWS_ACCOUNT}:key/{UUID-of-key}'

package:
  include:
    # - something
  exclude:
    - node_modules/**
    - .venv/**
    - __pycache__
    - secrets.yml

functions:
  emailDispatcher:
    handler: post_ses_delivery_lambda.lambda_handler
    events:
      - sns: ses-discourse-inbound

# CloudFormation resource templates
resources:
  Resources:
    EmailDispatcherLogGroup:
      Type: AWS::Logs::LogGroup
      Properties:
        RetentionInDays: "30"

plugins:
  - serverless-python-requirements
custom:
  pythonRequirements:
    pythonBin: .venv/bin/python
    dockerizePip: false