Initial commit: webmail client (M0/M1)
Three-pane webmail client. - Backend: FastAPI + async SQLAlchemy + arq worker (Postgres/Redis), IMAP/SMTP sync, threading, search, drafts/outbox with undo, attachments, 2FA. - Frontend: React 18 + Vite + TypeScript + TanStack Query + Tailwind; three-pane UI with rich-text composer, light/dark themes, resizable panels. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,200 @@
|
||||
"""initial schema (SPEC §4)
|
||||
|
||||
Revision ID: 0001
|
||||
Revises:
|
||||
Create Date: 2026-07-16
|
||||
|
||||
The authoritative DDL lives in SPEC §4; this migration reproduces it verbatim.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
from alembic import op
|
||||
|
||||
revision: str = "0001"
|
||||
down_revision: str | None = None
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
UPGRADE_STMTS: list[str] = [
|
||||
"CREATE EXTENSION IF NOT EXISTS citext",
|
||||
"""
|
||||
CREATE TABLE users (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
handle citext UNIQUE NOT NULL,
|
||||
password_hash text NOT NULL,
|
||||
totp_secret bytea,
|
||||
recovery_email citext,
|
||||
settings jsonb NOT NULL DEFAULT '{}',
|
||||
created_at timestamptz NOT NULL DEFAULT now()
|
||||
)
|
||||
""",
|
||||
"""
|
||||
CREATE TABLE accounts (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
email citext NOT NULL,
|
||||
display_name text NOT NULL DEFAULT '',
|
||||
color text NOT NULL DEFAULT '#888888',
|
||||
imap_config jsonb NOT NULL,
|
||||
smtp_config jsonb NOT NULL,
|
||||
secret_ct bytea NOT NULL,
|
||||
discovery_src text NOT NULL,
|
||||
state text NOT NULL DEFAULT 'backfilling',
|
||||
state_detail text NOT NULL DEFAULT '',
|
||||
last_sync_at timestamptz,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
UNIQUE (user_id, email)
|
||||
)
|
||||
""",
|
||||
"CREATE INDEX ix_accounts_user_id ON accounts (user_id)",
|
||||
"""
|
||||
CREATE TABLE mailboxes (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
account_id uuid NOT NULL REFERENCES accounts(id) ON DELETE CASCADE,
|
||||
user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
path text NOT NULL,
|
||||
special_use text,
|
||||
uidvalidity bigint NOT NULL DEFAULT 0,
|
||||
highestmodseq bigint,
|
||||
last_seen_uid bigint NOT NULL DEFAULT 0,
|
||||
UNIQUE (account_id, path)
|
||||
)
|
||||
""",
|
||||
"""
|
||||
CREATE TABLE threads (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
subject_norm text NOT NULL,
|
||||
snippet text NOT NULL DEFAULT '',
|
||||
participants jsonb NOT NULL DEFAULT '[]',
|
||||
account_ids uuid[] NOT NULL DEFAULT '{}',
|
||||
message_count int NOT NULL DEFAULT 0,
|
||||
unread_count int NOT NULL DEFAULT 0,
|
||||
has_attachments boolean NOT NULL DEFAULT false,
|
||||
last_message_at timestamptz NOT NULL
|
||||
)
|
||||
""",
|
||||
"CREATE INDEX ix_threads_user_last ON threads (user_id, last_message_at DESC)",
|
||||
"""
|
||||
CREATE TABLE messages (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
account_id uuid NOT NULL REFERENCES accounts(id) ON DELETE CASCADE,
|
||||
mailbox_id uuid NOT NULL REFERENCES mailboxes(id) ON DELETE CASCADE,
|
||||
uid bigint NOT NULL,
|
||||
thread_id uuid REFERENCES threads(id),
|
||||
message_id_hdr text,
|
||||
in_reply_to text,
|
||||
references_hdr text[] NOT NULL DEFAULT '{}',
|
||||
subject text NOT NULL DEFAULT '',
|
||||
from_addr jsonb NOT NULL DEFAULT '[]',
|
||||
to_addr jsonb NOT NULL DEFAULT '[]',
|
||||
cc_addr jsonb NOT NULL DEFAULT '[]',
|
||||
date_hdr timestamptz,
|
||||
internal_date timestamptz NOT NULL,
|
||||
flags text[] NOT NULL DEFAULT '{}',
|
||||
size_bytes int NOT NULL DEFAULT 0,
|
||||
has_attachments boolean NOT NULL DEFAULT false,
|
||||
snippet text NOT NULL DEFAULT '',
|
||||
blob_sha256 text NOT NULL,
|
||||
body_indexed tsvector,
|
||||
UNIQUE (mailbox_id, uid)
|
||||
)
|
||||
""",
|
||||
"CREATE INDEX ix_messages_user_thread ON messages (user_id, thread_id)",
|
||||
"CREATE INDEX ix_messages_account_msgid ON messages (account_id, message_id_hdr)",
|
||||
"CREATE INDEX ix_messages_body_indexed ON messages USING gin (body_indexed)",
|
||||
"""
|
||||
CREATE TABLE body_parts (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
message_id uuid NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
|
||||
kind text NOT NULL,
|
||||
content text NOT NULL,
|
||||
sanitizer_v int NOT NULL DEFAULT 0
|
||||
)
|
||||
""",
|
||||
"""
|
||||
CREATE TABLE attachments (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
message_id uuid NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
|
||||
user_id uuid NOT NULL,
|
||||
filename text NOT NULL,
|
||||
mime_type text NOT NULL,
|
||||
size_bytes int NOT NULL,
|
||||
content_id text,
|
||||
blob_sha256 text NOT NULL
|
||||
)
|
||||
""",
|
||||
"""
|
||||
CREATE TABLE drafts (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
account_id uuid NOT NULL REFERENCES accounts(id) ON DELETE CASCADE,
|
||||
thread_id uuid REFERENCES threads(id),
|
||||
reply_to_message_id uuid REFERENCES messages(id),
|
||||
kind text NOT NULL DEFAULT 'reply',
|
||||
payload jsonb NOT NULL,
|
||||
updated_at timestamptz NOT NULL DEFAULT now()
|
||||
)
|
||||
""",
|
||||
"""
|
||||
CREATE TABLE outbox (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id uuid NOT NULL,
|
||||
draft_id uuid NOT NULL,
|
||||
state text NOT NULL DEFAULT 'undo_window',
|
||||
send_after timestamptz NOT NULL,
|
||||
error text
|
||||
)
|
||||
""",
|
||||
"""
|
||||
CREATE TABLE saved_searches (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
name text NOT NULL,
|
||||
query text NOT NULL,
|
||||
pinned boolean NOT NULL DEFAULT true,
|
||||
UNIQUE (user_id, name)
|
||||
)
|
||||
""",
|
||||
"""
|
||||
CREATE TABLE known_recipients (
|
||||
user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
email citext NOT NULL,
|
||||
name text NOT NULL DEFAULT '',
|
||||
times_used int NOT NULL DEFAULT 0,
|
||||
last_used_at timestamptz NOT NULL,
|
||||
PRIMARY KEY (user_id, email)
|
||||
)
|
||||
""",
|
||||
"CREATE INDEX ix_known_recipients_user_email "
|
||||
"ON known_recipients (user_id, email text_pattern_ops)",
|
||||
]
|
||||
|
||||
DOWNGRADE_STMTS: list[str] = [
|
||||
"DROP TABLE IF EXISTS known_recipients",
|
||||
"DROP TABLE IF EXISTS saved_searches",
|
||||
"DROP TABLE IF EXISTS outbox",
|
||||
"DROP TABLE IF EXISTS drafts",
|
||||
"DROP TABLE IF EXISTS attachments",
|
||||
"DROP TABLE IF EXISTS body_parts",
|
||||
"DROP TABLE IF EXISTS messages",
|
||||
"DROP TABLE IF EXISTS threads",
|
||||
"DROP TABLE IF EXISTS mailboxes",
|
||||
"DROP TABLE IF EXISTS accounts",
|
||||
"DROP TABLE IF EXISTS users",
|
||||
]
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
for stmt in UPGRADE_STMTS:
|
||||
op.execute(stmt)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
for stmt in DOWNGRADE_STMTS:
|
||||
op.execute(stmt)
|
||||
Reference in New Issue
Block a user