0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【AWS】SESバウンスメール監視・通知について

Last updated at Posted at 2023-11-29

最近、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に蓄積されたデータを確認し、未達のメールアドレスに対して適切な措置を講じます。
キャプチャ.PNG

■実装
SNS
qiita.rb
boundTopic
  `Lambda関数「bounceRecord」をサブスクリプションする`
CloudWatch_Alarms_Topic_SES
  `Lambda関数「ses-ms」をサブスクリプションする`
DynamoDB
qiita.rb
SESNotifications
  `パーティションキーは、SESMessageId (String)`
  `Time to Live (TTL)は、ExpirationTime`
Lambda(bounceRecord)
実行ロール
qiita.rb
AmazonDynamoDBFullAccess
AWSLambdaBasicExecutionRole
qiita.rb
# 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);
  }
}
qiita.rb
# 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)
実行ロール
qiita.rb
AmazonSESFullAccess
AWSLambdaBasicExecutionRole
qiita.rb
# 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);
  }};
qiita.rb
# 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)
}};
qiita.rb
# 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)

s.PNG

■SES側の設定

Amazon SES → 検証済み ID → ×××@××.co.jp → BounceのSNSトピック設定
キャプチャ.PNG

■検証

Alarm発生しましたら、メール通知
同時にDynamoDBにバウンスメールのレコードは登録されていることを確認しました。
キャプ3チャ.PNG

■まとめ

メール送信が禁止されないように検知時、SESバウンスメールへの適切な対策を行うことは、システム運用上で不可欠です。SESMessageTypeが"Delivery"または"Complaint"の場合の対応については、今回はサンプルソースから割愛させていただきました。

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?