Files
outlookEmailPlus/outlook_web/services/providers.py
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

130 lines
4.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from __future__ import annotations
from typing import Any, Dict, List
# 对齐:PRD-00005 / FD-00005 / TDD-00005
# 职责:集中维护“邮箱提供商”元数据与 IMAP 文件夹映射,避免前后端重复维护默认 host/port 与 folder 兼容策略。
# 邮箱提供商配置(用于前端选择与默认 IMAP host/port)
MAIL_PROVIDERS: Dict[str, Dict[str, Any]] = {
"outlook": {
"label": "Outlook",
"imap_host": "outlook.live.com",
"imap_port": 993,
"account_type": "outlook",
"note": "使用 OAuth2 认证(client_id + refresh_token)",
},
"gmail": {
"label": "Gmail",
"imap_host": "imap.gmail.com",
"imap_port": 993,
"account_type": "imap",
"note": "需开启 IMAP,并使用应用专用密码(非登录密码)",
},
"qq": {
"label": "QQ 邮箱",
"imap_host": "imap.qq.com",
"imap_port": 993,
"account_type": "imap",
"note": "需开启 IMAP 服务,使用授权码(非 QQ 密码)",
},
"163": {
"label": "163 邮箱",
"imap_host": "imap.163.com",
"imap_port": 993,
"account_type": "imap",
"note": "需开启 IMAP 服务,使用授权码",
},
"126": {
"label": "126 邮箱",
"imap_host": "imap.126.com",
"imap_port": 993,
"account_type": "imap",
"note": "需开启 IMAP 服务,使用授权码",
},
"yahoo": {
"label": "Yahoo 邮箱",
"imap_host": "imap.mail.yahoo.com",
"imap_port": 993,
"account_type": "imap",
"note": "需在账号安全设置中生成应用密码",
},
"aliyun": {
"label": "阿里邮箱",
"imap_host": "imap.aliyun.com",
"imap_port": 993,
"account_type": "imap",
"note": "使用阿里邮箱登录密码",
},
"custom": {
"label": "自定义 IMAP",
"imap_host": "",
"imap_port": 993,
"account_type": "imap",
"note": "请手动填写 IMAP 服务器地址和端口",
},
}
# provider -> 逻辑文件夹名(inbox/junkemail/deleteditems)-> 候选 IMAP 文件夹名列表
PROVIDER_FOLDER_MAP: Dict[str, Dict[str, List[str]]] = {
"gmail": {
"inbox": ["INBOX"],
"junkemail": ["[Gmail]/Spam", "[Gmail]/垃圾邮件"],
"deleteditems": ["[Gmail]/Trash", "[Gmail]/已删除邮件"],
},
"qq": {
"inbox": ["INBOX"],
"junkemail": ["Junk", "&V4NXPpCuTvY-"],
"deleteditems": ["Deleted Messages", "&XfJT0ZABkK5O9g-"],
},
"163": {
"inbox": ["INBOX"],
"junkemail": ["&V4NXPpCuTvY-"],
"deleteditems": ["&XfJT0ZABkK5O9g-"],
},
"yahoo": {
"inbox": ["INBOX"],
"junkemail": ["Bulk Mail"],
"deleteditems": ["Trash"],
},
"_default": {
"inbox": ["INBOX"],
"junkemail": ["Junk", "Spam", "SPAM", "Bulk Mail"],
"deleteditems": ["Trash", "Deleted", "Deleted Messages"],
},
}
def get_imap_folder_candidates(provider: str, folder: str) -> List[str]:
"""
根据 provider 和逻辑文件夹名(inbox/junkemail/deleteditems),
返回候选 IMAP 文件夹名列表(按优先级排序)。
不存在的 provider 退回 _default。
"""
provider_key = (provider or "").strip() or "_default"
folder_key = (folder or "").strip().lower() or "inbox"
folder_map = PROVIDER_FOLDER_MAP.get(provider_key, PROVIDER_FOLDER_MAP["_default"])
return folder_map.get(
folder_key, PROVIDER_FOLDER_MAP["_default"].get(folder_key, ["INBOX"])
)
def get_provider_list() -> List[Dict[str, Any]]:
"""返回供前端展示的 provider 列表(顺序固定:outlook 在前,custom 在后)"""
order = ["outlook", "gmail", "qq", "163", "126", "yahoo", "aliyun", "custom"]
result: List[Dict[str, Any]] = []
for key in order:
if key not in MAIL_PROVIDERS:
continue
p = MAIL_PROVIDERS[key]
result.append(
{
"key": key,
"label": p.get("label", key),
"account_type": p.get("account_type", "imap" if key != "outlook" else "outlook"),
"note": p.get("note", ""),
}
)
return result