Je pensais partager la configuration que j’ai élaborée pour utiliser AWS SES pour les e-mails sortants, les rebonds et les e-mails entrants. Le service SES comporte assurément quelques subtilités, et il m’a fallu beaucoup d’essais et d’erreurs pour comprendre exactement comment il fonctionne. Il s’agit davantage d’un déballage de mes notes que d’un guide pas à pas à suivre à la lettre. Ce document ne devrait pas être nécessaire, mais utilisez-le sous votre propre responsabilité. Et, bien sûr, lisez toujours et comprenez tout code écrit par d’autres que vous décidez d’implémenter.
Contexte :
Je travaille au déploiement de Discourse sur AWS et j’utilise autant de leurs services que possible pour garantir la fiabilité et la redondance. En tant que développeur, je suis plus à l’aise avec la ligne de commande et le code, et je souhaitais utiliser l’automatisation IaC. Mon environnement complet est déployé avec Terraform, mais j’ai essayé de naviguer dans la console web et d’aligner les éléments au mieux. IAM et les documents de politique sortent du cadre de cet article, mais je pense avoir indiqué où des éléments sont nécessaires.
Faire tourner une instance Postfix semble être une surcharge pour une seule application. Utiliser une boîte aux lettres POP3, c’est vraiment daté des années 90. Alors, je suis parti explorer le labyrinthe d’AWS.
J’ai trouvé quelques publications extrêmement utiles qui m’ont aidé dans ma quête
- 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
Le conteneur mail-receiver m’a également aidé à comprendre comment Discourse traite les messages
- Configure direct-delivery incoming email for self-hosted sites with Mail-Receiver
- Update mail-receiver to the release version
Au départ, je m’attendais à ce que le point de terminaison de webhook AWS gère les messages entrants, mais après avoir parcouru le code, j’ai réalisé que ce n’était pas le cas. J’ai basé mon code de récepteur lambda sur l’excellent exemple de @dltj. J’ai opté pour l’utilisation de SNS pour la livraison des messages au lieu de S3.
Prérequis
- Compte AWS
- Connaissances opérationnelles du DNS et des types d’enregistrements liés aux e-mails
- Un domaine (ou sous-domaine) sur lequel vous pouvez effectuer des modifications
Notes
- Tout ce qui est documenté doit être créé dans la même région AWS
- Le texte en gras italique comme ceci correspond à vos valeurs spécifiques à votre implémentation
- Le texte en italique correspond aux noms de variables, aux valeurs fixes ou aux éléments de l’interface utilisateur
Étapes
-
Créez une identité de domaine Simple Email Service (SES), votre.domaine, dans l’une des régions AWS prenant en charge la réception d’e-mails
-
Vérifiez l’identité du domaine
-
Créez un sujet Simple Notification Service (SNS), feedback-sns-topic, pour les notifications de retour d’information
a. Ajoutez l’ARN du sujet feedback-sns-topic à votre paramètreaws_sns_topic_arn_allowlist. -
Configurez l’identité du domaine votre.domaine
a. Activez la redirection des retours d’information sur les e-mails
b. Configurez les notifications de retour d’information pour les rebonds et les plaintes (et non la livraison) pour utiliser le sujet SNS feedback-sns-topic -
Créez un abonnement sur le sujet SNS feedback-sns-topic
a. Le protocole est HTTPS (vous n’utilisez plus HTTP, j’espère ?)
b. Définissez le point de terminaison sur https://votre.domaine/webhooks/aws (voir la publication sur VERP)
c. Assurez-vous que la livraison de message brut est désactivée -
Créez un autre sujet SNS, incoming-sns-topic, pour les e-mails entrants
-
Créez un ensemble de règles de réception d’e-mails SES, inbound-mail-set, s’il n’existe pas déjà un ensemble actif. Sinon, utilisez celui-ci, car il ne peut y en avoir qu’un seul actif
-
Créez une règle de réception dans l’ensemble de règles de réception inbound-mail-set
a. Définissez la condition du destinataire sur votre.domaine
b. Ajoutez une action pour publier sur le sujet SNS incoming-sns-topic, avec l’encodage Base64 -
Créez une clé API dans votre instance Discourse pour l’utilisateur system, en accordant l’action receive email sur la ressource email
-
Créez un secret dans Secret Manager, email-handler-secret, avec les clés suivantes et leurs valeurs respectives :
- api_endpoint - https://votre.domaine/admin/email/handle_mail
- api_key - depuis l’étape 9
- api_username - system, sauf si vous avez utilisé autre chose à l’étape 9
-
Créez une couche Lambda, lambda-receiver-layer, pour l’environnement d’exécution python3.10 contenant les bibliothèques requests et aws-lambda-powertools
-
Créez une fonction lambda, email-receiver-lambda, pour l’environnement d’exécution python3.10 avec le code du récepteur :
# 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)
-
Configurez la fonction lambda email-receiver-lambda :
a. Ajoutez la couche lambda-receiver-layer
b. Ajoutez la couche spécifique à la région pour AWS Parameter Store
c. Ajoutez la variable d’environnement SECRET_NAME avec la valeur email-handler-secret
d. Si vous souhaitez que des détails supplémentaires soient journalisés, ajoutez la variable d’environnement POWERTOOLS_LOGGER_LOG_EVENT avec la valeur true -
Accordez à la fonction lambda email-receiver-lambda la permission IAM secretsmanager:GetSecretValue pour le secret email-handler-secret
-
Créez un abonnement sur le sujet SNS incoming-sns-topic
a. Le protocole est AWS Lambda
b. Définissez le point de terminaison sur l’ARN de email-receiver-lambda -
Des permissions IAM seront nécessaires pour que l’abonnement SNS sur le sujet incoming-sns-topic puisse invoquer email-receiver-lambda, mais je pense que cela sera fait automatiquement lorsqu’il est configuré via la console
À des fins de débogage, ou pour vous agacer un peu, vous pouvez ajouter un abonnement par e-mail à l’un des sujets SNS pour surveiller les notifications.
J’ai rédigé cela en quelques sessions, mais je pense que c’est complet. Je peux essayer de répondre aux questions générales dans la mesure du temps disponible.
