--- name: elon-email-templates description: Use when creating or modifying email templates for Elon AI - welcome emails, role changes, notifications, teacher digests, or QR code invitations. This project uses HTML string templates, NOT React Email components. --- # Elon AI Email Templates ## Overview HTML string-based email templates for the Elon AI Classroom Assistant platform. This project does **NOT use React Email components** - all templates are pure HTML strings with TypeScript helper functions. > **Important:** The global `react-email` skill documents React Email patterns. This project uses a different approach optimized for simplicity and performance. Follow **this skill** for Elon AI email work. ## When to Use - Creating new transactional email templates - Modifying existing templates in `email-send.ts` - Understanding the email queue architecture - Adding new email types to the system ## Architecture ### Email Flow ``` API/Action → publishJob({jobType: "email:send"}) → Upstash QStash (queue) → emailSendHandler() → Template selection (EMAIL_TEMPLATES[templateId]) → HTML string generation → Resend API → User inbox ``` ### Key Files | File | Purpose | |------|---------| | `lib/jobs/handlers/email-send.ts` | All 12 email templates + handler (916 lines) | | `lib/email/constants.ts` | Centralized colors, styles, logo URL | | `lib/email/unsubscribe-token.ts` | HMAC-signed unsubscribe tokens | | `app/api/email/unsubscribe/route.ts` | Unsubscribe API endpoint | | `lib/jobs/types.ts` | `EmailSendPayloadSchema` definition | | `.email-previews/` | HTML preview files for visual testing | ## Template Pattern All templates use pure HTML strings with helper functions: ```typescript // Template structure in EMAIL_TEMPLATES object welcome: (data) => ({ subject: "Welcome to Elon AI!", html: wrapInBaseTemplate({ headerTitle: `Welcome, ${escapeHtml(data.name)}!`, headerSubtitle: "Your AI-powered study companion is ready", content: `

Hey ${escapeHtml(data.name)}, welcome to Elon AI!

${createButton("Get Started", url)} `, proTip: "Your conversations are FERPA-protected.", }), }), ``` ### Helper Functions | Function | Purpose | Example | |----------|---------|---------| | `wrapInBaseTemplate()` | Header + content + footer wrapper | See above | | `createButton()` | Primary/secondary CTA buttons | `createButton("Click Me", url, "primary")` | | `createInfoBox()` | Key-value info cards | `createInfoBox([{label: "Name", value: "John"}])` | | `escapeHtml()` | XSS protection for user content | `escapeHtml(user.name)` | ### Style Constants All styles are defined in `STYLES` object at top of `email-send.ts`: ```typescript const STYLES = { container: "...", // Max-width 600px centered layout header: "...", // Maroon gradient header headerTitle: "...", // White text for header body: "...", // White background body bodyText: "...", // Gray text paragraphs button: "...", // Maroon primary button buttonSecondary: "...", // White bordered button infoBox: "...", // Gray background info card proTipBox: "...", // Gold accent tip box footer: "...", // Gray footer // ... more }; ``` ## Existing Templates (12) | Template ID | Purpose | Key Data Fields | |-------------|---------|-----------------| | `welcome` | New student onboarding | `name` | | `role_change` | User role changed | `userName`, `oldRole`, `newRole` | | `assistant_published` | Teacher's assistant goes live | `assistantName`, `courseName`, `joinCode` | | `budget_warning` | Admin usage alert | `percentUsed`, `currentSpend`, `budgetLimit` | | `processing_failed` | File upload error | `fileName`, `errorMessage` | | `new_student_joined` | Admin notification | `studentName`, `studentEmail`, `joinedAt` | | `role_request` | Admin action needed | `userName`, `userEmail`, `requestedRole` | | `role_approved` | Role request approved | `name`, `newRole`, `tips[]`, `dashboardUrl` | | `role_denied` | Role request denied | `name`, `requestedRole`, `reason` | | `teacher_digest` | Weekly analytics | `totalSessions`, `uniqueStudents`, `satisfactionRate`, `topTopic` | | `qr_join_code` | QR code invitation | `assistantName`, `courseName`, `joinCode`, `qrCodeUrl` | ## Adding a New Template ### Step 1: Add to EMAIL_TEMPLATES object ```typescript // In lib/jobs/handlers/email-send.ts const EMAIL_TEMPLATES = { // ... existing templates // Your new template my_new_template: (data) => ({ subject: `Your subject with ${escapeHtml(data.dynamicValue)}`, html: wrapInBaseTemplate({ headerTitle: "Header Title", headerSubtitle: "Optional subtitle", content: `

${escapeHtml(data.message)}

${createButton("Action", data.actionUrl)} `, proTip: "Optional helpful tip", }), }), }; ``` ### Step 2: Add payload schema (if needed) If your template needs specific data validation, update `lib/jobs/types.ts`: ```typescript export const EmailSendPayloadSchema = z.object({ tenantId: z.string().uuid(), templateId: z.string(), // Template ID to: z.string().email(), subject: z.string().optional(), // Override template subject data: z.record(z.unknown()), // Template-specific data }); ``` ### Step 3: Trigger the email ```typescript import { publishJob } from "@/lib/jobs/publisher"; await publishJob({ jobType: "email:send", payload: { tenantId: user.tenantId, templateId: "my_new_template", to: user.email, data: { message: "Hello world!", actionUrl: "https://elon-ai.app/action", }, }, }); ``` ### Step 4: Generate preview Add test data and regenerate previews: ```bash pnpm email:preview ``` ## Security Requirements ### XSS Protection **ALWAYS** use `escapeHtml()` on user-controlled content: ```typescript // CORRECT - escaped

${escapeHtml(data.userName)}

// WRONG - XSS vulnerability!

${data.userName}

``` ### FERPA Compliance - Include `tenantId` in all email payloads - All sends are logged to `audit_logs` table - Don't include student PII in email subjects - Unsubscribe tokens use HMAC-SHA256 signing ### Multi-Tenant Scoping - Emails are always scoped to a tenant - Job queue handles tenant isolation - Audit logs record tenant context ## Email Client Compatibility ### DO - Use **PNG/JPG images** with absolute URLs - Use inline CSS styles (no classes) - Use table-based layouts for columns - Keep emails under 102KB - Test in Gmail, Outlook, Apple Mail ### DON'T - Use SVG images (Gmail strips them) - Use CSS Grid or Flexbox - Use media queries (limited support) - Use external CSS files - Use JavaScript ### Logo The logo is a hosted PNG at `public/email-assets/elon-ai-logo.png`: ```typescript // Defined in lib/jobs/handlers/email-send.ts const EMAIL_LOGO_URL = "https://elon-ai.app/email-assets/elon-ai-logo.png"; const LOGO_IMG = `Elon AI`; ``` To regenerate the logo PNG: ```bash pnpm tsx scripts/generate-email-logo.ts ``` ## Testing ### Visual Previews Open `.email-previews/index.html` to browse all templates visually. ### Unit Tests ```bash pnpm test tests/unit/email/ ``` ### Integration Tests ```bash pnpm test tests/integration/api/unsubscribe.test.ts ``` ### Development Mode When `RESEND_API_KEY` is not set, emails are logged to console instead of sent. ## Brand Colors | Color | Hex | Usage | |-------|-----|-------| | Maroon | `#73000a` | Headers, buttons, primary text | | Gold | `#b59a57` | Accents, pro tips, highlights | | White | `#ffffff` | Body backgrounds, button text | | Gray-50 | `#f9fafb` | Footer, info box backgrounds | | Gray-700 | `#374151` | Body text | ## Common Patterns ### Teacher Analytics Email The `teacher_digest` template demonstrates advanced patterns: - Dynamic headlines based on metrics - Conditional content based on activity level - Unsubscribe link with HMAC token - Color-coded metric changes ### QR Code Email The `qr_join_code` template shows how to include images: - Hosted QR code image URL - Fallback text if image fails - Clear call-to-action