"""HTTP routes for the mail module — mounted onto the main app via modules/registry.py.""" from __future__ import annotations import asyncio as _asyncio from typing import Any, Dict from fastapi import APIRouter, Body, HTTPException from . import backend as mail MANIFEST = {"key": "mail", "label": "Mail", "icon": "✉️"} router = APIRouter(prefix="/mail", tags=["mail"]) def _require_mail(account_id: str): if not mail.is_configured(account_id): raise HTTPException(status_code=400, detail="mail account not configured") # --- accounts -------------------------------------------------------------------- @router.get("/accounts") async def mail_accounts(): return {"accounts": mail.public_accounts()} @router.post("/accounts") async def mail_account_create(payload: Dict[str, Any] = Body(...)): return mail.save_account(None, payload) @router.put("/accounts/{account_id}") async def mail_account_update(account_id: str, payload: Dict[str, Any] = Body(...)): try: return mail.save_account(account_id, payload) except KeyError: raise HTTPException(status_code=404, detail="account not found") @router.delete("/accounts/{account_id}") async def mail_account_delete(account_id: str): mail.delete_account(account_id) return {"status": "deleted"} @router.post("/accounts/{account_id}/test") async def mail_account_test(account_id: str): _require_mail(account_id) return await _asyncio.to_thread(mail.test_connection, account_id) # --- mailbox ----------------------------------------------------------------------- @router.get("/folders") async def mail_folders(account_id: str): _require_mail(account_id) try: return {"folders": await _asyncio.to_thread(mail.list_folders, account_id)} except Exception as e: raise HTTPException(status_code=502, detail=f"IMAP error: {e}") @router.get("/messages") async def mail_messages(account_id: str, folder: str = "INBOX", limit: int = 30, offset: int = 0): _require_mail(account_id) try: msgs = await _asyncio.to_thread(mail.list_messages, account_id, folder, limit, offset) return {"messages": msgs} except Exception as e: raise HTTPException(status_code=502, detail=f"IMAP error: {e}") @router.get("/message") async def mail_message(account_id: str, folder: str, uid: str): _require_mail(account_id) try: msg = await _asyncio.to_thread(mail.get_message, account_id, folder, uid) except Exception as e: raise HTTPException(status_code=502, detail=f"IMAP error: {e}") if not msg: raise HTTPException(status_code=404, detail="message not found") return msg @router.post("/send") async def mail_send(payload: Dict[str, Any] = Body(...)): account_id = payload.get("account_id", "") _require_mail(account_id) to = (payload.get("to") or "").strip() if not to: raise HTTPException(status_code=400, detail="'to' is required") try: return await _asyncio.to_thread( mail.send_message, account_id, to, payload.get("subject", ""), payload.get("body", ""), payload.get("cc", ""), payload.get("from_addr", ""), ) except Exception as e: raise HTTPException(status_code=502, detail=f"SMTP error: {e}") @router.post("/seen") async def mail_seen(payload: Dict[str, Any] = Body(...)): account_id = payload.get("account_id", "") _require_mail(account_id) await _asyncio.to_thread(mail.set_seen, account_id, payload["folder"], payload["uid"], payload.get("seen", True)) return {"status": "ok"} @router.post("/delete") async def mail_delete(payload: Dict[str, Any] = Body(...)): account_id = payload.get("account_id", "") _require_mail(account_id) await _asyncio.to_thread(mail.delete_message, account_id, payload["folder"], payload["uid"]) return {"status": "deleted"}