Skip to content

Email Template System - Deep Dive Documentation

Generated: 2026-02-25 Scope: Full-stack — Backend EmailTemplate module + shared email services + entity + frontend visual builder + legacy v1 Files Analyzed: 44 Lines of Code: ~10,600+ Workflow Mode: Exhaustive Deep-Dive Known Issues Found: 60 (6 Critical, 10 High, 23 Medium, 21 Low) — Expert Panel reviewed: 5 recalibrated, 9 added, 1 removed Security Analysis: Red Team Attack Scenarios — 5 attack chains, 2 CRITICAL (SQL injection → DB compromise, Stored XSS → privilege escalation) Architecture Decisions: ADRs — 3 ADRs: unify email infra (4h), connect DB templates to sending (6h), delete v1 (5min) Pre-mortem Analysis: Failure Scenarios — 7 incidents: 2 catastrophic (SQL injection breach, XSS takeover), 1 certain (permission lockout), 1 insidious (silent email failure) Root Cause Analysis: 5 Whys — 5 chains → 3 systemic causes: zero test coverage, super_admin-only dev, no integration tracking


Overview

The Email Template System is a full-stack feature consisting of a backend CRUD module for managing email templates in the database, a visual drag-and-drop email builder on the frontend (v2), a legacy monolithic builder (v1), and an email sending infrastructure with three providers (Nodemailer, Resend, Brevo) using a factory pattern.

Critical Architectural Finding: The database-stored templates managed by the visual builder are completely disconnected from the actual email sending pipeline. All emails are sent using hardcoded TypeScript template functions in src/template/. The EmailTemplate CRUD module is a design/management tool with zero integration into email dispatch.

Purpose: Enable admins to design, manage, and store email templates via a visual builder; provide email sending infrastructure for transactional emails (verification, OTP, password reset, invitations).

Key Responsibilities: - Template CRUD with soft-delete, bulk operations, restore, trash management - Visual email builder with 11 element types, drag-and-drop, undo/redo, HTML import/export - 5 template presets (Purchase, Verification, Order, Invitation, Password Reset) - Email dispatch via 3 providers (Nodemailer, Resend, Brevo) with factory pattern - Transactional email templates for auth flows (hardcoded, separate from DB templates)

Integration Points: Auth system (email verification, OTP, password reset), Invite system, RBAC permissions, Config/environment variables, Database (PostgreSQL + TypeORM)


Architecture Overview

                    ┌─────────────────────────────────────────────────┐
                    │              EMAIL TEMPLATE SYSTEM              │
                    │                                                 │
                    │  ┌──────────────┐    ┌────────────────────┐     │
                    │  │  DESIGN PATH │    │   SENDING PATH     │     │
                    │  │  (Connected) │    │   (Disconnected)   │     │
                    │  │              │    │                    │     │
                    │  │  Frontend    │    │  Event Handlers    │     │
                    │  │  Builder UI  │    │  ├ registerEmail   │     │
                    │  │      │       │    │  ├ sendLoginOtp    │     │
                    │  │      ▼       │    │  ├ sendForgotPass  │     │
                    │  │  API CRUD    │    │  ├ inviteUserCreate│     │
                    │  │  10 endpoints│    │  └ inviteOnlyLogin │     │
                    │  │      │       │    │        │           │     │
                    │  │      ▼       │    │        ▼           │     │
                    │  │ email_       │    │  src/template/     │     │
                    │  │ templates    │    │  (5 hardcoded      │     │
                    │  │ (DB table)   │    │   TS functions)    │     │
                    │  │              │    │        │           │     │
                    │  │  ╳ DEAD END  │    │        ▼           │     │
                    │  │  Never read  │    │  EmailFactory OR   │     │
                    │  │  for sending │    │  helper/email.ts   │     │
                    │  └──────────────┘    │        │           │     │
                    │                      │        ▼           │     │
                    │                      │  SMTP / Resend /   │     │
                    │                      │  Brevo API         │     │
                    │                      └────────────────────┘     │
                    └─────────────────────────────────────────────────┘

Dual Email Sending Infrastructure

The system has two parallel, incompatible email sending architectures:

Architecture Config Key Env Var Providers Used By
Path A: helper/email.ts (Legacy) config.mail.type MAIL_TYPE Nodemailer, Resend sendLoginOtp, sendForgotPass, inviteOnlyLogin
Path B: lib/email/EmailFactory.ts (Newer) config.mail.service EMAIL_SERVICE Nodemailer, Resend, Brevo registerEmail, inviteUserCreate

Both must be configured independently via different environment variables.


Complete File Inventory

Backend — EmailTemplate Module (4 files, ~460 LOC)

# File LOC Purpose
1 saas-boilerplate/src/app/modules/v1/EmailTemplate/email_template.routes.ts ~35 10 route definitions with auth + contextMiddleware
2 saas-boilerplate/src/app/modules/v1/EmailTemplate/email_template.controller.ts ~110 10 controller actions mapping routes to service
3 saas-boilerplate/src/app/modules/v1/EmailTemplate/email_template.service.ts ~260 Business logic: CRUD, search, pagination, soft-delete, bulk ops
4 saas-boilerplate/src/app/modules/v1/EmailTemplate/email_template.validation.ts ~55 Zod schemas: createTemplate, updateTemplate, deleteByIds, bulkRestore

Backend — Email Services (9 files, ~285 LOC)

# File LOC Purpose
5 saas-boilerplate/src/lib/email/IEmailService.ts 6 Interface: single sendEmail(payload) method
6 saas-boilerplate/src/lib/email/email.type.ts 5 Type: IEmailSendPayload { to, subject, html }
7 saas-boilerplate/src/lib/email/EmailFactory.ts 32 Singleton factory: selects provider via EMAIL_SERVICE env
8 saas-boilerplate/src/lib/email/NodeMailerService.ts 55 SMTP email via nodemailer
9 saas-boilerplate/src/lib/email/ResendService.ts 25 Resend API email
10 saas-boilerplate/src/lib/email/BrevoService.ts 47 Brevo API email
11 saas-boilerplate/src/lib/email/email.ts 33 Orphaned standalone sender (DEAD CODE)
12 saas-boilerplate/src/lib/email/transporterMail.ts 19 Orphaned nodemailer transport (DEAD CODE)
13 saas-boilerplate/src/lib/email/template/signupEmailVerification.ts 13 Orphaned template (DEAD CODE, duplicate of registerEmailTemplate)

Backend — Legacy Email Helpers (3 files, ~97 LOC)

# File LOC Purpose
14 saas-boilerplate/src/helper/email.ts 29 ACTIVE legacy dispatcher: routes to nodemailer or resend via MAIL_TYPE
15 saas-boilerplate/src/helper/transporterMail.ts 14 Nodemailer transport used by helper/email.ts
16 saas-boilerplate/src/helper/email/resend.ts 54 Resend with attachment support, used by helper/email.ts

Backend — Hardcoded Templates (5 files, ~65 LOC)

# File LOC Purpose
17 saas-boilerplate/src/template/registerEmailTemplate.ts ~13 Signup verification email (used by auth.service)
18 saas-boilerplate/src/template/otpEmailTemplate.ts ~13 Login OTP email
19 saas-boilerplate/src/template/resetPassTemplate.ts ~13 Password reset email
20 saas-boilerplate/src/template/inviteUserEmailTemplate.ts ~13 User invitation email
21 saas-boilerplate/src/template/inviteOnlyTemplate.ts ~13 Invite-only login credentials email

Backend — Entity (1 file, ~35 LOC)

# File LOC Purpose
22 saas-boilerplate/src/entity/EmailTemplate.ts 35 TypeORM entity: 7 columns + timestamps + soft-delete

Frontend — API Layer (1 file, 238 LOC)

# File LOC Purpose
23 Sass-boilerplate-frontend-v1/src/api/email-templates/index.js 238 10 React Query hooks (CRUD + bulk + restore)

Frontend — v2 Components (20 files, ~6,372 LOC)

# File LOC Purpose
24 email-templates-page.jsx 821 List page: DataTable, search, tabs (Active/Draft/Trash), CRUD actions
25 email-template-edit-page.jsx 43 Edit wrapper: fetches template by ID, renders form
26 email-template-form.jsx 549 Core builder: element list, DnD, undo/redo, preview, save/export
27 email-preview.jsx 482 Static HTML preview with iframe rendering
28 interactive-email-preview.jsx 1025 Click-to-select preview with element highlighting
29 element-editor.jsx 95 Editor panel: delegates to content + style sub-editors
30 element-editor-content.jsx 733 Content editing per element type (text, URLs, images, items)
31 element-editor-style.jsx 772 Style editing (colors, sizes, padding, alignment, fonts)
32 element-toolbox.jsx 75 Sidebar palette of 11 draggable element types
33 toolbar.jsx 129 Top bar: undo/redo, device preview, HTML import/export
34 save-template-modal.jsx 248 Save dialog: title, type (11 purpose options), subject fields
35 html-export-modal.jsx 79 Copy/download generated HTML
36 html-import-modal.jsx 225 Paste/upload HTML for import to builder
37 sortable-element.jsx 161 DnD element wrapper (@dnd-kit) with action buttons
38 template-selector-modal.jsx 121 Choose from 5 preset templates
39 global-styles-panel.jsx 248 Container/body/font/link global styles
40 form-fields.jsx 125 Reusable form field components (text, select, color, range, checkbox)
41 mobile-bottom-nav.jsx 48 Mobile: bottom navigation tabs
42 mobile-properties-drawer.jsx 154 Mobile: swipe-up properties panel
43 mobile-toolbox-drawer.jsx 140 Mobile: swipe-up toolbox drawer

Frontend — Utilities (6 files, ~2,178 LOC)

# File LOC Purpose
44 utils/constants.js 295 Element types, defaults, social networks, purpose options, history actions
45 utils/html-generator.js 175 Elements + globalStyles → full HTML5 email document
46 utils/html-parser.js 845 HTML string → elements array + globalStyles object
47 utils/index.js 21 Barrel re-exports (has circular import issue)
48 utils/template-presets.js 804 5 preset templates with pre-configured elements
49 utils/utils.js 38 generateId() + historyReducer for undo/redo

Frontend — Legacy v1 (2 files, ~1,783 LOC)

# File LOC Purpose
50 email-templates-v1/email-temlate-form.jsx (typo) ~1,588 Monolithic builder: 8 element types, no undo, no import
51 email-templates-v1/email-template-page.jsx 195 List page with mock data, links to nonexistent edit route

Frontend — Page Routes (4 files)

# Route File Permission
52 /dashboard/email-templates email-templates/page.js email-template.view
53 /dashboard/email-templates/create email-templates/create/page.js NONE (missing!)
54 /dashboard/email-templates/[id] email-templates/[id]/page.js email-template.create (wrong — should be .update)
55 /dashboard/email-templates-v1/create email-templates-v1/create/page.js NONE

Database Schema

email_templates Table

CREATE TABLE email_templates (
    id          SERIAL PRIMARY KEY,
    title       VARCHAR(255) DEFAULT NULL,
    type        VARCHAR(255) NOT NULL UNIQUE,
    subject     VARCHAR(255) DEFAULT NULL,
    elements    JSONB NOT NULL,
    html        TEXT NOT NULL,
    "createdAt" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    "updatedAt" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    "deletedAt" TIMESTAMP DEFAULT NULL
);
Column DB Type Nullable Default Unique Notes
id SERIAL NOT NULL auto-inc PK @PrimaryGeneratedColumn()
title VARCHAR(255) YES null No Template display name
type VARCHAR(255) NOT NULL none YES e.g. forgot_password, verify_email, booking_reminder
subject VARCHAR(255) YES null No Email subject line
elements JSONB NOT NULL none No Builder element array (see Data Model)
html TEXT NOT NULL none No Generated HTML output
createdAt TIMESTAMP NOT NULL NOW() No Auto-managed
updatedAt TIMESTAMP NOT NULL NOW() No Auto-managed
deletedAt TIMESTAMP YES null No Soft-delete (@DeleteDateColumn)

Relationships: None. Completely standalone entity — no foreign keys to or from any other table.

Migrations: None. Project uses synchronize: true in development. No migration files exist for any entity.

Seed Data: None. No email template records or permissions are seeded.

Indexes: Only PK (id) and UNIQUE (type). No index on deletedAt despite frequent soft-delete queries.


API Endpoints

Mounted at /api/v1/email-templates with auth() + contextMiddleware on all routes.

# Method Path Permission Validation Description
1 GET /all NONE None All templates (no pagination, ignores trash)
2 GET / email-template.view None Paginated list with search, filters, ordering
3 GET /:id email-template.read None Single template by ID
4 POST / email-template.create createTemplate Create (auto-restores trashed duplicate by type)
5 PATCH /:id email-template.update updateTemplate Update template fields
6 PATCH /:id/restore email-template.restore None Restore from trash (checks type conflicts)
7 PATCH /bulk-delete email-template.delete deleteTemplateByIds Soft-delete multiple
8 PATCH /bulk-restore email-template.restore bulkEmailTemplateRestore Restore multiple (NO type conflict check!)
9 DELETE /:id email-template.delete None Soft-delete single
10 DELETE /:id/permanent email-template.delete None Permanent delete

Note: All permission checks are done inside service methods via checkPermissionAndThrow(), not via route-level hasPermission middleware. This is inconsistent with how other modules handle permissions.

Frontend API Hooks (10 hooks + 1 phantom)

Hook Backend Route Status
useEmailTemplates GET / OK
useEmailTemplateById GET /:id OK
useCreateEmailTemplate POST / OK
useUpdateEmailTemplate PATCH /:id OK
useDeleteEmailTemplate DELETE /:id OK
useBulkDeleteEmailTemplates PATCH /bulk-delete OK
usePermanentDeleteEmailTemplate DELETE /:id/permanent OK
useRestoreEmailTemplate PATCH /:id/restore OK
useBulkRestoreEmailTemplates PATCH /bulk-restore OK
useBulkPermanentDeleteEmailTemplates DELETE /bulk-permanent-delete 404 — Route does not exist

Email Template Data Model (Frontend Builder)

GlobalStyles Object

{
  bodyBackground: "#f4f4f4",       // Body/page background
  containerBackground: "#ffffff",   // Email container background
  containerWidth: "600",           // Container max-width (px)
  containerPadding: "20",          // Outer padding (px)
  contentPadding: "30",            // Inner content padding (px)
  borderRadius: "8",              // Container border-radius (px)
  fontFamily: "Arial, sans-serif", // Base font
  linkColor: "#007bff",           // Default link color
  baseFontSize: "14",             // Base font size (px)
  lineHeight: "1.6",              // Base line-height
}

Element Types (11 types)

Type Key Properties Notes
heading content, level(h1-h6), color, fontSize, fontWeight, textAlign, backgroundColor, padding, margin
text content, color, fontSize, fontWeight, lineHeight, textAlign, backgroundColor, padding
image src (URL), alt, width(%), height, alignment, backgroundColor, padding, borderRadius No upload — manual URL only
link content, href, color, fontSize, textAlign, padding, textDecoration
button content, href, backgroundColor, color, fontSize, fontWeight, textAlign, padding, paddingHorizontal, borderRadius, width
spacer height, backgroundColor
divider color, height, width(%), margin
list items[], listType(ordered/unordered), color, fontSize, lineHeight, padding
social socialLinks[{name,url}], iconStyle(circle/rounded/square), iconSize, alignment, color, padding, backgroundColor 7 networks with inline SVG
columns columns(2-3), gap, padding, backgroundColor, textColor, col½/3Content
table rows, columns, cells[][], orientation, borders, header config, alternate row colors Supports text + image cells

All elements also have: id (string), type (string), isHidden (boolean, optional).

Template Presets (5)

Preset Elements Theme Color Key Features
Purchase Successful 7 Blue (#2563eb) 2-col table with totals, CTA button
Email Verification 10 Green (#10b981) Verification link, expiry notice
Order Processing 10 Amber (#f59e0b) 4-col table with images, 3-col summary
Event Invitation 16 Purple (#8b5cf6) Vertical table, social links
Password Reset 18 Red (#ef4444) Security tips list, warning banner

Email Sending Infrastructure

Provider Comparison

Feature NodeMailerService ResendService BrevoService helper/email.ts
Path Factory (B) Factory (B) Factory (B) Legacy (A)
Error handling Rejects (propagates) No try/catch (bubbles) Swallows (console.error only) Propagates in prod, swallows in dev
From address Hardcoded "Sass Template" <[email protected]> config.mail.user config.mail.user (no name) Same hardcoded "Sass Template"
Attachments No No No Yes (via helper/email/resend.ts)
Multiple recipients No No No Yes (via helper/email/resend.ts)
Verify before send Yes (every call!) No No No
TLS validation Disabled (rejectUnauthorized: false) N/A (API) N/A (API) N/A

Nodemailer Transport Instances (Triple!)

# Location secure TLS Used By
1 lib/email/NodeMailerService.ts Yes (port === '465') rejectUnauthorized: false Factory path (newer)
2 lib/email/transporterMail.ts Yes (port === '465') rejectUnauthorized: false DEAD CODE (orphaned)
3 helper/transporterMail.ts No No Legacy path (active)

IEmailSendPayload Declarations (Triple!)

# Location Status
1 lib/email/email.type.ts Canonical
2 lib/email/email.ts Local duplicate
3 helper/email.ts Local duplicate

Environment Variables

Env Var Purpose In .env.example Used By
MAIL_HOST SMTP host Yes NodeMailerService, both transporters
MAIL_PORT SMTP port Yes NodeMailerService, both transporters
MAIL_USER SMTP user / from Yes All services
MAIL_PASS SMTP password Yes NodeMailerService, both transporters
MAIL_TYPE Legacy dispatcher selector Yes helper/email.ts ("nodemailer" or "resend")
RESEND_API_KEY Resend API key Yes ResendService, helper/email/resend.ts
EMAIL_SERVICE Factory selector NO EmailFactory.ts ("nodemailer", "resend", "brevo")
BREVO_API_KEY Brevo API key NO BrevoService
MAIL_FROM From address NO UNUSED (configured but never read)

Frontend Component Architecture

Component Hierarchy

email-templates/page.js
  └── EmailTemplatesPage
       ├── DataTable (list view with tabs: Active/Draft/Trash)
       ├── ConfirmationModal (delete/restore dialogs)
       └── EmailPreview (preview modal with dangerouslySetInnerHTML)

email-templates/create/page.js
  └── EmailTemplateForm (builder)
       ├── Toolbar (undo/redo, device preview, import/export)
       ├── ElementToolbox (11 draggable element types)
       ├── SortableElement[] (@dnd-kit sortable list)
       │    └── Action buttons (move up/down, duplicate, hide, delete)
       ├── ElementEditor
       │    ├── ElementEditorContent (type-specific content fields)
       │    └── ElementEditorStyle (type-specific style fields)
       ├── GlobalStylesPanel
       ├── InteractiveEmailPreview (click-to-select preview)
       ├── SaveTemplateModal (title, type, subject)
       ├── HtmlExportModal (copy/download HTML)
       ├── HtmlImportModal (paste/upload HTML)
       ├── TemplateSelectorModal (5 presets)
       ├── MobileBottomNav (responsive)
       ├── MobileToolboxDrawer (responsive)
       └── MobilePropertiesDrawer (responsive)

email-templates/[id]/page.js
  └── PermissionWrapper (email-template.create — WRONG, should be .update)
       └── EmailTemplateEditPage
            └── EmailTemplateForm (same builder, prefilled with existing data)

State Management

The EmailTemplateForm component manages ALL state locally via useState and useReducer:

State Type Purpose
elements Element[] Current element list (managed via historyReducer for undo/redo)
selectedElementId string | null Currently selected element for editing
globalStyles GlobalStyles Container/body styling
activeTab string Active panel: "elements", "editor", "global", "preview"
devicePreview string Preview mode: "desktop", "tablet", "mobile"
showSaveModal boolean Save dialog visibility
showExportModal boolean Export dialog visibility
showImportModal boolean Import dialog visibility
showPresetModal boolean Preset selector visibility

No Redux, no Context — fully self-contained component state.

User Flow: Creating an Email Template

  1. Navigate to /dashboard/email-templates (requires email-template.view permission in sidebar)
  2. Click "Add New" button (requires email-template.create permission check)
  3. Redirected to /dashboard/email-templates/create (NO server-side permission check)
  4. Choose a preset template from selector OR start blank
  5. Add elements from toolbox (drag-and-drop via @dnd-kit)
  6. Click elements to select → edit content and styles in side panel
  7. Preview in desktop/tablet/mobile modes
  8. Optionally import HTML from external source
  9. Click "Save" → fill title, type (purpose), subject in modal
  10. Template saved to email_templates table via POST /api/v1/email-templates
  11. HTML is generated client-side via generateFullHtml() and sent with the save request
  12. Template sits in database — never used for actual email sending

User Flow: Editing an Email Template

  1. From list page, click edit icon on a template
  2. Navigated to /dashboard/email-templates/[id]
  3. useEmailTemplateById fetches template data
  4. elements array loaded into builder state
  5. Edit as needed, save via PATCH /api/v1/email-templates/:id

HTML Generation Pipeline

elements[] + globalStyles → generateFullHtml() → Full HTML5 email document
- Filters hidden elements (isHidden) - Maps each element to inline-styled HTML via type-specific generators - Wraps in table-based email-safe layout (outer table → container table → content) - Full DOCTYPE, head (charset, viewport), body with background

HTML Import Pipeline

HTML string → DOMParser → parseHtmlToElements() → { elements[], globalStyles }
- Parses via browser DOMParser API - Extracts global styles from body/container/content DOM structure - Recursively walks DOM nodes, routing by tag name to type-specific parsers - Distinguishes layout tables (presentation) from data tables (borders/th) - Fallback aggressive search if no elements found

Generator/Parser Asymmetry

Element Type Generator Parser Round-trip
heading Yes Yes OK
text Yes Yes OK
image Yes Yes OK
link Yes Yes OK
button Yes Yes OK
spacer Yes Yes OK
divider Yes Yes OK
list Yes Yes OK
social Yes No LOSSY — parsed as generic links
columns Yes No LOSSY — parsed as layout or flat elements
table Yes Yes OK
blockquote No Yes LOSSY — imported but renders empty
code No Yes LOSSY — imported but renders empty

Dependency Graph

Backend Dependencies

email_template.routes.ts
  ├── email_template.controller.ts
  │    └── email_template.service.ts
  │         ├── EmailTemplate (entity)
  │         ├── checkPermissionAndThrow (helper)
  │         ├── getDbRepository (shared)
  │         ├── parsePaginationQuery (helper)
  │         └── requestContext (shared)
  └── email_template.validation.ts
       └── validateRequest (middleware)

EmailFactory.ts (NOT connected to EmailTemplate module)
  ├── NodeMailerService.ts
  ├── ResendService.ts
  └── BrevoService.ts

helper/email.ts (NOT connected to EmailTemplate module)
  ├── helper/transporterMail.ts
  └── helper/email/resend.ts

Frontend Dependencies

email-templates/page.js
  └── email-templates-page.jsx
       ├── api/email-templates/index.js (10 hooks)
       ├── components/ui/* (DataTable, Dialog, etc.)
       └── hooks/useCheckPermission.js

email-templates/create/page.js & [id]/page.js
  └── email-template-form.jsx
       ├── utils/constants.js (ELEMENT_TYPES, DEFAULT_*)
       ├── utils/utils.js (generateId, historyReducer)
       ├── utils/html-generator.js (generateFullHtml)
       ├── utils/html-parser.js (parseHtmlToElements)
       ├── utils/template-presets.js (TEMPLATE_PRESETS)
       ├── sortable-element.jsx (@dnd-kit/sortable)
       ├── element-editor.jsx
       │    ├── element-editor-content.jsx
       │    └── element-editor-style.jsx
       ├── element-toolbox.jsx
       ├── toolbar.jsx
       ├── global-styles-panel.jsx
       ├── interactive-email-preview.jsx
       ├── save-template-modal.jsx
       ├── html-export-modal.jsx
       ├── html-import-modal.jsx
       ├── template-selector-modal.jsx
       ├── form-fields.jsx
       ├── mobile-bottom-nav.jsx
       ├── mobile-toolbox-drawer.jsx
       └── mobile-properties-drawer.jsx

Entry Points (Not imported by others in scope)

  • email-templates/page.js — list page
  • email-templates/create/page.js — create page
  • email-templates/[id]/page.js — edit page
  • email-templates-v1/create/page.js — legacy v1 create
  • email_template.routes.ts — backend route entry

Leaf Nodes (Don't import others in scope)

  • email.type.ts — type definitions only
  • IEmailService.ts — interface only
  • utils/constants.js — constants only
  • utils/utils.js — utilities only
  • form-fields.jsx — reusable form fields

Circular Dependencies

  • utils/index.js line 19: export * from "../utils" resolves to itself (circular barrel re-export). Should be export * from "./utils".

Testing Analysis

Test Coverage: 0% — No test files exist anywhere in the project.

Testing Gaps: - No unit tests for service CRUD operations - No unit tests for HTML generator (element → HTML) - No unit tests for HTML parser (HTML → elements) — this is the most complex file at 845 LOC - No integration tests for API endpoints - No component tests for the builder UI - No E2E tests for template creation workflow - No validation testing for edge cases (empty elements, malformed HTML, XSS payloads)

Suggested Tests: 1. html-generator.test.js — Test each element type generates correct HTML 2. html-parser.test.js — Test round-trip fidelity for each element type 3. email_template.service.test.ts — Test CRUD, soft-delete, restore, bulk operations 4. email_template.routes.test.ts — Test auth, permissions, validation 5. EmailFactory.test.ts — Test provider selection, singleton behavior 6. email-template-form.test.jsx — Test builder interactions, undo/redo, DnD


Security Analysis — Red Team Attack Chains

Full analysis: docs/extra-docs/email-template-attack-scenarios.md

Attacker profile: Authenticated user with zero permissions (freshly registered).

Chain Attack Feasibility Impact Overall
1 SQL Injection via orderBy → blind DB extraction of users, credentials, secrets HIGH (trivial) CRITICAL CRITICAL
2 Stored XSS (via Chain 1 DB write + dangerouslySetInnerHTML) → admin session hijack → self-role-change to super_admin HIGH (chained) CRITICAL CRITICAL
3 MITM email interception (rejectUnauthorized: false) → capture verification/reset/OTP tokens → account takeover MEDIUM HIGH HIGH
4 Permission bypass (getAllTemplates no check) → info disclosure of all template HTML/elements HIGH (trivial) LOW MEDIUM
5 Bulk restore type collision → UNIQUE constraint 500 error LOW LOW LOW

Critical path: Chain 1 → Chain 2 creates a realistic "new signup → super_admin in <5 minutes" scenario by combining orderBy SQL injection, dangerouslySetInnerHTML stored XSS, and the known PATCH /users/profile self-role-change vulnerability.

P0 fixes (block both CRITICAL chains in ~45 min): 1. Whitelist orderBy values (15 min) 2. Add DOMPurify to template preview (30 min)


Known Issues

Expert Panel Review (2026-02-25): Winston (Architect), Amelia (Dev), Quinn (QA) peer-reviewed all issues. 5 severity recalibrations, 9 new issues added, 1 removed (L9 was a valid re-export, not circular). Original 52 → 60.

Critical (6)

# Issue Location Description
C1 SQL injection via orderBy email_template.service.ts:59 User-supplied query.orderBy interpolated directly into template.${query.orderBy} QueryBuilder without validation or whitelist. Exploitation: blind conditional injection in ORDER BY position (e.g., CASE WHEN (SELECT...) THEN id ELSE title END). Confirmed exploitable by panel.
C2 getAllTemplates has NO permission check email_template.service.ts:18-27 GET /email-templates/all skips checkPermissionAndThrow(). Any authenticated user can read all templates including HTML and JSONB elements, bypassing RBAC.
C3 email-template permissions NOT seeded src/seed/data/permission.ts Zero email-template.* permissions exist in seed data. Non-super_admin users are permanently locked out of all email template operations since permissions cannot be granted (they don't exist in the Permission table).
C4 EmailTemplate entity NEVER used for sending All event handlers DB-stored templates are managed via CRUD but never queried for email dispatch. All 5 event handlers use hardcoded TypeScript template functions from src/template/. The type field (e.g., forgot_password, verify_email) implies lookup-by-type intent, but this was never implemented.
C5 Frontend calls non-existent endpoint api/email-templates/index.js:209 useBulkPermanentDeleteEmailTemplates calls DELETE /email-templates/bulk-permanent-delete which does not exist in backend routes. Always returns 404.
C6 Dual email infrastructure with different config helper/email.ts vs lib/email/EmailFactory.ts Two parallel sending architectures use different env vars (MAIL_TYPE vs EMAIL_SERVICE). Setting one does NOT affect the other. Both must be configured since different event handlers use different paths.

High (10)

# Issue Location Description
H1 BrevoService silently swallows errors BrevoService.ts:38-40 catch block only calls console.error without re-throwing. Caller gets resolved Promise even when sending fails. Inconsistent with other providers.
H3 elements field has no Zod validation email_template.validation.ts JSONB elements field absent from all Zod schemas. Arbitrary JSON stored without schema validation.
H4 bulkEmailTemplateRestore skips type conflict check email_template.service.ts Single restore checks for active templates with same type before restoring. Bulk restore blindly calls repo.restore(ids) — can violate UNIQUE constraint on type.
H5 Edit page checks wrong permission email-templates/[id]/page.js:9 PermissionWrapper checks email-template.create instead of email-template.update. User with create-only permission can access edit page.
H6 Create page has no PermissionWrapper email-templates/create/page.js Any authenticated user can navigate directly to /dashboard/email-templates/create and use the full builder UI. Backend rejects on save, but entire UI is exposed.
H7 EMAIL_SERVICE missing from .env.example .env.example Factory path requires EMAIL_SERVICE env var but it's undocumented. Without it, getEmailService() throws "Unknown email service type: undefined".
H9 handleItemsPerPageChange undefined email-templates-page.jsx:668 Function referenced in items-per-page dropdown onChange handler but never defined. Causes runtime error when changing page size.
H10 dangerouslySetInnerHTML without sanitization (list preview) email-templates-page.jsx:805 Template HTML rendered in preview modal without DOMPurify or any sanitization. If malicious HTML is stored (via SQL injection C1 or direct API), it executes in admin browser (stored XSS).
H16 Race condition on duplicate type (unhandled 500) email_template.service.ts Create flow does check-then-insert without transaction. Concurrent requests can both pass check and violate UNIQUE constraint — returns raw PostgreSQL error to user. (Promoted from M16 by panel — user-facing 500 in realistic multi-user scenario.)
H17 No pagination on getAllTemplates (DoS vector) email_template.service.ts:18-27 repo.find() with no limit. Combined with C2 (no permission check), any authenticated user can trigger unbounded memory allocation. 10K templates = massive JSON response, potential OOM. (NEW — found by panel.)

Medium (23)

# Issue Location Description
M1 tls: { rejectUnauthorized: false } NodeMailerService.ts:33, lib/email/transporterMail.ts Disables TLS certificate validation, allowing MITM attacks in production.
M2 Hardcoded "Sass Template" from address NodeMailerService.ts:40, helper/email.ts:15 "Sass" instead of "SaaS" in email sender name. Hardcoded instead of configurable.
M3 NodeMailerService new Promise(async ...) NodeMailerService.ts:37 Explicit promise construction antipattern. Should be async method directly.
M4 transporter.verify() on every send NodeMailerService.ts:39 Establishes SMTP connection to verify, then sends. Doubles latency per email.
M5 ResendService from reads wrong config ResendService.ts:21 Uses config.mail.user (SMTP username) instead of config.resend.mail_user.
M6 'use server' directive in Express backend lib/email/email.ts:1 Next.js Server Actions directive is meaningless in Express.js. Copy-paste artifact.
M7 signupEmailVerification.ts is duplicate dead code lib/email/template/signupEmailVerification.ts Near-identical to src/template/registerEmailTemplate.ts but with fewer params (no name). Never imported.
M8 BrevoService setApiKey() on every send BrevoService.ts:22 API key set inside sendEmail() instead of constructor. Redundant per-call operation.
M9 MAIL_FROM configured but never used config/index.ts config.mail.from exists in config but no email service reads it. All hardcode their from addresses.
M10 title and subject typed string but default null EmailTemplate.ts Entity declares title: string with default: null. TypeScript won't warn about null values. Should be string | null.
M11 Frontend filter tabs have no backend support email-templates-page.jsx "Active" and "Draft" tabs exist in UI but entity has no status field — only deletedAt for trash.
M12 No image upload integration element-editor-content.jsx Builder requires manual URL entry for images. No integration with existing Cloudinary/S3 upload infrastructure.
M13 api.md incorrectly documents GET /all permission docs/architecture/api.md:1340 Documents email-template.view permission for GET /all, but code has no permission check.
M14 Frontend references phantom permission email-templates-page.jsx:450 email-template.trash permission checked in UI but never defined or checked in backend.
M15 HTML parser/generator asymmetry html-generator.js, html-parser.js Social and columns elements can be generated but not parsed (lossy import). Blockquote and code can be parsed but not generated (render empty on re-export).
M17 BREVO_API_KEY missing from .env.example .env.example Brevo provider requires this key but it's not listed. Only affects Brevo users. (Demoted from H8 by panel — conditional on provider choice.)
M18 Triple nodemailer transporter instances NodeMailerService.ts, lib/email/transporterMail.ts, helper/transporterMail.ts Three transport instances with same credentials but different TLS/security configs. Confusing but functionally harmless. (Demoted from H11 by panel — no user impact.)
M19 No size limit on html field EmailTemplate.ts, email_template.validation.ts html column is TEXT (no length constraint). Zod schema doesn't validate length. A user could save a 50MB HTML document. (NEW — found by panel.)
M20 Duplicate type returns raw 500 error email_template.service.ts (create) Creating a template with an existing active type doesn't check for active duplicates before INSERT. UNIQUE constraint violation produces raw PostgreSQL error, not 409 Conflict. (NEW — found by panel.)
M21 No autosave in builder email-template-form.jsx All state in component memory (useState/useReducer). Browser crash = all work lost. Undo/redo history is also in-memory only. (NEW — found by panel.)
M22 interactiveEmailPreview renders HTML in same origin interactive-email-preview.jsx Complete template HTML rendered inside a div in the builder page. Script tags and event handlers execute in builder context. Same XSS risk as H10 but different location. (NEW — found by panel.)
M23 No audit trail for template changes email_template.service.ts No versioning, changelog, or updatedBy column. If template is maliciously modified, no forensic trail. Only updatedAt timestamp exists. (NEW — found by panel.)
M24 No email delivery monitoring All email services No logging of send success/failure per recipient and type. BrevoService swallows errors silently. No health check endpoint. Silent failures invisible until user complaints. (NEW — found by panel, related to pre-mortem incident 2.)

Low (21)

# Issue Location Description
L1 console.log on every getEmailService() call EmailFactory.ts:5 Log fires every call, not just first instantiation. Spams logs in busy app.
L2 No singleton reset for testing EmailFactory.ts Module-scoped instance has no reset. Makes unit testing difficult.
L3 Unused imports in BrevoService BrevoService.ts CreateContact and ContactsApi imported but never used.
L4 Inconsistent variable naming NodeMailerService.ts this.transPorter with capital P.
L5 Redundant html after spread ResendService.ts:21 ...payload, html: payload.html — spread already includes html.
L6 lib/email/email.ts is dead code lib/email/email.ts Standalone sender not imported anywhere. Orphaned alongside transporterMail.ts.
L7 signupEmailVerification.ts is dead code lib/email/template/signupEmailVerification.ts Never imported. Hardcoded ${"there"} greeting (was meant to be parameterized).
L8 Duplicate generateId implementations utils/utils.js vs utils/html-parser.js Different ID formats: el_{ts}_{random5} vs {random9}. Inconsistent but functional.
L10 containerBorderRadius in presets never rendered template-presets.js Several preset elements include this property but html-generator.js ignores it.
L11 v1 links to nonexistent edit route email-template-page.jsx:163 (v1) Links to /dashboard/email-templates/edit/${id} which doesn't exist (v2 uses /${id}).
L12 Legacy v1 still accessible email-templates-v1/create/page.js Older monolithic builder accessible at /dashboard/email-templates-v1/create.
L13 Commented-out slug column EmailTemplate.ts:12-13 Dead code: // @Column({ unique: true }) slug: string;
L14 No index on deletedAt EmailTemplate.ts Frequent soft-delete queries filter by deletedAt but no index exists for performance.
L15 findByIds deprecated email_template.service.ts:250 TypeORM 0.3+ deprecates repo.findByIds(). Use repo.findBy({ id: In(ids) }).
L16 api.md describes elements as "BlockNote blocks" docs/architecture/api.md:1363 Elements are custom DnD builder format, not BlockNote.
L17 fs.readFileSync blocks event loop helper/email/resend.ts Synchronous file read for attachment encoding blocks the Node.js event loop.
L18 No XSS sanitization on template URLs All hardcoded templates verifyUrl, resetPassLink, otp interpolated into HTML without sanitization.
L19 min(0) allows ID=0 email_template.validation.ts Zod validation for template IDs uses .min(0) but PostgreSQL serial starts at 1.
L20 Dead test code in BrevoService BrevoService.ts:25-33 Unused const email object with hardcoded test values. Never sent. Zero runtime impact. (Demoted from H2 by panel.)
L21 No keyboard accessibility for DnD sortable-element.jsx, email-template-form.jsx @dnd-kit supports keyboard sensors but only PointerSensor is configured. Keyboard/screen reader users cannot reorder elements. (NEW — found by panel.)
L22 HTML export may leak internal URLs html-export-modal.jsx Exported HTML includes any internal/staging URLs in link/button/image elements. Could leak when shared externally. (NEW — found by panel.)
L23 Edit page has no loading/error states email-template-edit-page.jsx Calls useEmailTemplateById(id) but doesn't handle loading or error. Missing template (deleted/wrong ID) shows undefined data with no feedback. (NEW — found by panel.)

Severity Summary

Severity Count Panel Changes
Critical 6 Unchanged (C1 attack vector clarified)
High 10 -H2(→L20), -H8(→M17), -H11(→M18), +M16(→H16), +NEW H17
Medium 23 +H8(M17), +H11(M18), -M16(→H16), +NEW M19-M24
Low 21 +H2(L20), -L9(removed — valid re-export), +NEW L21-L23
Total 60 +9 new, -1 removed, 5 recalibrated

Similar Patterns Elsewhere

Pattern Email Template Module Also Used In Notes
Soft-delete + restore + bulk ops EmailTemplate service Blog, GenericPage, Product Same @DeleteDateColumn + manual restore pattern
checkPermissionAndThrow in service All methods except getAllTemplates User, Role, Blog, etc. Standard RBAC pattern
Paginated list with search + order templateList Most CRUD services Common query builder pattern
React Query hooks (CRUD) api/email-templates/ All 16 API hook domains Same pattern: query + mutations + invalidation
DataTable with tabs email-templates-page.jsx Users, Blogs, Products pages Same AdvanceTable + tab filter pattern

Reusable Utilities Available (Not Used by Email Templates)

Utility Path Could Be Used For
uploadImage / handleFileUpload src/helper/uploadImage.ts Adding image upload to builder (M12)
CloudinaryStorage / AwsStorage src/shared/storage/ Image hosting for email template images
sanitizeImageName src/shared/sanitizeImageName.ts Image URL sanitization
DOMPurify Could add to frontend Sanitizing preview HTML (H10)

Modification Guidance

Detailed ADRs with implementation plans, code samples, and phasing: docs/extra-docs/email-template-adrs.md

ADR-1: Unify Email Infrastructure (~4 hrs)

Consolidate all sending onto lib/email/EmailFactory.ts. Extend IEmailSendPayload with to: string | string[], optional attachments, optional from. Migrate 3 legacy handlers (sendLoginOtp, sendForgotPass, inviteOnlyLogin) to getEmailService().sendEmail(). Delete 6 legacy/dead files. Single EMAIL_SERVICE env var replaces both EMAIL_SERVICE and MAIL_TYPE.

ADR-2: Connect DB Templates to Sending (~6 hrs)

New lib/email/templateResolver.ts: query email_templates by type with 5-min TTL cache, interpolate {{variables}} via Mustache-like replacement, fall back to hardcoded TypeScript template if no DB record found. Update 5 event handlers (3-line change each). Seed 5 default templates matching current hardcoded HTML. Seed email-template.* permissions.

ADR-3: Delete Legacy v1 (~5 min)

Delete email-templates-v1/ directory (2 components + 1 route page). v2 is a complete superset. Git history preserves code.

To Add a New Element Type

  1. Add type constant to utils/constants.jsELEMENT_TYPES
  2. Add default properties to DEFAULT_ELEMENTS
  3. Add content editor section in element-editor-content.jsx
  4. Add style editor section in element-editor-style.jsx
  5. Add HTML generation case in html-generator.js
  6. Add HTML parser case in html-parser.js
  7. Add icon and label in element-toolbox.jsx

Implementation Phasing

Phase 1 — P0 Security (~45 min):  Whitelist orderBy + DOMPurify preview
Phase 2 — ADR-3 (~5 min):         Delete v1 builder
Phase 3 — ADR-1 (~4 hrs):         Unify email infrastructure
Phase 4 — ADR-2 (~6 hrs):         Connect DB templates to sending
Phase 5 — Follow-up (backlog):    Insert Variable UI, elements validation,
                                   sandboxed iframe, image upload integration

Testing Checklist for Changes

  • Verify all 10 API endpoints return correct responses
  • Test permission checks: each endpoint denies unauthorized users
  • Test getAllTemplates — confirm if permission bypass is intentional
  • Test create with duplicate type — verify proper error handling
  • Test bulk restore — verify type conflict handling
  • Test HTML generation for each element type
  • Test HTML parser round-trip for each element type
  • Test undo/redo with multiple operations
  • Test drag-and-drop reordering
  • Test HTML import with various email HTML formats
  • Test preset template loading
  • Test save/update with all field combinations
  • Test trash/restore/permanent-delete flows
  • Test mobile responsive builder UI
  • Test orderBy parameter with SQL injection payloads
  • Test bulk-permanent-delete resolves to valid endpoint

Contributor Checklist

Risks & Gotchas: - The orderBy SQL injection (C1) is the highest-priority security fix - getAllTemplates permission bypass (C2) exposes all template data - The dual email infrastructure means changes to email sending must be made in TWO places - Frontend and backend have different permission expectations (e.g., email-template.trash only exists in frontend) - The elements JSONB has no schema — any shape is accepted and stored - HTML preview uses dangerouslySetInnerHTML — XSS risk if templates contain malicious content

Pre-change Verification Steps: 1. Check which email sending path your change affects (factory vs helper) 2. Verify permission strings match between frontend and backend 3. Test with non-super_admin user (permissions won't exist unless manually created) 4. Check both .env vars: MAIL_TYPE AND EMAIL_SERVICE

Suggested Tests Before PR: 1. Run the app, attempt template CRUD as non-super_admin — verify behavior 2. Test GET /email-templates/all without permission — should it be allowed? 3. Test orderBy=1;DROP TABLE-- on GET /email-templates 4. Test bulk operations with conflicting types 5. Create template, export HTML, import HTML — verify round-trip fidelity


Generated by document-project workflow (deep-dive mode) Base Documentation: docs/index.md Scan Date: 2026-02-25 Analysis Mode: Exhaustive