Back to home

AWS SESで受けたメールをそのまま転送するCDKを作ってみた。

4 min read
Table of Contents

お久しぶりです。

最近、フロントエンドのお仕事に従事をしている中の人です。

以前にブログ記事に書いた CDK で AWS SES を作ってみた という記事はご覧いただけましたでしょうか。

ご覧になっていない人は見ていただければと思います。

本記事はその記事の続きになります。

作ったもの

SESでメール転送するCDKです。

SESで生やしたメールアドレスに、メールが届いたら指定したメールアドレスに転送するものを作りました。

無駄に添付ファイルも転送するという抜かりない実装です。

リポジトリ

使い方も README.md に書いてあります。
ご参照ください。

GitHub - Momijinn/sample-cdk-ses-forwarding: CDKで作るSESでメール転送する
github.com
image

アーキテクチャ

AWS SES / S3 / Lambda というシンプルな構成です。

  1. SES でメールを受信したら、S3にメール情報を保管。
  2. Lambdaを起動してS3にあるメール情報を取得し、指定したメールアドレスへメール情報を転送。

をしています。

詰まったところ

SES に Lambda をくっつけてイベントをもらってもメール情報はない

AWS SES の受信設定で、Lambdaが選べることは知っていました。

そのため、「メールを受信したら、Lambdaを起動してイベントの中にある情報を使って転送すればOKじゃん」みたいなことを考えていました。

しかし、実際には、メールの本文情報は入っていないです。

そのため、 S3 にメール本文をいれてLambdaで拾ってくる みたいなことをしています。

ちなみに、Lambdaで受け取るSESの情報は以下。

{
    "Records": [
        {
            "eventSource": "aws:ses",
            "eventVersion": "1.0",
            "ses": {
                "mail": {
                    "timestamp": "2025-04-17T12:39:34.969Z",
                    "source": "******m",
                    "messageId": "******",
                    "destination": [
                        "SES_MAIL_ADDRESS"
                    ],
                    "headersTruncated": false,
                    "headers": [
                        {
                            "name": "Return-Path",
                            "value": "<******m>"
                        },
                        {
                            "name": "Received",
                            "value": "from e234-2.smtp-out.ap-northeast-1.amazonses.com (e234-2.smtp-out.ap-northeast-1.amazonses.com [23.251.234.2]) by inbound-smtp.ap-northeast-1.amazonaws.com with SMTP id ****** for SES_MAIL_ADDRESS; Thu, 17 Apr 2025 12:39:34 +0000 (UTC)"
                        },
                        {
                            "name": "Received-SPF",
                            "value": "pass (spfCheck: domain of mail.aws.autumn-color.com designates 23.251.234.2 as permitted sender) client-ip=23.251.234.2; envelope-from=******m; helo=e234-2.smtp-out.ap-northeast-1.amazonses.com;"
                        },
                        {
                            "name": "Authentication-Results",
                            "value": "amazonses.com; spf=pass (spfCheck: domain of mail.aws.autumn-color.com designates 23.251.234.2 as permitted sender) client-ip=23.251.234.2; envelope-from=******m; helo=e234-2.smtp-out.ap-northeast-1.amazonses.com; dkim=pass [email protected]; dkim=pass [email protected]; dmarc=pass header.from=aws.autumn-color.com;"
                        },
                        {
                            "name": "X-SES-RECEIPT",
                            "value": "*****"
                        },
                        {
                            "name": "X-SES-DKIM-SIGNATURE",
                            "value": "****"
                        },
                        {
                            "name": "DKIM-Signature",
                            "value": "****"
                        },
                        {
                            "name": "DKIM-Signature",
                            "value": "******"
                        },
                        {
                            "name": "From",
                            "value": "SES_MAIL_ADDRESS"
                        },
                        {
                            "name": "To",
                            "value": "SES_MAIL_ADDRESS"
                        },
                        {
                            "name": "Subject",
                            "value": "aaa"
                        },
                        {
                            "name": "MIME-Version",
                            "value": "1.0"
                        },
                        {
                            "name": "Content-Type",
                            "value": "text/html; charset=UTF-8"
                        },
                        {
                            "name": "Content-Transfer-Encoding",
                            "value": "quoted-printable"
                        },
                        {
                            "name": "Message-ID",
                            "value": "<0106019643c2fe70-01c9b657-03e7-4680-b70d-841ec51460c0-000000@ap-northeast-1.amazonses.com>"
                        },
                        {
                            "name": "Date",
                            "value": "Thu, 17 Apr 2025 12:39:34 +0000"
                        },
                        {
                            "name": "Feedback-ID",
                            "value": "::1.ap-northeast-1.z/aeDskmBB8vGsMr5LSN5IbWhbzIn7+kspXDou76DTs=:AmazonSES"
                        },
                        {
                            "name": "X-SES-Outgoing",
                            "value": "2025.04.17-23.251.234.2"
                        }
                    ],
                    "commonHeaders": {
                        "returnPath": "******m",
                        "from": [
                            "SES_MAIL_ADDRESS"
                        ],
                        "date": "Thu, 17 Apr 2025 12:39:34 +0000",
                        "to": [
                            "SES_MAIL_ADDRESS"
                        ],
                        "messageId": "<0106019643c2fe70-01c9b657-03e7-4680-b70d-841ec51460c0-000000@ap-northeast-1.amazonses.com>",
                        "subject": "aaa"
                    }
                },
                "receipt": {
                    "timestamp": "2025-04-17T12:39:34.969Z",
                    "processingTimeMillis": 243,
                    "recipients": [
                        "SES_MAIL_ADDRESS"
                    ],
                    "spamVerdict": {
                        "status": "DISABLED"
                    },
                    "virusVerdict": {
                        "status": "DISABLED"
                    },
                    "spfVerdict": {
                        "status": "PASS"
                    },
                    "dkimVerdict": {
                        "status": "PASS"
                    },
                    "dmarcVerdict": {
                        "status": "PASS"
                    },
                    "action": {
                        "type": "Lambda",
                        "functionArn": "arn:aws:lambda:ap-northeast-1:****",
                        "invocationType": "Event"
                    }
                }
            }
        }
    ]
}

メール受信ルールの有効化

メールの受信ルールを設定だけしても、メールは受信されないです。

メールの受信ルールの有効化をする必要があります。

コードでいうと、以下の箇所( コードでいうと app/lib/app-stack.ts )がルール有効化の箇所です。

// SES の受信ルール有効化
new cr.AwsCustomResource(this, `${PREFIX}EnableReceiptRuleSet`, {
  onCreate: {
    service: 'SES',
    action: 'setActiveReceiptRuleSet',
    parameters: {
      RuleSetName: ruleSet.receiptRuleSetName,
    },
    physicalResourceId: cr.PhysicalResourceId.of(`Activate-${ruleSet.receiptRuleSetName}`),
  },
  onUpdate: {
    service: 'SES',
    action: 'setActiveReceiptRuleSet',
    parameters: {
      RuleSetName: ruleSet.receiptRuleSetName,
    },
    physicalResourceId: cr.PhysicalResourceId.of(`Activate-${ruleSet.receiptRuleSetName}`),
  },
  onDelete: {
    service: 'SES',
    action: 'setActiveReceiptRuleSet',
    parameters: {
      RuleSetName: '',
    },
    physicalResourceId: cr.PhysicalResourceId.of(`Activate-${ruleSet.receiptRuleSetName}`),
  },
  policy: cr.AwsCustomResourcePolicy.fromSdkCalls({
    resources: cr.AwsCustomResourcePolicy.ANY_RESOURCE,
  })
});

onCreateonUpdateonDelete がある理由は、ルールを変更したときや削除した時の実行はどうするかを書いています。

はじめは onCreate のみで作っていましたが、試行錯誤していてルール変更したときに CDK から怒られたり、変更後自動でルールが有効化しなかったりしました。

以下の ISSUE より、onDelete を実装しろとのことなのでちゃんと実装しました。ついでに、 onUpdate もした。

`aws_ses`: `ReceiptRuleSet` is not made active automatically · Issue #28823 · aws/aws-cdk
github.com
image

エラーメッセージは以下。

Received response status [FAILED] from custom resource. Message returned: User: arn:aws:sts::***:assumed-role/*** is not authorized to perform: ses:DeleteReceiptRuleSet because no identity-based policy allows the ses:DeleteReceiptRuleSet action (RequestId: ***)

Lambdaの初期化がおわらない

LambdaをCDKといっしょにデプロイするとき、Docker でのビルドが走らせるため、コンテナのセットアップが入ります。

その時に、ずっと RUN npm install --global [email protected] で止まっていました。

この場合、ローカルで動かして言えるCPUのアーキテクチャを揃えるとうまくいくっぽいです。

中の人の場合、M4 macBook なので cdk.aws_lambda.Architecture.ARM_64 を追記したらうまくいった。

const lambdaFunction = new cdk.aws_lambda_nodejs.NodejsFunction(this, `${PREFIX}LambdaFunction`, {
  entry: 'lambda/src/index.ts',
  handler: 'handler', 
  runtime: cdk.aws_lambda.Runtime.NODEJS_22_X, 
  memorySize: 128, 
  timeout: cdk.Duration.seconds(30), 
  architecture: cdk.aws_lambda.Architecture.ARM_64, // これ
  // 略
});

CustomResource を変更して deploy したらエラーになった

CustomResource を変更して 再度デプロイみたいなことをしたら以下のエラーになりました。

****/Resource/Default (***) Modifying service token is not allowed.

この場合、AwsCustomResourcefunctionName を固定化していることが原因らしいです。

new cr.AwsCustomResource(this, `${PREFIX}EnableReceiptRuleSet`, {
  functionName: `${PREFIX}EnableReceiptRuleSet`, // ←これ

内部的に作られる Lambda の名前を指定してしまっているため、CDK が更新のたびにそれを「既存の名前で上書きしよう」としてしまい、そのときに ServiceToken が変更される=CloudFormation 的に禁止、という事になっている とのことです。
(chatGPT 曰く)

皆様も気をつけましょう。

添付ファイルも転送したい

現時点(2025/04)において、LambdaでSESを使ったメール送信には、 @aws-sdk/client-ses を使います。

このパッケージをつかうと簡単にメール送信ができてしまいますが、
メールを作るためには、 SendEmailCommandSendRawEmailCommand の2種類があります。

前者はお手軽で、後者はフルマニュアルでガシガシメールの仕様に従って書く必要があります。

メールの文章だけなら SendEmailCommand で十分対応できます。

しかし、添付ファイルも転送したい場合は、SendRawEmailCommand を使う必要があります。

幸いにも、中の人が chatGPT と共に書いたサンプルコードがあるのでご参照ください。

sample-cdk-ses-forwarding/app/lambda/src/index.ts at main · Momijinn/sample-cdk-ses-forwarding
github.com
image

まとめ

AWS SESで受けたメールをそのまま転送するCDKを作ってみました。

躓いた点も書いたので、同じ問題にぶちあった人は是非参考にしてみてください。

参考文献

Amazon SES でカスタムドメインを使用して受信したメールを S3 バケットに格納する構成を AWS CDK で実装してみた | DevelopersIO
dev.classmethod.jp
image
SESで受信したメールをLambdaで別の宛先に転送してみた | DevelopersIO
dev.classmethod.jp
image
[サンプルあり]AWS SESでメール受信を行い転送を行うプログラムの紹介 - グリーク通信
glic.co.jp
image