最近、AWSから「Amazon SES Bounce Review Period for AWS Account」の通知が届いています。しかし、SESを使用する送信元メールアドレスがメーリングリストであるため、未達のメールがあっても通知が届かない仕様となっています。その結果、バウンスメールの閾値を超える警告が届いている状態で、具体的な未達のメールアドレスを判断しにくい状況になっています。
SESから送信されるメールのメトリクスには、Bounce RateとComplaint Rateの2つがあります。前者はアドレスが実在しないか、メールのドメインが実在しないか、受信者のサーバーがメールを拒否したなどが原因によるもので、Bounce Rateが閾値に近づくと、AWSから通知があり、対応が求められます。しかし、これを放置すると閾値を超えてしまい、送信が制限されてしまいます。特にインフラ監視メールなどは送信できない状態になると運用に大きな影響が出るため、これを防ぐ環境を整えるのは急務となります。
■設計
・未達メールの詳細を調査できるように、メール情報をDynamoDBに保存します。ただし、大量のデータが溜まると、DynamoDBの保存料金が高くなる可能性があるため、TTLの設定が必要です。
・SESメトリクスを監視し、閾値を超えるとAWSから通知が届きます。運用者はこれを受けてDynamoDBに蓄積されたデータを確認し、未達のメールアドレスに対して適切な措置を講じます。
■実装
SNS
boundTopic
`Lambda関数「bounceRecord」をサブスクリプションする`
CloudWatch_Alarms_Topic_SES
`Lambda関数「ses-ms」をサブスクリプションする`
DynamoDB
SESNotifications
`パーティションキーは、SESMessageId (String)`
`Time to Live (TTL)は、ExpirationTime`
Lambda(bounceRecord)
実行ロール
AmazonDynamoDBFullAccess
AWSLambdaBasicExecutionRole
# index.js
import { PutItemCommand } from "@aws-sdk/client-dynamodb";
import { ddbClient } from "./ddbClient.js";
export const handler = async function (event) {
console.log("event:", JSON.stringify(event, null, 2));
let SnsPublishTime = event.Records[0].Sns.Timestamp;
let SESMessage = event.Records[0].Sns.Message;
SESMessage = JSON.parse(SESMessage);
let SESMessageType = SESMessage.notificationType;
let SESMessageId = SESMessage.mail.messageId;
let SESSourceAddress = SESMessage.mail.source.toString();
let SESDestinationAddress = SESMessage.mail.destination.toString();
let SESSubject = SESMessage.mail.commonHeaders.subject;
console.log(Math.floor(Date.now() / 1000))
let ExpirationTime = Math.floor(Date.now() / 1000) + 3600
let params = {};
console.log("Type is:", SESMessageType);
console.log("Subject is :", SESSubject);
if (SESMessageType == "Bounce") {
let SESbounceSummary = JSON.stringify(SESMessage.bounce.bouncedRecipients);
params = {
TableName: "SESNotifications",
Item: {
SESMessageId: { S: SESMessageId },
SnsPublishTime: { S: SnsPublishTime },
SESSourceAddress: { S: SESSourceAddress },
SESDestinationAddress: { S: SESDestinationAddress },
SESbounceSummary: { S: SESbounceSummary },
SESMessageType: { S: SESMessageType },
SESSubject: { S: SESSubject },
ExpirationTime: { N: ExpirationTime.toString() },
},
};
} else if SESMessageType == "Delivery") {
} else if (SESMessageType == "Complaint") {
}
try {
const data = await ddbClient.send(new PutItemCommand(params));
console.log("Successfully create item into inventory SESNotifications.", data);
} catch (error) {
console.log("Error", error);
}
}
# ddbClient.js
import { DynamoDBClient } from "@aws-sdk/client-dynamodb"
const REGION = "ap-northeast-1"
const ddbClient = new DynamoDBClient({ region: REGION })
export { ddbClient }
Lambda(ses-ms)
実行ロール
AmazonSESFullAccess
AWSLambdaBasicExecutionRole
# index.js
import { msFunc } from "./mailSend.js";
import dayjs from 'dayjs';
export const handler = async function (event) {
console.log("event:", JSON.stringify(event, null, 2));
let mail_Subject = process.env.NOTICEMAIL_SUBJECT
let mail_from = process.env.NOTICEMAIL_FROM
let mail_to = process.env.NOTICEMAIL_TO
let bounce_body = "";
let AlertMessage = JSON.parse(event.Records[0].Sns.Message);
let SnsPublishTime = event.Records[0].Sns.Timestamp;
const time = dayjs(SnsPublishTime).format('YYYY-MM-DD HH: mm :ss').toString();
console.log(time);
let Threshold = AlertMessage.Trigger.Threshold.toString();
bounce_body = bounce_body + "いつもお世話になっております。<br>";
bounce_body = bounce_body + time + "にて、下記エラーは発生しております。<br>";
bounce_body = bounce_body + "SESバウンス閾値: " + Threshold + "を超えておりますので、至急対応してください。";
try {
const mailSendRes = await msFunc(mail_to, mail_from, mail_Subject, bounce_body);
} catch (e) {
console.error(e);
}};
# mailSend.js
import { SendEmailCommand } from "@aws-sdk/client-ses";
import { sesClient } from "./sesClient.js";
export const msFunc = async function (toAddress, fromAddress, mailSubject, mailbody) {
const createSendEmailCommand = (toAddress, fromAddress, mailSubject, mailbody) => {
return new SendEmailCommand({
Destination: {
ToAddresses: [
toAddress,
],
},
Message: {
Subject: {
Data: mailSubject,
Charset: "UTF-8",
},
Body: {
Text: {
Data: mailbody,
Charset: "UTF-8",
},
Html: {
Data: mailbody,
Charset: "UTF-8",
},
},
},
Source: fromAddress,
ReplyToAddresses: [],
});
};
try {
const response = await sesClient.send(createSendEmailCommand(toAddress, fromAddress,mailSubject, mailbody));
console.log(response);
} catch (err) {
console.log(err)
}};
# sesClient.js
import { SESClient } from "@aws-sdk/client-ses"
const REGION = "ap-northeast-1"
const sesClient = new SESClient({ region: REGION })
export { sesClient }
■CloudWatchAlarm(CloudWatch_Alerm_SES_BounceRate)
■SES側の設定
Amazon SES → 検証済み ID → ×××@××.co.jp → BounceのSNSトピック設定
■検証
Alarm発生しましたら、メール通知
同時にDynamoDBにバウンスメールのレコードは登録されていることを確認しました。
■まとめ
メール送信が禁止されないように検知時、SESバウンスメールへの適切な対策を行うことは、システム運用上で不可欠です。SESMessageTypeが"Delivery"または"Complaint"の場合の対応については、今回はサンプルソースから割愛させていただきました。