Files
AiTool/backend/app/routers/email_configs.py
2026-03-15 16:38:59 +08:00

184 lines
5.8 KiB
Python

"""
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 []