feat(email): 新增 Outlook 收件箱功能

- 后端:GET /api/email-services/{id}/inbox 端点,通过 IMAPNewProvider 读取收件箱
- 前端:收件箱列表模态框(支持仅未读筛选、刷新)
- 前端:邮件正文弹窗
- Outlook 操作列新增「收件箱」按钮
This commit is contained in:
cnlimiter
2026-03-21 02:37:35 +08:00
committed by Mison
parent 668500028a
commit 344cf0088c
3 changed files with 235 additions and 25 deletions

View File

@@ -67,7 +67,7 @@ class ServiceTestResult(BaseModel):
class OutlookBatchImportRequest(BaseModel):
"""Outlook 批量导入请求"""
data: str # 多行数据,每行格式: 邮箱----密码 或 邮箱----密码----client_id----refresh_token
data: str # 多行数据,每行格式: 邮箱----密码----client_id----refresh_token
enabled: bool = True
priority: int = 0
@@ -461,11 +461,8 @@ async def batch_import_outlook(request: OutlookBatchImportRequest):
"""
批量导入 Outlook 邮箱账户
支持两种格式:
- 格式一(密码认证):邮箱----密码
- 格式二XOAUTH2 认证):邮箱----密码----client_id----refresh_token
每行一个账户,使用四个连字符(----)分隔字段
格式(每行):邮箱----密码----client_id----refresh_token
使用四个连字符(----)分隔字段
"""
lines = request.data.strip().split("\n")
total = len(lines)
@@ -484,14 +481,18 @@ async def batch_import_outlook(request: OutlookBatchImportRequest):
parts = line.split("----")
# 验证格式
if len(parts) < 2:
# 必须是四字段格式
if len(parts) < 4:
failed += 1
errors.append(f"{i+1}: 格式错误,至少需要邮箱和密码")
errors.append(
f"{i+1}: 格式错误,必须为 邮箱----密码----client_id----refresh_token"
)
continue
email = parts[0].strip()
password = parts[1].strip()
client_id = parts[2].strip()
refresh_token = parts[3].strip()
# 验证邮箱格式
if "@" not in email:
@@ -499,6 +500,12 @@ async def batch_import_outlook(request: OutlookBatchImportRequest):
errors.append(f"{i+1}: 无效的邮箱地址: {email}")
continue
# 验证 OAuth 字段非空
if not client_id or not refresh_token:
failed += 1
errors.append(f"{i+1}: [{email}] client_id 或 refresh_token 不能为空")
continue
# 检查是否已存在
existing = db.query(EmailServiceModel).filter(
EmailServiceModel.service_type == "outlook",
@@ -513,17 +520,11 @@ async def batch_import_outlook(request: OutlookBatchImportRequest):
# 构建配置
config = {
"email": email,
"password": password
"password": password,
"client_id": client_id,
"refresh_token": refresh_token,
}
# 检查是否有 OAuth 信息(格式二)
if len(parts) >= 4:
client_id = parts[2].strip()
refresh_token = parts[3].strip()
if client_id and refresh_token:
config["client_id"] = client_id
config["refresh_token"] = refresh_token
# 创建服务记录
try:
service = EmailServiceModel(
@@ -608,3 +609,86 @@ async def test_tempmail_service(request: TempmailTestRequest):
except Exception as e:
logger.error(f"测试临时邮箱失败: {e}")
return {"success": False, "message": f"测试失败: {str(e)}"}
# ============== 收件箱 ==============
@router.get("/{service_id}/inbox")
async def get_outlook_inbox(
service_id: int,
count: int = Query(30, ge=1, le=100),
only_unseen: bool = Query(False),
):
"""获取 Outlook 收件箱邮件列表"""
with get_db() as db:
service = db.query(EmailServiceModel).filter(EmailServiceModel.id == service_id).first()
if not service:
raise HTTPException(status_code=404, detail="服务不存在")
if service.service_type != "outlook":
raise HTTPException(status_code=400, detail="仅支持 Outlook 类型服务")
config = service.config or {}
email_addr = config.get("email", "")
client_id = config.get("client_id", "")
refresh_token = config.get("refresh_token", "")
# client_id 为空时尝试使用全局默认值
if not client_id:
from ...config.settings import get_settings
client_id = get_settings().outlook_default_client_id or ""
if not client_id or not refresh_token:
raise HTTPException(status_code=400, detail="该账户缺少 OAuth 配置client_id / refresh_token无法读取收件箱")
try:
from ...services.outlook.account import OutlookAccount
from ...services.outlook.token_manager import TokenManager
from ...services.outlook.providers.imap_new import IMAPNewProvider
from ...services.outlook.providers.base import ProviderConfig
account = OutlookAccount(
email=email_addr,
password=config.get("password", ""),
client_id=client_id,
refresh_token=refresh_token,
)
provider_config = ProviderConfig(
proxy_url=None,
timeout=30,
service_id=service_id,
)
provider = IMAPNewProvider(account, provider_config)
connected = provider.connect()
if not connected:
raise HTTPException(status_code=502, detail="IMAP 连接失败,请检查 OAuth 配置")
try:
messages = provider.get_recent_emails(count=count, only_unseen=only_unseen)
finally:
provider.disconnect()
emails = []
for m in messages:
received_str = m.received_at.isoformat() if m.received_at else None
emails.append({
"id": m.id or "",
"subject": m.subject or "",
"sender": m.sender or "",
"received_at": received_str,
"body_preview": m.body_preview or (m.body or "")[:200],
"body": m.body or "",
"is_read": m.is_read,
})
return {
"email": email_addr,
"total": len(emails),
"emails": emails,
}
except HTTPException:
raise
except Exception as e:
logger.error(f"获取收件箱失败 service_id={service_id}: {e}")
raise HTTPException(status_code=500, detail=f"获取收件箱失败: {str(e)}")