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:
+14
-5
@@ -131,6 +131,10 @@ class Account(Base):
|
||||
imap_config: Mapped[dict[str, object]] = mapped_column(JSONB, nullable=False)
|
||||
smtp_config: Mapped[dict[str, object]] = mapped_column(JSONB, nullable=False)
|
||||
secret_ct: Mapped[bytes] = mapped_column(BYTEA, nullable=False)
|
||||
# external: any IMAP/SMTP server (the user's password in secret_ct);
|
||||
# native: the user's own mailbox on the hosted mail server — permanent, no
|
||||
# stored password (SPEC App. D §D.4; app/hosted/).
|
||||
kind: Mapped[str] = mapped_column(Text, nullable=False, server_default=text("'external'"))
|
||||
discovery_src: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
state: Mapped[str] = mapped_column(Text, nullable=False, server_default=text("'backfilling'"))
|
||||
state_detail: Mapped[str] = mapped_column(Text, nullable=False, server_default=text("''"))
|
||||
@@ -161,9 +165,12 @@ class Mailbox(Base):
|
||||
)
|
||||
path: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
special_use: Mapped[str | None] = mapped_column(Text)
|
||||
uidvalidity: Mapped[int] = mapped_column(Integer, nullable=False, server_default=text("0"))
|
||||
highestmodseq: Mapped[int | None] = mapped_column(Integer)
|
||||
last_seen_uid: Mapped[int] = mapped_column(Integer, nullable=False, server_default=text("0"))
|
||||
# IMAP ids are unsigned 32-bit (MODSEQ 63-bit): bigint, not integer.
|
||||
uidvalidity: Mapped[int] = mapped_column(BigInteger, nullable=False, server_default=text("0"))
|
||||
highestmodseq: Mapped[int | None] = mapped_column(BigInteger)
|
||||
last_seen_uid: Mapped[int] = mapped_column(
|
||||
BigInteger, nullable=False, server_default=text("0")
|
||||
)
|
||||
|
||||
|
||||
class Thread(Base):
|
||||
@@ -214,7 +221,7 @@ class Message(Base):
|
||||
restore_mailbox_id: Mapped[uuid.UUID | None] = mapped_column(
|
||||
UUID(as_uuid=True), ForeignKey("mailboxes.id", ondelete="SET NULL")
|
||||
)
|
||||
uid: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
uid: Mapped[int] = mapped_column(BigInteger, nullable=False)
|
||||
thread_id: Mapped[uuid.UUID | None] = mapped_column(
|
||||
UUID(as_uuid=True), ForeignKey("threads.id")
|
||||
)
|
||||
@@ -321,7 +328,7 @@ class Draft(Base):
|
||||
payload: Mapped[dict[str, object]] = mapped_column(JSONB, nullable=False)
|
||||
# UID of the mirrored copy in the account's IMAP Drafts folder (so the draft
|
||||
# is visible/continuable on other mail clients). Replaced on each edit.
|
||||
imap_uid: Mapped[int | None] = mapped_column(Integer)
|
||||
imap_uid: Mapped[int | None] = mapped_column(BigInteger)
|
||||
updated_at: Mapped[dt.datetime] = _now()
|
||||
|
||||
|
||||
@@ -367,6 +374,8 @@ class UserAppPassword(Base):
|
||||
created_at: Mapped[dt.datetime] = _now()
|
||||
# Set by the mail server's sign-in, once there is one.
|
||||
last_used_at: Mapped[dt.datetime | None] = mapped_column(TIMESTAMP(timezone=True))
|
||||
# The hosted mail server's id of the same password (§D.5), when it has one.
|
||||
server_id: Mapped[str | None] = mapped_column(Text)
|
||||
|
||||
|
||||
class PushSubscription(Base):
|
||||
|
||||
Reference in New Issue
Block a user