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:
2026-07-17 23:20:34 +02:00
co-authored by Claude Opus 4.8
commit a5ec0d21cb
174 changed files with 22239 additions and 0 deletions
+52
View File
@@ -0,0 +1,52 @@
"""Recipient autocomplete from known_recipients (SPEC §9.1, §4).
Prefix match on email or name, ranked by ``times_used`` decayed by recency. The
composer merges these with plugin capability providers client-side (M3).
"""
from __future__ import annotations
from typing import Annotated
from fastapi import APIRouter, Depends, Query
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 KnownRecipient
router = APIRouter(prefix="/recipients", tags=["compose"])
DbSession = Annotated[AsyncSession, Depends(get_session)]
class Suggestion(BaseModel):
name: str
email: str
@router.get("/suggest", response_model=list[Suggestion])
async def suggest(
user: CurrentUser, db: DbSession, q: str = Query("", max_length=200)
) -> list[Suggestion]:
term = q.strip()
if not term:
return []
like = f"{term}%"
# Recency-decayed score: times_used / (1 + age_in_days). Ranks frequent and
# recent correspondents ahead of stale ones (SPEC §9.1).
age_days = func.extract("epoch", func.now() - KnownRecipient.last_used_at) / 86400.0
score = KnownRecipient.times_used / (1.0 + age_days)
stmt = (
select(KnownRecipient.name, KnownRecipient.email)
.where(
KnownRecipient.user_id == user.id,
KnownRecipient.email.ilike(like) | KnownRecipient.name.ilike(like),
)
.order_by(score.desc())
.limit(10)
)
rows = (await db.execute(stmt)).all()
return [Suggestion(name=row.name, email=row.email) for row in rows]