Amazon SESの連絡先リストによる購読管理を試してみました
初めに
先日メールのメルマガの登録・解除の話を別件のついでで少しだけ話す機会がありました。
話したのちにアプリで対応しようとするとそこそこ労力が必要な感覚があるからAmazon SESでよしなにできる機能がないかなと調べたところ「連絡先リスト(Contact List)」という機能を見つけました。
名前から送信用のメーリングリスト的な機能かな?と思っていたのですがそういった機能ではないようで、むしろメールの購読のオプトイン/オプトアウトを管理しオプトアウトされた連絡先にはメールを飛ばさないようにする機能オンリーのようです。
AWSのドキュメントページは以下となりますのでより詳細な情報はこちらもご参照ください。
自分の調べ方が悪いのか利用に関する情報が公式ドキュメント以外で見当たらなかったので実際に利用しどのような挙動になるかみていきます。
リストとトピック
こちらを使うためのグルーピングとしてリストとトピックの2つ概念が存在します。
概念としてはリストの中にトピックが存在し(最大20個まで所属可能)、各メールアドレス+任意属性を組とした情報をリストに所属させ、さらにそこに対してトピックを結びつける形となります。
AWS アカウント ごとに許可される連絡先リストは 1 つだけです。
なおリストは現状では複数作れるわけではないようで1アカウントに1つのみとなります(後述しますが実際に試す限りはリージョン毎?)。一応listコマンドは実装されているので複数利用できる未来もあるのかもしれません。
どこまでを一塊として「連絡先」として呼称するかドキュメント上の定義は曖昧ですが以降このドキュメントでは「各メールアドレス+任意属性」の1つの組を「連絡先」と呼称します。
連絡先にはトピックを複数結びつけることができトピックごとに購読を管理することができます。
例として「最新情報」と「お役立ち情報」のような2つのメールマガジンをその顧客が読んでおりそれを管理したい場合は、その連絡先に対してその2つのトピックを結びつける形となります。
途中でその顧客が「最新情報」が不要になった場合はそちらのトピックをオプトアウトすることで以降メールの送信時にそのトピックを指定して送信すると顧客にメールが届かなくなります(挙動としてはバウンスとなるようです)。
実際トピックにどのような意味を持たせるかは設計やご利用方法にはよりますがカテゴリのようなものを用意しておきそれをユーザに割り当ててそれごとに購読要否を管理できます。
個人的にはトピックについては連絡先に購読情報とセットで結びつけられているものと頭の中でイメージしています。
メール送信時の購読管理の利用
AWS APIを利用の場合はListManagementOptions
を指定、SMTPインターフェースを利用の場合はX-SES-LIST-MANAGEMENT-OPTIONS
ヘッダに対してリスト名およびトピック名を指定することで利用可能です。
そのトピックに含まれているかつオプトアウトとなっている場合はバウンスし、それ以外の場合は送信に成功する形となります。
これを指定している状態で{{amazonSESUnsubscribeUrl}}
という値をメール本文に埋め込むとこの部分にそのメールアドレスでの購読管理用のリンクが追加され、受信者がこのリンクを通して購読および購読解除ができるようになります。
SMTPインターフェースの場合は標準化された仕様である(らしい)List-Unsubscribe
およびList-Unsubscribe-Post
も利用可能なようですが標準仕様の読み込みが少し必要となりそうですのでまたの機会に試してみようと思います。
リストとトピックの作成
まずはリストとそこに含まれるトピックを作成します。
今回はAWS CLIで操作を行いますがaws sesv2 create-contact-list
コマンドで新規の登録が可能です。
情報の指定はJSONで行いますが今回はExampleContactListName
という名前でリストを作成し、おすすめ商品を通知するニュースレター管理用のrecommend-item
トピック、その他の雑多な情報を含めたニュースレターを管理するother-news
トピックを用意します。
JSONの情報は以下のとおりです。
{ "ContactListName": "ExampleContactListName", "Description": "Creating a contact list example", "Topics": [ { "TopicName": "recommend-item", "DisplayName": "recommend item letter", "Description": "recommend item news letter topic", "DefaultSubscriptionStatus": "OPT_IN" }, { "TopicName": "other-news", "DisplayName": "news letter", "Description": "mixed news letter topis", "DefaultSubscriptionStatus": "OPT_IN" } ] }
DefaultSubscriptionStatus
はリスト追加時のデフォルトの購読状況の設定でOPT_IN
(オプトイン)、OPT_OUT
(オプトアウト)が指定可能です。
実行に成功しても特にレスポンスは無いようですが、再度実行するとリストが最大1個しか作れない旨のエラーが確認できます。
$ aws sesv2 create-contact-list --cli-input-json file://ses-contact-list.json $ aws sesv2 create-contact-list --cli-input-json file://ses-contact-list.json An error occurred (BadRequestException) when calling the CreateContactList operation: A maximum of 1 Lists allowed per account.
ドキュメント上アカウントごとに一つということで複数リージョンで使いたい場合はどうするんだろう?と思ったのですが実際に実行してみるとリージョン毎に1つリストを作成できました。
# 上記のJSONの`ExampleContactListName`を`ExampleContactListNameUsEast2`に変更して実行 $ aws sesv2 create-contact-list --cli-input-json file://ses-contact.json --region us-west-2 $ aws sesv2 list-contact-lists { "ContactLists": [ { "ContactListName": "ExampleContactListName", "LastUpdatedTimestamp": "2023-12-21T16:12:39.461000+09:00" } ] } $ aws sesv2 list-contact-lists --region us-east-2 { "ContactLists": [ { "ContactListName": "ExampleContactListNameUsEast2", "LastUpdatedTimestamp": "2023-12-21T16:14:48.750000+09:00" } ] }
現時点では少なくとも東京リージョンではマネージメントコンソールからリストを確認できる画面はなさそうなので、この辺りの管理が必要であれば現状は独自で管理できるような何かを準備する必要がありそうです。
連絡先の登録
こちらもJSONに登録値を記載しそれをAPIで登録します。
CreateImportJob
を利用することでS3上にあげたJSONやCSVからまとめて取り込めるようですが、準備が手間なのと今回は購読の挙動を見ることを主軸に置くためCreateContact
を利用して個々に登録します。
JSONは以下の通りです(emailAddressの指定は実際には私のアドレスです)。
{ "ContactListName": "ExampleContactListName", "EmailAddress": "example@example.com", "UnsubscribeAll": false, "AttributesData": "{\"Name\":\"Junya\"}", "TopicPreferences": [ { "TopicName": "recommend-item", "SubscriptionStatus": "OPT_IN" }, { "TopicName": "other-news", "SubscriptionStatus": "OPT_OUT" } ] }
登録コマンドを実行しますが相変わらず出力は空です。一応JSONキーの大文字小文字の区別はあるようです。
% aws sesv2 create-contact --cli-input-json file://ses-contact.json #大文字小文字に誤りがあるとエラー % aws sesv2 create-contact --cli-input-json file://ses-contact-invalid.json Parameter validation failed: Missing required parameter in input: "EmailAddress" Unknown parameter in input: "unsubscribeAll", must be one of: ContactListName, EmailAddress, TopicPreferences, UnsubscribeAll, AttributesData Unknown parameter in input: "attributesData", must be one of: ContactListName, EmailAddress, TopicPreferences, UnsubscribeAll, AttributesData Unknown parameter in input: "topicPreferences", must be one of: ContactListName, EmailAddress, TopicPreferences, UnsubscribeAll, AttributesData
list-conacts
で登録されていることが確認できます。
% aws sesv2 list-contacts --contact-list-name ExampleContactListName { "Contacts": [ { "EmailAddress": "example@example.com", "TopicPreferences": [ { "TopicName": "other-news", "SubscriptionStatus": "OPT_OUT" }, { "TopicName": "recommend-item", "SubscriptionStatus": "OPT_IN" } ], "UnsubscribeAll": false, "LastUpdatedTimestamp": "2023-12-21T22:51:03.078000+09:00" } ] }
なお登録時に存在しているトピックを全て指定していない場合そのトピックに関する指定がない状態ではなく、デフォルト値で設定されるようです。
# recommend-itemのトピックが未指定 $ cat ses-contact-1.json { "ContactListName": "ExampleContactListName", "EmailAddress": "example+1@example.com", "UnsubscribeAll": false, "AttributesData": "{\"Name\":\"Junya\"}", "TopicPreferences": [ { "TopicName": "other-news", "SubscriptionStatus": "OPT_OUT" } ] } $ aws sesv2 create-contact-list --cli-input-json file://ses-contact.json $ aws sesv2 list-contacts --contact-list-name ExampleContactListName { "Contacts": [ { "EmailAddress": "example+1@example.com", "TopicPreferences": [ { "TopicName": "other-news", "SubscriptionStatus": "OPT_OUT" } ], "TopicDefaultPreferences": [ { "TopicName": "recommend-item", "SubscriptionStatus": "OPT_IN" } ], "UnsubscribeAll": false, "LastUpdatedTimestamp": "2023-12-21T23:02:14.175000+09:00" } .... ] }
なお後述しますが送信する際にリスト・トピックに所属していないアドレスに送信した場合は送信タイミングで登録されるため必ずしも送信する準備としてこちらのAPIを呼ぶ必要はありません。
オプトイン対象へのメール送信
メール送信はSMTPインターフェースの利用で送信します。送信にはopenssl
コマンドを利用して生のSMTPで対話します。
X-SES-LIST-MANAGEMENT-OPTIONS: {contactListName}; topic={topicName}
というようにヘッダを含めることで利用可能です。
今回はオプトインとなっているrecomend-itemを指定して送ります。
$ openssl s_client -crlf -quiet -starttls smtp -connect email-smtp.ap-northeast-1.amazonaws.com:587 depth=2 C = US, O = Amazon, CN = Amazon Root CA 1 verify return:1 depth=1 C = US, O = Amazon, CN = Amazon RSA 2048 M01 verify return:1 depth=0 CN = email-smtp.ap-northeast-1.amazonaws.com verify return:1 250 Ok EHLO mail.example.com 250-email-smtp.amazonaws.com 250-8BITMIME 250-STARTTLS 250-AUTH PLAIN LOGIN 250 Ok AUTH PLAIN xxxxxxxxxxx 235 Authentication successful. MAIL FROM: option@example.com 250 Ok RCPT TO: example@example.net 250 Ok DATA 354 End data with <CR><LF>.<CR><LF> From: option@example.com To: example@example.net Subject: from ses X-SES-LIST-MANAGEMENT-OPTIONS: ExampleContactListName; topic=recommend-item im from ses {{amazonSESUnsubscribeUrl}} . 250 Ok 0106018c8fa80422-a1c6b10d-6544-48af-9aea-85433c6e17c2-000000
メールを確認すると{{amazonSESUnsubscribeUrl}}
の部分が購読解除用のリンクに置き換わっています。
メールヘッダーを確認してみるとX-SES-LIST-MANAGEMENT-OPTIONS
の指定はなくなっていました。
おそらくAWS側でList-Unsubscribe
及びList-Unsubscribe-Post
に置換されたものと思われます。
... Date: Fri, 22 Dec 2023 03:53:22 +0000 List-Unsubscribe: <mailto:unsubscribe-xxxxxxxxx> List-Unsubscribe-Post: List-Unsubscribe=One-Click Feedback-ID: 1.ap-northeast-1.l/xxxxxxx=:AmazonSES X-SES-Outgoing: 2023.12.22-23.251.234.12 im from ses ...
ちなみに存在しないトピック名を指定して送ってしまったのですがこの場合は送信エラーとなるようです
... Subject: from ses X-SES-LIST-MANAGEMENT-OPTIONS: ExampleContactListName; topic=recommend im from ses {{amazonSESUnsubscribeUrl}} . 554 Message rejected: List: ExampleContactListName doesn't contain Topic: recommend
ユーザ側からのオプトアウト
先ほどのメール内のリンクをクリックして購読を解除してみます。
ワンクリック解除かなと思っていたのですがどうやらユーザリストに含まれる全てのトピックが表示され全て操作でき、また解除だけではなく登録もできるようです。
今回はrecommend item letter
を購読解除、news letter
を新規購読と逆転させてみます。
成功すると同画面で以下のようなダイアログが表示されます。
ページはカスタマイズできなさそう(?)であり英語ページとなっているので国内向けサービスだと利用しづらいケースは多いかもしれません。
list-contacts
で購読を操作した連絡先を確認してみると指定した通りに変更されていることが確認できます。
... { "EmailAddress": "example@example.com", "TopicPreferences": [ { "TopicName": "other-news", "SubscriptionStatus": "OPT_IN" }, { "TopicName": "recommend-item", "SubscriptionStatus": "OPT_OUT" } ], "UnsubscribeAll": false, "LastUpdatedTimestamp": "2023-12-22T13:07:37.832000+09:00" } ...
オプトアウトされた対象へのメール送信
先ほど購読を解除したrecommend-item
を指定してメールを送ってみます。
送信処理自体は250 OKで処理されるようです。
$ openssl s_client -crlf -quiet -starttls smtp -connect email-smtp.ap-northeast-1.amazonaws.com:587 ... To: example+optout@example.com Subject: from ses X-SES-LIST-MANAGEMENT-OPTIONS: ExampleContactListName; topic=recommend-item im from ses {{amazonSESUnsubscribeUrl}} . 250 Ok 0106018c8fbdf44c-bc6d1381-93a6-4b06-bc91-c8b698d20a69-000000
VDMでイベントを確認してみると実際には永続的なバウンスとして処理されていることが確認できます。
なおGMail等で利用可能なエイリアスでリストに登録されていないexample+optin@example.com
に送ったところこちらもバウンス扱いとなリました。
Firehose経由でS3に送信されたイベントは以下のとおりです。
{ "eventType": "Bounce", "bounce": { "feedbackId": "xxxxxxx", "bounceType": "Permanent", "bounceSubType": "UnsubscribedRecipient", "bouncedRecipients": [ { "emailAddress": "example@example.net", "action": "failed", "status": "5.7.1", "diagnosticCode": "Amazon SES did not send the message to this address because the contact has unsubscribed from receiving emails." } ], "timestamp": "2023-12-22T04:17:20.345Z", "reportingMTA": "dns;amazonses.com" }, "mail": { "timestamp": "2023-12-22T04:17:19.948Z", "source": "optin@example.com", "sourceArn": "arn:aws:ses:ap-northeast-1:xxxxxxx:identity/example.com", "sendingAccountId": "xxxxx", "messageId": "0106018c8fbdf44c-bc6d1381-93a6-4b06-bc91-c8b698d20a69-000000", "destination": [ "example@example.net" ], "headersTruncated": false, "headers": [ { "name": "Received", "value": "from mail.xxxxx.com..." }, { "name": "From", "value": "optin@example.com" }, { "name": "To", "value": "example@example.net" }, { "name": "Subject", "value": "from ses" } ], "commonHeaders": { "from": [ "optin@example.com" ], "to": [ "example@example.net" ], "messageId": "0106018c8fbdf44c-bc6d1381-93a6-4b06-bc91-c8b698d20a69-000000", "subject": "from ses" }, "tags": { "ses:source-tls-version": [ "TLSv1.2" ], "ses:operation": [ "SendSmtpEmail" ], "ses:configuration-set": [ "examplecom-configuration" ], "ses:recipient-isp": [ "UNKNOWN_ISP" ], "ses:source-ip": [ "xxx.xxx.xxx.xxx" ], "ses:from-domain": [ "example.com" ], "ses:sender-identity": [ "example.com" ], "ses:caller-identity": [ "xxxxxxx" ] } } }
永続的なバウンス(ハードバウンス)のためAmazon SESの利用停止の原因となるバウンスの対象扱いでは?と思いましたが、メトリクスやVDMを見る限りカウントはされておらずサプレッションリストへの追加もありませんでした(送信前でAWS側で止めているタイプの送信のため?)。
ただ反映遅延等もあるかと思いますのでこのカウントについては試行直後に見た限りはそうだったほどで捉えていただければと思います。
リストに含まれない対象への送信
連絡先リストにない受信者の E メールアドレスへの SendEmail リクエストに ListManagementOptions を含めると、連絡先がリストに自動的に作成されます。
上記のように連絡先リストにない連絡先を対象としてメールを送ると特に拒否されるということはなく自動的に管理下に入るようです。
登録を行っていない私の個人のgmail宛に送ってみます。
届きました。
連絡先リストを確認すると送信先に追加されていました。
... { "EmailAddress": "gmail@example.com", "TopicPreferences": [ { "TopicName": "recommend-item", "SubscriptionStatus": "OPT_IN" } ], "TopicDefaultPreferences": [ { "TopicName": "other-news", "SubscriptionStatus": "OPT_IN" } ], "UnsubscribeAll": false, "LastUpdatedTimestamp": "2023-12-22T14:00:53.058000+09:00" } ....
異なるシステム間の時刻がどれほど差異があるかわかりませんが、がDateヘッダーの値はFri, 22 Dec 2023 05:00:53 +0000
、Receivedヘッダ値のうち日付の部分がThu, 21 Dec 2023 21:00:55 -0800 (PST)
となっているため登録処理と並行もしくは直前あたりに処理を入れていそうな気がします。
終わりに
今回はAmazon SESの機能を利用しての購読管理を試してみました。
アプリ側で実装しようと思う場合は仕組みや画面諸々必要なためここがヘッダー一つ、プレースホルダーテキストひとつで良いので機能自体はかなり良いのではないでしょうか。
ただし現状購読状態を確認するような画面がないのでその管理をコマンドで行うか自前で管理画面を作る必要があることや、AWS側より提供されるインターフェースの言語が英語となっているのでもう一歩あれば!という機能かもしれません。
(ユーザ側で見える購読管理部分もトピックの説明文自体は日本語で登録できるかと思いますがヘッダー部分等が...)