Files
outlookEmailPlus/outlook_web/controllers/emails.py
T
胡绍康andClaude Opus 4.6 4c1920d7c1 feat: 迁移 emails 模块到 controllers 层
- 迁移 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>
2026-02-25 08:58:36 +08:00

408 lines
14 KiB
Python
Raw 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
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