J’ai pensé partager la configuration que j’ai mise au point pour utiliser AWS SES pour les e-mails sortants, les rebonds et les e-mails entrants. Il y a certainement des nuances dans le service SES, et il a fallu beaucoup d’essais et d’erreurs pour comprendre exactement comment il fonctionne. Ceci est plus un déballage de cerveau qu’un guide étape par étape. Cela devrait être inutile, mais utilisez à vos propres risques. Et par tous les moyens, lisez toujours et comprenez tout code écrit par d’autres que vous implémentez.
Contexte :
Je travaille au déploiement de Discourse dans AWS et à l’utilisation de tous les services possibles pour assurer 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 voulais utiliser l’IaC automation. Mon environnement entier est déployé avec Terraform, mais j’ai essayé de cliquer à travers la console web et d’aligner les choses du mieux que j’ai pu. IAM et les documents de politique sont au-delà de la portée de ceci, mais je crois que j’ai indiqué où les choses sont nécessaires.
Faire fonctionner une instance Postfix semble excessif pour une seule application. Utiliser une boîte aux lettres POP3 est tellement des années 90. Je me suis donc plongé dans le terrier de lapin AWS.
J’ai trouvé quelques articles extrêmement utiles qui ont aidé 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
Initialement, je m’attendais à ce que le point de terminaison webhook AWS gère les messages entrants, mais après avoir parcouru le code, j’ai réalisé que ce ne serait pas le cas. J’ai basé mon code de récepteur lambda sur l’excellent exemple de @dltj. J’ai choisi d’utiliser SNS pour la livraison des messages au lieu de S3.
Prérequis
- Compte AWS
- Connaissance pratique du DNS et des types d’enregistrements liés aux e-mails
- Un domaine (ou sous-domaine) dans lequel vous pouvez apporter des modifications
Notes
- Tout ce qui est documenté doit être créé dans la même région AWS
- Le texte en gras et italique comme ceci sont vos valeurs spécifiques à l’implémentation
- Le texte en italique sont des noms de variables, des valeurs fixes ou des éléments de l’interface utilisateur
Étapes
-
Créez une identité de domaine Simple Email Service (SES), your.domain, 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
-
Configurez l’identité de domaine your.domain
a. Activez la redirection des retours d’information par e-mail
b. Configurez les notifications de retour d’information sur les rebonds et les plaintes (pas les livraisons) 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 toujours pas HTTP, n’est-ce pas ?)
b. Définissez le point de terminaison sur https://your.domain/webhooks/aws (voir le post VERP)
c. Sélectionnez l’activation de la livraison de messages bruts -
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’en existe pas d’actif. Si c’est le cas, utilisez celui-ci car il ne peut y avoir qu’un seul ensemble de règles 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 your.domain
b. Ajoutez une action pour publier sur le sujet SNS incoming-sns-topic, en encodant 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://your.domain/admin/email/handle_mail
- api_key - de l’étape 9
- api_username - system, sauf si vous avez utilisé quelque chose de différent à l’étape 9
-
Créez une couche Lambda, lambda-receiver-layer, pour le runtime python3.10 contenant les bibliothèques requests et aws-lambda-powertools
-
Créez une fonction Lambda, email-receiver-lambda, pour le runtime 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 des détails supplémentaires enregistrés, ajoutez la variable d’environnement POWERTOOLS_LOGGER_LOG_EVENT avec la valeur true -
Accordez à la fonction Lambda email-receiver-lambda l’autorisation 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 autorisations 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 lors de la configuration via la console
À des fins de débogage, ou pour votre propre agacement général, vous pouvez ajouter un abonnement par e-mail à l’un ou l’autre des sujets SNS pour surveiller les notifications.
J’ai mis cela par écrit en quelques sessions, mais je pense que c’est tout. Je peux essayer de répondre aux questions générales dans la mesure du temps le permet.
