# 電子報介接備忘錄（租戶端 / Wagtail）

最後更新：2026-02-18

## 1. 目標與前提

本備忘錄用於整理租戶端（Wagtail）接下來的實作方向，重點是先完成可運行流程，再逐步補完細節。

## 2. 已確認決策（本次會議結論）

1. Member Center 雖為共用平台，但目前只有一個租戶。
2. 現階段「發確認信」與「訂閱/退訂頁面」先做在 Wagtail。
3. Wagtail 與 Member Center 的溝通以 API 為主。
4. SMTP relay 僅負責寄出一次，重送、token 安全、簽章等由其他系統或工具負責，不屬 Wagtail 責任。
5. 工作大項與順序固定為：
   1. 準備工作（電子報系統設定）
   2. 訂閱流程
   3. 退訂流程
   4. 電子報 app
   5. 發信流程（排程）

## 3. 工作大項與執行順序（含範圍）

## 3.1 準備工作（電子報系統設定，第一優先）

範圍：
- 建立 Member Center 連線設定（base URL、tenant_id、list_id、API timeout 等）。
- 建立 Send Engine 連線設定（base URL、OAuth scope、API timeout 等）。
- 建立一般發信設定（SMTP relay、寄件者名稱/信箱、reply-to、預設編碼等）。
- 建立模板設定：
  - 訂閱確認信 template
  - 訂閱確認（成功）頁面 template
  - 取消訂閱頁面 template

重點：
- 設定集中管理，避免散落在程式碼或多處後台欄位。
- 區分「系統連線設定」與「內容模板設定」，便於權限與維運。

完成標準（DoD）：
- 可在單一設定入口完成上述設定並通過基本驗證。
- 模板可被後續訂閱/退訂流程直接引用。

## 3.2 訂閱流程（第二優先）

範圍：
- 在站台頁面區塊（暫定 Footer）提供 email 訂閱入口。
- 後端接收 email，呼叫 Member Center 訂閱 API。
- 由 Wagtail 送出訂閱確認信（透過 SMTP relay）。
- 使用者點信內連結回到 Wagtail 確認頁。
- 確認頁自動呼叫 Member Center 確認 API。

重點：
- 流程中 token 需一路帶入與驗證。
- 確認信內容採 HTML（可附純文字 fallback）。

完成標準（DoD）：
- 可從站台成功發起訂閱。
- 可收到確認信並完成確認。
- 確認成功/失敗頁有明確訊息與追蹤記錄。

## 3.3 退訂流程（第三優先）

範圍：
- 電子報底部提供退訂連結，導向 Wagtail 退訂頁。
- 進入退訂頁時，先向 Member Center 申請退訂 token。
- 使用者按確認退訂後，呼叫 Member Center 退訂 API 完成流程。

重點：
- token 不做長期保存。
- 頁面需可處理 token 失效/不存在等錯誤狀態。

完成標準（DoD）：
- 使用者能從信件連結完成退訂。
- 退訂結果頁可正確呈現成功/失敗狀態。

## 3.4 電子報 app（第四優先）

範圍：
- 在 Wagtail 後台提供電子報內容管理能力。
- 提供 HTML 編輯器建立/編修內容。
- 支援可替換參數（例如 token、email）。

重點：
- 編輯器中的連結參數以佔位符表示，發送前由 backend 替換。
- 若有 Member Center API 相關連結需在內容中可配置，也採相同參數機制。

完成標準（DoD）：
- 編輯器可存草稿與更新內容。
- 內容中參數可在送出前正確替換。

## 3.5 發信流程（排程，第五優先）

範圍：
- 可建立發送任務與排程時間。
- 排程前可持續編輯內容與調整時間。
- 到時觸發時：先取 Member Center auth，再將內容包固定 header/footer，送到 Send Engine。

重點：
- 「內容定稿」與「實際送出」拆開。
- 發送前的模板組裝由 Wagtail backend 負責。

完成標準（DoD）：
- 排程到點可成功建立 Send Engine send job。
- 失敗可記錄原因並可人工重試。

## 4. 參數替換規則（先行約定）

建議佔位符格式（待實作時可再定版）：
- `{{token}}`
- `{{email}}`
- `{{list_id}}`
- `{{tenant_id}}`
- `{{confirm_url}}`
- `{{unsubscribe_url}}`

替換時機：
- 發信前 backend 最後一步統一替換。

注意事項：
- URL 參數需做 URL encode。
- 缺少必要參數時，中止發送並記錄可追蹤錯誤。

## 5. URL 與系統歸屬標註規範（文件撰寫規則）

後續文件凡提到網址，必須明確標註「是哪個系統」：

1. Wagtail（租戶站台）網址
- 例：`https://{tenant-site}/newsletter/confirm?token=...`
- 用途：使用者互動頁、站台前台/後台頁面。

2. Member Center API 網址
- 例：`https://{member-center}/newsletter/subscribe`
- 用途：訂閱、確認、退訂 token、退訂等 API。

3. Send Engine API 網址
- 例：`https://{send-engine}/api/send-jobs`
- 用途：建立/查詢/取消發信任務。

4. SMTP Relay 連線端點
- 用途：Wagtail 寄送確認信。
- 備註：僅作為寄送通道，不承擔重送與安全機制。

## 6. 非 Wagtail 責任邊界（本階段）

以下不納入目前 Wagtail 工作範圍：
- SMTP 後續重送策略。
- token 簽章與高階安全策略。
- 跨系統風控與防濫用機制。

## 7. 下一步（實作啟動清單）

1. 先完成電子報系統設定（Member Center / Send Engine / SMTP / 模板）。
2. 再完成訂閱流程 API 串接與確認頁。
3. 再完成退訂頁與退訂 API 串接。
4. 建立電子報 app 與 HTML 編輯器資料模型。
5. 最後接排程任務與 Send Engine 發信。

## 8. Member Center API 實際規格（以 member_center 程式碼 / OpenAPI 為準）

資料來源（2026-02-17 比對）：
- `../member_center/src/MemberCenter.Api/Controllers/NewsletterController.cs`
- `../member_center/docs/openapi.yaml`

### 8.1 Base URL 與路徑

- OpenAPI `servers.url` 為 `/api`，部署時實際呼叫通常為：
  - `https://{member-center}/api/newsletter/...`
- 若部署已在 gateway 做 path rewrite，也可為：
  - `https://{member-center}/newsletter/...`
- 租戶端必須以實際部署路徑設定 `member_center_base_url`。

### 8.2 Endpoint / Method / Request

1. 訂閱
- `POST /newsletter/subscribe`
- JSON body（必要）：
  - `list_id`（Guid）
  - `email`（string）
- JSON body（可選）：
  - `preferences`（object）
  - `source`（string）
- 回傳包含 `confirm_token`

2. 訂閱確認（雙重驗證）
- `GET /newsletter/confirm?token=...`
- 注意：**confirm 是 GET，不是 POST**

3. 單一名單退訂
- `POST /newsletter/unsubscribe`
- JSON body（必要）：
  - `token`（string）

4. 申請退訂 token
- `POST /newsletter/unsubscribe-token`
- JSON body（必要）：
  - `list_id`（Guid）
  - `email`（string）
- 回傳：
  - `unsubscribe_token`（string）

### 8.3 與租戶端目前實作對齊結果

- `subscribe`：已使用 `POST`。
- `confirm`：已改為 `GET + query token`（修正 HTTP 405 問題）。
- `unsubscribe`：已使用 `POST`，且 body 僅送 `token`。
- `unsubscribe-token`：已使用 `POST`，且 body 送 `list_id + email`。

## 9. List-Unsubscribe One-Click 規範（發信流程階段導入）

說明：
- 本節為「電子報 app + 排程發信」階段的目標規範。
- 已完成的訂閱/退訂流程維持現況，不回頭重做；待發信流程上線時再整合 one-click。

### 9.1 目標

- 提供電子報編輯與排程。
- 產生 `List-Unsubscribe` one-click header。
- 呼叫發信代理 API。
- 提供 unsubscribe endpoint（one-click + 人類點擊頁）。

### 9.2 Unsubscribe Token 設計

建議使用 JWT 或 HMAC token。

Token 內容建議：
- `subscriber_id`
- `list_id`
- `site_id`
- `exp`
- `nonce`（optional）

安全要求：
- 不需登入即可退訂。
- 必須可 idempotent。
- Token 需有過期時間。
- Token 必須簽章驗證。

### 9.3 Unsubscribe Endpoint

1. One-click endpoint
- `POST /u/unsubscribe`
- Request：`token`
- 流程：
  - 驗證 token
  - 呼叫會員平台 API
  - 回傳 `200`
- 不可：
  - 要求登入
  - 要求二次確認

2. 人類點擊頁面
- `GET /u/unsubscribe?token=xxx`
- 流程：
  - 驗證 token
  - 顯示退訂成功頁面

### 9.4 發信 Header 規則

Newsletter 必須包含：
- `List-Unsubscribe: <https://domain/u/unsubscribe?token=xxx>`
- `List-Unsubscribe-Post: List-Unsubscribe=One-Click`

註記：
- Transactional email 不應包含 List-Unsubscribe。

### 9.5 與會員平台整合（one-click 目標介面）

呼叫 API：
- `POST /api/subscriptions/unsubscribe`

Request：
- `subscriber_id`
- `list_id`
- `source: "one_click"`
- `campaign_id`

回傳：
- `success`
- `already_unsubscribed`

### 9.6 測試案例

1. Token 驗證
- 正常 token -> 成功退訂
- 過期 token -> `400` 或 `410`
- 已退訂 -> `200`
- 簽章錯誤 -> `400`

2. Header 驗證
- Newsletter 發送時必定包含 `List-Unsubscribe`
- Transactional email 不包含 `List-Unsubscribe`
- Header 格式正確

### 9.7 安全與 UX

安全：
- Unsubscribe endpoint 不受 CSRF 限制
- Token 驗證必須 server-side
- 記錄 audit log
- 不顯示 `subscriber_id` 在 URL 明文

UX：
- 退訂成功頁面簡潔
- 提供重新訂閱入口
- 不強迫填問卷

### 9.8 目前實作狀態（Wagtail）

- 已提供 one-click endpoint：
  - `POST /u/unsubscribe`（token 可由 query/body 提供）
  - `GET /u/unsubscribe?token=...`（人類點擊頁）
- 已實作 token 驗證（HMAC + `exp`）與 idempotent 行為（重複退訂維持 200）。
- 已實作 server-side audit log（`OneClickUnsubscribeAudit`）。
- 已提供 one-click URL + Header 產生工具：
  - `List-Unsubscribe`
  - `List-Unsubscribe-Post`

### 9.9 Send Engine 發送介接（依 `../mass_mail_engine/docs/openapi.yaml` 對齊）

已對齊的 API：
1. 建立 send job
- `POST /api/send-jobs`
- request 以 `CreateSendJobRequest` 為主（`list_id`、`subject`、`body_html/body_text/template`）
- response 讀取：
  - `send_job_id`
  - `status`

2. 查詢 send job
- `GET /api/send-jobs/{id}`
- 若建立後狀態非最終態，持續輪詢直到最終態或超過輪詢上限。

狀態對應（Wagtail campaign）：
- Send Engine `completed` -> campaign `sent`
- Send Engine `failed` / `cancelled` -> campaign `failed`
- 建立失敗 / 輪詢逾時 / 輪詢錯誤 -> campaign `failed`

Send Engine 最終態（terminal）：
- `completed`
- `failed`
- `cancelled`

備註：
- 目前 dispatch 紀錄改為「每次送 job / 重試一次一筆」，不再是逐收件者一筆。
- 若要做投遞到單一收件者的最終監控（delivery/bounce/complaint），仍建議接 Send Engine webhook 或事件回寫機制處理。

### 9.10 One-Click Token 模式（目前採用）

目前採用：`MemberCenter token relay`

流程：
1. CMS 發送 payload 時同時提供：
   - `template.list_unsubscribe_url_template`（給 Send Engine 產生 `List-Unsubscribe` header）
   信內可點連結由 `body_html/body_text` 直接使用 `{{unsubscribe_token}}` 組 URL。  
   例如：`https://{cms}/u/unsubscribe?token={{unsubscribe_token}}`
2. Send Engine 於發送時向 Member Center 取得每位收件者的退訂 token，並替換 `{{unsubscribe_token}}`。
3. 使用者點擊信內退訂（或 mailbox one-click）後，進入 CMS `/u/unsubscribe`。
4. CMS 不自行驗簽 token，直接以 S2S 呼叫 Member Center `unsubscribe(token)` 完成退訂。

備註：
- 舊的 CMS 自簽 HMAC one-click token 保留工具函式作為備援，但主流程已切到 relay 模式。

## 10. 下一階段演進備忘（先記錄，待試用回饋後再排）

說明：
- 本節為「規劃中」項目，先記錄方向，不立即實作。
- 優先等待：電子報排版產出 + 實際 user 試用回饋。

### 10.1 編輯器能力演進

現況：
- 目前使用 Wagtail Draftail 作為 HTML 編輯器，已可插入圖片（圖庫選擇 / 上傳）。

待回饋後評估：
- 若編輯需求提升（table、欄位版型、拖拉區塊、進階 email 元件），再評估導入更完整的 newsletter editor。
- 原則：先確認真實編輯痛點，再導入新編輯器，避免過早複雜化。

### 10.2 電子報樣式（CSS）策略

建議方向：
- 採「固定樣式基底 + 內容編輯」模式。
- CSS 不建議只放 `<head><style>`，實務上需考慮 email client 相容性，通常會在發送前做 CSS inlining（將重要樣式轉為 inline style）。
- 可保留少量 head style（例如 media query），但核心排版建議以 inline 為主。

### 10.3 外框模板（Header / Footer / Shell）管理

目標：
- 提供可編輯的 newsletter shell（例如 `layout.html`），內含 head、header、footer 與內容插槽。

建議實作概念：
- 內容編輯區僅負責 body 內文。
- 發送時由 backend 組裝：`shell + content + one-click headers/links`。
- 預覽也走同一組裝流程，避免「預覽長得跟實際寄出不一致」。

### 10.4 退訂連結與個人化參數責任分工

共識方向：
- 退訂連結需與收件者身分綁定（每位收件者唯一 token / URL）。
- 內容模板需預留替換位（例如 `{{unsubscribe_url}}`）。

責任分工建議：
- CMS：產內容、產 placeholder、組裝信件與 one-click header。
- Send Engine：執行發送、接收投遞回報（bounce/complaint 等）。
- SES（或下游 provider）：最終投遞與回執來源。

### 10.5 文章區塊直接引入

需求方向：
- 支援從既有文章中挑選區塊或全文片段匯入電子報。

建議：
- 第一階段可先做「選文章 -> 帶入標題/摘要/首圖/連結」。
- 第二階段再考慮「可編輯快照」或「保持動態同步」策略。

### 10.6 預覽能力

需求方向：
- 內文編輯預覽。
- 樣式（shell）編輯預覽。
- 能注入假資料（假收件者、假 token、假文章）看最終結果。

建議：
- 做「最終寄出 HTML 預覽」功能，預覽管線與正式發送使用同一套 renderer。
- 後續可加多裝置寬度與常見 email client 快速檢視模式（若使用者回饋有需要）。
