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:
@@ -0,0 +1,75 @@
|
||||
"""hosted mailboxes: native accounts and the mail server's directory (SPEC App. D)
|
||||
|
||||
* ``accounts.kind`` — ``external`` (any IMAP/SMTP server, the user's own
|
||||
password in the vault) or ``native`` (the user's mailbox on the hosted mail
|
||||
server: permanent, signed in to without any stored password, §D.4).
|
||||
* ``mail_directory`` — the view the mail server's SQL directory reads (§D.3):
|
||||
one row per user, its address — and no password: the webmail password never
|
||||
opens the mailbox in a mail app, only app passwords do (the mail server's
|
||||
own, §D.5) — and ``active`` false for a blocked user. Plus the service accounts of
|
||||
``mail_service_accounts``: the webmail's own worker, which may act for any
|
||||
mailbox and nothing else.
|
||||
* ``user_app_passwords.server_id`` — the mail server's id of the same app
|
||||
password (§D.5), to revoke it there too.
|
||||
|
||||
The mail server reads the view as its own role, given SELECT on it alone by
|
||||
``app.hosted.provision`` (the role is created there, with its password).
|
||||
|
||||
Revision ID: 0025
|
||||
Revises: 0024
|
||||
Create Date: 2026-10-11
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
from alembic import op
|
||||
|
||||
revision: str = "0025"
|
||||
down_revision: str | None = "0024"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.execute("ALTER TABLE accounts ADD COLUMN kind text NOT NULL DEFAULT 'external'")
|
||||
op.execute(
|
||||
"ALTER TABLE accounts ADD CONSTRAINT ck_accounts_kind "
|
||||
"CHECK (kind IN ('external', 'native'))"
|
||||
)
|
||||
# One mailbox per user.
|
||||
op.execute("CREATE UNIQUE INDEX uq_accounts_native ON accounts (user_id) WHERE kind = 'native'")
|
||||
op.execute("ALTER TABLE user_app_passwords ADD COLUMN server_id text")
|
||||
op.execute(
|
||||
"""
|
||||
CREATE TABLE mail_service_accounts (
|
||||
name citext PRIMARY KEY,
|
||||
secret text NOT NULL,
|
||||
description text NOT NULL DEFAULT ''
|
||||
)
|
||||
"""
|
||||
)
|
||||
op.execute(
|
||||
"""
|
||||
CREATE VIEW mail_directory AS
|
||||
SELECT u.handle AS name,
|
||||
NULL::text AS secret,
|
||||
'individual'::text AS type,
|
||||
u.handle::text AS description,
|
||||
u.blocked_at IS NULL AS active
|
||||
FROM users u
|
||||
UNION ALL
|
||||
SELECT s.name, s.secret, 'individual'::text, s.description, true
|
||||
FROM mail_service_accounts s
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.execute("DROP VIEW IF EXISTS mail_directory")
|
||||
op.execute("DROP TABLE IF EXISTS mail_service_accounts")
|
||||
op.execute("ALTER TABLE user_app_passwords DROP COLUMN IF EXISTS server_id")
|
||||
op.execute("DROP INDEX IF EXISTS uq_accounts_native")
|
||||
op.execute("ALTER TABLE accounts DROP CONSTRAINT IF EXISTS ck_accounts_kind")
|
||||
op.execute("ALTER TABLE accounts DROP COLUMN IF EXISTS kind")
|
||||
Reference in New Issue
Block a user