- 迁移 4 个 emails 相关 API 到 controllers/emails.py: - api_get_emails - api_delete_emails - api_get_email_detail - api_extract_verification - 更新 routes/emails.py 移除 impl 参数 - 更新测试用例以 mock repositories 和 services 模块 - 保持 Graph API → IMAP 回退机制 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
408 lines
14 KiB
Python
408 lines
14 KiB
Python
from __future__ import annotations
|
||
|
||
from typing import Any
|
||
|
||
from flask import jsonify, request
|
||
|
||
from outlook_web import config
|
||
from outlook_web.audit import log_audit
|
||
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.security.auth import login_required
|
||
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"
|
||
|
||
|
||
# ==================== 邮件 API ====================
|
||
|
||
|
||
@login_required
|
||
def api_get_emails(email_addr: str) -> Any:
|
||
"""获取邮件列表(支持分页,不使用缓存)"""
|
||
account = accounts_repo.get_account_by_email(email_addr)
|
||
|
||
if not account:
|
||
error_payload = build_error_payload(
|
||
"ACCOUNT_NOT_FOUND",
|
||
"账号不存在",
|
||
"NotFoundError",
|
||
404,
|
||
f"email={email_addr}"
|
||
)
|
||
return jsonify({'success': False, 'error': error_payload})
|
||
|
||
folder = request.args.get('folder', 'inbox') # inbox, junkemail, deleteditems
|
||
skip = int(request.args.get('skip', 0))
|
||
top = int(request.args.get('top', 20))
|
||
|
||
# 获取分组代理设置
|
||
proxy_url = ''
|
||
if account.get('group_id'):
|
||
group = groups_repo.get_group_by_id(account['group_id'])
|
||
if group:
|
||
proxy_url = group.get('proxy_url', '') or ''
|
||
|
||
# 收集所有错误信息
|
||
all_errors = {}
|
||
|
||
# 1. 尝试 Graph API
|
||
graph_result = graph_service.get_emails_graph(account['client_id'], account['refresh_token'], folder, skip, top, proxy_url)
|
||
if graph_result.get("success"):
|
||
emails = graph_result.get("emails", [])
|
||
# 更新刷新时间
|
||
db = get_db()
|
||
db.execute('''
|
||
UPDATE accounts
|
||
SET last_refresh_at = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP
|
||
WHERE email = ?
|
||
''', (email_addr,))
|
||
db.commit()
|
||
|
||
# 格式化 Graph API 返回的数据
|
||
formatted = []
|
||
for e in emails:
|
||
formatted.append({
|
||
'id': e.get('id'),
|
||
'subject': e.get('subject', '无主题'),
|
||
'from': e.get('from', {}).get('emailAddress', {}).get('address', '未知'),
|
||
'date': e.get('receivedDateTime', ''),
|
||
'is_read': e.get('isRead', False),
|
||
'has_attachments': e.get('hasAttachments', False),
|
||
'body_preview': e.get('bodyPreview', '')
|
||
})
|
||
|
||
return jsonify({
|
||
'success': True,
|
||
'emails': formatted,
|
||
'method': 'Graph API',
|
||
'has_more': len(formatted) >= top
|
||
})
|
||
else:
|
||
graph_error = graph_result.get("error")
|
||
all_errors["graph"] = graph_error
|
||
|
||
# 如果是代理错误,不再回退 IMAP
|
||
if isinstance(graph_error, dict) and graph_error.get('type') in ('ProxyError', 'ConnectionError'):
|
||
return jsonify({
|
||
'success': False,
|
||
'error': '代理连接失败,请检查分组代理设置',
|
||
'details': all_errors
|
||
})
|
||
|
||
imap_new_result = imap_service.get_emails_imap_with_server(
|
||
account['email'], account['client_id'], account['refresh_token'],
|
||
folder, skip, top, IMAP_SERVER_NEW
|
||
)
|
||
if imap_new_result.get("success"):
|
||
return jsonify({
|
||
'success': True,
|
||
'emails': imap_new_result.get("emails", []),
|
||
'method': 'IMAP (New)',
|
||
'has_more': False # IMAP 分页暂未完全实现
|
||
})
|
||
else:
|
||
all_errors["imap_new"] = imap_new_result.get("error")
|
||
|
||
# 3. 尝试旧版 IMAP (outlook.office365.com)
|
||
imap_old_result = imap_service.get_emails_imap_with_server(
|
||
account['email'], account['client_id'], account['refresh_token'],
|
||
folder, skip, top, IMAP_SERVER_OLD
|
||
)
|
||
if imap_old_result.get("success"):
|
||
return jsonify({
|
||
'success': True,
|
||
'emails': imap_old_result.get("emails", []),
|
||
'method': 'IMAP (Old)',
|
||
'has_more': False
|
||
})
|
||
else:
|
||
all_errors["imap_old"] = imap_old_result.get("error")
|
||
|
||
return jsonify({
|
||
'success': False,
|
||
'error': '无法获取邮件,所有方式均失败',
|
||
'details': all_errors
|
||
})
|
||
|
||
|
||
@login_required
|
||
def api_delete_emails() -> Any:
|
||
"""批量删除邮件(永久删除)"""
|
||
data = request.json
|
||
email_addr = data.get('email', '')
|
||
message_ids = data.get('ids', [])
|
||
|
||
if not email_addr or not message_ids:
|
||
return jsonify({'success': False, 'error': '参数不完整'})
|
||
|
||
account = accounts_repo.get_account_by_email(email_addr)
|
||
if not account:
|
||
return jsonify({'success': False, 'error': '账号不存在'})
|
||
|
||
# 获取分组代理设置
|
||
proxy_url = ''
|
||
if account.get('group_id'):
|
||
group = groups_repo.get_group_by_id(account['group_id'])
|
||
if group:
|
||
proxy_url = group.get('proxy_url', '') or ''
|
||
|
||
response_data, method_used = email_delete_service.delete_emails_with_fallback(
|
||
email_addr=email_addr,
|
||
client_id=account["client_id"],
|
||
refresh_token=account["refresh_token"],
|
||
message_ids=message_ids,
|
||
proxy_url=proxy_url,
|
||
delete_emails_graph=graph_service.delete_emails_graph,
|
||
delete_emails_imap=imap_service.delete_emails_imap,
|
||
imap_server_new=IMAP_SERVER_NEW,
|
||
imap_server_old=IMAP_SERVER_OLD,
|
||
)
|
||
|
||
if method_used == "graph":
|
||
log_audit("delete", "email", email_addr, f"删除邮件 {len(message_ids)} 封(Graph API)")
|
||
elif method_used == "imap_new":
|
||
log_audit("delete", "email", email_addr, f"删除邮件 {len(message_ids)} 封(IMAP New)")
|
||
elif method_used == "imap_old":
|
||
log_audit("delete", "email", email_addr, f"删除邮件 {len(message_ids)} 封(IMAP Old)")
|
||
|
||
return jsonify(response_data)
|
||
|
||
|
||
@login_required
|
||
def api_get_email_detail(email_addr: str, message_id: str) -> Any:
|
||
"""获取邮件详情"""
|
||
account = accounts_repo.get_account_by_email(email_addr)
|
||
|
||
if not account:
|
||
return jsonify({'success': False, 'error': '账号不存在'})
|
||
|
||
method = request.args.get('method', 'graph')
|
||
folder = request.args.get('folder', 'inbox')
|
||
|
||
if method == 'graph':
|
||
# 获取分组代理设置
|
||
proxy_url = ''
|
||
if account.get('group_id'):
|
||
group = groups_repo.get_group_by_id(account['group_id'])
|
||
if group:
|
||
proxy_url = group.get('proxy_url', '') or ''
|
||
|
||
detail = graph_service.get_email_detail_graph(account['client_id'], account['refresh_token'], message_id, proxy_url)
|
||
if detail:
|
||
return jsonify({
|
||
'success': True,
|
||
'email': {
|
||
'id': detail.get('id'),
|
||
'subject': detail.get('subject', '无主题'),
|
||
'from': detail.get('from', {}).get('emailAddress', {}).get('address', '未知'),
|
||
'to': ', '.join([r.get('emailAddress', {}).get('address', '') for r in detail.get('toRecipients', [])]),
|
||
'cc': ', '.join([r.get('emailAddress', {}).get('address', '') for r in detail.get('ccRecipients', [])]),
|
||
'date': detail.get('receivedDateTime', ''),
|
||
'body': detail.get('body', {}).get('content', ''),
|
||
'body_type': detail.get('body', {}).get('contentType', 'text')
|
||
}
|
||
})
|
||
|
||
# 如果 Graph API 失败,尝试 IMAP
|
||
detail = imap_service.get_email_detail_imap(account['email'], account['client_id'], account['refresh_token'], message_id, folder)
|
||
if detail:
|
||
return jsonify({'success': True, 'email': detail})
|
||
|
||
return jsonify({'success': False, 'error': '获取邮件详情失败'})
|
||
|
||
|
||
@login_required
|
||
def api_extract_verification(email_addr: str) -> Any:
|
||
"""
|
||
提取验证码和链接接口
|
||
|
||
功能:从指定邮箱的最新邮件中提取验证码和链接
|
||
|
||
实现策略(多 API 回退机制):
|
||
1. Graph API (inbox) - 优先从收件箱获取
|
||
2. Graph API (junkemail) - 从垃圾邮件获取
|
||
3. IMAP (新服务器) - Graph API 失败时回退
|
||
4. IMAP (旧服务器) - 最后的回退方案
|
||
"""
|
||
from outlook_web.services.verification_extractor import extract_verification_info
|
||
|
||
# 获取账号信息
|
||
account = accounts_repo.get_account_by_email(email_addr)
|
||
|
||
if not account:
|
||
error_payload = build_error_payload(
|
||
"ACCOUNT_NOT_FOUND",
|
||
"邮箱不存在",
|
||
"NotFoundError",
|
||
404,
|
||
f"email={email_addr}"
|
||
)
|
||
return jsonify({'success': False, 'error': error_payload}), 404
|
||
|
||
# 获取分组代理设置
|
||
proxy_url = ''
|
||
if account.get('group_id'):
|
||
group = groups_repo.get_group_by_id(account['group_id'])
|
||
if group:
|
||
proxy_url = group.get('proxy_url', '') or ''
|
||
|
||
# 收集邮件(同时从收件箱和垃圾邮件获取)
|
||
emails = []
|
||
graph_success = False
|
||
|
||
# 1. 尝试 Graph API 从收件箱获取最新邮件
|
||
try:
|
||
inbox_result = graph_service.get_emails_graph(
|
||
account['client_id'],
|
||
account['refresh_token'],
|
||
folder='inbox',
|
||
skip=0,
|
||
top=1,
|
||
proxy_url=proxy_url
|
||
)
|
||
if inbox_result.get("success"):
|
||
emails.extend(inbox_result.get("emails", []))
|
||
graph_success = True
|
||
except Exception:
|
||
pass
|
||
|
||
# 2. 尝试 Graph API 从垃圾邮件获取最新邮件
|
||
try:
|
||
junk_result = graph_service.get_emails_graph(
|
||
account['client_id'],
|
||
account['refresh_token'],
|
||
folder='junkemail',
|
||
skip=0,
|
||
top=1,
|
||
proxy_url=proxy_url
|
||
)
|
||
if junk_result.get("success"):
|
||
emails.extend(junk_result.get("emails", []))
|
||
graph_success = True
|
||
except Exception:
|
||
pass
|
||
|
||
# 3. 如果 Graph API 失败,尝试 IMAP 回退
|
||
if not graph_success or not emails:
|
||
# 尝试新版 IMAP 服务器
|
||
try:
|
||
imap_new_result = imap_service.get_emails_imap_with_server(
|
||
account['email'], account['client_id'], account['refresh_token'],
|
||
folder='inbox', skip=0, top=1, server=IMAP_SERVER_NEW
|
||
)
|
||
if imap_new_result.get("success"):
|
||
emails.extend(imap_new_result.get("emails", []))
|
||
except Exception:
|
||
pass
|
||
|
||
# 尝试旧版 IMAP 服务器
|
||
try:
|
||
imap_old_result = imap_service.get_emails_imap_with_server(
|
||
account['email'], account['client_id'], account['refresh_token'],
|
||
folder='inbox', skip=0, top=1, server=IMAP_SERVER_OLD
|
||
)
|
||
if imap_old_result.get("success"):
|
||
emails.extend(imap_old_result.get("emails", []))
|
||
except Exception:
|
||
pass
|
||
|
||
if not emails:
|
||
error_payload = build_error_payload(
|
||
"EMAIL_NOT_FOUND",
|
||
"未找到邮件",
|
||
"NotFoundError",
|
||
404,
|
||
f"email={email_addr}"
|
||
)
|
||
return jsonify({'success': False, 'error': error_payload}), 404
|
||
|
||
# 按时间排序,取最新的一封
|
||
emails.sort(key=lambda x: x.get('receivedDateTime', '') or x.get('date', ''), reverse=True)
|
||
latest_email = emails[0]
|
||
|
||
# 获取邮件详情以获取完整内容
|
||
email_detail = None
|
||
|
||
# 尝试 Graph API 获取详情
|
||
try:
|
||
email_detail = graph_service.get_email_detail_graph(
|
||
account['client_id'],
|
||
account['refresh_token'],
|
||
latest_email.get('id'),
|
||
proxy_url
|
||
)
|
||
except Exception:
|
||
pass
|
||
|
||
# 如果 Graph API 失败,尝试 IMAP 获取详情
|
||
if not email_detail:
|
||
try:
|
||
email_detail = imap_service.get_email_detail_imap(
|
||
account['email'],
|
||
account['client_id'],
|
||
account['refresh_token'],
|
||
latest_email.get('id'),
|
||
'inbox'
|
||
)
|
||
except Exception:
|
||
pass
|
||
|
||
# 构建邮件对象用于提取
|
||
email_obj = {
|
||
'subject': latest_email.get('subject', ''),
|
||
'body_preview': latest_email.get('bodyPreview', '') or latest_email.get('body_preview', '')
|
||
}
|
||
|
||
if email_detail:
|
||
# Graph API 格式
|
||
if 'body' in email_detail:
|
||
body_content = email_detail.get('body', {})
|
||
email_obj['body'] = body_content.get('content', '') if body_content.get('contentType') == 'text' else ''
|
||
email_obj['body_html'] = body_content.get('content', '') if body_content.get('contentType') == 'html' else ''
|
||
email_obj['bodyContent'] = body_content.get('content', '')
|
||
email_obj['bodyContentType'] = body_content.get('contentType', 'text')
|
||
# IMAP 格式
|
||
elif 'body' in email_detail or 'body_html' in email_detail:
|
||
email_obj['body'] = email_detail.get('body', '')
|
||
email_obj['body_html'] = email_detail.get('body_html', '')
|
||
|
||
try:
|
||
# 尝试从邮件详情提取验证信息
|
||
result = extract_verification_info(email_obj)
|
||
|
||
return jsonify({
|
||
'success': True,
|
||
'data': result,
|
||
'message': '提取成功'
|
||
})
|
||
|
||
except ValueError as e:
|
||
# 未找到验证信息
|
||
error_payload = build_error_payload(
|
||
"VERIFICATION_NOT_FOUND",
|
||
str(e),
|
||
"NotFoundError",
|
||
404,
|
||
f"email={email_addr}"
|
||
)
|
||
return jsonify({'success': False, 'error': error_payload}), 404
|
||
|
||
except Exception as e:
|
||
# 其他错误
|
||
error_payload = build_error_payload(
|
||
"EXTRACT_ERROR",
|
||
"提取失败",
|
||
"ExtractError",
|
||
500,
|
||
str(e)
|
||
)
|
||
return jsonify({'success': False, 'error': error_payload}), 500
|