Files
outlookEmailPlus/outlook_web/app.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

160 lines
5.5 KiB
Python

from __future__ import annotations
from typing import Optional
_APP_INSTANCE = None
def create_app(*, autostart_scheduler: Optional[bool] = None):
"""
应用工厂(迁移期实现):
- 统一装配入口,便于测试与后续 Blueprint/分层拆分
- 控制 import-time 副作用:初始化/调度器启动放到 create_app 中受控执行
- routes 采用 Blueprint 模块化注册(URL 不变)
"""
global _APP_INSTANCE
if _APP_INSTANCE is None:
from pathlib import Path
from flask import Flask
from werkzeug.exceptions import HTTPException
from werkzeug.middleware.proxy_fix import ProxyFix
from outlook_web import config
from outlook_web.db import init_db, register_db
from outlook_web.middleware import (
attach_trace_id_and_normalize_errors,
ensure_trace_id,
handle_exception,
handle_http_exception,
)
from outlook_web.routes import (
accounts,
audit,
emails,
groups,
oauth,
pages,
scheduler,
settings,
system,
tags,
temp_emails,
)
from outlook_web.security.csrf import init_csrf
# 初始化(DB/目录等)
repo_root = Path(__file__).resolve().parents[1]
templates_dir = repo_root / "templates"
static_dir = repo_root / "static"
# 确保目录存在
templates_dir.mkdir(parents=True, exist_ok=True)
static_dir.mkdir(parents=True, exist_ok=True)
# 确保数据目录存在
data_dir = config.get_database_path()
if data_dir:
import os
os.makedirs(
os.path.dirname(data_dir) if os.path.dirname(data_dir) else ".",
exist_ok=True,
)
# 初始化数据库
init_db()
app = Flask(
__name__,
template_folder=str(templates_dir),
static_folder=str(static_dir),
static_url_path="/static",
)
# 注入版本号到所有模板(用于 UI 显示)
from outlook_web import __version__ as APP_VERSION
@app.context_processor
def inject_app_version():
return {"APP_VERSION": APP_VERSION}
app.secret_key = config.require_secret_key()
app.config["PERMANENT_SESSION_LIFETIME"] = 60 * 60 * 24 * 7 # 7 天
app.config["SESSION_COOKIE_HTTPONLY"] = True
app.config["SESSION_COOKIE_SAMESITE"] = "Lax"
# ProxyFix 中间件(仅在配置启用时应用)
# 注意:启用前必须配置 TRUSTED_PROXIES 环境变量
if config.get_proxy_fix_enabled():
app.wsgi_app = ProxyFix(
app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_prefix=1
)
# DB teardown(请求结束释放连接)
register_db(app)
# 配置日志(确保 outlook_web 命名空间的日志输出到 stderr)
import logging
import sys
_log_handler = logging.StreamHandler(sys.stderr)
_log_handler.setFormatter(
logging.Formatter("%(asctime)s %(name)s %(levelname)s %(message)s", datefmt="%H:%M:%S")
)
_ow_logger = logging.getLogger("outlook_web")
if not _ow_logger.handlers:
_ow_logger.addHandler(_log_handler)
_ow_logger.setLevel(logging.INFO)
# CSRF(可选)
_csrf, csrf_exempt, _generate_csrf = init_csrf(app)
# trace_id + error 结构标准化(迁移到 middleware 模块)
app.before_request(ensure_trace_id)
app.after_request(attach_trace_id_and_normalize_errors)
app.register_error_handler(HTTPException, handle_http_exception)
app.register_error_handler(Exception, handle_exception)
# Blueprint 路由注册(URL 不变)
app.register_blueprint(pages.create_blueprint(csrf_exempt=csrf_exempt))
app.register_blueprint(groups.create_blueprint())
app.register_blueprint(tags.create_blueprint())
app.register_blueprint(accounts.create_blueprint())
app.register_blueprint(emails.create_blueprint())
app.register_blueprint(temp_emails.create_blueprint(csrf_exempt=csrf_exempt))
app.register_blueprint(oauth.create_blueprint())
app.register_blueprint(settings.create_blueprint())
app.register_blueprint(scheduler.create_blueprint())
app.register_blueprint(system.create_blueprint())
app.register_blueprint(audit.create_blueprint())
# 打印初始化信息
print("=" * 60)
print("Outlook 邮件 Web 应用已初始化")
print(f"数据库文件: {config.get_database_path()}")
print(f"GPTMail API: {config.get_gptmail_base_url()}")
print("=" * 60)
_APP_INSTANCE = app
# 调度器启动控制
if autostart_scheduler is None:
from outlook_web.services import graph as graph_service
from outlook_web.services import scheduler as scheduler_service
if scheduler_service.should_autostart_scheduler():
scheduler_service.init_scheduler(
_APP_INSTANCE, graph_service.test_refresh_token
)
elif autostart_scheduler:
from outlook_web.services import graph as graph_service
from outlook_web.services import scheduler as scheduler_service
scheduler_service.init_scheduler(
_APP_INSTANCE, graph_service.test_refresh_token
)
return _APP_INSTANCE