"""Mailbox (folder) listing (SPEC ยง9.3 folder views, archive action).""" from __future__ import annotations import uuid from typing import Annotated from fastapi import APIRouter, Depends from pydantic import BaseModel from sqlalchemy import func, select from sqlalchemy.ext.asyncio import AsyncSession from app.db import get_session from app.identity.deps import CurrentUser from app.models import Draft, Mailbox, Message from app.outbox.service import not_being_sent router = APIRouter(prefix="/mailboxes", tags=["mail"]) DbSession = Annotated[AsyncSession, Depends(get_session)] class MailboxOut(BaseModel): id: uuid.UUID account_id: uuid.UUID path: str special_use: str | None class FolderCounts(BaseModel): inbox_unread: int # unread messages sitting in Inbox mailboxes drafts: int # drafts shown in the Drafts folder (local + un-adopted remote) @router.get("", response_model=list[MailboxOut]) async def list_mailboxes(user: CurrentUser, db: DbSession) -> list[MailboxOut]: rows = await db.scalars( select(Mailbox).where(Mailbox.user_id == user.id).order_by(Mailbox.account_id, Mailbox.path) ) return [ MailboxOut(id=m.id, account_id=m.account_id, path=m.path, special_use=m.special_use) for m in rows ] @router.get("/counts", response_model=FolderCounts) async def folder_counts(user: CurrentUser, db: DbSession) -> FolderCounts: """Badge counts for the sidebar: unread Inbox messages and Drafts-folder items.""" inbox_unread = await db.scalar( select(func.count()) .select_from(Message) .join(Mailbox, Mailbox.id == Message.mailbox_id) .where( Message.user_id == user.id, Mailbox.special_use == "inbox", ~Message.flags.contains(["\\Seen"]), ) ) # Drafts folder = local drafts + IMAP-Drafts messages we haven't adopted (mirror # UIDs excluded), matching what list_drafts + list_remote_drafts return. local = await db.scalar( select(func.count()).select_from(Draft).where(Draft.user_id == user.id, not_being_sent()) ) owned = { (r.account_id, r.imap_uid) for r in await db.execute( select(Draft.account_id, Draft.imap_uid).where( Draft.user_id == user.id, Draft.imap_uid.isnot(None) ) ) } remote_rows = await db.execute( select(Message.account_id, Message.uid) .join(Mailbox, Mailbox.id == Message.mailbox_id) .where(Message.user_id == user.id, Mailbox.special_use == "drafts") ) remote = sum(1 for r in remote_rows if (r.account_id, r.uid) not in owned) return FolderCounts(inbox_unread=int(inbox_unread or 0), drafts=int(local or 0) + remote)