CREATE TABLE mail_accounts ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, name TEXT NOT NULL DEFAULT '', email TEXT NOT NULL, provider TEXT NOT NULL DEFAULT 'imap', imap_host TEXT NOT NULL DEFAULT '', imap_port INT NOT NULL DEFAULT 993, imap_tls BOOLEAN NOT NULL DEFAULT true, smtp_host TEXT NOT NULL DEFAULT '', smtp_port INT NOT NULL DEFAULT 587, smtp_tls BOOLEAN NOT NULL DEFAULT true, credentials BYTEA, last_sync_at TIMESTAMPTZ, sync_state JSONB NOT NULL DEFAULT '{}', is_active BOOLEAN NOT NULL DEFAULT true, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX idx_mail_accounts_user ON mail_accounts(user_id); CREATE TABLE mail_identities ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), account_id UUID NOT NULL REFERENCES mail_accounts(id) ON DELETE CASCADE, email TEXT NOT NULL, name TEXT NOT NULL DEFAULT '', is_default BOOLEAN NOT NULL DEFAULT false, signature_html TEXT NOT NULL DEFAULT '', created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX idx_mail_identities_account ON mail_identities(account_id); CREATE TABLE mail_folders ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), account_id UUID NOT NULL REFERENCES mail_accounts(id) ON DELETE CASCADE, name TEXT NOT NULL, remote_name TEXT NOT NULL, folder_type TEXT NOT NULL DEFAULT 'custom', uidvalidity BIGINT NOT NULL DEFAULT 0, highest_modseq BIGINT NOT NULL DEFAULT 0, message_count INT NOT NULL DEFAULT 0, unread_count INT NOT NULL DEFAULT 0, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), UNIQUE(account_id, remote_name) ); CREATE INDEX idx_mail_folders_account ON mail_folders(account_id); CREATE TABLE messages ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), account_id UUID NOT NULL REFERENCES mail_accounts(id) ON DELETE CASCADE, folder_id UUID NOT NULL REFERENCES mail_folders(id) ON DELETE CASCADE, message_id TEXT NOT NULL DEFAULT '', thread_id UUID, uid BIGINT NOT NULL DEFAULT 0, subject TEXT NOT NULL DEFAULT '', from_addr JSONB NOT NULL DEFAULT '[]', to_addrs JSONB NOT NULL DEFAULT '[]', cc_addrs JSONB NOT NULL DEFAULT '[]', bcc_addrs JSONB NOT NULL DEFAULT '[]', reply_to JSONB NOT NULL DEFAULT '[]', date TIMESTAMPTZ NOT NULL DEFAULT NOW(), snippet TEXT NOT NULL DEFAULT '', body_text TEXT NOT NULL DEFAULT '', body_html TEXT NOT NULL DEFAULT '', flags TEXT[] NOT NULL DEFAULT '{}', labels TEXT[] NOT NULL DEFAULT '{}', has_attachments BOOLEAN NOT NULL DEFAULT false, raw_size INT NOT NULL DEFAULT 0, in_reply_to TEXT NOT NULL DEFAULT '', references_header TEXT[] NOT NULL DEFAULT '{}', search_vector tsvector, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX idx_messages_account_folder ON messages(account_id, folder_id); CREATE INDEX idx_messages_thread ON messages(thread_id); CREATE INDEX idx_messages_date ON messages(date DESC); CREATE INDEX idx_messages_message_id ON messages(message_id); CREATE INDEX idx_messages_flags ON messages USING GIN(flags); CREATE INDEX idx_messages_labels ON messages USING GIN(labels); CREATE INDEX idx_messages_search ON messages USING GIN(search_vector); CREATE UNIQUE INDEX idx_messages_uid ON messages(folder_id, uid); -- Auto-update search_vector on insert/update CREATE FUNCTION messages_search_vector_update() RETURNS trigger AS $$ BEGIN NEW.search_vector := setweight(to_tsvector('simple', COALESCE(NEW.subject, '')), 'A') || setweight(to_tsvector('simple', COALESCE(NEW.from_addr::text, '')), 'B') || setweight(to_tsvector('simple', COALESCE(NEW.to_addrs::text, '')), 'B') || setweight(to_tsvector('simple', COALESCE(NEW.body_text, '')), 'C'); RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER messages_search_trigger BEFORE INSERT OR UPDATE OF subject, from_addr, to_addrs, body_text ON messages FOR EACH ROW EXECUTE FUNCTION messages_search_vector_update(); CREATE TABLE attachments ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), message_id UUID NOT NULL REFERENCES messages(id) ON DELETE CASCADE, filename TEXT NOT NULL DEFAULT '', content_type TEXT NOT NULL DEFAULT 'application/octet-stream', size BIGINT NOT NULL DEFAULT 0, s3_bucket TEXT NOT NULL DEFAULT 'mail-attachments', s3_key TEXT NOT NULL DEFAULT '', content_id TEXT NOT NULL DEFAULT '', is_inline BOOLEAN NOT NULL DEFAULT false, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX idx_attachments_message ON attachments(message_id); CREATE TABLE mail_rules ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, account_id UUID REFERENCES mail_accounts(id) ON DELETE CASCADE, name TEXT NOT NULL DEFAULT '', priority INT NOT NULL DEFAULT 0, is_active BOOLEAN NOT NULL DEFAULT true, conditions JSONB NOT NULL DEFAULT '[]', actions JSONB NOT NULL DEFAULT '[]', llm_config JSONB, match_count BIGINT NOT NULL DEFAULT 0, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX idx_mail_rules_user ON mail_rules(user_id); CREATE INDEX idx_mail_rules_priority ON mail_rules(user_id, priority); CREATE TABLE webhook_templates ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, name TEXT NOT NULL, url TEXT NOT NULL, method TEXT NOT NULL DEFAULT 'POST', headers JSONB NOT NULL DEFAULT '{}', body_template TEXT NOT NULL DEFAULT '', is_active BOOLEAN NOT NULL DEFAULT true, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX idx_webhook_templates_user ON webhook_templates(user_id); CREATE TABLE webhook_logs ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), template_id UUID NOT NULL REFERENCES webhook_templates(id) ON DELETE CASCADE, message_id UUID REFERENCES messages(id) ON DELETE SET NULL, status_code INT, response_body TEXT NOT NULL DEFAULT '', error TEXT NOT NULL DEFAULT '', duration_ms INT NOT NULL DEFAULT 0, executed_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX idx_webhook_logs_template ON webhook_logs(template_id); CREATE INDEX idx_webhook_logs_executed ON webhook_logs(executed_at DESC); CREATE TABLE outbox ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, account_id UUID NOT NULL REFERENCES mail_accounts(id) ON DELETE CASCADE, identity_id UUID REFERENCES mail_identities(id) ON DELETE SET NULL, to_addrs JSONB NOT NULL DEFAULT '[]', cc_addrs JSONB NOT NULL DEFAULT '[]', bcc_addrs JSONB NOT NULL DEFAULT '[]', subject TEXT NOT NULL DEFAULT '', body_text TEXT NOT NULL DEFAULT '', body_html TEXT NOT NULL DEFAULT '', in_reply_to TEXT NOT NULL DEFAULT '', references_header TEXT[] NOT NULL DEFAULT '{}', attachments JSONB NOT NULL DEFAULT '[]', scheduled_at TIMESTAMPTZ, sent_at TIMESTAMPTZ, status TEXT NOT NULL DEFAULT 'draft', error TEXT NOT NULL DEFAULT '', retry_count INT NOT NULL DEFAULT 0, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX idx_outbox_user ON outbox(user_id); CREATE INDEX idx_outbox_status ON outbox(status) WHERE status IN ('queued', 'sending'); CREATE INDEX idx_outbox_scheduled ON outbox(scheduled_at) WHERE scheduled_at IS NOT NULL AND status = 'queued';