diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index ffb402c..8108987 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -20,7 +20,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up Python uses: actions/setup-python@v5 @@ -36,7 +36,7 @@ jobs: run: black --check --diff . - name: Run isort (Import Sorting Check) - run: isort --check-only --diff . + run: isort --check-only --diff --profile black . - name: Run Flake8 (Linting) run: flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics @@ -53,7 +53,7 @@ jobs: actions: read steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up Python uses: actions/setup-python@v5 diff --git a/.github/workflows/docker-build-push.yml b/.github/workflows/docker-build-push.yml index fd265fa..438ec36 100644 --- a/.github/workflows/docker-build-push.yml +++ b/.github/workflows/docker-build-push.yml @@ -26,7 +26,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 diff --git a/.github/workflows/python-tests.yml b/.github/workflows/python-tests.yml index 9e350e8..931ec07 100644 --- a/.github/workflows/python-tests.yml +++ b/.github/workflows/python-tests.yml @@ -26,7 +26,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up Python uses: actions/setup-python@v5 diff --git a/outlook_mail_reader.py b/outlook_mail_reader.py index 5a69ce6..bd351d9 100644 --- a/outlook_mail_reader.py +++ b/outlook_mail_reader.py @@ -11,7 +11,7 @@ Outlook 邮件读取测试工具 import email import imaplib from email.header import decode_header -from typing import Optional, List, Dict, Any +from typing import Any, Dict, List, Optional import requests diff --git a/outlook_web/app.py b/outlook_web/app.py index 7fbbe18..5d0e173 100644 --- a/outlook_web/app.py +++ b/outlook_web/app.py @@ -18,17 +18,16 @@ def create_app(*, autostart_scheduler: Optional[bool] = None): from pathlib import Path from flask import Flask - from werkzeug.middleware.proxy_fix import ProxyFix from werkzeug.exceptions import HTTPException + from werkzeug.middleware.proxy_fix import ProxyFix from outlook_web import config - from outlook_web.db import register_db, init_db - from outlook_web.security.csrf import init_csrf + from outlook_web.db import init_db, register_db from outlook_web.middleware import ( - ensure_trace_id, attach_trace_id_and_normalize_errors, - handle_http_exception, + ensure_trace_id, handle_exception, + handle_http_exception, ) from outlook_web.routes import ( accounts, @@ -43,6 +42,7 @@ def create_app(*, autostart_scheduler: Optional[bool] = None): tags, temp_emails, ) + from outlook_web.security.csrf import init_csrf # 初始化(DB/目录等) repo_root = Path(__file__).resolve().parents[1] @@ -128,16 +128,16 @@ def create_app(*, autostart_scheduler: Optional[bool] = None): # 调度器启动控制 if autostart_scheduler is None: - from outlook_web.services import scheduler as scheduler_service 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 scheduler as scheduler_service 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 diff --git a/outlook_web/controllers/accounts.py b/outlook_web/controllers/accounts.py index 217dd9d..79cffa8 100644 --- a/outlook_web/controllers/accounts.py +++ b/outlook_web/controllers/accounts.py @@ -15,14 +15,14 @@ from outlook_web.db import get_db from outlook_web.errors import build_error_payload from outlook_web.repositories import accounts as accounts_repo from outlook_web.repositories import groups as groups_repo -from outlook_web.repositories import tags as tags_repo from outlook_web.repositories import refresh_logs as refresh_logs_repo from outlook_web.repositories import settings as settings_repo -from outlook_web.repositories.refresh_runs import create_refresh_run, finish_refresh_run +from outlook_web.repositories import tags as tags_repo from outlook_web.repositories.distributed_locks import ( acquire_distributed_lock, release_distributed_lock, ) +from outlook_web.repositories.refresh_runs import create_refresh_run, finish_refresh_run from outlook_web.security.auth import get_client_ip, get_user_agent, login_required from outlook_web.security.crypto import decrypt_data from outlook_web.services import graph as graph_service @@ -835,12 +835,12 @@ def api_export_selected_accounts() -> Any: @login_required def api_generate_export_verify_token() -> Any: """生成导出验证token(二次验证)""" + from outlook_web.repositories import settings as settings_repo from outlook_web.security.auth import ( - issue_export_verify_token, get_client_ip, get_user_agent, + issue_export_verify_token, ) - from outlook_web.repositories import settings as settings_repo from outlook_web.security.crypto import verify_password data = request.json diff --git a/outlook_web/controllers/audit.py b/outlook_web/controllers/audit.py index ff6d3bc..6e20726 100644 --- a/outlook_web/controllers/audit.py +++ b/outlook_web/controllers/audit.py @@ -7,7 +7,6 @@ from flask import jsonify, request from outlook_web.audit import query_audit_logs from outlook_web.security.auth import login_required - # ==================== 审计日志 API ==================== diff --git a/outlook_web/controllers/emails.py b/outlook_web/controllers/emails.py index 719e926..912f8c5 100644 --- a/outlook_web/controllers/emails.py +++ b/outlook_web/controllers/emails.py @@ -15,7 +15,6 @@ from outlook_web.services import email_delete as email_delete_service from outlook_web.services import graph as graph_service from outlook_web.services import imap as imap_service - # IMAP 服务器配置 IMAP_SERVER_OLD = "outlook.office365.com" IMAP_SERVER_NEW = "outlook.live.com" diff --git a/outlook_web/controllers/groups.py b/outlook_web/controllers/groups.py index 7a9ba46..9a87b77 100644 --- a/outlook_web/controllers/groups.py +++ b/outlook_web/controllers/groups.py @@ -8,6 +8,7 @@ from urllib.parse import quote from flask import Response, jsonify, request +from outlook_web.audit import log_audit from outlook_web.errors import build_error_payload from outlook_web.repositories import accounts as accounts_repo from outlook_web.repositories import groups as groups_repo @@ -18,7 +19,6 @@ from outlook_web.security.auth import ( get_user_agent, login_required, ) -from outlook_web.audit import log_audit def sanitize_input(text: str, max_length: int = 500) -> str: diff --git a/outlook_web/controllers/oauth.py b/outlook_web/controllers/oauth.py index 912dd18..e7e3cf3 100644 --- a/outlook_web/controllers/oauth.py +++ b/outlook_web/controllers/oauth.py @@ -14,7 +14,6 @@ from outlook_web.security.auth import ( login_required, ) - # OAuth 配置 OAUTH_SCOPES = [ "offline_access", diff --git a/outlook_web/controllers/pages.py b/outlook_web/controllers/pages.py index 93e11bb..7bb1507 100644 --- a/outlook_web/controllers/pages.py +++ b/outlook_web/controllers/pages.py @@ -15,7 +15,6 @@ from outlook_web.security.auth import ( ) from outlook_web.security.crypto import verify_password - # ==================== 页面路由 ==================== diff --git a/outlook_web/controllers/scheduler.py b/outlook_web/controllers/scheduler.py index 9ecce43..45b9280 100644 --- a/outlook_web/controllers/scheduler.py +++ b/outlook_web/controllers/scheduler.py @@ -12,7 +12,6 @@ from outlook_web.db import create_sqlite_connection from outlook_web.repositories import settings as settings_repo from outlook_web.security.auth import login_required - # 常量 REFRESH_LOCK_NAME = "token_refresh" diff --git a/outlook_web/controllers/settings.py b/outlook_web/controllers/settings.py index e90d522..c23d23c 100644 --- a/outlook_web/controllers/settings.py +++ b/outlook_web/controllers/settings.py @@ -11,7 +11,6 @@ from outlook_web.repositories import settings as settings_repo from outlook_web.security.auth import login_required from outlook_web.security.crypto import hash_password - # ==================== 设置 API ==================== @@ -64,10 +63,11 @@ def api_get_settings() -> Any: def api_update_settings() -> Any: """更新设置""" # 延迟导入避免循环依赖 - from outlook_web.services import scheduler as scheduler_service - from outlook_web.services import graph as graph_service from flask import current_app + from outlook_web.services import graph as graph_service + from outlook_web.services import scheduler as scheduler_service + data = request.json updated = [] errors = [] diff --git a/outlook_web/controllers/system.py b/outlook_web/controllers/system.py index fd758ff..75aefdd 100644 --- a/outlook_web/controllers/system.py +++ b/outlook_web/controllers/system.py @@ -17,7 +17,6 @@ from outlook_web.db import ( from outlook_web.repositories import settings as settings_repo from outlook_web.security.auth import login_required - # 常量 REFRESH_LOCK_NAME = "token_refresh" diff --git a/outlook_web/middleware/__init__.py b/outlook_web/middleware/__init__.py index 4c6b334..23c10ec 100644 --- a/outlook_web/middleware/__init__.py +++ b/outlook_web/middleware/__init__.py @@ -1,11 +1,11 @@ # Middleware module -from outlook_web.middleware.trace import ( - ensure_trace_id, - attach_trace_id_and_normalize_errors, -) from outlook_web.middleware.error_handler import ( - handle_http_exception, handle_exception, + handle_http_exception, +) +from outlook_web.middleware.trace import ( + attach_trace_id_and_normalize_errors, + ensure_trace_id, ) __all__ = [ diff --git a/outlook_web/repositories/distributed_locks.py b/outlook_web/repositories/distributed_locks.py index 5a1bd97..9ec2811 100644 --- a/outlook_web/repositories/distributed_locks.py +++ b/outlook_web/repositories/distributed_locks.py @@ -1,10 +1,9 @@ from __future__ import annotations +import sqlite3 import time from typing import Any, Dict, Optional -import sqlite3 - def acquire_distributed_lock( conn: sqlite3.Connection, diff --git a/outlook_web/security/csrf.py b/outlook_web/security/csrf.py index 78cc8fa..b2fd785 100644 --- a/outlook_web/security/csrf.py +++ b/outlook_web/security/csrf.py @@ -2,9 +2,9 @@ from __future__ import annotations from typing import Callable, Optional, Tuple - try: - from flask_wtf.csrf import CSRFProtect, generate_csrf as _generate_csrf # type: ignore + from flask_wtf.csrf import CSRFProtect + from flask_wtf.csrf import generate_csrf as _generate_csrf # type: ignore CSRF_AVAILABLE = True except ImportError: # pragma: no cover diff --git a/outlook_web/services/imap.py b/outlook_web/services/imap.py index 2100726..6e89c57 100644 --- a/outlook_web/services/imap.py +++ b/outlook_web/services/imap.py @@ -8,8 +8,8 @@ from typing import Any, Dict, List, Optional import requests from outlook_web.errors import build_error_payload -from outlook_web.services.http import get_response_details from outlook_web.services.graph import get_access_token_graph +from outlook_web.services.http import get_response_details # Token 端点 TOKEN_URL_IMAP = "https://login.microsoftonline.com/consumers/oauth2/v2.0/token" diff --git a/outlook_web/services/refresh.py b/outlook_web/services/refresh.py index 3c5e736..0f69ccc 100644 --- a/outlook_web/services/refresh.py +++ b/outlook_web/services/refresh.py @@ -8,13 +8,12 @@ from typing import Any, Callable, Dict, Iterator, List, Optional, Tuple from outlook_web.db import create_sqlite_connection from outlook_web.errors import build_error_payload, generate_trace_id -from outlook_web.security.crypto import decrypt_data from outlook_web.repositories.distributed_locks import ( acquire_distributed_lock, release_distributed_lock, ) from outlook_web.repositories.refresh_runs import create_refresh_run, finish_refresh_run - +from outlook_web.security.crypto import decrypt_data REFRESH_LOCK_TTL_SECONDS = 60 * 60 * 2 # 2 小时,避免异常中断导致长时间卡死 diff --git a/outlook_web/services/scheduler.py b/outlook_web/services/scheduler.py index ea14d03..c350c50 100644 --- a/outlook_web/services/scheduler.py +++ b/outlook_web/services/scheduler.py @@ -161,9 +161,10 @@ def init_scheduler(app, test_refresh_token): return _scheduler_instance try: - from apscheduler.schedulers.background import BackgroundScheduler import atexit + from apscheduler.schedulers.background import BackgroundScheduler + scheduler = BackgroundScheduler() configure_scheduler_jobs(scheduler, app, test_refresh_token) atexit.register(lambda: scheduler.shutdown()) diff --git a/outlook_web/services/verification_extractor.py b/outlook_web/services/verification_extractor.py index 9a4ad21..0f7fd74 100644 --- a/outlook_web/services/verification_extractor.py +++ b/outlook_web/services/verification_extractor.py @@ -13,8 +13,7 @@ from __future__ import annotations import html import re from html.parser import HTMLParser -from typing import List, Optional, Dict, Any - +from typing import Any, Dict, List, Optional # 验证码关键词列表(支持中英文) VERIFICATION_KEYWORDS = [ diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..2bdc945 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,6 @@ +[tool.black] +line-length = 127 + +[tool.isort] +profile = "black" +line_length = 127 diff --git a/start.py b/start.py index 8b294b2..2c9a09a 100644 --- a/start.py +++ b/start.py @@ -5,6 +5,7 @@ """ import os import sys + from dotenv import load_dotenv # 加载 .env 文件中的环境变量 diff --git a/tests/test_api_extract_verification.py b/tests/test_api_extract_verification.py index 7405238..64bcbca 100644 --- a/tests/test_api_extract_verification.py +++ b/tests/test_api_extract_verification.py @@ -8,10 +8,10 @@ - 设置 API 测试(轮询配置) """ -import unittest -import sys import os +import sys import time +import unittest # 添加项目根目录到 Python 路径 sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) diff --git a/tests/test_core_features.py b/tests/test_core_features.py index 5f704f9..f88deae 100644 --- a/tests/test_core_features.py +++ b/tests/test_core_features.py @@ -3,7 +3,7 @@ import unittest import uuid from unittest.mock import patch -from tests._import_app import import_web_app_module, clear_login_attempts +from tests._import_app import clear_login_attempts, import_web_app_module class CoreFeatureTests(unittest.TestCase): diff --git a/tests/test_error_and_trace.py b/tests/test_error_and_trace.py index e644532..44f267d 100644 --- a/tests/test_error_and_trace.py +++ b/tests/test_error_and_trace.py @@ -3,7 +3,7 @@ import unittest import uuid from unittest.mock import patch -from tests._import_app import import_web_app_module, clear_login_attempts +from tests._import_app import clear_login_attempts, import_web_app_module class ErrorAndTraceTests(unittest.TestCase): diff --git a/tests/test_gptmail_error_handling.py b/tests/test_gptmail_error_handling.py index 79cc28d..e27dd11 100644 --- a/tests/test_gptmail_error_handling.py +++ b/tests/test_gptmail_error_handling.py @@ -1,5 +1,6 @@ import unittest -from unittest.mock import patch, MagicMock +from unittest.mock import MagicMock, patch + import requests from outlook_web.services import gptmail diff --git a/tests/test_masking_audit_and_import.py b/tests/test_masking_audit_and_import.py index e5152ed..e3f1951 100644 --- a/tests/test_masking_audit_and_import.py +++ b/tests/test_masking_audit_and_import.py @@ -2,7 +2,7 @@ import unittest import uuid from unittest.mock import patch -from tests._import_app import import_web_app_module, clear_login_attempts +from tests._import_app import clear_login_attempts, import_web_app_module class MaskingAuditAndImportTests(unittest.TestCase): diff --git a/tests/test_performance.py b/tests/test_performance.py index 8e7a44c..3933813 100644 --- a/tests/test_performance.py +++ b/tests/test_performance.py @@ -7,10 +7,10 @@ - 轮询性能测试(不影响页面交互) """ -import unittest -import sys import os +import sys import time +import unittest # 添加项目根目录到 Python 路径 sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) diff --git a/tests/test_smoke_contract.py b/tests/test_smoke_contract.py index d11f925..d3f54b5 100644 --- a/tests/test_smoke_contract.py +++ b/tests/test_smoke_contract.py @@ -1,6 +1,6 @@ import unittest -from tests._import_app import import_web_app_module, clear_login_attempts +from tests._import_app import clear_login_attempts, import_web_app_module class SmokeContractTests(unittest.TestCase): diff --git a/tests/test_ui_redesign_bugs.py b/tests/test_ui_redesign_bugs.py index 0db4bef..981ad52 100644 --- a/tests/test_ui_redesign_bugs.py +++ b/tests/test_ui_redesign_bugs.py @@ -25,7 +25,7 @@ class TestUIRedesignBugFixes(unittest.TestCase): cls.app = cls.module.app def _login(self, client): - resp = client.post('/login', json={'password': 'testpass123'}) + resp = client.post("/login", json={"password": "testpass123"}) self.assertEqual(resp.status_code, 200) def _get_client(self): @@ -39,8 +39,8 @@ class TestUIRedesignBugFixes(unittest.TestCase): def test_bug002_current_account_bar_exists(self): """BUG-002: index.html 中存在 currentAccountBar 元素""" client = self._get_client() - resp = client.get('/') - html = resp.data.decode('utf-8') + resp = client.get("/") + html = resp.data.decode("utf-8") self.assertIn('id="currentAccountBar"', html) self.assertIn('id="currentAccountEmail"', html) self.assertIn('id="currentAccount"', html) @@ -48,18 +48,19 @@ class TestUIRedesignBugFixes(unittest.TestCase): def test_bug002_accounts_js_shows_account_bar(self): """BUG-002: accounts.js 中 selectAccount 应操作 currentAccountBar 而非 currentAccount""" client = self._get_client() - resp = client.get('/static/js/features/accounts.js') - js = resp.data.decode('utf-8') - self.assertIn('currentAccountBar', js, - "selectAccount() 应操作 #currentAccountBar 元素") + resp = client.get("/static/js/features/accounts.js") + js = resp.data.decode("utf-8") + self.assertIn( + "currentAccountBar", js, "selectAccount() 应操作 #currentAccountBar 元素" + ) # ==================== BUG-003: 设置页面 ==================== def test_bug003_settings_page_has_content(self): """BUG-003: 设置页面应有实质性内容,而非仅占位文字""" client = self._get_client() - resp = client.get('/') - html = resp.data.decode('utf-8') + resp = client.get("/") + html = resp.data.decode("utf-8") settings_section = re.search( r'id="page-settings".*?(?=id="page-|$)', html, re.DOTALL ) @@ -68,8 +69,8 @@ class TestUIRedesignBugFixes(unittest.TestCase): def test_bug003_navigate_triggers_settings_load(self): """BUG-003: navigate('settings') 应触发设置加载""" client = self._get_client() - resp = client.get('/static/js/main.js') - js = resp.data.decode('utf-8') + resp = client.get("/static/js/main.js") + js = resp.data.decode("utf-8") self.assertIn("settings", js) # ==================== BUG-004: 弹窗居中 ==================== @@ -77,122 +78,140 @@ class TestUIRedesignBugFixes(unittest.TestCase): def test_bug004_modal_css_centering(self): """BUG-004: CSS 中 .modal 应有 flex 居中属性""" client = self._get_client() - resp = client.get('/static/css/main.css') - css = resp.data.decode('utf-8') - self.assertIn('align-items: center', css) - self.assertIn('justify-content: center', css) + resp = client.get("/static/css/main.css") + css = resp.data.decode("utf-8") + self.assertIn("align-items: center", css) + self.assertIn("justify-content: center", css) def test_bug004_modal_content_max_width(self): """BUG-004: .modal-content 应有 max-width 限制""" client = self._get_client() - resp = client.get('/static/css/main.css') - css = resp.data.decode('utf-8') - self.assertIn('max-width: 560px', css) + resp = client.get("/static/css/main.css") + css = resp.data.decode("utf-8") + self.assertIn("max-width: 560px", css) def test_bug004_all_modals_use_modal_class(self): - """BUG-004: modals.html 中所有弹窗应使用 class='modal' """ + """BUG-004: modals.html 中所有弹窗应使用 class='modal'""" client = self._get_client() - resp = client.get('/') - html = resp.data.decode('utf-8') + resp = client.get("/") + html = resp.data.decode("utf-8") modal_ids = re.findall(r'id="(\w+Modal)"', html) for modal_id in modal_ids: - if modal_id == 'fullscreenEmailModal': + if modal_id == "fullscreenEmailModal": continue pattern = rf'