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:
@@ -98,23 +98,25 @@ async def list_threads(
|
||||
raise_api_error(400, "bad_ids", "invalid thread id in ids")
|
||||
conditions.append(Thread.id.in_(wanted or [uuid.uuid4()]))
|
||||
elif view.startswith("account:"):
|
||||
# A single account's inbox: threads with a message in THIS account's
|
||||
# inbox mailbox. Keeps the per-account view consistent with the unified
|
||||
# Inbox (archived/trashed/sent threads don't leak in).
|
||||
# A single account's mail: threads with a message in ANY of THIS account's
|
||||
# mailboxes except Trash/Spam (those are only seen via their own folders).
|
||||
try:
|
||||
account_id = uuid.UUID(view.split(":", 1)[1])
|
||||
except ValueError:
|
||||
raise_api_error(400, "bad_view", "invalid account id in view")
|
||||
in_account_inbox = (
|
||||
in_account = (
|
||||
select(Message.thread_id)
|
||||
.join(Mailbox, Mailbox.id == Message.mailbox_id)
|
||||
.where(
|
||||
Message.user_id == user.id,
|
||||
Message.account_id == account_id,
|
||||
Mailbox.special_use == "inbox",
|
||||
or_(
|
||||
Mailbox.special_use.is_(None),
|
||||
Mailbox.special_use.notin_(_REMOVAL_FOLDERS),
|
||||
),
|
||||
)
|
||||
)
|
||||
conditions.append(Thread.id.in_(in_account_inbox))
|
||||
conditions.append(Thread.id.in_(in_account))
|
||||
elif view.startswith("folder:"):
|
||||
# Threads with at least one message in a mailbox of this special_use,
|
||||
# across all of the user's accounts (unified per-folder view).
|
||||
|
||||
Reference in New Issue
Block a user