Sender avatars, PDF password preview, contacts autosave, account-view + Trash/Spam auto-purge, UI polish

Sender avatars (privacy-safe, server-fetched, cached):
- New GET /api/avatar resolves Gravatar (d=404) then the sender domain's favicon
  under the SSRF guard, Redis-cached (hit+miss); gated by SENDER_AVATARS_ENABLED.
  GuardedResponse gains raw bytes. Shown in the mail list, reading pane, People
  panel, and Contacts (overlay that falls back to initials on a miss).

PDF preview:
- Password-protected PDFs now prompt in-place and retry (pdf.js PasswordException),
  with a clear error for other load failures instead of a blank overlay. The
  password is remembered per attachment until the thread is left; clicking the
  field no longer dismisses the overlay.

Contacts:
- Autosave reply recipients: message.sent now carries to/cc (name+email); the
  contacts plugin subscribes and adds any non-contact recipients to the Autosaved
  group. Contact avatars show the gravatar/favicon via /api/avatar.

Mail views / retention:
- Clicking an account shows all its mail except Trash/Spam (was inbox-only).
- Daily cron autopurge_trash_spam permanently deletes Trash/Spam older than
  TRASH_RETENTION_DAYS (default 30; 0 disables) — server EXPUNGE + local rows.

UI polish:
- Ultimo signet logo in the Mail/Calendar/Contacts sidebar header and collapsed
  rail; consistent 11px top margin and vertically-aligned header row across states
  (fixed-size flex icon buttons); SVG favicon on the browser tab.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U72FZiF1W1pb8XxyWBTREA
This commit is contained in:
2026-08-06 19:23:27 +02:00
co-authored by Claude Opus 4.8
parent 4c200ab94a
commit 12a67825cc
23 changed files with 530 additions and 49 deletions
+7 -1
View File
@@ -21,13 +21,19 @@ import logging
import signal
from typing import Any
from arq import cron
from arq.worker import Worker
from app.config import get_settings
from app.queue import QUEUE_BULK, QUEUE_INTERACTIVE, redis_settings
from app.sync.actions import autopurge_trash_spam
from app.sync.coordinator import SyncCoordinator
from workers.worker import BULK_FUNCTIONS, INTERACTIVE_FUNCTIONS
# Core scheduled jobs. Daily Trash/Spam retention purge at 03:17 (off-peak, and a
# minute that won't collide with typical top-of-hour crons).
_CORE_CRON_JOBS = [cron(autopurge_trash_spam, hour=3, minute=17)]
logger = logging.getLogger(__name__)
_SHUTDOWN_DRAIN_S = 10.0 # §7: release leases + drain within this budget
@@ -67,7 +73,7 @@ def build_arq_workers(role: str) -> list[Worker]:
QUEUE_INTERACTIVE,
s.interactive_concurrency,
120,
cron_jobs=plugin_cron_jobs(),
cron_jobs=[*_CORE_CRON_JOBS, *plugin_cron_jobs()],
),
_worker(BULK_FUNCTIONS, QUEUE_BULK, s.bulk_concurrency, 3600),
]