Код конечной точки для получения почты AWS SES / AWS Lambda?

Я думаю, что лучший способ реализовать mail-receiver (поскольку я уже использую AWS SES) — это использовать API через функцию Lambda для вызова конечной точки API /admin/email/handle_mail.

В интернете есть руководства о том, как принимать почту в SES и передавать её через S3 в функцию Lambda. Также существуют готовые функции на Node.js, которые забирают это письмо и пересылают его на почтовый сервер.

Однако вместо этого кажется более правильным просто вызывать API напрямую из функции Lambda, передавая письмо. (Я не поддерживаю собственный локальный почтовый сервер и в любом случае не хочу использовать опросы).

Поэтому мне могла бы помочь более подробная документация (насколько мне известно, страниц документации нет) о вызове этой конечной точки и о том, что именно отправлять (это REST-конечная точка? Есть ли альтернатива через WebSocket? Какой формат данных нужно отправить?) или какой-то код для использования. Нужно ли мне всё ещё запускать этот дополнительный контейнер?
Configure direct-delivery incoming email for self-hosted sites with Mail-Receiver (этот пост уже 6 лет)

Кто-то, похоже, реализовал это на Python Update mail-receiver to the release version - #7 by dltj, но там нет деталей, а мне нужно сделать это на Node.js/JavaScript.

У кого-то есть рабочая JavaScript-функция, которой можно поделиться? Спасибо.

@dltj здесь. Я не могу помочь с Node/JavaScript, но могу предложить псевдокод в виде Python-скрипта, который я использую для реализации этой задачи:

""" AWS Lambda для получения сообщения от SES и публикации его в 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):
    """Логирование атрибутов объекта контекста, полученного от вызывающего Lambda"""
    LOGGER.info(
        "Оставшееся время (мс): %s. Поток логов: %s. Группа логов: %s. Лимит памяти (МБ): %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):
    """Обработка события доставки SES"""
    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(
            "Обработка SES с mID %s (%s) от %s",
            mailMessageId,
            sesMessageId,
            mailSender,
        )

        deliveryRecipients = mail["destination"]
        for recipientEmailAddress in deliveryRecipients:
            LOGGER.info("Получатель доставки: %s", recipientEmailAddress)

            s3resource = boto3.resource("s3")
            bucket = s3resource.Bucket(EMAIL_S3_BUCKET)
            obj = bucket.Object(sesMessageId)
            LOGGER.info("Получение %s/%s", EMAIL_S3_BUCKET, sesMessageId)
            post_data = {"email": obj.get()["Body"].read()}

            LOGGER.info("Отправка на %s", DISCOURSE_SITE)
            r = requests.post(
                DISCOURSE_URL, headers=DISCOURSE_API_HEADERS, data=post_data
            )
            if r.status_code == 200:
                LOGGER.info("Письмо принято Discourse: %s", DISCOURSE_SITE)
                obj.delete()
                processed = True
            else:
                LOGGER.error(
                    "Письмо отклонено %s (%s): %s",
                    DISCOURSE_SITE,
                    r.status_code,
                    r.content,
                )

    return processed

Я использую Serverless для управления развёртыванием и поддержкой Lambda. Если вы хотите пойти по похожему пути, вы можете начать с этого файла serverless.yml:

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:
    # - что-то
  exclude:
    - node_modules/**
    - .venv/**
    - __pycache__
    - secrets.yml

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

# Шаблоны ресурсов CloudFormation
resources:
  Resources:
    EmailDispatcherLogGroup:
      Type: AWS::Logs::LogGroup
      Properties:
        RetentionInDays: "30"

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

Спасибо, хорошее начало, даже если это не JS.

  1. Как создать этот (зашифрованный) API-ключ? Нужно ли его шифровать?
  2. Нужно ли мне включить опцию «Включено ручное опрос» (manual polling enabled) для отправки писем через API в ответ на электронные письма, чтобы активировать API-эндпоинт, или он активен по умолчанию?
  3. На что следует обратить особое внимание в плане разрешений AWS или правил SES?

Ключ не нужно шифровать. Код инфраструктуры для нашего проекта находится в общем пространстве, поэтому я не хотел прописывать ключ непосредственно в исходный код.

На моём сайте включена функция «вручную опрашивать», хотя я точно не помню, что именно она делает.

Ничего необычного в правах доступа к AWS, кроме отправки и чтения/записи во временный бакет, я не припоминаю.

Удачи!

Есть ли где-нибудь в веб-интерфейсе место для генерации API-ключа? Не могу найти такого в настройках. Может быть, вы просто задаете его как переменную окружения при сборке? В любом случае, я не знаю, как сгенерировать API-ключ.

Ключ API Discourse? Он генерируется в административном интерфейсе по адресу https://{discourse_url}/admin/api/keys

Да, я видел это, но не подумал, что это правильно, потому что у “granular” не было разрешения handle_email, и я не был уверен, какого пользователя выбрать или всех сразу.

Поэтому я сгенерировал ключ для пользователя “system” с глобальными правами. Так ли вы поступили?

Ну, я хотел использовать своё приложение “postman”, чтобы изучить/попробовать его, но без документации я не знаю, что отправлять через POST, в каких форматах и как передавать ключ. Если бы я лучше знал Python, возможно, смог бы разобраться, посмотрев ваш код.

Я понимаю, что это проект с открытым исходным кодом, поэтому понятно, что нет реальной документации по использованию API handle_mail — только помощь от таких людей, как вы… спасибо!

Это не является недокументированным из-за того, что проект с открытым исходным кодом; это недокументировано, потому что никто (или очень немногие) не спрашивали.

Предлагается отправлять закодированное письмо методом POST в параметре email_encoded. Файл routes.rb показывает, что это POST-запрос к маршруту https://discourse.example.com/admin/email/handle_mail. Я взял это из примера шаблона получателя почты (и рекомендую вам просто использовать его, так как в таком случае вы уже были бы завершены), но эта информация также есть в discourse/config/routes.rb at main · discourse/discourse · GitHub (что не сразу понятно, если вы не разбираетесь в Rails).

«Шаблон образца получателя почты»
Где он находится? В репозитории? GitHub - discourse/discourse: A platform for community discussion. Free, open, simple. · GitHub?

Мне удалось довольно хорошо разобраться в этом, изучив код на Python.

Успех после нескольких дней усилий!

@dltj Мне удалось переписать ваш код на Node.js с использованием aws-sdk и получить модули. Большое спасибо.

На пути возникло несколько препятствий: от AWS (SES, IAM, S3, Lambda), настройки serverless, правильного формирования API-запросов до корректной конфигурации параметров Discourse.

Я планирую написать подробную тему о том, что именно я сделал, чтобы всё заработало. Описание будет настолько детальным, что любой сможет включить эту функцию без необходимости писать код или ломать голову. Главное преимущество этого метода заключается в том, что при использовании SES вам НЕ нужно настраивать почтовый сервер для домена. Это оставляет вам свободу для создания полностью независимого почтового сервера для вашего домена.

Если вам интересно, как это было сделано, заходите позже — я опубликую ссылку здесь, когда всё будет готово (через несколько дней).

Добавляя к этому, @dltj, мы скоро удалим параметр email из маршрута admin/email/handle_mail. Вам нужно отправлять тело письма в виде строки, закодированной в base64, в параметре email_encoded для этого маршрута.

Кодирование стало одной из проблем, которые мне не удалось решить.

Мне не удалось заставить закодированную электронную почту работать.

  1. @martin, согласно вашему сообщению, email_encoded указано неверно. Я пробовал оба варианта: email_encoded вызывает ошибку, но encoded_email всё ещё выдаёт предупреждение.

body: 'warning: the email parameter is deprecated. all POST requests to this route should be sent with a base64 strict encoded encoded_email parameter instead. email has been received and is queued for processing',

  1. Нужно ли мне конвертировать входящую почту в обычный текст (что я сейчас и делаю), а затем обратно в base64, или, скорее всего, то, что AWS SES сохраняет в бакете, уже закодировано в base64? Если я передаю тело письма так, как его сохранил SES, оно не отображается в журнале полученных писем. Команда POST возвращает ошибку, только предупреждение. В общем, я застрял на этом этапе.

Пожалуйста, отложите удаление ключа email, пока мы не сможем это исправить.

Даже если я декодирую в обычный текст и кодирую обратно в base64, я всё равно получаю предупреждение, и письмо не отображается как полученное :roll_eyes:

      const en_package = {
      headers: DISCOURSE_API_HEADERS,
      json: {encoded_email: Buffer.from(body.toString(),'base64') }
    }
   
   console.log(Buffer.from(body.toString(),'base64'))
   const res = await rest.post(DISCOURSE_URL,en_package)

Ещё раз: если я отправляю обычный текст с “emal”, то же самое сообщение принимается и обрабатывается.

Вы постоянно говорите о “strict” — что это вообще значит? В Node.js/JS есть только два варианта base64: “base64” и “base64url”.

Почему API продолжает выдавать предупреждение, если я использую encoded_email, даже если в кодировке есть ошибка?

Я попробовал curl, и он не принял encoded_email, но email_encoded подошёл… ах, значит, предупреждение неверное!

Итак, я снова попробовал свой код с email_encoded, но на этот раз использовал модуль для кодирования в base64 вместо встроенного — и всё заработало, уф!

Ах! Я теперь вижу это. Спасибо за предупреждение, @martin — я добавлю это в свой список задач.

Ого, это полностью моя вина (я ошибся в предупреждении), сделаю небольшой PR, чтобы исправить это.

Этот PR исправит проблему, его должны объединить позже сегодня DEV: Fix typo for email encoded by martin-brennan · Pull Request #15577 · discourse/discourse · GitHub.

Отлично, спасибо за то, что поделились.
Мы три года использовали устаревшую версию pre-base64 в Lambda, и месяц назад она перестала работать.

Мы не можем использовать SMTP на порту 25, так как хостимся за tunnel через Cloudflare, и зачем возиться с портом 25, если Amazon отлично справляется с блокировкой шума через SES?

В любом случае, я на пределе своих сил.

Код работает с Postman и Python, запущенным на локальном компьютере. Что-то идёт не так при отправке из AWS Lambda с идентичным кодом.

Forbidden
UTF-8

Что ж, проблема решена —
хотя запросы передавались, режим «bot fight mode» НЕ дружит с API Discourse. Возможно, из-за кодировки base64?

Надеюсь, это сэкономит кому-то 72 часа жизни:
Отключите это: