""" Email accounts for multi-email finance sync. Stored in data/email_configs.json. """ import json import uuid from pathlib import Path from typing import Any, Dict, List from fastapi import APIRouter, HTTPException, status from pydantic import BaseModel, Field router = APIRouter(prefix="/settings/email", tags=["email-configs"]) CONFIG_PATH = Path("data/email_configs.json") class EmailConfigCreate(BaseModel): host: str = Field(..., description="IMAP host") port: int = Field(993, description="IMAP port") user: str = Field(..., description="Email address") password: str = Field(..., description="Password or authorization code") mailbox: str = Field("INBOX", description="Mailbox name") active: bool = Field(True, description="Include in sync") class EmailConfigRead(BaseModel): id: str host: str port: int user: str mailbox: str active: bool class EmailConfigUpdate(BaseModel): host: str | None = None port: int | None = None user: str | None = None password: str | None = None mailbox: str | None = None active: bool | None = None def _load_configs() -> List[Dict[str, Any]]: if not CONFIG_PATH.exists(): return [] try: data = json.loads(CONFIG_PATH.read_text(encoding="utf-8")) return data if isinstance(data, list) else [] except Exception: return [] def _save_configs(configs: List[Dict[str, Any]]) -> None: CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True) CONFIG_PATH.write_text( json.dumps(configs, ensure_ascii=False, indent=2), encoding="utf-8", ) def _to_read(c: Dict[str, Any]) -> Dict[str, Any]: return { "id": c["id"], "host": c["host"], "port": c["port"], "user": c["user"], "mailbox": c.get("mailbox", "INBOX"), "active": c.get("active", True), } @router.get("", response_model=List[EmailConfigRead]) async def list_email_configs(): """List all email account configs (password omitted).""" configs = _load_configs() return [_to_read(c) for c in configs] @router.post("", response_model=EmailConfigRead, status_code=status.HTTP_201_CREATED) async def create_email_config(payload: EmailConfigCreate): """Add a new email account.""" configs = _load_configs() new_id = str(uuid.uuid4()) configs.append({ "id": new_id, "host": payload.host, "port": payload.port, "user": payload.user, "password": payload.password, "mailbox": payload.mailbox, "active": payload.active, }) _save_configs(configs) return _to_read(configs[-1]) @router.put("/{config_id}", response_model=EmailConfigRead) async def update_email_config(config_id: str, payload: EmailConfigUpdate): """Update an email account (omit password to keep existing).""" configs = _load_configs() for c in configs: if c.get("id") == config_id: if payload.host is not None: c["host"] = payload.host if payload.port is not None: c["port"] = payload.port if payload.user is not None: c["user"] = payload.user if payload.password is not None: c["password"] = payload.password if payload.mailbox is not None: c["mailbox"] = payload.mailbox if payload.active is not None: c["active"] = payload.active _save_configs(configs) return _to_read(c) raise HTTPException(status_code=404, detail="Email config not found") @router.delete("/{config_id}", status_code=status.HTTP_204_NO_CONTENT) async def delete_email_config(config_id: str): """Remove an email account.""" configs = _load_configs() new_list = [c for c in configs if c.get("id") != config_id] if len(new_list) == len(configs): raise HTTPException(status_code=404, detail="Email config not found") _save_configs(new_list) @router.get("/{config_id}/folders") async def list_email_folders(config_id: str): """ List mailbox folders for this account (for choosing custom labels). Returns [{ "raw": "...", "decoded": "收件箱" }, ...]. Use decoded for display and for mailbox config. """ import asyncio from backend.app.services.email_service import list_mailboxes_for_config configs = _load_configs() config = next((c for c in configs if c.get("id") == config_id), None) if not config: raise HTTPException(status_code=404, detail="Email config not found") host = config.get("host") user = config.get("user") password = config.get("password") port = int(config.get("port", 993)) if not all([host, user, password]): raise HTTPException(status_code=400, detail="Config missing host/user/password") def _fetch(): return list_mailboxes_for_config(host, port, user, password) try: folders = await asyncio.to_thread(_fetch) except Exception as e: raise HTTPException(status_code=502, detail=f"无法连接邮箱或获取文件夹列表: {e}") from e return {"folders": [{"raw": r, "decoded": d} for r, d in folders]} def get_email_configs_for_sync() -> List[Dict[str, Any]]: """Return list of configs that are active (for sync). Falls back to env if file empty.""" configs = _load_configs() active = [c for c in configs if c.get("active", True)] if active: return active # Fallback to single account from env import os host = os.getenv("IMAP_HOST") user = os.getenv("IMAP_USER") password = os.getenv("IMAP_PASSWORD") if host and user and password: return [{ "id": "env", "host": host, "port": int(os.getenv("IMAP_PORT", "993")), "user": user, "password": password, "mailbox": os.getenv("IMAP_MAILBOX", "INBOX"), "active": True, }] return []