Admins configure one mail server; each staff member gets their own mailbox
login. Read, reply and compose against real IMAP/SMTP.
IMAP and SMTP are raw TCP, so none of this can run in a browser — every
operation is a TanStack Start server function. mail.functions.ts ships to
the client bundle, so imapflow/nodemailer/mailparser and the crypto
helpers are imported inside handlers, never at the top level. Verified
that Nitro inlines all three into .output/server/_libs, since the Docker
runner stage copies only .output and has no node_modules.
Note this ties the app to the Node deployment: the default local build
targets Cloudflare Workers, which cannot open IMAP sockets.
Credential handling, since a mailbox password grants full read and send
access to someone's mail:
- user_mailboxes has RLS enabled, no policies, and SELECT revoked from
anon and authenticated. Verified: teacher and admin both see zero rows
and no ciphertext; only service_role can read it. The revoke is belt and
braces — Supabase's default privileges had granted SELECT, leaving the
table one stray policy away from leaking.
- Passwords are sealed with AES-256-GCM using MAIL_CRED_KEY from
.env.secret, so a database dump alone opens nothing. GCM also makes
tampering fail the auth tag instead of decrypting to garbage.
- Provisioning verifies credentials against the live IMAP server before
storing them, so typos surface at setup rather than as a broken inbox.
Message bodies render as plain text; sender HTML is never injected, which
would execute sender-controlled markup and leak read receipts via
tracking pixels.
Mailboxes are limited to admins and teachers. Students are excluded
deliberately — external mail for minors carries archiving, monitoring and
consent obligations that should be chosen, not inherited.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>