核心变更: - 数据库: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>
160 lines
5.5 KiB
Python
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
|