Code de point de terminaison pour la réception d'e-mails AWS SES / AWS Lambda ?

Je pense que la meilleure façon d’implémenter le récepteur d’e-mails (puisque j’utilise déjà AWS SES) est d’utiliser l’API via une fonction Lambda pour appeler le point de terminaison de l’API /admin/email/handle_mail.

Il existe des tutoriels sur la manière d’acheminer les e-mails vers SES et de les transmettre via S3 à une fonction Lambda. Il existe également des fonctions NodeJS pré-cuites pour récupérer cet e-mail et le relayer vers un serveur de messagerie.

Mais plutôt que de faire cela, il semble préférable d’appeler directement l’API depuis une fonction Lambda en lui passant l’e-mail. (Je ne maintiens pas mon propre serveur de messagerie local et je ne veux pas non plus utiliser le polling).

Je pourrais donc bénéficier d’un peu d’aide, soit par une documentation plus détaillée (pas de pages de documentation à ma connaissance) sur l’appel de ce point de terminaison et sur ce qu’il faut envoyer exactement (est-ce un point de terminaison REST ? existe-t-il une alternative WebSocket ? quel est le format de ce qu’il faut envoyer ?) soit par du code à partager. Dois-je toujours exécuter ce conteneur supplémentaire ?
Configure direct-delivery incoming email for self-hosted sites with Mail-Receiver (ce post date maintenant de 6 ans)

Quelqu’un a apparemment fait cela avec Python Update mail-receiver to the release version - #7 by dltj ? mais il n’y a pas de détails et je devrai le faire en node/javascript.

Quelqu’un a-t-il une fonction Javascript fonctionnelle qu’il peut partager ? Merci.

2 « J'aime »

@dltj ici. Je ne peux pas aider avec Node/JavaScript, mais je peux proposer un pseudo-code sous forme de Python que j’utilise pour y parvenir :

"""AWS Lambda pour recevoir un message de SES et le publier sur Discourse"""
import json
import logging
import boto3
import requests
from base64 import b64decode

LOGGER = logging.getLogger(__name__)
LOGGER.setLevel(logging.INFO)

# pylint: disable=C0301
ENCRYPTED_DISCOURSE_KEY = "{cyper-text}"
DISCOURSE_KEY = boto3.client("kms").decrypt(
    CiphertextBlob=b64decode(ENCRYPTED_DISCOURSE_KEY)
)["Plaintext"]
DISCOURSE_SITE = "discuss.folio.org"
EMAIL_S3_BUCKET = "openlibraryfoundation-discourse-mail"

DISCOURSE_URL = f"https://{DISCOURSE_SITE}/admin/email/handle_mail"
DISCOURSE_API_HEADERS = {
    "Api-Key": DISCOURSE_KEY,
    "Api-Username": "system",
}


def log_lambda_context(context):
    """Connecter les attributs de l'objet contexte reçu de l'invocateur Lambda
    """
    LOGGER.info(
        "Temps restant (ms) : %s. Flux de journal : %s. Groupe de journaux : %s. Limites de mémoire (Mo) : %s",
        context.get_remaining_time_in_millis(),
        context.log_stream_name,
        context.log_group_name,
        context.memory_limit_in_mb,
    )


def lambda_handler(event, context):
    """Gérer l'événement de livraison SES
    """
    log_lambda_context(context)
    LOGGER.info(json.dumps(event))

    processed = False

    for record in event["Records"]:
        mail = record["ses"]["mail"]
        mailMessageId = mail["commonHeaders"]["messageId"]
        sesMessageId = mail["messageId"]
        mailSender = mail["source"]

        LOGGER.info(
            "Traitement d'un SES avec mID %s (%s) de %s",
            mailMessageId,
            sesMessageId,
            mailSender,
        )

        deliveryRecipients = mail["destination"]
        for recipientEmailAddress in deliveryRecipients:
            LOGGER.info("Destinataire de livraison : %s", recipientEmailAddress)

            s3resource = boto3.resource("s3")
            bucket = s3resource.Bucket(EMAIL_S3_BUCKET)
            obj = bucket.Object(sesMessageId)
            LOGGER.info("Obtention de %s/%s", EMAIL_S3_BUCKET, sesMessageId)
            post_data = {"email": obj.get()["Body"].read()}

            LOGGER.info("Publication sur %s", DISCOURSE_SITE)
            r = requests.post(
                DISCOURSE_URL, headers=DISCOURSE_API_HEADERS, data=post_data
            )
            if r.status_code == 200:
                LOGGER.info("Mail accepté par Discourse : %s", DISCOURSE_SITE)
                obj.delete()
                processed = True
            else:
                LOGGER.error(
                    "Mail rejeté par %s (%s) : %s",
                    DISCOURSE_SITE,
                    r.status_code,
                    r.content,
                )

    return processed

J’utilise Serverless pour gérer le déploiement et la maintenance de la Lambda. Si vous souhaitez suivre une voie similaire, n’hésitez pas à commencer par ce fichier serverless.yml :

service: ses-post-to-discourse

frameworkVersion: "=1.36.1"

provider:
  name: aws
  runtime: python3.7
  onError: arn:aws:sns:us-east-1:{AWS_ACCOUNT}:ses-discourse-DLQ
  region: us-east-1
  iamRoleStatements:
  - Effect: 'Allow'
    Action:
      - 'ses:SendBounce'
    Resource:
      - '*'
  - Effect: 'Allow'
    Action:
      - 's3:*'
    Resource:
      - 'arn:aws:s3:::openlibraryfoundation-discourse-mail/*'
  - Effect: 'Allow'
    Action:
      - 'kms:decrypt'
    Resource:
      - 'arn:aws:kms:us-east-1:{AWS_ACCOUNT}:key/{UUID-of-key}'

package:
  include:
    # - something
  exclude:
    - node_modules/**
    - .venv/**
    - __pycache__
    - secrets.yml

functions:
  emailDispatcher:
    handler: post_ses_delivery_lambda.lambda_handler
    events:
      - sns: ses-discourse-inbound

# CloudFormation resource templates
resources:
  Resources:
    EmailDispatcherLogGroup:
      Type: AWS::Logs::LogGroup
      Properties:
        RetentionInDays: "30"

plugins:
  - serverless-python-requirements
custom:
  pythonRequirements:
    pythonBin: .venv/bin/python
    dockerizePip: false

Merci, bon début même si ce n’est pas du JS.

  1. Comment créer cette clé API (cryptée) ? Faut-il la crypter ?
  2. Dois-je cocher “manual polling enabled” (Push emails using the API for email replies) pour que le point de terminaison de l’API soit actif ou l’est-il par défaut ?
  3. Y a-t-il des éléments auxquels il faut prêter une attention particulière en termes d’autorisations AWS ou de règles SES ?

La clé n’a pas besoin d’être chiffrée. Le code d’infrastructure de notre projet se trouve dans un espace partagé, je ne voulais donc pas coder en dur la clé dans la source.

J’ai bien “l’interrogation manuelle activée” sur mon site, bien que je ne me souvienne pas spécifiquement de ce que cela fait.

Rien d’inhabituel dont je me souvienne concernant les autorisations AWS au-delà de l’envoi et de la lecture/écriture dans le compartiment temporaire.

Bonne chance !

Y a-t-il un endroit dans l’interface utilisateur web où l’on génère la clé API ? Je ne trouve rien dans les paramètres. Peut-être suffit-il de la définir sur ce que vous voulez comme variable d’environnement lors de la compilation ? Quoi qu’il en soit, je n’ai aucune idée de comment générer une clé API.

La clé API Discourse ? Elle est générée dans l’interface d’administration sur https://{discourse_url}/admin/api/keys

1 « J'aime »

Oui, j’ai vu ça mais je ne pensais pas que c’était correct car « granular » n’avait pas la permission handle_email et j’hésitais à choisir quel utilisateur ou tous les utilisateurs.

J’ai donc généré une clé pour l’utilisateur « system » avec la permission globale. Est-ce ce que vous avez fait ?

Eh bien, je voulais utiliser mon application « postman » pour apprendre/l’essayer, mais sans documentation, je ne sais pas quoi POSTer, dans quel(s) format(s) et comment passer la clé. Si j’étais plus familier avec Python, je pourrais peut-être comprendre cela en regardant votre code.

Je sais que c’est un projet open source, donc il est compréhensible qu’il n’y ait pas de vraie documentation sur l’utilisation de l’API handle_mail, seulement une aide de personnes comme vous… merci !

Ce n’est pas non documenté parce que c’est open source, c’est non documenté parce que personne (ou pas grand monde) ne l’a demandé.

Suggestions que vous voulez POST l’e-mail encodé dans un truc email_encoded. routes.rb montre que c’est un POST et la route, qui est https://discourse.example.com/admin/email/handle_mail. Je l’ai obtenu à partir du modèle d’exemple du récepteur d’e-mails (et je vous recommande de l’utiliser, car si vous l’aviez fait, vous auriez déjà terminé), mais il est également dans discourse/config/routes.rb at main · discourse/discourse · GitHub (ce qui n’est pas immédiatement clair ce qui se passe à moins de comprendre rals).

1 « J'aime »

« modèle d’exemple de destinataire d’e-mail »
Où se trouve-t-il ? Dans le dépôt ? GitHub - discourse/discourse: A platform for community discussion. Free, open, simple. ?

J’ai réussi à comprendre la plupart des choses grâce au code Python.

Succès après quelques jours d’efforts !

@dltj J’ai pu réécrire votre code en nodejs en utilisant les modules aws-sdk et get. Merci beaucoup.

Il y a eu plusieurs obstacles en cours de route, d’AWS (ses, IAM, S3, lambda), en manipulant serverless, en corrigeant l’appel API, jusqu’à la configuration correcte de discourse.

Ce que je vais faire, c’est écrire un sujet détaillé sur exactement ce que j’ai fait pour que cela fonctionne, suffisamment de détails pour que quelqu’un puisse l’activer sans avoir à coder ou à s’arracher les cheveux. Le véritable avantage de cette méthode est qu’en utilisant ses, on n’a PAS besoin de configurer de serveur de messagerie pour le domaine. Cela vous laisse libre de configurer un serveur de messagerie totalement indépendant pour votre domaine.

Si vous êtes intéressé par la façon dont cela a été fait, revenez plus tard, je posterai un lien ici quand ce sera prêt (dans quelques jours).

3 « J'aime »

En complément, @dltj nous allons bientôt supprimer le paramètre email de la route admin/email/handle_mail. Vous devrez envoyer le corps de l’e-mail sous forme de chaîne encodée en base64 dans un paramètre email_encoded à cette route.

3 « J'aime »

l’encodage était l’un des problèmes que je ne pouvais pas résoudre.
Je n’ai pas réussi à faire fonctionner l’e-mail encodé.

  1. @martin, d’après votre publication, email_encoded est incorrect. J’ai essayé les deux, email_encoded génère une erreur, mais encoded_email donne toujours l’avertissement.

body: 'warning: the email parameter is deprecated. all POST requests to this route should be sent with a base64 strict encoded encoded_email parameter instead. email has been received and is queued for processing',

  1. Dois-je convertir mon e-mail entrant en texte brut (ce que je fais maintenant) et le reconvertir en base64, ou est-il probable que ce qu’AWS Ses stocke dans le bucket soit déjà en base64 ? Si je transmets le corps de l’e-mail tel qu’AWS Ses l’a enregistré, il n’apparaît pas dans le journal des e-mails reçus. La commande POST ne renvoie aucune erreur, juste l’avertissement. Donc, en gros, je suis bloqué ici.
    Veuillez retarder la suppression de la clé email jusqu’à ce que nous puissions y parvenir.

même si je décode en texte brut et que je recode en base64, j’obtiens toujours l’avertissement et l’e-mail n’apparaît pas comme reçu :roll_eyes:

      const en_package = {
      headers: DISCOURSE_API_HEADERS,
      json: {encoded_email:Buffer.from(body.toString(),'base64') }
    }
   
   console.log(Buffer.from(body.toString(),'base64'))
   const res = await rest.post(DISCOURSE_URL,en_package)

encore une fois, si j’envoie du texte brut avec “emal”, le même message est reçu et traité.
Vous continuez à dire “strict”, est-ce que cela signifie quelque chose ? Nodejs/JS n’a que deux saveurs base64, “base64” et “base64url”.

Pourquoi l’API continue-t-elle de donner l’avertissement si j’utilise encoded_email même si j’ai une erreur dans l’encodage.

1 « J'aime »

J’ai essayé curl et il n’aime pas encoded_email mais il acceptait email_encoded… argh, donc l’avertissement est incorrect !

J’ai donc réessayé mon code mais avec email_encoded, mais cette fois j’ai utilisé un module d’encodage base64 au lieu de celui intégré et ça fonctionne, ouf !

2 « J'aime »

Ah ! Je vois maintenant. Merci pour l’information, @martin — je vais ajouter cela à ma liste de choses à faire.

2 « J'aime »

Wow, c’est totalement de ma faute (d’avoir mal orthographié l’avertissement), je vais faire une petite PR pour le corriger.

2 « J'aime »

Ce PR va le corriger, il devrait être fusionné plus tard aujourd’hui DEV: Fix typo for email encoded by martin-brennan · Pull Request #15577 · discourse/discourse · GitHub.

2 « J'aime »

C’est super, merci de partager.
Nous utilisions la version obsolète pré-base64 pendant trois ans sur Lambda et elle a cessé de fonctionner il y a un mois.

Nous ne pouvons pas utiliser le SMTP :25 car nous sommes hébergés derrière un tunnel cloudflared et pourquoi jouer avec le port 25 si Amazon fait un si bon travail pour bloquer le bruit avec SES ?

Quoi qu’il en soit, je suis au bout du rouleau.

Le code fonctionne avec Postman et Python exécuté sur un ordinateur local. Quelque chose semble être corrompu lors de l’envoi depuis AWS Lambda avec un code identique.

Forbidden
UTF-8

Eh bien, j’ai résolu mon problème -
même si les requêtes étaient transmises, le ‘mode de lutte contre les bots’ ne fonctionne PAS bien avec l’API Discourse. Peut-être à cause de l’encodage base64 ?

J’espère que cela fera économiser 72 heures de vie à quelqu’un :
Désactivez ceci :

2 « J'aime »