AWS SES / AWS Lambda メール受信エンドポイントコード?

メール受信者の実装方法として、AWS SES をすでに利用しているので、Lambda 関数経由で API を使用し、/admin/email/handle_mail API エンドポイントを呼び出すのが最善だと考えています。

SES にメールを取り込み、S3 経由で Lambda 関数に渡す方法に関するチュートリアルはたくさんあります。そのメールを取得してメールサーバーに中継するための、すぐに使える Node.js 関数もいくつか存在します。

しかし、それを行う代わりに、Lambda 関数から直接 API を呼び出し、メールを渡す方が良いように思えます。(独自のローカルメールサーバーは管理しておらず、ポーリングも使用したくありません。)

そのため、そのエンドポイントの呼び出し方や、具体的に何を送信すべきかについての詳細なドキュメント(知る限りドキュメントページはありません)や、コードの共有について少し助けが必要です。(REST エンドポイントですか?WebSocket の代替手段はありますか?送信するものの形式は?)追加のコンテナを実行する必要はありますか?
Configure direct-delivery incoming email for self-hosted sites with Mail-Receiver (その投稿はもう 6 年前です)

誰かが Python でこれを実行したようですが Update mail-receiver to the release version - #7 by dltj? 詳細がなく、Node/JavaScript で行う必要があります。

動作する JavaScript 関数をお持ちの方はいらっしゃいますか?よろしくお願いします。

「いいね!」 2

@dltj さん、こんにちは。Node/JavaScript についてはお手伝いできませんが、私がこれを行うために使用している Python の疑似コードを提供できます。

""" AWS Lambda to receive message from SES and post it to 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):
    """Log the attributes of the context object received from the Lambda invoker
    """
    LOGGER.info(
        "Time remaining (ms): %s.  Log stream: %s.  Log group: %s.  Mem. limits(MB): %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):
    """ Handle SES delivery event """
    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(
            "Processing an SES with mID %s (%s) from %s",
            mailMessageId,
            sesMessageId,
            mailSender,
        )

        deliveryRecipients = mail["destination"]
        for recipientEmailAddress in deliveryRecipients:
            LOGGER.info("Delivery recipient: %s", recipientEmailAddress)

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

            LOGGER.info("Posting to %s", DISCOURSE_SITE)
            r = requests.post(
                DISCOURSE_URL, headers=DISCOURSE_API_HEADERS, data=post_data
            )
            if r.status_code == 200:
                LOGGER.info("Mail accepted by Discourse: %s", DISCOURSE_SITE)
                obj.delete()
                processed = True
            else:
                LOGGER.error(
                    "Mail rejected by %s (%s): %s",
                    DISCOURSE_SITE,
                    r.status_code,
                    r.content,
                )

    return processed

デプロイと保守には Serverless を使用しています。同様の方法を検討される場合は、この 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

ありがとうございます。JSではありませんが、良いスタートです。

  1. (暗号化された)APIキーはどのように作成しますか?暗号化する必要がありますか?
  2. メール返信をAPIでプッシュメールするには、「手動ポーリングを有効にする」をチェックする必要がありますか、それともデフォルトでアクティブになりますか?
  3. AWSの権限またはSESルールに関して、特に注意すべき点はありますか?

キーを暗号化する必要はありません。プロジェクトのインフラストラクチャコードは共有スペースにあるため、ソースにキーをハードコーディングしたくありませんでした。

サイトで「手動ポーリングが有効」になっていますが、具体的に何をするものだったかは覚えていません。

一時的なバケットへの送受信以外に、AWSの権限に関して特に変わったことは何もありません。

幸運を祈ります!

APIキーを生成する場所はWeb UIにありますか?設定のどこにも見つかりません。環境変数として設定してからビルドするのでしょうか?いずれにせよ、APIキーの生成方法がわかりません。

Discourse API キーは、https://{discourse_url}/admin/api/keys の管理インターフェイスで生成されます。

「いいね!」 1

しかし、「granular」にはhandle_email権限がなく、どのユーザーまたはすべてのユーザーを選択すべきか不明だったので、それを見ました。

そのため、ユーザー「system」のグローバル権限を持つキーを生成しました。あなたもそうしましたか?

Postmanアプリを使用して学習/試用したかったのですが、ドキュメントがないため、どのような形式でPOSTすればよいか、キーの渡し方がわかりません。Pythonに慣れていれば、コードを見てこれを理解できるかもしれません。

これはオープンソースプロジェクトであり、handle_mail APIの使用に関する実際のドキュメントがないことは理解できます。あなたのような人々からの助けしかありません…ありがとう!

それはドキュメント化されていないからではなく、オープンソースだからです。誰も(あるいはほとんど誰も)尋ねてこなかったからドキュメント化されていないのです。

エンコードされたメールを email_encoded というもので POST したいという提案ですね。routes.rb を見ると、POST で、ルートは https://discourse.example.com/admin/email/handle_mail です。これはメール受信者のサンプルテンプレートから取得しました(そして、それを使えばすでに完了していたはずなので、それを使うことをお勧めします)が、discourse/config/routes.rb at main · discourse/discourse · GitHub にもあります(これは、Rails を理解していないと、何が起こっているのかすぐに明確ではありません)。

「いいね!」 1

「メール受信者サンプルテンプレート」はどこにありますか?リポジトリの GitHub - discourse/discourse: A platform for community discussion. Free, open, simple. にありますか?

Pythonコードからほとんど理解できました。

数日間の努力の末、成功しました!

@dltj AWS SDKとgetモジュールを使用して、あなたのコードをNode.jsに書き直すことができました。本当にありがとうございました。

AWS(SES、IAM、S3、Lambda)、サーバーレスの操作、API呼び出しの正確な設定、Discourseの設定の適切な調整など、多くの障害がありました。

これを機能させるために行ったことを詳細なトピックとして記述する予定です。コーディングや髪の毛を抜く必要なしに、誰でもこれを有効にできるほどの詳細さです。この方法の本当の利点は、SESを使用することで、ドメインのメールサーバーを設定する必要がないことです。これにより、ドメイン用の完全に独立したメールサーバーを自由に設定できます。

どのように行ったかに興味がある場合は、数日後に準備ができたらここにリンクを投稿しますので、また確認してください。

「いいね!」 3

これに加えて、@dltj さん、まもなく admin/email/handle_mail ルートから email パラメータを削除します。このルートには、メールの本文を base64 エンコードされた文字列として email_encoded パラメータで送信する必要があります。

「いいね!」 3

エンコーディングは解決できなかった問題の1つでした。
エンコードされたメールを機能させることができませんでした。

  1. @martin あなたの投稿によると、email_encoded は間違っています。両方試しましたが、email_encoded はエラーをスローし、encoded_email は警告を出し続けます。

body: '警告: email パラメータは非推奨です。このルートへのすべての POST リクエストは、代わりに base64 strict エンコードされた encoded_email パラメータで送信する必要があります。email は受信され、処理のためにキューに入れられています。',

  1. 受信メールをプレーンテキスト(現在行っていること)に変換してから base64 に戻す必要がありますか、それとも AWS Ses がバケットに保存しているものはすでに base64 ですか?SES が保存したメール本文を渡すと、受信メールログに表示されません。POST コマンドはエラーを返しませんが、警告は返します。基本的に、ここで詰まっています。

これを機能させることができるまで、email キーの削除を遅らせてください。

プレーンテキストにデコードしてからBase64に再エンコードしても、警告が表示され、メールが受信として表示されません: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)

もう一度、プレーンテキストで「emal」を送信すると、同じメッセージが受信および処理されます。
「厳密」と言い続けますが、それは何か意味があるのですか? Nodejs/JSにはBase64のフレーバーは「base64」と「base64url」の2つしかありません。

エンコードエラーが発生している場合でも、encoded_emailを使用するとAPIが警告を出し続けるのはなぜですか。

「いいね!」 1

curlを試したのですが、encoded_emailは受け付けず、email_encodedは受け付けました…うーん、警告は間違っていたようです!

もう一度コードを試しましたが、email_encodedを使用し、組み込みのエンコードモジュールではなくbase64エンコードモジュールを使用しました。これで動作しました。ふぅ!

「いいね!」 2

ああ!今わかりました。情報提供ありがとうございます、@martin。これを私のやることリストに追加します。

「いいね!」 2

それは完全に私のミスです(警告を間違えました)。修正のためにマイナーなPRを行います。

「いいね!」 2

これで修正されるはずです。本日中にマージされる予定です https://github.com/discourse/discourse/pull/15577。

「いいね!」 2

ありがとうございます。共有いただき感謝いたします。
3年間、Lambdaで非推奨のbase64前のバージョンを使用していましたが、1か月前に動作しなくなりました。

Cloudflareトンネルの後ろでホストしているため、SMTP:25は使用できません。また、AmazonがSESでノイズをブロックするのに非常にうまく機能しているのに、なぜポート25をいじる必要があるのでしょうか?

とにかく、私は正気を失いかけています。

コードはPostmanとローカルコンピューターで実行されているPythonで動作します。AWS Lambdaから同じコードで送信すると、何かが壊れているようです。

Forbidden
UTF-8

さて、問題を解決しました。
クエリは渡されていましたが、「ボット対策モード」はDiscourse APIと全く相性が良くありませんでした。base64エンコードのためでしょうか?

これが誰かの72時間を節約できれば幸いです。
これをオフにしてください:

「いいね!」 2