Pensei em compartilhar a configuração que elaborei para usar o AWS SES para e-mails de saída, devoluções (bounce) e e-mails recebidos. O serviço SES tem certamente algumas nuances, e foi preciso bastante tentativa e erro para entender exatamente como ele funciona. Isso é mais um despejo de ideias do que um guia passo a passo. Deveria ser desnecessário, mas use por sua conta e risco. E, de qualquer forma, sempre leia e entenda qualquer código escrito por outros que você implementar.
Contexto:
Estou trabalhando para implantar o Discourse na AWS e utilizar todos os seus serviços possíveis para garantir confiabilidade e redundância. Como desenvolvedor, me sinto mais à vontade com a linha de comando e código, e queria usar automação de IaC. Meu ambiente inteiro está sendo implantado com Terraform, mas tentei navegar pela console web e alinhar as coisas da melhor forma possível. IAM e documentos de política estão fora do escopo deste post, mas acredito que indiquei onde são necessários.
Executar uma instância do Postfix parece exagero para uma única aplicação. Usar uma caixa de correio POP3 é coisa dos anos 90. Então, fui fundo no coelho da AWS.
Encontrei alguns posts 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 as mensagens
- Configure direct-delivery incoming email for self-hosted sites with Mail-Receiver
- Update mail-receiver to the release version
Inicialmente, eu esperava que o endpoint de webhook da AWS lidasse com as mensagens recebidas, mas, após analisar o código, percebi que não o faria. Baseei meu código de receptor Lambda no excelente exemplo de @dltj. Optei por usar o SNS para a entrega de mensagens em vez do S3.
Pré-requisitos
- Conta AWS
- Conhecimento prático de DNS e dos tipos de registros relacionados a e-mail
- Um domínio (ou subdomínio) no qual você possa fazer alterações
Notas
- Tudo o que está documentado deve ser criado na mesma região AWS
- O texto em **negrito e itálico como este são valores específicos da sua implementação
- Texto em itálico são nomes de variáveis, valores fixos ou elementos de interface
Etapas
-
Crie uma identidade de domínio do Simple Email Service (SES), your.domain, em uma das regiões AWS que suportam recebimento de e-mails
-
Verifique a identidade do domínio
-
Crie um tópico do Simple Notification Service (SNS), feedback-sns-topic, para notificações de feedback
a. Adicione o ARN do tópico feedback-sns-topic à sua configuraçãoaws_sns_topic_arn_allowlist. -
Configure a identidade do domínio your.domain
a. Ative o encaminhamento de feedback de e-mail
b. Configure as notificações de feedback de bounce e reclamação (não entrega) para usar o tópico SNS feedback-sns-topic -
Crie uma assinatura no tópico SNS feedback-sns-topic
a. O protocolo é HTTPS (você não está usando HTTP, está?)
b. Defina o endpoint como https://your.domain/webhooks/aws (veja o post sobre VERP)
c. Certifique-se de que a entrega de mensagem bruta esteja desativada -
Crie outro tópico SNS, incoming-sns-topic, para e-mails recebidos
-
Crie um conjunto de regras de recebimento de e-mail do SES, inbound-mail-set, se não houver um conjunto 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 de destinatário como your.domain
b. Adicione uma ação para publicar no tópico SNS incoming-sns-topic, com codificação Base64 -
Crie uma chave de API na sua instância do 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://your.domain/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
#
# A permissão é hereby concedida, gratuitamente, a qualquer pessoa que obtenha uma cópia
# deste software e dos arquivos de documentação associados (o "Software"), para lidar
# com o Software sem restrição, incluindo, sem limitação, os direitos
# de usar, copiar, modificar, mesclar, publicar, distribuir, sublicenciar e/ou vender
# cópias do Software, e para permitir às pessoas a quem o Software é
# fornecido para fazer o mesmo, sujeito às seguintes condições:
#
# A notificação de copyright acima e esta notificação de permissão devem ser incluídas em todas
# as cópias ou porções substanciais do Software.
#
# O SOFTWARE É FORNECIDO "COMO ESTÁ", SEM GARANTIA DE QUALQUER TIPO, EXPRESSA OU
# IMPLÍCITA, INCLUINDO, MAS NÃO SE LIMITANDO A, AS GARANTIAS DE COMERCIALIZAÇÃO,
# ADEQUAÇÃO A UM FIM ESPECÍFICO E NÃO VIOLAÇÃO. EM NENHUM CASO OS
# AUTORES OU TITULARES DE DIREITOS AUTORAIS SERÃO RESPONSÁVEIS POR QUALQUER REIVINDICAÇÃO, DANOS OU OUTRA
# RESPONSABILIDADE, SEJA EM UMA AÇÃO DE CONTRATO, TORT OU OUTRA, DECORRENTE DE,
# EM CONEXÃO COM O SOFTWARE OU O USO OU OUTRAS NEGOCIAÇÕES NO
# 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'Variável de ambiente {e} ausente')
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'Chave {exc} ausente na mensagem')
try:
receipt_action = message_receipt['action']
except KeyError as exc:
raise RuntimeError(f'Chave {exc} ausente no recibo')
try:
action_encoding = receipt_action['encoding']
except KeyError as exc:
raise RuntimeError(f'Chave {exc} ausente na ação')
try:
mail_source = message_mail['source']
mail_destination = ','.join(message_mail['destination'])
except KeyError as exc:
raise RuntimeError(f'Chave {exc} ausente no e-mail')
logger.info(f'Processando registro SNS {message_type} {sns.get_type} com MessageId {sns.message_id} de {mail_source} para {mail_destination}')
# 'email' está obsoleto, mas apenas por precaução caso algo esteja configurado incorretamente
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 o AWS Parameter Store
c. Adicione a variável de ambiente SECRET_NAME com o valor email-handler-secret
d. Se você quiser mais detalhes registrados, adicione a variável de ambiente POWERTOOLS_LOGGER_LOG_EVENT com o valor true -
Conceda à função Lambda email-receiver-lambda a 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 como o ARN de email-receiver-lambda -
Serão necessárias permissões IAM para que a assinatura do SNS no tópico incoming-sns-topic possa invocar email-receiver-lambda, mas acredito que isso será feito automaticamente quando configurado através da console
Para fins de depuração ou para auto-incomodação geral, você pode adicionar uma assinatura de e-mail a qualquer um dos tópicos SNS para monitorar as notificações.
Escrevi isso em algumas sessões, mas acho que está tudo. Posso tentar responder a perguntas gerais conforme o tempo permitir.
