2026-07-17 23:20:34 +02:00
|
|
|
"""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
|
2026-07-22 07:40:29 +02:00
|
|
|
from sqlalchemy import func, select
|
2026-07-17 23:20:34 +02:00
|
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
|
|
|
|
|
|
from app.db import get_session
|
|
|
|
|
from app.identity.deps import CurrentUser
|
2026-07-22 07:40:29 +02:00
|
|
|
from app.models import Draft, Mailbox, Message
|
2026-10-05 22:08:42 +02:00
|
|
|
from app.outbox.service import not_being_sent
|
2026-07-17 23:20:34 +02:00
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
2026-07-22 07:40:29 +02:00
|
|
|
class FolderCounts(BaseModel):
|
|
|
|
|
inbox_unread: int # unread messages sitting in Inbox mailboxes
|
|
|
|
|
drafts: int # drafts shown in the Drafts folder (local + un-adopted remote)
|
|
|
|
|
|
|
|
|
|
|
2026-07-17 23:20:34 +02:00
|
|
|
@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
|
|
|
|
|
]
|
2026-07-22 07:40:29 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@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.
|
2026-10-05 22:08:42 +02:00
|
|
|
local = await db.scalar(
|
|
|
|
|
select(func.count()).select_from(Draft).where(Draft.user_id == user.id, not_being_sent())
|
|
|
|
|
)
|
2026-07-22 07:40:29 +02:00
|
|
|
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)
|