76 lines
2.8 KiB
Python
76 lines
2.8 KiB
Python
"""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")
|