brand object with name, logo, address, contact, phone, site URL.branding.colors (primary, secondary, footer).#2563eb.brand.site_url and brand.phone resolved but unused.All go through one send_custom_email path and one of three templates. No password-reset email (Firebase owns auth). No customer invoice email yet.
| Type | Template | Trigger |
|---|---|---|
| Student added | simple.html | admin adds student |
| Tenant invitation | simple.html | invite created |
| Test email | simple.html | admin clicks test |
| Enrollment welcome | simple.html | enrollment completes |
| Admin enrollment / payment notice | simple.html | paid enrollment |
| Assessment deadline reminder | assessment_deadline_reminder.html | cron |
| Live class reminder | live_class_reminder.html | cron |
All options keep a locked, table-based layout shell for email client safety.
| Option | What tenant controls | Editor | Effort | Risk | Verdict |
|---|---|---|---|---|---|
| A. Branded layout only | Colors, logo, name, footer from existing branding | None (uses Branding page) | S | very low | Ship first |
| B. A + content blocks | A, plus header tagline, footer signature, intro/outro text | Reduced Quill editor | M | medium | Ship second |
| C. Full raw template per type | Whole HTML body per email type | Full HTML editor | L | high | Skip |
| D. Markdown into shell | Markdown body per type | Markdown editor | M | medium | Later, if asked |
A closes the main complaint (emails look off-brand) with almost no risk and reuses colors that already exist. B then adds real voice control by reusing the Quill editor with a reduced toolbar and a backend sanitizer, storing only small content blocks, never a full body. Skip C. Revisit D only on demand.
Reuse the existing react-quill-new editor for B.
It already uploads images to R2. What it lacks for email: table layout (kept in
the locked shell), an email-client preview and width guard (we add an iframe and
fixed 600px width), email-safe output (we strip classes and keep an inline-style
allowlist on save), and dark-mode handling (done in the shell, not tenant HTML).
email prefix.Expose a small fixed set: user first name and email, tenant name, contact,
site URL, course title and URL, plus action_url.
Sandbox: use simple named placeholder substitution, not a full Jinja2 environment. Replace known tokens from a fixed dict, escape every value, and leave unknown tokens blank. Keep full Jinja2 only for the shell we control.
nh3 (fast, maintained). Run on save and render.javascript: and data:.rel=noopener.srcdoc, scripts off.branding.colors and logo; add an optional email_layout sub-object.tenants.email_branding for header tagline, footer signature, intro/outro, alignment, enabled, updated_by.tenant_email_templates table if per-type templates ever land. Defer./admin/tenant-settings/email-branding; sanitize on write..../email-branding/preview returns safe HTML, no send.effective_* values, like the current email settings contract.EmailBrand with resolved email colors (primary, CTA, footer, foregrounds).css_inline only if we move to a shared style block.email.branding.enabled.| # | Decision | Recommendation |
|---|---|---|
| 1 | Ship A first, then B? | Yes. A is low risk, fixes the main complaint. |
| 2 | CTA color: derive from primary or explicit field? | Derive from primary; allow override later. |
| 3 | Phase B editor: Quill or Markdown? | Quill; tenants know it and it uploads to R2. |
| 4 | Backend sanitizer: nh3 or bleach? | nh3; fast and maintained. |
| 5 | External images: R2 only or any https? | R2 uploads only. |
| 6 | Email language: add English now? | Keep Vietnamese; multi-locale is a later ticket. |
| Ticket | Scope | Size | Depends on |
|---|---|---|---|
| SP-XXX (parent) | Tenant-branded transactional emails | - | - |
| SP-XXX-1 | Backend color helper (hex to shades, readable foreground) | M | none |
| SP-XXX-2 | Templates use resolved colors + logo alignment; real plain-text | M | 1 |
| SP-XXX-6 | Preview render endpoint (safe HTML, no send) | S | 2 |
| SP-XXX-3 | Frontend email preview panel on Branding page | S | 2, 6 |
| SP-XXX-4 | Backend sanitizer (nh3) + placeholder substitution | M | none |
| SP-XXX-5 | Data model + tenant-admin API for content blocks | L | 4 |
| SP-XXX-8 | Entitlement flag + platform-admin read/reset | S | 5 |
| SP-XXX-7 | Frontend Emails settings UI (reduced Quill + preview) | M | 5, 6 |
Order: 1, 2, 6, 3 (Phase A ships) then 4, 5, 8, 7 (Phase B ships).
Prepared for review. See scoping.md for the full text and findings.md for file references.