Amazon SESのContact Listを管理する画面をLaravelで作ってみた

Amazon SESのContact Listを管理する画面をLaravelで作ってみた

2026.02.14

こんにちは。クラウド事業本部コンサルティング部の桑野です。
皆さんはAmazon SESでメール購読のオプトイン/オプトアウトを管理したいな〜と思ったことはありませんか?
私はあります。

今回はAWS SDK for PHPを使ってPHP Laravelのアプリケーションからオプトイン/オプトアウトのリンクが埋め込まれたメールを配信する方法をお伝えします。
PHPでの実装例となりますが、SDKを使うのであればTypeScriptやPythonへの応用も可能です。

はじめに

最近メール送信の方法を検討するという機会があったのですが、メールの世界では苦情やバウンスという概念があり、それが発生するとIPレピュテーションという指標が低下し、最悪の場合メールが届かなくなるという事態に発展するようです。

SESも苦情やバウンスを考慮する必要があります。
簡単にお伝えすると、ユーザーから「このメールはスパムである」と報告されたり、存在しないメールアドレスに送信したりすると、AWSから「レビュー」の対象であると判断され、苦情やバウンスを減らす改善を行わないと、SESの利用が制限されるというものです。

一般的なオプトイン、オプトアウトは以下のとおりです。

  • オプトイン:同意する意思を表明すること
  • オプトアウト:拒否する意思を表明すること

SESでのオプトイン、オプトアウトは以下のとおりです。

  • オプトイン:通常通りメールが届く
  • オプトアウト:メールが届かないようSESがブロックしてくれる

この機能があることで、ユーザーは苦情を報告する以外の手段でメールを受信しない選択をすることが可能となります。
結果的に苦情率を上げないことに一定の効果をもたらすと考えています。

前提

以下の条件で検証しています。

  • OS:macOS Tahoe バージョン 26.2
  • チップ:Apple M4
  • Docker Client:28.4.0
  • Docker Server:28.3.3
  • Colima:0.8.4
  • docker compose:2.39.3
  • AWS Vault:7.2.0

本ブログではAmazon SESが有効化されており、利用できることを前提にしております。
SESの有効化手順、SESを利用可能なIAMロールの準備方法やAWS Vaultの使い方については触れません。

今回ご紹介する内容のコードは以下のリポジトリにあります。
SESを使える状況下であれば、Dockerコンテナを使って手軽に試していただくことが可能です。

https://github.com/k-kuwan0/ses-contact-list-managed-by-laravel

コンテナ環境の構築には以下の記事も参考にどうぞ。

https://dev.classmethod.jp/articles/colima-docker-terraform/

オプトアウトのプレースホルダーとは

{{amazonSESUnsubscribeUrl}}のことです。

こちらをメール本文に埋め込むことで以下の画像のように、「配信停止はこちら」というリンクが表示されます。

スクリーンショット 2026-02-13 20.50.50

リンクを開くと、各トピックが表示されており、それぞれをチェックボックスで購読するか、しないかを選ぶことができます。

スクリーンショット 2026-02-13 19.16.02

メールを送信するには、SendEmailを使うのですが、プレースホルダーをメール本文に埋め込む以外にListManagementOptions内でcontactListNametopicNameを指定する必要があります。

https://docs.aws.amazon.com/ja_jp/ses/latest/dg/sending-email-subscription-management.html

contactListtopicというのが何かというのは次のパートで解説します。

Contact listとは

Contact list(連絡先リスト)とは独自のメーリングリストで、特定の1つ以上のトピックを購読したすべての連絡先を保存できるものとなっています。

https://docs.aws.amazon.com/ja_jp/ses/latest/dg/sending-email-list-management.html

コンタクトリストには複数のトピックを登録することができます。
トピックは属性として送信するメールに付与することができます。
メール送信時、送信先のアドレスが自動的に連絡先リストに登録され、アドレスには各トピックに対してオプトインかオプトアウトという状態を持つというイメージです。
受信アドレスが受信するメールのトピックに対し、オプトアウトの状態を持っている場合、メールはSESでブロックされ、受信されることはありません。
概念については、以下のブログがとても参考になります。

https://dev.classmethod.jp/articles/subscription-management-used-amazon-ses/

オプトアウトのプレースホルダーを有効化するためには、コンタクトリストとトピックの登録が必要不可欠となります。

前置きが長くなりましたが、そのページに遷移するためのリンクをメール本文に埋め込む方法を解説していきます。

実装

今回Laravelに組み込む形で検証をしたため、サービスクラスの実装を抜粋しています。

<?php

namespace App\Services;

use Aws\SesV2\SesV2Client;
use Aws\Exception\AwsException;

class SesContactListService
{
    private SesV2Client $client;
    private string $contactListName;

    public function __construct()
    {
        $config = [
            'version' => 'latest',
            'region' => config('services.ses.region', 'ap-northeast-1'),
        ];

        // config経由で認証情報を取得
        $accessKeyId = config('services.ses.key');
        $secretAccessKey = config('services.ses.secret');
        $sessionToken = config('services.ses.token');

        if ($accessKeyId && $secretAccessKey) {
            $credentials = [
                'key' => $accessKeyId,
                'secret' => $secretAccessKey,
            ];
            if ($sessionToken) {
                $credentials['token'] = $sessionToken;
            }
            $config['credentials'] = $credentials;
        }

        $this->client = new SesV2Client($config);
        $this->contactListName = config('services.ses.contact_list_name', 'my-contact-list');
    }

    /**
     * オプトアウトリンク付きでメールを送信する
     *
     * SESがプレースホルダー {{amazonSESUnsubscribeUrl}} を自動的に
     * オプトアウト管理ページのURLに変換します。
     *
     * @param string $from 送信元メールアドレス
     * @param string $to 送信先メールアドレス
     * @param string $subject 件名
     * @param string $htmlBody HTML本文({{amazonSESUnsubscribeUrl}}を含む)
     * @param string $contactListName コンタクトリスト名
     * @param string $topicName トピック名
     * @return array
     */
    public function sendEmailWithOptOut(
        string $from,
        string $to,
        string $subject,
        string $htmlBody,
        string $contactListName,
        string $topicName
    ): array {
        $result = $this->client->sendEmail([
            'FromEmailAddress' => $from,
            'Destination' => [
                'ToAddresses' => [$to],
            ],
            'Content' => [
                'Simple' => [
                    'Subject' => [
                        'Data' => $subject,
                        'Charset' => 'UTF-8',
                    ],
                    'Body' => [
                        'Html' => [
                            'Data' => $htmlBody,
                            'Charset' => 'UTF-8',
                        ],
                    ],
                ],
            ],
            // コンタクトリストとトピックを指定することで、
            // オプトアウトリンクが自動生成される
            'ListManagementOptions' => [
                'ContactListName' => $contactListName,
                'TopicName' => $topicName,
            ],
        ]);

        return [
            'success' => true,
            'messageId' => $result['MessageId'],
            'message' => 'メールを送信しました',
        ];
    }

    /**
     * オプトアウトリンクのプレースホルダーを含むHTMLテンプレートを生成する
     *
     * @param string $content 本文コンテンツ
     * @param string $optOutLinkText オプトアウトリンクのテキスト
     * @return string
     */
    public static function buildHtmlWithOptOutLink(
        string $content,
        string $optOutLinkText = '配信停止はこちら'
    ): string {
        return <<<HTML
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
</head>
<body>
    {$content}
    <hr>
    <p style="font-size: 12px; color: #666;">
        このメールの配信を停止したい場合は、
        <a href="{{amazonSESUnsubscribeUrl}}">{$optOutLinkText}</a>
        をクリックしてください。
    </p>
</body>
</html>
HTML;
    }
}

重要なのは、あらかじめSDKやCLIで作成しておいたコンタクトリストとトピックをListManagementOptionsの中でContactListNameTopicNameとして指定することです。
プレースホルダーについては、buildHtmlWithOptOutLink関数の方でメール本文を組み立てています。

最終的にControllerでこれらの処理を組み合わせてメール送信処理を実行しています。
以下がControllerの実装です。

    /**
     * オプトアウトリンク付きでメールを送信する
     */
    public function sendEmail(Request $request): JsonResponse
    {
        $validated = $request->validate([
            'from' => 'required|email',
            'to' => 'required|email',
            'subject' => 'required|string',
            'content' => 'required|string',
            'list_name' => 'required|string',
            'topic_name' => 'required|string',
        ]);

        // オプトアウトリンク付きのHTMLを生成
        $htmlBody = SesContactListService::buildHtmlWithOptOutLink(
            $validated['content']
        );

        $result = $this->sesService->sendEmailWithOptOut(
            $validated['from'],
            $validated['to'],
            $validated['subject'],
            $htmlBody,
            $validated['list_name'],
            $validated['topic_name']
        );

        return response()->json($result);
    }

メール送信を行うだけであれば、さほど難しい処理ではないかなという印象です。AWS SDKが用意されているのは本当にありがたいなと日々感じます。

Contact Listの管理が必要

アプリケーション側での対応はSendEmailでオプションを追加するだけで良かったですが、AWSインフラ側もオプションを有効化するためにコンタクトリストの作成とトピックの登録が必要となります。
ただし、私が調べる限り、コンタクトリストは2026年2月13日現在、AWSマネジメントコンソールから管理するUIが提供されておらず、CLIやSDKで操作することとなります。

コンタクトリストを管理しようと考えた際、必要となるaws sesv2コマンドは以下のとおりかなと想定しています。

  • コンタクトリストの作成: create-contact-list
  • コンタクトリストの取得: list-contact-lists
  • トピック追加: update-contact-list
  • コンタクト一覧取得(アドレス): list-contacts
  • コンタクト詳細取得(アドレスの購読状況): get-contact
  • コンタクト更新(購読状況変更): update-contact

https://docs.aws.amazon.com/cli/latest/reference/sesv2/

数が非常に多いですね。
これらをCLIだけで管理し、運用していくのは大変であるとなんとなく想像がつきます。
これらをうまく管理するにはどうすれば良いでしょうか。

Laravelで管理画面を作成してみた

ということで、簡易ではありますが管理画面を作成してみました。
仕組みとしては、AWS SDK for PHPを使い、Laravelアプリケーションから呼び出すだけです。
ローカル環境で動かす前提のCLIの代わりとなる補助アプリというイメージでご覧いただければと思います。

ホーム画面

ただの導線です。

スクリーンショット 2026-02-14 1.32.36

コンタクトリスト管理画面

コンタクトリストの作成状況とリストに登録されているトピックの一覧を表示します。
今回はいろんな方にサクッと試してもらえるよう、コンタクトリストを削除する機能も備えています。
本番運用する際は消しておいた方がいいかもしれませんね。
その下にはトピックの追加ができるようになっています。

スクリーンショット 2026-02-14 1.49.38

コンタクトリストが作成されていない場合は、コンタクトリストの作成が可能です。
基本的にコンタクトリストは1つしか作成できないため、環境変数で固定するような作りにしています。
作成の際は少なくとも一つはトピックも登録するようなUIにしています。

スクリーンショット 2026-02-14 1.49.53

コンタクト管理画面

リストに登録されているコンタクトの登録と登録されているコンタクトの一覧が確認できます。

画面上部はコンタクトの登録と検索機能を用意しています。

スクリーンショット 2026-02-14 1.40.50

さらにその下には検索結果に合致するコンタクトが一覧で表示されます。

スクリーンショット 2026-02-14 1.42.04

コンタクトをクリックすると、コンタクトの購読状況が確認できます。
すべてのメールを一括で停止することもできれば、配信を再開することができます。

スクリーンショット 2026-02-14 1.43.56

下にスクロールすると、トピック別の購読状況の確認と操作ができます。

スクリーンショット 2026-02-14 1.45.30

メール送信画面

実際にメールを送信することもできます。
SESで使用可能なドメインを使って、メールを送信することができます。

スクリーンショット 2026-02-14 1.46.17

プレビュー機能を用意しているため、送信前にどんな本文が送られるのかをチェックしてから送信することができます。

スクリーンショット 2026-02-14 1.48.10

あくまでも試験的に送信するための機能という位置付けで用意しています。


これらの画面は使用する際のイメージも想像しながら作成しました。
以下が運用イメージです。

普段使い

  • コンタクトリストの作成
  • コンタクトリストの作成状況・トピック登録状況の確認
  • トピックの登録

顧客問い合わせ時

  • コンタクトの検索
  • コンタクトの詳細確認
  • コンタクトの購読状況変更
  • テストメール送信

実際に運用する中で改善できるポイントはまだまだたくさんあると思います。
運用する中での改善点があれば更新していきます。

もし、試験的にコンタクトリストを作成したという場合は、検証が終わった後は忘れずにコンタクトリストを削除するようにしましょう。
画面からなら削除ボタンをクリックするだけで消せますので有効活用していただければ幸いです。

まとめ

いかがだったでしょうか。
メール本文自体はプレースホルダーとオプションを追加するだけで簡単にオプトアウトのリンクを差し込むことができます。
一方でオプトアウトの状況を管理するコンタクトリストの取り扱いはCLIの操作となってしまうため、運用しようと考えるとなかなかハードルが高いかなという印象を抱いています。
そのうちマネジメントコンソールでも確認や操作ができるようになると一層SESが使いやすくなるかもしれません。
それまでは私が作成したような自前の管理画面が、ハードルを下げることに少しでも貢献できれば嬉しいなと考えています。
最後までご覧いただきありがとうございました。

この記事をシェアする

FacebookHatena blogX

関連記事