-- OpenMail initial schema. See ARCHITECTURE.md ยง3. -- Native, agent-shaped data model (not Mox's per-account bbolt index). CREATE EXTENSION IF NOT EXISTS pgcrypto; -- gen_random_uuid() -- Tenant isolation. CREATE TABLE IF NOT EXISTS pods ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), name text NOT NULL, created_at timestamptz NOT NULL DEFAULT now(), UNIQUE (name) ); -- Per-domain DKIM keys + DNS verification state. CREATE TABLE IF NOT EXISTS domains ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), pod_id uuid NOT NULL REFERENCES pods(id) ON DELETE CASCADE, name text NOT NULL, dkim_selector text, dkim_privkey_ref text, -- pointer to key in secret store; never the key itself verified boolean NOT NULL DEFAULT false, created_at timestamptz NOT NULL DEFAULT now(), UNIQUE (name) ); -- An agent-owned address; first-class API resource. CREATE TABLE IF NOT EXISTS inboxes ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), pod_id uuid NOT NULL REFERENCES pods(id) ON DELETE CASCADE, address text NOT NULL, display_name text, metadata jsonb NOT NULL DEFAULT '{}'::jsonb, created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now(), UNIQUE (address) ); CREATE TABLE IF NOT EXISTS threads ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), inbox_id uuid NOT NULL REFERENCES inboxes(id) ON DELETE CASCADE, subject text, last_message_id uuid, message_count integer NOT NULL DEFAULT 0, labels text[] NOT NULL DEFAULT '{}', created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now() ); CREATE TABLE IF NOT EXISTS messages ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), inbox_id uuid NOT NULL REFERENCES inboxes(id) ON DELETE CASCADE, thread_id uuid REFERENCES threads(id) ON DELETE SET NULL, message_id_hdr text, -- RFC 5322 Message-ID in_reply_to text, "references" text[] NOT NULL DEFAULT '{}', from_addr text, to_addrs text[] NOT NULL DEFAULT '{}', cc text[] NOT NULL DEFAULT '{}', bcc text[] NOT NULL DEFAULT '{}', subject text, preview text, text text, html text, extracted_text text, -- quoted history stripped extracted_html text, raw_object_key text, -- pointer to raw .eml in object store spf text, -- inbound auth verdicts (from mox pkgs) dkim text, dmarc text, junk_score real, labels text[] NOT NULL DEFAULT '{}', size_bytes bigint, headers jsonb NOT NULL DEFAULT '{}'::jsonb, ts tsvector, -- full-text search created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now() ); CREATE TABLE IF NOT EXISTS attachments ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), message_id uuid NOT NULL REFERENCES messages(id) ON DELETE CASCADE, filename text, content_type text, size_bytes bigint, object_key text NOT NULL, inline boolean NOT NULL DEFAULT false, content_id text ); CREATE TABLE IF NOT EXISTS drafts ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), inbox_id uuid NOT NULL REFERENCES inboxes(id) ON DELETE CASCADE, thread_id uuid REFERENCES threads(id) ON DELETE SET NULL, to_addrs text[] NOT NULL DEFAULT '{}', cc text[] NOT NULL DEFAULT '{}', bcc text[] NOT NULL DEFAULT '{}', subject text, text text, html text, send_at timestamptz, client_id text, created_at timestamptz NOT NULL DEFAULT now() ); -- Bearer tokens; only the hash is stored. CREATE TABLE IF NOT EXISTS api_keys ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), pod_id uuid NOT NULL REFERENCES pods(id) ON DELETE CASCADE, hash bytea NOT NULL, scopes text[] NOT NULL DEFAULT '{}', created_at timestamptz NOT NULL DEFAULT now(), UNIQUE (hash) ); CREATE TABLE IF NOT EXISTS webhooks ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), pod_id uuid NOT NULL REFERENCES pods(id) ON DELETE CASCADE, url text NOT NULL, event_types text[] NOT NULL DEFAULT '{}', secret text NOT NULL, created_at timestamptz NOT NULL DEFAULT now() ); -- Outbound send queue + retries. CREATE TABLE IF NOT EXISTS outbox ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), message_id uuid NOT NULL REFERENCES messages(id) ON DELETE CASCADE, status text NOT NULL DEFAULT 'queued', -- queued|sending|sent|failed attempts integer NOT NULL DEFAULT 0, last_error text, next_attempt_at timestamptz NOT NULL DEFAULT now() ); CREATE TABLE IF NOT EXISTS events ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), inbox_id uuid REFERENCES inboxes(id) ON DELETE CASCADE, type text NOT NULL, payload jsonb NOT NULL DEFAULT '{}'::jsonb, created_at timestamptz NOT NULL DEFAULT now() ); CREATE INDEX IF NOT EXISTS idx_inboxes_pod ON inboxes(pod_id); CREATE INDEX IF NOT EXISTS idx_threads_inbox ON threads(inbox_id); CREATE INDEX IF NOT EXISTS idx_messages_thread ON messages(thread_id); CREATE INDEX IF NOT EXISTS idx_messages_inbox_time ON messages(inbox_id, created_at DESC); -- Supports per-delivery thread resolution (message_id_hdr = ANY(...) per inbox). CREATE INDEX IF NOT EXISTS idx_messages_msgid ON messages(inbox_id, message_id_hdr) WHERE message_id_hdr IS NOT NULL; CREATE INDEX IF NOT EXISTS idx_messages_ts ON messages USING gin(ts); CREATE INDEX IF NOT EXISTS idx_messages_labels ON messages USING gin(labels); CREATE INDEX IF NOT EXISTS idx_threads_labels ON threads USING gin(labels); CREATE INDEX IF NOT EXISTS idx_outbox_due ON outbox(next_attempt_at) WHERE status = 'queued';