SMTP 伺服器設定(SMTP Config)
功能群:系統管理|三層 RBAC 模型與
system_configskey-value 慣例先讀 功能群總覽 §2 / §4事實基準:2026-07-05 從 FE
src/views/smtp-config/+ BEapi/system_config/、api/notification/routes/mail_route.py+ routes.json / db_schema.json 掃出
變更紀錄
| 日期 | FR | 變更 |
|---|---|---|
| 2026-07-06 | FR-047 | 自 notify-smtp-config.md 拆出:SMTP
郵件伺服器設定(列表+表單)獨立成頁(通知頻道設定另見 通知頻道設定) |
1. 功能描述
SMTP 伺服器設定頁讓平台管理員維護系統對外寄信所用的 SMTP
郵件伺服器參數,可依郵件網域分列多組設定(如
example.com 用一組、*
萬用預設用一組)。設定值走 public.system_configs key-value
慣例(見 _overview
§4),group=SMTP、key=郵件網域字串或
*。本頁為列表 + 表單兩條
route:/system/smtp-config-manage(列表)與
/system/smtp-config(表單,新增 / 編輯共用)。權限基調已於
2026-06-29 改為平台層
admin-only(租戶管理員被移除此能力,見 §3),與仍為租戶可管的
通知頻道設定 不同。
- SMTP 列表頁(
/system/smtp-config-manage):DataTable 依郵件網域分列多組設定,支援關鍵字搜尋。 - SMTP 表單頁(
/system/smtp-config):新增 / 編輯共用,含基本欄位、TLS/啟用開關、帳號/密碼、測試郵件發送 Dialog。
主要使用者:平台 / root
租戶管理員(smtp-config.* 能力持有者);一般租戶管理員
2026-06-29 起已看不到本選單。
角色速覽
| 角色 | 一句話 |
|---|---|
平台 / root 租戶管理員(smtp-config.* 能力持有者) |
可管全平台 SMTP 設定(2026-06-29 起租戶管理員已被移除此能力,見 §3) |
| 一般租戶管理員 | 無此選單(ui_routes 可見性已從所有非 root
租戶角色移除) |
| 任何登入者 | ⚠️ 直打寫入 / 測試 API 無守門(見 §3) |
1.1 功能總覽(本頁全部功能)
| # | 功能 | 說明 | 位置 | 詳述 |
|---|---|---|---|---|
| 1 | SMTP 設定列表 | DataTable 列出所有 group=SMTP 設定列(依郵件網域 key
分列),支援關鍵字搜尋 |
列表頁 | §6.3 |
| 2 | SMTP 新增設定 | 導向表單頁(新增模式),key=郵件網域 |
列表頁 | UC-SMTP-01 |
| 3 | SMTP 編輯設定 | 導向表單頁(編輯模式,帶 uid) |
列表頁每列 | UC-SMTP-01 |
| 4 | SMTP 刪除設定 | key='*'(預設萬用設定)該列隱藏刪除鈕、BE 亦擋 |
列表頁每列 | §12 坑 2 |
| 5 | SMTP 基本欄位 | 郵件網域(key)*/ Host*/ Port*/ From*(email 格式) / From Name | 表單頁 | UC-SMTP-01 |
| 6 | SMTP TLS / 啟用開關 | 兩個獨立 InputSwitch | 表單頁 | — |
| 7 | SMTP 帳號 / 密碼 | user(明文)/ secret(password input +
SECRET_MASK) |
表單頁 | §12 坑 1 |
| 8 | SMTP 測試郵件發送 | 表單驗證通過後開 Dialog 收「收件信箱」,送出走獨立 test API | 表單頁 | UC-SMTP-02 |
| 9 | SMTP 表單離開未存提醒 | hasChange 旗標 + onBeforeRouteLeave
confirm dialog |
表單頁 | — |
展開成 UC:UC-SMTP-01(SMTP 設定建立 / 編輯)、UC-SMTP-02(SMTP 測試郵件發送)。其餘為常規列表 / 表單操作。
2. Use Case
角色速覽
| 角色 | 一句話 |
|---|---|
平台 / root 租戶管理員(smtp-config.* 能力持有者) |
可管全平台 SMTP 設定(2026-06-29 起租戶管理員已被移除此能力,見 §3) |
| 一般租戶管理員 | 無此選單(ui_routes 可見性已從所有非 root
租戶角色移除) |
| 任何登入者 | ⚠️ 直打寫入 / 測試 API 無守門(見 §3) |
視覺慣例:判斷框(黃菱形)= 分支條件;橙框 = 擋下;紅框 = 錯誤終點(標 error code);綠框 = 成功終點;虛線 = 可選路徑(secret 欄位 focus/blur 互動)。
UC-SMTP-01 SMTP 設定建立 / 編輯
| 項目 | 內容 |
|---|---|
| 角色 | 具 smtp-config.* 能力者(2026-06-29 起僅平台
Administrator,見 §3) |
| 前置條件 | 已登入;表單前端驗證(郵件網域 / Host / Port / From email 格式)通過 |
| 產出 / 後置條件 | system_configs(group=SMTP,
key=郵件網域)新增或更新一列;key='*' 之列建立後 key
鎖定不可改 |
UC-SMTP-02 SMTP 測試郵件發送
| 項目 | 內容 |
|---|---|
| 角色 | 同 UC-SMTP-01(表單頁內操作,無獨立角色檢查) |
| 前置條件 | 表單必填驗證通過;Dialog 內「收件信箱」需為合法 email |
| 產出 / 後置條件 | 對外寄出一封測試郵件(走表單目前值,非已存 DB 值);不寫 DB |
3. 權限矩陣
系統管理群整體基調見 _overview §5;本頁 FE/BE 對照如下:
| 操作 | FE 判定 | BE 強制 | BE 檢查位置 |
|---|---|---|---|
| 進入 SMTP 設定頁 | ui_routes
可見性(smtp-config.read);2026-06-29 起已從所有非
root 租戶角色移除,僅平台 Administrator 可見 |
無端點層強制(僅 @jwt_required) |
system_config_route.py |
| 儲存 / 刪除 SMTP 設定 | smtp-config.* 能力(同上,已限平台層) |
⚠️ 僅 @jwt_required,無角色 /
能力守門(直打 API 任何登入者皆可寫) |
system_config_route.py:53-88 |
| SMTP 測試郵件發送 | 無額外判定,表單驗證通過即可按 | ⚠️ 僅 @jwt_required,無角色守門、無 admin
檢查 |
api/notification/routes/mail_route.py:18-32 |
⚠️ SMTP 測試郵件發送完全無守門:SMTP 測試郵件發送(
/mail/test)完全無角色 / admin 檢查,任何登入使用者都能用表單目前值(甚至未儲存的值)觸發一次真實寄信。與 SMTP 設定本身已收斂為平台層可見性不對稱——選單層級擋住一般租戶管理員看到表單,但 API 本身沒有對應強制,繞過 FE 直打 API 可寄信。收進 §12。對照:通知頻道設定 的測試端點在 app service 層有is_admin檢查(NOTIFY_403001),SMTP 這條缺此保護。
4. 狀態機與前置條件
SMTP 設定無業務狀態機(system_configs 是單純 key-value
覆蓋式設定),前置條件集中在欄位驗證、SECRET_MASK
語意與 key='*' 萬用列語意:
| 條件 | 效果 | 出處 |
|---|---|---|
SMTP secret 欄位:BE 一律回傳前先剔除 secret |
FE 收到的 GET response 無 secret 明文,只能靠「有無 value」判斷要不要顯示遮罩 | system_config_route.py(HIDDEN_SECRET = ['SMTP','THIRD_PARTY_LOGIN','NOTIFY_CONFIG']) |
changePwd=false(未手動改過 secret) |
PUT/POST 時 BE 用 DB 既存 secret 覆蓋
payload,忽略前端送來的遮罩字串 |
jedi-system-config system_config_service.py:87-98 +
system_config_domain_service.py:106-124 |
changePwd=true |
採用 payload 送入的新值 | 同上 |
SMTP key='*' |
建立後 key 欄位鎖定(表單
disabled),視為租戶找不到對應網域設定時的預設 fallback |
SmtpConfigForm.vue:227(:disabled="uid && currData.key === '*'");BE
亦擋刪除(見 §6.3) |
SMTP 儲存新設定,同 (group, key) 已存在 |
409 SYSTEM_CONFIG_409001 |
jedi-system-config
system_config_domain_service.py:65-77(add_config) |
5. UI 設計
版面骨架(列表頁 DataTable + 表單頁欄位群 + 測試郵件 Dialog):
📸 實機截圖待補(亮色模式)。待補清單:SMTP 設定列表頁(DataTable)、SMTP 表單頁(含測試郵件 Dialog)。
狀態呈現:
- secret 類欄位(SMTP 密碼)用
type="password"的原生<input>(非 PrimeVuePassword元件),已設定過時顯示常數字串********(SECRET_MASK);使用者 focus 該欄位時清空以便輸入新值,若 blur 時未輸入任何字元則還原遮罩、且不標記「已變更」。 - 列表頁 DataTable 內建
loading狀態列。 - 表單頁有
hasChange旗標 +onBeforeRouteLeave攔截,離開前跳確認 dialog。 - 無 socket;儲存後皆重新 GET 該筆設定回填(非樂觀更新)。
6. API 規格
Envelope:成功 {"code": 1, "data": …}、失敗
{"code": 0, "msg": "…"}(HTTP 4xx/5xx,見專案 CLAUDE.md
Response Format)。
6.1 總清單(本頁呼叫的全部 endpoint)
| 分類 | Method + Path | 說明 | 完整規格 |
|---|---|---|---|
| 列表 | GET /api/1.0/system/configs/SMTP |
取得全部 SMTP 設定列 | §6.3 |
| 明細 | GET /api/1.0/system/config/<uid> |
取得單筆 SMTP 設定(secret 已剔除) | §6.3 |
| 建立 | POST /api/1.0/system/config |
新增一組 SMTP 設定 | §6.3 |
| 更新 | PUT /api/1.0/system/config/<uid> |
更新(含 changePwd 旗標) |
§6.3 |
| 刪除 | DELETE /api/1.0/system/config/<uid> |
軟刪除;key='*' 之列 BE 擋刪除 |
§6.3 |
| 測試寄信 | POST /api/1.0/mail/test |
用表單目前值寄一封測試郵件 | §6.4 |
⚠️ routes.json 中另有
/api/1.0/send-user-test-mail(auth.sendtestmailfromjediauth),是 jedi-auth 套件自帶的獨立測試寄信路由,本頁 FE 未呼叫(FEAPI.SEND_TEST_MAIL常數指向/mail/test,即api/notification/routes/mail_route.py::SendMailTestRoute)。兩者不要混淆——不要誤植前者到本頁流程。endpoint 存在性已對
routes.json核實;system_configs明細 / group 讀寫的通用 CRUD 亦見 _overview §4。
6.3 SMTP 設定(列表 / 明細 / CRUD)
[GET] /system/configs/SMTP — Response
data:SystemConfigResponse[](value
含 host/port/from/from_name/tls/enable/user,secret
已剔除)。出處:system_config_route.py:20-37(SystemConfigsRoute.get)
[GET] /system/config/<uid> — 單筆明細,shape
同上。出處:system_config_route.py:39-51
[POST] /system/config(建立)
Request(SystemConfigRequest,api/system_config/serializers/system_config.py:3-7):
| 欄位 | 型別 | 必填 | 說明 |
|---|---|---|---|
| group | string | 是 | 固定 SMTP |
| key | string | 是 | 郵件網域(如 example.com)或
*(萬用預設) |
| value | dict | 是 | {host, port, from, from_name, tls, enable, user, secret} |
同 (group, key) 已存在 → 409
SYSTEM_CONFIG_409001(jedi-system-config
system_config_domain_service.py:65-70)。
[PUT] /system/config/<uid>(更新)
Request:同上 +
changePwd(bool,是否手動改過密碼欄)。BE
額外行為(system_config_service.py:86-98):group=SMTP 且
key != '*' 時才允許改
key;changePwd=false 時用既存
secret 覆蓋。
[DELETE] /system/config/<uid> —
軟刪除;group=SMTP 且 key='*' → 404
SYSTEM_CONFIG_404001(domain service
視為不可刪,system_config_service.py:126-127)。
Response(PUT/POST):SystemConfigResponse(value
含 SMTP 欄位,secret 已剔除)。
6.4 SMTP 測試郵件發送
[POST] /mail/test
Request(SendMailTestRequest,api/notification/serializers/mail.py:4-5,僅宣告
to 一欄;smtp_config/changePwd 為
route 內直接讀 raw JSON,未入 schema):
| 欄位 | 型別 | 必填 | 說明 |
|---|---|---|---|
| to | string | 是(schema 未標 required,但無值會導致 domain 解析 email domain 時炸例外) | 收件信箱,支援逗號分隔多筆 |
| smtp_config | dict | 否 | 表單目前完整值({group, key, value:{...}});省略則依
to 的 email domain 反查 system_configs |
| changePwd | bool | 否 | 同 §6.3 語意;false 用 DB 既存 secret |
Response(BooleanResponse):{data: true}。
BE
鏈路:SendMailTestRoute.post(mail_route.py:18-32)→
TestMailService.send_test_mail(app/notification/service/test_mail_service.py:19-77):①
解析 to 的 email domain;② 若未帶
smtp_config,依 domain 查
system_configs(SMTP, <domain>),查無或
enable=false 則 fallback 查 key='*';③
changePwd=false 時用查到列的既存 secret;④ 組
SmtpEmailConfigDTO 呼叫 jedi-notification 寄信;失敗 →
raise BadRequestError(ErrorCode.MAIL_SEND_FAILED)(⚠️
此碼未在 common/code/error_code.py 定義,見 §12 坑
4)。
7. 前端檔案地圖(compliance-manager-fe/)
| 檔案 | 角色 |
|---|---|
src/views/smtp-config/SmtpConfigManage.vue |
SMTP 設定列表頁(DataTable + 搜尋 + 導向表單) |
src/views/smtp-config/SmtpConfigForm.vue |
SMTP 設定表單頁(新增 / 編輯共用 + 測試郵件 Dialog + Vuelidate 驗證) |
src/config/router/index.js:330-351 |
兩條 route:smtp-config-manage /
smtp-config |
src/config/api/api.js:328-336 |
SYSTEM_CONFIG / SYSTEM_CONFIGS /
SEND_TEST_MAIL(→ /mail/test)端點常數 |
src/config/locales/i18n/<locale>/pages.json(smtp_config
節) |
SMTP 頁 i18n(與其他頁共用 pages.json,非獨立檔) |
src/service/BaseService.js |
共用 axios 封裝(get/post/put/delete) |
8. 後端檔案地圖
| 鏈路 | Route | App Service | 底層 |
|---|---|---|---|
| SMTP 設定 CRUD | api/system_config/routes/system_config_route.py(SystemConfigsRoute/SystemConfigRoute) |
無主專案 app service 包裝,route 直接注入 jedi-system-config
SystemConfigService |
jedi-system-config system_config_domain_service.py →
ISystemConfigRepo |
| SMTP 測試寄信 | api/notification/routes/mail_route.py::SendMailTestRoute |
app/notification/service/test_mail_service.py::TestMailService |
jedi-system-config(讀既存設定)+ jedi-notification
NotificationService(SMTP client) |
DDD 提醒:
system_config_route.py的 Route class 直接注入外部套件SystemConfigService、無主專案 app service 層,屬「外部套件擁有 domain」情形(比照 role-manage 的 jedi-auth 模式)——代表本頁寫入 API 完全無法插入主專案自訂的角色檢查(除非改包一層 app service)。SMTP 測試寄信(SendMailTestRoute)走主專案TestMailService但未做任何權限檢查;新增類似「測試 / 敏感操作」端點應比照 通知頻道設定 的NotifyConfigTestServiceis_admin檢查模式,不要比照本頁無守門寫法。
9. DB
9.1 資料表總清單(本頁讀寫的全部表)
| 表 | 讀/寫 | 說明 | 詳述 |
|---|---|---|---|
public.system_configs |
讀 / 寫 | SMTP 設定(group=SMTP,key=郵件網域或
*) |
§9.3 |
public.roles / public.role_capabilities /
public.capabilities /
public.route_capabilities /
public.ui_routes |
讀 | 決定本頁選單可見性(smtp-config.* 能力) |
_overview §6 |
本頁無獨立專表:SMTP 只是
system_configs的一個group值,不建專表(_overview §7 坑 7)。
9.2 ER 圖
本頁讀寫的表結構單純(單表 + RBAC 唯讀依賴),沿用 _overview §6 核心資料模型圖,不另畫 ER 圖。
9.3 核心表欄位(取自 db_schema.json,含 DB comment)
public.system_configs(comment:「系統配置表:儲存系統設定參數,以群組和鍵值對方式組織」):
| 欄位 | 型別 | 說明 |
|---|---|---|
| id | integer | 主鍵 ID |
| uid | varchar(50) | 配置唯一識別碼 UUID |
| group | varchar(255) | 配置群組:本頁用 SMTP |
| key | varchar(255) | 配置鍵名:郵件網域字串或 *(萬用預設) |
| value | jsonb | 配置值:{host, port, from, from_name, tls, enable, user, secret} |
| tenant_id | integer(nullable) | 設定歸屬租戶;SMTP 2026-06-29 起實務上已收斂為平台層共用(見 §12 坑 3) |
| org_unit_id | integer(nullable) | 設定歸屬部門(本頁未使用) |
| created_at / updated_at | timestamp | 建立 / 更新時間 |
⚠️ 無
(group, key)的 DB 層唯一約束:db_schema.json顯示該表僅id主鍵、無其他 index/unique constraint;重複防護完全靠 domain service 的get_by_group_and_key先查後判(add_config查到既有列才 raiseSYSTEM_CONFIG_409001),屬應用層防護非資料庫層保證,並發建立有理論競態風險(見 §12 坑 5)。無
created_user/updated_user欄位:本表只有created_at/updated_at時間戳,無操作者欄位,不適用專案 CLAUDE.md 的審計欄位 enrich 規範。
10. 頁面邏輯與資料對應
關鍵欄位對應:
| 畫面元素 | FE state | API 欄位 |
|---|---|---|
| SMTP 郵件網域 | currData.key |
key |
| SMTP host/port/from/from_name/tls/enable/user/secret | currData.value.* |
value.* |
| SMTP 是否改過密碼 | passwordHasChange(bool ref) |
payload 頂層 changePwd |
儲存流程:SMTP 表單無 uid query 走
POST /system/config(建立成功後 router.push
帶上新 uid 轉編輯模式);有 uid 走
PUT /system/config/{uid}(成功後 getConfig()
重載)。
錯誤對應:儲存 / 測試失敗 → toast 顯示
err.message 或固定文案(send_error);BE error
envelope 常見碼見 §6 各段。
11. 背景行為與外部依賴
| 類型 | 內容 |
|---|---|
| 通知 | 無站內通知;本頁「測試」本身即是對外寄出郵件(SMTP mail),非系統站內通知 |
| Job / 排程 | 無 |
| Socket | 無;操作後皆手動重打 GET 回填 |
| jedi-* 套件 | jedi-system-config(system_configs CRUD 之
service/domain/repo)、jedi-notification(實際對外寄信的
SMTP client,NotificationService +
SmtpEmailConfigDTO)、jedi-auth(JWT
context) |
| 系統參數 | 無 feature
flag;HIDDEN_SECRET = ['SMTP', 'THIRD_PARTY_LOGIN', 'NOTIFY_CONFIG']
是寫死在 system_config_route.py 的常數,決定哪些 group 的
GET response 剔除 secret |
12. 邊界情況與已知坑
- SECRET_MASK 是純前端慣例,非 BE 回傳值:BE 對
HIDDEN_SECRET內的 group 一律在 response 剔除secret欄位,FE 靠「value存在即代表已設定」自行決定要不要顯示********常數字串;changePwd旗標才是 BE 判斷要不要覆蓋密碼的依據,遮罩字串本身送到 BE 也無害(因為changePwd=false時 BE 根本不採用 payload 的 secret)。 - SMTP
key='*'是萬用 fallback,不可刪除:TestMailService.send_test_mail找不到收件 domain 對應設定時會 fallback 查key='*';domain service 對group=SMTP AND key='*'的刪除請求一律回 404(視為不存在),FE 亦隱藏該列刪除鈕,兩端一致但建立時若忘了先建*,未知網域收件會直接找不到設定回False(非拋例外,寄信邏輯「靜默失敗」,見坑 4)。 - SMTP 已於 2026-06-29 改平台層
admin-only:本頁權限基調為平台級,通知頻道設定
仍租戶可管——若之後要新增第三種設定頁,先確認該設定屬性層級(租戶級 vs
平台級),別預設沿用同一權限模型(見
scripts/sql/2026-06-29-platform-only-smtp-ldap-log-capabilities.sql、2026-06-29-platform-capability-flag-and-guard.sql)。 MAIL_SEND_FAILED錯誤碼未定義:app/notification/service/test_mail_service.py:76raise BadRequestError(ErrorCode.MAIL_SEND_FAILED),但common/code/error_code.py內沒有此常數——寄信真正失敗時這行會先炸AttributeError(而非預期的 400 回應),使用者看到的會是 500 而非「發送失敗」提示。docs/api/notification/generate_docx.py:461已註記此為潛在問題但未修。修正需另開 bug session(新增MAIL_SEND_FAILED = ("郵件發送失敗", "NOTIFY_400004")之類的碼)。system_configs無(group, key)DB 唯一約束:重複防護完全靠 app 層先查後插(get_by_group_and_key→ 有則 409),高併發下(如兩個管理員同時建立同網域設定)理論上可競態出重複列;本頁流量低,記錄不修。- SMTP 測試郵件發送完全無守門:SMTP
測試寄信(
/mail/test)沒有任何角色 / admin 檢查,只要登入即可用任意表單值觸發真實寄信(可被用來做垃圾郵件轉發測試)。選單層級擋住一般租戶管理員看到表單,但 API 本身沒有對應強制。已知缺口,本 spec 只記錄不修。 - SMTP
測試寄信可帶未儲存的表單值:
doSendTestMailClick送出smtp_config: currData.value(前端當下狀態,非重新從 DB 讀取),代表使用者可以「先測試、確認能寄信、再考慮要不要儲存」——這是刻意設計(測試不等於儲存),但也代表測試成功不保證儲存後仍是同一組設定(若忘記按儲存)。 send-user-test-mail是另一條路由,本頁不用:routes.json 存在auth.sendtestmailfromjediauth(/api/1.0/send-user-test-mail),是 jedi-auth 套件自帶、FE 未呼叫的獨立測試寄信端點;維護時勿誤改到它以為在改本頁邏輯。
13. 開發與驗證
- 跑起來:BE
python main_socketio.py(port 8000,log 在log/app.log);FE 在compliance-manager-fe/起 Vite dev server。BE 改 service code 後必須重啟(無 hot reload)。 - 測試帳號:SMTP 需平台 / root
租戶管理員角色才看得到「郵件伺服器設定」選單項。帳號見 memory
reference_dev_login/.env(憑證不寫入本文件)。 - 導航路徑:登入 →
側選單「系統管理」→「郵件伺服器設定」(
/system/smtp-config-manage)。 - 前置資料:SMTP 建議先建
key='*'萬用列,避免未知網域測試寄信直接失敗。 - E2E:
compliance-manager-test/repo(Cucumber + Playwright);本頁相關 feature 檔以smtp-config/SMTP搜尋。 - 相關文件:通知頻道設定 → notify-channels.md;平台層權限收斂決策見
scripts/sql/2026-06-29-platform-only-smtp-ldap-log-capabilities.sql與2026-06-29-platform-capability-flag-and-guard.sql檔頭註解;DB / API 全量見 GAI-SD-02/03。