mail

メール送信者がone-click unsubscriptionを実装する方式を検討します

mail

訂正

  • 2024-01-31: 指定するメールヘッダの用法を訂正しました。訂正以前はList-Unsubscribe-Post ヘッダにエンドポイントのURLを記載するとしていました。正しくはList-Unsubscribe ヘッダにエンドポイントのURLを記載します。また、List-Unsubscribe-Post ヘッダには List-Unsubscribe=One-Click と記載します。

はじめに

こんにちは、新卒 13th の@donokunです。12 月は SUZURI 事業部に参加して、いちごジャムとたまごペーストがトーストにあうことを学びました。騙されたと思って喫茶店のモーニングで試してください。ジャムが塗ってあるトーストにたまごペーストを上から塗って食べるのです。

この記事ではワンクリックでの配信停止(one-click unsubscription)という E メール送信者ガイドライン1でメール送信者に求められている機能を実装した話を紹介します。普段 Web アプリケーションフレームワークに任せていたセッションについて考える機会になりました。

ワンクリックでの配信停止のモチベーションやそれぞれのロールの責務などは RFC8058 で定められており、よく説明されています。必要に応じて説明するのであらかじめ理解しておく必要はありません。

  1. 訂正
  2. はじめに
  3. 背景: ワンクリックでの配信停止とその目的
    1. 配信停止のプロトコル
  4. 送信者が行う実装
    1. 本題: メールヘッダに設定する URL の設計
  5. URL の設計と送信者の実装
    1. 暗号化する方式
    2. ランダムな識別子を使う方式
    3. 注意点
    4. SUZURI での判断
  6. 終わりに

背景: ワンクリックでの配信停止とその目的

ワンクリックでの配信停止 (one-click unsubscription) はRFC8058で提案されている機能で、メールの受信者がメールクライアント上での操作だけでメールの配信を停止できるようにします。配信停止を簡単にすることで、迷惑メールと報告されにくくなることが期待できます。

反対に、ワンクリックではない配信停止は、通知の設定画面のリンクをメールに貼るといったパターンです。通知設定画面を開くためには、アプリケーションへのサインインが必要で、そのためにはパスワードを思い出さないといけません。大変です。しばらく使っていないサービスであればパスワードを思い出すのを諦めて迷惑メールに設定したくなることでしょう。

配信停止のプロトコル

実装の全体像を把握するために、RFC8058 で定められた配信停止の流れを抑えておきましょう。先ほど少し触れたように、ワンクリックでの配信停止の仕組みは RFC8058 として共有されており、メール送信者が提供するべき機能も定められています。

登場人物は 3 つに分かれます。メール送信者(今回は私たち SUZURI 事業部、図では Sender と書きます)、メールクライアント(Thunderbird や Gmail、Yahoo Mail のクライアントなど、図では Client)、そしてメール受信者(SUZURI のユーザー、図では Receiver)です。

ワンクリックでの配信停止のシーケンス図

ワンクリックでの配信停止のシーケンス図

図に示したように、ワンクリックでの配信停止は以下の流れで実現されます。

  1. まず送信者がメールを作成します。このときにメールヘッダ(List-Unsubscribe ヘッダ)に配信停止リクエスト URL を記述します。この URL はステップ 5 で使います。
  2. メールを届けます。ユーザのメールクライアントで閲覧できるようになるところまでがこのステップに含まれると考えてください。
  3. 受信者がメールを開いたとき、クライアントは配信停止の UI を表示します。
  4. 受信者は配信停止したければ、メールクライアント上でその操作をします。
  5. クライアントは List-Unsubscribe ヘッダに記述されている URL に POST リクエストを送ります。
  6. 送信者は POST リクエストを受け取り、配信停止の処理を行います。

以上がワンクリックでの配信停止の流れです。

追記: 2024-01-31 記事の内容に誤りがあったため修正しました。詳細は訂正を参照してください。

送信者が行う実装

送信者が実装する箇所は、先ほどの図の 1. メールの作成と、6. 配信停止リクエストの処理です。つまり配信停止用の URL を発行してメールヘッダに埋め込む処理と、その URL へのリクエストを受け付ける処理です。

ワンクリックでの配信停止で送信者が実装する箇所

ワンクリックでの配信停止で送信者が実装する箇所

本題: メールヘッダに設定する URL の設計

さて、この記事の本題に入ります。送信者の実装の中で特に面白いのは URL の設計です。RFC8058 の 3.1 節に送信者が実装する機能の要件がまとめられており、その中で URL の設計についても言及されています。ざっとまとめると以下のような要件があります。

  • URL にはユーザを識別する情報と配信停止するメールの種類を含めること
    • それらは opaque なものであること(偽造や意味の推測をできないこと)
  • HTTP リクエストには Cookie などの情報は含まれないことに留意すること

要件で定められている事項のほかに、URL を変更しにくいことを留意しておくと良いでしょう。Web ページと異なりメールはリロードするものではなく、一度配信した文面は長くユーザの手元に残るでしょう。そのため配信する URL を変更しても、過去に配信した URL が長く残り続けることになります。

URL の設計と送信者の実装

URL にどんな情報をどのように持たせて、それをどのようにサーバで解釈するかが論点です。私たちの中では二つの候補が残りました。

  • 暗号化する方式: ユーザ ID と配信停止対象の種別を辞書で表現し、シリアライズしたものを暗号化して URL に含める
    • 例: https://example.com/unsubscription/<暗号化した辞書>
  • ランダムな識別子を使う方式: ユーザに固有のランダムな識別子を DB に保存しておき、それと配信停止対象の種別を URL に含める
    • 例: https://example.com/unsubscription/<ランダムな識別子>?type=<配信停止対象の種別> DB にランダムな識別子とユーザ ID の対応を保存しておく

最終的にはランダムな識別子を使う方式を採用しましたが、状況によっては暗号化を用いる方法も有効でしょう。詳細を述べます。

暗号化する方式

stack overflowで紹介されている方法です。リクエストに含めたい情報を辞書として表現し、それを暗号化した上でクエリパラメータやパスの一部として URL に含めます。POST リクエストの処理ではその情報を復号して対象ユーザの該当する配信を停止します。この方法のメリットは DB が不要なことです。

...
attrs = {
  user_id: user.id,
  type: 'weekly_report'
}
crypt_attrs = Rails.application.message_verifier(salt).generate(attrs)
url = "https://example.com/unsubscription/#{crypt_attrs}"
headers['List-Unsubscribe'] = "<#{url}>"
headers['List-Unsubscribe-Post'] = 'List-Unsubscribe=One-Click'
...

ランダムな識別子を使う方式

ユーザを識別するためのランダムな識別子を DB に保存しておき、ユーザID の代わりに URL に含めます。DB が必要になることがデメリットですが、URL の生成に暗号化が不要になるのがメリットです。その他にもユーザとランダムな識別子の対応はDBで管理するため、機密情報が復号される心配をしないで済むメリットがあります。

# DBに保存してあるランダムな識別子(user.unsubscription_token)を使う
url = "https://example.com/unsubscription/#{user.unsubscription_token}?type=weekly_report"
headers['List-Unsubscribe'] = "<#{url}>"
headers['List-Unsubscribe-Post'] = 'List-Unsubscribe=One-Click'

追記: 2024-01-31 記事の内容に誤りがあったため修正しました。詳細は訂正を参照してください。

注意点

どちらの方式を採用するにしてもURLセーフな文字種を使う必要があることに注意してください。暗号化した辞書やランダムな識別子はURLに含めるためです。

Railsで実装する場合、暗号化する方式ではMessageVerifierの初期化時にオプションを設定することでURLセーフな文字種を使えるようです。ランダムな識別子を使う方式では SecureRandom.urlsafe_base64を使うと良いでしょう。

SUZURI での判断

どちらの実装も RFC8058 で求められている要件を満たします。私たちは運用上の観点からランダムな識別子を使う方式を採用しました。

メール配信の際にBIツール(Metabase)で条件を絞って配信対象を絞り込み、配信先メールアドレスと配信停止URLのリストを生成することがあります。例えばマーケティング施策で、施策に特有な条件を満たすユーザにのみメールを配信することがあります。

BIツールは本番環境のデータベースに接続しておりSQLで参照できますが、アプリケーションのプログラムは使えません。

仮に暗号化する方式を採用した場合、SQLでアプリケーションコードと互換性のある暗号化を実装・メンテナンスする必要が生じます。一方でランダムな識別子を使う方式では、データベースに登録された識別子を用いれば良いので簡単にURLを生成できます。

以上の理由からランダムな識別子を使う方式の方が開発・運用コストが低いと判断し、それを採用しました。

終わりに

One-click unsubscriptionの実装では、アプリケーションフレームワークが提供してくれるセッションのような機能を、要件に合うように設計・実装したことになるかと思っています。普段フレームワークに任せきりで、あまり考えない内容を議論できて楽しかったです。

設計や実装、記事の執筆にあたってチームの先輩エンジニアにたくさん教えていただきました。ありがとうございました!

  1. Google が公開している送信者ガイドライン: https://support.google.com/mail/answer/81126?hl=ja