核心变更: - 数据库:Schema v3,accounts 表新增 account_type/provider/imap_host/imap_port/imap_password 字段 - 后端:新增通用 IMAP 服务(imap_generic.py)与邮箱提供商配置(providers.py) - 导入:支持 Outlook/QQ/163/Gmail/自定义 IMAP 多格式批量导入,兼容注释行与导出格式 - 导出:按邮箱类型分组导出,保留 provider 信息便于重新导入 - 邮件读取:IMAP 账号走通用 IMAP 服务,Outlook 保持原 Graph/XOAUTH2 链路 - 验证码提取:IMAP 账号通过 IMAP 获取邮件详情后提取 - 安全约束:IMAP 账号禁用远程删除,敏感字段(imap_password)加密存储 - 前端:新增邮箱类型下拉、自定义 IMAP 配置区、provider 标签显示 - 调度器:定时刷新仅处理 Outlook 账号(IMAP 无 OAuth token 刷新语义) - 日志:配置 outlook_web 命名空间日志输出到 stderr 技术细节: - 保持 Outlook 旧格式完全兼容(四段式:email----password----client_id----refresh_token) - IMAP 账号使用空字符串占位 client_id/refresh_token(满足 DB NOT NULL 约束) - 支持导出格式智能识别(5 段 custom、3 段 provider、2 段配合输入框) - 前端 DOMPurify 未加载时回退为基本 HTML 过滤(CDN 不可达场景) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
16 KiB
FD-00005|多邮箱统一管理 — 功能实现设计文档
文档信息
- 文档编号: FD-00005
- 创建日期: 2026-03-02
- 版本: V1.0
- 状态: 草案
- 对齐 PRD:
docs/PRD/PRD-00005-多邮箱统一管理.md - 覆盖范围: 后端(Flask + SQLite)+ 前端(templates + static/js)
一、功能概述
1.1 设计目标
在保持现有 Outlook 功能完全不变(Graph API 主路径、XOAUTH2 IMAP 回退路径、现有 API/前端调用方式)前提下:
- 支持新增 IMAP 类型邮箱(Gmail / QQ / 163 / 126 / Yahoo / 阿里邮箱 / 自定义 IMAP)。
- 在同一界面统一管理:账号新增、分组复用、邮件列表/详情、验证码提取。
- IMAP 账号支持授权码/应用专用密码的加密存储与脱敏展示。
- 导入/导出兼容:Outlook 保持原格式;IMAP 提供新格式与“按 provider 分类导出”。
1.2 核心约束(必须遵守)
- 不改造既有 Outlook 流程:任何 Outlook 账号的 Graph + XOAUTH2 行为不做逻辑变更(仅允许在“按账号类型路由”层面加分支)。
- 不改变现有 API 的 URL 与基本响应结构:
GET /api/emails/<email_addr>GET /api/emails/<email_addr>/extract-verificationPOST /api/emails/delete
- 遵循 PRD 10.1 决策:
accounts.client_id/accounts.refresh_token保持 NOT NULL(不做 ALTER COLUMN),IMAP 账号使用占位空字符串''。 - IMAP 不做远程删除:IMAP 类型账号的删除接口直接返回“不支持远程删除”,不执行任何远程 IMAP 操作。
- 定时刷新任务只刷新 Outlook:scheduler 查询必须过滤
account_type='outlook' OR account_type IS NULL。
1.3 Out of Scope(本期明确不做)
- 本地邮件持久化、后台轮询、规则引擎、推送/Webhook、框架迁移、IMAP 远程删除。
二、数据模型与迁移设计(SQLite)
2.1 accounts 表新增字段(兼容旧数据)
目标是在不破坏既有表结构的情况下,新增 IMAP 所需配置字段:
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
account_type |
TEXT | 'outlook' |
'outlook' / 'imap'(老数据自动视为 outlook) |
provider |
TEXT | 'outlook' |
'outlook' / 'gmail' / 'qq' / '163' / '126' / 'yahoo' / 'aliyun' / 'custom' |
imap_host |
TEXT | NULL | IMAP 服务器地址(IMAP 必填,Outlook 忽略) |
imap_port |
INTEGER | 993 | IMAP 端口(默认 SSL 993) |
imap_password |
TEXT | NULL | IMAP 授权码/应用专用密码(加密存储) |
迁移方式(在 outlook_web/db.py:init_db() 中做幂等迁移):
ALTER TABLE accounts ADD COLUMN account_type TEXT DEFAULT 'outlook';
ALTER TABLE accounts ADD COLUMN provider TEXT DEFAULT 'outlook';
ALTER TABLE accounts ADD COLUMN imap_host TEXT;
ALTER TABLE accounts ADD COLUMN imap_port INTEGER DEFAULT 993;
ALTER TABLE accounts ADD COLUMN imap_password TEXT;
注:SQLite
ALTER TABLE ADD COLUMN可幂等实现需“先检测列是否存在”,实现方式:PRAGMA table_info(accounts)扫描列名后再执行对应ALTER。
2.2 既有 NOT NULL 约束的处理(PRD 10.1)
由于 client_id / refresh_token 为 NOT NULL:
- Outlook:按现有逻辑存储真实值(refresh_token 仍加密存储)。
- IMAP:写入占位空字符串:
client_id = ''refresh_token = ''password = ''(保持password语义为 Outlook 账号登录密码/展示用;IMAP 密码独立存放在imap_password)
2.3 敏感字段加密迁移
现有 migrate_sensitive_data() 已覆盖 password、refresh_token 的加密迁移。
本期新增:
- 将
imap_password纳入迁移逻辑(若发现非加密明文则执行encrypt_data())。
2.4 Schema 版本
outlook_web/db.py 当前 DB_SCHEMA_VERSION = 2,本需求涉及 accounts 表新增列,需升级为:
DB_SCHEMA_VERSION = 3- 在迁移成功后写入 settings:
db_schema_version=3
三、邮箱提供商配置(后端常量)
3.1 providers 常量文件
新增文件:outlook_web/services/providers.py
包含两部分:
MAIL_PROVIDERS:用于前端下拉展示与默认 IMAP host/port 注入(不入库)。PROVIDER_FOLDER_MAP:IMAP 文件夹名映射(对齐 Outlook 的inbox/junkemail/deleteditems三类)。
关键规则:
provider='custom':必须由用户提供imap_host/imap_port,其余 provider 从常量读取默认值。- 文件夹映射中允许多候选值(按顺序尝试
SELECT成功的那一个)。 - QQ/163 的非 ASCII 文件夹名可能为 IMAP UTF-7 编码(如
&V4NXPpCuTvY-),实现时需验证并保留扩展点。
3.2 providers API
新增 API:
GET /api/providers
返回结构(稳定、可扩展):
{
"success": true,
"providers": [
{"key":"outlook","label":"Outlook","account_type":"outlook","note":"..."},
{"key":"qq","label":"QQ 邮箱","account_type":"imap","note":"..."}
]
}
四、IMAP 通用服务设计(新增)
4.1 新增 imap_generic 服务
新增文件:outlook_web/services/imap_generic.py
职责:
- 使用
imaplib.IMAP4_SSL(host, port)建立连接 - 使用
connection.login(email, imap_password)登录 - 通过 providers 的 folder map 将
folder(inbox/junkemail/deleteditems)映射到实际 IMAP 文件夹名 - 统一输出邮件列表/详情结构,尽量对齐现有
api_get_emails()的 JSON 字段(id/subject/from/date/body_preview/is_read/has_attachments)
4.2 公共返回结构(建议)
邮件列表:
{
"success": true,
"emails": [
{
"id": "<message_uid_or_message_id>",
"subject": "...",
"from": "...",
"date": "...",
"is_read": false,
"has_attachments": false,
"body_preview": "..."
}
],
"method": "IMAP (Generic)",
"has_more": false
}
说明:
- IMAP 天然分页成本较高,本期可以先保持
has_more=false(与现有 Outlook IMAP 回退一致)。 id推荐使用 IMAP UID(稳定且可 fetch),并在get_email_detail_imap_generic()中以 UID fetch。
4.3 错误处理与脱敏
IMAP 错误常见场景:
- 认证失败(授权码错误/应用密码失效)
- 服务器不可达/端口错误
- SSL 握手失败
- Folder 不存在
要求:
- 对外错误信息不得包含
imap_password、refresh_token 等敏感信息。 outlook_web/errors.py的错误载荷(如build_error_payload)可复用,建议为 IMAP 定义 code:IMAP_AUTH_FAILEDIMAP_CONNECT_FAILEDIMAP_FOLDER_NOT_FOUND
五、账号导入/编辑/导出(后端 + repo)
5.1 Repository 层改造(accounts)
文件:outlook_web/repositories/accounts.py
改造点:
load_accounts()/get_account_by_*():补充解密imap_password(若存在),并确保返回包含account_type/provider/imap_host/imap_port字段。add_account():扩展签名支持新增字段;并对imap_password进行加密存储。update_account():根据account_type放宽校验规则:- Outlook:仍要求
client_id/refresh_token非空 - IMAP:允许
client_id=''/refresh_token='',但要求imap_password与imap_host(custom)满足约束
- Outlook:仍要求
5.2 添加账号 API(兼容旧接口 + 新增 IMAP 模式)
文件:outlook_web/controllers/accounts.py,接口:POST /api/accounts
兼容策略:
- 旧请求(Outlook 批量导入)保持不变:继续支持
account_string格式email----password----client_id----refresh_token。 - 新增 IMAP:允许前端传入结构化 JSON(单个添加)或“先选 provider 再提交文本”的批量导入。
建议请求体格式:
- Outlook(保持现状):
{ "account_string": "a@outlook.com----pwd----client_id----refresh_token", "group_id": 1 }
- IMAP(批量导入,provider 非 custom):
{
"provider": "qq",
"group_id": 1,
"account_string": "user1@qq.com----授权码1\nuser2@qq.com----授权码2"
}
- IMAP(批量导入,provider=custom):
{
"provider": "custom",
"group_id": 1,
"account_string": "user@custom.com----密码----imap.custom.com----993"
}
解析规则:
- 若
provider缺失或为outlook:按旧逻辑解析 4 段。 - 若
provider为 IMAP:- 非 custom:每行 2 段(email----imap_password)
- custom:每行 4 段(email----imap_password----imap_host----imap_port)
入库规则(与 PRD 对齐):
- IMAP:
password=''、client_id=''、refresh_token='',真实凭据写入imap_password(加密)并填充imap_host/imap_port/provider/account_type='imap'。 - Outlook:保持原字段语义不变,同时写入
account_type='outlook'、provider='outlook'。
5.3 导出账号(按 provider 分类)
文件:outlook_web/controllers/accounts.py
接口:
GET /api/accounts/export(现有接口,保持不变)POST /api/accounts/export-selected(现有接口,保持不变)
导出格式(PRD 10.3):
# === Outlook 账号 ===
user1@outlook.com----password----client_id----refresh_token
# === IMAP 账号(QQ邮箱)===
user1@qq.com----授权码----qq
# === IMAP 账号(自定义)===
user@custom.com----密码----custom----imap.custom.com----993
实现要点:
- 导出必须使用
load_accounts()(自动解密)后再拼接。 - 需要保证 Outlook 在前、IMAP 在后;IMAP 再按 provider 分组。
- 注意:导出文件包含敏感数据(refresh_token / imap_password),必须保留“二次验证”机制不变。
六、邮件 API 路由与行为(按账号类型分发)
6.1 邮件列表 GET /api/emails/<email_addr>
文件:outlook_web/controllers/emails.py
改造原则:
account_type='outlook' or NULL:继续走现有逻辑(Graph -> IMAP XOAUTH2 回退)。account_type='imap':直接走imap_generic,不再尝试 Graph,也不走 XOAUTH2 IMAP。
文件夹参数保持不变:
folder=inbox|junkemail|deleteditems
IMAP folder 映射流程:
- 根据
provider取PROVIDER_FOLDER_MAP[provider],无则退回_default。 - 依次尝试候选文件夹名,找到第一个可
SELECT的文件夹。
6.2 邮件详情(建议补齐统一入口)
当前验证码提取会需要“获取邮件详情”,建议为 IMAP 新增:
imap_generic.get_email_detail_imap_generic(...)
并在 extract-verification 中复用该能力。
6.3 验证码提取 GET /api/emails/<email_addr>/extract-verification
文件:outlook_web/controllers/emails.py
保持外部接口不变,仅内部按账号类型路由:
- Outlook:保持现有流程(Graph/IMAP XOAUTH2 获取最新邮件 + 详情)。
- IMAP:
imap_generic.get_emails_imap_generic(folder='INBOX', top=1)imap_generic.get_email_detail_imap_generic(message_id)- 复用
outlook_web/services/verification_extract.py:extract_verification_info()
七、删除 API 行为调整(IMAP 禁止远程删除)
接口:POST /api/emails/delete
文件:outlook_web/controllers/emails.py
行为:
- Outlook:保持原实现(
email_delete_service+ Graph API 远程永久删除)。 - IMAP:直接返回:
{ "success": false, "error": "IMAP 邮箱不支持远程删除,请在邮箱客户端中操作" }
并记录审计日志(操作类型 delete,对象 imap_email)。
八、前端交互设计(模板 + JS)
8.1 添加账号模态框:增加 provider 选择与动态表单
文件:templates/partials/modals.html
改造点(保持现有结构尽量少改):
- 在“选择分组”上方或下方新增“邮箱类型/提供商”下拉:
- 默认
Outlook - 选项来自
GET /api/providers或前端常量(建议后端提供,避免重复维护)
- 默认
- 根据 provider 动态更新:
- textarea placeholder
- 格式提示(form-hint)
- custom 时显示
imap_host/imap_port输入(单个添加模式)或提示批量格式含 host/port
8.2 addAccount() 请求体适配
文件:static/js/features/accounts.js
当前实现固定发送:
{ account_string: input, group_id: groupId }
调整为:
- Outlook:保持原字段
- IMAP:增加
provider字段(以及 custom 的 host/port,若采用结构化输入模式)
8.3 账号列表显示:增加类型标识
文件:templates/index.html + static/js/features/accounts.js
后端 GET /api/accounts 需要在返回列表中包含:
account_typeprovider
前端在账号卡片上展示 provider label tag(如 [QQ邮箱]、[Gmail]、[自定义]),用于快速识别。
8.4 编辑账号(建议最小可用)
文件:templates/partials/modals.html + static/js/features/accounts.js
建议:
- Outlook:保持原字段(password/client_id/refresh_token)
- IMAP:增加可编辑字段:
- provider(只读或可改,建议先只读)
- imap_password(可选更新,不回显明文;支持重新设置)
- imap_host/imap_port(custom 必填;非 custom 只读展示)
九、定时刷新任务与刷新按钮兼容性
9.1 Scheduler 过滤(必须)
文件:outlook_web/services/scheduler.py 或相关查询处
将原查询:
FROM accounts WHERE status = 'active'
改为:
FROM accounts
WHERE status = 'active'
AND (account_type = 'outlook' OR account_type IS NULL)
9.2 刷新按钮(PRD 10.2)
IMAP 账号保留刷新按钮,点击走现有逻辑可能失败(Graph token 获取失败),属于“已知但可接受”的行为。
后续优化(不在本期验收范围):
- 将 IMAP 刷新按钮改为“测试连接”。
十、实施顺序(建议)
- Phase 1(后端基础)
db.py:schema version 升级 + accounts 新列迁移 + imap_password 加密迁移services/providers.py:providers + folder mapservices/imap_generic.py:IMAP 通用服务
- Phase 2(业务接入)
repositories/accounts.py:CRUD 支持新字段 + 解密/加密controllers/accounts.py:新增 IMAP 导入解析 + providers API + 导出格式分组controllers/emails.py:按 account_type 路由(列表/验证码/删除分支)services/scheduler.py:过滤 account_type
- Phase 3(前端)
templates/partials/modals.html:添加账号表单改造static/js/features/accounts.js:导入逻辑请求体/提示适配- 账号列表 tag 展示
十一、验收检查表
P0(必须)
- 旧 Outlook 账号:
GET /api/emails/<email>仍优先 Graph,失败后仍可 XOAUTH2 IMAP 回退 - 旧 Outlook 批量导入格式仍可用:
email----password----client_id----refresh_token - 新增 IMAP 账号可导入(QQ/163/Gmail 任一)并可拉取邮件列表/详情
- 新增 IMAP 账号可执行验证码提取(至少能从最新一封邮件中提取到验证码或明确返回未找到)
- IMAP 账号调用删除接口返回“不支持远程删除”,且没有远程副作用
- 定时刷新任务不再处理 IMAP 账号(仅 Outlook)
imap_password在 DB 中为加密串(不以明文出现)
P1(建议)
GET /api/providers返回 providers 列表,前端下拉可用- 导出文件按 Outlook/IMAP provider 分类,且包含 provider 标识(满足 PRD 10.3)
- 账号列表可清晰区分 Outlook 与 IMAP
P2(可选)
- IMAP 文件夹映射覆盖垃圾箱/已删除(至少 gmail/qq/163 之一验证通过)
- 错误码与用户提示更精细(认证失败/连接失败/文件夹不存在)
十二、回滚与风险控制
- 数据库升级前必须提示备份(现有
init_db()已具备升级提示机制),建议升级 trace_id 可追踪。 - 增量迁移仅
ADD COLUMN,回滚主要依赖 DB 备份文件。 - IMAP 连接失败、授权码错误等问题必须保证:
- 不影响同分组下 Outlook 账号正常使用
- 错误消息脱敏,不泄漏密码/token
文档版本:V1.0 | 已基于 Sequential Thinking 进行模块化拆解 | 状态:草案