このレシピは配線済みの @event/mailer(recipe add-mailer)と @event/leads(recipe add-leads)を土台に、 自社の連絡先へニュースレター(一斉配信)を特定電子メール法に沿って送る運用フロー を通しで示すランブック。 パッケージを組む手順ではなく、「連絡先の入口 → セグメント → キャンペーン → 配信解除 → 計測」を実際に回す運用が対象。 対象 app を apps/<app> とする(ZFILMS 本番は zfilms-lp)。
設計の芯: 送信の可否は常に 同意(consent)と suppression で決まる。氏名・メール等の PII は AES-256-GCM で暗号化して保存し、平文カラムを持たない。配信解除は取り消せない一方向操作として、 ハッシュ(blind index)だけを残して本文(メールアドレス)は crypto-shred する。
1. 前提と法令(特定電子メール法)
広告・宣伝を含むメール(ニュースレターはこれに当たる)は 原則オプトイン(事前同意)が必要。 ただし例外がある — Web サイト等で自ら公表している企業アドレス(かつ「広告メール受信拒否」の表示が無い)宛には、 事前同意なしで送れる(特電法 施行規則の公表アドレス例外)。この例外に乗る場合でも、次の3点は 常に必須 である。
| 要件 | 内容 |
|---|---|
| ① 送信者表示 | 送信者の氏名・名称と、問い合わせを受け付ける連絡先(メール / URL)を本文に明記する |
| ② オプトアウトの常設 | 配信解除(受信拒否)の手段を全メールに常設し、解除後は送らない |
| ③ 収集元の記録 | いつ・どこで取得したアドレスかを記録し、求めに応じて示せるようにする |
個人アドレス・フリーメール(gmail.com 等)は公表アドレス例外の対象外なので、原則オプトインが要る。 emailDomain がフリーメールなら「公開フォームからの自発同意(basis=form_opt_in)」がある連絡先だけを 送信対象にする、という運用で切り分ける(→ 3. セグメント)。
PII の保存: 氏名・メール・会社等は AES-256-GCM で暗号化して保存し、平文カラムを持たない。 検索・重複判定・suppression 照合は本文ではなく 決定的ハッシュ(blind index) で行う。これにより、 配信解除時に暗号文を破棄(crypto-shred)してもハッシュだけは残せ、「解除済みアドレスの再取込」を後段で弾ける(→ 5. 配信解除)。
2. 連絡先の入口(3系統)
送信対象の連絡先は3経路から入るが、出口(suppression / 配信解除)は全経路で共通 にする。 どの経路で入っても、解除済み・バウンス済みのアドレスには二度と送らない。
(a) 公開フォーム(自発オプトイン) — recipe add-leads で 配線済みの /contact。ここからの登録は本人の自発同意なので basis=form_opt_in として記録する。 個人アドレスでも送信できる、最も強い同意根拠。
(b) 過去問い合わせの取込 — Webflow / Framer で受けた過去の問い合わせを CSV エクスポートし、取込スクリプトで 連絡先へ変換する。取込時に 収集元(source)と同意根拠(basis)を必ず刻む。
# DRY-RUN(既定・書き込まない): 差分と検出カラムだけ表示
node scripts/import-inquiries.mjs ./exports/webflow-inquiries.csv --source webflow
# 実適用: --apply で連絡先を upsert(source=inquiry / basis=past_inquiry を記録)
node scripts/import-inquiries.mjs ./exports/webflow-inquiries.csv --source webflow --apply
node scripts/import-inquiries.mjs ./exports/framer-contact.csv --source framer --apply
--source は webflow / framer を取り、各エクスポートの列名差を吸収する。取込レコードは source=inquiry / basis=past_inquiry(過去に自社へ問い合わせた = 取引関係のある連絡先)として consent を記録する。 取込でも既存の suppression とは常に照合し、解除済みは復活させない。
(c) Web 収集リード(プロスペクティング) — 公開情報から収集した企業アドレス。これは別レシピ collect-web-leads(準備中)で扱う。 収集しただけの生リードは送信対象に しない。担当が内容を確認し 承認したものだけ を連絡先へ昇格させ、 公表アドレス例外(1.)の条件を満たすかをここで人が判断する。
3. セグメント(動的絞り込み)
セグメントは連絡先を 動的に絞り込む保存済みクエリ で、送信のたびに評価される(送信時点の母集団を毎回数え直す)。 絞り込みキーは4つ。
| キー | 例 | 用途 |
|---|---|---|
status | subscribed / unsubscribed / bounced | 送信可能な状態か |
source | form_opt_in / inquiry / prospecting | 入口別(同意の強さ)で分ける |
tags | ivs2026 / vip | 手動 or 取込時に付与したラベル |
emailDomain | gmail.com / 4s.link | フリーメール(要オプトイン)と企業ドメインの切り分け |
システム定義の既定セグメントを2つ持つ。
- 全購読者 —
status=subscribed(解除・バウンスを除いた送信可能な全員)。 - フォーム購読 —
status=subscribedかつsource=form_opt_in(自発同意のみ。フリーメール宛でも最も安全に送れる母集団)。
フリーメール宛に送るときは「フォーム購読」を基点にし、企業ドメインの公表アドレス(例外適用)は emailDomain で別セグメントに切って条件を分ける。
4. キャンペーン作成 → プレビュー → テスト送信 → 送信 / 予約
キャンペーンは admin から作る。エントリは2つ — 一覧の /admin/email/campaigns、または各画面から開く作成モーダル。 作成から送信までは次の順で進む。
- 作成 — 件名・本文(テンプレ)・対象セグメントを選ぶ。差出人は
@event/mailerのMAIL_FROM(検証済みドメイン)を使う(recipe add-mailer)。 - プレビュー — 本文は
iframeにsrcDocで隔離描画し、実際の受信 HTML を確認する。 - セグメント人数の live count — 対象セグメントの現在の該当人数をその場で数えて出す(送信規模の最終確認。0 件や桁違いをここで気づく)。
- テスト送信 — 自分のアドレスに1通だけ実送信し、差出人・件名・解除リンク・レンダリングを実機確認する。
- 送信 / 予約 — 「今すぐ」か「日時指定」を選ぶ。どちらも即時には飛ばさず、キャンペーンを
queued(orscheduled)にして キューに積むだけ。
実送信はキュー + cron。毎分走る dispatch cron が scheduled の到来分と queued を拾い、Resend の batch 送信(1 リクエスト最大 100 通) に分割して送る。UI の送信操作と実配信を分離することで、 大量送信でもリクエストがタイムアウトせず、失敗分だけ再送できる。
// vercel.json — 毎分 dispatch(送信キューを掃く)
{
"crons": [{ "path": "/api/cron/email/dispatch", "schedule": "* * * * *" }]
}
cron は毎回 suppression を再照合してから送る(キュー投入後に解除された宛先を除外する)。
5. 配信解除(unsubscribe)
解除は 2経路 で常設する。どちらも即時・確認画面なしで反映する(ワンクリック解除)。
- RFC 8058 List-Unsubscribe(メールクライアントの解除ボタン) — 全送信メールに次のヘッダを付ける。Gmail / Apple Mail 等はこれを読んで受信トレイ上に「配信停止」ボタンを出し、POST 一発で解除できる。
List-Unsubscribe: <https://<app>/api/email/unsubscribe?t=TOKEN>, <mailto:unsubscribe@<domain>?subject=unsubscribe>
List-Unsubscribe-Post: List-Unsubscribe=One-Click
- 本文リンク — 全メール本文フッタにも解除リンクを常設する(① 送信者表示と並ぶ特電法の必須要件)。
解除すると連絡先は status=unsubscribed になり全セグメントから外れる。このとき suppression にはハッシュ(blind index)だけを残し、 暗号化された本文は破棄(crypto-shred) する。ハッシュを残すのは、後で 2(b) / 2(c) のどの経路から同じアドレスが 再取込されても 入口で照合して弾く ため。「一度解除した人を名簿の入れ替えで復活させない」を保証する仕組みである。
バウンス・苦情の自動 suppression — Resend の webhook(email.bounced / email.complained)を受けたら、 その宛先を自動で suppression に入れて status=bounced にする。ハードバウンスや「迷惑メール報告」への再送は 送達レピュテーションを直接痛めるので、人手を介さず即時に止める。
6. 計測(webhook 集計)
配信の質は Resend の webhook を集計して見る。dispatch した各メールに紐づくイベントを受けて、 キャンペーン単位で開封・クリック・バウンス・苦情を積み上げる。
| webhook イベント | 集計 | 用途 |
|---|---|---|
email.delivered | 到達 | 分母(送達できた数) |
email.opened | 開封 | 件名・差出人の当たり判定 |
email.clicked | クリック | 本文 CTA の効き |
email.bounced | バウンス | 名簿の劣化(→ 自動 suppression) |
email.complained | 苦情 | 迷惑メール報告(→ 自動 suppression) |
webhook 受け口は署名検証(Resend の署名ヘッダ)を通してから記録する。開封・クリックはキャンペーンのレポートに、 バウンス・苦情は 5. の自動 suppression にそれぞれ流れる。
7. 運用チェックリスト
送信前(テスト送信の直後・本送信の直前に毎回):
- 送信者表示 — 本文フッタに送信者の名称と連絡先があるか(特電法 ①)。
- 解除リンク — 本文の解除リンクと List-Unsubscribe ヘッダの両方が生きているか(特電法 ②)。
- セグメント人数 — live count が想定どおりか(0 件・桁違いは中止のサイン)。
- テスト自己受信 — 自分宛のテスト送信が実際に届き、差出人・件名・レンダリングが崩れていないか。
送信後(配信開始〜数時間・翌日):
- バウンス率 — 高止まりなら名簿が古い。取込元(2b / 2c)の質を疑い、以後の対象を絞る。
- 苦情率 — 上がるなら同意の弱い母集団(
source=inquiry/prospecting)へ送りすぎ。フォーム購読中心に戻す。
一般に バウンス率・苦情率が上がると送達レピュテーションが落ち、正当なメールも迷惑メール送りになる。 数値が悪化したら送信を止め、セグメントを source=form_opt_in(自発同意)中心に絞り直すのが復帰の定石。
関連レシピ
- add-mailer — 送信基盤(
@event/mailer/ Resend / テンプレ)の配線。本レシピの前提。 - add-leads — 公開フォーム
/contact(入口 2a)の配線。 - collect-web-leads — Web 収集リード(入口 2c / プロスペクティング)。準備中。承認済みのみ連絡先へ昇格させる。