fix: align external api behavior with PRD-00008
This commit is contained in:
1 parent
2c4ca1f0cb
commit
1d4d848702
7 files changed
+330
-33
No files matched your search
@@ -581,7 +581,7 @@ def api_extract_verification(email_addr: str) -> Any:
|
||||
# ==================== External Emails API ====================
|
||||
|
||||
|
||||
def _parse_external_common_args() -> dict:
|
||||
def _parse_external_common_args(*, default_since_minutes: int | None = None) -> dict:
|
||||
"""解析 external API 通用 query 参数(按 TDD-00008 做基础校验)。"""
|
||||
email_addr = (request.args.get("email") or "").strip()
|
||||
if not email_addr or "@" not in email_addr:
|
||||
@@ -608,7 +608,7 @@ def _parse_external_common_args() -> dict:
|
||||
raise external_api_service.InvalidParamError("top 参数无效")
|
||||
|
||||
since_minutes_raw = request.args.get("since_minutes", None)
|
||||
since_minutes = None
|
||||
since_minutes = default_since_minutes
|
||||
if since_minutes_raw not in (None, ""):
|
||||
try:
|
||||
since_minutes = int(since_minutes_raw)
|
||||
@@ -763,6 +763,13 @@ def api_external_get_message_raw(message_id: str) -> Any:
|
||||
message_id=message_id,
|
||||
folder=args["folder"],
|
||||
)
|
||||
external_api_service.audit_external_api_access(
|
||||
action="external_api_access",
|
||||
email_addr=args["email"] or "",
|
||||
endpoint="/api/external/messages/{message_id}/raw",
|
||||
status="ok",
|
||||
details={"method": detail.get("method")},
|
||||
)
|
||||
return jsonify(
|
||||
external_api_service.ok(
|
||||
{
|
||||
@@ -774,15 +781,29 @@ def api_external_get_message_raw(message_id: str) -> Any:
|
||||
)
|
||||
)
|
||||
except external_api_service.ExternalApiError as exc:
|
||||
external_api_service.audit_external_api_access(
|
||||
action="external_api_access",
|
||||
email_addr=(request.args.get("email") or "").strip(),
|
||||
endpoint="/api/external/messages/{message_id}/raw",
|
||||
status="error",
|
||||
details={"code": exc.code},
|
||||
)
|
||||
return _external_error_response(exc)
|
||||
except Exception:
|
||||
except Exception as exc:
|
||||
external_api_service.audit_external_api_access(
|
||||
action="external_api_access",
|
||||
email_addr=(request.args.get("email") or "").strip(),
|
||||
endpoint="/api/external/messages/{message_id}/raw",
|
||||
status="error",
|
||||
details={"code": "INTERNAL_ERROR", "err": type(exc).__name__},
|
||||
)
|
||||
return jsonify(external_api_service.fail("INTERNAL_ERROR", "服务内部错误")), 500
|
||||
|
||||
|
||||
@api_key_required
|
||||
def api_external_get_verification_code() -> Any:
|
||||
try:
|
||||
args = _parse_external_common_args()
|
||||
args = _parse_external_common_args(default_since_minutes=10)
|
||||
code_length = (request.args.get("code_length") or "").strip() or None
|
||||
code_regex = (request.args.get("code_regex") or "").strip() or None
|
||||
code_source = (request.args.get("code_source") or "all").strip().lower()
|
||||
@@ -828,7 +849,7 @@ def api_external_get_verification_code() -> Any:
|
||||
@api_key_required
|
||||
def api_external_get_verification_link() -> Any:
|
||||
try:
|
||||
args = _parse_external_common_args()
|
||||
args = _parse_external_common_args(default_since_minutes=10)
|
||||
result = external_api_service.get_verification_result(
|
||||
email_addr=args["email"],
|
||||
folder=args["folder"],
|
||||
|
||||
@@ -272,7 +272,23 @@ def api_external_health() -> Any:
|
||||
"server_time_utc": utcnow().isoformat() + "Z",
|
||||
"database": "ok" if db_ok else "error",
|
||||
}
|
||||
external_api_service.audit_external_api_access(
|
||||
action="external_api_access",
|
||||
email_addr="",
|
||||
endpoint="/api/external/health",
|
||||
status="ok",
|
||||
details={"database": data["database"]},
|
||||
)
|
||||
return jsonify(external_api_service.ok(data))
|
||||
except Exception as exc:
|
||||
external_api_service.audit_external_api_access(
|
||||
action="external_api_access",
|
||||
email_addr="",
|
||||
endpoint="/api/external/health",
|
||||
status="error",
|
||||
details={"code": "INTERNAL_ERROR", "err": type(exc).__name__},
|
||||
)
|
||||
return jsonify(external_api_service.fail("INTERNAL_ERROR", "服务内部错误")), 500
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
@@ -292,6 +308,13 @@ def api_external_capabilities() -> Any:
|
||||
"wait_message",
|
||||
],
|
||||
}
|
||||
external_api_service.audit_external_api_access(
|
||||
action="external_api_access",
|
||||
email_addr="",
|
||||
endpoint="/api/external/capabilities",
|
||||
status="ok",
|
||||
details={"feature_count": len(data["features"])},
|
||||
)
|
||||
return jsonify(external_api_service.ok(data))
|
||||
|
||||
|
||||
@@ -300,10 +323,24 @@ def api_external_account_status() -> Any:
|
||||
"""对外账号状态检查"""
|
||||
email_addr = (request.args.get("email") or "").strip()
|
||||
if not email_addr or "@" not in email_addr:
|
||||
external_api_service.audit_external_api_access(
|
||||
action="external_api_access",
|
||||
email_addr=email_addr,
|
||||
endpoint="/api/external/account-status",
|
||||
status="error",
|
||||
details={"code": "INVALID_PARAM"},
|
||||
)
|
||||
return jsonify(external_api_service.fail("INVALID_PARAM", "email 参数不合法")), 400
|
||||
|
||||
account = accounts_repo.get_account_by_email(email_addr)
|
||||
if not account:
|
||||
external_api_service.audit_external_api_access(
|
||||
action="external_api_access",
|
||||
email_addr=email_addr,
|
||||
endpoint="/api/external/account-status",
|
||||
status="error",
|
||||
details={"code": "ACCOUNT_NOT_FOUND"},
|
||||
)
|
||||
return jsonify(external_api_service.fail("ACCOUNT_NOT_FOUND", "账号不存在", data={"email": email_addr})), 404
|
||||
|
||||
account_type = (account.get("account_type") or "outlook").strip().lower()
|
||||
@@ -317,18 +354,22 @@ def api_external_account_status() -> Any:
|
||||
else:
|
||||
can_read = bool((account.get("client_id") or "").strip()) and bool((account.get("refresh_token") or "").strip())
|
||||
|
||||
return jsonify(
|
||||
external_api_service.ok(
|
||||
{
|
||||
"email": email_addr,
|
||||
"exists": True,
|
||||
"account_type": account_type,
|
||||
"provider": provider,
|
||||
"group_id": account.get("group_id"),
|
||||
"status": account.get("status"),
|
||||
"last_refresh_at": account.get("last_refresh_at"),
|
||||
"preferred_method": preferred_method,
|
||||
"can_read": can_read,
|
||||
}
|
||||
)
|
||||
data = {
|
||||
"email": email_addr,
|
||||
"exists": True,
|
||||
"account_type": account_type,
|
||||
"provider": provider,
|
||||
"group_id": account.get("group_id"),
|
||||
"status": account.get("status"),
|
||||
"last_refresh_at": account.get("last_refresh_at"),
|
||||
"preferred_method": preferred_method,
|
||||
"can_read": can_read,
|
||||
}
|
||||
external_api_service.audit_external_api_access(
|
||||
action="external_api_access",
|
||||
email_addr=email_addr,
|
||||
endpoint="/api/external/account-status",
|
||||
status="ok",
|
||||
details={"preferred_method": preferred_method, "can_read": can_read},
|
||||
)
|
||||
return jsonify(external_api_service.ok(data))
|
||||
@@ -384,6 +384,14 @@ def get_message_detail_for_external(
|
||||
proxy_url,
|
||||
)
|
||||
method_label = "Graph API"
|
||||
graph_raw_content = None
|
||||
if detail:
|
||||
graph_raw_content = graph_service.get_email_raw_graph(
|
||||
account.get("client_id") or "",
|
||||
account.get("refresh_token") or "",
|
||||
message_id,
|
||||
proxy_url,
|
||||
)
|
||||
if not detail:
|
||||
detail = imap_service.get_email_detail_imap_with_server(
|
||||
email_addr,
|
||||
@@ -420,7 +428,7 @@ def get_message_detail_for_external(
|
||||
|
||||
html_content = body_content if body_type == "html" else ""
|
||||
content = body_content if body_type == "text" else extract_email_text({"body_html": html_content})
|
||||
raw_content = body_content
|
||||
raw_content = str(graph_raw_content or body_content)
|
||||
|
||||
from_address = (detail.get("from") or {}).get("emailAddress", {}).get("address", "")
|
||||
to_address = ",".join([r.get("emailAddress", {}).get("address", "") for r in (detail.get("toRecipients") or [])])
|
||||
@@ -519,17 +527,20 @@ def wait_for_message(
|
||||
if poll_interval <= 0 or poll_interval > timeout_seconds:
|
||||
raise InvalidParamError("poll_interval 参数无效")
|
||||
|
||||
baseline_timestamp = int(time.time())
|
||||
start = time.time()
|
||||
last_error: Optional[ExternalApiError] = None
|
||||
while True:
|
||||
try:
|
||||
return get_latest_message_for_external(
|
||||
latest_message = get_latest_message_for_external(
|
||||
email_addr=email_addr,
|
||||
folder=folder,
|
||||
from_contains=from_contains,
|
||||
subject_contains=subject_contains,
|
||||
since_minutes=since_minutes,
|
||||
)
|
||||
if int(latest_message.get("timestamp") or 0) >= baseline_timestamp:
|
||||
return latest_message
|
||||
except MailNotFoundError as exc:
|
||||
last_error = exc
|
||||
|
||||
|
||||
@@ -183,6 +183,35 @@ def get_email_detail_graph(
|
||||
return None
|
||||
|
||||
|
||||
def get_email_raw_graph(
|
||||
client_id: str,
|
||||
refresh_token: str,
|
||||
message_id: str,
|
||||
proxy_url: str = None,
|
||||
) -> Optional[str]:
|
||||
"""使用 Graph API 获取邮件 MIME RAW 内容。"""
|
||||
access_token = get_access_token_graph(client_id, refresh_token, proxy_url)
|
||||
if not access_token:
|
||||
return None
|
||||
|
||||
try:
|
||||
url = f"https://graph.microsoft.com/v1.0/me/messages/{message_id}/$value"
|
||||
headers = {
|
||||
"Authorization": f"Bearer {access_token}",
|
||||
}
|
||||
|
||||
proxies = build_proxies(proxy_url)
|
||||
res = requests.get(url, headers=headers, timeout=30, proxies=proxies)
|
||||
|
||||
if res.status_code != 200:
|
||||
return None
|
||||
|
||||
res.encoding = res.encoding or "utf-8"
|
||||
return res.text
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def test_refresh_token(client_id: str, refresh_token: str, proxy_url: str = None) -> tuple[bool, str | None]:
|
||||
"""测试 refresh token 是否有效,返回 (是否成功, 错误信息)"""
|
||||
ok, err, _new_refresh_token = test_refresh_token_with_rotation(client_id, refresh_token, proxy_url)
|
||||
|
||||
Reference in new issue
Block a user