Files
胡绍康andClaude Opus 4.6 f7a8817970 style: 使用 Black 和 isort 格式化代码
- 使用 Black 格式化 47 个 Python 文件
- 使用 isort 修复 import 排序
- 修复 Code Quality CI/CD 检查失败问题

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 13:12:16 +08:00

237 lines
8.0 KiB
Python
Raw Permalink 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
import html
import json
from datetime import datetime
from typing import Any, Dict
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
from outlook_web.repositories import temp_emails as temp_emails_repo
from outlook_web.security.auth import (
consume_export_verify_token,
get_client_ip,
get_user_agent,
login_required,
)
def sanitize_input(text: str, max_length: int = 500) -> str:
"""
净化用户输入,防止XSS攻击
- 转义HTML特殊字符
- 限制长度
- 移除控制字符
"""
if not text:
return ""
# 限制长度
text = text[:max_length]
# 移除控制字符(保留换行和制表符)
text = "".join(char for char in text if char.isprintable() or char in "\n\t")
# 转义HTML特殊字符
text = html.escape(text, quote=True)
return text
# ==================== 分组 API ====================
@login_required
def api_get_groups() -> Any:
"""获取所有分组"""
groups = groups_repo.load_groups()
# 添加每个分组的邮箱数量
for group in groups:
if group["name"] == "临时邮箱":
# 临时邮箱分组从 temp_emails 表获取数量
group["account_count"] = temp_emails_repo.get_temp_email_count()
else:
group["account_count"] = groups_repo.get_group_account_count(group["id"])
return jsonify({"success": True, "groups": groups})
@login_required
def api_get_group(group_id: int) -> Any:
"""获取单个分组"""
group = groups_repo.get_group_by_id(group_id)
if not group:
return jsonify({"success": False, "error": "分组不存在"})
group["account_count"] = groups_repo.get_group_account_count(group_id)
return jsonify({"success": True, "group": group})
@login_required
def api_add_group() -> Any:
"""添加分组"""
data = request.json
name = sanitize_input(data.get("name", "").strip(), max_length=100)
description = sanitize_input(data.get("description", ""), max_length=500)
color = data.get("color", "#1a1a1a")
proxy_url = data.get("proxy_url", "").strip()
if not name:
return jsonify({"success": False, "error": "分组名称不能为空"})
group_id = groups_repo.add_group(name, description, color, proxy_url)
if group_id:
log_audit("create", "group", str(group_id), f"创建分组:{name}")
return jsonify({"success": True, "message": "分组创建成功", "group_id": group_id})
else:
return jsonify({"success": False, "error": "分组名称已存在"})
@login_required
def api_update_group(group_id: int) -> Any:
"""更新分组"""
data = request.json
name = sanitize_input(data.get("name", "").strip(), max_length=100)
description = sanitize_input(data.get("description", ""), max_length=500)
color = data.get("color", "#1a1a1a")
proxy_url = data.get("proxy_url", "").strip()
if not name:
return jsonify({"success": False, "error": "分组名称不能为空"})
existing = groups_repo.get_group_by_id(group_id)
if not existing:
error_payload = build_error_payload(
code="GROUP_NOT_FOUND",
message="分组不存在",
err_type="NotFoundError",
status=404,
details=f"group_id={group_id}",
)
return jsonify({"success": False, "error": error_payload}), 404
# 系统分组保护:不允许重命名(避免破坏系统逻辑)
if existing.get("is_system") and name != existing.get("name"):
error_payload = build_error_payload(
code="SYSTEM_GROUP_PROTECTED",
message="系统分组不允许重命名",
err_type="ForbiddenError",
status=403,
details=f"group_id={group_id}",
)
return jsonify({"success": False, "error": error_payload}), 403
if groups_repo.update_group(group_id, name, description, color, proxy_url):
# 不记录 proxy_url 明文(可能包含代理账号/密码)
details = json.dumps(
{
"name": name,
"has_description": bool(description),
"color": color,
"proxy_configured": bool(proxy_url),
},
ensure_ascii=False,
)
log_audit("update", "group", str(group_id), details)
return jsonify({"success": True, "message": "分组更新成功"})
else:
return jsonify({"success": False, "error": "更新失败"})
@login_required
def api_delete_group(group_id: int) -> Any:
"""删除分组"""
group = groups_repo.get_group_by_id(group_id)
if not group:
error_payload = build_error_payload(
code="GROUP_NOT_FOUND",
message="分组不存在",
err_type="NotFoundError",
status=404,
details=f"group_id={group_id}",
)
return jsonify({"success": False, "error": error_payload}), 404
if group.get("is_system"):
error_payload = build_error_payload(
code="SYSTEM_GROUP_PROTECTED",
message="系统分组不能删除",
err_type="ForbiddenError",
status=403,
details=f"group_id={group_id}",
)
return jsonify({"success": False, "error": error_payload}), 403
default_group_id = groups_repo.get_default_group_id()
if group_id == default_group_id or group.get("name") == "默认分组":
error_payload = build_error_payload(
code="DEFAULT_GROUP_PROTECTED",
message="默认分组不能删除",
err_type="ForbiddenError",
status=403,
details=f"group_id={group_id}",
)
return jsonify({"success": False, "error": error_payload}), 403
if groups_repo.delete_group(group_id):
log_audit("delete", "group", str(group_id), "删除分组并迁移账号到默认分组")
return jsonify({"success": True, "message": "分组已删除,邮箱已移至默认分组"})
else:
return jsonify({"success": False, "error": "删除失败"})
@login_required
def api_export_group(group_id: int) -> Any:
"""导出分组下的所有邮箱账号为 TXT 文件(需要二次验证)"""
# 从请求头获取二次验证 token(避免 URL 泄露)
verify_token = request.headers.get("X-Export-Token")
client_ip = get_client_ip()
user_agent = get_user_agent()
ok, error_message = consume_export_verify_token(verify_token, client_ip, user_agent)
if not ok:
return (
jsonify({"success": False, "error": error_message, "need_verify": True}),
401,
)
group = groups_repo.get_group_by_id(group_id)
if not group:
return jsonify({"success": False, "error": "分组不存在"})
# 使用 load_accounts 获取该分组下的所有账号(自动解密)
accounts = accounts_repo.load_accounts(group_id)
if not accounts:
return jsonify({"success": False, "error": "该分组下没有邮箱账号"})
# 记录审计日志
log_audit(
"export",
"group",
str(group_id),
f"导出分组 '{group['name']}' 的 {len(accounts)} 个账号",
)
# 生成导出内容(格式:email----password----client_id----refresh_token)
lines = []
for acc in accounts:
line = f"{acc['email']}----{acc.get('password', '')}----{acc['client_id']}----{acc['refresh_token']}"
lines.append(line)
content = "\n".join(lines)
# 生成文件名(使用 URL 编码处理中文)
filename = f"{group['name']}_accounts_{datetime.now().strftime('%Y%m%d_%H%M%S')}.txt"
encoded_filename = quote(filename)
# 返回文件下载响应
return Response(
content,
mimetype="text/plain; charset=utf-8",
headers={"Content-Disposition": f"attachment; filename*=UTF-8''{encoded_filename}"},
)