Files
webmail-client/backend/app/api/schemas.py
T
lukasandClaude Opus 5.5 5bf8fc81f6 Search: all results with a real count; bulk actions on results stay done
Search showed at most the first 50 matching messages ("1–50 of 50"). The
first page now carries the number of matching conversations, further pages
load as the list is paged through (a conversation spanning two appears once;
oldest-first loads them all, up to 40 pages), and the Unread/Pinned tabs filter
on the server, so their count covers every result.

Bulk Delete / Archive / Restore over selected results acts at once: the rows
leave the list (greyed instead when archived from a search, which still finds
them), the search results are refreshed, and the reading pane returns to
"Select a conversation". Each move landing on the server refreshes them again,
and a hidden row only comes back when no move has landed for 30 seconds — a
long batch no longer brings deleted results back.

Settings → Accounts → Notifications: shorter description.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 21:15:06 +02:00

117 lines
2.7 KiB
Python

"""Shared API response models (SPEC §9.1)."""
from __future__ import annotations
import datetime as dt
import uuid
from pydantic import BaseModel
class Address(BaseModel):
name: str = ""
email: str = ""
class ThreadSummary(BaseModel):
id: uuid.UUID
subject: str
snippet: str
participants: list[Address]
# Distinct From addresses of the thread's visible messages, oldest first —
# what the list's sender column shows (recipients excluded).
senders: list[Address] = []
account_ids: list[uuid.UUID]
message_count: int
unread_count: int
has_attachments: bool
pinned: bool
last_message_at: dt.datetime
class ThreadListOut(BaseModel):
threads: list[ThreadSummary]
cursor: str | None = None
total: int = 0
class AttachmentOut(BaseModel):
id: uuid.UUID
filename: str
mime_type: str
size_bytes: int
class MessageEnvelope(BaseModel):
id: uuid.UUID
account_id: uuid.UUID
mailbox_id: uuid.UUID
thread_id: uuid.UUID | None
subject: str
from_addr: list[Address]
to_addr: list[Address]
cc_addr: list[Address]
bcc_addr: list[Address]
date: dt.datetime
flags: list[str]
is_unread: bool
is_starred: bool
has_attachments: bool
has_remote_images: bool
# The sender is on the user's trusted list, so remote images load without a prompt.
images_allowed: bool = False
size_bytes: int
snippet: str
# Other accounts this same delivery also landed in (collapsed duplicates).
also_in_account_ids: list[uuid.UUID] = []
class ThreadDetail(BaseModel):
id: uuid.UUID
subject: str
order: str # "asc" | "desc"
pinned: bool
messages: list[MessageEnvelope] # always ASC (SPEC §9.4)
class FlagChangeRequest(BaseModel):
add: list[str] = []
remove: list[str] = []
class MarkReadRequest(BaseModel):
view: str = "unified" # unified | folder:<special> | account:<uuid>
class MoveRequest(BaseModel):
mailbox_id: uuid.UUID
class AccountOut(BaseModel):
id: uuid.UUID
email: str
display_name: str
color: str
state: str
signature: str = ""
sync_window: str = "all"
backfill_total: int = 0
backfill_done: int = 0
# "unreachable" while the mail server has not answered for a while.
sync_error: str | None = None
sync_error_since: str | None = None
# Why the state is what it is (e.g. the server's reply to a refused sign-in).
state_detail: str = ""
class SearchThreadHit(BaseModel):
thread: ThreadSummary
highlights: dict[str, str]
class SearchOut(BaseModel):
threads: list[SearchThreadHit]
cursor: str | None = None
# Conversations matching in all (first page only; None where not counted).
total: int | None = None