7714 lines
316 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html>
<html lang="en" class="no-js">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="description" content="Build Power. Not Rent It. Own your digital infrastructure.">
<meta name="author" content="Bunker Operations">
<link rel="canonical" href="https://bnkserve.org/v2/features/email-templates/template-system/">
<link rel="prev" href="../">
<link rel="next" href="../editor/">
<link rel="icon" href="../../../../assets/favicon.png">
<meta name="generator" content="mkdocs-1.6.1, mkdocs-material-9.7.1">
<title>Template System - Changemaker Lite</title>
<link rel="stylesheet" href="../../../../assets/stylesheets/main.484c7ddc.min.css">
<link rel="stylesheet" href="../../../../assets/stylesheets/palette.ab4e12ef.min.css">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Inter:300,300i,400,400i,700,700i%7CJetBrains+Mono:400,400i,700,700i&display=fallback">
<style>:root{--md-text-font:"Inter";--md-code-font:"JetBrains Mono"}</style>
<link rel="stylesheet" href="../../../../stylesheets/extra.css">
<link rel="stylesheet" href="../../../../stylesheets/home.css">
<link rel="stylesheet" href="../../../../assets/css/video-player.css">
<script>__md_scope=new URL("../../../..",location),__md_hash=e=>[...e].reduce(((e,_)=>(e<<5)-e+_.charCodeAt(0)),0),__md_get=(e,_=localStorage,t=__md_scope)=>JSON.parse(_.getItem(t.pathname+"."+e)),__md_set=(e,_,t=localStorage,a=__md_scope)=>{try{t.setItem(a.pathname+"."+e,JSON.stringify(_))}catch(e){}}</script>
<meta property="og:type" content="website" />
<meta property="og:title" content="Template System - Changemaker Lite" />
<meta property="og:description" content="Build Power. Not Rent It. Own your digital infrastructure." />
<meta property="og:image" content="https://bnkserve.org/assets/images/social/v2/features/email-templates/template-system.png" />
<meta property="og:image:type" content="image/png" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:url" content="https://bnkserve.org/v2/features/email-templates/template-system/" />
<meta property="twitter:card" content="summary_large_image" />
<meta property="twitter:title" content="Template System - Changemaker Lite" />
<meta property="twitter:description" content="Build Power. Not Rent It. Own your digital infrastructure." />
<meta property="twitter:image" content="https://bnkserve.org/assets/images/social/v2/features/email-templates/template-system.png" />
</head>
<body dir="ltr" data-md-color-scheme="slate" data-md-color-primary="deep-purple" data-md-color-accent="amber">
<input class="md-toggle" data-md-toggle="drawer" type="checkbox" id="__drawer" autocomplete="off">
<input class="md-toggle" data-md-toggle="search" type="checkbox" id="__search" autocomplete="off">
<label class="md-overlay" for="__drawer"></label>
<div data-md-component="skip">
<a href="#email-template-system" class="md-skip">
Skip to content
</a>
</div>
<div data-md-component="announce">
</div>
<header class="md-header md-header--shadow md-header--lifted" data-md-component="header">
<nav class="md-header__inner md-grid" aria-label="Header">
<a href="../../../.." title="Changemaker Lite" class="md-header__button md-logo" aria-label="Changemaker Lite" data-md-component="logo">
<img src="../../../../assets/logo.png" alt="logo">
</a>
<label class="md-header__button md-icon" for="__drawer">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M3 6h18v2H3zm0 5h18v2H3zm0 5h18v2H3z"/></svg>
</label>
<div class="md-header__title" data-md-component="header-title">
<div class="md-header__ellipsis">
<div class="md-header__topic">
<span class="md-ellipsis">
Changemaker Lite
</span>
</div>
<div class="md-header__topic" data-md-component="header-topic">
<span class="md-ellipsis">
Template System
</span>
</div>
</div>
</div>
<form class="md-header__option" data-md-component="palette">
<input class="md-option" data-md-color-media="" data-md-color-scheme="slate" data-md-color-primary="deep-purple" data-md-color-accent="amber" aria-label="Switch to light mode" type="radio" name="__palette" id="__palette_0">
<label class="md-header__button md-icon" title="Switch to light mode" for="__palette_1" hidden>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="m17.75 4.09-2.53 1.94.91 3.06-2.63-1.81-2.63 1.81.91-3.06-2.53-1.94L12.44 4l1.06-3 1.06 3zm3.5 6.91-1.64 1.25.59 1.98-1.7-1.17-1.7 1.17.59-1.98L15.75 11l2.06-.05L18.5 9l.69 1.95zm-2.28 4.95c.83-.08 1.72 1.1 1.19 1.85-.32.45-.66.87-1.08 1.27C15.17 23 8.84 23 4.94 19.07c-3.91-3.9-3.91-10.24 0-14.14.4-.4.82-.76 1.27-1.08.75-.53 1.93.36 1.85 1.19-.27 2.86.69 5.83 2.89 8.02a9.96 9.96 0 0 0 8.02 2.89m-1.64 2.02a12.08 12.08 0 0 1-7.8-3.47c-2.17-2.19-3.33-5-3.49-7.82-2.81 3.14-2.7 7.96.31 10.98 3.02 3.01 7.84 3.12 10.98.31"/></svg>
</label>
<input class="md-option" data-md-color-media="" data-md-color-scheme="default" data-md-color-primary="deep-purple" data-md-color-accent="amber" aria-label="Switch to dark mode" type="radio" name="__palette" id="__palette_1">
<label class="md-header__button md-icon" title="Switch to dark mode" for="__palette_0" hidden>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 7a5 5 0 0 1 5 5 5 5 0 0 1-5 5 5 5 0 0 1-5-5 5 5 0 0 1 5-5m0 2a3 3 0 0 0-3 3 3 3 0 0 0 3 3 3 3 0 0 0 3-3 3 3 0 0 0-3-3m0-7 2.39 3.42C13.65 5.15 12.84 5 12 5s-1.65.15-2.39.42zM3.34 7l4.16-.35A7.2 7.2 0 0 0 5.94 8.5c-.44.74-.69 1.5-.83 2.29zm.02 10 1.76-3.77a7.131 7.131 0 0 0 2.38 4.14zM20.65 7l-1.77 3.79a7.02 7.02 0 0 0-2.38-4.15zm-.01 10-4.14.36c.59-.51 1.12-1.14 1.54-1.86.42-.73.69-1.5.83-2.29zM12 22l-2.41-3.44c.74.27 1.55.44 2.41.44.82 0 1.63-.17 2.37-.44z"/></svg>
</label>
</form>
<script>var palette=__md_get("__palette");if(palette&&palette.color){if("(prefers-color-scheme)"===palette.color.media){var media=matchMedia("(prefers-color-scheme: light)"),input=document.querySelector(media.matches?"[data-md-color-media='(prefers-color-scheme: light)']":"[data-md-color-media='(prefers-color-scheme: dark)']");palette.color.media=input.getAttribute("data-md-color-media"),palette.color.scheme=input.getAttribute("data-md-color-scheme"),palette.color.primary=input.getAttribute("data-md-color-primary"),palette.color.accent=input.getAttribute("data-md-color-accent")}for(var[key,value]of Object.entries(palette.color))document.body.setAttribute("data-md-color-"+key,value)}</script>
<label class="md-header__button md-icon" for="__search">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M9.5 3A6.5 6.5 0 0 1 16 9.5c0 1.61-.59 3.09-1.56 4.23l.27.27h.79l5 5-1.5 1.5-5-5v-.79l-.27-.27A6.52 6.52 0 0 1 9.5 16 6.5 6.5 0 0 1 3 9.5 6.5 6.5 0 0 1 9.5 3m0 2C7 5 5 7 5 9.5S7 14 9.5 14 14 12 14 9.5 12 5 9.5 5"/></svg>
</label>
<div class="md-search" data-md-component="search" role="dialog">
<label class="md-search__overlay" for="__search"></label>
<div class="md-search__inner" role="search">
<form class="md-search__form" name="search">
<input type="text" class="md-search__input" name="query" aria-label="Search" placeholder="Search" autocapitalize="off" autocorrect="off" autocomplete="off" spellcheck="false" data-md-component="search-query" required>
<label class="md-search__icon md-icon" for="__search">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M9.5 3A6.5 6.5 0 0 1 16 9.5c0 1.61-.59 3.09-1.56 4.23l.27.27h.79l5 5-1.5 1.5-5-5v-.79l-.27-.27A6.52 6.52 0 0 1 9.5 16 6.5 6.5 0 0 1 3 9.5 6.5 6.5 0 0 1 9.5 3m0 2C7 5 5 7 5 9.5S7 14 9.5 14 14 12 14 9.5 12 5 9.5 5"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M20 11v2H8l5.5 5.5-1.42 1.42L4.16 12l7.92-7.92L13.5 5.5 8 11z"/></svg>
</label>
<nav class="md-search__options" aria-label="Search">
<a href="javascript:void(0)" class="md-search__icon md-icon" title="Share" aria-label="Share" data-clipboard data-clipboard-text="" data-md-component="search-share" tabindex="-1">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M18 16.08c-.76 0-1.44.3-1.96.77L8.91 12.7c.05-.23.09-.46.09-.7s-.04-.47-.09-.7l7.05-4.11c.54.5 1.25.81 2.04.81a3 3 0 0 0 3-3 3 3 0 0 0-3-3 3 3 0 0 0-3 3c0 .24.04.47.09.7L8.04 9.81C7.5 9.31 6.79 9 6 9a3 3 0 0 0-3 3 3 3 0 0 0 3 3c.79 0 1.5-.31 2.04-.81l7.12 4.15c-.05.21-.08.43-.08.66 0 1.61 1.31 2.91 2.92 2.91s2.92-1.3 2.92-2.91A2.92 2.92 0 0 0 18 16.08"/></svg>
</a>
<button type="reset" class="md-search__icon md-icon" title="Clear" aria-label="Clear" tabindex="-1">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M19 6.41 17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>
</button>
</nav>
<div class="md-search__suggest" data-md-component="search-suggest"></div>
</form>
<div class="md-search__output">
<div class="md-search__scrollwrap" tabindex="0" data-md-scrollfix>
<div class="md-search-result" data-md-component="search-result">
<div class="md-search-result__meta">
Initializing search
</div>
<ol class="md-search-result__list" role="presentation"></ol>
</div>
</div>
</div>
</div>
</div>
<div class="md-header__source">
<a href="https://gitea.bnkops.com/admin/changemaker.lite" title="Go to repository" class="md-source" data-md-component="source">
<div class="md-source__icon md-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Free 7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2025 Fonticons, Inc.--><path d="M439.6 236.1 244 40.5c-5.4-5.5-12.8-8.5-20.4-8.5s-15 3-20.4 8.4L162.5 81l51.5 51.5c27.1-9.1 52.7 16.8 43.4 43.7l49.7 49.7c34.2-11.8 61.2 31 35.5 56.7-26.5 26.5-70.2-2.9-56-37.3L240.3 199v121.9c25.3 12.5 22.3 41.8 9.1 55-6.4 6.4-15.2 10.1-24.3 10.1s-17.8-3.6-24.3-10.1c-17.6-17.6-11.1-46.9 11.2-56v-123c-20.8-8.5-24.6-30.7-18.6-45L142.6 101 8.5 235.1C3 240.6 0 247.9 0 255.5s3 15 8.5 20.4l195.6 195.7c5.4 5.4 12.7 8.4 20.4 8.4s15-3 20.4-8.4l194.7-194.7c5.4-5.4 8.4-12.8 8.4-20.4s-3-15-8.4-20.4"/></svg>
</div>
<div class="md-source__repository">
changemaker.lite
</div>
</a>
</div>
</nav>
<nav class="md-tabs" aria-label="Tabs" data-md-component="tabs">
<div class="md-grid">
<ul class="md-tabs__list">
<li class="md-tabs__item">
<a href="../../../.." class="md-tabs__link">
Home
</a>
</li>
<li class="md-tabs__item md-tabs__item--active">
<a href="../../../" class="md-tabs__link">
V2 Documentation
</a>
</li>
<li class="md-tabs__item">
<a href="../../../../phil/" class="md-tabs__link">
Philosophy
</a>
</li>
<li class="md-tabs__item">
<a href="../../../../v1/" class="md-tabs__link">
V1 Documentation (Legacy)
</a>
</li>
<li class="md-tabs__item">
<a href="../../../../blog/" class="md-tabs__link">
Blog
</a>
</li>
</ul>
</div>
</nav>
</header>
<div class="md-container" data-md-component="container">
<main class="md-main" data-md-component="main">
<div class="md-main__inner md-grid">
<div class="md-sidebar md-sidebar--primary" data-md-component="sidebar" data-md-type="navigation" >
<div class="md-sidebar__scrollwrap">
<div class="md-sidebar__inner">
<nav class="md-nav md-nav--primary md-nav--lifted" aria-label="Navigation" data-md-level="0">
<label class="md-nav__title" for="__drawer">
<a href="../../../.." title="Changemaker Lite" class="md-nav__button md-logo" aria-label="Changemaker Lite" data-md-component="logo">
<img src="../../../../assets/logo.png" alt="logo">
</a>
Changemaker Lite
</label>
<div class="md-nav__source">
<a href="https://gitea.bnkops.com/admin/changemaker.lite" title="Go to repository" class="md-source" data-md-component="source">
<div class="md-source__icon md-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Free 7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2025 Fonticons, Inc.--><path d="M439.6 236.1 244 40.5c-5.4-5.5-12.8-8.5-20.4-8.5s-15 3-20.4 8.4L162.5 81l51.5 51.5c27.1-9.1 52.7 16.8 43.4 43.7l49.7 49.7c34.2-11.8 61.2 31 35.5 56.7-26.5 26.5-70.2-2.9-56-37.3L240.3 199v121.9c25.3 12.5 22.3 41.8 9.1 55-6.4 6.4-15.2 10.1-24.3 10.1s-17.8-3.6-24.3-10.1c-17.6-17.6-11.1-46.9 11.2-56v-123c-20.8-8.5-24.6-30.7-18.6-45L142.6 101 8.5 235.1C3 240.6 0 247.9 0 255.5s3 15 8.5 20.4l195.6 195.7c5.4 5.4 12.7 8.4 20.4 8.4s15-3 20.4-8.4l194.7-194.7c5.4-5.4 8.4-12.8 8.4-20.4s-3-15-8.4-20.4"/></svg>
</div>
<div class="md-source__repository">
changemaker.lite
</div>
</a>
</div>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href="../../../.." class="md-nav__link">
<span class="md-ellipsis">
Home
</span>
</a>
</li>
<li class="md-nav__item md-nav__item--active md-nav__item--section md-nav__item--nested">
<input class="md-nav__toggle md-toggle " type="checkbox" id="__nav_2" checked>
<div class="md-nav__link md-nav__container">
<a href="../../../" class="md-nav__link ">
<span class="md-ellipsis">
V2 Documentation
</span>
</a>
<label class="md-nav__link " for="__nav_2" id="__nav_2_label" tabindex="">
<span class="md-nav__icon md-icon"></span>
</label>
</div>
<nav class="md-nav" data-md-level="1" aria-labelledby="__nav_2_label" aria-expanded="true">
<label class="md-nav__title" for="__nav_2">
<span class="md-nav__icon md-icon"></span>
V2 Documentation
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item md-nav__item--section md-nav__item--nested">
<input class="md-nav__toggle md-toggle md-toggle--indeterminate" type="checkbox" id="__nav_2_2" >
<div class="md-nav__link md-nav__container">
<a href="../../../getting-started/" class="md-nav__link ">
<span class="md-ellipsis">
Getting Started
</span>
</a>
<label class="md-nav__link " for="__nav_2_2" id="__nav_2_2_label" tabindex="">
<span class="md-nav__icon md-icon"></span>
</label>
</div>
<nav class="md-nav" data-md-level="2" aria-labelledby="__nav_2_2_label" aria-expanded="false">
<label class="md-nav__title" for="__nav_2_2">
<span class="md-nav__icon md-icon"></span>
Getting Started
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href="../../../getting-started/quick-start/" class="md-nav__link">
<span class="md-ellipsis">
Quick Start
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item md-nav__item--section md-nav__item--nested">
<input class="md-nav__toggle md-toggle md-toggle--indeterminate" type="checkbox" id="__nav_2_3" >
<div class="md-nav__link md-nav__container">
<a href="../../../architecture/" class="md-nav__link ">
<span class="md-ellipsis">
Architecture
</span>
</a>
<label class="md-nav__link " for="__nav_2_3" id="__nav_2_3_label" tabindex="">
<span class="md-nav__icon md-icon"></span>
</label>
</div>
<nav class="md-nav" data-md-level="2" aria-labelledby="__nav_2_3_label" aria-expanded="false">
<label class="md-nav__title" for="__nav_2_3">
<span class="md-nav__icon md-icon"></span>
Architecture
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href="../../../architecture/dual-api/" class="md-nav__link">
<span class="md-ellipsis">
Dual API System
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../architecture/authentication/" class="md-nav__link">
<span class="md-ellipsis">
Authentication & Security
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item md-nav__item--section md-nav__item--nested">
<input class="md-nav__toggle md-toggle md-toggle--indeterminate" type="checkbox" id="__nav_2_4" >
<div class="md-nav__link md-nav__container">
<a href="../../../backend/" class="md-nav__link ">
<span class="md-ellipsis">
Backend
</span>
</a>
<label class="md-nav__link " for="__nav_2_4" id="__nav_2_4_label" tabindex="">
<span class="md-nav__icon md-icon"></span>
</label>
</div>
<nav class="md-nav" data-md-level="2" aria-labelledby="__nav_2_4_label" aria-expanded="false">
<label class="md-nav__title" for="__nav_2_4">
<span class="md-nav__icon md-icon"></span>
Backend
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item md-nav__item--pruned md-nav__item--nested">
<a href="../../../backend/modules/" class="md-nav__link">
<span class="md-ellipsis">
Modules
</span>
<span class="md-nav__icon md-icon"></span>
</a>
</li>
<li class="md-nav__item md-nav__item--pruned md-nav__item--nested">
<a href="../../../backend/services/" class="md-nav__link">
<span class="md-ellipsis">
Services
</span>
<span class="md-nav__icon md-icon"></span>
</a>
</li>
<li class="md-nav__item md-nav__item--pruned md-nav__item--nested">
<a href="../../../backend/middleware/" class="md-nav__link">
<span class="md-ellipsis">
Middleware
</span>
<span class="md-nav__icon md-icon"></span>
</a>
</li>
<li class="md-nav__item md-nav__item--pruned md-nav__item--nested">
<a href="../../../backend/utilities/" class="md-nav__link">
<span class="md-ellipsis">
Utilities
</span>
<span class="md-nav__icon md-icon"></span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item md-nav__item--section md-nav__item--nested">
<input class="md-nav__toggle md-toggle md-toggle--indeterminate" type="checkbox" id="__nav_2_5" >
<div class="md-nav__link md-nav__container">
<a href="../../../frontend/" class="md-nav__link ">
<span class="md-ellipsis">
Frontend
</span>
</a>
<label class="md-nav__link " for="__nav_2_5" id="__nav_2_5_label" tabindex="">
<span class="md-nav__icon md-icon"></span>
</label>
</div>
<nav class="md-nav" data-md-level="2" aria-labelledby="__nav_2_5_label" aria-expanded="false">
<label class="md-nav__title" for="__nav_2_5">
<span class="md-nav__icon md-icon"></span>
Frontend
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item md-nav__item--pruned md-nav__item--nested">
<a href="../../../frontend/components/" class="md-nav__link">
<span class="md-ellipsis">
Components
</span>
<span class="md-nav__icon md-icon"></span>
</a>
</li>
<li class="md-nav__item md-nav__item--pruned md-nav__item--nested">
<a href="../../../frontend/layouts/" class="md-nav__link">
<span class="md-ellipsis">
Layouts
</span>
<span class="md-nav__icon md-icon"></span>
</a>
</li>
<li class="md-nav__item md-nav__item--pruned md-nav__item--nested">
<a href="../../../frontend/pages/" class="md-nav__link">
<span class="md-ellipsis">
Pages
</span>
<span class="md-nav__icon md-icon"></span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item md-nav__item--section md-nav__item--nested">
<input class="md-nav__toggle md-toggle md-toggle--indeterminate" type="checkbox" id="__nav_2_6" >
<div class="md-nav__link md-nav__container">
<a href="../../../database/" class="md-nav__link ">
<span class="md-ellipsis">
Database
</span>
</a>
<label class="md-nav__link " for="__nav_2_6" id="__nav_2_6_label" tabindex="">
<span class="md-nav__icon md-icon"></span>
</label>
</div>
<nav class="md-nav" data-md-level="2" aria-labelledby="__nav_2_6_label" aria-expanded="false">
<label class="md-nav__title" for="__nav_2_6">
<span class="md-nav__icon md-icon"></span>
Database
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href="../../../database/schema/" class="md-nav__link">
<span class="md-ellipsis">
Schema Overview
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../database/migrations/" class="md-nav__link">
<span class="md-ellipsis">
Migrations
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../database/seeding/" class="md-nav__link">
<span class="md-ellipsis">
Seeding
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../database/indexes/" class="md-nav__link">
<span class="md-ellipsis">
Indexes
</span>
</a>
</li>
<li class="md-nav__item md-nav__item--pruned md-nav__item--nested">
<a href="../../../database/models/" class="md-nav__link">
<span class="md-ellipsis">
Models
</span>
<span class="md-nav__icon md-icon"></span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item md-nav__item--active md-nav__item--section md-nav__item--nested">
<input class="md-nav__toggle md-toggle " type="checkbox" id="__nav_2_7" checked>
<div class="md-nav__link md-nav__container">
<a href="../../" class="md-nav__link ">
<span class="md-ellipsis">
Features
</span>
</a>
<label class="md-nav__link " for="__nav_2_7" id="__nav_2_7_label" tabindex="">
<span class="md-nav__icon md-icon"></span>
</label>
</div>
<nav class="md-nav" data-md-level="2" aria-labelledby="__nav_2_7_label" aria-expanded="true">
<label class="md-nav__title" for="__nav_2_7">
<span class="md-nav__icon md-icon"></span>
Features
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item md-nav__item--pruned md-nav__item--nested">
<a href="../../influence/" class="md-nav__link">
<span class="md-ellipsis">
Influence
</span>
<span class="md-nav__icon md-icon"></span>
</a>
</li>
<li class="md-nav__item md-nav__item--pruned md-nav__item--nested">
<a href="../../map/" class="md-nav__link">
<span class="md-ellipsis">
Map
</span>
<span class="md-nav__icon md-icon"></span>
</a>
</li>
<li class="md-nav__item md-nav__item--pruned md-nav__item--nested">
<a href="../../landing-pages/" class="md-nav__link">
<span class="md-ellipsis">
Landing Pages
</span>
<span class="md-nav__icon md-icon"></span>
</a>
</li>
<li class="md-nav__item md-nav__item--active md-nav__item--nested">
<input class="md-nav__toggle md-toggle " type="checkbox" id="__nav_2_7_5" checked>
<div class="md-nav__link md-nav__container">
<a href="../" class="md-nav__link ">
<span class="md-ellipsis">
Email Templates
</span>
</a>
<label class="md-nav__link " for="__nav_2_7_5" id="__nav_2_7_5_label" tabindex="0">
<span class="md-nav__icon md-icon"></span>
</label>
</div>
<nav class="md-nav" data-md-level="3" aria-labelledby="__nav_2_7_5_label" aria-expanded="true">
<label class="md-nav__title" for="__nav_2_7_5">
<span class="md-nav__icon md-icon"></span>
Email Templates
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item md-nav__item--active">
<input class="md-nav__toggle md-toggle" type="checkbox" id="__toc">
<label class="md-nav__link md-nav__link--active" for="__toc">
<span class="md-ellipsis">
Template System
</span>
<span class="md-nav__icon md-icon"></span>
</label>
<a href="./" class="md-nav__link md-nav__link--active">
<span class="md-ellipsis">
Template System
</span>
</a>
<nav class="md-nav md-nav--secondary" aria-label="On this page">
<label class="md-nav__title" for="__toc">
<span class="md-nav__icon md-icon"></span>
On this page
</label>
<ul class="md-nav__list" data-md-component="toc" data-md-scrollfix>
<li class="md-nav__item">
<a href="#overview" class="md-nav__link">
<span class="md-ellipsis">
Overview
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#architecture" class="md-nav__link">
<span class="md-ellipsis">
Architecture
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#database-models" class="md-nav__link">
<span class="md-ellipsis">
Database Models
</span>
</a>
<nav class="md-nav" aria-label="Database Models">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#emailtemplate" class="md-nav__link">
<span class="md-ellipsis">
EmailTemplate
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#emailtemplatevariable" class="md-nav__link">
<span class="md-ellipsis">
EmailTemplateVariable
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#emailtemplateversion" class="md-nav__link">
<span class="md-ellipsis">
EmailTemplateVersion
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#emailtemplatetestlog" class="md-nav__link">
<span class="md-ellipsis">
EmailTemplateTestLog
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#template-categories" class="md-nav__link">
<span class="md-ellipsis">
Template Categories
</span>
</a>
<nav class="md-nav" aria-label="Template Categories">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#influence-category" class="md-nav__link">
<span class="md-ellipsis">
INFLUENCE Category
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#map-category" class="md-nav__link">
<span class="md-ellipsis">
MAP Category
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#system-category" class="md-nav__link">
<span class="md-ellipsis">
SYSTEM Category
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#variable-interpolation" class="md-nav__link">
<span class="md-ellipsis">
Variable Interpolation
</span>
</a>
<nav class="md-nav" aria-label="Variable Interpolation">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#basic-variables" class="md-nav__link">
<span class="md-ellipsis">
Basic Variables
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#conditional-blocks" class="md-nav__link">
<span class="md-ellipsis">
Conditional Blocks
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#loops-each-blocks" class="md-nav__link">
<span class="md-ellipsis">
Loops (Each Blocks)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#raw-html-unescaped" class="md-nav__link">
<span class="md-ellipsis">
Raw HTML (Unescaped)
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#admin-workflow" class="md-nav__link">
<span class="md-ellipsis">
Admin Workflow
</span>
</a>
<nav class="md-nav" aria-label="Admin Workflow">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#viewing-templates" class="md-nav__link">
<span class="md-ellipsis">
Viewing Templates
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#creating-template" class="md-nav__link">
<span class="md-ellipsis">
Creating Template
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#editing-template" class="md-nav__link">
<span class="md-ellipsis">
Editing Template
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#testing-template" class="md-nav__link">
<span class="md-ellipsis">
Testing Template
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#activatingdeactivating-template" class="md-nav__link">
<span class="md-ellipsis">
Activating/Deactivating Template
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#developer-workflow-adding-new-template" class="md-nav__link">
<span class="md-ellipsis">
Developer Workflow (Adding New Template)
</span>
</a>
<nav class="md-nav" aria-label="Developer Workflow (Adding New Template)">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#step-1-define-template-key" class="md-nav__link">
<span class="md-ellipsis">
Step 1: Define Template Key
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#step-2-create-template-via-seed-script" class="md-nav__link">
<span class="md-ellipsis">
Step 2: Create Template via Seed Script
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#step-3-define-variables" class="md-nav__link">
<span class="md-ellipsis">
Step 3: Define Variables
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#step-4-use-in-code" class="md-nav__link">
<span class="md-ellipsis">
Step 4: Use in Code
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#step-5-document-template" class="md-nav__link">
<span class="md-ellipsis">
Step 5: Document Template
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#code-examples" class="md-nav__link">
<span class="md-ellipsis">
Code Examples
</span>
</a>
<nav class="md-nav" aria-label="Code Examples">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#send-email-from-template" class="md-nav__link">
<span class="md-ellipsis">
Send Email from Template
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#template-service-implementation" class="md-nav__link">
<span class="md-ellipsis">
Template Service Implementation
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#handlebars-helper-registration" class="md-nav__link">
<span class="md-ellipsis">
Handlebars Helper Registration
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#error-handling" class="md-nav__link">
<span class="md-ellipsis">
Error Handling
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#troubleshooting" class="md-nav__link">
<span class="md-ellipsis">
Troubleshooting
</span>
</a>
<nav class="md-nav" aria-label="Troubleshooting">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#problem-template-not-found" class="md-nav__link">
<span class="md-ellipsis">
Problem: Template not found
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#problem-variable-not-replaced-shows-var-in-email" class="md-nav__link">
<span class="md-ellipsis">
Problem: Variable not replaced (shows {{VAR}} in email)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#problem-missing-required-variable-error" class="md-nav__link">
<span class="md-ellipsis">
Problem: Missing required variable error
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#problem-email-sent-to-wrong-recipient" class="md-nav__link">
<span class="md-ellipsis">
Problem: Email sent to wrong recipient
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#problem-html-rendering-broken-in-email-client" class="md-nav__link">
<span class="md-ellipsis">
Problem: HTML rendering broken in email client
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#performance-considerations" class="md-nav__link">
<span class="md-ellipsis">
Performance Considerations
</span>
</a>
<nav class="md-nav" aria-label="Performance Considerations">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#template-loading" class="md-nav__link">
<span class="md-ellipsis">
Template Loading
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#handlebars-compilation" class="md-nav__link">
<span class="md-ellipsis">
Handlebars Compilation
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#bulk-email-sending" class="md-nav__link">
<span class="md-ellipsis">
Bulk Email Sending
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#security-considerations" class="md-nav__link">
<span class="md-ellipsis">
Security Considerations
</span>
</a>
<nav class="md-nav" aria-label="Security Considerations">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#xss-cross-site-scripting-in-email-clients" class="md-nav__link">
<span class="md-ellipsis">
XSS (Cross-Site Scripting) in Email Clients
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#email-address-validation" class="md-nav__link">
<span class="md-ellipsis">
Email Address Validation
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#rate-limiting-template-test-sends" class="md-nav__link">
<span class="md-ellipsis">
Rate Limiting Template Test Sends
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#template-injection-attacks" class="md-nav__link">
<span class="md-ellipsis">
Template Injection Attacks
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#related-documentation" class="md-nav__link">
<span class="md-ellipsis">
Related Documentation
</span>
</a>
<nav class="md-nav" aria-label="Related Documentation">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#frontend-documentation" class="md-nav__link">
<span class="md-ellipsis">
Frontend Documentation
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#backend-documentation" class="md-nav__link">
<span class="md-ellipsis">
Backend Documentation
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#database-documentation" class="md-nav__link">
<span class="md-ellipsis">
Database Documentation
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#feature-documentation" class="md-nav__link">
<span class="md-ellipsis">
Feature Documentation
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#configuration" class="md-nav__link">
<span class="md-ellipsis">
Configuration
</span>
</a>
</li>
</ul>
</nav>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="../editor/" class="md-nav__link">
<span class="md-ellipsis">
Editor
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../variables/" class="md-nav__link">
<span class="md-ellipsis">
Variables
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../versioning/" class="md-nav__link">
<span class="md-ellipsis">
Versioning
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item md-nav__item--pruned md-nav__item--nested">
<a href="../../media/" class="md-nav__link">
<span class="md-ellipsis">
Media
</span>
<span class="md-nav__icon md-icon"></span>
</a>
</li>
<li class="md-nav__item md-nav__item--pruned md-nav__item--nested">
<a href="../../newsletter/" class="md-nav__link">
<span class="md-ellipsis">
Newsletter
</span>
<span class="md-nav__icon md-icon"></span>
</a>
</li>
<li class="md-nav__item md-nav__item--pruned md-nav__item--nested">
<a href="../../observability/" class="md-nav__link">
<span class="md-ellipsis">
Observability
</span>
<span class="md-nav__icon md-icon"></span>
</a>
</li>
<li class="md-nav__item md-nav__item--pruned md-nav__item--nested">
<a href="../../tunnel/" class="md-nav__link">
<span class="md-ellipsis">
Tunnel
</span>
<span class="md-nav__icon md-icon"></span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item md-nav__item--section md-nav__item--nested">
<input class="md-nav__toggle md-toggle md-toggle--indeterminate" type="checkbox" id="__nav_2_8" >
<div class="md-nav__link md-nav__container">
<a href="../../../deployment/" class="md-nav__link ">
<span class="md-ellipsis">
Deployment
</span>
</a>
<label class="md-nav__link " for="__nav_2_8" id="__nav_2_8_label" tabindex="">
<span class="md-nav__icon md-icon"></span>
</label>
</div>
<nav class="md-nav" data-md-level="2" aria-labelledby="__nav_2_8_label" aria-expanded="false">
<label class="md-nav__title" for="__nav_2_8">
<span class="md-nav__icon md-icon"></span>
Deployment
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href="../../../deployment/docker-compose/" class="md-nav__link">
<span class="md-ellipsis">
Docker Compose
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../deployment/environment-variables/" class="md-nav__link">
<span class="md-ellipsis">
Environment Variables
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../deployment/nginx/" class="md-nav__link">
<span class="md-ellipsis">
Nginx Configuration
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../deployment/ssl-tls/" class="md-nav__link">
<span class="md-ellipsis">
SSL/TLS
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../deployment/tunneling/" class="md-nav__link">
<span class="md-ellipsis">
Tunneling
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../deployment/monitoring-stack/" class="md-nav__link">
<span class="md-ellipsis">
Monitoring Stack
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../deployment/healthchecks/" class="md-nav__link">
<span class="md-ellipsis">
Health Checks
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../deployment/scaling/" class="md-nav__link">
<span class="md-ellipsis">
Scaling
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../deployment/backup-restore/" class="md-nav__link">
<span class="md-ellipsis">
Backup & Restore
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item md-nav__item--section md-nav__item--nested">
<input class="md-nav__toggle md-toggle md-toggle--indeterminate" type="checkbox" id="__nav_2_9" >
<div class="md-nav__link md-nav__container">
<a href="../../../development/" class="md-nav__link ">
<span class="md-ellipsis">
Development
</span>
</a>
<label class="md-nav__link " for="__nav_2_9" id="__nav_2_9_label" tabindex="">
<span class="md-nav__icon md-icon"></span>
</label>
</div>
<nav class="md-nav" data-md-level="2" aria-labelledby="__nav_2_9_label" aria-expanded="false">
<label class="md-nav__title" for="__nav_2_9">
<span class="md-nav__icon md-icon"></span>
Development
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href="../../../development/local-setup/" class="md-nav__link">
<span class="md-ellipsis">
Local Setup
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../development/docker-workflow/" class="md-nav__link">
<span class="md-ellipsis">
Docker Workflow
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../development/git-workflow/" class="md-nav__link">
<span class="md-ellipsis">
Git Workflow
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../development/npm-commands/" class="md-nav__link">
<span class="md-ellipsis">
NPM Commands
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../development/migrations/" class="md-nav__link">
<span class="md-ellipsis">
Migrations
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../development/typescript/" class="md-nav__link">
<span class="md-ellipsis">
TypeScript
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../development/testing/" class="md-nav__link">
<span class="md-ellipsis">
Testing
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../development/debugging/" class="md-nav__link">
<span class="md-ellipsis">
Debugging
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../development/code-style/" class="md-nav__link">
<span class="md-ellipsis">
Code Style
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item md-nav__item--section md-nav__item--nested">
<input class="md-nav__toggle md-toggle md-toggle--indeterminate" type="checkbox" id="__nav_2_10" >
<div class="md-nav__link md-nav__container">
<a href="../../../api-reference/" class="md-nav__link ">
<span class="md-ellipsis">
API Reference
</span>
</a>
</div>
<nav class="md-nav" data-md-level="2" aria-labelledby="__nav_2_10_label" aria-expanded="false">
<label class="md-nav__title" for="__nav_2_10">
<span class="md-nav__icon md-icon"></span>
API Reference
</label>
<ul class="md-nav__list" data-md-scrollfix>
</ul>
</nav>
</li>
<li class="md-nav__item md-nav__item--section md-nav__item--nested">
<input class="md-nav__toggle md-toggle md-toggle--indeterminate" type="checkbox" id="__nav_2_11" >
<div class="md-nav__link md-nav__container">
<a href="../../../user-guides/" class="md-nav__link ">
<span class="md-ellipsis">
User Guides
</span>
</a>
<label class="md-nav__link " for="__nav_2_11" id="__nav_2_11_label" tabindex="">
<span class="md-nav__icon md-icon"></span>
</label>
</div>
<nav class="md-nav" data-md-level="2" aria-labelledby="__nav_2_11_label" aria-expanded="false">
<label class="md-nav__title" for="__nav_2_11">
<span class="md-nav__icon md-icon"></span>
User Guides
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href="../../../user-guides/admin-guide/" class="md-nav__link">
<span class="md-ellipsis">
Admin Guide
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../user-guides/campaign-manager-guide/" class="md-nav__link">
<span class="md-ellipsis">
Campaign Manager Guide
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../user-guides/map-organizer-guide/" class="md-nav__link">
<span class="md-ellipsis">
Map Organizer Guide
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../user-guides/content-editor-guide/" class="md-nav__link">
<span class="md-ellipsis">
Content Editor Guide
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../user-guides/volunteer-guide/" class="md-nav__link">
<span class="md-ellipsis">
Volunteer Guide
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item md-nav__item--section md-nav__item--nested">
<input class="md-nav__toggle md-toggle md-toggle--indeterminate" type="checkbox" id="__nav_2_12" >
<div class="md-nav__link md-nav__container">
<a href="../../../troubleshooting/" class="md-nav__link ">
<span class="md-ellipsis">
Troubleshooting
</span>
</a>
<label class="md-nav__link " for="__nav_2_12" id="__nav_2_12_label" tabindex="">
<span class="md-nav__icon md-icon"></span>
</label>
</div>
<nav class="md-nav" data-md-level="2" aria-labelledby="__nav_2_12_label" aria-expanded="false">
<label class="md-nav__title" for="__nav_2_12">
<span class="md-nav__icon md-icon"></span>
Troubleshooting
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href="../../../troubleshooting/faq/" class="md-nav__link">
<span class="md-ellipsis">
FAQ
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../troubleshooting/common-errors/" class="md-nav__link">
<span class="md-ellipsis">
Common Errors
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../troubleshooting/auth-issues/" class="md-nav__link">
<span class="md-ellipsis">
Auth Issues
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../troubleshooting/database-issues/" class="md-nav__link">
<span class="md-ellipsis">
Database Issues
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../troubleshooting/docker-issues/" class="md-nav__link">
<span class="md-ellipsis">
Docker Issues
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../troubleshooting/email-issues/" class="md-nav__link">
<span class="md-ellipsis">
Email Issues
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../troubleshooting/geocoding-issues/" class="md-nav__link">
<span class="md-ellipsis">
Geocoding Issues
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../troubleshooting/monitoring-issues/" class="md-nav__link">
<span class="md-ellipsis">
Monitoring Issues
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../troubleshooting/performance-optimization/" class="md-nav__link">
<span class="md-ellipsis">
Performance Optimization
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item md-nav__item--section md-nav__item--nested">
<input class="md-nav__toggle md-toggle md-toggle--indeterminate" type="checkbox" id="__nav_2_13" >
<div class="md-nav__link md-nav__container">
<a href="../../../migration/" class="md-nav__link ">
<span class="md-ellipsis">
Migration
</span>
</a>
<label class="md-nav__link " for="__nav_2_13" id="__nav_2_13_label" tabindex="">
<span class="md-nav__icon md-icon"></span>
</label>
</div>
<nav class="md-nav" data-md-level="2" aria-labelledby="__nav_2_13_label" aria-expanded="false">
<label class="md-nav__title" for="__nav_2_13">
<span class="md-nav__icon md-icon"></span>
Migration
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href="../../../migration/feature-parity/" class="md-nav__link">
<span class="md-ellipsis">
Feature Parity
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../migration/breaking-changes/" class="md-nav__link">
<span class="md-ellipsis">
Breaking Changes
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../migration/api-changes/" class="md-nav__link">
<span class="md-ellipsis">
API Changes
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../migration/data-migration/" class="md-nav__link">
<span class="md-ellipsis">
Data Migration
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item md-nav__item--section md-nav__item--nested">
<input class="md-nav__toggle md-toggle md-toggle--indeterminate" type="checkbox" id="__nav_2_14" >
<div class="md-nav__link md-nav__container">
<a href="../../../contributing/" class="md-nav__link ">
<span class="md-ellipsis">
Contributing
</span>
</a>
<label class="md-nav__link " for="__nav_2_14" id="__nav_2_14_label" tabindex="">
<span class="md-nav__icon md-icon"></span>
</label>
</div>
<nav class="md-nav" data-md-level="2" aria-labelledby="__nav_2_14_label" aria-expanded="false">
<label class="md-nav__title" for="__nav_2_14">
<span class="md-nav__icon md-icon"></span>
Contributing
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href="../../../contributing/development-setup/" class="md-nav__link">
<span class="md-ellipsis">
Development Setup
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../contributing/code-of-conduct/" class="md-nav__link">
<span class="md-ellipsis">
Code of Conduct
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../contributing/pull-requests/" class="md-nav__link">
<span class="md-ellipsis">
Pull Requests
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../contributing/roadmap/" class="md-nav__link">
<span class="md-ellipsis">
Roadmap
</span>
</a>
</li>
</ul>
</nav>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item md-nav__item--pruned md-nav__item--nested">
<a href="../../../../phil/" class="md-nav__link">
<span class="md-ellipsis">
Philosophy
</span>
<span class="md-nav__icon md-icon"></span>
</a>
</li>
<li class="md-nav__item md-nav__item--pruned md-nav__item--nested">
<a href="../../../../v1/" class="md-nav__link">
<span class="md-ellipsis">
V1 Documentation (Legacy)
</span>
<span class="md-nav__icon md-icon"></span>
</a>
</li>
<li class="md-nav__item md-nav__item--pruned md-nav__item--nested">
<a href="../../../../blog/" class="md-nav__link">
<span class="md-ellipsis">
Blog
</span>
<span class="md-nav__icon md-icon"></span>
</a>
</li>
</ul>
</nav>
</div>
</div>
</div>
<div class="md-sidebar md-sidebar--secondary" data-md-component="sidebar" data-md-type="toc" >
<div class="md-sidebar__scrollwrap">
<div class="md-sidebar__inner">
<nav class="md-nav md-nav--secondary" aria-label="On this page">
<label class="md-nav__title" for="__toc">
<span class="md-nav__icon md-icon"></span>
On this page
</label>
<ul class="md-nav__list" data-md-component="toc" data-md-scrollfix>
<li class="md-nav__item">
<a href="#overview" class="md-nav__link">
<span class="md-ellipsis">
Overview
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#architecture" class="md-nav__link">
<span class="md-ellipsis">
Architecture
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#database-models" class="md-nav__link">
<span class="md-ellipsis">
Database Models
</span>
</a>
<nav class="md-nav" aria-label="Database Models">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#emailtemplate" class="md-nav__link">
<span class="md-ellipsis">
EmailTemplate
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#emailtemplatevariable" class="md-nav__link">
<span class="md-ellipsis">
EmailTemplateVariable
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#emailtemplateversion" class="md-nav__link">
<span class="md-ellipsis">
EmailTemplateVersion
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#emailtemplatetestlog" class="md-nav__link">
<span class="md-ellipsis">
EmailTemplateTestLog
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#template-categories" class="md-nav__link">
<span class="md-ellipsis">
Template Categories
</span>
</a>
<nav class="md-nav" aria-label="Template Categories">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#influence-category" class="md-nav__link">
<span class="md-ellipsis">
INFLUENCE Category
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#map-category" class="md-nav__link">
<span class="md-ellipsis">
MAP Category
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#system-category" class="md-nav__link">
<span class="md-ellipsis">
SYSTEM Category
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#variable-interpolation" class="md-nav__link">
<span class="md-ellipsis">
Variable Interpolation
</span>
</a>
<nav class="md-nav" aria-label="Variable Interpolation">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#basic-variables" class="md-nav__link">
<span class="md-ellipsis">
Basic Variables
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#conditional-blocks" class="md-nav__link">
<span class="md-ellipsis">
Conditional Blocks
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#loops-each-blocks" class="md-nav__link">
<span class="md-ellipsis">
Loops (Each Blocks)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#raw-html-unescaped" class="md-nav__link">
<span class="md-ellipsis">
Raw HTML (Unescaped)
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#admin-workflow" class="md-nav__link">
<span class="md-ellipsis">
Admin Workflow
</span>
</a>
<nav class="md-nav" aria-label="Admin Workflow">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#viewing-templates" class="md-nav__link">
<span class="md-ellipsis">
Viewing Templates
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#creating-template" class="md-nav__link">
<span class="md-ellipsis">
Creating Template
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#editing-template" class="md-nav__link">
<span class="md-ellipsis">
Editing Template
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#testing-template" class="md-nav__link">
<span class="md-ellipsis">
Testing Template
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#activatingdeactivating-template" class="md-nav__link">
<span class="md-ellipsis">
Activating/Deactivating Template
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#developer-workflow-adding-new-template" class="md-nav__link">
<span class="md-ellipsis">
Developer Workflow (Adding New Template)
</span>
</a>
<nav class="md-nav" aria-label="Developer Workflow (Adding New Template)">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#step-1-define-template-key" class="md-nav__link">
<span class="md-ellipsis">
Step 1: Define Template Key
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#step-2-create-template-via-seed-script" class="md-nav__link">
<span class="md-ellipsis">
Step 2: Create Template via Seed Script
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#step-3-define-variables" class="md-nav__link">
<span class="md-ellipsis">
Step 3: Define Variables
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#step-4-use-in-code" class="md-nav__link">
<span class="md-ellipsis">
Step 4: Use in Code
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#step-5-document-template" class="md-nav__link">
<span class="md-ellipsis">
Step 5: Document Template
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#code-examples" class="md-nav__link">
<span class="md-ellipsis">
Code Examples
</span>
</a>
<nav class="md-nav" aria-label="Code Examples">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#send-email-from-template" class="md-nav__link">
<span class="md-ellipsis">
Send Email from Template
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#template-service-implementation" class="md-nav__link">
<span class="md-ellipsis">
Template Service Implementation
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#handlebars-helper-registration" class="md-nav__link">
<span class="md-ellipsis">
Handlebars Helper Registration
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#error-handling" class="md-nav__link">
<span class="md-ellipsis">
Error Handling
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#troubleshooting" class="md-nav__link">
<span class="md-ellipsis">
Troubleshooting
</span>
</a>
<nav class="md-nav" aria-label="Troubleshooting">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#problem-template-not-found" class="md-nav__link">
<span class="md-ellipsis">
Problem: Template not found
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#problem-variable-not-replaced-shows-var-in-email" class="md-nav__link">
<span class="md-ellipsis">
Problem: Variable not replaced (shows {{VAR}} in email)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#problem-missing-required-variable-error" class="md-nav__link">
<span class="md-ellipsis">
Problem: Missing required variable error
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#problem-email-sent-to-wrong-recipient" class="md-nav__link">
<span class="md-ellipsis">
Problem: Email sent to wrong recipient
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#problem-html-rendering-broken-in-email-client" class="md-nav__link">
<span class="md-ellipsis">
Problem: HTML rendering broken in email client
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#performance-considerations" class="md-nav__link">
<span class="md-ellipsis">
Performance Considerations
</span>
</a>
<nav class="md-nav" aria-label="Performance Considerations">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#template-loading" class="md-nav__link">
<span class="md-ellipsis">
Template Loading
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#handlebars-compilation" class="md-nav__link">
<span class="md-ellipsis">
Handlebars Compilation
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#bulk-email-sending" class="md-nav__link">
<span class="md-ellipsis">
Bulk Email Sending
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#security-considerations" class="md-nav__link">
<span class="md-ellipsis">
Security Considerations
</span>
</a>
<nav class="md-nav" aria-label="Security Considerations">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#xss-cross-site-scripting-in-email-clients" class="md-nav__link">
<span class="md-ellipsis">
XSS (Cross-Site Scripting) in Email Clients
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#email-address-validation" class="md-nav__link">
<span class="md-ellipsis">
Email Address Validation
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#rate-limiting-template-test-sends" class="md-nav__link">
<span class="md-ellipsis">
Rate Limiting Template Test Sends
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#template-injection-attacks" class="md-nav__link">
<span class="md-ellipsis">
Template Injection Attacks
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#related-documentation" class="md-nav__link">
<span class="md-ellipsis">
Related Documentation
</span>
</a>
<nav class="md-nav" aria-label="Related Documentation">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#frontend-documentation" class="md-nav__link">
<span class="md-ellipsis">
Frontend Documentation
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#backend-documentation" class="md-nav__link">
<span class="md-ellipsis">
Backend Documentation
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#database-documentation" class="md-nav__link">
<span class="md-ellipsis">
Database Documentation
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#feature-documentation" class="md-nav__link">
<span class="md-ellipsis">
Feature Documentation
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#configuration" class="md-nav__link">
<span class="md-ellipsis">
Configuration
</span>
</a>
</li>
</ul>
</nav>
</li>
</ul>
</nav>
</div>
</div>
</div>
<div class="md-content" data-md-component="content">
<nav class="md-path" aria-label="Navigation" >
<ol class="md-path__list">
<li class="md-path__item">
<a href="../../../.." class="md-path__link">
<span class="md-ellipsis">
Home
</span>
</a>
</li>
<li class="md-path__item">
<a href="../../../" class="md-path__link">
<span class="md-ellipsis">
V2 Documentation
</span>
</a>
</li>
<li class="md-path__item">
<a href="../../" class="md-path__link">
<span class="md-ellipsis">
Features
</span>
</a>
</li>
<li class="md-path__item">
<a href="../" class="md-path__link">
<span class="md-ellipsis">
Email Templates
</span>
</a>
</li>
</ol>
</nav>
<article class="md-content__inner md-typeset">
<a href="https://gitea.bnkops.com/admin/changemaker.lite/src/branch/main/mkdocs/docs/v2/features/email-templates/template-system.md" title="Edit this page" class="md-content__button md-icon" rel="edit">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M10 20H6V4h7v5h5v3.1l2-2V8l-6-6H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h4zm10.2-7c.1 0 .3.1.4.2l1.3 1.3c.2.2.2.6 0 .8l-1 1-2.1-2.1 1-1c.1-.1.2-.2.4-.2m0 3.9L14.1 23H12v-2.1l6.1-6.1z"/></svg>
</a>
<a href="https://gitea.bnkops.com/admin/changemaker.lite/src/branch/main/mkdocs/docs/v2/features/email-templates/template-system.md" title="View source of this page" class="md-content__button md-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M17 18c.56 0 1 .44 1 1s-.44 1-1 1-1-.44-1-1 .44-1 1-1m0-3c-2.73 0-5.06 1.66-6 4 .94 2.34 3.27 4 6 4s5.06-1.66 6-4c-.94-2.34-3.27-4-6-4m0 6.5a2.5 2.5 0 0 1-2.5-2.5 2.5 2.5 0 0 1 2.5-2.5 2.5 2.5 0 0 1 2.5 2.5 2.5 2.5 0 0 1-2.5 2.5M9.27 20H6V4h7v5h5v4.07c.7.08 1.36.25 2 .49V8l-6-6H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h4.5a8.2 8.2 0 0 1-1.23-2"/></svg>
</a>
<h1 id="email-template-system">Email Template System<a class="headerlink" href="#email-template-system" title="Permanent link">&para;</a></h1>
<h2 id="overview">Overview<a class="headerlink" href="#overview" title="Permanent link">&para;</a></h2>
<p>The Email Template System provides centralized management of all transactional and campaign emails sent by Changemaker Lite. It enables administrators to create, edit, and maintain email templates with variable interpolation, version control, and testing capabilities.</p>
<p><strong>Key Features:</strong></p>
<ul>
<li><strong>Centralized Management</strong> — All email templates stored in database, editable via admin GUI</li>
<li><strong>Variable Interpolation</strong><code>{{VAR}}</code> syntax powered by Handlebars template engine</li>
<li><strong>Three Categories</strong> — INFLUENCE (campaign emails), MAP (shift/canvass emails), SYSTEM (platform emails)</li>
<li><strong>Dual Format Support</strong> — HTML + plain text versions for all templates</li>
<li><strong>System Templates</strong> — Protected templates with deletion prevention for critical platform emails</li>
<li><strong>Version Control</strong> — Automatic version history on every save with rollback capability</li>
<li><strong>Test Send</strong> — Preview rendered emails before deploying to production</li>
<li><strong>Variable Validation</strong> — Required vs optional variables with runtime validation</li>
</ul>
<p><strong>Use Cases:</strong></p>
<ul>
<li><strong>Advocacy Campaigns</strong> — Custom email templates for representative outreach</li>
<li><strong>Shift Notifications</strong> — Confirmation and reminder emails for volunteer shifts</li>
<li><strong>User Onboarding</strong> — Welcome emails, verification emails, password resets</li>
<li><strong>Response Moderation</strong> — Notification emails when responses are approved/rejected</li>
<li><strong>Canvass Summaries</strong> — End-of-session reports sent to volunteers</li>
</ul>
<hr />
<h2 id="architecture">Architecture<a class="headerlink" href="#architecture" title="Permanent link">&para;</a></h2>
<pre class="mermaid"><code>flowchart TB
subgraph "Email Service Layer"
Service[EmailService&lt;br/&gt;email.service.ts]
Service --&gt; Load[Load Template by Key]
Service --&gt; Validate[Validate Required Variables]
Service --&gt; Interpolate[Handlebars Interpolation]
end
subgraph "Database Models"
Template[(EmailTemplate)]
Variables[(EmailTemplateVariable)]
Versions[(EmailTemplateVersion)]
TestLogs[(EmailTemplateTestLog)]
Template --&gt;|1:N| Variables
Template --&gt;|1:N| Versions
Template --&gt;|1:N| TestLogs
end
subgraph "Output Channels"
HTML[HTML Email]
Text[Plain Text Email]
Preview[Preview Rendering]
end
Load --&gt; Template
Template --&gt; Variables
Validate --&gt; Variables
Interpolate --&gt; HTML
Interpolate --&gt; Text
Interpolate --&gt; Preview
HTML --&gt; SMTP[Nodemailer SMTP]
Text --&gt; SMTP
Preview --&gt; Admin[Admin GUI]
SMTP --&gt; Sent[Email Sent]
Sent --&gt; TestLogs
Service -.-&gt;|Test Mode| MailHog[MailHog&lt;br/&gt;Dev Capture]
style Service fill:#4a90e2,color:#fff
style Template fill:#50c878,color:#fff
style SMTP fill:#ff6b6b,color:#fff</code></pre>
<p><strong>Component Responsibilities:</strong></p>
<ul>
<li><strong>EmailService</strong> — Core email sending logic with template loading and interpolation</li>
<li><strong>EmailTemplate</strong> — Template metadata (key, name, category, content, active status)</li>
<li><strong>EmailTemplateVariable</strong> — Variable definitions (key, label, required/optional, sample values)</li>
<li><strong>EmailTemplateVersion</strong> — Version history snapshots with change notes</li>
<li><strong>EmailTemplateTestLog</strong> — Test send audit trail with success/failure logging</li>
<li><strong>Handlebars Engine</strong> — Template compilation and variable interpolation</li>
<li><strong>Nodemailer</strong> — SMTP transport for production email delivery</li>
<li><strong>MailHog</strong> — Development email capture (when EMAIL_TEST_MODE=true)</li>
</ul>
<hr />
<h2 id="database-models">Database Models<a class="headerlink" href="#database-models" title="Permanent link">&para;</a></h2>
<h3 id="emailtemplate">EmailTemplate<a class="headerlink" href="#emailtemplate" title="Permanent link">&para;</a></h3>
<p><strong>Core template storage with metadata and content.</strong></p>
<table>
<thead>
<tr>
<th>Field</th>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>id</code></td>
<td>String (CUID)</td>
<td>Primary key</td>
</tr>
<tr>
<td><code>key</code></td>
<td>String (unique)</td>
<td>Programmatic identifier (e.g., "shift-signup-confirmation")</td>
</tr>
<tr>
<td><code>name</code></td>
<td>String</td>
<td>Display name for admin GUI</td>
</tr>
<tr>
<td><code>description</code></td>
<td>String (optional)</td>
<td>Template purpose and usage notes</td>
</tr>
<tr>
<td><code>category</code></td>
<td>Enum</td>
<td>INFLUENCE, MAP, or SYSTEM</td>
</tr>
<tr>
<td><code>subjectLine</code></td>
<td>String</td>
<td>Email subject (supports {{VARIABLES}})</td>
</tr>
<tr>
<td><code>htmlContent</code></td>
<td>Text</td>
<td>HTML email body with Handlebars syntax</td>
</tr>
<tr>
<td><code>textContent</code></td>
<td>Text</td>
<td>Plain text fallback version</td>
</tr>
<tr>
<td><code>isSystem</code></td>
<td>Boolean</td>
<td>If true, cannot be deleted (critical platform emails)</td>
</tr>
<tr>
<td><code>isActive</code></td>
<td>Boolean</td>
<td>If false, template is disabled and won't send</td>
</tr>
<tr>
<td><code>createdAt</code></td>
<td>DateTime</td>
<td>Creation timestamp</td>
</tr>
<tr>
<td><code>updatedAt</code></td>
<td>DateTime</td>
<td>Last modification timestamp</td>
</tr>
<tr>
<td><code>createdByUserId</code></td>
<td>String (optional)</td>
<td>User who created template</td>
</tr>
<tr>
<td><code>updatedByUserId</code></td>
<td>String (optional)</td>
<td>User who last modified template</td>
</tr>
</tbody>
</table>
<p><strong>Relations:</strong>
- <code>variables</code> — EmailTemplateVariable[] (1:N)
- <code>versions</code> — EmailTemplateVersion[] (1:N)
- <code>testLogs</code> — EmailTemplateTestLog[] (1:N)</p>
<p><strong>Indexes:</strong>
- Unique index on <code>key</code> for fast lookups
- Index on <code>category</code> for filtered queries
- Index on <code>isActive</code> for production template queries</p>
<hr />
<h3 id="emailtemplatevariable">EmailTemplateVariable<a class="headerlink" href="#emailtemplatevariable" title="Permanent link">&para;</a></h3>
<p><strong>Variable definitions for template interpolation.</strong></p>
<table>
<thead>
<tr>
<th>Field</th>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>id</code></td>
<td>String (CUID)</td>
<td>Primary key</td>
</tr>
<tr>
<td><code>templateId</code></td>
<td>String</td>
<td>Foreign key to EmailTemplate</td>
</tr>
<tr>
<td><code>key</code></td>
<td>String</td>
<td>Variable name (e.g., "USER_NAME")</td>
</tr>
<tr>
<td><code>label</code></td>
<td>String</td>
<td>Display label for admin GUI</td>
</tr>
<tr>
<td><code>description</code></td>
<td>String (optional)</td>
<td>Variable purpose and usage notes</td>
</tr>
<tr>
<td><code>isRequired</code></td>
<td>Boolean</td>
<td>If true, must be provided in data object</td>
</tr>
<tr>
<td><code>isConditional</code></td>
<td>Boolean</td>
<td>If true, used in {{#if}} blocks (truthy/falsy)</td>
</tr>
<tr>
<td><code>sampleValue</code></td>
<td>String (optional)</td>
<td>Example value for testing and preview</td>
</tr>
<tr>
<td><code>sortOrder</code></td>
<td>Int</td>
<td>Display order in editor variable panel</td>
</tr>
<tr>
<td><code>createdAt</code></td>
<td>DateTime</td>
<td>Creation timestamp</td>
</tr>
</tbody>
</table>
<p><strong>Relations:</strong>
- <code>template</code> — EmailTemplate (N:1)</p>
<p><strong>Constraints:</strong>
- Unique index on <code>(templateId, key)</code> to prevent duplicate variables</p>
<hr />
<h3 id="emailtemplateversion">EmailTemplateVersion<a class="headerlink" href="#emailtemplateversion" title="Permanent link">&para;</a></h3>
<p><strong>Version history snapshots for audit trail and rollback.</strong></p>
<table>
<thead>
<tr>
<th>Field</th>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>id</code></td>
<td>String (CUID)</td>
<td>Primary key</td>
</tr>
<tr>
<td><code>templateId</code></td>
<td>String</td>
<td>Foreign key to EmailTemplate</td>
</tr>
<tr>
<td><code>versionNumber</code></td>
<td>Int</td>
<td>Auto-incremented version number (1, 2, 3...)</td>
</tr>
<tr>
<td><code>subjectLine</code></td>
<td>String</td>
<td>Subject at time of version</td>
</tr>
<tr>
<td><code>htmlContent</code></td>
<td>Text</td>
<td>HTML content snapshot</td>
</tr>
<tr>
<td><code>textContent</code></td>
<td>Text</td>
<td>Plain text content snapshot</td>
</tr>
<tr>
<td><code>changeNotes</code></td>
<td>String (optional)</td>
<td>Admin-provided change description</td>
</tr>
<tr>
<td><code>createdByUserId</code></td>
<td>String (optional)</td>
<td>User who created this version</td>
</tr>
<tr>
<td><code>createdAt</code></td>
<td>DateTime</td>
<td>Version creation timestamp</td>
</tr>
</tbody>
</table>
<p><strong>Relations:</strong>
- <code>template</code> — EmailTemplate (N:1)
- <code>createdBy</code> — User (N:1)</p>
<p><strong>Constraints:</strong>
- Unique index on <code>(templateId, versionNumber)</code> for version lookup
- Auto-increment logic in service layer (finds max + 1)</p>
<hr />
<h3 id="emailtemplatetestlog">EmailTemplateTestLog<a class="headerlink" href="#emailtemplatetestlog" title="Permanent link">&para;</a></h3>
<p><strong>Test send audit trail for debugging and compliance.</strong></p>
<table>
<thead>
<tr>
<th>Field</th>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>id</code></td>
<td>String (CUID)</td>
<td>Primary key</td>
</tr>
<tr>
<td><code>templateId</code></td>
<td>String</td>
<td>Foreign key to EmailTemplate</td>
</tr>
<tr>
<td><code>recipientEmail</code></td>
<td>String</td>
<td>Email address test was sent to</td>
</tr>
<tr>
<td><code>testData</code></td>
<td>JSON</td>
<td>Sample variable data used for interpolation</td>
</tr>
<tr>
<td><code>success</code></td>
<td>Boolean</td>
<td>Whether send succeeded</td>
</tr>
<tr>
<td><code>errorMessage</code></td>
<td>String (optional)</td>
<td>Error details if send failed</td>
</tr>
<tr>
<td><code>messageId</code></td>
<td>String (optional)</td>
<td>SMTP message ID if send succeeded</td>
</tr>
<tr>
<td><code>sentByUserId</code></td>
<td>String (optional)</td>
<td>User who triggered test send</td>
</tr>
<tr>
<td><code>createdAt</code></td>
<td>DateTime</td>
<td>Test send timestamp</td>
</tr>
</tbody>
</table>
<p><strong>Relations:</strong>
- <code>template</code> — EmailTemplate (N:1)
- <code>sentBy</code> — User (N:1)</p>
<p><strong>Indexes:</strong>
- Index on <code>templateId</code> for template-specific test history
- Index on <code>createdAt</code> for chronological queries</p>
<hr />
<h2 id="template-categories">Template Categories<a class="headerlink" href="#template-categories" title="Permanent link">&para;</a></h2>
<h3 id="influence-category">INFLUENCE Category<a class="headerlink" href="#influence-category" title="Permanent link">&para;</a></h3>
<p><strong>Purpose:</strong> Advocacy campaign emails sent to representatives or response notifications to participants.</p>
<p><strong>System Templates:</strong></p>
<table>
<thead>
<tr>
<th>Key</th>
<th>Name</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>campaign-email</code></td>
<td>Campaign Email to Representative</td>
<td>Main advocacy email template sent on behalf of participants</td>
</tr>
<tr>
<td><code>response-verification</code></td>
<td>Response Verification Email</td>
<td>Email asking participants to verify their response submission</td>
</tr>
<tr>
<td><code>response-approved</code></td>
<td>Response Approval Notification</td>
<td>Email notifying participant their response is published on wall</td>
</tr>
<tr>
<td><code>response-rejected</code></td>
<td>Response Rejection Notification</td>
<td>Email notifying participant their response was rejected (with reason)</td>
</tr>
</tbody>
</table>
<p><strong>Common Variables:</strong>
- <code>USER_NAME</code> — Participant's full name
- <code>USER_EMAIL</code> — Participant's email address
- <code>CAMPAIGN_TITLE</code> — Campaign name
- <code>CAMPAIGN_SLUG</code> — URL-safe campaign identifier
- <code>REPRESENTATIVE_NAME</code> — Representative's full name
- <code>REPRESENTATIVE_EMAIL</code> — Representative's email address
- <code>REPRESENTATIVE_TITLE</code> — Representative's title (e.g., "MP for...")
- <code>CUSTOM_MESSAGE</code> — Participant's custom message to representative
- <code>RESPONSE_TEXT</code> — Participant's response wall submission
- <code>VERIFICATION_LINK</code> — Unique verification URL
- <code>ADMIN_NOTES</code> — Moderator's rejection reason</p>
<hr />
<h3 id="map-category">MAP Category<a class="headerlink" href="#map-category" title="Permanent link">&para;</a></h3>
<p><strong>Purpose:</strong> Location-based emails for volunteer shifts, canvassing sessions, and shift management.</p>
<p><strong>System Templates:</strong></p>
<table>
<thead>
<tr>
<th>Key</th>
<th>Name</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>shift-signup-confirmation</code></td>
<td>Shift Signup Confirmation</td>
<td>Email confirming volunteer's shift registration</td>
</tr>
<tr>
<td><code>shift-reminder</code></td>
<td>Shift Reminder</td>
<td>Email sent 24 hours before shift starts</td>
</tr>
<tr>
<td><code>shift-cancellation</code></td>
<td>Shift Cancellation Notice</td>
<td>Email notifying volunteer of shift cancellation</td>
</tr>
<tr>
<td><code>canvass-session-summary</code></td>
<td>Canvass Session Summary</td>
<td>End-of-session report with visit statistics</td>
</tr>
</tbody>
</table>
<p><strong>Common Variables:</strong>
- <code>USER_NAME</code> — Volunteer's full name
- <code>USER_EMAIL</code> — Volunteer's email address
- <code>USER_PHONE</code> — Volunteer's phone number (optional)
- <code>SHIFT_TITLE</code> — Shift name
- <code>SHIFT_DATE</code> — Shift date (formatted)
- <code>SHIFT_TIME</code> — Shift time range (e.g., "10:00 AM - 2:00 PM")
- <code>SHIFT_LOCATION</code> — Shift meeting location
- <code>CUT_NAME</code> — Canvass area name
- <code>VISIT_COUNT</code> — Number of doors knocked
- <code>CONTACT_COUNT</code> — Number of successful contacts
- <code>SUPPORT_COUNT</code> — Number of supporters identified
- <code>CANCELLATION_REASON</code> — Why shift was cancelled</p>
<hr />
<h3 id="system-category">SYSTEM Category<a class="headerlink" href="#system-category" title="Permanent link">&para;</a></h3>
<p><strong>Purpose:</strong> Core platform emails for user management, authentication, and system notifications.</p>
<p><strong>System Templates:</strong></p>
<table>
<thead>
<tr>
<th>Key</th>
<th>Name</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>user-welcome</code></td>
<td>Welcome Email</td>
<td>Email sent to new user registrations</td>
</tr>
<tr>
<td><code>password-reset</code></td>
<td>Password Reset Email</td>
<td>Email with password reset link</td>
</tr>
<tr>
<td><code>email-verification</code></td>
<td>Email Verification</td>
<td>Email address verification for new accounts</td>
</tr>
<tr>
<td><code>account-locked</code></td>
<td>Account Locked Notice</td>
<td>Security notification for locked accounts</td>
</tr>
</tbody>
</table>
<p><strong>Common Variables:</strong>
- <code>USER_NAME</code> — User's full name
- <code>USER_EMAIL</code> — User's email address
- <code>VERIFICATION_LINK</code> — Unique verification URL (expires in 24h)
- <code>RESET_LINK</code> — Unique password reset URL (expires in 1h)
- <code>SUPPORT_EMAIL</code> — Platform support email address
- <code>SITE_NAME</code> — Platform name (from SiteSettings)
- <code>SITE_URL</code> — Platform base URL
- <code>LOGIN_URL</code> — Direct link to login page
- <code>LOCKOUT_REASON</code> — Why account was locked</p>
<hr />
<h2 id="variable-interpolation">Variable Interpolation<a class="headerlink" href="#variable-interpolation" title="Permanent link">&para;</a></h2>
<p>The template system uses <a href="https://handlebarsjs.com/">Handlebars</a> for powerful variable interpolation with support for basic variables, conditional blocks, loops, and helpers.</p>
<h3 id="basic-variables">Basic Variables<a class="headerlink" href="#basic-variables" title="Permanent link">&para;</a></h3>
<p><strong>Syntax:</strong> <code>{{VARIABLE_NAME}}</code></p>
<p><strong>Example Template:</strong>
<div class="language-html highlight"><pre><span></span><code><span id="__span-0-1"><a id="__codelineno-0-1" name="__codelineno-0-1" href="#__codelineno-0-1"></a><span class="p">&lt;</span><span class="nt">p</span><span class="p">&gt;</span>Dear {{USER_NAME}},<span class="p">&lt;/</span><span class="nt">p</span><span class="p">&gt;</span>
</span><span id="__span-0-2"><a id="__codelineno-0-2" name="__codelineno-0-2" href="#__codelineno-0-2"></a>
</span><span id="__span-0-3"><a id="__codelineno-0-3" name="__codelineno-0-3" href="#__codelineno-0-3"></a><span class="p">&lt;</span><span class="nt">p</span><span class="p">&gt;</span>Thank you for signing up for <span class="p">&lt;</span><span class="nt">strong</span><span class="p">&gt;</span>{{SHIFT_TITLE}}<span class="p">&lt;/</span><span class="nt">strong</span><span class="p">&gt;</span> on {{SHIFT_DATE}}.<span class="p">&lt;/</span><span class="nt">p</span><span class="p">&gt;</span>
</span><span id="__span-0-4"><a id="__codelineno-0-4" name="__codelineno-0-4" href="#__codelineno-0-4"></a>
</span><span id="__span-0-5"><a id="__codelineno-0-5" name="__codelineno-0-5" href="#__codelineno-0-5"></a><span class="p">&lt;</span><span class="nt">p</span><span class="p">&gt;</span>We&#39;ll see you at {{SHIFT_LOCATION}} at {{SHIFT_TIME}}.<span class="p">&lt;/</span><span class="nt">p</span><span class="p">&gt;</span>
</span><span id="__span-0-6"><a id="__codelineno-0-6" name="__codelineno-0-6" href="#__codelineno-0-6"></a>
</span><span id="__span-0-7"><a id="__codelineno-0-7" name="__codelineno-0-7" href="#__codelineno-0-7"></a><span class="p">&lt;</span><span class="nt">p</span><span class="p">&gt;</span>If you have any questions, email us at {{SUPPORT_EMAIL}}.<span class="p">&lt;/</span><span class="nt">p</span><span class="p">&gt;</span>
</span></code></pre></div></p>
<p><strong>Sample Data:</strong>
<div class="language-json highlight"><pre><span></span><code><span id="__span-1-1"><a id="__codelineno-1-1" name="__codelineno-1-1" href="#__codelineno-1-1"></a><span class="p">{</span>
</span><span id="__span-1-2"><a id="__codelineno-1-2" name="__codelineno-1-2" href="#__codelineno-1-2"></a><span class="w"> </span><span class="nt">&quot;USER_NAME&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;Jane Smith&quot;</span><span class="p">,</span>
</span><span id="__span-1-3"><a id="__codelineno-1-3" name="__codelineno-1-3" href="#__codelineno-1-3"></a><span class="w"> </span><span class="nt">&quot;SHIFT_TITLE&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;Door Knocking - Downtown&quot;</span><span class="p">,</span>
</span><span id="__span-1-4"><a id="__codelineno-1-4" name="__codelineno-1-4" href="#__codelineno-1-4"></a><span class="w"> </span><span class="nt">&quot;SHIFT_DATE&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;Saturday, March 15, 2026&quot;</span><span class="p">,</span>
</span><span id="__span-1-5"><a id="__codelineno-1-5" name="__codelineno-1-5" href="#__codelineno-1-5"></a><span class="w"> </span><span class="nt">&quot;SHIFT_LOCATION&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;Campaign Office (123 Main St)&quot;</span><span class="p">,</span>
</span><span id="__span-1-6"><a id="__codelineno-1-6" name="__codelineno-1-6" href="#__codelineno-1-6"></a><span class="w"> </span><span class="nt">&quot;SHIFT_TIME&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;10:00 AM - 2:00 PM&quot;</span><span class="p">,</span>
</span><span id="__span-1-7"><a id="__codelineno-1-7" name="__codelineno-1-7" href="#__codelineno-1-7"></a><span class="w"> </span><span class="nt">&quot;SUPPORT_EMAIL&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;volunteer@example.org&quot;</span>
</span><span id="__span-1-8"><a id="__codelineno-1-8" name="__codelineno-1-8" href="#__codelineno-1-8"></a><span class="p">}</span>
</span></code></pre></div></p>
<p><strong>Rendered Output:</strong>
<div class="language-html highlight"><pre><span></span><code><span id="__span-2-1"><a id="__codelineno-2-1" name="__codelineno-2-1" href="#__codelineno-2-1"></a><span class="p">&lt;</span><span class="nt">p</span><span class="p">&gt;</span>Dear Jane Smith,<span class="p">&lt;/</span><span class="nt">p</span><span class="p">&gt;</span>
</span><span id="__span-2-2"><a id="__codelineno-2-2" name="__codelineno-2-2" href="#__codelineno-2-2"></a>
</span><span id="__span-2-3"><a id="__codelineno-2-3" name="__codelineno-2-3" href="#__codelineno-2-3"></a><span class="p">&lt;</span><span class="nt">p</span><span class="p">&gt;</span>Thank you for signing up for <span class="p">&lt;</span><span class="nt">strong</span><span class="p">&gt;</span>Door Knocking - Downtown<span class="p">&lt;/</span><span class="nt">strong</span><span class="p">&gt;</span> on Saturday, March 15, 2026.<span class="p">&lt;/</span><span class="nt">p</span><span class="p">&gt;</span>
</span><span id="__span-2-4"><a id="__codelineno-2-4" name="__codelineno-2-4" href="#__codelineno-2-4"></a>
</span><span id="__span-2-5"><a id="__codelineno-2-5" name="__codelineno-2-5" href="#__codelineno-2-5"></a><span class="p">&lt;</span><span class="nt">p</span><span class="p">&gt;</span>We&#39;ll see you at Campaign Office (123 Main St) at 10:00 AM - 2:00 PM.<span class="p">&lt;/</span><span class="nt">p</span><span class="p">&gt;</span>
</span><span id="__span-2-6"><a id="__codelineno-2-6" name="__codelineno-2-6" href="#__codelineno-2-6"></a>
</span><span id="__span-2-7"><a id="__codelineno-2-7" name="__codelineno-2-7" href="#__codelineno-2-7"></a><span class="p">&lt;</span><span class="nt">p</span><span class="p">&gt;</span>If you have any questions, email us at volunteer@example.org.<span class="p">&lt;/</span><span class="nt">p</span><span class="p">&gt;</span>
</span></code></pre></div></p>
<hr />
<h3 id="conditional-blocks">Conditional Blocks<a class="headerlink" href="#conditional-blocks" title="Permanent link">&para;</a></h3>
<p><strong>Syntax:</strong> <code>{{#if CONDITION}} ... {{else}} ... {{/if}}</code></p>
<p><strong>Example Template:</strong>
<div class="language-html highlight"><pre><span></span><code><span id="__span-3-1"><a id="__codelineno-3-1" name="__codelineno-3-1" href="#__codelineno-3-1"></a><span class="p">&lt;</span><span class="nt">p</span><span class="p">&gt;</span>Dear {{USER_NAME}},<span class="p">&lt;/</span><span class="nt">p</span><span class="p">&gt;</span>
</span><span id="__span-3-2"><a id="__codelineno-3-2" name="__codelineno-3-2" href="#__codelineno-3-2"></a>
</span><span id="__span-3-3"><a id="__codelineno-3-3" name="__codelineno-3-3" href="#__codelineno-3-3"></a><span class="p">&lt;</span><span class="nt">p</span><span class="p">&gt;</span>Your shift confirmation for {{SHIFT_TITLE}} is below.<span class="p">&lt;/</span><span class="nt">p</span><span class="p">&gt;</span>
</span><span id="__span-3-4"><a id="__codelineno-3-4" name="__codelineno-3-4" href="#__codelineno-3-4"></a>
</span><span id="__span-3-5"><a id="__codelineno-3-5" name="__codelineno-3-5" href="#__codelineno-3-5"></a>{{#if HAS_PHONE}}
</span><span id="__span-3-6"><a id="__codelineno-3-6" name="__codelineno-3-6" href="#__codelineno-3-6"></a><span class="p">&lt;</span><span class="nt">p</span><span class="p">&gt;&lt;</span><span class="nt">strong</span><span class="p">&gt;</span>We&#39;ll call you at {{USER_PHONE}} if there are any changes.<span class="p">&lt;/</span><span class="nt">strong</span><span class="p">&gt;&lt;/</span><span class="nt">p</span><span class="p">&gt;</span>
</span><span id="__span-3-7"><a id="__codelineno-3-7" name="__codelineno-3-7" href="#__codelineno-3-7"></a>{{else}}
</span><span id="__span-3-8"><a id="__codelineno-3-8" name="__codelineno-3-8" href="#__codelineno-3-8"></a><span class="p">&lt;</span><span class="nt">p</span><span class="p">&gt;</span>We recommend adding a phone number to your profile for shift updates.<span class="p">&lt;/</span><span class="nt">p</span><span class="p">&gt;</span>
</span><span id="__span-3-9"><a id="__codelineno-3-9" name="__codelineno-3-9" href="#__codelineno-3-9"></a>{{/if}}
</span><span id="__span-3-10"><a id="__codelineno-3-10" name="__codelineno-3-10" href="#__codelineno-3-10"></a>
</span><span id="__span-3-11"><a id="__codelineno-3-11" name="__codelineno-3-11" href="#__codelineno-3-11"></a>{{#if IS_CUT_ASSIGNED}}
</span><span id="__span-3-12"><a id="__codelineno-3-12" name="__codelineno-3-12" href="#__codelineno-3-12"></a><span class="p">&lt;</span><span class="nt">p</span><span class="p">&gt;</span>You&#39;ve been assigned to canvass <span class="p">&lt;</span><span class="nt">strong</span><span class="p">&gt;</span>{{CUT_NAME}}<span class="p">&lt;/</span><span class="nt">strong</span><span class="p">&gt;</span>.<span class="p">&lt;/</span><span class="nt">p</span><span class="p">&gt;</span>
</span><span id="__span-3-13"><a id="__codelineno-3-13" name="__codelineno-3-13" href="#__codelineno-3-13"></a>{{/if}}
</span></code></pre></div></p>
<p><strong>Sample Data:</strong>
<div class="language-json highlight"><pre><span></span><code><span id="__span-4-1"><a id="__codelineno-4-1" name="__codelineno-4-1" href="#__codelineno-4-1"></a><span class="p">{</span>
</span><span id="__span-4-2"><a id="__codelineno-4-2" name="__codelineno-4-2" href="#__codelineno-4-2"></a><span class="w"> </span><span class="nt">&quot;USER_NAME&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;John Doe&quot;</span><span class="p">,</span>
</span><span id="__span-4-3"><a id="__codelineno-4-3" name="__codelineno-4-3" href="#__codelineno-4-3"></a><span class="w"> </span><span class="nt">&quot;SHIFT_TITLE&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;Canvassing - North District&quot;</span><span class="p">,</span>
</span><span id="__span-4-4"><a id="__codelineno-4-4" name="__codelineno-4-4" href="#__codelineno-4-4"></a><span class="w"> </span><span class="nt">&quot;HAS_PHONE&quot;</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span>
</span><span id="__span-4-5"><a id="__codelineno-4-5" name="__codelineno-4-5" href="#__codelineno-4-5"></a><span class="w"> </span><span class="nt">&quot;USER_PHONE&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;(555) 123-4567&quot;</span><span class="p">,</span>
</span><span id="__span-4-6"><a id="__codelineno-4-6" name="__codelineno-4-6" href="#__codelineno-4-6"></a><span class="w"> </span><span class="nt">&quot;IS_CUT_ASSIGNED&quot;</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span>
</span><span id="__span-4-7"><a id="__codelineno-4-7" name="__codelineno-4-7" href="#__codelineno-4-7"></a><span class="w"> </span><span class="nt">&quot;CUT_NAME&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;North District - Zone A&quot;</span>
</span><span id="__span-4-8"><a id="__codelineno-4-8" name="__codelineno-4-8" href="#__codelineno-4-8"></a><span class="p">}</span>
</span></code></pre></div></p>
<p><strong>Rendered Output:</strong>
<div class="language-html highlight"><pre><span></span><code><span id="__span-5-1"><a id="__codelineno-5-1" name="__codelineno-5-1" href="#__codelineno-5-1"></a><span class="p">&lt;</span><span class="nt">p</span><span class="p">&gt;</span>Dear John Doe,<span class="p">&lt;/</span><span class="nt">p</span><span class="p">&gt;</span>
</span><span id="__span-5-2"><a id="__codelineno-5-2" name="__codelineno-5-2" href="#__codelineno-5-2"></a>
</span><span id="__span-5-3"><a id="__codelineno-5-3" name="__codelineno-5-3" href="#__codelineno-5-3"></a><span class="p">&lt;</span><span class="nt">p</span><span class="p">&gt;</span>Your shift confirmation for Canvassing - North District is below.<span class="p">&lt;/</span><span class="nt">p</span><span class="p">&gt;</span>
</span><span id="__span-5-4"><a id="__codelineno-5-4" name="__codelineno-5-4" href="#__codelineno-5-4"></a>
</span><span id="__span-5-5"><a id="__codelineno-5-5" name="__codelineno-5-5" href="#__codelineno-5-5"></a><span class="p">&lt;</span><span class="nt">p</span><span class="p">&gt;&lt;</span><span class="nt">strong</span><span class="p">&gt;</span>We&#39;ll call you at (555) 123-4567 if there are any changes.<span class="p">&lt;/</span><span class="nt">strong</span><span class="p">&gt;&lt;/</span><span class="nt">p</span><span class="p">&gt;</span>
</span><span id="__span-5-6"><a id="__codelineno-5-6" name="__codelineno-5-6" href="#__codelineno-5-6"></a>
</span><span id="__span-5-7"><a id="__codelineno-5-7" name="__codelineno-5-7" href="#__codelineno-5-7"></a><span class="p">&lt;</span><span class="nt">p</span><span class="p">&gt;</span>You&#39;ve been assigned to canvass <span class="p">&lt;</span><span class="nt">strong</span><span class="p">&gt;</span>North District - Zone A<span class="p">&lt;/</span><span class="nt">strong</span><span class="p">&gt;</span>.<span class="p">&lt;/</span><span class="nt">p</span><span class="p">&gt;</span>
</span></code></pre></div></p>
<p><strong>Truthy/Falsy Values:</strong>
- <code>true</code>, non-empty strings, non-zero numbers → truthy
- <code>false</code>, <code>null</code>, <code>undefined</code>, <code>0</code>, <code>""</code> → falsy</p>
<hr />
<h3 id="loops-each-blocks">Loops (Each Blocks)<a class="headerlink" href="#loops-each-blocks" title="Permanent link">&para;</a></h3>
<p><strong>Syntax:</strong> <code>{{#each ARRAY}} ... {{/each}}</code></p>
<p><strong>Example Template:</strong>
<div class="language-html highlight"><pre><span></span><code><span id="__span-6-1"><a id="__codelineno-6-1" name="__codelineno-6-1" href="#__codelineno-6-1"></a><span class="p">&lt;</span><span class="nt">p</span><span class="p">&gt;</span>Dear {{USER_NAME}},<span class="p">&lt;/</span><span class="nt">p</span><span class="p">&gt;</span>
</span><span id="__span-6-2"><a id="__codelineno-6-2" name="__codelineno-6-2" href="#__codelineno-6-2"></a>
</span><span id="__span-6-3"><a id="__codelineno-6-3" name="__codelineno-6-3" href="#__codelineno-6-3"></a><span class="p">&lt;</span><span class="nt">p</span><span class="p">&gt;</span>Your email will be sent to the following representatives:<span class="p">&lt;/</span><span class="nt">p</span><span class="p">&gt;</span>
</span><span id="__span-6-4"><a id="__codelineno-6-4" name="__codelineno-6-4" href="#__codelineno-6-4"></a>
</span><span id="__span-6-5"><a id="__codelineno-6-5" name="__codelineno-6-5" href="#__codelineno-6-5"></a><span class="p">&lt;</span><span class="nt">ul</span><span class="p">&gt;</span>
</span><span id="__span-6-6"><a id="__codelineno-6-6" name="__codelineno-6-6" href="#__codelineno-6-6"></a>{{#each REPRESENTATIVES}}
</span><span id="__span-6-7"><a id="__codelineno-6-7" name="__codelineno-6-7" href="#__codelineno-6-7"></a> <span class="p">&lt;</span><span class="nt">li</span><span class="p">&gt;</span>
</span><span id="__span-6-8"><a id="__codelineno-6-8" name="__codelineno-6-8" href="#__codelineno-6-8"></a> <span class="p">&lt;</span><span class="nt">strong</span><span class="p">&gt;</span>{{name}}<span class="p">&lt;/</span><span class="nt">strong</span><span class="p">&gt;</span> ({{title}})<span class="p">&lt;</span><span class="nt">br</span><span class="p">&gt;</span>
</span><span id="__span-6-9"><a id="__codelineno-6-9" name="__codelineno-6-9" href="#__codelineno-6-9"></a> Email: {{email}}
</span><span id="__span-6-10"><a id="__codelineno-6-10" name="__codelineno-6-10" href="#__codelineno-6-10"></a> <span class="p">&lt;/</span><span class="nt">li</span><span class="p">&gt;</span>
</span><span id="__span-6-11"><a id="__codelineno-6-11" name="__codelineno-6-11" href="#__codelineno-6-11"></a>{{/each}}
</span><span id="__span-6-12"><a id="__codelineno-6-12" name="__codelineno-6-12" href="#__codelineno-6-12"></a><span class="p">&lt;/</span><span class="nt">ul</span><span class="p">&gt;</span>
</span><span id="__span-6-13"><a id="__codelineno-6-13" name="__codelineno-6-13" href="#__codelineno-6-13"></a>
</span><span id="__span-6-14"><a id="__codelineno-6-14" name="__codelineno-6-14" href="#__codelineno-6-14"></a><span class="p">&lt;</span><span class="nt">p</span><span class="p">&gt;</span>Your custom message:<span class="p">&lt;/</span><span class="nt">p</span><span class="p">&gt;</span>
</span><span id="__span-6-15"><a id="__codelineno-6-15" name="__codelineno-6-15" href="#__codelineno-6-15"></a><span class="p">&lt;</span><span class="nt">blockquote</span><span class="p">&gt;</span>{{CUSTOM_MESSAGE}}<span class="p">&lt;/</span><span class="nt">blockquote</span><span class="p">&gt;</span>
</span></code></pre></div></p>
<p><strong>Sample Data:</strong>
<div class="language-json highlight"><pre><span></span><code><span id="__span-7-1"><a id="__codelineno-7-1" name="__codelineno-7-1" href="#__codelineno-7-1"></a><span class="p">{</span>
</span><span id="__span-7-2"><a id="__codelineno-7-2" name="__codelineno-7-2" href="#__codelineno-7-2"></a><span class="w"> </span><span class="nt">&quot;USER_NAME&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;Alice Johnson&quot;</span><span class="p">,</span>
</span><span id="__span-7-3"><a id="__codelineno-7-3" name="__codelineno-7-3" href="#__codelineno-7-3"></a><span class="w"> </span><span class="nt">&quot;REPRESENTATIVES&quot;</span><span class="p">:</span><span class="w"> </span><span class="p">[</span>
</span><span id="__span-7-4"><a id="__codelineno-7-4" name="__codelineno-7-4" href="#__codelineno-7-4"></a><span class="w"> </span><span class="p">{</span>
</span><span id="__span-7-5"><a id="__codelineno-7-5" name="__codelineno-7-5" href="#__codelineno-7-5"></a><span class="w"> </span><span class="nt">&quot;name&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;Jane Doe&quot;</span><span class="p">,</span>
</span><span id="__span-7-6"><a id="__codelineno-7-6" name="__codelineno-7-6" href="#__codelineno-7-6"></a><span class="w"> </span><span class="nt">&quot;title&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;MP for Downtown&quot;</span><span class="p">,</span>
</span><span id="__span-7-7"><a id="__codelineno-7-7" name="__codelineno-7-7" href="#__codelineno-7-7"></a><span class="w"> </span><span class="nt">&quot;email&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;jane.doe@parliament.ca&quot;</span>
</span><span id="__span-7-8"><a id="__codelineno-7-8" name="__codelineno-7-8" href="#__codelineno-7-8"></a><span class="w"> </span><span class="p">},</span>
</span><span id="__span-7-9"><a id="__codelineno-7-9" name="__codelineno-7-9" href="#__codelineno-7-9"></a><span class="w"> </span><span class="p">{</span>
</span><span id="__span-7-10"><a id="__codelineno-7-10" name="__codelineno-7-10" href="#__codelineno-7-10"></a><span class="w"> </span><span class="nt">&quot;name&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;John Smith&quot;</span><span class="p">,</span>
</span><span id="__span-7-11"><a id="__codelineno-7-11" name="__codelineno-7-11" href="#__codelineno-7-11"></a><span class="w"> </span><span class="nt">&quot;title&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;City Councillor Ward 3&quot;</span><span class="p">,</span>
</span><span id="__span-7-12"><a id="__codelineno-7-12" name="__codelineno-7-12" href="#__codelineno-7-12"></a><span class="w"> </span><span class="nt">&quot;email&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;john.smith@city.ca&quot;</span>
</span><span id="__span-7-13"><a id="__codelineno-7-13" name="__codelineno-7-13" href="#__codelineno-7-13"></a><span class="w"> </span><span class="p">}</span>
</span><span id="__span-7-14"><a id="__codelineno-7-14" name="__codelineno-7-14" href="#__codelineno-7-14"></a><span class="w"> </span><span class="p">],</span>
</span><span id="__span-7-15"><a id="__codelineno-7-15" name="__codelineno-7-15" href="#__codelineno-7-15"></a><span class="w"> </span><span class="nt">&quot;CUSTOM_MESSAGE&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;Please support Bill C-123 to address climate change.&quot;</span>
</span><span id="__span-7-16"><a id="__codelineno-7-16" name="__codelineno-7-16" href="#__codelineno-7-16"></a><span class="p">}</span>
</span></code></pre></div></p>
<p><strong>Rendered Output:</strong>
<div class="language-html highlight"><pre><span></span><code><span id="__span-8-1"><a id="__codelineno-8-1" name="__codelineno-8-1" href="#__codelineno-8-1"></a><span class="p">&lt;</span><span class="nt">p</span><span class="p">&gt;</span>Dear Alice Johnson,<span class="p">&lt;/</span><span class="nt">p</span><span class="p">&gt;</span>
</span><span id="__span-8-2"><a id="__codelineno-8-2" name="__codelineno-8-2" href="#__codelineno-8-2"></a>
</span><span id="__span-8-3"><a id="__codelineno-8-3" name="__codelineno-8-3" href="#__codelineno-8-3"></a><span class="p">&lt;</span><span class="nt">p</span><span class="p">&gt;</span>Your email will be sent to the following representatives:<span class="p">&lt;/</span><span class="nt">p</span><span class="p">&gt;</span>
</span><span id="__span-8-4"><a id="__codelineno-8-4" name="__codelineno-8-4" href="#__codelineno-8-4"></a>
</span><span id="__span-8-5"><a id="__codelineno-8-5" name="__codelineno-8-5" href="#__codelineno-8-5"></a><span class="p">&lt;</span><span class="nt">ul</span><span class="p">&gt;</span>
</span><span id="__span-8-6"><a id="__codelineno-8-6" name="__codelineno-8-6" href="#__codelineno-8-6"></a> <span class="p">&lt;</span><span class="nt">li</span><span class="p">&gt;</span>
</span><span id="__span-8-7"><a id="__codelineno-8-7" name="__codelineno-8-7" href="#__codelineno-8-7"></a> <span class="p">&lt;</span><span class="nt">strong</span><span class="p">&gt;</span>Jane Doe<span class="p">&lt;/</span><span class="nt">strong</span><span class="p">&gt;</span> (MP for Downtown)<span class="p">&lt;</span><span class="nt">br</span><span class="p">&gt;</span>
</span><span id="__span-8-8"><a id="__codelineno-8-8" name="__codelineno-8-8" href="#__codelineno-8-8"></a> Email: jane.doe@parliament.ca
</span><span id="__span-8-9"><a id="__codelineno-8-9" name="__codelineno-8-9" href="#__codelineno-8-9"></a> <span class="p">&lt;/</span><span class="nt">li</span><span class="p">&gt;</span>
</span><span id="__span-8-10"><a id="__codelineno-8-10" name="__codelineno-8-10" href="#__codelineno-8-10"></a> <span class="p">&lt;</span><span class="nt">li</span><span class="p">&gt;</span>
</span><span id="__span-8-11"><a id="__codelineno-8-11" name="__codelineno-8-11" href="#__codelineno-8-11"></a> <span class="p">&lt;</span><span class="nt">strong</span><span class="p">&gt;</span>John Smith<span class="p">&lt;/</span><span class="nt">strong</span><span class="p">&gt;</span> (City Councillor Ward 3)<span class="p">&lt;</span><span class="nt">br</span><span class="p">&gt;</span>
</span><span id="__span-8-12"><a id="__codelineno-8-12" name="__codelineno-8-12" href="#__codelineno-8-12"></a> Email: john.smith@city.ca
</span><span id="__span-8-13"><a id="__codelineno-8-13" name="__codelineno-8-13" href="#__codelineno-8-13"></a> <span class="p">&lt;/</span><span class="nt">li</span><span class="p">&gt;</span>
</span><span id="__span-8-14"><a id="__codelineno-8-14" name="__codelineno-8-14" href="#__codelineno-8-14"></a><span class="p">&lt;/</span><span class="nt">ul</span><span class="p">&gt;</span>
</span><span id="__span-8-15"><a id="__codelineno-8-15" name="__codelineno-8-15" href="#__codelineno-8-15"></a>
</span><span id="__span-8-16"><a id="__codelineno-8-16" name="__codelineno-8-16" href="#__codelineno-8-16"></a><span class="p">&lt;</span><span class="nt">p</span><span class="p">&gt;</span>Your custom message:<span class="p">&lt;/</span><span class="nt">p</span><span class="p">&gt;</span>
</span><span id="__span-8-17"><a id="__codelineno-8-17" name="__codelineno-8-17" href="#__codelineno-8-17"></a><span class="p">&lt;</span><span class="nt">blockquote</span><span class="p">&gt;</span>Please support Bill C-123 to address climate change.<span class="p">&lt;/</span><span class="nt">blockquote</span><span class="p">&gt;</span>
</span></code></pre></div></p>
<p><strong>Loop Variables:</strong>
- <code>{{@index}}</code> — 0-based index
- <code>{{@first}}</code> — true if first item
- <code>{{@last}}</code> — true if last item</p>
<hr />
<h3 id="raw-html-unescaped">Raw HTML (Unescaped)<a class="headerlink" href="#raw-html-unescaped" title="Permanent link">&para;</a></h3>
<p><strong>Syntax:</strong> <code>{{{VARIABLE_NAME}}}</code> (triple braces)</p>
<p><strong>By default, Handlebars escapes HTML</strong> to prevent XSS attacks. Use triple braces for trusted HTML content.</p>
<p><strong>Example Template:</strong>
<div class="language-html highlight"><pre><span></span><code><span id="__span-9-1"><a id="__codelineno-9-1" name="__codelineno-9-1" href="#__codelineno-9-1"></a><span class="p">&lt;</span><span class="nt">p</span><span class="p">&gt;</span>Dear {{USER_NAME}},<span class="p">&lt;/</span><span class="nt">p</span><span class="p">&gt;</span>
</span><span id="__span-9-2"><a id="__codelineno-9-2" name="__codelineno-9-2" href="#__codelineno-9-2"></a>
</span><span id="__span-9-3"><a id="__codelineno-9-3" name="__codelineno-9-3" href="#__codelineno-9-3"></a><span class="p">&lt;</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">&quot;message-content&quot;</span><span class="p">&gt;</span>
</span><span id="__span-9-4"><a id="__codelineno-9-4" name="__codelineno-9-4" href="#__codelineno-9-4"></a> {{{FORMATTED_MESSAGE}}}
</span><span id="__span-9-5"><a id="__codelineno-9-5" name="__codelineno-9-5" href="#__codelineno-9-5"></a><span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
</span></code></pre></div></p>
<p><strong>Sample Data:</strong>
<div class="language-json highlight"><pre><span></span><code><span id="__span-10-1"><a id="__codelineno-10-1" name="__codelineno-10-1" href="#__codelineno-10-1"></a><span class="p">{</span>
</span><span id="__span-10-2"><a id="__codelineno-10-2" name="__codelineno-10-2" href="#__codelineno-10-2"></a><span class="w"> </span><span class="nt">&quot;USER_NAME&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;Bob Wilson&quot;</span><span class="p">,</span>
</span><span id="__span-10-3"><a id="__codelineno-10-3" name="__codelineno-10-3" href="#__codelineno-10-3"></a><span class="w"> </span><span class="nt">&quot;FORMATTED_MESSAGE&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;&lt;p&gt;This is &lt;strong&gt;bold&lt;/strong&gt; and &lt;em&gt;italic&lt;/em&gt; text.&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Item 1&lt;/li&gt;&lt;li&gt;Item 2&lt;/li&gt;&lt;/ul&gt;&quot;</span>
</span><span id="__span-10-4"><a id="__codelineno-10-4" name="__codelineno-10-4" href="#__codelineno-10-4"></a><span class="p">}</span>
</span></code></pre></div></p>
<p><strong>Rendered Output:</strong>
<div class="language-html highlight"><pre><span></span><code><span id="__span-11-1"><a id="__codelineno-11-1" name="__codelineno-11-1" href="#__codelineno-11-1"></a><span class="p">&lt;</span><span class="nt">p</span><span class="p">&gt;</span>Dear Bob Wilson,<span class="p">&lt;/</span><span class="nt">p</span><span class="p">&gt;</span>
</span><span id="__span-11-2"><a id="__codelineno-11-2" name="__codelineno-11-2" href="#__codelineno-11-2"></a>
</span><span id="__span-11-3"><a id="__codelineno-11-3" name="__codelineno-11-3" href="#__codelineno-11-3"></a><span class="p">&lt;</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">&quot;message-content&quot;</span><span class="p">&gt;</span>
</span><span id="__span-11-4"><a id="__codelineno-11-4" name="__codelineno-11-4" href="#__codelineno-11-4"></a> <span class="p">&lt;</span><span class="nt">p</span><span class="p">&gt;</span>This is <span class="p">&lt;</span><span class="nt">strong</span><span class="p">&gt;</span>bold<span class="p">&lt;/</span><span class="nt">strong</span><span class="p">&gt;</span> and <span class="p">&lt;</span><span class="nt">em</span><span class="p">&gt;</span>italic<span class="p">&lt;/</span><span class="nt">em</span><span class="p">&gt;</span> text.<span class="p">&lt;/</span><span class="nt">p</span><span class="p">&gt;&lt;</span><span class="nt">ul</span><span class="p">&gt;&lt;</span><span class="nt">li</span><span class="p">&gt;</span>Item 1<span class="p">&lt;/</span><span class="nt">li</span><span class="p">&gt;&lt;</span><span class="nt">li</span><span class="p">&gt;</span>Item 2<span class="p">&lt;/</span><span class="nt">li</span><span class="p">&gt;&lt;/</span><span class="nt">ul</span><span class="p">&gt;</span>
</span><span id="__span-11-5"><a id="__codelineno-11-5" name="__codelineno-11-5" href="#__codelineno-11-5"></a><span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
</span></code></pre></div></p>
<p><strong>Security Warning:</strong> Only use <code>{{{...}}}</code> for content generated by the application, never for user-submitted content without sanitization.</p>
<hr />
<h2 id="admin-workflow">Admin Workflow<a class="headerlink" href="#admin-workflow" title="Permanent link">&para;</a></h2>
<h3 id="viewing-templates">Viewing Templates<a class="headerlink" href="#viewing-templates" title="Permanent link">&para;</a></h3>
<ol>
<li><strong>Navigate to Email Templates Page</strong></li>
<li>Admin sidebar → Email Templates</li>
<li>
<p>Shows table with all templates grouped by category</p>
</li>
<li>
<p><strong>Filter and Search</strong></p>
</li>
<li>Filter by category (INFLUENCE, MAP, SYSTEM)</li>
<li>Search by template name or key</li>
<li>
<p>Toggle "Show Inactive" to view disabled templates</p>
</li>
<li>
<p><strong>Template Details</strong></p>
</li>
<li>Click template row to view details modal</li>
<li>See subject line, category, active status, system flag</li>
<li>View variable list with required/optional labels</li>
<li>Access version history tab</li>
<li>Access test send tab</li>
</ol>
<hr />
<h3 id="creating-template">Creating Template<a class="headerlink" href="#creating-template" title="Permanent link">&para;</a></h3>
<ol>
<li><strong>Click "New Template" Button</strong></li>
<li>
<p>Opens template creation modal</p>
</li>
<li>
<p><strong>Enter Template Metadata</strong></p>
</li>
<li><strong>Key</strong> — Programmatic identifier (lowercase-with-dashes)</li>
<li><strong>Name</strong> — Display name for admin GUI</li>
<li><strong>Description</strong> — Template purpose and usage notes</li>
<li><strong>Category</strong> — Select INFLUENCE, MAP, or SYSTEM</li>
<li>
<p><strong>System Flag</strong> — Check if template is critical (prevents deletion)</p>
</li>
<li>
<p><strong>Define Variables</strong></p>
</li>
<li>Click "Add Variable" in variables section</li>
<li>Enter variable key (UPPERCASE_WITH_UNDERSCORES)</li>
<li>Enter label and description</li>
<li>Toggle required/conditional flags</li>
<li>Provide sample value for testing</li>
<li>
<p>Set sort order (drag to reorder)</p>
</li>
<li>
<p><strong>Write Template Content</strong></p>
</li>
<li><strong>Subject Line</strong> — Enter subject with optional {{VARIABLES}}</li>
<li><strong>HTML Content</strong> — Write HTML body with {{VARIABLES}}</li>
<li>
<p><strong>Text Content</strong> — Write plain text fallback</p>
</li>
<li>
<p><strong>Save Template</strong></p>
</li>
<li>Click "Save" to create template</li>
<li>Creates version 1 automatically</li>
<li>Template is active by default</li>
</ol>
<hr />
<h3 id="editing-template">Editing Template<a class="headerlink" href="#editing-template" title="Permanent link">&para;</a></h3>
<ol>
<li><strong>Open Template</strong></li>
<li>Email Templates page → click template</li>
<li>
<p>Opens detail modal</p>
</li>
<li>
<p><strong>Click "Edit" Button</strong></p>
</li>
<li>Opens EmailTemplateEditorPage in new tab</li>
<li>
<p>Shows split-pane editor (HTML + Text)</p>
</li>
<li>
<p><strong>Modify Content</strong></p>
</li>
<li>Edit subject line, HTML, or text content</li>
<li>Use variable insertion buttons to add {{VARIABLES}}</li>
<li>
<p>Preview rendered output with sample data</p>
</li>
<li>
<p><strong>Add Change Notes</strong></p>
</li>
<li>Enter description of changes in "Change Notes" field</li>
<li>
<p>Used for version history audit trail</p>
</li>
<li>
<p><strong>Save Changes</strong></p>
</li>
<li>Click "Save" button</li>
<li>Creates new version automatically</li>
<li>Redirects to Email Templates page</li>
</ol>
<hr />
<h3 id="testing-template">Testing Template<a class="headerlink" href="#testing-template" title="Permanent link">&para;</a></h3>
<ol>
<li><strong>Open Template Detail Modal</strong></li>
<li>
<p>Click template from list</p>
</li>
<li>
<p><strong>Navigate to "Test Send" Tab</strong></p>
</li>
<li>
<p><strong>Enter Test Parameters</strong></p>
</li>
<li><strong>Recipient Email</strong> — Your email address for test</li>
<li><strong>Sample Data</strong> — JSON object with variable values</li>
<li>
<p>Pre-filled with variable sample values</p>
</li>
<li>
<p><strong>Click "Send Test Email"</strong></p>
</li>
<li>Template is rendered with sample data</li>
<li>Email sent via SMTP (or MailHog in test mode)</li>
<li>
<p>Success/failure notification displayed</p>
</li>
<li>
<p><strong>Check Test Log</strong></p>
</li>
<li>View test send history in "Test Logs" tab</li>
<li>See timestamp, recipient, success status, error messages</li>
<li>Review sample data used for each test</li>
</ol>
<hr />
<h3 id="activatingdeactivating-template">Activating/Deactivating Template<a class="headerlink" href="#activatingdeactivating-template" title="Permanent link">&para;</a></h3>
<ol>
<li>
<p><strong>Open Template Detail Modal</strong></p>
</li>
<li>
<p><strong>Toggle "Active" Switch</strong></p>
</li>
<li>When inactive, template won't send emails</li>
<li>
<p>Useful for disabling seasonal templates or broken templates</p>
</li>
<li>
<p><strong>Confirm Action</strong></p>
</li>
<li>System templates require additional confirmation</li>
<li>Deactivating system template may break critical platform functions</li>
</ol>
<hr />
<h2 id="developer-workflow-adding-new-template">Developer Workflow (Adding New Template)<a class="headerlink" href="#developer-workflow-adding-new-template" title="Permanent link">&para;</a></h2>
<h3 id="step-1-define-template-key">Step 1: Define Template Key<a class="headerlink" href="#step-1-define-template-key" title="Permanent link">&para;</a></h3>
<p>Choose a descriptive, unique key using lowercase with dashes:</p>
<p><strong>Good Keys:</strong>
- <code>shift-signup-confirmation</code>
- <code>canvass-session-summary</code>
- <code>response-verification</code></p>
<p><strong>Bad Keys:</strong>
- <code>template1</code> (not descriptive)
- <code>ShiftSignup</code> (wrong case)
- <code>shift_signup</code> (use dashes, not underscores)</p>
<hr />
<h3 id="step-2-create-template-via-seed-script">Step 2: Create Template via Seed Script<a class="headerlink" href="#step-2-create-template-via-seed-script" title="Permanent link">&para;</a></h3>
<p><strong>Add to <code>api/prisma/seed.ts</code>:</strong></p>
<div class="language-typescript highlight"><pre><span></span><code><span id="__span-12-1"><a id="__codelineno-12-1" name="__codelineno-12-1" href="#__codelineno-12-1"></a><span class="k">await</span><span class="w"> </span><span class="nx">prisma</span><span class="p">.</span><span class="nx">emailTemplate</span><span class="p">.</span><span class="nx">upsert</span><span class="p">({</span>
</span><span id="__span-12-2"><a id="__codelineno-12-2" name="__codelineno-12-2" href="#__codelineno-12-2"></a><span class="w"> </span><span class="nx">where</span><span class="o">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">key</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;shift-signup-confirmation&#39;</span><span class="w"> </span><span class="p">},</span>
</span><span id="__span-12-3"><a id="__codelineno-12-3" name="__codelineno-12-3" href="#__codelineno-12-3"></a><span class="w"> </span><span class="nx">update</span><span class="o">:</span><span class="w"> </span><span class="p">{},</span>
</span><span id="__span-12-4"><a id="__codelineno-12-4" name="__codelineno-12-4" href="#__codelineno-12-4"></a><span class="w"> </span><span class="nx">create</span><span class="o">:</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-12-5"><a id="__codelineno-12-5" name="__codelineno-12-5" href="#__codelineno-12-5"></a><span class="w"> </span><span class="nx">key</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;shift-signup-confirmation&#39;</span><span class="p">,</span>
</span><span id="__span-12-6"><a id="__codelineno-12-6" name="__codelineno-12-6" href="#__codelineno-12-6"></a><span class="w"> </span><span class="nx">name</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;Shift Signup Confirmation&#39;</span><span class="p">,</span>
</span><span id="__span-12-7"><a id="__codelineno-12-7" name="__codelineno-12-7" href="#__codelineno-12-7"></a><span class="w"> </span><span class="nx">description</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;Email sent to volunteers when they sign up for a shift&#39;</span><span class="p">,</span>
</span><span id="__span-12-8"><a id="__codelineno-12-8" name="__codelineno-12-8" href="#__codelineno-12-8"></a><span class="w"> </span><span class="nx">category</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;MAP&#39;</span><span class="p">,</span>
</span><span id="__span-12-9"><a id="__codelineno-12-9" name="__codelineno-12-9" href="#__codelineno-12-9"></a><span class="w"> </span><span class="nx">isSystem</span><span class="o">:</span><span class="w"> </span><span class="kt">true</span><span class="p">,</span>
</span><span id="__span-12-10"><a id="__codelineno-12-10" name="__codelineno-12-10" href="#__codelineno-12-10"></a><span class="w"> </span><span class="nx">isActive</span><span class="o">:</span><span class="w"> </span><span class="kt">true</span><span class="p">,</span>
</span><span id="__span-12-11"><a id="__codelineno-12-11" name="__codelineno-12-11" href="#__codelineno-12-11"></a><span class="w"> </span><span class="nx">subjectLine</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;Confirmed: {{SHIFT_TITLE}} on {{SHIFT_DATE}}&#39;</span><span class="p">,</span>
</span><span id="__span-12-12"><a id="__codelineno-12-12" name="__codelineno-12-12" href="#__codelineno-12-12"></a><span class="w"> </span><span class="nx">htmlContent</span><span class="o">:</span><span class="w"> </span><span class="sb">`</span>
</span><span id="__span-12-13"><a id="__codelineno-12-13" name="__codelineno-12-13" href="#__codelineno-12-13"></a><span class="sb"> &lt;p&gt;Dear {{USER_NAME}},&lt;/p&gt;</span>
</span><span id="__span-12-14"><a id="__codelineno-12-14" name="__codelineno-12-14" href="#__codelineno-12-14"></a>
</span><span id="__span-12-15"><a id="__codelineno-12-15" name="__codelineno-12-15" href="#__codelineno-12-15"></a><span class="sb"> &lt;p&gt;Thank you for signing up for &lt;strong&gt;{{SHIFT_TITLE}}&lt;/strong&gt;!&lt;/p&gt;</span>
</span><span id="__span-12-16"><a id="__codelineno-12-16" name="__codelineno-12-16" href="#__codelineno-12-16"></a>
</span><span id="__span-12-17"><a id="__codelineno-12-17" name="__codelineno-12-17" href="#__codelineno-12-17"></a><span class="sb"> &lt;p&gt;&lt;strong&gt;Details:&lt;/strong&gt;&lt;/p&gt;</span>
</span><span id="__span-12-18"><a id="__codelineno-12-18" name="__codelineno-12-18" href="#__codelineno-12-18"></a><span class="sb"> &lt;ul&gt;</span>
</span><span id="__span-12-19"><a id="__codelineno-12-19" name="__codelineno-12-19" href="#__codelineno-12-19"></a><span class="sb"> &lt;li&gt;&lt;strong&gt;Date:&lt;/strong&gt; {{SHIFT_DATE}}&lt;/li&gt;</span>
</span><span id="__span-12-20"><a id="__codelineno-12-20" name="__codelineno-12-20" href="#__codelineno-12-20"></a><span class="sb"> &lt;li&gt;&lt;strong&gt;Time:&lt;/strong&gt; {{SHIFT_TIME}}&lt;/li&gt;</span>
</span><span id="__span-12-21"><a id="__codelineno-12-21" name="__codelineno-12-21" href="#__codelineno-12-21"></a><span class="sb"> &lt;li&gt;&lt;strong&gt;Location:&lt;/strong&gt; {{SHIFT_LOCATION}}&lt;/li&gt;</span>
</span><span id="__span-12-22"><a id="__codelineno-12-22" name="__codelineno-12-22" href="#__codelineno-12-22"></a><span class="sb"> &lt;/ul&gt;</span>
</span><span id="__span-12-23"><a id="__codelineno-12-23" name="__codelineno-12-23" href="#__codelineno-12-23"></a>
</span><span id="__span-12-24"><a id="__codelineno-12-24" name="__codelineno-12-24" href="#__codelineno-12-24"></a><span class="sb"> {{#if HAS_PHONE}}</span>
</span><span id="__span-12-25"><a id="__codelineno-12-25" name="__codelineno-12-25" href="#__codelineno-12-25"></a><span class="sb"> &lt;p&gt;We&#39;ll call you at {{USER_PHONE}} if there are any changes.&lt;/p&gt;</span>
</span><span id="__span-12-26"><a id="__codelineno-12-26" name="__codelineno-12-26" href="#__codelineno-12-26"></a><span class="sb"> {{/if}}</span>
</span><span id="__span-12-27"><a id="__codelineno-12-27" name="__codelineno-12-27" href="#__codelineno-12-27"></a>
</span><span id="__span-12-28"><a id="__codelineno-12-28" name="__codelineno-12-28" href="#__codelineno-12-28"></a><span class="sb"> &lt;p&gt;See you there!&lt;/p&gt;</span>
</span><span id="__span-12-29"><a id="__codelineno-12-29" name="__codelineno-12-29" href="#__codelineno-12-29"></a><span class="sb"> `</span><span class="p">,</span>
</span><span id="__span-12-30"><a id="__codelineno-12-30" name="__codelineno-12-30" href="#__codelineno-12-30"></a><span class="w"> </span><span class="nx">textContent</span><span class="o">:</span><span class="w"> </span><span class="sb">`</span>
</span><span id="__span-12-31"><a id="__codelineno-12-31" name="__codelineno-12-31" href="#__codelineno-12-31"></a><span class="sb">Dear {{USER_NAME}},</span>
</span><span id="__span-12-32"><a id="__codelineno-12-32" name="__codelineno-12-32" href="#__codelineno-12-32"></a>
</span><span id="__span-12-33"><a id="__codelineno-12-33" name="__codelineno-12-33" href="#__codelineno-12-33"></a><span class="sb">Thank you for signing up for {{SHIFT_TITLE}}!</span>
</span><span id="__span-12-34"><a id="__codelineno-12-34" name="__codelineno-12-34" href="#__codelineno-12-34"></a>
</span><span id="__span-12-35"><a id="__codelineno-12-35" name="__codelineno-12-35" href="#__codelineno-12-35"></a><span class="sb">Details:</span>
</span><span id="__span-12-36"><a id="__codelineno-12-36" name="__codelineno-12-36" href="#__codelineno-12-36"></a><span class="sb">- Date: {{SHIFT_DATE}}</span>
</span><span id="__span-12-37"><a id="__codelineno-12-37" name="__codelineno-12-37" href="#__codelineno-12-37"></a><span class="sb">- Time: {{SHIFT_TIME}}</span>
</span><span id="__span-12-38"><a id="__codelineno-12-38" name="__codelineno-12-38" href="#__codelineno-12-38"></a><span class="sb">- Location: {{SHIFT_LOCATION}}</span>
</span><span id="__span-12-39"><a id="__codelineno-12-39" name="__codelineno-12-39" href="#__codelineno-12-39"></a>
</span><span id="__span-12-40"><a id="__codelineno-12-40" name="__codelineno-12-40" href="#__codelineno-12-40"></a><span class="sb">{{#if HAS_PHONE}}</span>
</span><span id="__span-12-41"><a id="__codelineno-12-41" name="__codelineno-12-41" href="#__codelineno-12-41"></a><span class="sb">We&#39;ll call you at {{USER_PHONE}} if there are any changes.</span>
</span><span id="__span-12-42"><a id="__codelineno-12-42" name="__codelineno-12-42" href="#__codelineno-12-42"></a><span class="sb">{{/if}}</span>
</span><span id="__span-12-43"><a id="__codelineno-12-43" name="__codelineno-12-43" href="#__codelineno-12-43"></a>
</span><span id="__span-12-44"><a id="__codelineno-12-44" name="__codelineno-12-44" href="#__codelineno-12-44"></a><span class="sb">See you there!</span>
</span><span id="__span-12-45"><a id="__codelineno-12-45" name="__codelineno-12-45" href="#__codelineno-12-45"></a><span class="sb"> `</span><span class="p">,</span>
</span><span id="__span-12-46"><a id="__codelineno-12-46" name="__codelineno-12-46" href="#__codelineno-12-46"></a><span class="w"> </span><span class="p">},</span>
</span><span id="__span-12-47"><a id="__codelineno-12-47" name="__codelineno-12-47" href="#__codelineno-12-47"></a><span class="p">});</span>
</span></code></pre></div>
<p><strong>Run Seed:</strong>
<div class="language-bash highlight"><pre><span></span><code><span id="__span-13-1"><a id="__codelineno-13-1" name="__codelineno-13-1" href="#__codelineno-13-1"></a>docker<span class="w"> </span>compose<span class="w"> </span><span class="nb">exec</span><span class="w"> </span>api<span class="w"> </span>npx<span class="w"> </span>prisma<span class="w"> </span>db<span class="w"> </span>seed
</span></code></pre></div></p>
<hr />
<h3 id="step-3-define-variables">Step 3: Define Variables<a class="headerlink" href="#step-3-define-variables" title="Permanent link">&para;</a></h3>
<p><strong>Add variables in same seed script:</strong></p>
<div class="language-typescript highlight"><pre><span></span><code><span id="__span-14-1"><a id="__codelineno-14-1" name="__codelineno-14-1" href="#__codelineno-14-1"></a><span class="kd">const</span><span class="w"> </span><span class="nx">template</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">await</span><span class="w"> </span><span class="nx">prisma</span><span class="p">.</span><span class="nx">emailTemplate</span><span class="p">.</span><span class="nx">findUnique</span><span class="p">({</span>
</span><span id="__span-14-2"><a id="__codelineno-14-2" name="__codelineno-14-2" href="#__codelineno-14-2"></a><span class="w"> </span><span class="nx">where</span><span class="o">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">key</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;shift-signup-confirmation&#39;</span><span class="w"> </span><span class="p">},</span>
</span><span id="__span-14-3"><a id="__codelineno-14-3" name="__codelineno-14-3" href="#__codelineno-14-3"></a><span class="p">});</span>
</span><span id="__span-14-4"><a id="__codelineno-14-4" name="__codelineno-14-4" href="#__codelineno-14-4"></a>
</span><span id="__span-14-5"><a id="__codelineno-14-5" name="__codelineno-14-5" href="#__codelineno-14-5"></a><span class="kd">const</span><span class="w"> </span><span class="nx">variables</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span>
</span><span id="__span-14-6"><a id="__codelineno-14-6" name="__codelineno-14-6" href="#__codelineno-14-6"></a><span class="w"> </span><span class="p">{</span>
</span><span id="__span-14-7"><a id="__codelineno-14-7" name="__codelineno-14-7" href="#__codelineno-14-7"></a><span class="w"> </span><span class="nx">key</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;USER_NAME&#39;</span><span class="p">,</span>
</span><span id="__span-14-8"><a id="__codelineno-14-8" name="__codelineno-14-8" href="#__codelineno-14-8"></a><span class="w"> </span><span class="nx">label</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;User Name&#39;</span><span class="p">,</span>
</span><span id="__span-14-9"><a id="__codelineno-14-9" name="__codelineno-14-9" href="#__codelineno-14-9"></a><span class="w"> </span><span class="nx">description</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;Full name of the volunteer&#39;</span><span class="p">,</span>
</span><span id="__span-14-10"><a id="__codelineno-14-10" name="__codelineno-14-10" href="#__codelineno-14-10"></a><span class="w"> </span><span class="nx">isRequired</span><span class="o">:</span><span class="w"> </span><span class="kt">true</span><span class="p">,</span>
</span><span id="__span-14-11"><a id="__codelineno-14-11" name="__codelineno-14-11" href="#__codelineno-14-11"></a><span class="w"> </span><span class="nx">isConditional</span><span class="o">:</span><span class="w"> </span><span class="kt">false</span><span class="p">,</span>
</span><span id="__span-14-12"><a id="__codelineno-14-12" name="__codelineno-14-12" href="#__codelineno-14-12"></a><span class="w"> </span><span class="nx">sampleValue</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;John Doe&#39;</span><span class="p">,</span>
</span><span id="__span-14-13"><a id="__codelineno-14-13" name="__codelineno-14-13" href="#__codelineno-14-13"></a><span class="w"> </span><span class="nx">sortOrder</span><span class="o">:</span><span class="w"> </span><span class="kt">1</span><span class="p">,</span>
</span><span id="__span-14-14"><a id="__codelineno-14-14" name="__codelineno-14-14" href="#__codelineno-14-14"></a><span class="w"> </span><span class="p">},</span>
</span><span id="__span-14-15"><a id="__codelineno-14-15" name="__codelineno-14-15" href="#__codelineno-14-15"></a><span class="w"> </span><span class="p">{</span>
</span><span id="__span-14-16"><a id="__codelineno-14-16" name="__codelineno-14-16" href="#__codelineno-14-16"></a><span class="w"> </span><span class="nx">key</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;SHIFT_TITLE&#39;</span><span class="p">,</span>
</span><span id="__span-14-17"><a id="__codelineno-14-17" name="__codelineno-14-17" href="#__codelineno-14-17"></a><span class="w"> </span><span class="nx">label</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;Shift Title&#39;</span><span class="p">,</span>
</span><span id="__span-14-18"><a id="__codelineno-14-18" name="__codelineno-14-18" href="#__codelineno-14-18"></a><span class="w"> </span><span class="nx">description</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;Name of the shift&#39;</span><span class="p">,</span>
</span><span id="__span-14-19"><a id="__codelineno-14-19" name="__codelineno-14-19" href="#__codelineno-14-19"></a><span class="w"> </span><span class="nx">isRequired</span><span class="o">:</span><span class="w"> </span><span class="kt">true</span><span class="p">,</span>
</span><span id="__span-14-20"><a id="__codelineno-14-20" name="__codelineno-14-20" href="#__codelineno-14-20"></a><span class="w"> </span><span class="nx">isConditional</span><span class="o">:</span><span class="w"> </span><span class="kt">false</span><span class="p">,</span>
</span><span id="__span-14-21"><a id="__codelineno-14-21" name="__codelineno-14-21" href="#__codelineno-14-21"></a><span class="w"> </span><span class="nx">sampleValue</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;Door Knocking - Downtown&#39;</span><span class="p">,</span>
</span><span id="__span-14-22"><a id="__codelineno-14-22" name="__codelineno-14-22" href="#__codelineno-14-22"></a><span class="w"> </span><span class="nx">sortOrder</span><span class="o">:</span><span class="w"> </span><span class="kt">2</span><span class="p">,</span>
</span><span id="__span-14-23"><a id="__codelineno-14-23" name="__codelineno-14-23" href="#__codelineno-14-23"></a><span class="w"> </span><span class="p">},</span>
</span><span id="__span-14-24"><a id="__codelineno-14-24" name="__codelineno-14-24" href="#__codelineno-14-24"></a><span class="w"> </span><span class="p">{</span>
</span><span id="__span-14-25"><a id="__codelineno-14-25" name="__codelineno-14-25" href="#__codelineno-14-25"></a><span class="w"> </span><span class="nx">key</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;SHIFT_DATE&#39;</span><span class="p">,</span>
</span><span id="__span-14-26"><a id="__codelineno-14-26" name="__codelineno-14-26" href="#__codelineno-14-26"></a><span class="w"> </span><span class="nx">label</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;Shift Date&#39;</span><span class="p">,</span>
</span><span id="__span-14-27"><a id="__codelineno-14-27" name="__codelineno-14-27" href="#__codelineno-14-27"></a><span class="w"> </span><span class="nx">description</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;Formatted shift date&#39;</span><span class="p">,</span>
</span><span id="__span-14-28"><a id="__codelineno-14-28" name="__codelineno-14-28" href="#__codelineno-14-28"></a><span class="w"> </span><span class="nx">isRequired</span><span class="o">:</span><span class="w"> </span><span class="kt">true</span><span class="p">,</span>
</span><span id="__span-14-29"><a id="__codelineno-14-29" name="__codelineno-14-29" href="#__codelineno-14-29"></a><span class="w"> </span><span class="nx">isConditional</span><span class="o">:</span><span class="w"> </span><span class="kt">false</span><span class="p">,</span>
</span><span id="__span-14-30"><a id="__codelineno-14-30" name="__codelineno-14-30" href="#__codelineno-14-30"></a><span class="w"> </span><span class="nx">sampleValue</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;Saturday, March 15, 2026&#39;</span><span class="p">,</span>
</span><span id="__span-14-31"><a id="__codelineno-14-31" name="__codelineno-14-31" href="#__codelineno-14-31"></a><span class="w"> </span><span class="nx">sortOrder</span><span class="o">:</span><span class="w"> </span><span class="kt">3</span><span class="p">,</span>
</span><span id="__span-14-32"><a id="__codelineno-14-32" name="__codelineno-14-32" href="#__codelineno-14-32"></a><span class="w"> </span><span class="p">},</span>
</span><span id="__span-14-33"><a id="__codelineno-14-33" name="__codelineno-14-33" href="#__codelineno-14-33"></a><span class="w"> </span><span class="p">{</span>
</span><span id="__span-14-34"><a id="__codelineno-14-34" name="__codelineno-14-34" href="#__codelineno-14-34"></a><span class="w"> </span><span class="nx">key</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;SHIFT_TIME&#39;</span><span class="p">,</span>
</span><span id="__span-14-35"><a id="__codelineno-14-35" name="__codelineno-14-35" href="#__codelineno-14-35"></a><span class="w"> </span><span class="nx">label</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;Shift Time&#39;</span><span class="p">,</span>
</span><span id="__span-14-36"><a id="__codelineno-14-36" name="__codelineno-14-36" href="#__codelineno-14-36"></a><span class="w"> </span><span class="nx">description</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;Shift time range&#39;</span><span class="p">,</span>
</span><span id="__span-14-37"><a id="__codelineno-14-37" name="__codelineno-14-37" href="#__codelineno-14-37"></a><span class="w"> </span><span class="nx">isRequired</span><span class="o">:</span><span class="w"> </span><span class="kt">true</span><span class="p">,</span>
</span><span id="__span-14-38"><a id="__codelineno-14-38" name="__codelineno-14-38" href="#__codelineno-14-38"></a><span class="w"> </span><span class="nx">isConditional</span><span class="o">:</span><span class="w"> </span><span class="kt">false</span><span class="p">,</span>
</span><span id="__span-14-39"><a id="__codelineno-14-39" name="__codelineno-14-39" href="#__codelineno-14-39"></a><span class="w"> </span><span class="nx">sampleValue</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;10:00 AM - 2:00 PM&#39;</span><span class="p">,</span>
</span><span id="__span-14-40"><a id="__codelineno-14-40" name="__codelineno-14-40" href="#__codelineno-14-40"></a><span class="w"> </span><span class="nx">sortOrder</span><span class="o">:</span><span class="w"> </span><span class="kt">4</span><span class="p">,</span>
</span><span id="__span-14-41"><a id="__codelineno-14-41" name="__codelineno-14-41" href="#__codelineno-14-41"></a><span class="w"> </span><span class="p">},</span>
</span><span id="__span-14-42"><a id="__codelineno-14-42" name="__codelineno-14-42" href="#__codelineno-14-42"></a><span class="w"> </span><span class="p">{</span>
</span><span id="__span-14-43"><a id="__codelineno-14-43" name="__codelineno-14-43" href="#__codelineno-14-43"></a><span class="w"> </span><span class="nx">key</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;SHIFT_LOCATION&#39;</span><span class="p">,</span>
</span><span id="__span-14-44"><a id="__codelineno-14-44" name="__codelineno-14-44" href="#__codelineno-14-44"></a><span class="w"> </span><span class="nx">label</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;Shift Location&#39;</span><span class="p">,</span>
</span><span id="__span-14-45"><a id="__codelineno-14-45" name="__codelineno-14-45" href="#__codelineno-14-45"></a><span class="w"> </span><span class="nx">description</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;Meeting location for shift&#39;</span><span class="p">,</span>
</span><span id="__span-14-46"><a id="__codelineno-14-46" name="__codelineno-14-46" href="#__codelineno-14-46"></a><span class="w"> </span><span class="nx">isRequired</span><span class="o">:</span><span class="w"> </span><span class="kt">true</span><span class="p">,</span>
</span><span id="__span-14-47"><a id="__codelineno-14-47" name="__codelineno-14-47" href="#__codelineno-14-47"></a><span class="w"> </span><span class="nx">isConditional</span><span class="o">:</span><span class="w"> </span><span class="kt">false</span><span class="p">,</span>
</span><span id="__span-14-48"><a id="__codelineno-14-48" name="__codelineno-14-48" href="#__codelineno-14-48"></a><span class="w"> </span><span class="nx">sampleValue</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;Campaign Office (123 Main St)&#39;</span><span class="p">,</span>
</span><span id="__span-14-49"><a id="__codelineno-14-49" name="__codelineno-14-49" href="#__codelineno-14-49"></a><span class="w"> </span><span class="nx">sortOrder</span><span class="o">:</span><span class="w"> </span><span class="kt">5</span><span class="p">,</span>
</span><span id="__span-14-50"><a id="__codelineno-14-50" name="__codelineno-14-50" href="#__codelineno-14-50"></a><span class="w"> </span><span class="p">},</span>
</span><span id="__span-14-51"><a id="__codelineno-14-51" name="__codelineno-14-51" href="#__codelineno-14-51"></a><span class="w"> </span><span class="p">{</span>
</span><span id="__span-14-52"><a id="__codelineno-14-52" name="__codelineno-14-52" href="#__codelineno-14-52"></a><span class="w"> </span><span class="nx">key</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;HAS_PHONE&#39;</span><span class="p">,</span>
</span><span id="__span-14-53"><a id="__codelineno-14-53" name="__codelineno-14-53" href="#__codelineno-14-53"></a><span class="w"> </span><span class="nx">label</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;Has Phone&#39;</span><span class="p">,</span>
</span><span id="__span-14-54"><a id="__codelineno-14-54" name="__codelineno-14-54" href="#__codelineno-14-54"></a><span class="w"> </span><span class="nx">description</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;Whether user provided phone number&#39;</span><span class="p">,</span>
</span><span id="__span-14-55"><a id="__codelineno-14-55" name="__codelineno-14-55" href="#__codelineno-14-55"></a><span class="w"> </span><span class="nx">isRequired</span><span class="o">:</span><span class="w"> </span><span class="kt">false</span><span class="p">,</span>
</span><span id="__span-14-56"><a id="__codelineno-14-56" name="__codelineno-14-56" href="#__codelineno-14-56"></a><span class="w"> </span><span class="nx">isConditional</span><span class="o">:</span><span class="w"> </span><span class="kt">true</span><span class="p">,</span>
</span><span id="__span-14-57"><a id="__codelineno-14-57" name="__codelineno-14-57" href="#__codelineno-14-57"></a><span class="w"> </span><span class="nx">sampleValue</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;true&#39;</span><span class="p">,</span>
</span><span id="__span-14-58"><a id="__codelineno-14-58" name="__codelineno-14-58" href="#__codelineno-14-58"></a><span class="w"> </span><span class="nx">sortOrder</span><span class="o">:</span><span class="w"> </span><span class="kt">6</span><span class="p">,</span>
</span><span id="__span-14-59"><a id="__codelineno-14-59" name="__codelineno-14-59" href="#__codelineno-14-59"></a><span class="w"> </span><span class="p">},</span>
</span><span id="__span-14-60"><a id="__codelineno-14-60" name="__codelineno-14-60" href="#__codelineno-14-60"></a><span class="w"> </span><span class="p">{</span>
</span><span id="__span-14-61"><a id="__codelineno-14-61" name="__codelineno-14-61" href="#__codelineno-14-61"></a><span class="w"> </span><span class="nx">key</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;USER_PHONE&#39;</span><span class="p">,</span>
</span><span id="__span-14-62"><a id="__codelineno-14-62" name="__codelineno-14-62" href="#__codelineno-14-62"></a><span class="w"> </span><span class="nx">label</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;User Phone&#39;</span><span class="p">,</span>
</span><span id="__span-14-63"><a id="__codelineno-14-63" name="__codelineno-14-63" href="#__codelineno-14-63"></a><span class="w"> </span><span class="nx">description</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;User phone number (optional)&#39;</span><span class="p">,</span>
</span><span id="__span-14-64"><a id="__codelineno-14-64" name="__codelineno-14-64" href="#__codelineno-14-64"></a><span class="w"> </span><span class="nx">isRequired</span><span class="o">:</span><span class="w"> </span><span class="kt">false</span><span class="p">,</span>
</span><span id="__span-14-65"><a id="__codelineno-14-65" name="__codelineno-14-65" href="#__codelineno-14-65"></a><span class="w"> </span><span class="nx">isConditional</span><span class="o">:</span><span class="w"> </span><span class="kt">false</span><span class="p">,</span>
</span><span id="__span-14-66"><a id="__codelineno-14-66" name="__codelineno-14-66" href="#__codelineno-14-66"></a><span class="w"> </span><span class="nx">sampleValue</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;(555) 123-4567&#39;</span><span class="p">,</span>
</span><span id="__span-14-67"><a id="__codelineno-14-67" name="__codelineno-14-67" href="#__codelineno-14-67"></a><span class="w"> </span><span class="nx">sortOrder</span><span class="o">:</span><span class="w"> </span><span class="kt">7</span><span class="p">,</span>
</span><span id="__span-14-68"><a id="__codelineno-14-68" name="__codelineno-14-68" href="#__codelineno-14-68"></a><span class="w"> </span><span class="p">},</span>
</span><span id="__span-14-69"><a id="__codelineno-14-69" name="__codelineno-14-69" href="#__codelineno-14-69"></a><span class="p">];</span>
</span><span id="__span-14-70"><a id="__codelineno-14-70" name="__codelineno-14-70" href="#__codelineno-14-70"></a>
</span><span id="__span-14-71"><a id="__codelineno-14-71" name="__codelineno-14-71" href="#__codelineno-14-71"></a><span class="k">for</span><span class="w"> </span><span class="p">(</span><span class="kd">const</span><span class="w"> </span><span class="nx">variable</span><span class="w"> </span><span class="k">of</span><span class="w"> </span><span class="nx">variables</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-14-72"><a id="__codelineno-14-72" name="__codelineno-14-72" href="#__codelineno-14-72"></a><span class="w"> </span><span class="k">await</span><span class="w"> </span><span class="nx">prisma</span><span class="p">.</span><span class="nx">emailTemplateVariable</span><span class="p">.</span><span class="nx">upsert</span><span class="p">({</span>
</span><span id="__span-14-73"><a id="__codelineno-14-73" name="__codelineno-14-73" href="#__codelineno-14-73"></a><span class="w"> </span><span class="nx">where</span><span class="o">:</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-14-74"><a id="__codelineno-14-74" name="__codelineno-14-74" href="#__codelineno-14-74"></a><span class="w"> </span><span class="nx">templateId_key</span><span class="o">:</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-14-75"><a id="__codelineno-14-75" name="__codelineno-14-75" href="#__codelineno-14-75"></a><span class="w"> </span><span class="nx">templateId</span><span class="o">:</span><span class="w"> </span><span class="kt">template</span><span class="o">!</span><span class="p">.</span><span class="nx">id</span><span class="p">,</span>
</span><span id="__span-14-76"><a id="__codelineno-14-76" name="__codelineno-14-76" href="#__codelineno-14-76"></a><span class="w"> </span><span class="nx">key</span><span class="o">:</span><span class="w"> </span><span class="kt">variable.key</span><span class="p">,</span>
</span><span id="__span-14-77"><a id="__codelineno-14-77" name="__codelineno-14-77" href="#__codelineno-14-77"></a><span class="w"> </span><span class="p">},</span>
</span><span id="__span-14-78"><a id="__codelineno-14-78" name="__codelineno-14-78" href="#__codelineno-14-78"></a><span class="w"> </span><span class="p">},</span>
</span><span id="__span-14-79"><a id="__codelineno-14-79" name="__codelineno-14-79" href="#__codelineno-14-79"></a><span class="w"> </span><span class="nx">update</span><span class="o">:</span><span class="w"> </span><span class="p">{},</span>
</span><span id="__span-14-80"><a id="__codelineno-14-80" name="__codelineno-14-80" href="#__codelineno-14-80"></a><span class="w"> </span><span class="nx">create</span><span class="o">:</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-14-81"><a id="__codelineno-14-81" name="__codelineno-14-81" href="#__codelineno-14-81"></a><span class="w"> </span><span class="nx">templateId</span><span class="o">:</span><span class="w"> </span><span class="kt">template</span><span class="o">!</span><span class="p">.</span><span class="nx">id</span><span class="p">,</span>
</span><span id="__span-14-82"><a id="__codelineno-14-82" name="__codelineno-14-82" href="#__codelineno-14-82"></a><span class="w"> </span><span class="p">...</span><span class="nx">variable</span><span class="p">,</span>
</span><span id="__span-14-83"><a id="__codelineno-14-83" name="__codelineno-14-83" href="#__codelineno-14-83"></a><span class="w"> </span><span class="p">},</span>
</span><span id="__span-14-84"><a id="__codelineno-14-84" name="__codelineno-14-84" href="#__codelineno-14-84"></a><span class="w"> </span><span class="p">});</span>
</span><span id="__span-14-85"><a id="__codelineno-14-85" name="__codelineno-14-85" href="#__codelineno-14-85"></a><span class="p">}</span>
</span></code></pre></div>
<hr />
<h3 id="step-4-use-in-code">Step 4: Use in Code<a class="headerlink" href="#step-4-use-in-code" title="Permanent link">&para;</a></h3>
<p><strong>Send email from template:</strong></p>
<div class="language-typescript highlight"><pre><span></span><code><span id="__span-15-1"><a id="__codelineno-15-1" name="__codelineno-15-1" href="#__codelineno-15-1"></a><span class="k">import</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">emailService</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="kr">from</span><span class="w"> </span><span class="s1">&#39;@/services/email.service&#39;</span><span class="p">;</span>
</span><span id="__span-15-2"><a id="__codelineno-15-2" name="__codelineno-15-2" href="#__codelineno-15-2"></a>
</span><span id="__span-15-3"><a id="__codelineno-15-3" name="__codelineno-15-3" href="#__codelineno-15-3"></a><span class="k">await</span><span class="w"> </span><span class="nx">emailService</span><span class="p">.</span><span class="nx">sendFromTemplate</span><span class="p">(</span><span class="s1">&#39;shift-signup-confirmation&#39;</span><span class="p">,</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-15-4"><a id="__codelineno-15-4" name="__codelineno-15-4" href="#__codelineno-15-4"></a><span class="w"> </span><span class="nx">recipientEmail</span><span class="o">:</span><span class="w"> </span><span class="kt">volunteer.email</span><span class="p">,</span>
</span><span id="__span-15-5"><a id="__codelineno-15-5" name="__codelineno-15-5" href="#__codelineno-15-5"></a><span class="w"> </span><span class="nx">data</span><span class="o">:</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-15-6"><a id="__codelineno-15-6" name="__codelineno-15-6" href="#__codelineno-15-6"></a><span class="w"> </span><span class="nx">USER_NAME</span><span class="o">:</span><span class="w"> </span><span class="kt">volunteer.name</span><span class="p">,</span>
</span><span id="__span-15-7"><a id="__codelineno-15-7" name="__codelineno-15-7" href="#__codelineno-15-7"></a><span class="w"> </span><span class="nx">SHIFT_TITLE</span><span class="o">:</span><span class="w"> </span><span class="kt">shift.title</span><span class="p">,</span>
</span><span id="__span-15-8"><a id="__codelineno-15-8" name="__codelineno-15-8" href="#__codelineno-15-8"></a><span class="w"> </span><span class="nx">SHIFT_DATE</span><span class="o">:</span><span class="w"> </span><span class="kt">dayjs</span><span class="p">(</span><span class="nx">shift</span><span class="p">.</span><span class="nx">startTime</span><span class="p">).</span><span class="nx">format</span><span class="p">(</span><span class="s1">&#39;dddd, MMMM D, YYYY&#39;</span><span class="p">),</span>
</span><span id="__span-15-9"><a id="__codelineno-15-9" name="__codelineno-15-9" href="#__codelineno-15-9"></a><span class="w"> </span><span class="nx">SHIFT_TIME</span><span class="o">:</span><span class="w"> </span><span class="sb">`</span><span class="si">${</span><span class="nx">dayjs</span><span class="p">(</span><span class="nx">shift</span><span class="p">.</span><span class="nx">startTime</span><span class="p">).</span><span class="nx">format</span><span class="p">(</span><span class="s1">&#39;h:mm A&#39;</span><span class="p">)</span><span class="si">}</span><span class="sb"> - </span><span class="si">${</span><span class="nx">dayjs</span><span class="p">(</span><span class="nx">shift</span><span class="p">.</span><span class="nx">endTime</span><span class="p">).</span><span class="nx">format</span><span class="p">(</span><span class="s1">&#39;h:mm A&#39;</span><span class="p">)</span><span class="si">}</span><span class="sb">`</span><span class="p">,</span>
</span><span id="__span-15-10"><a id="__codelineno-15-10" name="__codelineno-15-10" href="#__codelineno-15-10"></a><span class="w"> </span><span class="nx">SHIFT_LOCATION</span><span class="o">:</span><span class="w"> </span><span class="kt">shift.location</span><span class="p">,</span>
</span><span id="__span-15-11"><a id="__codelineno-15-11" name="__codelineno-15-11" href="#__codelineno-15-11"></a><span class="w"> </span><span class="nx">HAS_PHONE</span><span class="o">:</span><span class="w"> </span><span class="o">!!</span><span class="nx">volunteer</span><span class="p">.</span><span class="nx">phone</span><span class="p">,</span>
</span><span id="__span-15-12"><a id="__codelineno-15-12" name="__codelineno-15-12" href="#__codelineno-15-12"></a><span class="w"> </span><span class="nx">USER_PHONE</span><span class="o">:</span><span class="w"> </span><span class="kt">volunteer.phone</span><span class="w"> </span><span class="o">||</span><span class="w"> </span><span class="s1">&#39;&#39;</span><span class="p">,</span>
</span><span id="__span-15-13"><a id="__codelineno-15-13" name="__codelineno-15-13" href="#__codelineno-15-13"></a><span class="w"> </span><span class="p">},</span>
</span><span id="__span-15-14"><a id="__codelineno-15-14" name="__codelineno-15-14" href="#__codelineno-15-14"></a><span class="p">});</span>
</span></code></pre></div>
<hr />
<h3 id="step-5-document-template">Step 5: Document Template<a class="headerlink" href="#step-5-document-template" title="Permanent link">&para;</a></h3>
<p><strong>Add to API documentation:</strong></p>
<p>Create entry in <code>mkdocs/docs/v2/api/email-templates.md</code>:</p>
<div class="language-markdown highlight"><pre><span></span><code><span id="__span-16-1"><a id="__codelineno-16-1" name="__codelineno-16-1" href="#__codelineno-16-1"></a><span class="gu">## shift-signup-confirmation</span>
</span><span id="__span-16-2"><a id="__codelineno-16-2" name="__codelineno-16-2" href="#__codelineno-16-2"></a>
</span><span id="__span-16-3"><a id="__codelineno-16-3" name="__codelineno-16-3" href="#__codelineno-16-3"></a><span class="gs">**Category:**</span> MAP
</span><span id="__span-16-4"><a id="__codelineno-16-4" name="__codelineno-16-4" href="#__codelineno-16-4"></a><span class="gs">**System:**</span> Yes
</span><span id="__span-16-5"><a id="__codelineno-16-5" name="__codelineno-16-5" href="#__codelineno-16-5"></a>
</span><span id="__span-16-6"><a id="__codelineno-16-6" name="__codelineno-16-6" href="#__codelineno-16-6"></a>Sent when volunteer signs up for a shift.
</span><span id="__span-16-7"><a id="__codelineno-16-7" name="__codelineno-16-7" href="#__codelineno-16-7"></a>
</span><span id="__span-16-8"><a id="__codelineno-16-8" name="__codelineno-16-8" href="#__codelineno-16-8"></a><span class="gs">**Required Variables:**</span>
</span><span id="__span-16-9"><a id="__codelineno-16-9" name="__codelineno-16-9" href="#__codelineno-16-9"></a><span class="k">-</span><span class="w"> </span>USER_NAME, SHIFT_TITLE, SHIFT_DATE, SHIFT_TIME, SHIFT_LOCATION
</span><span id="__span-16-10"><a id="__codelineno-16-10" name="__codelineno-16-10" href="#__codelineno-16-10"></a>
</span><span id="__span-16-11"><a id="__codelineno-16-11" name="__codelineno-16-11" href="#__codelineno-16-11"></a><span class="gs">**Optional Variables:**</span>
</span><span id="__span-16-12"><a id="__codelineno-16-12" name="__codelineno-16-12" href="#__codelineno-16-12"></a><span class="k">-</span><span class="w"> </span>HAS_PHONE (conditional), USER_PHONE
</span><span id="__span-16-13"><a id="__codelineno-16-13" name="__codelineno-16-13" href="#__codelineno-16-13"></a>
</span><span id="__span-16-14"><a id="__codelineno-16-14" name="__codelineno-16-14" href="#__codelineno-16-14"></a><span class="gs">**Usage:**</span>
</span><span id="__span-16-15"><a id="__codelineno-16-15" name="__codelineno-16-15" href="#__codelineno-16-15"></a>\```typescript
</span><span id="__span-16-16"><a id="__codelineno-16-16" name="__codelineno-16-16" href="#__codelineno-16-16"></a>await emailService.sendFromTemplate(&#39;shift-signup-confirmation&#39;, { ... });
</span><span id="__span-16-17"><a id="__codelineno-16-17" name="__codelineno-16-17" href="#__codelineno-16-17"></a>\```
</span></code></pre></div>
<hr />
<h2 id="code-examples">Code Examples<a class="headerlink" href="#code-examples" title="Permanent link">&para;</a></h2>
<h3 id="send-email-from-template">Send Email from Template<a class="headerlink" href="#send-email-from-template" title="Permanent link">&para;</a></h3>
<p><strong>Basic Usage:</strong></p>
<div class="language-typescript highlight"><pre><span></span><code><span id="__span-17-1"><a id="__codelineno-17-1" name="__codelineno-17-1" href="#__codelineno-17-1"></a><span class="k">import</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">emailService</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="kr">from</span><span class="w"> </span><span class="s1">&#39;@/services/email.service&#39;</span><span class="p">;</span>
</span><span id="__span-17-2"><a id="__codelineno-17-2" name="__codelineno-17-2" href="#__codelineno-17-2"></a>
</span><span id="__span-17-3"><a id="__codelineno-17-3" name="__codelineno-17-3" href="#__codelineno-17-3"></a><span class="k">await</span><span class="w"> </span><span class="nx">emailService</span><span class="p">.</span><span class="nx">sendFromTemplate</span><span class="p">(</span><span class="s1">&#39;user-welcome&#39;</span><span class="p">,</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-17-4"><a id="__codelineno-17-4" name="__codelineno-17-4" href="#__codelineno-17-4"></a><span class="w"> </span><span class="nx">recipientEmail</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;newuser@example.com&#39;</span><span class="p">,</span>
</span><span id="__span-17-5"><a id="__codelineno-17-5" name="__codelineno-17-5" href="#__codelineno-17-5"></a><span class="w"> </span><span class="nx">data</span><span class="o">:</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-17-6"><a id="__codelineno-17-6" name="__codelineno-17-6" href="#__codelineno-17-6"></a><span class="w"> </span><span class="nx">USER_NAME</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;Alice Smith&#39;</span><span class="p">,</span>
</span><span id="__span-17-7"><a id="__codelineno-17-7" name="__codelineno-17-7" href="#__codelineno-17-7"></a><span class="w"> </span><span class="nx">USER_EMAIL</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;newuser@example.com&#39;</span><span class="p">,</span>
</span><span id="__span-17-8"><a id="__codelineno-17-8" name="__codelineno-17-8" href="#__codelineno-17-8"></a><span class="w"> </span><span class="nx">VERIFICATION_LINK</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;https://cmlite.org/verify/abc123&#39;</span><span class="p">,</span>
</span><span id="__span-17-9"><a id="__codelineno-17-9" name="__codelineno-17-9" href="#__codelineno-17-9"></a><span class="w"> </span><span class="nx">SITE_NAME</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;Changemaker Lite&#39;</span><span class="p">,</span>
</span><span id="__span-17-10"><a id="__codelineno-17-10" name="__codelineno-17-10" href="#__codelineno-17-10"></a><span class="w"> </span><span class="nx">SITE_URL</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;https://cmlite.org&#39;</span><span class="p">,</span>
</span><span id="__span-17-11"><a id="__codelineno-17-11" name="__codelineno-17-11" href="#__codelineno-17-11"></a><span class="w"> </span><span class="p">},</span>
</span><span id="__span-17-12"><a id="__codelineno-17-12" name="__codelineno-17-12" href="#__codelineno-17-12"></a><span class="p">});</span>
</span></code></pre></div>
<p><strong>With Conditional Variables:</strong></p>
<div class="language-typescript highlight"><pre><span></span><code><span id="__span-18-1"><a id="__codelineno-18-1" name="__codelineno-18-1" href="#__codelineno-18-1"></a><span class="k">await</span><span class="w"> </span><span class="nx">emailService</span><span class="p">.</span><span class="nx">sendFromTemplate</span><span class="p">(</span><span class="s1">&#39;response-verification&#39;</span><span class="p">,</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-18-2"><a id="__codelineno-18-2" name="__codelineno-18-2" href="#__codelineno-18-2"></a><span class="w"> </span><span class="nx">recipientEmail</span><span class="o">:</span><span class="w"> </span><span class="kt">participant.email</span><span class="p">,</span>
</span><span id="__span-18-3"><a id="__codelineno-18-3" name="__codelineno-18-3" href="#__codelineno-18-3"></a><span class="w"> </span><span class="nx">data</span><span class="o">:</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-18-4"><a id="__codelineno-18-4" name="__codelineno-18-4" href="#__codelineno-18-4"></a><span class="w"> </span><span class="nx">USER_NAME</span><span class="o">:</span><span class="w"> </span><span class="kt">participant.name</span><span class="p">,</span>
</span><span id="__span-18-5"><a id="__codelineno-18-5" name="__codelineno-18-5" href="#__codelineno-18-5"></a><span class="w"> </span><span class="nx">CAMPAIGN_TITLE</span><span class="o">:</span><span class="w"> </span><span class="kt">campaign.title</span><span class="p">,</span>
</span><span id="__span-18-6"><a id="__codelineno-18-6" name="__codelineno-18-6" href="#__codelineno-18-6"></a><span class="w"> </span><span class="nx">RESPONSE_TEXT</span><span class="o">:</span><span class="w"> </span><span class="kt">response.content</span><span class="p">,</span>
</span><span id="__span-18-7"><a id="__codelineno-18-7" name="__codelineno-18-7" href="#__codelineno-18-7"></a><span class="w"> </span><span class="nx">VERIFICATION_LINK</span><span class="o">:</span><span class="w"> </span><span class="sb">`https://cmlite.org/responses/verify/</span><span class="si">${</span><span class="nx">response</span><span class="p">.</span><span class="nx">verificationToken</span><span class="si">}</span><span class="sb">`</span><span class="p">,</span>
</span><span id="__span-18-8"><a id="__codelineno-18-8" name="__codelineno-18-8" href="#__codelineno-18-8"></a><span class="w"> </span><span class="nx">HAS_CUSTOM_MESSAGE</span><span class="o">:</span><span class="w"> </span><span class="o">!!</span><span class="nx">response</span><span class="p">.</span><span class="nx">customMessage</span><span class="p">,</span>
</span><span id="__span-18-9"><a id="__codelineno-18-9" name="__codelineno-18-9" href="#__codelineno-18-9"></a><span class="w"> </span><span class="nx">CUSTOM_MESSAGE</span><span class="o">:</span><span class="w"> </span><span class="kt">response.customMessage</span><span class="w"> </span><span class="o">||</span><span class="w"> </span><span class="s1">&#39;&#39;</span><span class="p">,</span>
</span><span id="__span-18-10"><a id="__codelineno-18-10" name="__codelineno-18-10" href="#__codelineno-18-10"></a><span class="w"> </span><span class="p">},</span>
</span><span id="__span-18-11"><a id="__codelineno-18-11" name="__codelineno-18-11" href="#__codelineno-18-11"></a><span class="p">});</span>
</span></code></pre></div>
<p><strong>With Loops (Array Variables):</strong></p>
<div class="language-typescript highlight"><pre><span></span><code><span id="__span-19-1"><a id="__codelineno-19-1" name="__codelineno-19-1" href="#__codelineno-19-1"></a><span class="k">await</span><span class="w"> </span><span class="nx">emailService</span><span class="p">.</span><span class="nx">sendFromTemplate</span><span class="p">(</span><span class="s1">&#39;campaign-email&#39;</span><span class="p">,</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-19-2"><a id="__codelineno-19-2" name="__codelineno-19-2" href="#__codelineno-19-2"></a><span class="w"> </span><span class="nx">recipientEmail</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;representative@parliament.ca&#39;</span><span class="p">,</span>
</span><span id="__span-19-3"><a id="__codelineno-19-3" name="__codelineno-19-3" href="#__codelineno-19-3"></a><span class="w"> </span><span class="nx">data</span><span class="o">:</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-19-4"><a id="__codelineno-19-4" name="__codelineno-19-4" href="#__codelineno-19-4"></a><span class="w"> </span><span class="nx">USER_NAME</span><span class="o">:</span><span class="w"> </span><span class="kt">participant.name</span><span class="p">,</span>
</span><span id="__span-19-5"><a id="__codelineno-19-5" name="__codelineno-19-5" href="#__codelineno-19-5"></a><span class="w"> </span><span class="nx">USER_EMAIL</span><span class="o">:</span><span class="w"> </span><span class="kt">participant.email</span><span class="p">,</span>
</span><span id="__span-19-6"><a id="__codelineno-19-6" name="__codelineno-19-6" href="#__codelineno-19-6"></a><span class="w"> </span><span class="nx">CAMPAIGN_TITLE</span><span class="o">:</span><span class="w"> </span><span class="kt">campaign.title</span><span class="p">,</span>
</span><span id="__span-19-7"><a id="__codelineno-19-7" name="__codelineno-19-7" href="#__codelineno-19-7"></a><span class="w"> </span><span class="nx">CUSTOM_MESSAGE</span><span class="o">:</span><span class="w"> </span><span class="kt">emailData.customMessage</span><span class="p">,</span>
</span><span id="__span-19-8"><a id="__codelineno-19-8" name="__codelineno-19-8" href="#__codelineno-19-8"></a><span class="w"> </span><span class="nx">REPRESENTATIVES</span><span class="o">:</span><span class="w"> </span><span class="kt">emailData.representatives.map</span><span class="p">(</span><span class="nx">rep</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p">({</span>
</span><span id="__span-19-9"><a id="__codelineno-19-9" name="__codelineno-19-9" href="#__codelineno-19-9"></a><span class="w"> </span><span class="nx">name</span><span class="o">:</span><span class="w"> </span><span class="kt">rep.name</span><span class="p">,</span>
</span><span id="__span-19-10"><a id="__codelineno-19-10" name="__codelineno-19-10" href="#__codelineno-19-10"></a><span class="w"> </span><span class="nx">title</span><span class="o">:</span><span class="w"> </span><span class="kt">rep.title</span><span class="p">,</span>
</span><span id="__span-19-11"><a id="__codelineno-19-11" name="__codelineno-19-11" href="#__codelineno-19-11"></a><span class="w"> </span><span class="nx">email</span><span class="o">:</span><span class="w"> </span><span class="kt">rep.email</span><span class="p">,</span>
</span><span id="__span-19-12"><a id="__codelineno-19-12" name="__codelineno-19-12" href="#__codelineno-19-12"></a><span class="w"> </span><span class="p">})),</span>
</span><span id="__span-19-13"><a id="__codelineno-19-13" name="__codelineno-19-13" href="#__codelineno-19-13"></a><span class="w"> </span><span class="p">},</span>
</span><span id="__span-19-14"><a id="__codelineno-19-14" name="__codelineno-19-14" href="#__codelineno-19-14"></a><span class="p">});</span>
</span></code></pre></div>
<hr />
<h3 id="template-service-implementation">Template Service Implementation<a class="headerlink" href="#template-service-implementation" title="Permanent link">&para;</a></h3>
<p><strong>Core sendFromTemplate Method:</strong></p>
<div class="language-typescript highlight"><pre><span></span><code><span id="__span-20-1"><a id="__codelineno-20-1" name="__codelineno-20-1" href="#__codelineno-20-1"></a><span class="c1">// api/src/services/email.service.ts</span>
</span><span id="__span-20-2"><a id="__codelineno-20-2" name="__codelineno-20-2" href="#__codelineno-20-2"></a>
</span><span id="__span-20-3"><a id="__codelineno-20-3" name="__codelineno-20-3" href="#__codelineno-20-3"></a><span class="k">import</span><span class="w"> </span><span class="nx">Handlebars</span><span class="w"> </span><span class="kr">from</span><span class="w"> </span><span class="s1">&#39;handlebars&#39;</span><span class="p">;</span>
</span><span id="__span-20-4"><a id="__codelineno-20-4" name="__codelineno-20-4" href="#__codelineno-20-4"></a><span class="k">import</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">prisma</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="kr">from</span><span class="w"> </span><span class="s1">&#39;@/config/database&#39;</span><span class="p">;</span>
</span><span id="__span-20-5"><a id="__codelineno-20-5" name="__codelineno-20-5" href="#__codelineno-20-5"></a><span class="k">import</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">EmailTemplateNotFoundError</span><span class="p">,</span><span class="w"> </span><span class="nx">MissingRequiredVariableError</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="kr">from</span><span class="w"> </span><span class="s1">&#39;@/utils/errors&#39;</span><span class="p">;</span>
</span><span id="__span-20-6"><a id="__codelineno-20-6" name="__codelineno-20-6" href="#__codelineno-20-6"></a>
</span><span id="__span-20-7"><a id="__codelineno-20-7" name="__codelineno-20-7" href="#__codelineno-20-7"></a><span class="kd">class</span><span class="w"> </span><span class="nx">EmailService</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-20-8"><a id="__codelineno-20-8" name="__codelineno-20-8" href="#__codelineno-20-8"></a><span class="w"> </span><span class="k">async</span><span class="w"> </span><span class="nx">sendFromTemplate</span><span class="p">(</span>
</span><span id="__span-20-9"><a id="__codelineno-20-9" name="__codelineno-20-9" href="#__codelineno-20-9"></a><span class="w"> </span><span class="nx">templateKey</span><span class="o">:</span><span class="w"> </span><span class="kt">string</span><span class="p">,</span>
</span><span id="__span-20-10"><a id="__codelineno-20-10" name="__codelineno-20-10" href="#__codelineno-20-10"></a><span class="w"> </span><span class="nx">options</span><span class="o">:</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-20-11"><a id="__codelineno-20-11" name="__codelineno-20-11" href="#__codelineno-20-11"></a><span class="w"> </span><span class="nx">recipientEmail</span><span class="o">:</span><span class="w"> </span><span class="kt">string</span><span class="p">;</span>
</span><span id="__span-20-12"><a id="__codelineno-20-12" name="__codelineno-20-12" href="#__codelineno-20-12"></a><span class="w"> </span><span class="nx">data</span><span class="o">:</span><span class="w"> </span><span class="kt">Record</span><span class="o">&lt;</span><span class="kt">string</span><span class="p">,</span><span class="w"> </span><span class="nx">unknown</span><span class="o">&gt;</span><span class="p">;</span>
</span><span id="__span-20-13"><a id="__codelineno-20-13" name="__codelineno-20-13" href="#__codelineno-20-13"></a><span class="w"> </span><span class="nx">attachments?</span><span class="o">:</span><span class="w"> </span><span class="kt">Array</span><span class="o">&lt;</span><span class="p">{</span><span class="w"> </span><span class="nx">filename</span><span class="o">:</span><span class="w"> </span><span class="kt">string</span><span class="p">;</span><span class="w"> </span><span class="nx">path</span><span class="o">:</span><span class="w"> </span><span class="kt">string</span><span class="w"> </span><span class="p">}</span><span class="o">&gt;</span><span class="p">;</span>
</span><span id="__span-20-14"><a id="__codelineno-20-14" name="__codelineno-20-14" href="#__codelineno-20-14"></a><span class="w"> </span><span class="p">}</span>
</span><span id="__span-20-15"><a id="__codelineno-20-15" name="__codelineno-20-15" href="#__codelineno-20-15"></a><span class="w"> </span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-20-16"><a id="__codelineno-20-16" name="__codelineno-20-16" href="#__codelineno-20-16"></a><span class="w"> </span><span class="c1">// 1. Load template with variables</span>
</span><span id="__span-20-17"><a id="__codelineno-20-17" name="__codelineno-20-17" href="#__codelineno-20-17"></a><span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">template</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">await</span><span class="w"> </span><span class="nx">prisma</span><span class="p">.</span><span class="nx">emailTemplate</span><span class="p">.</span><span class="nx">findUnique</span><span class="p">({</span>
</span><span id="__span-20-18"><a id="__codelineno-20-18" name="__codelineno-20-18" href="#__codelineno-20-18"></a><span class="w"> </span><span class="nx">where</span><span class="o">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">key</span><span class="o">:</span><span class="w"> </span><span class="kt">templateKey</span><span class="p">,</span><span class="w"> </span><span class="nx">isActive</span><span class="o">:</span><span class="w"> </span><span class="kt">true</span><span class="w"> </span><span class="p">},</span>
</span><span id="__span-20-19"><a id="__codelineno-20-19" name="__codelineno-20-19" href="#__codelineno-20-19"></a><span class="w"> </span><span class="nx">include</span><span class="o">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">variables</span><span class="o">:</span><span class="w"> </span><span class="kt">true</span><span class="w"> </span><span class="p">},</span>
</span><span id="__span-20-20"><a id="__codelineno-20-20" name="__codelineno-20-20" href="#__codelineno-20-20"></a><span class="w"> </span><span class="p">});</span>
</span><span id="__span-20-21"><a id="__codelineno-20-21" name="__codelineno-20-21" href="#__codelineno-20-21"></a>
</span><span id="__span-20-22"><a id="__codelineno-20-22" name="__codelineno-20-22" href="#__codelineno-20-22"></a><span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="o">!</span><span class="nx">template</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-20-23"><a id="__codelineno-20-23" name="__codelineno-20-23" href="#__codelineno-20-23"></a><span class="w"> </span><span class="k">throw</span><span class="w"> </span><span class="ow">new</span><span class="w"> </span><span class="nx">EmailTemplateNotFoundError</span><span class="p">(</span><span class="sb">`Template not found or inactive: </span><span class="si">${</span><span class="nx">templateKey</span><span class="si">}</span><span class="sb">`</span><span class="p">);</span>
</span><span id="__span-20-24"><a id="__codelineno-20-24" name="__codelineno-20-24" href="#__codelineno-20-24"></a><span class="w"> </span><span class="p">}</span>
</span><span id="__span-20-25"><a id="__codelineno-20-25" name="__codelineno-20-25" href="#__codelineno-20-25"></a>
</span><span id="__span-20-26"><a id="__codelineno-20-26" name="__codelineno-20-26" href="#__codelineno-20-26"></a><span class="w"> </span><span class="c1">// 2. Validate required variables</span>
</span><span id="__span-20-27"><a id="__codelineno-20-27" name="__codelineno-20-27" href="#__codelineno-20-27"></a><span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">requiredVars</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">template</span><span class="p">.</span><span class="nx">variables</span><span class="p">.</span><span class="nx">filter</span><span class="p">(</span><span class="nx">v</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="nx">v</span><span class="p">.</span><span class="nx">isRequired</span><span class="p">);</span>
</span><span id="__span-20-28"><a id="__codelineno-20-28" name="__codelineno-20-28" href="#__codelineno-20-28"></a><span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">missingVars</span><span class="o">:</span><span class="w"> </span><span class="kt">string</span><span class="p">[]</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[];</span>
</span><span id="__span-20-29"><a id="__codelineno-20-29" name="__codelineno-20-29" href="#__codelineno-20-29"></a>
</span><span id="__span-20-30"><a id="__codelineno-20-30" name="__codelineno-20-30" href="#__codelineno-20-30"></a><span class="w"> </span><span class="k">for</span><span class="w"> </span><span class="p">(</span><span class="kd">const</span><span class="w"> </span><span class="nx">variable</span><span class="w"> </span><span class="k">of</span><span class="w"> </span><span class="nx">requiredVars</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-20-31"><a id="__codelineno-20-31" name="__codelineno-20-31" href="#__codelineno-20-31"></a><span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="nx">options</span><span class="p">.</span><span class="nx">data</span><span class="p">[</span><span class="nx">variable</span><span class="p">.</span><span class="nx">key</span><span class="p">]</span><span class="w"> </span><span class="o">===</span><span class="w"> </span><span class="kc">undefined</span><span class="w"> </span><span class="o">||</span><span class="w"> </span><span class="nx">options</span><span class="p">.</span><span class="nx">data</span><span class="p">[</span><span class="nx">variable</span><span class="p">.</span><span class="nx">key</span><span class="p">]</span><span class="w"> </span><span class="o">===</span><span class="w"> </span><span class="kc">null</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-20-32"><a id="__codelineno-20-32" name="__codelineno-20-32" href="#__codelineno-20-32"></a><span class="w"> </span><span class="nx">missingVars</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="nx">variable</span><span class="p">.</span><span class="nx">key</span><span class="p">);</span>
</span><span id="__span-20-33"><a id="__codelineno-20-33" name="__codelineno-20-33" href="#__codelineno-20-33"></a><span class="w"> </span><span class="p">}</span>
</span><span id="__span-20-34"><a id="__codelineno-20-34" name="__codelineno-20-34" href="#__codelineno-20-34"></a><span class="w"> </span><span class="p">}</span>
</span><span id="__span-20-35"><a id="__codelineno-20-35" name="__codelineno-20-35" href="#__codelineno-20-35"></a>
</span><span id="__span-20-36"><a id="__codelineno-20-36" name="__codelineno-20-36" href="#__codelineno-20-36"></a><span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="nx">missingVars</span><span class="p">.</span><span class="nx">length</span><span class="w"> </span><span class="o">&gt;</span><span class="w"> </span><span class="mf">0</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-20-37"><a id="__codelineno-20-37" name="__codelineno-20-37" href="#__codelineno-20-37"></a><span class="w"> </span><span class="k">throw</span><span class="w"> </span><span class="ow">new</span><span class="w"> </span><span class="nx">MissingRequiredVariableError</span><span class="p">(</span>
</span><span id="__span-20-38"><a id="__codelineno-20-38" name="__codelineno-20-38" href="#__codelineno-20-38"></a><span class="w"> </span><span class="sb">`Missing required variables for template </span><span class="si">${</span><span class="nx">templateKey</span><span class="si">}</span><span class="sb">: </span><span class="si">${</span><span class="nx">missingVars</span><span class="p">.</span><span class="nx">join</span><span class="p">(</span><span class="s1">&#39;, &#39;</span><span class="p">)</span><span class="si">}</span><span class="sb">`</span>
</span><span id="__span-20-39"><a id="__codelineno-20-39" name="__codelineno-20-39" href="#__codelineno-20-39"></a><span class="w"> </span><span class="p">);</span>
</span><span id="__span-20-40"><a id="__codelineno-20-40" name="__codelineno-20-40" href="#__codelineno-20-40"></a><span class="w"> </span><span class="p">}</span>
</span><span id="__span-20-41"><a id="__codelineno-20-41" name="__codelineno-20-41" href="#__codelineno-20-41"></a>
</span><span id="__span-20-42"><a id="__codelineno-20-42" name="__codelineno-20-42" href="#__codelineno-20-42"></a><span class="w"> </span><span class="c1">// 3. Compile Handlebars templates</span>
</span><span id="__span-20-43"><a id="__codelineno-20-43" name="__codelineno-20-43" href="#__codelineno-20-43"></a><span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">compiledSubject</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">Handlebars</span><span class="p">.</span><span class="nx">compile</span><span class="p">(</span><span class="nx">template</span><span class="p">.</span><span class="nx">subjectLine</span><span class="p">);</span>
</span><span id="__span-20-44"><a id="__codelineno-20-44" name="__codelineno-20-44" href="#__codelineno-20-44"></a><span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">compiledHtml</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">Handlebars</span><span class="p">.</span><span class="nx">compile</span><span class="p">(</span><span class="nx">template</span><span class="p">.</span><span class="nx">htmlContent</span><span class="p">);</span>
</span><span id="__span-20-45"><a id="__codelineno-20-45" name="__codelineno-20-45" href="#__codelineno-20-45"></a><span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">compiledText</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">Handlebars</span><span class="p">.</span><span class="nx">compile</span><span class="p">(</span><span class="nx">template</span><span class="p">.</span><span class="nx">textContent</span><span class="p">);</span>
</span><span id="__span-20-46"><a id="__codelineno-20-46" name="__codelineno-20-46" href="#__codelineno-20-46"></a>
</span><span id="__span-20-47"><a id="__codelineno-20-47" name="__codelineno-20-47" href="#__codelineno-20-47"></a><span class="w"> </span><span class="c1">// 4. Interpolate variables</span>
</span><span id="__span-20-48"><a id="__codelineno-20-48" name="__codelineno-20-48" href="#__codelineno-20-48"></a><span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">subject</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">compiledSubject</span><span class="p">(</span><span class="nx">options</span><span class="p">.</span><span class="nx">data</span><span class="p">);</span>
</span><span id="__span-20-49"><a id="__codelineno-20-49" name="__codelineno-20-49" href="#__codelineno-20-49"></a><span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">html</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">compiledHtml</span><span class="p">(</span><span class="nx">options</span><span class="p">.</span><span class="nx">data</span><span class="p">);</span>
</span><span id="__span-20-50"><a id="__codelineno-20-50" name="__codelineno-20-50" href="#__codelineno-20-50"></a><span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">text</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">compiledText</span><span class="p">(</span><span class="nx">options</span><span class="p">.</span><span class="nx">data</span><span class="p">);</span>
</span><span id="__span-20-51"><a id="__codelineno-20-51" name="__codelineno-20-51" href="#__codelineno-20-51"></a>
</span><span id="__span-20-52"><a id="__codelineno-20-52" name="__codelineno-20-52" href="#__codelineno-20-52"></a><span class="w"> </span><span class="c1">// 5. Send via Nodemailer</span>
</span><span id="__span-20-53"><a id="__codelineno-20-53" name="__codelineno-20-53" href="#__codelineno-20-53"></a><span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">result</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">await</span><span class="w"> </span><span class="k">this</span><span class="p">.</span><span class="nx">send</span><span class="p">({</span>
</span><span id="__span-20-54"><a id="__codelineno-20-54" name="__codelineno-20-54" href="#__codelineno-20-54"></a><span class="w"> </span><span class="nx">to</span><span class="o">:</span><span class="w"> </span><span class="kt">options.recipientEmail</span><span class="p">,</span>
</span><span id="__span-20-55"><a id="__codelineno-20-55" name="__codelineno-20-55" href="#__codelineno-20-55"></a><span class="w"> </span><span class="nx">subject</span><span class="p">,</span>
</span><span id="__span-20-56"><a id="__codelineno-20-56" name="__codelineno-20-56" href="#__codelineno-20-56"></a><span class="w"> </span><span class="nx">html</span><span class="p">,</span>
</span><span id="__span-20-57"><a id="__codelineno-20-57" name="__codelineno-20-57" href="#__codelineno-20-57"></a><span class="w"> </span><span class="nx">text</span><span class="p">,</span>
</span><span id="__span-20-58"><a id="__codelineno-20-58" name="__codelineno-20-58" href="#__codelineno-20-58"></a><span class="w"> </span><span class="nx">attachments</span><span class="o">:</span><span class="w"> </span><span class="kt">options.attachments</span><span class="p">,</span>
</span><span id="__span-20-59"><a id="__codelineno-20-59" name="__codelineno-20-59" href="#__codelineno-20-59"></a><span class="w"> </span><span class="p">});</span>
</span><span id="__span-20-60"><a id="__codelineno-20-60" name="__codelineno-20-60" href="#__codelineno-20-60"></a>
</span><span id="__span-20-61"><a id="__codelineno-20-61" name="__codelineno-20-61" href="#__codelineno-20-61"></a><span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="nx">result</span><span class="p">;</span>
</span><span id="__span-20-62"><a id="__codelineno-20-62" name="__codelineno-20-62" href="#__codelineno-20-62"></a><span class="w"> </span><span class="p">}</span>
</span><span id="__span-20-63"><a id="__codelineno-20-63" name="__codelineno-20-63" href="#__codelineno-20-63"></a>
</span><span id="__span-20-64"><a id="__codelineno-20-64" name="__codelineno-20-64" href="#__codelineno-20-64"></a><span class="w"> </span><span class="k">private</span><span class="w"> </span><span class="k">async</span><span class="w"> </span><span class="nx">send</span><span class="p">(</span><span class="nx">options</span><span class="o">:</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-20-65"><a id="__codelineno-20-65" name="__codelineno-20-65" href="#__codelineno-20-65"></a><span class="w"> </span><span class="nx">to</span><span class="o">:</span><span class="w"> </span><span class="kt">string</span><span class="p">;</span>
</span><span id="__span-20-66"><a id="__codelineno-20-66" name="__codelineno-20-66" href="#__codelineno-20-66"></a><span class="w"> </span><span class="nx">subject</span><span class="o">:</span><span class="w"> </span><span class="kt">string</span><span class="p">;</span>
</span><span id="__span-20-67"><a id="__codelineno-20-67" name="__codelineno-20-67" href="#__codelineno-20-67"></a><span class="w"> </span><span class="nx">html</span><span class="o">:</span><span class="w"> </span><span class="kt">string</span><span class="p">;</span>
</span><span id="__span-20-68"><a id="__codelineno-20-68" name="__codelineno-20-68" href="#__codelineno-20-68"></a><span class="w"> </span><span class="nx">text</span><span class="o">:</span><span class="w"> </span><span class="kt">string</span><span class="p">;</span>
</span><span id="__span-20-69"><a id="__codelineno-20-69" name="__codelineno-20-69" href="#__codelineno-20-69"></a><span class="w"> </span><span class="nx">attachments?</span><span class="o">:</span><span class="w"> </span><span class="kt">Array</span><span class="o">&lt;</span><span class="p">{</span><span class="w"> </span><span class="nx">filename</span><span class="o">:</span><span class="w"> </span><span class="kt">string</span><span class="p">;</span><span class="w"> </span><span class="nx">path</span><span class="o">:</span><span class="w"> </span><span class="kt">string</span><span class="w"> </span><span class="p">}</span><span class="o">&gt;</span><span class="p">;</span>
</span><span id="__span-20-70"><a id="__codelineno-20-70" name="__codelineno-20-70" href="#__codelineno-20-70"></a><span class="w"> </span><span class="p">})</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-20-71"><a id="__codelineno-20-71" name="__codelineno-20-71" href="#__codelineno-20-71"></a><span class="w"> </span><span class="c1">// Nodemailer implementation</span>
</span><span id="__span-20-72"><a id="__codelineno-20-72" name="__codelineno-20-72" href="#__codelineno-20-72"></a><span class="w"> </span><span class="c1">// See api/src/services/email.service.ts for full implementation</span>
</span><span id="__span-20-73"><a id="__codelineno-20-73" name="__codelineno-20-73" href="#__codelineno-20-73"></a><span class="w"> </span><span class="p">}</span>
</span><span id="__span-20-74"><a id="__codelineno-20-74" name="__codelineno-20-74" href="#__codelineno-20-74"></a><span class="p">}</span>
</span><span id="__span-20-75"><a id="__codelineno-20-75" name="__codelineno-20-75" href="#__codelineno-20-75"></a>
</span><span id="__span-20-76"><a id="__codelineno-20-76" name="__codelineno-20-76" href="#__codelineno-20-76"></a><span class="k">export</span><span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">emailService</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="ow">new</span><span class="w"> </span><span class="nx">EmailService</span><span class="p">();</span>
</span></code></pre></div>
<hr />
<h3 id="handlebars-helper-registration">Handlebars Helper Registration<a class="headerlink" href="#handlebars-helper-registration" title="Permanent link">&para;</a></h3>
<p><strong>Register custom helpers for common formatting:</strong></p>
<div class="language-typescript highlight"><pre><span></span><code><span id="__span-21-1"><a id="__codelineno-21-1" name="__codelineno-21-1" href="#__codelineno-21-1"></a><span class="c1">// api/src/services/email.service.ts</span>
</span><span id="__span-21-2"><a id="__codelineno-21-2" name="__codelineno-21-2" href="#__codelineno-21-2"></a>
</span><span id="__span-21-3"><a id="__codelineno-21-3" name="__codelineno-21-3" href="#__codelineno-21-3"></a><span class="k">import</span><span class="w"> </span><span class="nx">Handlebars</span><span class="w"> </span><span class="kr">from</span><span class="w"> </span><span class="s1">&#39;handlebars&#39;</span><span class="p">;</span>
</span><span id="__span-21-4"><a id="__codelineno-21-4" name="__codelineno-21-4" href="#__codelineno-21-4"></a><span class="k">import</span><span class="w"> </span><span class="nx">dayjs</span><span class="w"> </span><span class="kr">from</span><span class="w"> </span><span class="s1">&#39;dayjs&#39;</span><span class="p">;</span>
</span><span id="__span-21-5"><a id="__codelineno-21-5" name="__codelineno-21-5" href="#__codelineno-21-5"></a>
</span><span id="__span-21-6"><a id="__codelineno-21-6" name="__codelineno-21-6" href="#__codelineno-21-6"></a><span class="c1">// Date formatting helper</span>
</span><span id="__span-21-7"><a id="__codelineno-21-7" name="__codelineno-21-7" href="#__codelineno-21-7"></a><span class="nx">Handlebars</span><span class="p">.</span><span class="nx">registerHelper</span><span class="p">(</span><span class="s1">&#39;formatDate&#39;</span><span class="p">,</span><span class="w"> </span><span class="p">(</span><span class="nx">date</span><span class="o">:</span><span class="w"> </span><span class="kt">string</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="nb">Date</span><span class="p">,</span><span class="w"> </span><span class="nx">format</span><span class="o">:</span><span class="w"> </span><span class="kt">string</span><span class="p">)</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-21-8"><a id="__codelineno-21-8" name="__codelineno-21-8" href="#__codelineno-21-8"></a><span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="nx">dayjs</span><span class="p">(</span><span class="nx">date</span><span class="p">).</span><span class="nx">format</span><span class="p">(</span><span class="nx">format</span><span class="p">);</span>
</span><span id="__span-21-9"><a id="__codelineno-21-9" name="__codelineno-21-9" href="#__codelineno-21-9"></a><span class="p">});</span>
</span><span id="__span-21-10"><a id="__codelineno-21-10" name="__codelineno-21-10" href="#__codelineno-21-10"></a>
</span><span id="__span-21-11"><a id="__codelineno-21-11" name="__codelineno-21-11" href="#__codelineno-21-11"></a><span class="c1">// Currency formatting helper</span>
</span><span id="__span-21-12"><a id="__codelineno-21-12" name="__codelineno-21-12" href="#__codelineno-21-12"></a><span class="nx">Handlebars</span><span class="p">.</span><span class="nx">registerHelper</span><span class="p">(</span><span class="s1">&#39;currency&#39;</span><span class="p">,</span><span class="w"> </span><span class="p">(</span><span class="nx">amount</span><span class="o">:</span><span class="w"> </span><span class="kt">number</span><span class="p">)</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-21-13"><a id="__codelineno-21-13" name="__codelineno-21-13" href="#__codelineno-21-13"></a><span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="ow">new</span><span class="w"> </span><span class="nb">Intl</span><span class="p">.</span><span class="nx">NumberFormat</span><span class="p">(</span><span class="s1">&#39;en-CA&#39;</span><span class="p">,</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-21-14"><a id="__codelineno-21-14" name="__codelineno-21-14" href="#__codelineno-21-14"></a><span class="w"> </span><span class="nx">style</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;currency&#39;</span><span class="p">,</span>
</span><span id="__span-21-15"><a id="__codelineno-21-15" name="__codelineno-21-15" href="#__codelineno-21-15"></a><span class="w"> </span><span class="nx">currency</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;CAD&#39;</span><span class="p">,</span>
</span><span id="__span-21-16"><a id="__codelineno-21-16" name="__codelineno-21-16" href="#__codelineno-21-16"></a><span class="w"> </span><span class="p">}).</span><span class="nx">format</span><span class="p">(</span><span class="nx">amount</span><span class="p">);</span>
</span><span id="__span-21-17"><a id="__codelineno-21-17" name="__codelineno-21-17" href="#__codelineno-21-17"></a><span class="p">});</span>
</span><span id="__span-21-18"><a id="__codelineno-21-18" name="__codelineno-21-18" href="#__codelineno-21-18"></a>
</span><span id="__span-21-19"><a id="__codelineno-21-19" name="__codelineno-21-19" href="#__codelineno-21-19"></a><span class="c1">// Pluralize helper</span>
</span><span id="__span-21-20"><a id="__codelineno-21-20" name="__codelineno-21-20" href="#__codelineno-21-20"></a><span class="nx">Handlebars</span><span class="p">.</span><span class="nx">registerHelper</span><span class="p">(</span><span class="s1">&#39;pluralize&#39;</span><span class="p">,</span><span class="w"> </span><span class="p">(</span><span class="nx">count</span><span class="o">:</span><span class="w"> </span><span class="kt">number</span><span class="p">,</span><span class="w"> </span><span class="nx">singular</span><span class="o">:</span><span class="w"> </span><span class="kt">string</span><span class="p">,</span><span class="w"> </span><span class="nx">plural</span><span class="o">:</span><span class="w"> </span><span class="kt">string</span><span class="p">)</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-21-21"><a id="__codelineno-21-21" name="__codelineno-21-21" href="#__codelineno-21-21"></a><span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="nx">count</span><span class="w"> </span><span class="o">===</span><span class="w"> </span><span class="mf">1</span><span class="w"> </span><span class="o">?</span><span class="w"> </span><span class="nx">singular</span><span class="w"> </span><span class="o">:</span><span class="w"> </span><span class="kt">plural</span><span class="p">;</span>
</span><span id="__span-21-22"><a id="__codelineno-21-22" name="__codelineno-21-22" href="#__codelineno-21-22"></a><span class="p">});</span>
</span></code></pre></div>
<p><strong>Usage in Templates:</strong></p>
<div class="language-html highlight"><pre><span></span><code><span id="__span-22-1"><a id="__codelineno-22-1" name="__codelineno-22-1" href="#__codelineno-22-1"></a><span class="p">&lt;</span><span class="nt">p</span><span class="p">&gt;</span>Your shift is scheduled for {{formatDate SHIFT_DATE &quot;MMMM D, YYYY&quot;}}.<span class="p">&lt;/</span><span class="nt">p</span><span class="p">&gt;</span>
</span><span id="__span-22-2"><a id="__codelineno-22-2" name="__codelineno-22-2" href="#__codelineno-22-2"></a>
</span><span id="__span-22-3"><a id="__codelineno-22-3" name="__codelineno-22-3" href="#__codelineno-22-3"></a><span class="p">&lt;</span><span class="nt">p</span><span class="p">&gt;</span>You&#39;ve knocked on {{DOOR_COUNT}} {{pluralize DOOR_COUNT &quot;door&quot; &quot;doors&quot;}}.<span class="p">&lt;/</span><span class="nt">p</span><span class="p">&gt;</span>
</span><span id="__span-22-4"><a id="__codelineno-22-4" name="__codelineno-22-4" href="#__codelineno-22-4"></a>
</span><span id="__span-22-5"><a id="__codelineno-22-5" name="__codelineno-22-5" href="#__codelineno-22-5"></a><span class="p">&lt;</span><span class="nt">p</span><span class="p">&gt;</span>Campaign budget: {{currency CAMPAIGN_BUDGET}}<span class="p">&lt;/</span><span class="nt">p</span><span class="p">&gt;</span>
</span></code></pre></div>
<hr />
<h3 id="error-handling">Error Handling<a class="headerlink" href="#error-handling" title="Permanent link">&para;</a></h3>
<p><strong>Custom Error Classes:</strong></p>
<div class="language-typescript highlight"><pre><span></span><code><span id="__span-23-1"><a id="__codelineno-23-1" name="__codelineno-23-1" href="#__codelineno-23-1"></a><span class="c1">// api/src/utils/errors.ts</span>
</span><span id="__span-23-2"><a id="__codelineno-23-2" name="__codelineno-23-2" href="#__codelineno-23-2"></a>
</span><span id="__span-23-3"><a id="__codelineno-23-3" name="__codelineno-23-3" href="#__codelineno-23-3"></a><span class="k">export</span><span class="w"> </span><span class="kd">class</span><span class="w"> </span><span class="nx">EmailTemplateNotFoundError</span><span class="w"> </span><span class="k">extends</span><span class="w"> </span><span class="ne">Error</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-23-4"><a id="__codelineno-23-4" name="__codelineno-23-4" href="#__codelineno-23-4"></a><span class="w"> </span><span class="kr">constructor</span><span class="p">(</span><span class="nx">message</span><span class="o">:</span><span class="w"> </span><span class="kt">string</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-23-5"><a id="__codelineno-23-5" name="__codelineno-23-5" href="#__codelineno-23-5"></a><span class="w"> </span><span class="k">super</span><span class="p">(</span><span class="nx">message</span><span class="p">);</span>
</span><span id="__span-23-6"><a id="__codelineno-23-6" name="__codelineno-23-6" href="#__codelineno-23-6"></a><span class="w"> </span><span class="k">this</span><span class="p">.</span><span class="nx">name</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;EmailTemplateNotFoundError&#39;</span><span class="p">;</span>
</span><span id="__span-23-7"><a id="__codelineno-23-7" name="__codelineno-23-7" href="#__codelineno-23-7"></a><span class="w"> </span><span class="p">}</span>
</span><span id="__span-23-8"><a id="__codelineno-23-8" name="__codelineno-23-8" href="#__codelineno-23-8"></a><span class="p">}</span>
</span><span id="__span-23-9"><a id="__codelineno-23-9" name="__codelineno-23-9" href="#__codelineno-23-9"></a>
</span><span id="__span-23-10"><a id="__codelineno-23-10" name="__codelineno-23-10" href="#__codelineno-23-10"></a><span class="k">export</span><span class="w"> </span><span class="kd">class</span><span class="w"> </span><span class="nx">MissingRequiredVariableError</span><span class="w"> </span><span class="k">extends</span><span class="w"> </span><span class="ne">Error</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-23-11"><a id="__codelineno-23-11" name="__codelineno-23-11" href="#__codelineno-23-11"></a><span class="w"> </span><span class="kr">constructor</span><span class="p">(</span><span class="nx">message</span><span class="o">:</span><span class="w"> </span><span class="kt">string</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-23-12"><a id="__codelineno-23-12" name="__codelineno-23-12" href="#__codelineno-23-12"></a><span class="w"> </span><span class="k">super</span><span class="p">(</span><span class="nx">message</span><span class="p">);</span>
</span><span id="__span-23-13"><a id="__codelineno-23-13" name="__codelineno-23-13" href="#__codelineno-23-13"></a><span class="w"> </span><span class="k">this</span><span class="p">.</span><span class="nx">name</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;MissingRequiredVariableError&#39;</span><span class="p">;</span>
</span><span id="__span-23-14"><a id="__codelineno-23-14" name="__codelineno-23-14" href="#__codelineno-23-14"></a><span class="w"> </span><span class="p">}</span>
</span><span id="__span-23-15"><a id="__codelineno-23-15" name="__codelineno-23-15" href="#__codelineno-23-15"></a><span class="p">}</span>
</span><span id="__span-23-16"><a id="__codelineno-23-16" name="__codelineno-23-16" href="#__codelineno-23-16"></a>
</span><span id="__span-23-17"><a id="__codelineno-23-17" name="__codelineno-23-17" href="#__codelineno-23-17"></a><span class="k">export</span><span class="w"> </span><span class="kd">class</span><span class="w"> </span><span class="nx">TemplateCompilationError</span><span class="w"> </span><span class="k">extends</span><span class="w"> </span><span class="ne">Error</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-23-18"><a id="__codelineno-23-18" name="__codelineno-23-18" href="#__codelineno-23-18"></a><span class="w"> </span><span class="kr">constructor</span><span class="p">(</span><span class="nx">message</span><span class="o">:</span><span class="w"> </span><span class="kt">string</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-23-19"><a id="__codelineno-23-19" name="__codelineno-23-19" href="#__codelineno-23-19"></a><span class="w"> </span><span class="k">super</span><span class="p">(</span><span class="nx">message</span><span class="p">);</span>
</span><span id="__span-23-20"><a id="__codelineno-23-20" name="__codelineno-23-20" href="#__codelineno-23-20"></a><span class="w"> </span><span class="k">this</span><span class="p">.</span><span class="nx">name</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;TemplateCompilationError&#39;</span><span class="p">;</span>
</span><span id="__span-23-21"><a id="__codelineno-23-21" name="__codelineno-23-21" href="#__codelineno-23-21"></a><span class="w"> </span><span class="p">}</span>
</span><span id="__span-23-22"><a id="__codelineno-23-22" name="__codelineno-23-22" href="#__codelineno-23-22"></a><span class="p">}</span>
</span></code></pre></div>
<p><strong>Service Error Handling:</strong></p>
<div class="language-typescript highlight"><pre><span></span><code><span id="__span-24-1"><a id="__codelineno-24-1" name="__codelineno-24-1" href="#__codelineno-24-1"></a><span class="k">try</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-24-2"><a id="__codelineno-24-2" name="__codelineno-24-2" href="#__codelineno-24-2"></a><span class="w"> </span><span class="k">await</span><span class="w"> </span><span class="nx">emailService</span><span class="p">.</span><span class="nx">sendFromTemplate</span><span class="p">(</span><span class="s1">&#39;shift-reminder&#39;</span><span class="p">,</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-24-3"><a id="__codelineno-24-3" name="__codelineno-24-3" href="#__codelineno-24-3"></a><span class="w"> </span><span class="nx">recipientEmail</span><span class="o">:</span><span class="w"> </span><span class="kt">volunteer.email</span><span class="p">,</span>
</span><span id="__span-24-4"><a id="__codelineno-24-4" name="__codelineno-24-4" href="#__codelineno-24-4"></a><span class="w"> </span><span class="nx">data</span><span class="o">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="p">...</span><span class="w"> </span><span class="p">},</span>
</span><span id="__span-24-5"><a id="__codelineno-24-5" name="__codelineno-24-5" href="#__codelineno-24-5"></a><span class="w"> </span><span class="p">});</span>
</span><span id="__span-24-6"><a id="__codelineno-24-6" name="__codelineno-24-6" href="#__codelineno-24-6"></a><span class="p">}</span><span class="w"> </span><span class="k">catch</span><span class="w"> </span><span class="p">(</span><span class="nx">error</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-24-7"><a id="__codelineno-24-7" name="__codelineno-24-7" href="#__codelineno-24-7"></a><span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="nx">error</span><span class="w"> </span><span class="ow">instanceof</span><span class="w"> </span><span class="nx">EmailTemplateNotFoundError</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-24-8"><a id="__codelineno-24-8" name="__codelineno-24-8" href="#__codelineno-24-8"></a><span class="w"> </span><span class="nx">logger</span><span class="p">.</span><span class="nx">error</span><span class="p">(</span><span class="s1">&#39;Template not found&#39;</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">templateKey</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;shift-reminder&#39;</span><span class="w"> </span><span class="p">});</span>
</span><span id="__span-24-9"><a id="__codelineno-24-9" name="__codelineno-24-9" href="#__codelineno-24-9"></a><span class="w"> </span><span class="c1">// Fallback to default email or skip send</span>
</span><span id="__span-24-10"><a id="__codelineno-24-10" name="__codelineno-24-10" href="#__codelineno-24-10"></a><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="k">else</span><span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="nx">error</span><span class="w"> </span><span class="ow">instanceof</span><span class="w"> </span><span class="nx">MissingRequiredVariableError</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-24-11"><a id="__codelineno-24-11" name="__codelineno-24-11" href="#__codelineno-24-11"></a><span class="w"> </span><span class="nx">logger</span><span class="p">.</span><span class="nx">error</span><span class="p">(</span><span class="s1">&#39;Missing required variables&#39;</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">error</span><span class="o">:</span><span class="w"> </span><span class="kt">error.message</span><span class="w"> </span><span class="p">});</span>
</span><span id="__span-24-12"><a id="__codelineno-24-12" name="__codelineno-24-12" href="#__codelineno-24-12"></a><span class="w"> </span><span class="c1">// Log to Sentry, notify admin</span>
</span><span id="__span-24-13"><a id="__codelineno-24-13" name="__codelineno-24-13" href="#__codelineno-24-13"></a><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="k">else</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-24-14"><a id="__codelineno-24-14" name="__codelineno-24-14" href="#__codelineno-24-14"></a><span class="w"> </span><span class="nx">logger</span><span class="p">.</span><span class="nx">error</span><span class="p">(</span><span class="s1">&#39;Email send failed&#39;</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">error</span><span class="w"> </span><span class="p">});</span>
</span><span id="__span-24-15"><a id="__codelineno-24-15" name="__codelineno-24-15" href="#__codelineno-24-15"></a><span class="w"> </span><span class="k">throw</span><span class="w"> </span><span class="nx">error</span><span class="p">;</span>
</span><span id="__span-24-16"><a id="__codelineno-24-16" name="__codelineno-24-16" href="#__codelineno-24-16"></a><span class="w"> </span><span class="p">}</span>
</span><span id="__span-24-17"><a id="__codelineno-24-17" name="__codelineno-24-17" href="#__codelineno-24-17"></a><span class="p">}</span>
</span></code></pre></div>
<hr />
<h2 id="troubleshooting">Troubleshooting<a class="headerlink" href="#troubleshooting" title="Permanent link">&para;</a></h2>
<h3 id="problem-template-not-found">Problem: Template not found<a class="headerlink" href="#problem-template-not-found" title="Permanent link">&para;</a></h3>
<p><strong>Symptoms:</strong>
- <code>EmailTemplateNotFoundError: Template not found or inactive: shift-reminder</code>
- Email not sent, exception thrown</p>
<p><strong>Causes:</strong>
1. Template key typo (case-sensitive)
2. Template is inactive (<code>isActive = false</code>)
3. Template doesn't exist in database</p>
<p><strong>Solutions:</strong></p>
<p><strong>Check template exists:</strong>
<div class="language-sql highlight"><pre><span></span><code><span id="__span-25-1"><a id="__codelineno-25-1" name="__codelineno-25-1" href="#__codelineno-25-1"></a><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">email_templates</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="k">key</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;shift-reminder&#39;</span><span class="p">;</span>
</span></code></pre></div></p>
<p><strong>Check active status:</strong>
<div class="language-sql highlight"><pre><span></span><code><span id="__span-26-1"><a id="__codelineno-26-1" name="__codelineno-26-1" href="#__codelineno-26-1"></a><span class="k">SELECT</span><span class="w"> </span><span class="k">key</span><span class="p">,</span><span class="w"> </span><span class="n">is_active</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">email_templates</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="k">key</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;shift-reminder&#39;</span><span class="p">;</span>
</span></code></pre></div></p>
<p><strong>Activate template:</strong>
<div class="language-sql highlight"><pre><span></span><code><span id="__span-27-1"><a id="__codelineno-27-1" name="__codelineno-27-1" href="#__codelineno-27-1"></a><span class="k">UPDATE</span><span class="w"> </span><span class="n">email_templates</span><span class="w"> </span><span class="k">SET</span><span class="w"> </span><span class="n">is_active</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">true</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="k">key</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;shift-reminder&#39;</span><span class="p">;</span>
</span></code></pre></div></p>
<p><strong>Create template via admin GUI or seed script</strong> (see Developer Workflow above)</p>
<hr />
<h3 id="problem-variable-not-replaced-shows-var-in-email">Problem: Variable not replaced (shows {{VAR}} in email)<a class="headerlink" href="#problem-variable-not-replaced-shows-var-in-email" title="Permanent link">&para;</a></h3>
<p><strong>Symptoms:</strong>
- Rendered email shows <code>{{USER_NAME}}</code> instead of "John Doe"
- Variables appear as raw text in subject or body</p>
<p><strong>Causes:</strong>
1. Variable key typo in data object (case-sensitive)
2. Variable not provided in data object
3. Handlebars compilation failed silently
4. Using wrong interpolation syntax</p>
<p><strong>Solutions:</strong></p>
<p><strong>Check variable key matches exactly:</strong>
<div class="language-typescript highlight"><pre><span></span><code><span id="__span-28-1"><a id="__codelineno-28-1" name="__codelineno-28-1" href="#__codelineno-28-1"></a><span class="c1">// Template uses {{USER_NAME}}</span>
</span><span id="__span-28-2"><a id="__codelineno-28-2" name="__codelineno-28-2" href="#__codelineno-28-2"></a><span class="c1">// Data must have USER_NAME (not userName or user_name)</span>
</span><span id="__span-28-3"><a id="__codelineno-28-3" name="__codelineno-28-3" href="#__codelineno-28-3"></a><span class="nx">data</span><span class="o">:</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-28-4"><a id="__codelineno-28-4" name="__codelineno-28-4" href="#__codelineno-28-4"></a><span class="w"> </span><span class="nx">USER_NAME</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;John Doe&#39;</span><span class="p">,</span><span class="w"> </span><span class="c1">// ✓ Correct</span>
</span><span id="__span-28-5"><a id="__codelineno-28-5" name="__codelineno-28-5" href="#__codelineno-28-5"></a><span class="w"> </span><span class="nx">userName</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;John Doe&#39;</span><span class="p">,</span><span class="w"> </span><span class="c1">// ✗ Wrong case</span>
</span><span id="__span-28-6"><a id="__codelineno-28-6" name="__codelineno-28-6" href="#__codelineno-28-6"></a><span class="w"> </span><span class="nx">user_name</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;John Doe&#39;</span><span class="p">,</span><span class="w"> </span><span class="c1">// ✗ Wrong format</span>
</span><span id="__span-28-7"><a id="__codelineno-28-7" name="__codelineno-28-7" href="#__codelineno-28-7"></a><span class="p">}</span>
</span></code></pre></div></p>
<p><strong>Console log data object:</strong>
<div class="language-typescript highlight"><pre><span></span><code><span id="__span-29-1"><a id="__codelineno-29-1" name="__codelineno-29-1" href="#__codelineno-29-1"></a><span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s1">&#39;Template data:&#39;</span><span class="p">,</span><span class="w"> </span><span class="nb">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">(</span><span class="nx">options</span><span class="p">.</span><span class="nx">data</span><span class="p">,</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w"> </span><span class="mf">2</span><span class="p">));</span>
</span></code></pre></div></p>
<p><strong>Test Handlebars compilation:</strong>
<div class="language-typescript highlight"><pre><span></span><code><span id="__span-30-1"><a id="__codelineno-30-1" name="__codelineno-30-1" href="#__codelineno-30-1"></a><span class="kd">const</span><span class="w"> </span><span class="nx">Handlebars</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">require</span><span class="p">(</span><span class="s1">&#39;handlebars&#39;</span><span class="p">);</span>
</span><span id="__span-30-2"><a id="__codelineno-30-2" name="__codelineno-30-2" href="#__codelineno-30-2"></a><span class="kd">const</span><span class="w"> </span><span class="nx">template</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">Handlebars</span><span class="p">.</span><span class="nx">compile</span><span class="p">(</span><span class="s1">&#39;Hello {{USER_NAME}}!&#39;</span><span class="p">);</span>
</span><span id="__span-30-3"><a id="__codelineno-30-3" name="__codelineno-30-3" href="#__codelineno-30-3"></a><span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">template</span><span class="p">({</span><span class="w"> </span><span class="nx">USER_NAME</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;Test&#39;</span><span class="w"> </span><span class="p">}));</span><span class="w"> </span><span class="c1">// Should output: &quot;Hello Test!&quot;</span>
</span></code></pre></div></p>
<p><strong>Verify template content:</strong>
<div class="language-sql highlight"><pre><span></span><code><span id="__span-31-1"><a id="__codelineno-31-1" name="__codelineno-31-1" href="#__codelineno-31-1"></a><span class="k">SELECT</span><span class="w"> </span><span class="n">subject_line</span><span class="p">,</span><span class="w"> </span><span class="n">html_content</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">email_templates</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="k">key</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;shift-reminder&#39;</span><span class="p">;</span>
</span></code></pre></div></p>
<hr />
<h3 id="problem-missing-required-variable-error">Problem: Missing required variable error<a class="headerlink" href="#problem-missing-required-variable-error" title="Permanent link">&para;</a></h3>
<p><strong>Symptoms:</strong>
- <code>MissingRequiredVariableError: Missing required variables for template shift-reminder: SHIFT_DATE, SHIFT_TIME</code>
- Email not sent, exception thrown</p>
<p><strong>Causes:</strong>
1. Required variable not provided in data object
2. Variable value is <code>null</code> or <code>undefined</code></p>
<p><strong>Solutions:</strong></p>
<p><strong>Check EmailTemplateVariable.isRequired:</strong>
<div class="language-sql highlight"><pre><span></span><code><span id="__span-32-1"><a id="__codelineno-32-1" name="__codelineno-32-1" href="#__codelineno-32-1"></a><span class="k">SELECT</span><span class="w"> </span><span class="k">key</span><span class="p">,</span><span class="w"> </span><span class="n">label</span><span class="p">,</span><span class="w"> </span><span class="n">is_required</span>
</span><span id="__span-32-2"><a id="__codelineno-32-2" name="__codelineno-32-2" href="#__codelineno-32-2"></a><span class="k">FROM</span><span class="w"> </span><span class="n">email_template_variables</span>
</span><span id="__span-32-3"><a id="__codelineno-32-3" name="__codelineno-32-3" href="#__codelineno-32-3"></a><span class="k">WHERE</span><span class="w"> </span><span class="n">template_id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">(</span><span class="k">SELECT</span><span class="w"> </span><span class="n">id</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">email_templates</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="k">key</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;shift-reminder&#39;</span><span class="p">);</span>
</span></code></pre></div></p>
<p><strong>Provide all required variables:</strong>
<div class="language-typescript highlight"><pre><span></span><code><span id="__span-33-1"><a id="__codelineno-33-1" name="__codelineno-33-1" href="#__codelineno-33-1"></a><span class="k">await</span><span class="w"> </span><span class="nx">emailService</span><span class="p">.</span><span class="nx">sendFromTemplate</span><span class="p">(</span><span class="s1">&#39;shift-reminder&#39;</span><span class="p">,</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-33-2"><a id="__codelineno-33-2" name="__codelineno-33-2" href="#__codelineno-33-2"></a><span class="w"> </span><span class="nx">recipientEmail</span><span class="o">:</span><span class="w"> </span><span class="kt">volunteer.email</span><span class="p">,</span>
</span><span id="__span-33-3"><a id="__codelineno-33-3" name="__codelineno-33-3" href="#__codelineno-33-3"></a><span class="w"> </span><span class="nx">data</span><span class="o">:</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-33-4"><a id="__codelineno-33-4" name="__codelineno-33-4" href="#__codelineno-33-4"></a><span class="w"> </span><span class="nx">USER_NAME</span><span class="o">:</span><span class="w"> </span><span class="kt">volunteer.name</span><span class="p">,</span>
</span><span id="__span-33-5"><a id="__codelineno-33-5" name="__codelineno-33-5" href="#__codelineno-33-5"></a><span class="w"> </span><span class="nx">SHIFT_DATE</span><span class="o">:</span><span class="w"> </span><span class="kt">dayjs</span><span class="p">(</span><span class="nx">shift</span><span class="p">.</span><span class="nx">startTime</span><span class="p">).</span><span class="nx">format</span><span class="p">(</span><span class="s1">&#39;MMMM D, YYYY&#39;</span><span class="p">),</span><span class="w"> </span><span class="c1">// ✓ Required</span>
</span><span id="__span-33-6"><a id="__codelineno-33-6" name="__codelineno-33-6" href="#__codelineno-33-6"></a><span class="w"> </span><span class="nx">SHIFT_TIME</span><span class="o">:</span><span class="w"> </span><span class="kt">dayjs</span><span class="p">(</span><span class="nx">shift</span><span class="p">.</span><span class="nx">startTime</span><span class="p">).</span><span class="nx">format</span><span class="p">(</span><span class="s1">&#39;h:mm A&#39;</span><span class="p">),</span><span class="w"> </span><span class="c1">// ✓ Required</span>
</span><span id="__span-33-7"><a id="__codelineno-33-7" name="__codelineno-33-7" href="#__codelineno-33-7"></a><span class="w"> </span><span class="p">},</span>
</span><span id="__span-33-8"><a id="__codelineno-33-8" name="__codelineno-33-8" href="#__codelineno-33-8"></a><span class="p">});</span>
</span></code></pre></div></p>
<p><strong>Temporary fix (set isRequired = false):</strong>
<div class="language-sql highlight"><pre><span></span><code><span id="__span-34-1"><a id="__codelineno-34-1" name="__codelineno-34-1" href="#__codelineno-34-1"></a><span class="k">UPDATE</span><span class="w"> </span><span class="n">email_template_variables</span>
</span><span id="__span-34-2"><a id="__codelineno-34-2" name="__codelineno-34-2" href="#__codelineno-34-2"></a><span class="k">SET</span><span class="w"> </span><span class="n">is_required</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">false</span>
</span><span id="__span-34-3"><a id="__codelineno-34-3" name="__codelineno-34-3" href="#__codelineno-34-3"></a><span class="k">WHERE</span><span class="w"> </span><span class="n">template_id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">(</span><span class="k">SELECT</span><span class="w"> </span><span class="n">id</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">email_templates</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="k">key</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;shift-reminder&#39;</span><span class="p">)</span>
</span><span id="__span-34-4"><a id="__codelineno-34-4" name="__codelineno-34-4" href="#__codelineno-34-4"></a><span class="w"> </span><span class="k">AND</span><span class="w"> </span><span class="k">key</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;SHIFT_TIME&#39;</span><span class="p">;</span>
</span></code></pre></div></p>
<p><strong>Long-term fix:</strong> Update code to always provide required variables</p>
<hr />
<h3 id="problem-email-sent-to-wrong-recipient">Problem: Email sent to wrong recipient<a class="headerlink" href="#problem-email-sent-to-wrong-recipient" title="Permanent link">&para;</a></h3>
<p><strong>Symptoms:</strong>
- Test email sent to production recipient
- User receives email meant for another user</p>
<p><strong>Causes:</strong>
1. Wrong <code>recipientEmail</code> parameter
2. Email test mode disabled (<code>EMAIL_TEST_MODE=false</code>)
3. Variable interpolation pulled wrong user data</p>
<p><strong>Solutions:</strong></p>
<p><strong>Enable test mode in development:</strong>
<div class="language-bash highlight"><pre><span></span><code><span id="__span-35-1"><a id="__codelineno-35-1" name="__codelineno-35-1" href="#__codelineno-35-1"></a><span class="c1"># .env</span>
</span><span id="__span-35-2"><a id="__codelineno-35-2" name="__codelineno-35-2" href="#__codelineno-35-2"></a><span class="nv">EMAIL_TEST_MODE</span><span class="o">=</span><span class="nb">true</span>
</span></code></pre></div></p>
<p><strong>Check recipient email:</strong>
<div class="language-typescript highlight"><pre><span></span><code><span id="__span-36-1"><a id="__codelineno-36-1" name="__codelineno-36-1" href="#__codelineno-36-1"></a><span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s1">&#39;Sending email to:&#39;</span><span class="p">,</span><span class="w"> </span><span class="nx">options</span><span class="p">.</span><span class="nx">recipientEmail</span><span class="p">);</span>
</span></code></pre></div></p>
<p><strong>Use MailHog in dev:</strong>
- All emails captured at http://localhost:8025
- Never sent to real recipients</p>
<p><strong>Verify user data query:</strong>
<div class="language-typescript highlight"><pre><span></span><code><span id="__span-37-1"><a id="__codelineno-37-1" name="__codelineno-37-1" href="#__codelineno-37-1"></a><span class="kd">const</span><span class="w"> </span><span class="nx">volunteer</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">await</span><span class="w"> </span><span class="nx">prisma</span><span class="p">.</span><span class="nx">user</span><span class="p">.</span><span class="nx">findUnique</span><span class="p">({</span><span class="w"> </span><span class="nx">where</span><span class="o">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">id</span><span class="o">:</span><span class="w"> </span><span class="kt">volunteerId</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="p">});</span>
</span><span id="__span-37-2"><a id="__codelineno-37-2" name="__codelineno-37-2" href="#__codelineno-37-2"></a><span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s1">&#39;Volunteer email:&#39;</span><span class="p">,</span><span class="w"> </span><span class="nx">volunteer</span><span class="p">.</span><span class="nx">email</span><span class="p">);</span>
</span></code></pre></div></p>
<hr />
<h3 id="problem-html-rendering-broken-in-email-client">Problem: HTML rendering broken in email client<a class="headerlink" href="#problem-html-rendering-broken-in-email-client" title="Permanent link">&para;</a></h3>
<p><strong>Symptoms:</strong>
- Email looks correct in preview but broken in Gmail/Outlook
- Images not loading
- Styles not applied</p>
<p><strong>Causes:</strong>
1. Email client doesn't support modern CSS
2. External images blocked by email client
3. Invalid HTML structure</p>
<p><strong>Solutions:</strong></p>
<p><strong>Use inline styles (not CSS classes):</strong>
<div class="language-html highlight"><pre><span></span><code><span id="__span-38-1"><a id="__codelineno-38-1" name="__codelineno-38-1" href="#__codelineno-38-1"></a><span class="cm">&lt;!-- ✗ Won&#39;t work in many email clients --&gt;</span>
</span><span id="__span-38-2"><a id="__codelineno-38-2" name="__codelineno-38-2" href="#__codelineno-38-2"></a><span class="p">&lt;</span><span class="nt">p</span> <span class="na">class</span><span class="o">=</span><span class="s">&quot;highlight&quot;</span><span class="p">&gt;</span>Important message<span class="p">&lt;/</span><span class="nt">p</span><span class="p">&gt;</span>
</span><span id="__span-38-3"><a id="__codelineno-38-3" name="__codelineno-38-3" href="#__codelineno-38-3"></a>
</span><span id="__span-38-4"><a id="__codelineno-38-4" name="__codelineno-38-4" href="#__codelineno-38-4"></a><span class="cm">&lt;!-- ✓ Use inline styles --&gt;</span>
</span><span id="__span-38-5"><a id="__codelineno-38-5" name="__codelineno-38-5" href="#__codelineno-38-5"></a><span class="p">&lt;</span><span class="nt">p</span> <span class="na">style</span><span class="o">=</span><span class="s">&quot;background-color: #ffeb3b; padding: 10px; font-weight: bold;&quot;</span><span class="p">&gt;</span>Important message<span class="p">&lt;/</span><span class="nt">p</span><span class="p">&gt;</span>
</span></code></pre></div></p>
<p><strong>Use tables for layout (not flexbox/grid):</strong>
<div class="language-html highlight"><pre><span></span><code><span id="__span-39-1"><a id="__codelineno-39-1" name="__codelineno-39-1" href="#__codelineno-39-1"></a><span class="cm">&lt;!-- ✓ Email-safe layout --&gt;</span>
</span><span id="__span-39-2"><a id="__codelineno-39-2" name="__codelineno-39-2" href="#__codelineno-39-2"></a><span class="p">&lt;</span><span class="nt">table</span> <span class="na">width</span><span class="o">=</span><span class="s">&quot;100%&quot;</span> <span class="na">cellpadding</span><span class="o">=</span><span class="s">&quot;0&quot;</span> <span class="na">cellspacing</span><span class="o">=</span><span class="s">&quot;0&quot;</span><span class="p">&gt;</span>
</span><span id="__span-39-3"><a id="__codelineno-39-3" name="__codelineno-39-3" href="#__codelineno-39-3"></a> <span class="p">&lt;</span><span class="nt">tr</span><span class="p">&gt;</span>
</span><span id="__span-39-4"><a id="__codelineno-39-4" name="__codelineno-39-4" href="#__codelineno-39-4"></a> <span class="p">&lt;</span><span class="nt">td</span> <span class="na">style</span><span class="o">=</span><span class="s">&quot;padding: 20px;&quot;</span><span class="p">&gt;</span>
</span><span id="__span-39-5"><a id="__codelineno-39-5" name="__codelineno-39-5" href="#__codelineno-39-5"></a> <span class="p">&lt;</span><span class="nt">p</span><span class="p">&gt;</span>Content here<span class="p">&lt;/</span><span class="nt">p</span><span class="p">&gt;</span>
</span><span id="__span-39-6"><a id="__codelineno-39-6" name="__codelineno-39-6" href="#__codelineno-39-6"></a> <span class="p">&lt;/</span><span class="nt">td</span><span class="p">&gt;</span>
</span><span id="__span-39-7"><a id="__codelineno-39-7" name="__codelineno-39-7" href="#__codelineno-39-7"></a> <span class="p">&lt;/</span><span class="nt">tr</span><span class="p">&gt;</span>
</span><span id="__span-39-8"><a id="__codelineno-39-8" name="__codelineno-39-8" href="#__codelineno-39-8"></a><span class="p">&lt;/</span><span class="nt">table</span><span class="p">&gt;</span>
</span></code></pre></div></p>
<p><strong>Embed images as data URIs or use absolute URLs:</strong>
<div class="language-html highlight"><pre><span></span><code><span id="__span-40-1"><a id="__codelineno-40-1" name="__codelineno-40-1" href="#__codelineno-40-1"></a><span class="cm">&lt;!-- ✓ Absolute URL --&gt;</span>
</span><span id="__span-40-2"><a id="__codelineno-40-2" name="__codelineno-40-2" href="#__codelineno-40-2"></a><span class="p">&lt;</span><span class="nt">img</span> <span class="na">src</span><span class="o">=</span><span class="s">&quot;https://cmlite.org/logo.png&quot;</span> <span class="na">alt</span><span class="o">=</span><span class="s">&quot;Logo&quot;</span><span class="p">&gt;</span>
</span><span id="__span-40-3"><a id="__codelineno-40-3" name="__codelineno-40-3" href="#__codelineno-40-3"></a>
</span><span id="__span-40-4"><a id="__codelineno-40-4" name="__codelineno-40-4" href="#__codelineno-40-4"></a><span class="cm">&lt;!-- ✓ Data URI (small images only) --&gt;</span>
</span><span id="__span-40-5"><a id="__codelineno-40-5" name="__codelineno-40-5" href="#__codelineno-40-5"></a><span class="p">&lt;</span><span class="nt">img</span> <span class="na">src</span><span class="o">=</span><span class="s">&quot;data:image/png;base64,iVBORw0KG...&quot;</span> <span class="na">alt</span><span class="o">=</span><span class="s">&quot;Icon&quot;</span><span class="p">&gt;</span>
</span></code></pre></div></p>
<p><strong>Test in multiple email clients:</strong>
- Use <a href="https://www.litmus.com/">Litmus</a> or <a href="https://www.emailonacid.com/">Email on Acid</a>
- Test in Gmail, Outlook, Apple Mail, Yahoo Mail</p>
<hr />
<h2 id="performance-considerations">Performance Considerations<a class="headerlink" href="#performance-considerations" title="Permanent link">&para;</a></h2>
<h3 id="template-loading">Template Loading<a class="headerlink" href="#template-loading" title="Permanent link">&para;</a></h3>
<p><strong>Current Implementation:</strong>
- Templates fetched from database on every send
- Includes variable definitions in same query
- No caching layer</p>
<p><strong>Performance Impact:</strong>
- Single database query per email send (~10ms)
- Acceptable for low-volume sends (&lt; 100/min)
- May bottleneck for high-volume campaigns (&gt; 1000/min)</p>
<p><strong>Optimization Options:</strong></p>
<p><strong>1. In-Memory Caching:</strong>
<div class="language-typescript highlight"><pre><span></span><code><span id="__span-41-1"><a id="__codelineno-41-1" name="__codelineno-41-1" href="#__codelineno-41-1"></a><span class="c1">// api/src/services/email.service.ts</span>
</span><span id="__span-41-2"><a id="__codelineno-41-2" name="__codelineno-41-2" href="#__codelineno-41-2"></a>
</span><span id="__span-41-3"><a id="__codelineno-41-3" name="__codelineno-41-3" href="#__codelineno-41-3"></a><span class="k">private</span><span class="w"> </span><span class="nx">templateCache</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="ow">new</span><span class="w"> </span><span class="nb">Map</span><span class="o">&lt;</span><span class="kt">string</span><span class="p">,</span><span class="w"> </span><span class="nx">EmailTemplate</span><span class="w"> </span><span class="o">&amp;</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">variables</span><span class="o">:</span><span class="w"> </span><span class="kt">EmailTemplateVariable</span><span class="p">[]</span><span class="w"> </span><span class="p">}</span><span class="o">&gt;</span><span class="p">();</span>
</span><span id="__span-41-4"><a id="__codelineno-41-4" name="__codelineno-41-4" href="#__codelineno-41-4"></a><span class="k">private</span><span class="w"> </span><span class="nx">cacheExpiry</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="ow">new</span><span class="w"> </span><span class="nb">Map</span><span class="o">&lt;</span><span class="kt">string</span><span class="p">,</span><span class="w"> </span><span class="kt">number</span><span class="o">&gt;</span><span class="p">();</span>
</span><span id="__span-41-5"><a id="__codelineno-41-5" name="__codelineno-41-5" href="#__codelineno-41-5"></a><span class="k">private</span><span class="w"> </span><span class="k">readonly</span><span class="w"> </span><span class="nx">CACHE_TTL</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mf">5</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="mf">60</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="mf">1000</span><span class="p">;</span><span class="w"> </span><span class="c1">// 5 minutes</span>
</span><span id="__span-41-6"><a id="__codelineno-41-6" name="__codelineno-41-6" href="#__codelineno-41-6"></a>
</span><span id="__span-41-7"><a id="__codelineno-41-7" name="__codelineno-41-7" href="#__codelineno-41-7"></a><span class="k">async</span><span class="w"> </span><span class="nx">loadTemplate</span><span class="p">(</span><span class="nx">key</span><span class="o">:</span><span class="w"> </span><span class="kt">string</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-41-8"><a id="__codelineno-41-8" name="__codelineno-41-8" href="#__codelineno-41-8"></a><span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">now</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nb">Date</span><span class="p">.</span><span class="nx">now</span><span class="p">();</span>
</span><span id="__span-41-9"><a id="__codelineno-41-9" name="__codelineno-41-9" href="#__codelineno-41-9"></a>
</span><span id="__span-41-10"><a id="__codelineno-41-10" name="__codelineno-41-10" href="#__codelineno-41-10"></a><span class="w"> </span><span class="c1">// Check cache</span>
</span><span id="__span-41-11"><a id="__codelineno-41-11" name="__codelineno-41-11" href="#__codelineno-41-11"></a><span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">templateCache</span><span class="p">.</span><span class="nx">has</span><span class="p">(</span><span class="nx">key</span><span class="p">)</span><span class="w"> </span><span class="o">&amp;&amp;</span><span class="w"> </span><span class="k">this</span><span class="p">.</span><span class="nx">cacheExpiry</span><span class="p">.</span><span class="nx">get</span><span class="p">(</span><span class="nx">key</span><span class="p">)</span><span class="o">!</span><span class="w"> </span><span class="o">&gt;</span><span class="w"> </span><span class="nx">now</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-41-12"><a id="__codelineno-41-12" name="__codelineno-41-12" href="#__codelineno-41-12"></a><span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="k">this</span><span class="p">.</span><span class="nx">templateCache</span><span class="p">.</span><span class="nx">get</span><span class="p">(</span><span class="nx">key</span><span class="p">)</span><span class="o">!</span><span class="p">;</span>
</span><span id="__span-41-13"><a id="__codelineno-41-13" name="__codelineno-41-13" href="#__codelineno-41-13"></a><span class="w"> </span><span class="p">}</span>
</span><span id="__span-41-14"><a id="__codelineno-41-14" name="__codelineno-41-14" href="#__codelineno-41-14"></a>
</span><span id="__span-41-15"><a id="__codelineno-41-15" name="__codelineno-41-15" href="#__codelineno-41-15"></a><span class="w"> </span><span class="c1">// Load from database</span>
</span><span id="__span-41-16"><a id="__codelineno-41-16" name="__codelineno-41-16" href="#__codelineno-41-16"></a><span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">template</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">await</span><span class="w"> </span><span class="nx">prisma</span><span class="p">.</span><span class="nx">emailTemplate</span><span class="p">.</span><span class="nx">findUnique</span><span class="p">({</span>
</span><span id="__span-41-17"><a id="__codelineno-41-17" name="__codelineno-41-17" href="#__codelineno-41-17"></a><span class="w"> </span><span class="nx">where</span><span class="o">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">key</span><span class="p">,</span><span class="w"> </span><span class="nx">isActive</span><span class="o">:</span><span class="w"> </span><span class="kt">true</span><span class="w"> </span><span class="p">},</span>
</span><span id="__span-41-18"><a id="__codelineno-41-18" name="__codelineno-41-18" href="#__codelineno-41-18"></a><span class="w"> </span><span class="nx">include</span><span class="o">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">variables</span><span class="o">:</span><span class="w"> </span><span class="kt">true</span><span class="w"> </span><span class="p">},</span>
</span><span id="__span-41-19"><a id="__codelineno-41-19" name="__codelineno-41-19" href="#__codelineno-41-19"></a><span class="w"> </span><span class="p">});</span>
</span><span id="__span-41-20"><a id="__codelineno-41-20" name="__codelineno-41-20" href="#__codelineno-41-20"></a>
</span><span id="__span-41-21"><a id="__codelineno-41-21" name="__codelineno-41-21" href="#__codelineno-41-21"></a><span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="o">!</span><span class="nx">template</span><span class="p">)</span><span class="w"> </span><span class="k">throw</span><span class="w"> </span><span class="ow">new</span><span class="w"> </span><span class="nx">EmailTemplateNotFoundError</span><span class="p">(</span><span class="sb">`Template not found: </span><span class="si">${</span><span class="nx">key</span><span class="si">}</span><span class="sb">`</span><span class="p">);</span>
</span><span id="__span-41-22"><a id="__codelineno-41-22" name="__codelineno-41-22" href="#__codelineno-41-22"></a>
</span><span id="__span-41-23"><a id="__codelineno-41-23" name="__codelineno-41-23" href="#__codelineno-41-23"></a><span class="w"> </span><span class="c1">// Cache template</span>
</span><span id="__span-41-24"><a id="__codelineno-41-24" name="__codelineno-41-24" href="#__codelineno-41-24"></a><span class="w"> </span><span class="k">this</span><span class="p">.</span><span class="nx">templateCache</span><span class="p">.</span><span class="nx">set</span><span class="p">(</span><span class="nx">key</span><span class="p">,</span><span class="w"> </span><span class="nx">template</span><span class="p">);</span>
</span><span id="__span-41-25"><a id="__codelineno-41-25" name="__codelineno-41-25" href="#__codelineno-41-25"></a><span class="w"> </span><span class="k">this</span><span class="p">.</span><span class="nx">cacheExpiry</span><span class="p">.</span><span class="nx">set</span><span class="p">(</span><span class="nx">key</span><span class="p">,</span><span class="w"> </span><span class="nx">now</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="k">this</span><span class="p">.</span><span class="nx">CACHE_TTL</span><span class="p">);</span>
</span><span id="__span-41-26"><a id="__codelineno-41-26" name="__codelineno-41-26" href="#__codelineno-41-26"></a>
</span><span id="__span-41-27"><a id="__codelineno-41-27" name="__codelineno-41-27" href="#__codelineno-41-27"></a><span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="nx">template</span><span class="p">;</span>
</span><span id="__span-41-28"><a id="__codelineno-41-28" name="__codelineno-41-28" href="#__codelineno-41-28"></a><span class="p">}</span>
</span></code></pre></div></p>
<p><strong>2. Redis Caching:</strong>
<div class="language-typescript highlight"><pre><span></span><code><span id="__span-42-1"><a id="__codelineno-42-1" name="__codelineno-42-1" href="#__codelineno-42-1"></a><span class="k">import</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">redis</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="kr">from</span><span class="w"> </span><span class="s1">&#39;@/config/redis&#39;</span><span class="p">;</span>
</span><span id="__span-42-2"><a id="__codelineno-42-2" name="__codelineno-42-2" href="#__codelineno-42-2"></a>
</span><span id="__span-42-3"><a id="__codelineno-42-3" name="__codelineno-42-3" href="#__codelineno-42-3"></a><span class="k">async</span><span class="w"> </span><span class="nx">loadTemplate</span><span class="p">(</span><span class="nx">key</span><span class="o">:</span><span class="w"> </span><span class="kt">string</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-42-4"><a id="__codelineno-42-4" name="__codelineno-42-4" href="#__codelineno-42-4"></a><span class="w"> </span><span class="c1">// Try Redis cache</span>
</span><span id="__span-42-5"><a id="__codelineno-42-5" name="__codelineno-42-5" href="#__codelineno-42-5"></a><span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">cached</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">await</span><span class="w"> </span><span class="nx">redis</span><span class="p">.</span><span class="nx">get</span><span class="p">(</span><span class="sb">`email-template:</span><span class="si">${</span><span class="nx">key</span><span class="si">}</span><span class="sb">`</span><span class="p">);</span>
</span><span id="__span-42-6"><a id="__codelineno-42-6" name="__codelineno-42-6" href="#__codelineno-42-6"></a><span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="nx">cached</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-42-7"><a id="__codelineno-42-7" name="__codelineno-42-7" href="#__codelineno-42-7"></a><span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="nb">JSON</span><span class="p">.</span><span class="nx">parse</span><span class="p">(</span><span class="nx">cached</span><span class="p">);</span>
</span><span id="__span-42-8"><a id="__codelineno-42-8" name="__codelineno-42-8" href="#__codelineno-42-8"></a><span class="w"> </span><span class="p">}</span>
</span><span id="__span-42-9"><a id="__codelineno-42-9" name="__codelineno-42-9" href="#__codelineno-42-9"></a>
</span><span id="__span-42-10"><a id="__codelineno-42-10" name="__codelineno-42-10" href="#__codelineno-42-10"></a><span class="w"> </span><span class="c1">// Load from database</span>
</span><span id="__span-42-11"><a id="__codelineno-42-11" name="__codelineno-42-11" href="#__codelineno-42-11"></a><span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">template</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">await</span><span class="w"> </span><span class="nx">prisma</span><span class="p">.</span><span class="nx">emailTemplate</span><span class="p">.</span><span class="nx">findUnique</span><span class="p">({</span><span class="w"> </span><span class="p">...</span><span class="w"> </span><span class="p">});</span>
</span><span id="__span-42-12"><a id="__codelineno-42-12" name="__codelineno-42-12" href="#__codelineno-42-12"></a>
</span><span id="__span-42-13"><a id="__codelineno-42-13" name="__codelineno-42-13" href="#__codelineno-42-13"></a><span class="w"> </span><span class="c1">// Cache in Redis (5 min TTL)</span>
</span><span id="__span-42-14"><a id="__codelineno-42-14" name="__codelineno-42-14" href="#__codelineno-42-14"></a><span class="w"> </span><span class="k">await</span><span class="w"> </span><span class="nx">redis</span><span class="p">.</span><span class="nx">setex</span><span class="p">(</span><span class="sb">`email-template:</span><span class="si">${</span><span class="nx">key</span><span class="si">}</span><span class="sb">`</span><span class="p">,</span><span class="w"> </span><span class="mf">300</span><span class="p">,</span><span class="w"> </span><span class="nb">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">(</span><span class="nx">template</span><span class="p">));</span>
</span><span id="__span-42-15"><a id="__codelineno-42-15" name="__codelineno-42-15" href="#__codelineno-42-15"></a>
</span><span id="__span-42-16"><a id="__codelineno-42-16" name="__codelineno-42-16" href="#__codelineno-42-16"></a><span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="nx">template</span><span class="p">;</span>
</span><span id="__span-42-17"><a id="__codelineno-42-17" name="__codelineno-42-17" href="#__codelineno-42-17"></a><span class="p">}</span>
</span></code></pre></div></p>
<p><strong>3. Cache Invalidation:</strong>
<div class="language-typescript highlight"><pre><span></span><code><span id="__span-43-1"><a id="__codelineno-43-1" name="__codelineno-43-1" href="#__codelineno-43-1"></a><span class="c1">// When template is updated</span>
</span><span id="__span-43-2"><a id="__codelineno-43-2" name="__codelineno-43-2" href="#__codelineno-43-2"></a><span class="k">await</span><span class="w"> </span><span class="nx">redis</span><span class="p">.</span><span class="nx">del</span><span class="p">(</span><span class="sb">`email-template:</span><span class="si">${</span><span class="nx">template</span><span class="p">.</span><span class="nx">key</span><span class="si">}</span><span class="sb">`</span><span class="p">);</span>
</span><span id="__span-43-3"><a id="__codelineno-43-3" name="__codelineno-43-3" href="#__codelineno-43-3"></a><span class="k">this</span><span class="p">.</span><span class="nx">templateCache</span><span class="p">.</span><span class="ow">delete</span><span class="p">(</span><span class="nx">template</span><span class="p">.</span><span class="nx">key</span><span class="p">);</span>
</span></code></pre></div></p>
<hr />
<h3 id="handlebars-compilation">Handlebars Compilation<a class="headerlink" href="#handlebars-compilation" title="Permanent link">&para;</a></h3>
<p><strong>Performance:</strong>
- Handlebars compilation is fast (~1ms per template)
- No significant bottleneck for typical templates</p>
<p><strong>Large Templates:</strong>
- Templates &gt; 100KB may take 5-10ms to compile
- Solution: Pre-compile templates and cache compiled functions</p>
<p><strong>Pre-Compilation:</strong>
<div class="language-typescript highlight"><pre><span></span><code><span id="__span-44-1"><a id="__codelineno-44-1" name="__codelineno-44-1" href="#__codelineno-44-1"></a><span class="k">private</span><span class="w"> </span><span class="nx">compiledCache</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="ow">new</span><span class="w"> </span><span class="nb">Map</span><span class="o">&lt;</span><span class="kt">string</span><span class="p">,</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-44-2"><a id="__codelineno-44-2" name="__codelineno-44-2" href="#__codelineno-44-2"></a><span class="w"> </span><span class="nx">subject</span><span class="o">:</span><span class="w"> </span><span class="kt">HandlebarsTemplateDelegate</span><span class="p">;</span>
</span><span id="__span-44-3"><a id="__codelineno-44-3" name="__codelineno-44-3" href="#__codelineno-44-3"></a><span class="w"> </span><span class="nx">html</span><span class="o">:</span><span class="w"> </span><span class="kt">HandlebarsTemplateDelegate</span><span class="p">;</span>
</span><span id="__span-44-4"><a id="__codelineno-44-4" name="__codelineno-44-4" href="#__codelineno-44-4"></a><span class="w"> </span><span class="nx">text</span><span class="o">:</span><span class="w"> </span><span class="kt">HandlebarsTemplateDelegate</span><span class="p">;</span>
</span><span id="__span-44-5"><a id="__codelineno-44-5" name="__codelineno-44-5" href="#__codelineno-44-5"></a><span class="p">}</span><span class="o">&gt;</span><span class="p">();</span>
</span><span id="__span-44-6"><a id="__codelineno-44-6" name="__codelineno-44-6" href="#__codelineno-44-6"></a>
</span><span id="__span-44-7"><a id="__codelineno-44-7" name="__codelineno-44-7" href="#__codelineno-44-7"></a><span class="k">async</span><span class="w"> </span><span class="nx">sendFromTemplate</span><span class="p">(</span><span class="nx">templateKey</span><span class="o">:</span><span class="w"> </span><span class="kt">string</span><span class="p">,</span><span class="w"> </span><span class="nx">options</span><span class="o">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="p">...</span><span class="w"> </span><span class="p">})</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-44-8"><a id="__codelineno-44-8" name="__codelineno-44-8" href="#__codelineno-44-8"></a><span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">template</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">await</span><span class="w"> </span><span class="k">this</span><span class="p">.</span><span class="nx">loadTemplate</span><span class="p">(</span><span class="nx">templateKey</span><span class="p">);</span>
</span><span id="__span-44-9"><a id="__codelineno-44-9" name="__codelineno-44-9" href="#__codelineno-44-9"></a>
</span><span id="__span-44-10"><a id="__codelineno-44-10" name="__codelineno-44-10" href="#__codelineno-44-10"></a><span class="w"> </span><span class="c1">// Check compiled cache</span>
</span><span id="__span-44-11"><a id="__codelineno-44-11" name="__codelineno-44-11" href="#__codelineno-44-11"></a><span class="w"> </span><span class="kd">let</span><span class="w"> </span><span class="nx">compiled</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">this</span><span class="p">.</span><span class="nx">compiledCache</span><span class="p">.</span><span class="nx">get</span><span class="p">(</span><span class="nx">templateKey</span><span class="p">);</span>
</span><span id="__span-44-12"><a id="__codelineno-44-12" name="__codelineno-44-12" href="#__codelineno-44-12"></a>
</span><span id="__span-44-13"><a id="__codelineno-44-13" name="__codelineno-44-13" href="#__codelineno-44-13"></a><span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="o">!</span><span class="nx">compiled</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-44-14"><a id="__codelineno-44-14" name="__codelineno-44-14" href="#__codelineno-44-14"></a><span class="w"> </span><span class="nx">compiled</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-44-15"><a id="__codelineno-44-15" name="__codelineno-44-15" href="#__codelineno-44-15"></a><span class="w"> </span><span class="nx">subject</span><span class="o">:</span><span class="w"> </span><span class="kt">Handlebars.compile</span><span class="p">(</span><span class="nx">template</span><span class="p">.</span><span class="nx">subjectLine</span><span class="p">),</span>
</span><span id="__span-44-16"><a id="__codelineno-44-16" name="__codelineno-44-16" href="#__codelineno-44-16"></a><span class="w"> </span><span class="nx">html</span><span class="o">:</span><span class="w"> </span><span class="kt">Handlebars.compile</span><span class="p">(</span><span class="nx">template</span><span class="p">.</span><span class="nx">htmlContent</span><span class="p">),</span>
</span><span id="__span-44-17"><a id="__codelineno-44-17" name="__codelineno-44-17" href="#__codelineno-44-17"></a><span class="w"> </span><span class="nx">text</span><span class="o">:</span><span class="w"> </span><span class="kt">Handlebars.compile</span><span class="p">(</span><span class="nx">template</span><span class="p">.</span><span class="nx">textContent</span><span class="p">),</span>
</span><span id="__span-44-18"><a id="__codelineno-44-18" name="__codelineno-44-18" href="#__codelineno-44-18"></a><span class="w"> </span><span class="p">};</span>
</span><span id="__span-44-19"><a id="__codelineno-44-19" name="__codelineno-44-19" href="#__codelineno-44-19"></a><span class="w"> </span><span class="k">this</span><span class="p">.</span><span class="nx">compiledCache</span><span class="p">.</span><span class="nx">set</span><span class="p">(</span><span class="nx">templateKey</span><span class="p">,</span><span class="w"> </span><span class="nx">compiled</span><span class="p">);</span>
</span><span id="__span-44-20"><a id="__codelineno-44-20" name="__codelineno-44-20" href="#__codelineno-44-20"></a><span class="w"> </span><span class="p">}</span>
</span><span id="__span-44-21"><a id="__codelineno-44-21" name="__codelineno-44-21" href="#__codelineno-44-21"></a>
</span><span id="__span-44-22"><a id="__codelineno-44-22" name="__codelineno-44-22" href="#__codelineno-44-22"></a><span class="w"> </span><span class="c1">// Interpolate</span>
</span><span id="__span-44-23"><a id="__codelineno-44-23" name="__codelineno-44-23" href="#__codelineno-44-23"></a><span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">subject</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">compiled</span><span class="p">.</span><span class="nx">subject</span><span class="p">(</span><span class="nx">options</span><span class="p">.</span><span class="nx">data</span><span class="p">);</span>
</span><span id="__span-44-24"><a id="__codelineno-44-24" name="__codelineno-44-24" href="#__codelineno-44-24"></a><span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">html</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">compiled</span><span class="p">.</span><span class="nx">html</span><span class="p">(</span><span class="nx">options</span><span class="p">.</span><span class="nx">data</span><span class="p">);</span>
</span><span id="__span-44-25"><a id="__codelineno-44-25" name="__codelineno-44-25" href="#__codelineno-44-25"></a><span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">text</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">compiled</span><span class="p">.</span><span class="nx">text</span><span class="p">(</span><span class="nx">options</span><span class="p">.</span><span class="nx">data</span><span class="p">);</span>
</span><span id="__span-44-26"><a id="__codelineno-44-26" name="__codelineno-44-26" href="#__codelineno-44-26"></a>
</span><span id="__span-44-27"><a id="__codelineno-44-27" name="__codelineno-44-27" href="#__codelineno-44-27"></a><span class="w"> </span><span class="c1">// Send...</span>
</span><span id="__span-44-28"><a id="__codelineno-44-28" name="__codelineno-44-28" href="#__codelineno-44-28"></a><span class="p">}</span>
</span></code></pre></div></p>
<hr />
<h3 id="bulk-email-sending">Bulk Email Sending<a class="headerlink" href="#bulk-email-sending" title="Permanent link">&para;</a></h3>
<p><strong>Problem:</strong> Sending 1000+ emails sequentially is slow (1-2 seconds per email)</p>
<p><strong>Solution:</strong> Use BullMQ job queue for async batch processing</p>
<p><strong>Queue Implementation:</strong>
<div class="language-typescript highlight"><pre><span></span><code><span id="__span-45-1"><a id="__codelineno-45-1" name="__codelineno-45-1" href="#__codelineno-45-1"></a><span class="c1">// api/src/services/email-queue.service.ts</span>
</span><span id="__span-45-2"><a id="__codelineno-45-2" name="__codelineno-45-2" href="#__codelineno-45-2"></a>
</span><span id="__span-45-3"><a id="__codelineno-45-3" name="__codelineno-45-3" href="#__codelineno-45-3"></a><span class="k">import</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">Queue</span><span class="p">,</span><span class="w"> </span><span class="nx">Worker</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="kr">from</span><span class="w"> </span><span class="s1">&#39;bullmq&#39;</span><span class="p">;</span>
</span><span id="__span-45-4"><a id="__codelineno-45-4" name="__codelineno-45-4" href="#__codelineno-45-4"></a><span class="k">import</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">redis</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="kr">from</span><span class="w"> </span><span class="s1">&#39;@/config/redis&#39;</span><span class="p">;</span>
</span><span id="__span-45-5"><a id="__codelineno-45-5" name="__codelineno-45-5" href="#__codelineno-45-5"></a>
</span><span id="__span-45-6"><a id="__codelineno-45-6" name="__codelineno-45-6" href="#__codelineno-45-6"></a><span class="kd">const</span><span class="w"> </span><span class="nx">emailQueue</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="ow">new</span><span class="w"> </span><span class="nx">Queue</span><span class="p">(</span><span class="s1">&#39;email-queue&#39;</span><span class="p">,</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-45-7"><a id="__codelineno-45-7" name="__codelineno-45-7" href="#__codelineno-45-7"></a><span class="w"> </span><span class="nx">connection</span><span class="o">:</span><span class="w"> </span><span class="kt">redis</span><span class="p">,</span>
</span><span id="__span-45-8"><a id="__codelineno-45-8" name="__codelineno-45-8" href="#__codelineno-45-8"></a><span class="p">});</span>
</span><span id="__span-45-9"><a id="__codelineno-45-9" name="__codelineno-45-9" href="#__codelineno-45-9"></a>
</span><span id="__span-45-10"><a id="__codelineno-45-10" name="__codelineno-45-10" href="#__codelineno-45-10"></a><span class="c1">// Add email job</span>
</span><span id="__span-45-11"><a id="__codelineno-45-11" name="__codelineno-45-11" href="#__codelineno-45-11"></a><span class="k">export</span><span class="w"> </span><span class="k">async</span><span class="w"> </span><span class="kd">function</span><span class="w"> </span><span class="nx">queueEmail</span><span class="p">(</span><span class="nx">templateKey</span><span class="o">:</span><span class="w"> </span><span class="kt">string</span><span class="p">,</span><span class="w"> </span><span class="nx">options</span><span class="o">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="p">...</span><span class="w"> </span><span class="p">})</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-45-12"><a id="__codelineno-45-12" name="__codelineno-45-12" href="#__codelineno-45-12"></a><span class="w"> </span><span class="k">await</span><span class="w"> </span><span class="nx">emailQueue</span><span class="p">.</span><span class="nx">add</span><span class="p">(</span><span class="s1">&#39;send-template&#39;</span><span class="p">,</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-45-13"><a id="__codelineno-45-13" name="__codelineno-45-13" href="#__codelineno-45-13"></a><span class="w"> </span><span class="nx">templateKey</span><span class="p">,</span>
</span><span id="__span-45-14"><a id="__codelineno-45-14" name="__codelineno-45-14" href="#__codelineno-45-14"></a><span class="w"> </span><span class="nx">recipientEmail</span><span class="o">:</span><span class="w"> </span><span class="kt">options.recipientEmail</span><span class="p">,</span>
</span><span id="__span-45-15"><a id="__codelineno-45-15" name="__codelineno-45-15" href="#__codelineno-45-15"></a><span class="w"> </span><span class="nx">data</span><span class="o">:</span><span class="w"> </span><span class="kt">options.data</span><span class="p">,</span>
</span><span id="__span-45-16"><a id="__codelineno-45-16" name="__codelineno-45-16" href="#__codelineno-45-16"></a><span class="w"> </span><span class="p">});</span>
</span><span id="__span-45-17"><a id="__codelineno-45-17" name="__codelineno-45-17" href="#__codelineno-45-17"></a><span class="p">}</span>
</span><span id="__span-45-18"><a id="__codelineno-45-18" name="__codelineno-45-18" href="#__codelineno-45-18"></a>
</span><span id="__span-45-19"><a id="__codelineno-45-19" name="__codelineno-45-19" href="#__codelineno-45-19"></a><span class="c1">// Process email jobs</span>
</span><span id="__span-45-20"><a id="__codelineno-45-20" name="__codelineno-45-20" href="#__codelineno-45-20"></a><span class="kd">const</span><span class="w"> </span><span class="nx">emailWorker</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="ow">new</span><span class="w"> </span><span class="nx">Worker</span><span class="p">(</span><span class="s1">&#39;email-queue&#39;</span><span class="p">,</span><span class="w"> </span><span class="k">async</span><span class="w"> </span><span class="p">(</span><span class="nx">job</span><span class="p">)</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-45-21"><a id="__codelineno-45-21" name="__codelineno-45-21" href="#__codelineno-45-21"></a><span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">templateKey</span><span class="p">,</span><span class="w"> </span><span class="nx">recipientEmail</span><span class="p">,</span><span class="w"> </span><span class="nx">data</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">job</span><span class="p">.</span><span class="nx">data</span><span class="p">;</span>
</span><span id="__span-45-22"><a id="__codelineno-45-22" name="__codelineno-45-22" href="#__codelineno-45-22"></a><span class="w"> </span><span class="k">await</span><span class="w"> </span><span class="nx">emailService</span><span class="p">.</span><span class="nx">sendFromTemplate</span><span class="p">(</span><span class="nx">templateKey</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">recipientEmail</span><span class="p">,</span><span class="w"> </span><span class="nx">data</span><span class="w"> </span><span class="p">});</span>
</span><span id="__span-45-23"><a id="__codelineno-45-23" name="__codelineno-45-23" href="#__codelineno-45-23"></a><span class="p">},</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-45-24"><a id="__codelineno-45-24" name="__codelineno-45-24" href="#__codelineno-45-24"></a><span class="w"> </span><span class="nx">connection</span><span class="o">:</span><span class="w"> </span><span class="kt">redis</span><span class="p">,</span>
</span><span id="__span-45-25"><a id="__codelineno-45-25" name="__codelineno-45-25" href="#__codelineno-45-25"></a><span class="w"> </span><span class="nx">concurrency</span><span class="o">:</span><span class="w"> </span><span class="kt">10</span><span class="p">,</span><span class="w"> </span><span class="c1">// Process 10 emails in parallel</span>
</span><span id="__span-45-26"><a id="__codelineno-45-26" name="__codelineno-45-26" href="#__codelineno-45-26"></a><span class="p">});</span>
</span></code></pre></div></p>
<p><strong>Usage:</strong>
<div class="language-typescript highlight"><pre><span></span><code><span id="__span-46-1"><a id="__codelineno-46-1" name="__codelineno-46-1" href="#__codelineno-46-1"></a><span class="c1">// Queue 1000 emails</span>
</span><span id="__span-46-2"><a id="__codelineno-46-2" name="__codelineno-46-2" href="#__codelineno-46-2"></a><span class="k">for</span><span class="w"> </span><span class="p">(</span><span class="kd">const</span><span class="w"> </span><span class="nx">volunteer</span><span class="w"> </span><span class="k">of</span><span class="w"> </span><span class="nx">volunteers</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-46-3"><a id="__codelineno-46-3" name="__codelineno-46-3" href="#__codelineno-46-3"></a><span class="w"> </span><span class="k">await</span><span class="w"> </span><span class="nx">queueEmail</span><span class="p">(</span><span class="s1">&#39;shift-reminder&#39;</span><span class="p">,</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-46-4"><a id="__codelineno-46-4" name="__codelineno-46-4" href="#__codelineno-46-4"></a><span class="w"> </span><span class="nx">recipientEmail</span><span class="o">:</span><span class="w"> </span><span class="kt">volunteer.email</span><span class="p">,</span>
</span><span id="__span-46-5"><a id="__codelineno-46-5" name="__codelineno-46-5" href="#__codelineno-46-5"></a><span class="w"> </span><span class="nx">data</span><span class="o">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="p">...</span><span class="w"> </span><span class="p">},</span>
</span><span id="__span-46-6"><a id="__codelineno-46-6" name="__codelineno-46-6" href="#__codelineno-46-6"></a><span class="w"> </span><span class="p">});</span>
</span><span id="__span-46-7"><a id="__codelineno-46-7" name="__codelineno-46-7" href="#__codelineno-46-7"></a><span class="p">}</span>
</span></code></pre></div></p>
<hr />
<h2 id="security-considerations">Security Considerations<a class="headerlink" href="#security-considerations" title="Permanent link">&para;</a></h2>
<h3 id="xss-cross-site-scripting-in-email-clients">XSS (Cross-Site Scripting) in Email Clients<a class="headerlink" href="#xss-cross-site-scripting-in-email-clients" title="Permanent link">&para;</a></h3>
<p><strong>Risk:</strong> Admin-authored templates may contain malicious JavaScript</p>
<p><strong>Handlebars Auto-Escaping:</strong>
- By default, <code>{{VAR}}</code> escapes HTML entities
- <code>&amp;</code><code>&amp;amp;</code>, <code>&lt;</code><code>&amp;lt;</code>, <code>&gt;</code><code>&amp;gt;</code></p>
<p><strong>Raw HTML (Unescaped):</strong>
- <code>{{{VAR}}}</code> (triple braces) renders raw HTML
- Use ONLY for trusted, application-generated content
- NEVER use for user-submitted content without sanitization</p>
<p><strong>Example:</strong>
<div class="language-html highlight"><pre><span></span><code><span id="__span-47-1"><a id="__codelineno-47-1" name="__codelineno-47-1" href="#__codelineno-47-1"></a><span class="cm">&lt;!-- Safe: auto-escaped --&gt;</span>
</span><span id="__span-47-2"><a id="__codelineno-47-2" name="__codelineno-47-2" href="#__codelineno-47-2"></a><span class="p">&lt;</span><span class="nt">p</span><span class="p">&gt;</span>User message: {{USER_MESSAGE}}<span class="p">&lt;/</span><span class="nt">p</span><span class="p">&gt;</span>
</span><span id="__span-47-3"><a id="__codelineno-47-3" name="__codelineno-47-3" href="#__codelineno-47-3"></a>
</span><span id="__span-47-4"><a id="__codelineno-47-4" name="__codelineno-47-4" href="#__codelineno-47-4"></a><span class="cm">&lt;!-- Unsafe: unescaped (only use for trusted content) --&gt;</span>
</span><span id="__span-47-5"><a id="__codelineno-47-5" name="__codelineno-47-5" href="#__codelineno-47-5"></a><span class="p">&lt;</span><span class="nt">div</span><span class="p">&gt;</span>{{{FORMATTED_CONTENT}}}<span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
</span></code></pre></div></p>
<p><strong>Sanitization:</strong>
<div class="language-typescript highlight"><pre><span></span><code><span id="__span-48-1"><a id="__codelineno-48-1" name="__codelineno-48-1" href="#__codelineno-48-1"></a><span class="k">import</span><span class="w"> </span><span class="nx">DOMPurify</span><span class="w"> </span><span class="kr">from</span><span class="w"> </span><span class="s1">&#39;isomorphic-dompurify&#39;</span><span class="p">;</span>
</span><span id="__span-48-2"><a id="__codelineno-48-2" name="__codelineno-48-2" href="#__codelineno-48-2"></a>
</span><span id="__span-48-3"><a id="__codelineno-48-3" name="__codelineno-48-3" href="#__codelineno-48-3"></a><span class="kd">const</span><span class="w"> </span><span class="nx">sanitizedMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">DOMPurify</span><span class="p">.</span><span class="nx">sanitize</span><span class="p">(</span><span class="nx">userInput</span><span class="p">,</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-48-4"><a id="__codelineno-48-4" name="__codelineno-48-4" href="#__codelineno-48-4"></a><span class="w"> </span><span class="nx">ALLOWED_TAGS</span><span class="o">:</span><span class="w"> </span><span class="p">[</span><span class="s1">&#39;p&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;strong&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;em&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;ul&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;ol&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;li&#39;</span><span class="p">],</span>
</span><span id="__span-48-5"><a id="__codelineno-48-5" name="__codelineno-48-5" href="#__codelineno-48-5"></a><span class="w"> </span><span class="nx">ALLOWED_ATTR</span><span class="o">:</span><span class="w"> </span><span class="p">[],</span>
</span><span id="__span-48-6"><a id="__codelineno-48-6" name="__codelineno-48-6" href="#__codelineno-48-6"></a><span class="p">});</span>
</span><span id="__span-48-7"><a id="__codelineno-48-7" name="__codelineno-48-7" href="#__codelineno-48-7"></a>
</span><span id="__span-48-8"><a id="__codelineno-48-8" name="__codelineno-48-8" href="#__codelineno-48-8"></a><span class="k">await</span><span class="w"> </span><span class="nx">emailService</span><span class="p">.</span><span class="nx">sendFromTemplate</span><span class="p">(</span><span class="s1">&#39;response-notification&#39;</span><span class="p">,</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-48-9"><a id="__codelineno-48-9" name="__codelineno-48-9" href="#__codelineno-48-9"></a><span class="w"> </span><span class="nx">recipientEmail</span><span class="o">:</span><span class="w"> </span><span class="kt">admin.email</span><span class="p">,</span>
</span><span id="__span-48-10"><a id="__codelineno-48-10" name="__codelineno-48-10" href="#__codelineno-48-10"></a><span class="w"> </span><span class="nx">data</span><span class="o">:</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-48-11"><a id="__codelineno-48-11" name="__codelineno-48-11" href="#__codelineno-48-11"></a><span class="w"> </span><span class="nx">USER_MESSAGE</span><span class="o">:</span><span class="w"> </span><span class="kt">sanitizedMessage</span><span class="p">,</span><span class="w"> </span><span class="c1">// Safe to use {{{...}}}</span>
</span><span id="__span-48-12"><a id="__codelineno-48-12" name="__codelineno-48-12" href="#__codelineno-48-12"></a><span class="w"> </span><span class="p">},</span>
</span><span id="__span-48-13"><a id="__codelineno-48-13" name="__codelineno-48-13" href="#__codelineno-48-13"></a><span class="p">});</span>
</span></code></pre></div></p>
<hr />
<h3 id="email-address-validation">Email Address Validation<a class="headerlink" href="#email-address-validation" title="Permanent link">&para;</a></h3>
<p><strong>Risk:</strong> Invalid email addresses cause SMTP errors or bounce emails</p>
<p><strong>Validation Before Sending:</strong>
<div class="language-typescript highlight"><pre><span></span><code><span id="__span-49-1"><a id="__codelineno-49-1" name="__codelineno-49-1" href="#__codelineno-49-1"></a><span class="k">import</span><span class="w"> </span><span class="nx">validator</span><span class="w"> </span><span class="kr">from</span><span class="w"> </span><span class="s1">&#39;validator&#39;</span><span class="p">;</span>
</span><span id="__span-49-2"><a id="__codelineno-49-2" name="__codelineno-49-2" href="#__codelineno-49-2"></a>
</span><span id="__span-49-3"><a id="__codelineno-49-3" name="__codelineno-49-3" href="#__codelineno-49-3"></a><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="o">!</span><span class="nx">validator</span><span class="p">.</span><span class="nx">isEmail</span><span class="p">(</span><span class="nx">options</span><span class="p">.</span><span class="nx">recipientEmail</span><span class="p">))</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-49-4"><a id="__codelineno-49-4" name="__codelineno-49-4" href="#__codelineno-49-4"></a><span class="w"> </span><span class="k">throw</span><span class="w"> </span><span class="ow">new</span><span class="w"> </span><span class="ne">Error</span><span class="p">(</span><span class="s1">&#39;Invalid recipient email address&#39;</span><span class="p">);</span>
</span><span id="__span-49-5"><a id="__codelineno-49-5" name="__codelineno-49-5" href="#__codelineno-49-5"></a><span class="p">}</span>
</span></code></pre></div></p>
<p><strong>Bounce Handling:</strong>
- Monitor bounce notifications from SMTP provider
- Mark bounced emails in database
- Disable sending to repeatedly bounced addresses</p>
<hr />
<h3 id="rate-limiting-template-test-sends">Rate Limiting Template Test Sends<a class="headerlink" href="#rate-limiting-template-test-sends" title="Permanent link">&para;</a></h3>
<p><strong>Risk:</strong> Admin spamming test sends</p>
<p><strong>Rate Limit Implementation:</strong>
<div class="language-typescript highlight"><pre><span></span><code><span id="__span-50-1"><a id="__codelineno-50-1" name="__codelineno-50-1" href="#__codelineno-50-1"></a><span class="c1">// api/src/modules/email-templates/email-templates.routes.ts</span>
</span><span id="__span-50-2"><a id="__codelineno-50-2" name="__codelineno-50-2" href="#__codelineno-50-2"></a>
</span><span id="__span-50-3"><a id="__codelineno-50-3" name="__codelineno-50-3" href="#__codelineno-50-3"></a><span class="k">import</span><span class="w"> </span><span class="nx">rateLimit</span><span class="w"> </span><span class="kr">from</span><span class="w"> </span><span class="s1">&#39;express-rate-limit&#39;</span><span class="p">;</span>
</span><span id="__span-50-4"><a id="__codelineno-50-4" name="__codelineno-50-4" href="#__codelineno-50-4"></a>
</span><span id="__span-50-5"><a id="__codelineno-50-5" name="__codelineno-50-5" href="#__codelineno-50-5"></a><span class="kd">const</span><span class="w"> </span><span class="nx">testSendLimiter</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">rateLimit</span><span class="p">({</span>
</span><span id="__span-50-6"><a id="__codelineno-50-6" name="__codelineno-50-6" href="#__codelineno-50-6"></a><span class="w"> </span><span class="nx">windowMs</span><span class="o">:</span><span class="w"> </span><span class="kt">60</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="mf">1000</span><span class="p">,</span><span class="w"> </span><span class="c1">// 1 minute</span>
</span><span id="__span-50-7"><a id="__codelineno-50-7" name="__codelineno-50-7" href="#__codelineno-50-7"></a><span class="w"> </span><span class="nx">max</span><span class="o">:</span><span class="w"> </span><span class="kt">10</span><span class="p">,</span><span class="w"> </span><span class="c1">// 10 requests per minute</span>
</span><span id="__span-50-8"><a id="__codelineno-50-8" name="__codelineno-50-8" href="#__codelineno-50-8"></a><span class="w"> </span><span class="nx">message</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;Too many test sends. Please wait before trying again.&#39;</span><span class="p">,</span>
</span><span id="__span-50-9"><a id="__codelineno-50-9" name="__codelineno-50-9" href="#__codelineno-50-9"></a><span class="w"> </span><span class="nx">standardHeaders</span><span class="o">:</span><span class="w"> </span><span class="kt">true</span><span class="p">,</span>
</span><span id="__span-50-10"><a id="__codelineno-50-10" name="__codelineno-50-10" href="#__codelineno-50-10"></a><span class="w"> </span><span class="nx">legacyHeaders</span><span class="o">:</span><span class="w"> </span><span class="kt">false</span><span class="p">,</span>
</span><span id="__span-50-11"><a id="__codelineno-50-11" name="__codelineno-50-11" href="#__codelineno-50-11"></a><span class="p">});</span>
</span><span id="__span-50-12"><a id="__codelineno-50-12" name="__codelineno-50-12" href="#__codelineno-50-12"></a>
</span><span id="__span-50-13"><a id="__codelineno-50-13" name="__codelineno-50-13" href="#__codelineno-50-13"></a><span class="nx">router</span><span class="p">.</span><span class="nx">post</span><span class="p">(</span><span class="s1">&#39;/:id/test&#39;</span><span class="p">,</span><span class="w"> </span><span class="nx">testSendLimiter</span><span class="p">,</span><span class="w"> </span><span class="nx">requireRole</span><span class="p">(</span><span class="nx">SUPER_ADMIN</span><span class="p">),</span><span class="w"> </span><span class="k">async</span><span class="w"> </span><span class="p">(</span><span class="nx">req</span><span class="p">,</span><span class="w"> </span><span class="nx">res</span><span class="p">)</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-50-14"><a id="__codelineno-50-14" name="__codelineno-50-14" href="#__codelineno-50-14"></a><span class="w"> </span><span class="c1">// Test send implementation...</span>
</span><span id="__span-50-15"><a id="__codelineno-50-15" name="__codelineno-50-15" href="#__codelineno-50-15"></a><span class="p">});</span>
</span></code></pre></div></p>
<hr />
<h3 id="template-injection-attacks">Template Injection Attacks<a class="headerlink" href="#template-injection-attacks" title="Permanent link">&para;</a></h3>
<p><strong>Risk:</strong> Admin injects malicious Handlebars helpers or expressions</p>
<p><strong>Handlebars Security:</strong>
- Handlebars does NOT execute JavaScript (unlike eval)
- Helpers are pre-registered by application (admin can't add custom helpers)
- No access to Node.js globals or require()</p>
<p><strong>Safe:</strong>
<div class="language-html highlight"><pre><span></span><code><span id="__span-51-1"><a id="__codelineno-51-1" name="__codelineno-51-1" href="#__codelineno-51-1"></a>{{USER_NAME}}
</span><span id="__span-51-2"><a id="__codelineno-51-2" name="__codelineno-51-2" href="#__codelineno-51-2"></a>{{#if HAS_PHONE}}{{USER_PHONE}}{{/if}}
</span><span id="__span-51-3"><a id="__codelineno-51-3" name="__codelineno-51-3" href="#__codelineno-51-3"></a>{{#each ITEMS}}{{name}}{{/each}}
</span></code></pre></div></p>
<p><strong>Already Prevented by Handlebars:</strong>
<div class="language-html highlight"><pre><span></span><code><span id="__span-52-1"><a id="__codelineno-52-1" name="__codelineno-52-1" href="#__codelineno-52-1"></a><span class="cm">&lt;!-- These do NOT execute, render as literal text --&gt;</span>
</span><span id="__span-52-2"><a id="__codelineno-52-2" name="__codelineno-52-2" href="#__codelineno-52-2"></a>{{require(&#39;fs&#39;).readFileSync(&#39;/etc/passwd&#39;)}}
</span><span id="__span-52-3"><a id="__codelineno-52-3" name="__codelineno-52-3" href="#__codelineno-52-3"></a>{{process.env.DATABASE_URL}}
</span></code></pre></div></p>
<p><strong>Best Practice:</strong> Still review templates before activating</p>
<hr />
<h2 id="related-documentation">Related Documentation<a class="headerlink" href="#related-documentation" title="Permanent link">&para;</a></h2>
<h3 id="frontend-documentation">Frontend Documentation<a class="headerlink" href="#frontend-documentation" title="Permanent link">&para;</a></h3>
<ul>
<li><strong><a href="../../frontend/pages/email-templates-page.md">EmailTemplatesPage.tsx</a></strong> — Email templates list page with CRUD table</li>
<li><strong><a href="../../frontend/pages/email-template-editor-page.md">EmailTemplateEditorPage.tsx</a></strong> — Split-pane editor with preview</li>
<li><strong>Components:</strong></li>
<li>Variable insertion panel</li>
<li>Live preview renderer</li>
<li>Test send form</li>
<li>Version comparison modal</li>
</ul>
<h3 id="backend-documentation">Backend Documentation<a class="headerlink" href="#backend-documentation" title="Permanent link">&para;</a></h3>
<ul>
<li><strong><a href="../../api/modules/email-templates.md">Email Templates Module</a></strong> — API routes and schemas</li>
<li><code>GET /api/email-templates</code> — List templates (with filters)</li>
<li><code>POST /api/email-templates</code> — Create template</li>
<li><code>PUT /api/email-templates/:id</code> — Update template</li>
<li><code>DELETE /api/email-templates/:id</code> — Delete template (system templates protected)</li>
<li><code>POST /api/email-templates/:id/test</code> — Send test email</li>
<li><code>GET /api/email-templates/:id/versions</code> — Version history</li>
<li><code>POST /api/email-templates/:id/rollback/:versionNumber</code> — Restore version</li>
<li><strong><a href="../../api/services/email-service.md">Email Service</a></strong> — Core email sending logic</li>
<li><code>sendFromTemplate()</code> — Load, validate, interpolate, send</li>
<li><code>send()</code> — Low-level Nodemailer wrapper</li>
<li>Handlebars helper registration</li>
</ul>
<h3 id="database-documentation">Database Documentation<a class="headerlink" href="#database-documentation" title="Permanent link">&para;</a></h3>
<ul>
<li><strong><a href="../../../database/models/email-templates/">Email Templates Models</a></strong> — Schema definitions</li>
<li>EmailTemplate model</li>
<li>EmailTemplateVariable model</li>
<li>EmailTemplateVersion model</li>
<li>EmailTemplateTestLog model</li>
<li>Indexes and constraints</li>
</ul>
<h3 id="feature-documentation">Feature Documentation<a class="headerlink" href="#feature-documentation" title="Permanent link">&para;</a></h3>
<ul>
<li><strong><a href="../editor/">editor.md</a></strong> — Email template editor interface</li>
<li><strong><a href="../variables/">variables.md</a></strong> — Template variable system</li>
<li><strong><a href="../versioning/">versioning.md</a></strong> — Template version history</li>
</ul>
<h3 id="configuration">Configuration<a class="headerlink" href="#configuration" title="Permanent link">&para;</a></h3>
<ul>
<li><strong><a href="../../configuration/environment.md">Environment Variables</a></strong> — Email-related env vars</li>
<li><code>EMAIL_TEST_MODE</code> — Enable MailHog capture</li>
<li>SMTP settings (host, port, user, password)</li>
<li><strong><a href="../../../database/models/settings/">Site Settings</a></strong> — Site-wide email settings</li>
<li>Default from name/email</li>
<li>SMTP override settings</li>
</ul>
</article>
</div>
<script>var target=document.getElementById(location.hash.slice(1));target&&target.name&&(target.checked=target.name.startsWith("__tabbed_"))</script>
</div>
<button type="button" class="md-top md-icon" data-md-component="top" hidden>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M13 20h-2V8l-5.5 5.5-1.42-1.42L12 4.16l7.92 7.92-1.42 1.42L13 8z"/></svg>
Back to top
</button>
</main>
<footer class="md-footer">
<nav class="md-footer__inner md-grid" aria-label="Footer" >
<a href="../" class="md-footer__link md-footer__link--prev" aria-label="Previous: Email Templates">
<div class="md-footer__button md-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M20 11v2H8l5.5 5.5-1.42 1.42L4.16 12l7.92-7.92L13.5 5.5 8 11z"/></svg>
</div>
<div class="md-footer__title">
<span class="md-footer__direction">
Previous
</span>
<div class="md-ellipsis">
Email Templates
</div>
</div>
</a>
<a href="../editor/" class="md-footer__link md-footer__link--next" aria-label="Next: Editor">
<div class="md-footer__title">
<span class="md-footer__direction">
Next
</span>
<div class="md-ellipsis">
Editor
</div>
</div>
<div class="md-footer__button md-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M4 11v2h12l-5.5 5.5 1.42 1.42L19.84 12l-7.92-7.92L10.5 5.5 16 11z"/></svg>
</div>
</a>
</nav>
<div class="md-footer-meta md-typeset">
<div class="md-footer-meta__inner md-grid">
<div class="md-copyright">
<div class="md-copyright__highlight">
Copyright &copy; 2024 The Bunker Operations <a href="#__consent">Change cookie settings</a>
</div>
</div>
<div class="md-social">
<a href="https://gitea.bnkops.com/admin" target="_blank" rel="noopener" title="Gitea Repository" class="md-social__link">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Free 7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2025 Fonticons, Inc.--><path d="M173.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3.3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6m-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5.3-6.2 2.3m44.2-1.7c-2.9.7-4.9 2.6-4.6 4.9.3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9M252.8 8C114.1 8 8 113.3 8 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C436.2 457.8 504 362.9 504 252 504 113.3 391.5 8 252.8 8M105.2 352.9c-1.3 1-1 3.3.7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1m-10.8-8.1c-.7 1.3.3 2.9 2.3 3.9 1.6 1 3.6.7 4.3-.7.7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3.7m32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3.7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1m-11.4-14.7c-1.6 1-1.6 3.6 0 5.9s4.3 3.3 5.6 2.3c1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2"/></svg>
</a>
<a href="https://listmonk.bnkops.com/subscription/form" target="_blank" rel="noopener" title="Newsletter" class="md-social__link">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><!--! Font Awesome Free 7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2025 Fonticons, Inc.--><path d="M536.4-26.3c9.8-3.5 20.6-1 28 6.3s9.8 18.2 6.3 28l-178 496.9c-5 13.9-18.1 23.1-32.8 23.1-14.2 0-27-8.6-32.3-21.7l-64.2-158c-4.5-11-2.5-23.6 5.2-32.6l94.5-112.4c5.1-6.1 4.7-15-.9-20.6s-14.6-6-20.6-.9l-112.4 94.3c-9.1 7.6-21.6 9.6-32.6 5.2L38.1 216.8c-13.1-5.3-21.7-18.1-21.7-32.3 0-14.7 9.2-27.8 23.1-32.8z"/></svg>
</a>
</div>
</div>
</div>
</footer>
</div>
<div class="md-dialog" data-md-component="dialog">
<div class="md-dialog__inner md-typeset"></div>
</div>
<script id="__config" type="application/json">{"annotate": null, "base": "../../../..", "features": ["announce.dismiss", "content.action.edit", "content.action.view", "content.code.annotate", "content.code.copy", "content.tooltips", "navigation.expand", "navigation.footer", "navigation.indexes", "navigation.path", "navigation.prune", "navigation.sections", "navigation.tabs", "navigation.tabs.sticky", "navigation.top", "navigation.tracking", "search.highlight", "search.share", "search.suggest", "toc.follow"], "search": "../../../../assets/javascripts/workers/search.2c215733.min.js", "tags": null, "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}, "version": null}</script>
<script src="../../../../assets/javascripts/bundle.79ae519e.min.js"></script>
<script src="../../../../javascripts/home.js"></script>
<script src="../../../../javascripts/github-widget.js"></script>
<script src="../../../../javascripts/gitea-widget.js"></script>
<script src="../../../../assets/js/env-config.js"></script>
<script src="../../../../assets/js/video-player.js"></script>
</body>
</html>