# SRS — メール配信システム（Resend + Cloudflare）

IEEE 29148 / ISO 25010 準拠 · v1.0 · 2026-06-18 · 状態: Approved

---

## 1. はじめに
### 1.1 目的
非エンジニアでも、白基調のシンプルなダッシュボードから、一斉配信とステップメールを安全に管理できるメール配信システムを構築する。外部ツールから登録イベントを受けて配信を自動予約する連結機能を持つ。

### 1.2 スコープ
- **IN**: 購読者/リスト管理、一斉配信（下書き/予約/承認/送信/キャンセル）、ステップメール、外部連携（Webhook受信＋埋め込みフォーム）、配信停止、バウンス処理、ダッシュボード、安全装置（DRY_RUN/承認/冪等/上限）。
- **OUT**: LP/決済/会員機能（UTAGE的オールインワンは対象外）、A/Bテスト、複雑な分岐シナリオ。
- **DEFER**: 開封/クリック高度分析、複数管理者、ダブルオプトイン、Cloudflare Queues大量配信。

### 1.3 成功指標（数値）
- 予約配信が指定時刻±2分以内に開始される（OBJ-001）。
- 同一購読者への二重送信 0 件（再実行・障害復旧時も）（OBJ-002）。
- DRY_RUN 既定で、未承認の実送信が 0 件（OBJ-003）。
- 全配信に配信停止リンク・送信者表示が 100% 付与（OBJ-004 法令）。

## 2. ステークホルダー / 権限
- USR-001 管理者（自分1人）: 全機能。パスワード認証必須。
- USR-002 購読者（外部）: 登録・配信停止のみ（認証不要、トークン識別）。
- USR-003 外部ツール: Webhook/フォーム経由で登録イベント送信（トークン認証）。

## 3. 運用サイクル
リスト作成 → 登録フォーム/Webhook発行 → 外部ツールから登録 → 購読者がリスト/ステップに追加 → ステップメール自動進行 or 一斉配信を予約 → 承認 → スケジューラ送信 → 結果/バウンスをダッシュボードで確認。

## 4. 外部インターフェース
- API-RESEND-001: Resend `POST /emails/batch`（送信）。Idempotency-Key付与。
- API-RESEND-002: Resend Webhook 受信（bounced/complained/delivered）→ 購読者状態更新。
- API-CF-001: D1 binding（永続化）。
- API-CF-002: Cron Triggers（毎分スケジューラ）。

---

## 5. 機能要件（FR）

各 Must 要件は 受入基準(ACC) と テストケース(TC) と リスク(RISK) に接続。

### 認証 AUTH
- **FR-AUTH-001 [Must]** 管理画面の全API/ページは有効セッションを要求し、未認証は 401/ログインへ。
  - 根拠: USR-001 / 避けたい失敗（不正操作）。AC: ACC-FR-AUTH-001 / TC-FR-AUTH-001 / RISK-005
- **FR-AUTH-002 [Must]** 正しいパスワードでログイン時、HMAC署名付きHttpOnly Cookieを発行。誤りは拒否（定数時間比較）。
  - AC: ACC-FR-AUTH-002 / TC-FR-AUTH-002 / RISK-005

### 購読者/リスト SUB
- **FR-SUB-001 [Must]** 購読者を作成/一覧/状態変更（active/unsubscribed/bounced）できる。email は正規化(小文字)し一意。
  - AC: ACC-FR-SUB-001 / TC-FR-SUB-001 / RISK-002
- **FR-SUB-002 [Must]** 同一emailの重複登録は既存を更新し新規行を作らない（冪等）。
  - AC: ACC-FR-SUB-002 / TC-FR-SUB-002 / RISK-002
- **FR-SUB-003 [Must]** リスト(グループ)を作成し、購読者を所属/解除できる。
  - AC: ACC-FR-SUB-003 / TC-FR-SUB-003 / RISK-002
- **FR-SUB-004 [Should]** CSVは扱わず手動/連携登録のみ（スコープ簡素化）。AC: N/A

### 一斉配信 BC
- **FR-BC-001 [Must]** 配信(件名/HTML本文/対象リスト)を下書き作成できる。
  - AC: ACC-FR-BC-001 / TC-FR-BC-001 / RISK-001
- **FR-BC-002 [Must]** 配信に送信日時を設定し予約(scheduled)できる。即時送信も予約(now)として扱う。
  - AC: ACC-FR-BC-002 / TC-FR-BC-002 / RISK-003
- **FR-BC-003 [Must]** 実送信は「approved=true かつ DRY_RUN=false」のときのみ。未承認/ DRY_RUN は送信せず記録のみ（誤送信防止）。
  - AC: ACC-FR-BC-003 / TC-FR-BC-003 / RISK-001,004
- **FR-BC-004 [Must]** スケジューラは scheduled かつ scheduled_at<=now の配信を、楽観ロックで1度だけ sending に遷移させ送信する（二重送信防止）。
  - AC: ACC-FR-BC-004 / TC-FR-BC-004 / RISK-002
- **FR-BC-005 [Must]** 配信×購読者は一意。再実行時も未送信分のみ送る（冪等・再開可能）。
  - AC: ACC-FR-BC-005 / TC-FR-BC-005 / RISK-002
- **FR-BC-006 [Must]** sending到達前(status=scheduled)に限り配信をキャンセルできる。以降は不可。
  - AC: ACC-FR-BC-006 / TC-FR-BC-006 / RISK-006
- **FR-BC-007 [Must]** unsubscribed/bounced の購読者は送信対象から除外する。
  - AC: ACC-FR-BC-007 / TC-FR-BC-007 / RISK-007

### ステップメール STEP
- **FR-STEP-001 [Must]** ステップキャンペーン(対象リスト/トリガー)とステップ(順序/遅延分/件名/本文)を定義できる。
  - AC: ACC-FR-STEP-001 / TC-FR-STEP-001 / RISK-001
- **FR-STEP-002 [Must]** 購読者をキャンペーンに登録(enroll)すると、各ステップが遅延に従い順次送信される。
  - AC: ACC-FR-STEP-002 / TC-FR-STEP-002 / RISK-003
- **FR-STEP-003 [Must]** enrollment×ステップは一意で、各ステップは1回のみ送信（二重送信防止）。
  - AC: ACC-FR-STEP-003 / TC-FR-STEP-003 / RISK-002
- **FR-STEP-004 [Must]** 登録済みの購読者を重複enrollしない（冪等）。配信停止者は進行停止。
  - AC: ACC-FR-STEP-004 / TC-FR-STEP-004 / RISK-002,007

### 外部連携 HOOK
- **FR-HOOK-001 [Must]** リスト/キャンペーンに紐づくトークン付き受信URL(Webhook)を発行。`POST /api/hooks/:token {email,name?}` で購読者を登録し、リスト追加＋（紐付くキャンペーンがあれば）enroll する。
  - AC: ACC-FR-HOOK-001 / TC-FR-HOOK-001 / RISK-002,005
- **FR-HOOK-002 [Must]** 同トークンで埋め込み可能な登録フォーム `GET /f/:token` を提供し、送信で FR-HOOK-001 と同じ処理を行う（①②両対応）。
  - AC: ACC-FR-HOOK-002 / TC-FR-HOOK-002 / RISK-005
- **FR-HOOK-003 [Must]** 無効/不明トークンは 404、emailバリデーション失敗は 400。
  - AC: ACC-FR-HOOK-003 / TC-FR-HOOK-003 / RISK-005

### 配信停止/到達率 DELIV
- **FR-DELIV-001 [Must]** 全配信本文に配信停止リンク `/u/:token` と送信者表示フッターを自動付与（特定電子メール法）。
  - AC: ACC-FR-DELIV-001 / TC-FR-DELIV-001 / RISK-008
- **FR-DELIV-002 [Must]** `/u/:token` アクセスで該当購読者を unsubscribed にする（以降送信対象外）。
  - AC: ACC-FR-DELIV-002 / TC-FR-DELIV-002 / RISK-007
- **FR-DELIV-003 [Must]** Resend Webhook受信で bounced/complained を購読者状態へ反映（到達率保護）。署名検証。
  - AC: ACC-FR-DELIV-003 / TC-FR-DELIV-003 / RISK-007

### 安全/上限 SAFE
- **FR-SAFE-001 [Must]** 1ティック送信上限 `MAX_SENDS_PER_TICK`、日次上限 `MAX_SENDS_PER_DAY` を超えない（費用/レート暴走防止）。
  - AC: ACC-FR-SAFE-001 / TC-FR-SAFE-001 / RISK-004
- **FR-SAFE-002 [Must]** Resend送信に Idempotency-Key を付与する（API側二重送信防止）。
  - AC: ACC-FR-SAFE-002 / TC-FR-SAFE-002 / RISK-002

### ダッシュボード UI
- **FR-UI-001 [Must]** 白基調のダッシュボードで、購読者数・配信状況・直近送信を一覧表示。空状態は「まだデータがありません」と案内。
  - AC: ACC-FR-UI-001 / TC-FR-UI-001 / RISK-009
- **FR-UI-002 [Must]** 配信作成/予約/承認/キャンセル、ステップ作成、購読者/リスト/連携リンク管理をUIから操作できる。
  - AC: ACC-FR-UI-002 / TC-FR-UI-002 / RISK-009

---

## 6. 受入基準（ACC 抜粋 / Given-When-Then）
- ACC-FR-BC-004: Given scheduled配信が時刻到来, When scheduledハンドラを2回連続実行, Then sending遷移と送信は1回のみ。
- ACC-FR-BC-005: Given 3件中1件送信済みで中断, When 再実行, Then 残り2件のみ送信され既送信は再送されない。
- ACC-FR-BC-003: Given DRY_RUN=true, When 配信実行, Then Resend未呼出でsendsはskipped/recorded、ステータスはsent扱い(dry)。
- ACC-FR-SUB-002: Given email=a@x（既存）, When 同emailで登録, Then 行数不変・既存更新。
- ACC-FR-STEP-003: Given enrollがstep1送信済, When 再実行, Then step1再送なし、遅延満了でstep2送信。
- ACC-FR-DELIV-001: When 任意配信本文を生成, Then 末尾に `/u/<token>` と送信者表示文字列を含む。
- ACC-FR-HOOK-001: Given 有効token, When POST {email}, Then 購読者作成＋list所属＋(紐付campaignあれば)enroll、200。
- ACC-FR-SAFE-001: Given 未送信150件, MAX_SENDS_PER_TICK=100, When 1ティック, Then 送信100件・残50は次ティック。
（全ACCは TEST_PLAN.md のTC定義に1:1接続）

## 7. データ（概要 / 詳細は SDD）
DATA-001 subscribers, DATA-002 lists, DATA-003 list_subscribers, DATA-004 broadcasts, DATA-005 broadcast_sends, DATA-006 step_campaigns, DATA-007 step_emails, DATA-008 step_enrollments, DATA-009 step_sends, DATA-010 hooks, DATA-011 audit_log, DATA-012 settings.

## 8. 非機能要件（ISO 25010）
- NFR-PERF-001 [Must] 100件/ティックの送信処理が Cron 実行枠内で完了。TC-NFR-PERF-001
- NFR-REL-001 [Must] スケジューラ多重起動でも二重送信なし（冪等）。TC-NFR-REL-001
- NFR-SEC-001 [Must] 管理APIは認証必須、秘密情報はenv、Cookieは HttpOnly+Secure。TC-NFR-SEC-001
- NFR-MAINT-001 [Should] 中核ロジックは Store/Mailer 抽象で純粋関数化しテスト可能。TC-NFR-MAINT-001
- NFR-USE-001 [Should] 非エンジニアが説明なしで配信作成→予約まで到達できるUI。
- NFR-COMPAT-001 [Must] CFアカウント無しで `npm test`/`npm run build` が緑（InMemoryStore）。TC-NFR-COMPAT-001

## 9. セキュリティ/監査/法令
- SEC-001 トークンは推測困難(crypto乱数)。SEC-002 Resend Webhook署名検証。
- OPS-001 全送信/状態変更を audit_log に記録。
- 法令: 特定電子メール法（配信停止・送信者表示・オプトイン前提）。

## 10. 制約
- 指定技術: Resend + Cloudflare のみ（他送信基盤を導入しない）。
- 依存最小・ビルド不要のUI。破壊操作は既定で安全側。

## 11. トレーサビリティ
要件↔AC↔TC↔RISK は本SRSと TEST_PLAN.md のマトリクスで双方向管理。Must要件は全て TC 接続済（GO条件）。

## 12. リスク台帳（抜粋）
| ID | リスク | 対策 |
|---|---|---|
| RISK-001 | 誤送信（誤った内容/対象） | 承認ゲート+プレビュー+DRY_RUN |
| RISK-002 | 二重送信 | 一意制約+楽観ロック+Idempotency-Key |
| RISK-003 | ドタキャン/遅延 | 毎分Cron+楽観ロック+キャンセルはscheduled限定 |
| RISK-004 | 費用/レート超過 | ティック/日次上限 |
| RISK-005 | 不正アクセス | 認証+トークン+署名検証 |
| RISK-007 | 到達率低下 | バウンス/苦情で除外+配信停止 |
| RISK-008 | 法令違反 | 配信停止リンク+送信者表示の自動付与 |
