Files
outlookEmailPlus/docs/TODO/TODO-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

20 KiB
Raw Blame History

TODO-00005|多邮箱统一管理 — 任务待办清单

  • 文档状态: 草案
  • 版本: V1.0
  • 日期: 2026-03-02
  • 对齐 PRD: docs/PRD/PRD-00005-多邮箱统一管理.md
  • 对齐 FD: docs/FD/FD-00005-多邮箱统一管理.md
  • 对齐 TDD: docs/TDD/TDD-00005-多邮箱统一管理.md

前置准备

0.1 文档确认

  • PRD 文档已完成(docs/PRD/PRD-00005-多邮箱统一管理.md)
  • FD 文档已完成(docs/FD/FD-00005-多邮箱统一管理.md)
  • TDD 文档已完成(docs/TDD/TDD-00005-多邮箱统一管理.md)
  • 阅读并理解 PRD / FD / TDD 所有约束条款
  • 明确 P0 验收标准(TDD §12.2)

0.2 环境准备

  • 确认开发环境正常(Python、Flask、SQLite)
  • 确认依赖已安装:pip install -r requirements.txt
  • 确认数据库文件存在且可访问:data/outlook_accounts.db

0.3 基线测试(改动前必做)

  • 运行所有现有测试:python -m unittest discover -s tests -v
  • 记录当前全部测试通过的数量(作为基线)
  • 手动验证以下核心功能正常:
    • Outlook 账号登录 / 邮件读取 / 验证码提取
    • Graph API 正常 → IMAP XOAUTH2 回退正常
    • 批量导入 email----password----client_id----refresh_token 格式正常
    • 定时刷新任务正常

0.4 Git 准备

  • 创建功能分支:git checkout -b feature/multi-mailbox-support
  • 确认工作区干净:git status
  • 记录当前 commit hash(用于回滚参考)

P0(必须完成)


阶段 1:后端基础

目标:完成数据库迁移、邮箱提供商常量、通用 IMAP 服务。
涉及文件:db.py、services/providers.py(新增)、services/imap_generic.py(新增)


1.1 outlook_web/db.py — Schema v2 → v3 迁移

  • 将 DB_SCHEMA_VERSION 从 2 改为 3
  • 将原有 PRAGMA table_info(accounts) 查询与下方新增列检测合并为一次查询(避免重复 PRAGMA)
  • 在幂等迁移块中追加以下 5 列(先检测列是否存在,再执行 ALTER):
    • account_type TEXT DEFAULT 'outlook'
    • provider TEXT DEFAULT 'outlook'
    • imap_host TEXT
    • imap_port INTEGER DEFAULT 993
    • imap_password TEXT
  • 在 migrate_sensitive_data() 中追加对 imap_password 的加密迁移逻辑(检测 is_encrypted,非加密则调用 encrypt_data)
  • 验证:
    • 新数据库初始化后 PRAGMA table_info(accounts) 包含 5 个新列
    • 旧数据库升级后,原有 Outlook 账号的 account_type 默认为 'outlook'
    • db_schema_version 在 settings 表中写入为 3
  • 提交:git commit -m "feat: db schema v3, add IMAP columns to accounts"

1.2 outlook_web/services/providers.py — 新增文件

  • 创建文件 outlook_web/services/providers.py
  • 编写 MAIL_PROVIDERS 字典常量(含 outlook / gmail / qq / 163 / 126 / yahoo / aliyun / custom 共 8 个 provider)
  • 编写 PROVIDER_FOLDER_MAP 字典常量(含 gmail / qq / 163 / yahoo / _default 5 个 provider 的 inbox / junkemail / deleteditems 映射)
  • 编写 get_imap_folder_candidates(provider, folder) 函数(返回候选文件夹名列表,缺省退回 _default)
  • 编写 get_provider_list() 函数(返回固定顺序的 provider 列表供前端使用)
  • 验证:
    • get_imap_folder_candidates("qq", "junkemail") 返回 ["Junk", "&V4NXPpCuTvY-"]
    • get_imap_folder_candidates("unknown", "inbox") 退回 _default,返回 ["INBOX"]
    • get_provider_list() 返回列表,outlook 位于第一位,custom 位于最后
  • 提交:git commit -m "feat: add services/providers.py with MAIL_PROVIDERS constants"

1.3 outlook_web/services/imap_generic.py — 新增文件

  • 创建文件 outlook_web/services/imap_generic.py
  • 编写内部辅助函数 _resolve_imap_folder(mail, candidates)(按优先级 SELECT 文件夹,返回第一个成功的文件夹名)
  • 编写 get_emails_imap_generic(email_addr, imap_password, imap_host, imap_port, folder, provider, skip, top) 函数:
    • 使用 imaplib.IMAP4_SSL(imap_host, imap_port) 建立连接(含超时设置)
    • 使用 mail.login(email_addr, imap_password) 登录
    • 调用 get_imap_folder_candidates() 获取候选文件夹,调用 _resolve_imap_folder() 探测
    • 使用 mail.uid('SEARCH', ...) 获取 UID 列表,按倒序截取 skip+top 封
    • 使用 mail.uid('FETCH', ...) 获取邮件头(subject / from / date / flags)
    • 返回格式:{"success": True, "emails": [...], "method": "IMAP (Generic)", "has_more": False}
    • 每封邮件字段:id(IMAP UID 字符串)、subject、from、date、is_read、has_attachments、body_preview
    • finally 块中调用 mail.logout()
    • 错误时返回 {"success": False, "error": "<脱敏消息>", "error_code": "IMAP_AUTH_FAILED|IMAP_CONNECT_FAILED|IMAP_FOLDER_NOT_FOUND"}
    • 所有 except 块中,错误信息通过 sanitize_error_details() 脱敏(不暴露 imap_password)
  • 编写 get_email_detail_imap_generic(email_addr, imap_password, imap_host, imap_port, message_id, folder, provider) 函数:
    • 以 UID fetch 完整邮件(BODY[])
    • 解析 MIME,提取 body_text(text/plain)和 body_html(text/html)
    • 失败时返回 None
  • 验证:
    • 认证失败时返回 error_code="IMAP_AUTH_FAILED",错误消息不包含密码
    • 文件夹不存在时返回 error_code="IMAP_FOLDER_NOT_FOUND"
    • 正常拉取时 success=True,emails 为列表
  • 提交:git commit -m "feat: add services/imap_generic.py for standard IMAP access"

1.4 阶段 1 验收

  • 运行所有测试:python -m unittest discover -s tests -v
  • 确认测试数量不少于基线(新增模块不破坏旧测试)
  • 手动确认 Outlook 账号邮件读取功能完全正常(回归)
  • 手动确认定时刷新任务不受影响

阶段 2:业务接入

目标:Repository / Controller / Scheduler 按 account_type 路由分发,IMAP 账号增删改查全流程打通。
涉及文件:repositories/accounts.py、controllers/accounts.py、controllers/emails.py、services/scheduler.py


2.1 outlook_web/repositories/accounts.py — 改造

  • 在 load_accounts() 的 for row in rows 循环中,在现有 password / refresh_token 解密后,追加 imap_password 解密逻辑(解密失败不影响其他字段)
  • 扩展 add_account() 函数签名,新增 5 个可选参数:account_type、provider、imap_host、imap_port、imap_password
  • 在 add_account() 中,若 imap_password 不为空,调用 encrypt_data(imap_password) 后再入库
  • 在 add_account() 中,INSERT 语句补充 5 个新字段
  • 在 add_account() 中,增加校验分支:
    • account_type='imap':要求 imap_password 非空;provider='custom' 时要求 imap_host 非空
    • account_type='outlook'(或默认):要求 client_id / refresh_token 非空
  • 验证:
    • 添加 IMAP 账号后,DB 中 imap_password 为加密串,client_id / refresh_token 为 ''
    • load_accounts() 返回的账号包含 account_type、provider、imap_host、imap_port、imap_password(已解密)
    • 现有 Outlook 账号读取行为不受影响
  • 提交:git commit -m "feat: extend repositories/accounts to support IMAP fields"

2.2 outlook_web/controllers/accounts.py — 改造

2.2.1 POST /api/accounts — 解析 IMAP 导入格式
  • 读取请求中的 provider 字段(默认为 'outlook')
  • 若 provider 为 'outlook' 或缺失:保持现有 4 段格式解析逻辑不动
  • 若 provider 为其他 IMAP provider,新增分支:
    • 从 MAIL_PROVIDERS 读取 imap_host / imap_port 默认值
    • 按行解析 account_string:
      • 非 custom:2 段格式(email----imap_password)
      • custom:4 段格式(email----imap_password----imap_host----imap_port)
    • 调用 add_account(... account_type='imap', provider=provider, imap_host=..., imap_port=..., imap_password=...),password=''、client_id=''、refresh_token=''
2.2.2 GET /api/providers — 新增接口
  • 注册新路由 GET /api/providers
  • 调用 get_provider_list() 并返回 {"success": True, "providers": [...]}
2.2.3 导出格式改造
  • 在导出逻辑(GET /api/accounts/export 和 POST /api/accounts/export-selected)中调用改造后的 _build_export_text() 函数:

    • Outlook 账号放前,格式:email----password----client_id----refresh_token
    • IMAP 账号按 provider 分组放后,非 custom 格式:email----imap_password----provider
    • custom 格式:email----imap_password----custom----imap_host----imap_port
    • 每个分组前加注释行:# === Outlook 账号 === / # === IMAP 账号(QQ邮箱)===
  • 确认导出的"二次验证"机制(export_verify_tokens)完全不动

  • 验证:

    • 混合 Outlook + QQ 账号导出,Outlook 在前,QQ 在后,格式符合 PRD 10.3
    • GET /api/providers 返回 8 个 provider,outlook 在前
    • 旧 Outlook 批量导入格式仍正常(4 段格式)
    • QQ 邮箱批量导入(2 段格式)正常入库,imap_password 为加密串
  • 提交:git commit -m "feat: accounts controller supports IMAP import/export and providers API"


2.3 outlook_web/controllers/emails.py — 改造

2.3.1 GET /api/emails/<email_addr> — 邮件列表路由分发
  • 在获取账号信息后,读取 account.get("account_type")
  • 若 account_type == 'imap':调用 get_emails_imap_generic(...),透传 folder / skip / top 参数
  • 若 account_type 为 'outlook' 或 None:执行现有逻辑(Graph → XOAUTH2 回退),完全不动
2.3.2 GET /api/emails/<email_addr>/extract-verification — 验证码提取路由分发
  • 若 account_type == 'imap':
    • 调用 get_emails_imap_generic(... folder='inbox', top=1) 取最新 1 封
    • 调用 get_email_detail_imap_generic(...) 取详情
    • 调用现有 extract_verification_info(detail) 提取验证码(复用,不改)
  • 若 account_type 为 'outlook' 或 None:现有逻辑完全不动
2.3.3 POST /api/emails/delete — IMAP 禁止远程删除
  • 若 account_type == 'imap':

    • 调用 log_audit(action="delete", resource_type="imap_email", ...) 记录审计日志
    • 直接返回 {"success": False, "error": "IMAP 邮箱不支持远程删除,请在邮箱客户端中操作"}
    • 不执行任何远程 IMAP 操作
  • 若 account_type 为 'outlook' 或 None:现有逻辑完全不动

  • 验证:

    • Outlook 账号:GET /api/emails/<outlook> 走现有链路,method tag 为 Graph 或 IMAP (XOAUTH2)
    • IMAP 账号:GET /api/emails/<imap> 走 imap_generic,method 为 IMAP (Generic)
    • IMAP 账号删除返回 success=False 并含错误消息,无远程副作用
    • IMAP 账号验证码提取能从最新邮件中提取(或明确返回"未找到")
  • 提交:git commit -m "feat: emails controller routes by account_type for IMAP support"


2.4 outlook_web/services/scheduler.py — 改造

  • 找到第 268 行附近的 accounts 查询语句
  • 将原查询:
    FROM accounts WHERE status = 'active'
    
    改为:
    FROM accounts
    WHERE status = 'active'
      AND (account_type = 'outlook' OR account_type IS NULL)
    
  • 验证:
    • 存在 IMAP 账号时,定时刷新任务的账号列表中不包含 account_type='imap' 的账号
    • Outlook 账号的定时刷新正常执行,不受影响
  • 提交:git commit -m "fix: scheduler skips IMAP accounts in refresh task"

2.5 阶段 2 验收

  • 运行所有测试:python -m unittest discover -s tests -v
  • 确认测试通过数量不少于基线
  • 手动回归测试(Outlook 流程全量):
    • Outlook 批量导入(4 段格式)正常
    • Outlook 邮件读取(Graph → IMAP XOAUTH2 回退)正常
    • Outlook 验证码提取正常
    • Outlook 邮件删除(Graph 远程删除)正常
    • 定时刷新任务正常(IMAP 账号被跳过)
  • 手动测试(IMAP 新功能):
    • QQ 邮箱批量导入(2 段格式)正常入库
    • QQ 邮箱邮件列表拉取正常(或按预期返回 IMAP 错误)
    • QQ 邮箱验证码提取正常
    • QQ 邮箱调用删除接口返回"不支持远程删除"
    • GET /api/providers 正常返回

阶段 3:前端改造

目标:添加账号表单支持 IMAP provider 选择,账号列表展示 provider tag。
涉及文件:templates/partials/modals.html、static/js/features/accounts.js


3.1 templates/partials/modals.html — 添加账号模态框改造

  • 在"选择分组"下拉上方(或下方)新增"邮箱类型"下拉,id="accountProvider"���绑定 onchange="onProviderChange(this.value)"
  • 在 provider 下拉之后新增"自定义 IMAP 配置"区块(id="customImapFields",默认 display:none):
    • IMAP 服务器地址输入框(id="imapHost")
    • IMAP 端口输入框(id="imapPort",默认 993)
  • 在 textarea 下方新增格式提示 div(id="accountFormatHint",动态更新文字)
  • 确保现有模态框的所有 DOM ID、CSS 类名、其他字段保持不变

3.2 static/js/features/accounts.js — 功能适配

3.2.1 页面初始化:加载 providers
  • 新增 loadProviders() 异步函数,调用 GET /api/providers 填充 #accountProvider 下拉选项
  • 在添加账号模态框打开时(或页面初始化时)调用 loadProviders()
  • 初始化完成后调用一次 onProviderChange('outlook') 设置默认提示
3.2.2 新增 onProviderChange(provider) 函数
  • provider='outlook':隐藏 customImapFields,textarea placeholder 设为 4 段格式,hint 文字更新
  • provider='custom':显示 customImapFields,textarea placeholder 设为 2 段格式,hint 提示填写服务器
  • 其他 IMAP provider:隐藏 customImapFields,placeholder 设为 2 段格式,hint 提示使用授权码
3.2.3 addAccount() 请求体适配
  • 读取 #accountProvider 的当前值
  • 若 provider 为 'outlook' 或空:发送旧格式 { account_string, group_id }(向后兼容,不改)
  • 若 provider 为其他值:发送新格式 { provider, account_string, group_id }
  • 若 provider 为 'custom':检查 imapHost 和 imapPort(若采用文本批量方式,host/port 内嵌在 account_string 中,无需额外字段)
3.2.4 账号列表:provider tag 展示
  • 新增 getProviderLabel(provider) 辅助函数(返回 provider 的中文 label)

  • 在 renderAccountList() / 账号卡片模板中,为每个账号追加 provider tag HTML(如 <span class="account-provider-tag">QQ邮箱</span>)

  • 确认 GET /api/accounts 返回的账号对象包含 account_type 和 provider 字段(已由 repository 改造保证)

  • 验证:

    • 选择 Outlook,textarea placeholder 显示 4 段格式,自定义 IMAP 配置区不显示
    • 选择 QQ邮箱,textarea placeholder 显示 2 段格式,hint 提示使用授权码
    • 选择自定义 IMAP,自定义 IMAP 配置区显示
    • 使用 Outlook 格式导入账号,功能正常(回归)
    • 使用 QQ 格式导入账号,request body 包含 provider: 'qq'
    • 账号列表中 QQ 账号卡片显示 [QQ邮箱] tag,Outlook 账号显示 [Outlook] tag
  • 提交:git commit -m "feat: frontend add provider selector and IMAP account support"


3.3 阶段 3 验收

  • 运行所有测试:python -m unittest discover -s tests -v
  • 确认测试通过数量不少于基线
  • 手动端到端测试(完整流程):
    • 打开添加账号模态框,默认为 Outlook,格式提示正确
    • 切换到 QQ邮箱,提示变为授权码格式
    • 切换到自定义 IMAP,显示服务器地址/端口输入框
    • 使用 Outlook 格式成功导入 Outlook 账号,账号列表显示 [Outlook] tag
    • 使用 QQ 格式成功导入 QQ 账号,账号列表显示 [QQ邮箱] tag
    • 点击 QQ 账号,能拉取邮件列表(或返回 IMAP 错误提示)
    • 点击 QQ 账号邮件,能获取详情
    • 对 QQ 账号执行删除,前端收到"不支持远程删除"提示

阶段 4:P0 全量验收

  • 旧 Outlook 账号:GET /api/emails/<email> 仍优先 Graph,失败后仍可 XOAUTH2 IMAP 回退 ✅
  • 旧 Outlook 批量导入格式仍可用:email----password----client_id----refresh_token ✅
  • 新增 IMAP 账号可导入(QQ / 163 / Gmail 任一)并可拉取邮件列表 / 详情 ✅
  • 新增 IMAP 账号可执行验证码提取(成功提取或明确返回"未找到") ✅
  • IMAP 账号调用删除接口返回"不支持远程删除",且无远程副作用 ✅
  • 定时刷新任务不处理 IMAP 账号 ✅
  • imap_password 在 DB 中为加密串(不以明文出现) ✅
  • 运行所有测试全部通过 ✅

P1(建议完成)

5.1 GET /api/providers 前端联动

  • 确认 GET /api/providers 正常返回 8 个 provider
  • 确认前端下拉选项来自 API(而非硬编码),避免重复维护

5.2 导出文件按 Outlook / IMAP provider 分类

  • 确认导出文件格式符合 PRD 10.3(含分组注释行)
  • 确认混合账号导出,Outlook 在前,IMAP 按 provider 分组在后

5.3 账号列表可清晰区分 Outlook 与 IMAP

  • 确认各类账号在列表中有可见的 provider tag
  • 账号卡片视觉区分明确,用户可快速识别邮箱类型

P2(可选)

6.1 IMAP 文件夹映射验证

  • 测试 Gmail / QQ / 163 至少一个 provider 的垃圾邮件文件夹映射正常
  • 测试 Gmail / QQ / 163 至少一个 provider 的已删除邮件文件夹映射正常
  • 验证 QQ/163 的 IMAP UTF-7 编码文件夹名(如 &V4NXPpCuTvY-)能被正确 SELECT

6.2 IMAP 错误提示精细化

  • 认证失败时前端提示明确(如"授权码错误,请重新生成")
  • 连接失败时前端提示明确(如"服务器不可达,请检查网络")
  • 文件夹不存在时提示明确

风险与注意事项

风险点 应对措施
DB 迁移失败 迁移前打印备份提示(现有机制已有);迁移只做 ADD COLUMN,可安全回滚至 DB 备份
IMAP 连接阻塞 socket.setdefaulttimeout(30) 设置全局超时;IMAP 错误不影响同分组 Outlook 账号
imap_password 明文泄露 所有 except 块调用 sanitize_error_details();入库时必须先 encrypt_data()
旧 Outlook 流程回归 每个阶段结束后必须回归测试 Outlook 邮件读取 + 验证码提取 + 定时刷新
前端请求体不兼容 Outlook 分支发送旧格式,向后兼容;后端按 provider 字段是否存在做区分

进度跟踪

总体进度

  • 前置准备(0%)
  • 阶段 1:后端基础(0%)
  • 阶段 2:业务接入(0%)
  • 阶段 3:前端改造(0%)
  • 阶段 4:P0 全量验收(0%)

文件改造进度

文件 状态
outlook_web/db.py ⬜ 待开始
outlook_web/services/providers.py(新增) ⬜ 待开始
outlook_web/services/imap_generic.py(新增) ⬜ 待开始
outlook_web/repositories/accounts.py ⬜ 待开始
outlook_web/controllers/accounts.py ⬜ 待开始
outlook_web/controllers/emails.py ⬜ 待开始
outlook_web/services/scheduler.py ⬜ 待开始
templates/partials/modals.html ⬜ 待开始
static/js/features/accounts.js ⬜ 待开始

参考文档


文档版本: V1.0
最后更新: 2026-03-02