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>
94 lines
3.0 KiB
JSON
94 lines
3.0 KiB
JSON
{
|
|
"name": "tanstack_start_ts",
|
|
"private": true,
|
|
"sideEffects": false,
|
|
"type": "module",
|
|
"scripts": {
|
|
"dev": "vite dev",
|
|
"build": "vite build",
|
|
"build:dev": "vite build --mode development",
|
|
"preview": "vite preview",
|
|
"lint": "eslint .",
|
|
"format": "prettier --write ."
|
|
},
|
|
"dependencies": {
|
|
"@hookform/resolvers": "^5.2.2",
|
|
"@radix-ui/react-accordion": "^1.2.12",
|
|
"@radix-ui/react-alert-dialog": "^1.1.15",
|
|
"@radix-ui/react-aspect-ratio": "^1.1.8",
|
|
"@radix-ui/react-avatar": "^1.1.11",
|
|
"@radix-ui/react-checkbox": "^1.3.3",
|
|
"@radix-ui/react-collapsible": "^1.1.12",
|
|
"@radix-ui/react-context-menu": "^2.2.16",
|
|
"@radix-ui/react-dialog": "^1.1.15",
|
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
|
"@radix-ui/react-hover-card": "^1.1.15",
|
|
"@radix-ui/react-label": "^2.1.8",
|
|
"@radix-ui/react-menubar": "^1.1.16",
|
|
"@radix-ui/react-navigation-menu": "^1.2.14",
|
|
"@radix-ui/react-popover": "^1.1.15",
|
|
"@radix-ui/react-progress": "^1.1.8",
|
|
"@radix-ui/react-radio-group": "^1.3.8",
|
|
"@radix-ui/react-scroll-area": "^1.2.10",
|
|
"@radix-ui/react-select": "^2.2.6",
|
|
"@radix-ui/react-separator": "^1.1.8",
|
|
"@radix-ui/react-slider": "^1.3.6",
|
|
"@radix-ui/react-slot": "^1.2.4",
|
|
"@radix-ui/react-switch": "^1.2.6",
|
|
"@radix-ui/react-tabs": "^1.1.13",
|
|
"@radix-ui/react-toggle": "^1.1.10",
|
|
"@radix-ui/react-toggle-group": "^1.1.11",
|
|
"@radix-ui/react-tooltip": "^1.2.8",
|
|
"@supabase/supabase-js": "^2.108.2",
|
|
"@tailwindcss/vite": "^4.2.1",
|
|
"@tanstack/react-query": "^5.101.1",
|
|
"@tanstack/react-router": "^1.170.16",
|
|
"@tanstack/react-start": "^1.168.26",
|
|
"@tanstack/router-plugin": "^1.168.18",
|
|
"class-variance-authority": "^0.7.1",
|
|
"clsx": "^2.1.1",
|
|
"cmdk": "^1.1.1",
|
|
"date-fns": "^4.1.0",
|
|
"embla-carousel-react": "^8.6.0",
|
|
"imapflow": "^1.5.0",
|
|
"input-otp": "^1.4.2",
|
|
"lucide-react": "^0.575.0",
|
|
"mailparser": "^3.9.14",
|
|
"nodemailer": "^9.0.3",
|
|
"react": "^19.2.0",
|
|
"react-day-picker": "^9.14.0",
|
|
"react-dom": "^19.2.0",
|
|
"react-hook-form": "^7.71.2",
|
|
"react-resizable-panels": "^4.6.5",
|
|
"recharts": "^2.15.4",
|
|
"sonner": "^2.0.7",
|
|
"tailwind-merge": "^3.5.0",
|
|
"tailwindcss": "^4.2.1",
|
|
"tw-animate-css": "^1.3.4",
|
|
"vaul": "^1.1.2",
|
|
"vite-tsconfig-paths": "^6.0.2",
|
|
"zod": "^3.24.2"
|
|
},
|
|
"devDependencies": {
|
|
"@eslint/js": "^9.32.0",
|
|
"@lovable.dev/vite-tanstack-config": "2.6.4",
|
|
"@types/mailparser": "^3.4.6",
|
|
"@types/node": "^22.16.5",
|
|
"@types/nodemailer": "^8.0.1",
|
|
"@types/react": "^19.2.0",
|
|
"@types/react-dom": "^19.2.0",
|
|
"@vitejs/plugin-react": "^5.2.0",
|
|
"eslint": "^9.32.0",
|
|
"eslint-config-prettier": "^10.1.1",
|
|
"eslint-plugin-prettier": "^5.2.6",
|
|
"eslint-plugin-react-hooks": "^5.2.0",
|
|
"eslint-plugin-react-refresh": "^0.4.20",
|
|
"globals": "^15.15.0",
|
|
"nitro": "3.0.260603-beta",
|
|
"prettier": "^3.7.3",
|
|
"typescript": "^5.8.3",
|
|
"typescript-eslint": "^8.56.1",
|
|
"vite": "^8.0.16"
|
|
}
|
|
}
|