"""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")