AWS SES を使用して、送信、バウンス、および受信メールを行うための設定を共有したいと思います。SES サービスには確かにニュアンスがあり、それがどのように機能するかを正確に理解するには、かなりの試行錯誤が必要でした。これは、ステップバイステップの指示というよりは、思考の吐き出しです。不要かもしれませんが、ご自身の責任で使用してください。そして、他人が書いたコードを実装する場合は、必ずすべてを読み、理解してください。
背景:
AWS で Discourse をデプロイし、信頼性と冗長性を確保するために、利用可能なすべてのサービスを活用しようとしています。開発者として、コマンドラインとコードに慣れているため、IaC 自動化を使用したいと考えていました。私の環境全体は Terraform でデプロイされていますが、Web コンソールをクリックして、できる限り調整しようとしました。IAM とポリシー ドキュメントはこの範囲外ですが、必要な箇所は指摘したつもりです。
Postfix インスタンスを実行することは、単一のアプリケーションには過剰なようです。POP3 メールボックスを使用するのは、非常に 90 年代風です。そこで、AWS の奥深くまで潜り込みました。
私の探求に役立った、非常に役立つ投稿がいくつかありました。
- 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
メール受信者コンテナは、Discourse がメッセージをどのように処理するかを理解するのに役立ちました。
- Configure direct-delivery incoming email for self-hosted sites with Mail-Receiver
- Update mail-receiver to the release version
当初、AWS Webhook エンドポイントが受信メッセージを処理すると予想していましたが、コードを確認したところ、そうではないことに気づきました。Lambda 受信者コードは、@dltj による優れた例に基づいています。S3 ではなく、メッセージ配信に SNS を使用することを選択しました。
前提条件
- AWS アカウント
- DNS とメール関連のレコード タイプに関する十分な知識
- 変更を加えることができるドメイン(またはサブドメイン)
注意事項
- 文書化されているすべては、同じ AWS リージョンで作成する必要があります。
- このように太字と斜体で表示されているテキストは、実装固有の値です。
- 斜体テキストは、変数、固定値、または UI 要素の名前です。
手順
-
メール受信をサポートする AWS リージョンで、Simple Email Service (SES) ドメイン ID、your.domain を作成します。
-
ドメイン ID を検証します。
-
フィードバック通知用の Simple Notification Service (SNS) トピック、feedback-sns-topic を作成します。
-
your.domain ドメイン ID を設定します。
a. メールフィードバック転送を有効にします。
b. バウンスおよび苦情(配信ではない)フィードバック通知が SNS feedback-sns-topic トピックを使用するように設定します。 -
SNS feedback-sns-topic トピックにサブスクリプションを作成します。
a. プロトコルは HTTPS です(まだ HTTP を使用していますか?)。
b. エンドポイントを https://your.domain/webhooks/aws に設定します(VERP の投稿を参照)。
c. 生メッセージ配信を有効にするを選択します。 -
受信メール用の別の SNS トピック、incoming-sns-topic を作成します。
-
アクティブな既存のルールセットがない場合は、SES メール受信ルールセット、inbound-mail-set を作成します。アクティブなルールセットは 1 つしか存在できないため、既存のものをアクティブなものとして使用します。
-
inbound-mail-set 受信ルールセットに受信ルールを作成します。
a. 受信者条件を your.domain に設定します。
b. SNS トピック incoming-sns-topic に発行するアクションを追加します。エンコードは Base64 です。 -
Discourse インスタンスで、ユーザー system の API キーを作成し、email リソースに対する receive email アクションを付与します。
-
Secret Manager に、以下のキーとその値を持つシークレット、email-handler-secret を作成します。
- api_endpoint - https://your.domain/admin/email/handle_mail
- api_key - ステップ 9 から
- api_username - ステップ 9 で別のものを選択しない限り、system
-
python3.10 ランタイム用の Lambda レイヤー、lambda-receiver-layer を作成し、requests および aws-lambda-powertools ライブラリを含めます。
-
python3.10 ランタイム用の Lambda 関数、email-receiver-lambda を作成し、受信コードを含めます。
# 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)
-
email-receiver-lambda Lambda 関数を設定します。
a. レイヤー lambda-receiver-layer を追加します。
b. AWS Parameter Store 用のリージョン固有レイヤーを追加します。
c. 環境変数 SECRET_NAME を値 email-handler-secret で追加します。
d. ログに詳細を追加したい場合は、環境変数 POWERTOOLS_LOGGER_LOG_EVENT を値 true で追加します。 -
email-handler-secret シークレットに対して、Lambda 関数 email-receiver-lambda に IAM 権限 secretsmanager:GetSecretValue を付与します。
-
SNS トピック incoming-sns-topic にサブスクリプションを作成します。
a. プロトコルは AWS Lambda です。
b. エンドポイントを email-receiver-lambda の ARN に設定します。 -
incoming-sns-topic トピックの SNS サブスクリプションが email-receiver-lambda を呼び出すための IAM 権限が必要になりますが、コンソール経由で設定すると自動的に行われるはずです。
デバッグ目的、または単なる自己満足のために、どちらかの SNS トピックにメールサブスクリプションを追加して、通知を監視できます。
これを数回のセッションでまとめましたが、すべて網羅していると思います。時間があれば、一般的な質問に答えるようにします。
