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:
@@ -104,6 +104,7 @@ async def list_accounts(user: CurrentUser, db: DbSession) -> list[AccountOut]:
|
||||
sync_error=a.sync_error,
|
||||
sync_error_since=a.sync_error_since.isoformat() if a.sync_error_since else None,
|
||||
state_detail=a.state_detail,
|
||||
kind=a.kind,
|
||||
)
|
||||
for a in accounts
|
||||
]
|
||||
@@ -225,6 +226,8 @@ async def update_account(
|
||||
# re-encrypt the secret and re-sync. The password must be supplied (§10.2: the
|
||||
# stored secret is only decrypted in the worker, never here).
|
||||
conn_change = body.imap is not None or body.smtp is not None or body.password is not None
|
||||
if conn_change and account.kind == "native":
|
||||
raise_api_error(400, "native_account", "the settings of your own mailbox cannot be changed")
|
||||
if conn_change:
|
||||
if not body.password:
|
||||
raise_api_error(
|
||||
@@ -267,10 +270,20 @@ async def update_account(
|
||||
|
||||
@router.delete("/{account_id}", dependencies=[Depends(require_csrf)])
|
||||
async def remove_account(account_id: uuid.UUID, user: CurrentUser, db: DbSession) -> dict[str, str]:
|
||||
kind = await db.scalar(
|
||||
select(Account.kind).where(Account.id == account_id, Account.user_id == user.id)
|
||||
)
|
||||
if kind == "native":
|
||||
raise_api_error(409, "native_account", "your own mailbox cannot be removed")
|
||||
ok = await delete_account(db, user_id=user.id, account_id=account_id)
|
||||
if not ok:
|
||||
raise_api_error(404, "not_found", "account not found")
|
||||
await db.commit()
|
||||
# Removing an account for the user's own address (the mail at its old
|
||||
# provider) makes room for their hosted mailbox's (App. D §D.4).
|
||||
from app.hosted import native
|
||||
|
||||
await native.ensure_and_sync(db, user)
|
||||
return {"status": "deleted"}
|
||||
|
||||
|
||||
@@ -288,4 +301,5 @@ def _detail(account: Account) -> AccountDetailOut:
|
||||
backfill_done=account.backfill_done,
|
||||
imap_config=account.imap_config,
|
||||
smtp_config=account.smtp_config,
|
||||
kind=account.kind,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user