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-721 (parent) | Tenant-branded transactional emails | - | - |
| SP-730 | Phase A: tenant-branded email shell and preview. Backend color helper, themed templates with logo alignment, real plain-text fallback, preview endpoint, preview panel on the Branding page. | M | none |
| SP-731 | Phase B: tenant-editable email content blocks. nh3 sanitizer, placeholder substitution, email_branding JSONB + API, email asset uploads, entitlement, platform-admin view, Emails UI with reduced Quill and live preview. | L | SP-730 |
Each phase ships as one PR titled with its key. The detailed checklist for each phase lives in the ticket description.
Prepared for review. See scoping.md for the full text and findings.md for file references.