M6 hosted mailboxes (off by default); dark message mode; fixes
Hosted mailboxes (SPEC App. D), behind HOSTED_MAILBOXES (off: unchanged):
- Stalwart mail server, provisioned by `python -m app.hosted.provision`;
directory read from a Postgres view, app passwords only for mail apps
- native account per user, the worker signs in as master for the user
- raw messages of hosted mailboxes only cached, fetched back on demand
- migrations 0025 (accounts.kind, directory view), 0026 (IMAP ids bigint)
Mail:
- dark message mode switch in the reading pane (Thunderbird's algorithm)
- moves and folder creation quote the folder name ("Deleted Items"):
swipe-to-delete / Trash no longer bounce back
- thread unread counters: lock before recount, sorted lock order,
UIDVALIDITY re-import recounts, nightly repair_threads
- sidebar accounts sorted alphabetically, more space above headings
Settings: Security headings renamed; antispam accounts are explicit
(none ticked = none filtered), input widths, Activity hidden.
Plugin bundles served with Cache-Control: no-cache.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import contextlib
|
||||
import html as html_lib
|
||||
import io
|
||||
import uuid
|
||||
@@ -21,7 +22,7 @@ from app.api.settings import add_allowed_image_sender, allowed_image_senders
|
||||
from app.db import get_session
|
||||
from app.identity.deps import CurrentUser, require_csrf
|
||||
from app.indexer import attachments as att_store
|
||||
from app.indexer import blobstore, thumbs
|
||||
from app.indexer import rawstore, thumbs
|
||||
from app.indexer.mime import parse_message
|
||||
from app.indexer.sanitize import SANITIZER_VERSION, sanitize_html
|
||||
from app.indexer.thumbs import thumb_type
|
||||
@@ -128,7 +129,7 @@ async def message_html(
|
||||
# If the raw blob is unavailable, fall back to whatever is already stored.
|
||||
if allow_remote or stale:
|
||||
try:
|
||||
raw = blobstore.load(user.id, message.blob_sha256)
|
||||
raw = await rawstore.ensure_bytes(message)
|
||||
except FileNotFoundError:
|
||||
raw = None
|
||||
if raw is not None:
|
||||
@@ -191,8 +192,16 @@ def _content_disposition(disposition: str, filename: str) -> str:
|
||||
return f"{disposition}; filename=\"{fallback}\"; filename*=UTF-8''{quote(filename)}"
|
||||
|
||||
|
||||
async def _raw_ready(msg: Message) -> None:
|
||||
"""Have the message's raw source here (a hosted mailbox's is fetched from
|
||||
the mail server when not cached); a miss is left to the reader to report."""
|
||||
with contextlib.suppress(FileNotFoundError):
|
||||
await rawstore.ensure(msg)
|
||||
|
||||
|
||||
async def _attachment_bytes(msg: Message, att: Attachment) -> bytes:
|
||||
"""An attachment's bytes, taken out of its message (404 if they cannot be)."""
|
||||
await _raw_ready(msg)
|
||||
try:
|
||||
return await asyncio.to_thread(
|
||||
att_store.load, msg.user_id, msg.blob_sha256, att.blob_sha256, att.part_index
|
||||
@@ -236,6 +245,7 @@ async def forward_attachments(
|
||||
of its own (shared, by hash, with any other use of the same file).
|
||||
Returns them as the composer lists its attachments."""
|
||||
msg = await _owned_message(db, user.id, message_id)
|
||||
await _raw_ready(msg)
|
||||
rows = [
|
||||
ComposeAttachment(
|
||||
user_id=user.id,
|
||||
@@ -315,6 +325,8 @@ async def attachment_thumbnail(
|
||||
kind = thumb_type(att.mime_type, att.filename) if att is not None else None
|
||||
if att is None or kind is None:
|
||||
raise_api_error(404, "not_found", "no thumbnail")
|
||||
if not thumbs.made(msg.user_id, att.blob_sha256):
|
||||
await _raw_ready(msg)
|
||||
data = await asyncio.to_thread(
|
||||
thumbs.thumbnail,
|
||||
msg.user_id,
|
||||
@@ -350,6 +362,8 @@ async def message_attachment(
|
||||
)
|
||||
if att is None:
|
||||
raise_api_error(404, "not_found", "attachment not found")
|
||||
if not att_store.has_file(msg.user_id, att.blob_sha256):
|
||||
await _raw_ready(msg)
|
||||
try:
|
||||
# Taken out of the message into a short-lived file (kept a day after
|
||||
# its last use), so it can be streamed with HTTP Range like before.
|
||||
|
||||
Reference in New Issue
Block a user