送信、バウンス、および受信メールにAWS SESを使用するための、私が編み出した設定を共有したいと思います。SESサービスには確かにいくつかの微妙な点があり、それがどのように動作するのかを正確に理解するには、かなりの試行錯誤が必要でした。これは段階を追って dotted line をなぞるように説明するものではなく、どちらかというと脳内から一気に吐き出すような内容です。本来は不要な情報ですが、利用は自己責任でお願いします。また、他人が書いたコードを実装する際は、必ず通読して理解してください。
背景:
私はAWS上でDiscourseをデプロイし、信頼性と冗長性を確保するために、可能な限りAWSのサービスを活用しようとしています。開発者として、コマンドラインやコードの方がより慣れ親しんでおり、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
mail-receiver コンテナも、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) ドメインアイデンティティ your.domain を作成します。
-
ドメインアイデンティティを検証します。
-
フィードバック通知用のSimple Notification Service (SNS) トピック feedback-sns-topic を作成します。
a. feedback-sns-topic トピックのARNを、aws_sns_topic_arn_allowlist設定に追加します。 -
your.domain ドメインアイデンティティを設定します。
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. エンコード Base64 でSNSトピック incoming-sns-topic に公開するアクションを追加します。 -
ユーザー system に対して、email リソースの receive email アクションを許可するAPIキーをDiscourseインスタンスで作成します。
-
以下のキーとそれぞれの値を持つシークレット email-handler-secret をSecret Managerで作成します:
- api_endpoint - https://your.domain/admin/email/handle_mail
- api_key - ステップ9で作成したもの
- api_username - ステップ9で異なるものを使用していない限り、system
-
python3.10 ランタイム用で、requests および aws-lambda-powertools ライブラリを含むLambdaレイヤー lambda-receiver-layer を作成します。
-
以下の受信コードを持つ 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)
-
Lambda関数 email-receiver-lambda を設定します:
a. レイヤー lambda-receiver-layer を追加します。
b. AWS Parameter Store のリージョン固有のレイヤーを追加します。
c. 値が email-handler-secret の環境変数 SECRET_NAME を追加します。
d. 詳細なログを記録したい場合は、値が true の環境変数 POWERTOOLS_LOGGER_LOG_EVENT を追加します。 -
シークレット email-handler-secret に対して、Lambda関数 email-receiver-lambda にIAM権限 secretsmanager:GetSecretValue を付与します。
-
SNSトピック incoming-sns-topic にサブスクリプションを作成します。
a. プロトコルはAWS Lambdaです。
b. エンドポイントを email-receiver-lambda のARNに設定します。 -
SNSトピック incoming-sns-topic のサブスクリプションが email-receiver-lambda を呼び出すにはIAM権限が必要ですが、コンソールを通じて設定する場合、これは自動的に実行されるものと思います。
デバッグ目的、あるいは単なる自己嫌悪のため(笑)、SNSトピックのいずれかにメールサブスクリプションを追加して、通知を監視することができます。
これは数回に分けて記述しましたが、すべて網羅していると思います。時間が許す限り、一般的な質問にはお答えできます。
