diff --git a/api/outlook.py b/api/outlook.py new file mode 100644 index 0000000..f63a752 --- /dev/null +++ b/api/outlook.py @@ -0,0 +1,111 @@ +from datetime import datetime, timezone +from typing import List, Dict, Any + +from fastapi import APIRouter +from pydantic import BaseModel +from sqlmodel import Session, select + +from core.db import engine, OutlookAccountModel + +router = APIRouter(prefix="/outlook", tags=["outlook"]) + + +def _utcnow(): + return datetime.now(timezone.utc) + + +class OutlookBatchImportRequest(BaseModel): + data: str + enabled: bool = True + + +class OutlookBatchImportResponse(BaseModel): + total: int + success: int + failed: int + accounts: List[Dict[str, Any]] + errors: List[str] + + +@router.post("/batch-import", response_model=OutlookBatchImportResponse) +def batch_import_outlook(request: OutlookBatchImportRequest): + """ + 批量导入 Outlook 邮箱账户 + + 支持两种格式(每行一个账户,字段用 ---- 分隔): + - 邮箱----密码 + - 邮箱----密码----client_id----refresh_token + """ + lines = (request.data or "").splitlines() + total = len(lines) + success = 0 + failed = 0 + accounts: List[Dict[str, Any]] = [] + errors: List[str] = [] + + with Session(engine) as session: + for idx, raw_line in enumerate(lines): + line = str(raw_line or "").strip() + if not line or line.startswith("#"): + continue + + parts = [part.strip() for part in line.split("----")] + if len(parts) < 2: + failed += 1 + errors.append(f"行 {idx + 1}: 格式错误,至少需要邮箱和密码") + continue + + email = parts[0] + password = parts[1] + if "@" not in email: + failed += 1 + errors.append(f"行 {idx + 1}: 无效的邮箱地址: {email}") + continue + + existing = session.exec( + select(OutlookAccountModel).where(OutlookAccountModel.email == email) + ).first() + if existing: + failed += 1 + errors.append(f"行 {idx + 1}: 邮箱已存在: {email}") + continue + + client_id = parts[2] if len(parts) >= 3 else "" + refresh_token = parts[3] if len(parts) >= 4 else "" + + try: + account = OutlookAccountModel( + email=email, + password=password, + client_id=client_id, + refresh_token=refresh_token, + enabled=bool(request.enabled), + created_at=_utcnow(), + updated_at=_utcnow(), + ) + session.add(account) + session.commit() + session.refresh(account) + + accounts.append( + { + "id": account.id, + "email": account.email, + "has_oauth": bool(account.client_id and account.refresh_token), + "enabled": account.enabled, + } + ) + success += 1 + except Exception as e: + session.rollback() + failed += 1 + errors.append(f"行 {idx + 1}: 创建失败: {str(e)}") + + return OutlookBatchImportResponse( + total=total, + success=success, + failed=failed, + accounts=accounts, + errors=errors, + ) + diff --git a/core/db.py b/core/db.py index 15191c0..d36db07 100644 --- a/core/db.py +++ b/core/db.py @@ -49,6 +49,20 @@ class TaskLog(SQLModel, table=True): created_at: datetime = Field(default_factory=_utcnow) +class OutlookAccountModel(SQLModel, table=True): + __tablename__ = "outlook_accounts" + + id: Optional[int] = Field(default=None, primary_key=True) + email: str = Field(index=True, sa_column_kwargs={"unique": True}) + password: str + client_id: str = "" + refresh_token: str = "" + enabled: bool = True + created_at: datetime = Field(default_factory=_utcnow) + updated_at: datetime = Field(default_factory=_utcnow) + last_used: Optional[datetime] = None + + class ProxyModel(SQLModel, table=True): __tablename__ = "proxies" diff --git a/frontend/src/pages/Settings.tsx b/frontend/src/pages/Settings.tsx index 3b20a65..a9ce89b 100644 --- a/frontend/src/pages/Settings.tsx +++ b/frontend/src/pages/Settings.tsx @@ -1,5 +1,5 @@ import { useEffect, useState } from 'react' -import { App, Card, Form, Input, Select, Button, message, Tabs, Space, Tag, Typography, Modal, QRCode, Switch } from 'antd' +import { App, Card, Form, Input, Select, Button, message, Tabs, Space, Tag, Typography, Modal, QRCode, Switch, Alert } from 'antd' import { SaveOutlined, EyeOutlined, @@ -821,6 +821,74 @@ function IntegrationsPanel() { ) } +function OutlookImportSection() { + const { message: msg } = App.useApp() + const [value, setValue] = useState('') + const [loading, setLoading] = useState(false) + const [result, setResult] = useState(null) + + const handleSubmit = async () => { + const payload = String(value || '').trim() + if (!payload) { + msg.error('请输入 Outlook 账号内容') + return + } + setLoading(true) + try { + const res = await apiFetch('/outlook/batch-import', { + method: 'POST', + body: JSON.stringify({ data: payload, enabled: true }), + }) + setResult(res) + msg.success(`导入完成:成功 ${res.success} / 失败 ${res.failed}`) + } catch (e: any) { + msg.error(e?.message || '导入失败') + setResult({ error: e?.message || String(e) }) + } finally { + setLoading(false) + } + } + + return ( + 每行格式:邮箱----密码 或 邮箱----密码----client_id----refresh_token} + style={{ marginBottom: 16 }} + > + setValue(e.target.value)} + placeholder={`example@outlook.com----password\nexample@outlook.com----password----client_id----refresh_token`} + autoSize={{ minRows: 6, maxRows: 14 }} + /> + + + + + {result ? ( +
+ {'success' in result ? ( + {result.errors.join('\n')} + ) : undefined} + /> + ) : ( + + )} +
+ ) : null} +
+ ) +} + type TotpSetupState = 'idle' | 'setup' function SecurityPanel() { @@ -1169,6 +1237,7 @@ export default function Settings() { ))} {activeTab === 'mailbox' ? : null} + {activeTab === 'mailbox' ? : null} diff --git a/main.py b/main.py index f710543..d8e29dd 100644 --- a/main.py +++ b/main.py @@ -17,6 +17,7 @@ from api.config import router as config_router from api.actions import router as actions_router from api.integrations import router as integrations_router from api.auth import router as auth_router +from api.outlook import router as outlook_router EXPECTED_CONDA_ENV = os.getenv("APP_CONDA_ENV", "any-auto-register") @@ -108,6 +109,7 @@ app.include_router(config_router, prefix="/api") app.include_router(actions_router, prefix="/api") app.include_router(integrations_router, prefix="/api") app.include_router(auth_router, prefix="/api") +app.include_router(outlook_router, prefix="/api") @app.get("/api/solver/status")