Я подумал, что стоит поделиться конфигурацией, которую я разработал для использования AWS SES для исходящей, отбойной и входящей почты. У сервиса SES есть определенные нюансы, и мне пришлось немало потрудиться методом проб и ошибок, чтобы точно понять, как он работает. Это скорее дамп мыслей, чем пошаговая инструкция. Вероятно, это будет не нужно, но используйте на свой страх и риск. И, безусловно, всегда читайте и понимайте любой код, написанный другими, который вы планируете внедрять.
Предыстория:
Я работаю над развертыванием Discourse в AWS и стараюсь использовать все возможные их сервисы для обеспечения надежности и избыточности. Как разработчик, я лучше чувствую себя в командной строке и с кодом, и хотел использовать автоматизацию IaC. Вся моя среда развертывается с помощью Terraform, но я также пытался настроить все через веб-консоль, насколько это возможно. IAM и политики выходят за рамки этой статьи, но я указал, где они необходимы.
Запуск экземпляра Postfix кажется избыточным для одного приложения. Использование почтового ящика POP3 — это так по-девяностым. Так что я погрузился в кроличью нору AWS.
Я нашел несколько чрезвычайно полезных публикаций, которые помогли мне в моих поисках:
- AWS SES / AWS Lambda mail receiver endpoint code?
- How to use Amazon SES for sending emails to users?
- Configure VERP to handle bouncing e-mails
Контейнер mail-receiver также помог мне понять, как Discourse обрабатывает сообщения:
- Configure direct-delivery incoming email for self-hosted sites with Mail-Receiver
- Update mail-receiver to the release version
Изначально я ожидал, что веб-хук AWS будет обрабатывать входящие сообщения, но, изучив код, понял, что это не так. Я основал свой код lambda-приемника на отличном примере от @dltj. Я решил использовать SNS для доставки сообщений вместо S3.
Требования
- Аккаунт AWS
- Базовые знания DNS и типов записей, связанных с электронной почтой
- Домен (или поддомен), в котором вы можете вносить изменения
Примечания
- Все описанные ресурсы должны создаваться в одном и том же регионе AWS
- Жирный курсивный текст вот такой — это значения, специфичные для вашей реализации
- Курсивный текст — это имена переменных, фиксированные значения или элементы интерфейса
Шаги
-
Создайте доменную идентичность Simple Email Service (SES), your.domain, в одном из регионов AWS, поддерживающих получение электронной почты
-
Проверьте доменную идентичность
-
Создайте тему Simple Notification Service (SNS), feedback-sns-topic, для уведомлений об обратной связи
a. Добавьте ARN темы feedback-sns-topic в настройкуaws_sns_topic_arn_allowlist. -
Настройте доменную идентичность your.domain
a. Включите пересылку обратной связи по электронной почте
b. Настройте уведомления об обратной связи при отбоях и жалобах (не о доставке) на использование темы SNS feedback-sns-topic -
Создайте подписку на тему SNS feedback-sns-topic
a. Протокол — HTTPS (вы ведь не до сих пор используете HTTP, правда?)
b. Установите конечную точку https://your.domain/webhooks/aws (см. пост о VERP)
c. Убедитесь, что доставка сырых сообщений отключена -
Создайте еще одну тему SNS, incoming-sns-topic, для входящей почты
-
Создайте набор правил получения электронной почты SES, inbound-mail-set, если активного набора правил еще нет. Если есть, используйте его, так как может существовать только один активный набор правил
-
Создайте правило получения в наборе правил inbound-mail-set
a. Установите условие получателя на your.domain
b. Добавьте действие для публикации в тему SNS incoming-sns-topic, с кодированием Base64 -
Создайте API-ключ в вашем экземпляре Discourse для пользователя system, предоставив действие receive email для ресурса email
-
Создайте секрет в Secret Manager, email-handler-secret, со следующими ключами и соответствующими значениями:
- api_endpoint - https://your.domain/admin/email/handle_mail
- api_key - из шага 9
- api_username - system, если вы не использовали другое значение в шаге 9
-
Создайте слой Lambda, lambda-receiver-layer, для среды выполнения python3.10, содержащий библиотеки requests и aws-lambda-powertools
-
Создайте функцию Lambda, email-receiver-lambda, для среды выполнения python3.10 с кодом приемника:
# Copyright (c) 2023 Derek J. Lambert
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
import json
import os
from typing import TypedDict
import requests
from aws_lambda_powertools import Logger
from aws_lambda_powertools.utilities import parameters
from aws_lambda_powertools.utilities.data_classes import event_source
from aws_lambda_powertools.utilities.data_classes.sns_event import SNSEvent, SNSEventRecord
from aws_lambda_powertools.utilities.typing import LambdaContext
class Secret(TypedDict):
api_endpoint: str
api_username: str
api_key: str
service = os.getenv('AWS_LAMBDA_FUNCTION_NAME')
logger = Logger(log_uncaught_exceptions=True, service=service)
try:
SECRET_NAME = os.environ['SECRET_NAME']
except KeyError as e:
raise RuntimeError(f'Missing {e} environment variable')
AWS_EXTENSION_PORT = os.getenv('PARAMETERS_SECRETS_EXTENSION_HTTP_PORT', 2773)
EXTENSION_ENDPOINT = f'http://localhost:{AWS_EXTENSION_PORT}/secretsmanager/get?secretId={SECRET_NAME}'
def get_secret() -> Secret:
return parameters.get_secret(SECRET_NAME, transform='json')
def handle_record(record: SNSEventRecord):
sns = record.sns
sns_message = json.loads(sns.message)
try:
message_type = sns_message['notificationType']
message_mail = sns_message['mail']
message_content = sns_message['content']
message_receipt = sns_message['receipt']
except KeyError as exc:
raise RuntimeError(f'Key {exc} missing from message')
try:
receipt_action = message_receipt['action']
except KeyError as exc:
raise RuntimeError(f'Key {exc} missing from receipt')
try:
action_encoding = receipt_action['encoding']
except KeyError as exc:
raise RuntimeError(f'Key {exc} missing from action')
try:
mail_source = message_mail['source']
mail_destination = ','.join(message_mail['destination'])
except KeyError as exc:
raise RuntimeError(f'Key {exc} missing from mail')
logger.info(f'Processing SNS {message_type} {sns.get_type} record with MessageId {sns.message_id} from {mail_source} to {mail_destination}')
# 'email' is deprecated, but just in case something is configured incorrectly
body_key = 'email_encoded' if action_encoding == 'BASE64' else 'email'
request_body = {
body_key: message_content
}
secret = get_secret()
headers = {
'Api-Username': secret['api_username'],
'Api-Key': secret['api_key'],
}
response = requests.post(url=secret['api_endpoint'], headers=headers, json=request_body)
logger.info(response.text)
response.raise_for_status()
@event_source(data_class=SNSEvent)
@logger.inject_lambda_context
def lambda_handler(event: SNSEvent, context: LambdaContext):
for record in event.records:
handle_record(record)
-
Настройте функцию Lambda email-receiver-lambda:
a. Добавьте слой lambda-receiver-layer
b. Добавьте слой, специфичный для региона, для AWS Parameter Store
c. Добавьте переменную окружения SECRET_NAME со значением email-handler-secret
d. Если вы хотите получать дополнительные детали в логах, добавьте переменную окружения POWERTOOLS_LOGGER_LOG_EVENT со значением true -
Предоставьте функции Lambda email-receiver-lambda разрешение IAM secretsmanager:GetSecretValue для секрета email-handler-secret
-
Создайте подписку на тему SNS incoming-sns-topic
a. Протокол — AWS Lambda
b. Установите конечную точку на ARN email-receiver-lambda -
Для подписки SNS на теме incoming-sns-topic потребуются разрешения IAM для вызова email-receiver-lambda, но я полагаю, что это будет сделано автоматически при настройке через консоль.
В целях отладки или просто для собственного раздражения, вы можете добавить подписку на электронную почту на любую из тем SNS для мониторинга уведомлений.
Я записал это за несколько сессий, но думаю, что это все. Я могу попытаться ответить на общие вопросы, если будет время.
