Initial commit: webmail client (M0/M1)
Three-pane webmail client. - Backend: FastAPI + async SQLAlchemy + arq worker (Postgres/Redis), IMAP/SMTP sync, threading, search, drafts/outbox with undo, attachments, 2FA. - Frontend: React 18 + Vite + TypeScript + TanStack Query + Tailwind; three-pane UI with rich-text composer, light/dark themes, resizable panels. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
# INTERFACE VERSION: 1
|
||||
"""SearchBackend — the frozen search contract (SPEC §5.2).
|
||||
|
||||
Both backends (``pg.py`` for M0-M1, ``meili.py`` for M2) index the same
|
||||
:class:`SearchDoc` and honour invariant I-7 (every query carries ``user_id``).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Protocol
|
||||
from uuid import UUID
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SearchQuery:
|
||||
text: str | None # free-text remainder after operator extraction
|
||||
filters: dict[str, object] # parsed operators, keys per §8.2
|
||||
user_id: UUID # ALWAYS set (invariant I-7)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SearchHit:
|
||||
message_id: UUID
|
||||
thread_id: UUID
|
||||
score: float
|
||||
highlights: dict[str, str] # field -> HTML-escaped snippet with <mark>
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SearchDoc:
|
||||
"""The document indexed by every backend (SPEC §5.2)."""
|
||||
|
||||
message_id: UUID
|
||||
thread_id: UUID
|
||||
user_id: UUID
|
||||
account_id: UUID
|
||||
mailbox_special_use: str | None
|
||||
from_: list[dict[str, str]] # name+email
|
||||
to: list[dict[str, str]]
|
||||
cc: list[dict[str, str]]
|
||||
subject: str
|
||||
body_text: str
|
||||
attachment_names: list[str] = field(default_factory=list)
|
||||
has_attachments: bool = False
|
||||
is_unread: bool = False
|
||||
is_starred: bool = False
|
||||
date: int = 0 # unix seconds
|
||||
size_bytes: int = 0
|
||||
|
||||
|
||||
class SearchBackend(Protocol):
|
||||
async def index(self, doc: SearchDoc) -> None: ...
|
||||
async def delete(self, message_id: UUID) -> None: ...
|
||||
async def delete_user(self, user_id: UUID) -> None: ...
|
||||
async def query(
|
||||
self, q: SearchQuery, *, limit: int, cursor: str | None
|
||||
) -> tuple[list[SearchHit], str | None]: ...
|
||||
Reference in New Issue
Block a user