diff --git a/src/web/routes/email.py b/src/web/routes/email.py index 5f0123c..fb67149 100644 --- a/src/web/routes/email.py +++ b/src/web/routes/email.py @@ -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)}") diff --git a/static/js/email_services.js b/static/js/email_services.js index fafd85b..a53e96f 100644 --- a/static/js/email_services.js +++ b/static/js/email_services.js @@ -72,6 +72,25 @@ const elements = { editOutlookForm: document.getElementById('edit-outlook-form'), closeEditOutlookModal: document.getElementById('close-edit-outlook-modal'), cancelEditOutlook: document.getElementById('cancel-edit-outlook'), + + // 收件箱模态框 + inboxModal: document.getElementById('inbox-modal'), + closeInboxModal: document.getElementById('close-inbox-modal'), + inboxRefreshBtn: document.getElementById('inbox-refresh-btn'), + inboxOnlyUnseen: document.getElementById('inbox-only-unseen'), + inboxLoading: document.getElementById('inbox-loading'), + inboxTable: document.getElementById('inbox-table'), + inboxTbody: document.getElementById('inbox-tbody'), + inboxEmpty: document.getElementById('inbox-empty'), + inboxModalEmail: document.getElementById('inbox-modal-email'), + + // 邮件正文模态框 + emailDetailModal: document.getElementById('email-detail-modal'), + closeEmailDetailModal: document.getElementById('close-email-detail-modal'), + emailDetailSubject: document.getElementById('email-detail-subject'), + emailDetailSender: document.getElementById('email-detail-sender'), + emailDetailDate: document.getElementById('email-detail-date'), + emailDetailBody: document.getElementById('email-detail-body'), }; const CUSTOM_SUBTYPE_LABELS = { @@ -164,6 +183,12 @@ function initEventListeners() { document.addEventListener('click', () => { document.querySelectorAll('.dropdown-menu.active').forEach(m => m.classList.remove('active')); }); + + // 收件箱模态框事件 + elements.closeInboxModal.addEventListener('click', () => elements.inboxModal.classList.remove('active')); + elements.closeEmailDetailModal.addEventListener('click', () => elements.emailDetailModal.classList.remove('active')); + elements.inboxRefreshBtn.addEventListener('click', () => loadInbox(currentInboxServiceId, true)); + elements.inboxOnlyUnseen.addEventListener('change', () => loadInbox(currentInboxServiceId)); } function toggleEmailMoreMenu(btn) { @@ -247,6 +272,7 @@ async function loadOutlookServices() { ${format.date(service.last_used)}
+