-- Embedded mail: one shared IMAP/SMTP server configured by admins, with a -- per-user mailbox login. -- -- Threat model drives the shape here. A mailbox password grants full read and -- send access to someone's email, so: -- -- * user_mailboxes has RLS enabled and NO policies at all. Nothing reachable -- from a browser can read it — not even the mailbox owner, and not admins. -- Every access goes through server functions using the service role, the -- same pattern intake_tokens uses. -- * The password is never stored as plaintext. It is sealed with AES-256-GCM -- using MAIL_CRED_KEY from .env.secret, so a database dump alone is not -- enough to open anyone's mail. -- * The IMAP/SMTP *host* settings are not secret, so admins may read those -- directly to populate the setup form. CREATE TABLE IF NOT EXISTS public.mail_server_settings ( -- Single-row table: the school has one mail server. id BOOLEAN PRIMARY KEY DEFAULT TRUE CHECK (id = TRUE), imap_host TEXT NOT NULL, imap_port INT NOT NULL DEFAULT 993, imap_secure BOOLEAN NOT NULL DEFAULT TRUE, -- implicit TLS on connect smtp_host TEXT NOT NULL, smtp_port INT NOT NULL DEFAULT 587, smtp_secure BOOLEAN NOT NULL DEFAULT FALSE, -- false = STARTTLS upgrade updated_by UUID REFERENCES auth.users(id), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); GRANT SELECT ON public.mail_server_settings TO authenticated; GRANT ALL ON public.mail_server_settings TO service_role; ALTER TABLE public.mail_server_settings ENABLE ROW LEVEL SECURITY; -- Host/port are operational config, not secrets. Admins read them to fill in -- the setup form; writes go through a server function so they can be validated -- against a live login first. CREATE POLICY "mail settings admin read" ON public.mail_server_settings FOR SELECT TO authenticated USING (public.current_user_has_role('admin')); CREATE TABLE IF NOT EXISTS public.user_mailboxes ( user_id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE, email TEXT NOT NULL, -- AES-256-GCM sealed mailbox password. Never returned to any client. secret_ciphertext TEXT NOT NULL, secret_iv TEXT NOT NULL, secret_tag TEXT NOT NULL, last_verified_at TIMESTAMPTZ, created_by UUID REFERENCES auth.users(id), created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX IF NOT EXISTS idx_user_mailboxes_email ON public.user_mailboxes(email); GRANT ALL ON public.user_mailboxes TO service_role; ALTER TABLE public.user_mailboxes ENABLE ROW LEVEL SECURITY; -- Deliberately no policies and no grant to `authenticated`: credentials must -- never be reachable from the browser under any role. DROP TRIGGER IF EXISTS trg_mail_settings_upd ON public.mail_server_settings; CREATE TRIGGER trg_mail_settings_upd BEFORE UPDATE ON public.mail_server_settings FOR EACH ROW EXECUTE FUNCTION public.set_updated_at(); DROP TRIGGER IF EXISTS trg_user_mailboxes_upd ON public.user_mailboxes; CREATE TRIGGER trg_user_mailboxes_upd BEFORE UPDATE ON public.user_mailboxes FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();