Eu pensei em compartilhar a configuração que criei para usar o AWS SES para e-mails de saída, de rejeição e de entrada. Definitivamente há algumas nuances no serviço SES, e levou bastante tentativa e erro para entender exatamente como ele funciona. Isso é mais um desabafo do que um guia passo a passo. Deve ser desnecessário, mas use por sua conta e risco. E, por favor, sempre leia e entenda qualquer código escrito por outras pessoas que você implementar.
Contexto:
Estou trabalhando para implantar o Discourse na AWS e utilizar todos os serviços que puder para garantir confiabilidade e redundância. Como desenvolvedor, sinto-me mais confortável com a linha de comando e o código, e queria usar automação de IaC. Todo o meu ambiente está sendo implantado com Terraform, mas tentei clicar no console da web e alinhar as coisas da melhor maneira possível. IAM e documentos de política estão fora do escopo disso, mas acredito que indiquei onde as coisas são necessárias.
Executar uma instância Postfix parece exagero para uma única aplicação. Usar uma caixa de correio POP3 é muito anos 90. Então, mergulhei no universo AWS.
Encontrei algumas postagens extremamente úteis que auxiliaram minha busca:
- 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
O contêiner mail-receiver também me ajudou a entender como o Discourse processa mensagens:
- Configure direct-delivery incoming email for self-hosted sites with Mail-Receiver
- Update mail-receiver to the release version
Inicialmente, esperava que o endpoint de webhook da AWS lidasse com mensagens de entrada, mas depois de analisar o código, percebi que não faria isso. Baseei meu código de receptor lambda no excelente exemplo de @dltj. Optei por usar o SNS para entrega de mensagens em vez do S3.
Pré-requisitos
- Conta AWS
- Conhecimento prático de DNS e os tipos de registro relacionados a e-mail
- Um domínio (ou subdomínio) no qual você pode fazer alterações
Observações
- Tudo o que está documentado deve ser criado na mesma região da AWS
- Texto em negrito e itálico como este são seus valores específicos de implementação
- Texto em itálico são nomes de variáveis, valores fixos ou elementos da interface do usuário
Passos
-
Crie uma identidade de domínio do Simple Email Service (SES), seu.dominio, em uma das regiões da AWS que suportam recebimento de e-mail
-
Verifique a identidade do domínio
-
Crie um tópico do Simple Notification Service (SNS), feedback-sns-topic, para notificações de feedback
-
Configure a identidade de domínio seu.dominio
a. Habilite o encaminhamento de feedback de e-mail
b. Configure notificações de feedback de rejeição e reclamação (não de entrega) para usar o tópico feedback-sns-topic do SNS -
Crie uma assinatura no tópico feedback-sns-topic do SNS
a. O protocolo é HTTPS (você ainda não está usando HTTP, está?)
b. Defina o endpoint para https://seu.dominio/webhooks/aws (veja o post VERP)
c. Selecione habilitar entrega de mensagem bruta -
Crie outro tópico do SNS, incoming-sns-topic, para e-mail de entrada
-
Crie um conjunto de regras de recebimento de e-mail do SES, inbound-mail-set, se não houver um ativo existente. Se houver, use-o, pois só pode haver um conjunto de regras ativo
-
Crie uma regra de recebimento no conjunto de regras de recebimento inbound-mail-set
a. Defina a condição do destinatário para seu.dominio
b. Adicione a ação de publicar no tópico SNS incoming-sns-topic, codificando em Base64 -
Crie uma chave de API em sua instância Discourse para o usuário system, concedendo a ação receive email no recurso email
-
Crie um segredo no Secret Manager, email-handler-secret, com as seguintes chaves e seus respectivos valores:
- api_endpoint - https://seu.dominio/admin/email/handle_mail
- api_key - da etapa 9
- api_username - system, a menos que você tenha usado algo diferente na etapa 9
-
Crie uma camada Lambda, lambda-receiver-layer, para o runtime python3.10 contendo as bibliotecas requests e aws-lambda-powertools
-
Crie uma função Lambda, email-receiver-lambda, para o runtime python3.10 com o código do receptor:
# 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)
-
Configure a função Lambda email-receiver-lambda:
a. Adicione a camada lambda-receiver-layer
b. Adicione a camada específica da região para AWS Parameter Store
c. Adicione a variável de ambiente SECRET_NAME com o valor email-handler-secret
d. Se você quiser detalhes adicionais registrados, adicione a variável de ambiente POWERTOOLS_LOGGER_LOG_EVENT com o valor true -
Conceda à função Lambda email-receiver-lambda permissão IAM secretsmanager:GetSecretValue para o segredo email-handler-secret
-
Crie uma assinatura no tópico SNS incoming-sns-topic
a. O protocolo é AWS Lambda
b. Defina o endpoint para o ARN de email-receiver-lambda -
Permissões IAM serão necessárias para a assinatura SNS no tópico incoming-sns-topic para invocar email-receiver-lambda, mas acredito que isso será feito automaticamente ao configurar através do console
Para fins de depuração, ou auto-irritação geral, você pode adicionar uma assinatura de e-mail a qualquer um dos tópicos SNS para monitorar as notificações.
Coloquei isso em algumas sessões, mas acho que é tudo. Posso tentar responder a perguntas gerais conforme o tempo permitir.
