# HTTP API 參考

## 語言與錯誤碼

英文是預設語言。API、Web 控制台和通知渲染共用 `internal/i18n/locales/` 下的內嵌文案，`internal/i18n/languages.json` 定義語言程式碼和本機名稱。

| 程式碼    | 語言               |
| --------- | ------------------ |
| `en`      | English            |
| `zh-CN`   | 简体中文           |
| `zh-Hant` | 繁體中文           |
| `ja`      | 日本語             |
| `ko`      | 한국어             |
| `de`      | Deutsch            |
| `fr`      | Français           |
| `es`      | Español            |
| `pt-BR`   | Português (Brasil) |

- 首次存取按瀏覽器的 `Accept-Language` 選擇語言，切換後在本機儲存規範程式碼。控制台語言與已儲存的通知語言獨立。
- API（包括 App 管理介面）按 `Accept-Language` 權重協商，返回 `Content-Language` 和 `Vary: Accept-Language`。跳過不支援的偏好；預設、非法或全部不支援時回退 `en`，權重 0 的語言不參與協商。
- 地區別名對映到對應語言：`fr-CA` → `fr`，`es-MX` → `es`，`pt` / `pt-PT` → `pt-BR`。中文區分字形：`zh-Hans` / `zh-CN` / `zh-SG` → `zh-CN`；`zh-Hant` / `zh-TW` / `zh-HK` / `zh-MO` → `zh-Hant`。顯式字形優先於地區，單獨 `zh` 使用簡體。
- `GET /locales/default.json` 返回協商後的文案；`GET /locales/{规范代码}.json` 返回指定文案；未知文案路徑返回 `locale_not_supported`。
- 通道設定 `language` 獨立控制哨站生成的通知，支援上述規範程式碼及地區別名，儲存時歸一化；省略或空值使用 `en`，顯式不支援的值返回 400 `config_language_invalid`。請求語言不會覆蓋該設定。
- Native 測試文案由推播中繼服務根據配對裝置語言生成，哨站通知語言不會覆蓋它。推播中繼服務和 App 各自支援相應的語言。
- 控制台本機化標準資料夾角色及常見名稱。API 保留原始 IMAP 路徑、自定義名稱和 `folder_roles`，App 使用自己的文案翻譯角色。通知標題繼續使用原始資料夾末級名稱；使用者輸入、郵件主旨、內文和驗證碼保持原值。
- 日誌和 CLI 使用英文。業務錯誤保持穩定的英文 `snake_case` 錯誤碼與命名引數；儲存保留程式碼和引數，展示層負責翻譯。API 時間和數字保持原有機器可讀格式，控制台按所選語言格式化時間。

```json
{
  "error": {
    "code": "unauthorized",
    "message": "请使用有效会话或 API Token 登录。"
  }
}
```

投遞記錄的 `last_error` 與資料夾狀態的 `notice` 使用相同的 `{code, message, params?}` 結構，例如 `bark_http_error` 搭配 `{"status":"503"}`。按 `code` 判斷分支，`message` 用於展示。

每個信箱的獨立連線預算內預留 1 個名額給資料夾發現與連線測試。監聽需求按“實時 Folder 數 +（存在定時 Folder 時的 1 個）”計算，最多使用 `connection_limit - 1` 個名額（預設 9 個）；該信箱所有操作的總連線數保持在 `connection_limit`（2–100）以內。已儲存上限為 1 的設定按 `configuration_invalid` 降級啟動，允許透過該信箱的 PUT 介面修正。

## HTTP API

`GET /`、靜態資源和 `/healthz` 公開存取。首次設定完成前，其他管理介面統一返回 403 `setup_required`；`POST /api/v1/setup` 只能成功一次。設定後公開登入介面 `POST /api/v1/session`，其餘介面使用會話 Cookie 或 `Authorization: Bearer mwk_…`。

Cookie 屬性為 HttpOnly、SameSite=Strict、Path=/，TLS 連線或 HTTPS Origin 加 Secure；空閒 7 天或絕對 30 天過期。Cookie 修改請求同時要求 `X-CSRF-Token` 和使用 HTTP 或 HTTPS、主機與埠和請求 Host 完全一致的 `Origin`。Bearer 請求免於 CSRF；顯式提交錯誤的 Authorization 時，即使 Cookie 有效也拒絕請求。

登入和設定碼失敗按連線來源地址共享限額：15 分鐘內 10 次失敗，後續請求返回 429 `too_many_attempts`，`Retry-After` 為秒數。密碼修改也使用該限額，來源由 Gin ClientIP 按上述可信代理網段解析。登入和設定請求中的跨站 Origin 會被拒絕。由於所有內網對端都被視為可信代理，區域網內的客戶端可以偽造 X-Forwarded-For，把失敗嘗試分散到多個來源地址。不要把哨站監聽埠暴露在不可信的區域網中，只透過反向代理對外提供存取。

JSON 請求體上限 64 KiB，未知欄位或多餘 JSON 返回 400。密碼至少 12 個字元、最多 1024 個 UTF-8 位元組；使用者名稱及 Token 名稱為 1–128 位元組。管理 API 的時間戳統一為 UTC RFC 3339 字串，包括投遞 created_at/accepted_at、Token created_at/last_used_at、資料夾檢查時間、日誌時間和診斷生成時間；小數秒保留儲存精度。未使用 Token 的 last_used_at 為 null，尚未接受的投遞省略 accepted_at。計數、時長和 revision 保持數字。

401 表示會話或 API Token 鑑權失敗，或 `POST /api/v1/session` 登入憑據錯誤。設定碼錯誤返回 400 `setup_code_invalid`，嚮導回到設定碼步驟並保留使用者名稱。`PUT /api/v1/admin/password` 的當前密碼錯誤返回 400 `current_password_invalid`，仍計入失敗限速，當前會話保持有效。登入失敗與當前密碼錯誤在原表單內提示並保留輸入。

| 方法               | 路徑                                  | 請求與回應                                                                                                   |
| ------------------ | ------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
| POST               | `/api/v1/setup`                       | `{"code":"XXXX-XXXX-XXXX","username":"admin","password":"…"}`；201，Cookie 與 `csrf_token`；重複設定返回 409 |
| POST               | `/api/v1/session`                     | `{"username":"admin","password":"…"}`；200，Cookie 與 `csrf_token`                                           |
| GET                | `/api/v1/session`                     | 返回 `authenticated:true`、`csrf_token`；Bearer 返回 `method:"token"`                                        |
| DELETE             | `/api/v1/session`                     | 204，登出當前 Cookie 會話；需要會話鑑權                                                                      |
| PUT                | `/api/v1/admin/password`              | `{"current_password":"…","password":"…"}`；204，登出其他會話；Bearer 修改會登出全部會話                      |
| POST               | `/api/v1/tokens`                      | `{"name":"automation"}`；201，返回 `id`、`name`、`created_at`、`last_used_at` 和僅顯示一次的 `token`         |
| GET                | `/api/v1/tokens`                      | `{"tokens":[…]}`，僅後設資料                                                                                 |
| DELETE             | `/api/v1/tokens/:id`                  | 204，撤銷 Token；重複撤銷仍返回 204                                                                          |
| GET                | `/api/v1/mailboxes`                   | `{"mailboxes":[…]}`，信箱脫敏檢視列表                                                                        |
| POST               | `/api/v1/mailboxes`                   | 使用下方信箱表單建立；201，返回生成的 ID、revision 1 和脫敏檢視                                              |
| GET / PUT / DELETE | `/api/v1/mailboxes/:id`               | 讀取 / 攜帶當前 revision 更新 / 刪除（204）；不存在返回 404 `mailbox_not_found`                              |
| POST               | `/api/v1/mailboxes/test`              | 測試未儲存的信箱表單，返回 `{"folders":[…]}`，保持設定原值                                                   |
| POST               | `/api/v1/mailboxes/:id/test`          | 合併該信箱已儲存的憑據後測試，連線目標相同時省略密碼保留原值                                                 |
| GET                | `/api/v1/mailboxes/:id/folders`       | 使用該信箱預算發現可選資料夾                                                                                 |
| GET / PUT          | `/api/v1/mailboxes/:id/subscriptions` | 讀取 / 按獨立 revision 儲存訂閱；成功返回 202                                                                |
| GET / PUT          | `/api/v1/settings/delivery`           | 讀取 / 攜帶獨立 revision 儲存全域性通道設定                                                                  |
| DELETE             | `/api/v1/settings/delivery`           | 攜帶 `{revision}` 清除通道與憑據；204，版本衝突409                                                           |
| POST               | `/api/v1/settings/delivery/test`      | 合併已儲存憑據並測試通道表單；204，保持設定原值                                                              |
| GET                | `/api/v1/status`                      | 資料夾狀態、通道、投遞計數及按作用域組織的 notices                                                           |
| GET                | `/api/v1/diagnostics`                 | 下載脫敏 JSON 診斷報告，省略執行日誌                                                                         |
| GET                | `/api/v1/logs`                        | 鑑權後查詢執行日誌，支援 after、level、mailbox_id、limit（見下文）                                           |
| GET                | `/api/v1/deliveries`                  | 最近 50 條請求記錄，包含標題和信箱 / 資料夾摘要                                                              |
| POST               | `/api/v1/test-push`                   | 使用已儲存設定傳送測試，接受後返回 204，保持設定原值                                                         |
| POST               | `/api/v1/deliveries/:id/retry`        | 重新處理 `dead` 任務，返回 202                                                                               |

投遞記錄的可選 `message` 物件包含 `mailbox_id`、`mailbox_label`、`folder`、`subject`（字串或 null）和 `test`（布林值）。名稱按入隊時儲存，信箱改名或刪除後保持歷史來源。subject 為 null 表示標題未保留，空字串表示郵件無主旨。測試通知的 test=true，來源為空。摘要介面省略寄件者和內文。

建立信箱示例：

```json
{
  "label": "工作邮箱",
  "host": "imap.example.org",
  "port": 993,
  "username": "user@example.org",
  "password": "…",
  "connection_limit": 10
}
```

最多儲存 20 個信箱，包含待修正的無效設定；繼續建立返回 400 `mailbox_limit_exceeded`。ID 為 `mbx_` 加隨機串，建立後保持穩定。`label` 預設取 username（信箱地址），最長 64 個 Unicode 字元，禁止換行。使用者名稱超過 64 字元時需顯式填寫較短名稱。更新時省略名稱保留原值，空名稱使用本次提交的 username。

信箱 PUT 使用相同表單並攜帶 GET 讀到的 `revision`，例如 `"revision":1`，必須填寫 host、port、username。host、port、username 保持原值時，省略 password 保留原密碼，顯式空密碼會被拒絕。省略 `connection_limit` 保留當前值；新信箱預設 10，範圍 2–100。建立 POST 省略 revision 或傳 0。測試介面接受相同欄位，無須 revision，保持設定原值；未儲存信箱的測試需要憑據，帶 ID 的測試只合並該信箱的已有憑據。

信箱 GET 返回 `id`、`label`、`revision`、`host`、`port`、`username`、`connection_limit`、`connections_in_use` 與 `"password":{"configured":true}`。降級的無效信箱返回 `configured:false`，保留 ID 和 revision 供修正。讀取回應中的密碼物件屬於展示檢視，不能作為寫入值。

每個信箱的連線預算獨立。實時 Folder 各佔一條連線，全部定時 Folder 合計佔一條，需求最多為 `connection_limit - 1`，預留一條供管理。PUT 訂閱及修改連線上限共用此公式，容量不足返回 `connection_budget_exceeded`。未儲存信箱的測試使用臨時預算。未儲存信箱測試、已儲存信箱測試和資料夾發現三個入口還共用全域性併發上限 2，超出立即返回 429 `discovery_busy`，結束或取消時釋放名額。

首次儲存通道示例：

```json
{
  "revision": 0,
  "channel": "bark",
  "preview": "off",
  "retry_count": 0,
  "language": "zh-CN",
  "bark": { "endpoint": "https://api.day.app", "key": "…" }
}
```

每次更新前讀取 `/settings/delivery` 並提交其 revision，首次儲存從 0 增加到 1。Pushover 使用 `pushover.token`、`pushover.user`；Webhook 使用 `webhook.url`、`webhook.secret`。channel、preview、revision 必填；省略 language、retry_count 使用預設值。省略憑據（含 Webhook URL）保留已儲存值，顯式空的當前通道憑據會被拒絕；GET 將憑據替換為 `{"configured":true|false}`。獨立測試介面無須 revision。

信箱建立和連線設定修改在儲存前測試連線及登入。host、port、username、connection_limit 與現值相同且省略 password 時，PUT 只改 label：仍校驗 revision 並儲存，保持連線與進度，已入隊通知保留原名稱，之後入隊使用新名稱。建立或修改後的身分若與其他信箱相同，返回 409 `mailbox_duplicate`；host 與 username 忽略大小寫、port 精確匹配，同身分併發建立只成功一個。通道 PUT 對所有通道統一校驗欄位、地址、所選配對狀態與 revision，直接儲存，儲存過程不傳送測試或聯絡通道服務。顯式通道測試保持設定原值，失敗後仍可儲存合法設定。信箱網路測試在設定鎖外執行，現有監聽保持執行，信箱測試失敗保留原設定。設定 revision 過期返回 409 `settings_conflict`；信箱測試期間訂閱變化同樣使快照失效，需重新讀取後提交。各信箱、全域性通道分別維護 revision，修改不同信箱、同時修改信箱和通道互不衝突。

PUT 成功返回脫敏檢視並立即生效。信箱連線設定更新先停止自身監聽，再儲存並按需清理進度；儲存失敗恢復原監聽，其他信箱繼續執行。通道變更時，進行中的請求沿用原快照，後續嘗試使用新快照；尚未設定通道時佇列保持待處理。

`GET /api/v1/mailboxes/:id/subscriptions` 返回 `{"revision":1,"folders":[{"name":"INBOX","check":"realtime"},{"name":"Clients","check":"5m"}]}`。PUT 提交讀取到的 revision 與完整 folders 陣列，空陣列取消該信箱的全部訂閱。名稱在信箱內唯一，INBOX 大小寫統一，每項最多 4096 個 UTF-8 位元組；每項 name、check 必填，check 為 realtime、5m 或 15m；缺失、非法 check 或舊字串陣列返回 400。連線需求使用上文公式。只改 check 保留進度和事件 ID，銜接新郵件補查；取消後重新訂閱繼續重建基線。儲存成功返回 202 和下一個版本，舊版本返回 409 `subscriptions_conflict`。資料夾存在性由監聽確認，信箱離線時仍可取消訂閱。儲存失敗返回 503，當前訂閱保持不變。

`/status.folders[]` 每項包含 `mailbox_id`、`mailbox_label` 與 `check`。mode 為 idle、poll 或 scheduled；定時 Folder 另有 RFC 3339 時間 next_check（失敗時為下次重試時間），實時 Folder 省略該欄位。已儲存設定的 JSON 或模型校驗失敗時，對應項降級為未設定，在 `/status.notices` 的 `mailbox:<id>` 或 `delivery` 下報告 `configuration_invalid`，可透過該項 PUT 修正。訂閱超預算時僅暫停該信箱，在 `subscriptions:<id>` 下報告 `connection_budget_exceeded`；減少訂閱或提高預算後恢復。解密失敗仍拒絕啟動。

診斷白名單包含生成時間、構建/Go/平臺資訊、佇列計數以及匿名信箱和監聽狀態（含 check 與 mode）。`mailboxes[].index` 與 `watches[].mailbox_index` 對應本次報告的“信箱 1、信箱 2……”；信箱內監聽有獨立的 `index`。信箱條目包含設定及訂閱 notice 的錯誤碼。僅 `imap_folder_rejected` 可匯出 `watches[].response_code`。報告省略信箱 ID、顯示名稱、地址、主機、資料夾名、郵件內容、任務 ID、憑據及其他錯誤引數。

任務狀態：`pending` 等待請求，`accepted` 通道接受請求，`dead` 本輪請求結束且記錄失敗。

## 執行日誌

透過 `docker compose logs -f core`（或 `docker logs <容器名>`）檢視 JSON stderr 日誌。管理客戶端可使用會話 Cookie 或 API Token 查詢同一組執行事件：`GET /api/v1/logs?after=0&level=info&mailbox_id=mbx_example&limit=200`。

記憶體環形緩衝保留最近 **2000** 條 Info/Warn/Error 日誌，重啟清空。after 是排除本身的非負序號；level 為 info/warn/error，包含指定級別及以上，預設 info；省略 mailbox_id 時包含全部信箱和全域性操作。limit 預設 200，範圍 1–500。引數非法返回 400 `logs_request_invalid`。

```json
{
  "entries": [
    {
      "seq": 42,
      "time": "2026-09-29T00:00:00Z",
      "level": "info",
      "message": "Folder connection established",
      "attrs": {
        "mailbox_id": "mbx_example",
        "folder": "INBOX",
        "mode": "idle",
        "check": "realtime"
      }
    }
  ],
  "next": 42,
  "cleared_through": 0
}
```

下一次請求把 next 作為 after。達到頁大小時 next 為最後返回序號；遍歷完快照時為快照最大序號，過濾結果為空也推進。淘汰的日誌從當前保留記錄繼續；重啟導致 next 小於 after 時，將遊標重置為 0。條目按序號升序返回。

日誌包含信箱/Folder 生命週期、基線與入隊數量、投遞嘗試、設定變更和管理員操作，全域性事件的 mailbox_id 為空。attrs 僅輸出這些標量欄位：mailbox_id、folder、mode、check、code、event_id、channel、attempt、duration_ms、http_status、next_attempt、backoff_seconds、count、revision、token_id、version、listen。http_status=0 表示未收到 HTTP 回應；token_id 為公開記錄 ID，區別於 Token 完整值。密碼、Token/會話值、設定碼、寄件者、主旨、內文和憑據 URL 均省略；設定碼那一行只輸出到控制台。日誌包含 Folder 名稱，匿名診斷匯出繼續省略日誌和名稱。

資料夾掃描與管理員信箱測試返回原始路徑陣列 `folders`，並可附帶 `folder_roles`：路徑到 `inbox`、`drafts`、`sent`、`trash`、`junk`、`archive`、`all` 或 `flagged` 的對映。用途來自 IMAP SPECIAL-USE 屬性，INBOX 按協議名稱識別。Web 介面在缺少用途時，按常見英文資料夾名稱進行忽略大小寫的完整匹配；譯名後用括號保留原始路徑，其餘名稱保持原樣。介面翻譯不改變訂閱路徑。 通知標題僅顯示斜槓分隔路徑的末級名稱；結構化 folder 欄位保留完整路徑。App 信箱測試繼續僅返回連線狀態。

`GET /api/v1/session` 在 Cookie 會話回應中返回 `username`，Token 鑑權回應保持原有結構。

### App 接收裝置

通知設定增加 `native_pairing_id`。`native` 管道必須從 `/api/v1/native/devices` 選擇一個有效配對；測試通知和新事件只投遞給該裝置。更新時省略此欄位保留原選擇；空選擇返回 `native_target_required`，失效目標返回 `native_target_unavailable`。當前 native 目標撤銷後，通道與 native_pairing_id 同時清空，revision 增加一次；其他目標和通道保留。已開始的投遞保留原目標及撤銷結果。狀態同步、併發與重啟規則見[配對撤銷同步](https://github.com/mingzaily/mailwake/blob/main/docs/native-push.md#pairing-revocation-synchronization)。

App 授權頁分別管理管理權限和推播權限，兩者獨立撤銷。

### Native 測試通知

哨站使用簽章 `POST /v1/pairing-tests`，內文 `{pairing_id,test_id}`；每次顯式測試生成新的 UUID test_id。推播中繼服務生成固定內容並同步提交 APNs，無須 Native Push 權益。表單測試、App 已儲存設定測試和 `/api/v1/test-push` 共用此呼叫鏈。測試獨立於郵件 Outbox，不自動重試。

僅推播中繼服務返回 `accepted` 對應哨站 HTTP 204。APNs 拒絕、結果未知、`sending` 均返回本機化錯誤；推播中繼服務限頻對映為哨站 HTTP 429 並保留 `Retry-After`。安全日誌記錄推播中繼服務 HTTP 狀態與穩定錯誤碼，省去回應內文、Token 和通知內容。哨站與 App 共享推播中繼服務現有每裝置每分鐘、每主體每日額度及 `(device_id,test_id)` 冪等。使用者再次點選測試會建立新的測試 ID。

正式郵件仍使用 `/v1/push` 並校驗權益，事件 ID 或客戶端測試標誌均保持此規則。先部署支援配對測試介面的推播中繼服務，再部署此版本哨站；哨站不回退到正式郵件介面。

## 清除投遞記錄與執行日誌

- `DELETE /api/v1/deliveries` 返回 `200 {"deleted":N}`，永久清除全部歷史中已結束的 `accepted` / `dead` 記錄及關聯 Native 記錄。待投遞/重試、Native 的 pending/queued/sending 狀態、待執行的狀態檢查和活動更新會保留。清除失敗記錄後，其手動重試入口一併消失。信箱檢查點與設定保持原樣。
- `DELETE /api/v1/logs` 返回 `200 {"cleared_through":N}`，清除所有級別、所有信箱共享的記憶體日誌。序號繼續遞增，標準錯誤/容器日誌和匯出檔案保留。`GET /api/v1/logs` 同時返回 `cleared_through`；客戶端先移除快取中 `seq <= cleared_through` 的條目，再追加回應中的日誌。
- 兩個介面均要求管理員會話及 Origin/CSRF，或管理員 API Token。App 裝置憑證無法執行這些操作。重複清除安全；操作完成後仍可產生新任務與新日誌。

## App 郵件正文

`POST /api/v1/app/content` 使用裝置綁定的控制憑證與站主授予的 `content` 權限。請求為 `{ "reference": <message_ref> }`，回應包含 `text`、`truncated`、可選的清理後 `html` 和 `has_remote_images`。正文閱讀免費。

正文可讀後，使用相同憑證、權限和引用呼叫 `POST /api/v1/app/content/read`，將信箱中的郵件標記為已讀。成功回傳 `204 No Content` 與 `Cache-Control: no-store`。僅取得正文會保留信箱原有的已讀狀態。兩個操作均驗證目前授權、訂閱資料夾、信箱身分和郵件引用。

## 憑據與連線目標

測試或儲存信箱時，修改 `host`、`port` 或 `username` 須重新輸入已儲存的信箱密碼，否則返回 400 `mailbox_password_required_for_change`。修改已儲存的 Bark 位址須重新輸入裝置 Key，即使目前選中其他通道亦適用；否則返回 400 `bark_key_required_for_change`。位址包含連接埠、路徑和末尾斜線。拒絕時不建立連線、不傳送通知，設定與 revision 保持原值。

## App 請求診斷

診斷匯出使用 `schema_version: 2`，新增 `app_requests`：從共用的 2000 筆記憶體日誌擷取最近最多 300 筆已完成的 App 請求。每筆包含時間、伺服器產生的 `request_id`（同時透過 `X-Request-ID` 回傳）、路由範本、方法、HTTP 狀態、耗時、階段、結果與安全錯誤碼。匯出排除資源 ID、查詢參數、憑證、簽章參照、內文與完整執行日誌。重新啟動哨站或清除日誌會移除記錄，其他日誌也可能擠出較舊請求。下載前會更新報告。未到達哨站的請求仍需 App 診斷。

對於 `POST /api/v1/app/content/read`，哨站查詢 UID 和 flags：已讀直接成功，未讀則標記後再次確認。STORE OK 本身無法證明郵件存在。UID 不存在、UIDVALIDITY 改變或資料夾明確不存在回傳 404；服務商或權限失敗回傳 503，逾時回傳 504。遠端參照失效時，本機快取內文仍可閱讀。
