Comparto la configuración que ideé para usar AWS SES para correos electrónicos salientes, de rebote y entrantes. Definitivamente hay matices en el servicio SES, y requirió bastante prueba y error para entender exactamente cómo funciona. Esto es más una descarga de ideas que una guía paso a paso. Debería ser innecesario, pero úselo bajo su propio riesgo. Y, por supuesto, siempre lea y comprenda cualquier código escrito por otros que implemente.
Antecedentes:
Estoy trabajando para implementar Discourse en AWS y utilizar todos los servicios que pueda para garantizar la confiabilidad y la redundancia. Como desarrollador, me siento más cómodo con la línea de comandos y el código, y quería usar la automatización de IaC. Todo mi entorno se está implementando con Terraform, pero he intentado hacer clic en la consola web y alinear las cosas lo mejor que he podido. IAM y los documentos de políticas están más allá del alcance de esto, pero creo que he señalado dónde se necesitan las cosas.
Ejecutar una instancia de Postfix parece excesivo para una sola aplicación. Usar un buzón POP3 es muy de los 90. Así que me sumergí en el mundo 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 procesa 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 final del 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
- Conocimiento práctico de DNS y los tipos de registros relacionados con el correo electrónico
- Un dominio (o subdominio) en el que pueda realizar cambios
Notas
- Todo lo documentado debe crearse en la misma región de AWS
- El texto en negrita e cursiva como este son sus valores específicos de implementación
- El texto en cursiva son nombres de variables, valores fijos o elementos de la interfaz de usuario
Pasos
-
Cree una identidad de dominio de Simple Email Service (SES), su.dominio, en una de las regiones de AWS que admiten la recepción de correo electrónico
-
Verifique la identidad del dominio
-
Cree un tema de Simple Notification Service (SNS), feedback-sns-topic, para notificaciones de comentarios
-
Configure la identidad de dominio su.dominio
a. Habilite el reenvío de comentarios por correo electrónico
b. Configure las notificaciones de comentarios de rebote y quejas (no de entrega) para usar el tema feedback-sns-topic de SNS -
Cree una suscripción en el tema feedback-sns-topic de SNS
a. El protocolo es HTTPS (¿todavía no está usando HTTP?)
b. Establezca el punto final en https://su.dominio/webhooks/aws (consulte la publicación de VERP)
c. Seleccione habilitar la entrega de mensajes sin procesar -
Cree otro tema de SNS, incoming-sns-topic, para el correo electrónico entrante
-
Cree un conjunto de reglas de recepción de correo electrónico de SES, inbound-mail-set, si no existe uno activo. Si es así, úselo, ya que solo puede haber un conjunto de reglas activo
-
Cree una regla de recepción en el conjunto de reglas de recepción inbound-mail-set
a. Establezca la condición del destinatario en su.dominio
b. Agregue una acción para publicar en el tema de SNS incoming-sns-topic, codificando Base64 -
Cree una clave API en su instancia de Discourse para el usuario system, otorgando la acción receive email en el recurso email
-
Cree un secreto en Secret Manager, email-handler-secret, con las siguientes claves y sus respectivos valores:
- api_endpoint - https://su.dominio/admin/email/handle_mail
- api_key - del paso 9
- api_username - system, a menos que haya usado algo diferente en el paso 9
-
Cree una capa Lambda, lambda-receiver-layer, para el tiempo de ejecución python3.10 que contenga las bibliotecas requests y aws-lambda-powertools
-
Cree 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
#
# 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 la función Lambda email-receiver-lambda:
a. Agregue la capa lambda-receiver-layer
b. Agregue la capa específica de la región para AWS Parameter Store
c. Agregue la variable de entorno SECRET_NAME con el valor email-handler-secret
d. Si desea más detalles registrados, agregue la variable de entorno POWERTOOLS_LOGGER_LOG_EVENT con el valor true -
Otorgue a la función Lambda email-receiver-lambda el permiso IAM secretsmanager:GetSecretValue para el secreto email-handler-secret
-
Cree una suscripción en el tema SNS incoming-sns-topic
a. El protocolo es AWS Lambda
b. Establezca el punto final en el ARN de email-receiver-lambda -
Se necesitarán permisos IAM para la suscripción de SNS en el tema incoming-sns-topic para invocar email-receiver-lambda, pero creo que esto se hará automáticamente cuando se configure a través de la consola
Para fines de depuración, o autoinfligirse molestias en general, puede agregar una suscripción por correo electrónico a cualquiera de los temas de SNS para monitorear las notificaciones.
Lo escribí en un par de sesiones, pero creo que está todo. Puedo intentar responder preguntas generales según el tiempo lo permita.
