核心变更: - 数据库: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>
20 KiB
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 TEXTimap_port INTEGER DEFAULT 993imap_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
- 以 UID fetch 完整邮件(
- 验证:
- 认证失败时返回
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 账号读取行为不受影响
- 添加 IMAP 账号后,DB 中
- 提交:
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)
- 非 custom:2 段格式(
- 调用
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邮箱)===
- Outlook 账号放前,格式:
-
确认导出的"二次验证"机制(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 账号验证码提取能从最新邮件中提取(或明确返回"未找到")
- Outlook 账号:
-
提交:
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 账号的定时刷新正常执行,不受影响
- 存在 IMAP 账号时,定时刷新任务的账号列表中不包含
- 提交:
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)
- IMAP 服务器地址输入框(
- 在 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