Pensé que sería útil compartir la configuración que ideé para usar AWS SES para el correo saliente, de rebote y entrante. El servicio SES tiene matices importantes, y me llevó bastante prueba y error entender exactamente cómo funciona. Esto es más un volcado de ideas que una guía paso a paso. Debería ser innecesario, pero úsalo bajo tu propio riesgo. Y, por supuesto, siempre lee y comprende cualquier código escrito por otros que vayas a implementar.
Antecedentes:
Estoy trabajando en desplegar Discourse en AWS y aprovechar todos sus servicios posibles para garantizar fiabilidad y redundancia. Como desarrollador, me siento más cómodo con la línea de comandos y el código, y quise usar automatización de IaC. Todo mi entorno se está desplegando con Terraform, pero he intentado navegar por la consola web y alinear las cosas lo mejor posible. IAM y los documentos de política están fuera del alcance de esto, pero creo que he señalado dónde se necesitan.
Ejecutar una instancia de Postfix parece un exceso para una sola aplicación. Usar un buzón POP3 es algo muy de los 90. Así que me metí en el conejo de AWS.
Encontré algunas publicaciones extremadamente útiles que me ayudaron en mi búsqueda:
- 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
El contenedor mail-receiver también me ayudó a entender cómo Discourse digiere los mensajes:
- Configure direct-delivery incoming email for self-hosted sites with Mail-Receiver
- Update mail-receiver to the release version
Inicialmente esperaba que el punto de final de webhook de AWS manejara los mensajes entrantes, pero después de revisar el código me di cuenta de que no lo haría. Basé mi código de receptor lambda en el excelente ejemplo de @dltj. Opté por usar SNS para la entrega de mensajes en lugar de S3.
Prerrequisitos
- Cuenta de AWS
- Conocimientos prácticos de DNS y los tipos de registros relacionados con el correo electrónico
- Un dominio (o subdominio) en el que puedas realizar cambios
Notas
- Todo lo documentado debe crearse en la misma región de AWS
- El texto en **negrita y cursiva como este son valores específicos de tu implementación
- El texto en cursiva son nombres de variables, valores fijos o elementos de la interfaz de usuario
Pasos
-
Crea una identidad de dominio de Simple Email Service (SES), tu.dominio, en una de las regiones de AWS que admitan la recepción de correo
-
Verifica la identidad del dominio
-
Crea un tema de Simple Notification Service (SNS), feedback-sns-topic, para notificaciones de retroalimentación
a. Agrega el ARN del tema feedback-sns-topic a tu configuraciónaws_sns_topic_arn_allowlist. -
Configura la identidad del dominio tu.dominio
a. Habilita el reenvío de retroalimentación de correo
b. Configura las notificaciones de retroalimentación de rebote y queja (no de entrega) para usar el tema SNS feedback-sns-topic -
Crea una suscripción en el tema SNS feedback-sns-topic
a. El protocolo es HTTPS (¿no sigues usando HTTP, verdad?)
b. Establece el punto de final en https://tu.dominio/webhooks/aws (ver publicación sobre VERP)
c. Asegúrate de que la entrega de mensaje en bruto esté deshabilitada -
Crea otro tema SNS, incoming-sns-topic, para el correo entrante
-
Crea un conjunto de reglas de recepción de correo de SES, inbound-mail-set, si no existe uno activo. Si existe, úsalo, ya que solo puede haber un conjunto de reglas activo
-
Crea una regla de recepción en el conjunto de reglas de recepción inbound-mail-set
a. Establece la condición del destinatario en tu.dominio
b. Agrega una acción para publicar en el tema SNS incoming-sns-topic, codificación Base64 -
Crea una clave API en tu instancia de Discourse para el usuario system, otorgando la acción receive email en el recurso email
-
Crea un secreto en Secret Manager, email-handler-secret, con las siguientes claves y sus respectivos valores:
- api_endpoint - https://tu.dominio/admin/email/handle_mail
- api_key - desde el paso 9
- api_username - system, a menos que hayas usado algo diferente en el paso 9
-
Crea una capa Lambda, lambda-receiver-layer, para el tiempo de ejecución python3.10 que contenga las bibliotecas requests y aws-lambda-powertools
-
Crea una función lambda, email-receiver-lambda, para el tiempo de ejecución python3.10 con el código del receptor:
# Copyright (c) 2023 Derek J. Lambert
#
# Se concede permiso, de forma gratuita, a cualquier persona que obtenga una copia
# de este software y los archivos de documentación asociados (el "Software"), para tratar
# el Software sin restricción, incluyendo sin limitación los derechos
# de uso, copia, modificación, fusión, publicación, distribución, sublicencia y/o venta
# de copias del Software, y para permitir a las personas a las que se
# facilite el Software hacer lo mismo, sujeto a las siguientes condiciones:
#
# El aviso de copyright anterior y este aviso de permiso se incluirán en todos
# las copias o partes sustanciales del Software.
#
# EL SOFTWARE SE PROPORCIONA "TALES CUALES", SIN GARANTÍA DE NINGÚN TIPO, EXPRESA O
# IMPLÍCITA, INCLUIDAS, ENTRE OTRAS, LAS GARANTÍAS DE COMERCIABILIDAD,
# APTITUD PARA UN PROPÓSITO PARTICULAR Y NO INFRACCIÓN. EN NINGÚN CASO LOS
# AUTORES O TITULARES DEL COPYRIGHT SERÁN RESPONSABLES DE NINGÚN RECLAMO, DAÑO U OTRA
# RESPONSABILIDAD, YA SEA EN UNA ACCIÓN DE CONTRATO, DELITO U OTRO, DERIVADA DE,
# EN CONEXIÓN CON EL SOFTWARE O EL USO U OTROS TRATAMIENTOS DEL
# 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' está obsoleto, pero por si algo está configurado incorrectamente
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)
-
Configura la función lambda email-receiver-lambda:
a. Agrega la capa lambda-receiver-layer
b. Agrega la capa específica de la región para AWS Parameter Store
c. Agrega la variable de entorno SECRET_NAME con el valor email-handler-secret
d. Si deseas registrar detalles adicionales, agrega la variable de entorno POWERTOOLS_LOGGER_LOG_EVENT con el valor true -
Otorga a la función lambda email-receiver-lambda el permiso IAM secretsmanager:GetSecretValue para el secreto email-handler-secret
-
Crea una suscripción en el tema SNS incoming-sns-topic
a. El protocolo es AWS Lambda
b. Establece el punto de final en el ARN de email-receiver-lambda -
Se necesitarán permisos IAM para que la suscripción de SNS en el tema incoming-sns-topic invoque email-receiver-lambda, pero creo que esto se hará automáticamente cuando se configure a través de la consola
Con fines de depuración, o para molestarte a ti mismo en general, puedes agregar una suscripción de correo a cualquiera de los temas SNS para monitorear las notificaciones.
Lo escribí en un par de sesiones, pero creo que es todo. Puedo intentar responder preguntas generales en la medida en que el tiempo lo permita.
