Add embedded IMAP/SMTP mail for staff
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>
This commit is contained in:
@@ -531,6 +531,42 @@ export type Database = {
|
||||
},
|
||||
]
|
||||
}
|
||||
mail_server_settings: {
|
||||
Row: {
|
||||
id: boolean
|
||||
imap_host: string
|
||||
imap_port: number
|
||||
imap_secure: boolean
|
||||
smtp_host: string
|
||||
smtp_port: number
|
||||
smtp_secure: boolean
|
||||
updated_at: string
|
||||
updated_by: string | null
|
||||
}
|
||||
Insert: {
|
||||
id?: boolean
|
||||
imap_host: string
|
||||
imap_port?: number
|
||||
imap_secure?: boolean
|
||||
smtp_host: string
|
||||
smtp_port?: number
|
||||
smtp_secure?: boolean
|
||||
updated_at?: string
|
||||
updated_by?: string | null
|
||||
}
|
||||
Update: {
|
||||
id?: boolean
|
||||
imap_host?: string
|
||||
imap_port?: number
|
||||
imap_secure?: boolean
|
||||
smtp_host?: string
|
||||
smtp_port?: number
|
||||
smtp_secure?: boolean
|
||||
updated_at?: string
|
||||
updated_by?: string | null
|
||||
}
|
||||
Relationships: []
|
||||
}
|
||||
message_threads: {
|
||||
Row: {
|
||||
created_at: string
|
||||
@@ -1298,6 +1334,42 @@ export type Database = {
|
||||
},
|
||||
]
|
||||
}
|
||||
user_mailboxes: {
|
||||
Row: {
|
||||
created_at: string
|
||||
created_by: string | null
|
||||
email: string
|
||||
last_verified_at: string | null
|
||||
secret_ciphertext: string
|
||||
secret_iv: string
|
||||
secret_tag: string
|
||||
updated_at: string
|
||||
user_id: string
|
||||
}
|
||||
Insert: {
|
||||
created_at?: string
|
||||
created_by?: string | null
|
||||
email: string
|
||||
last_verified_at?: string | null
|
||||
secret_ciphertext: string
|
||||
secret_iv: string
|
||||
secret_tag: string
|
||||
updated_at?: string
|
||||
user_id: string
|
||||
}
|
||||
Update: {
|
||||
created_at?: string
|
||||
created_by?: string | null
|
||||
email?: string
|
||||
last_verified_at?: string | null
|
||||
secret_ciphertext?: string
|
||||
secret_iv?: string
|
||||
secret_tag?: string
|
||||
updated_at?: string
|
||||
user_id?: string
|
||||
}
|
||||
Relationships: []
|
||||
}
|
||||
user_roles: {
|
||||
Row: {
|
||||
created_at: string
|
||||
|
||||
Reference in New Issue
Block a user