Files
outlookEmailPlus/docs/FD/FD-00005-多邮箱统一管理.md
T
胡绍康andClaude Opus 4.6 64d8a6c357 feat(multi-mailbox): 实现多邮箱统一管理功能(PRD-00005)
核心变更:
- 数据库: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>
2026-03-03 22:54:39 +08:00

16 KiB
Raw Blame History

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/前端调用方式)前提下:

  1. 支持新增 IMAP 类型邮箱(Gmail / QQ / 163 / 126 / Yahoo / 阿里邮箱 / 自定义 IMAP)。
  2. 在同一界面统一管理:账号新增、分组复用、邮件列表/详情、验证码提取。
  3. IMAP 账号支持授权码/应用专用密码的加密存储与脱敏展示。
  4. 导入/导出兼容:Outlook 保持原格式;IMAP 提供新格式与“按 provider 分类导出”。

1.2 核心约束(必须遵守)

  1. 不改造既有 Outlook 流程:任何 Outlook 账号的 Graph + XOAUTH2 行为不做逻辑变更(仅允许在“按账号类型路由”层面加分支)。
  2. 不改变现有 API 的 URL 与基本响应结构:
    • GET /api/emails/<email_addr>
    • GET /api/emails/<email_addr>/extract-verification
    • POST /api/emails/delete
  3. 遵循 PRD 10.1 决策:accounts.client_id / accounts.refresh_token 保持 NOT NULL(不做 ALTER COLUMN),IMAP 账号使用占位空字符串 ''。
  4. IMAP 不做远程删除:IMAP 类型账号的删除接口直接返回“不支持远程删除”,不执行任何远程 IMAP 操作。
  5. 定时刷新任务只刷新 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

包含两部分:

  1. MAIL_PROVIDERS:用于前端下拉展示与默认 IMAP host/port 注入(不入库)。
  2. 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_FAILED
    • IMAP_CONNECT_FAILED
    • IMAP_FOLDER_NOT_FOUND

五、账号导入/编辑/导出(后端 + repo)

5.1 Repository 层改造(accounts)

文件:outlook_web/repositories/accounts.py

改造点:

  1. load_accounts() / get_account_by_*():补充解密 imap_password(若存在),并确保返回包含 account_type/provider/imap_host/imap_port 字段。
  2. add_account():扩展签名支持新增字段;并对 imap_password 进行加密存储。
  3. update_account():根据 account_type 放宽校验规则:
    • Outlook:仍要求 client_id / refresh_token 非空
    • IMAP:允许 client_id='' / refresh_token='',但要求 imap_password 与 imap_host(custom)满足约束

5.2 添加账号 API(兼容旧接口 + 新增 IMAP 模式)

文件:outlook_web/controllers/accounts.py,接口:POST /api/accounts

兼容策略:

  • 旧请求(Outlook 批量导入)保持不变:继续支持 account_string 格式 email----password----client_id----refresh_token。
  • 新增 IMAP:允许前端传入结构化 JSON(单个添加)或“先选 provider 再提交文本”的批量导入。

建议请求体格式:

  1. Outlook(保持现状):
{ "account_string": "a@outlook.com----pwd----client_id----refresh_token", "group_id": 1 }
  1. IMAP(批量导入,provider 非 custom):
{
  "provider": "qq",
  "group_id": 1,
  "account_string": "user1@qq.com----授权码1\nuser2@qq.com----授权码2"
}
  1. 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 映射流程:

  1. 根据 provider 取 PROVIDER_FOLDER_MAP[provider],无则退回 _default。
  2. 依次尝试候选文件夹名,找到第一个可 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:
    1. imap_generic.get_emails_imap_generic(folder='INBOX', top=1)
    2. imap_generic.get_email_detail_imap_generic(message_id)
    3. 复用 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

改造点(保持现有结构尽量少改):

  1. 在“选择分组”上方或下方新增“邮箱类型/提供商”下拉:
    • 默认 Outlook
    • 选项来自 GET /api/providers 或前端常量(建议后端提供,避免重复维护)
  2. 根据 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_type
  • provider

前端在账号卡片上展示 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 刷新按钮改为“测试连接”。

十、实施顺序(建议)

  1. Phase 1(后端基础)
    • db.py:schema version 升级 + accounts 新列迁移 + imap_password 加密迁移
    • services/providers.py:providers + folder map
    • services/imap_generic.py:IMAP 通用服务
  2. Phase 2(业务接入)
    • repositories/accounts.py:CRUD 支持新字段 + 解密/加密
    • controllers/accounts.py:新增 IMAP 导入解析 + providers API + 导出格式分组
    • controllers/emails.py:按 account_type 路由(列表/验证码/删除分支)
    • services/scheduler.py:过滤 account_type
  3. 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 进行模块化拆解 | 状态:草案