How to configure email templates in config/emails/ and reusable global elements in config/globals/.
Ctrl + click to view docs
Configuration lives in two places:
| Folder | Purpose |
|---|---|
config/emails/<name>/ |
Per-email templates: JSON config, JS attachments, HTML, text, and data files |
config/globals/<name>/ |
Reusable content blocks: shared HTML footers, signatures, and inline image attachments |
Each named email template is a folder under config/emails/. The folder name is what you pass to --config-email.
config/emails/
billing/
email.json ← required: email properties and metadata
email.js ← required: attachments array; optional: custom variables
html/
billingStatement.htm ← HTML body file(s)
data/
message.txt ← optional plain-text body
| File | Required | Purpose |
|---|---|---|
email.json |
Yes | Email properties: to, from, subject, html, attachments, globals, etc. |
email.js |
Yes | Exports emailAttachments array (attachments) and optionally emailVars object (custom template variables for email.json) |
html/<file> |
No | HTML body file(s). Any filename accepted in the html/ subfolder |
data/<file> |
No | Plain-text body file(s). Any filename accepted in the data/ subfolder |
email.json is the primary configuration file for a templated email. All properties listed below correspond directly to nodemailer message fields — except for globals, emailList, email-list, sendAll, and log, which control sendEmail-specific behaviour.
| Property | Type | Description |
|---|---|---|
to |
string |
Recipient address(es). Single address, semicolon-separated list, or pass-through placeholder. |
cc |
string |
CC address(es). Semicolon-separated for multiple. |
bcc |
string |
BCC address(es). Semicolon-separated for multiple. |
from |
string |
Account name (e.g. "_default") or an explicit display name <email> string. |
replyTo |
string |
Reply-to address. Account name or explicit address. |
subject |
string |
Subject line. Supports {{template}} variables and {{dates.*}} variables. |
html |
string | string[] |
HTML file reference. String: resolved from html/ folder. Array: catalog of files selectable via --message-html. |
text |
string |
Plain-text body file path, resolved from data/ folder. |
attachments |
string |
Reference to emailAttachments export in email.js. Use "{email.emailAttachments}". |
globals |
string[] |
Global template names to include (e.g. ["footer"]). Merges their HTML/text and attachments. |
emailList |
string |
List file name from lists/ (e.g. "billing"). Triggers bulk send. |
email-list |
object[] |
Inline contact list: [{"email": "...", "name": "..."}, ...]. |
sendAll |
boolean |
Send one email to all list contacts instead of individual emails. Requires emailList or email-list. |
dsn |
object |
Delivery Status Notification config. See DSN section. |
log |
boolean |
Set true to log send results to logs/. |
{
"to": "jane@example.com"
}Use a semicolon (;) to separate multiple addresses in a single string. This is the raw data form for sending to multiple recipients without using an email list.
{
"to": "jane@example.com; john@example.com; team@example.com"
}This sends one email with all addresses visible in the To: field — equivalent to nodemailer's multiple to recipients.
{
"to": "jane@example.com; john@example.com",
"cc": "manager@company.com; assistant@company.com",
"bcc": "archive@company.com"
}Use an email list with
emailListoremail-listwhen you need per-contact personalization (one email per recipient). Use semicolon-separated raw strings when all recipients should receive the same email at once and all addresses may be visible to each other.
CHANGE_SEND_TO is a legacy placeholder that signals "provide this at send time via --send-to". The CLI flag overrides whatever is in email.json.
{
"to": "CHANGE_SEND_TO"
}"_default" (or any account name from config/accounts/) can be used for from and replyTo to pull the address from the account config:
{
"from": "_default",
"replyTo": "_default"
}The subject field supports the full template variable syntax. Variables are wrapped in {{ }}.
{
"subject": "Invoice for {{contact.name}} — {{date.formatted}}"
}{
"subject": "{{dates.lastMonth}} {{dates.year}} - Revenue Summary"
}Result when sent in February 2026: "January 2026 - Revenue Summary"
{
"subject": "Q{{dates.lastQuarter}} {{dates.lastYear}} Report — {{contact.name}}"
}Provide the filename (without path prefix) and sendEmail resolves it from the html/ subfolder of the email's folder.
{
"html": "billingStatement"
}Resolves to config/emails/billing/html/billingStatement.htm.
You can also provide a full path:
{
"html": "config/emails/billing/html/billingStatement.htm"
}When you have multiple HTML variants, list them as an array. Use --message-html on the CLI to select which one to send.
{
"html": ["html_a", "html_b", "html_c"]
}# Select by 0-based index
sendEmail --config-email example --message-html 1
# Select by filename
sendEmail --config-email example --message-html html_b
# Use html.htm as the default (if present and not listed in array)
sendEmail --config-email example --message-htmlPoint to a text file in the data/ subfolder:
{
"text": "message"
}Resolves to config/emails/<name>/data/message.txt.
The attachments field is a string reference to the emailAttachments export in the paired email.js file.
Recommended notation:
{
"attachments": "{email.emailAttachments}"
}Legacy shorthand (still accepted):
{
"attachments": "emailAttachments"
}Both resolve to config/emails/<name>/email.js → export const emailAttachments.
List global folder names to include. Their attachments are merged into the email, and their HTML/text content replaces any matching {% global 'name' %} tags in your template files.
{
"globals": ["footer"]
}Multiple globals:
{
"globals": ["footer", "header"]
}Nested global:
{
"globals": ["footer/billing"]
}Use emailList to reference a list file from lists/:
{
"to": "CHANGE_SEND_TO",
"from": "_default",
"subject": "Invoice for {{contact.name}}",
"html": "invoice",
"emailList": "billing"
}This loads lists/billing.json and sends one email per contact (repetitive mode).
Use email-list for an inline contact list:
{
"to": "CHANGE_SEND_TO",
"from": "_default",
"subject": "Invoice for {{contact.name}}",
"html": "invoice",
"email-list": [
{ "email": "alice@example.com", "name": "Alice" },
{ "email": "bob@example.com", "name": "Bob" }
]
}Custom contact fields — add any extra field and reference it as {{contact.<field>}}:
{
"email-list": [
{ "email": "alice@example.com", "name": "Alice", "company": "Acme Corp", "invoiceNo": "INV-001" },
{ "email": "bob@example.com", "name": "Bob", "company": "Globex", "invoiceNo": "INV-002" }
]
}In the HTML template:
<p>Dear {{contact.name}} of {{contact.company}},</p>
<p>Invoice #{{contact.invoiceNo}} is attached.</p>Combine sendAll: true with an email list to send a single email addressed to all contacts, rather than one email per contact:
{
"to": "CHANGE_SEND_TO",
"from": "_default",
"subject": "Company Announcement",
"html": "announcement",
"sendAll": true,
"emailList": "staff"
}All recipient addresses are placed in the To: field of one email. Per-contact template variables ({{contact.name}}, {{contact.email}}) are not substituted in sendAll mode.
Request delivery status feedback from the receiving server:
{
"dsn": {
"id": "my-message-id",
"return": "headers",
"notify": ["success", "delay", "failure"],
"recipient": "_default"
}
}| Field | Values | Description |
|---|---|---|
id |
any string | Message identifier for DSN reporting |
return |
"headers" | "full" |
Return headers only or the full message body |
notify |
array of "success", "delay", "failure" |
When to receive a notification |
recipient |
account name or email | Where DSN reports are sent |
email.js lives alongside email.json and exports the emailAttachments array (required) and optionally an emailVars object for custom template variables. This is where all attachment definitions live — regular file attachments and inline images — and where you can define custom variables for use in email.json.
export const emailAttachments = [
{
filename: 'Invoice.pdf',
path: 'attachments/Invoice.pdf',
},
];| Property | Type | Required | Description |
|---|---|---|---|
filename |
string |
Yes | Display name shown in the email client |
path |
string |
Yes | File path, relative to the sendEmail root or absolute |
contentDisposition |
'attachment' | 'inline' |
No | Default: 'attachment'. Use 'inline' for images embedded in HTML. |
cid |
string |
No | Content-ID for inline images. Referenced in HTML as <img src="cid:...">. Required when contentDisposition is 'inline'. |
export const emailAttachments = [
{
filename: 'Billing Statement.pdf',
path: 'attachments/Billing Statement.pdf',
},
];export const emailAttachments = [
{
filename: 'logo.jpg',
path: 'img/logo.jpg',
contentDisposition: 'inline',
cid: 'logo@company.com',
},
];In the HTML template, reference with the cid: URI:
<img src="cid:logo@company.com" alt="Company Logo">export const emailAttachments = [
{
filename: 'Monthly Report.pdf',
path: 'attachments/Monthly Report.pdf',
},
{
filename: 'logo.jpg',
path: 'img/logo.jpg',
contentDisposition: 'inline',
cid: 'logo@billing.local',
},
{
filename: 'signature-banner.jpg',
path: 'img/banner.jpg',
contentDisposition: 'inline',
cid: 'banner@billing.local',
},
];Import a global's attachment array and spread it into emailAttachments:
import { globalAttachments as footerAttachments } from '../../globals/footer/global.js';
export const emailAttachments = [
{
filename: 'Invoice.pdf',
path: 'attachments/Invoice.pdf',
},
// Merges footer's inline images (logo, map, etc.)
...footerAttachments,
];Starting with sendEmail v1.x, you can use date-based dynamic filenames and paths in attachments. This is useful for monthly/quarterly reports, dated invoices, or any attachment that includes date information in its filename or path.
There are two approaches to using dates in attachments:
Use {{dates.*}} template syntax directly in filename and path strings. The engine will substitute these at runtime automatically.
export const emailAttachments = [
{
// Template variables are substituted at runtime
filename: 'Monthly Report - {{dates.lastMonth}} {{dates.year}}.pdf',
path: 'attachments/reports/{{dates.year}}/{{dates.month}}/report.pdf',
},
{
filename: 'Q{{dates.quarter}} Summary - {{dates.year}}.pdf',
path: 'attachments/quarterly/Q{{dates.quarter}}-{{dates.year}}.pdf',
},
];When to use: Simple date substitution without conditional logic.
Export a function instead of a static array. The function receives a dates object with all date properties and can perform calculations or conditional logic.
export const emailAttachments = (dates) => {
// Calculate which year to use based on the report month
// For January reports, use the previous year
const theYear = dates.lastMonth === "January" ? dates.lastYear : dates.year;
return [
{
filename: `Report - ${dates.lastMonth} ${theYear}.pdf`,
path: `reports/${theYear}/${dates.lastMonth}/report.pdf`,
},
{
filename: `Quarterly Summary Q${dates.quarter} ${dates.year}.pdf`,
path: `reports/quarterly/Q${dates.quarter}-${dates.year}.pdf`,
},
];
};When to use: Conditional logic, calculations, or complex date-based naming.
The dates object (for function exports) and {{dates.*}} template variables (for template substitution) provide access to:
| Variable | Description | Example Value |
|---|---|---|
dates.lastMonth |
Previous month name | "January" |
dates.month |
Current month name | "February" |
dates.year |
Current year | "2026" |
dates.lastYear |
Previous year | "2025" |
dates.quarter |
Current quarter (1-4) | 1 |
dates.lastQuarter |
Previous quarter (1-4) | 4 |
dates.day |
Day of month | "15" |
dates.monthNumber |
Month number | "02" |
dates.fullDate |
MM-DD-YYYY format | "02-15-2026" |
dates.isoDate |
ISO date YYYY-MM-DD | "2026-02-15" |
See TEMPLATING.md for the complete list.
A billing system that generates monthly reports where January's report uses the previous year:
export const emailAttachments = (dates) => {
const theYear = dates.lastMonth === "January" ? dates.lastYear : dates.year;
return [
{
filename: `Report - ${dates.lastMonth} ${theYear}.pdf`,
path: `../../Documents/Reports/${theYear}/Report - ${dates.lastMonth} ${theYear}.pdf`,
},
{
filename: 'Company Logo.jpg',
path: 'img/logo.jpg',
contentDisposition: 'inline',
cid: 'logo@company.com',
},
];
};| Scenario | Use |
|---|---|
| Simple date substitution | Template variables ({{dates.*}}) |
| Conditional logic based on dates | Function export with dates parameter |
| Static attachments (no dates) | Static array export (original) |
In addition to emailAttachments, you can export custom variables from email.js that can be used as template variables in the sibling email.json file. This enables dynamic configuration values based on conditional logic.
Export an emailVars object containing custom variables:
// Compute a conditional year based on current month
var theYear;
var date = new Date();
var monthCheck = date.getMonth();
if (monthCheck === 0) {
// January: use previous year for last month's report
theYear = '{{dates.lastYear}}';
} else {
theYear = '{{dates.year}}';
}
export const emailAttachments = [
{
filename: 'Report - {{dates.lastMonth}} ' + theYear + '.pdf',
path: '../../Documents/Reports/' + theYear + '/report.pdf',
}
];
// Export custom variables for use in email.json
export const emailVars = {
theYear,
reportType: 'Monthly',
department: 'Billing'
};Reference exported variables using {{variableName}} syntax:
{
"subject": "Report {{dates.lastMonth}} {{theYear}}",
"to": "reports@example.com",
"bcc": "{{department}}@example.com"
}The email.json file is processed with template substitution before being parsed. All custom variables from emailVars are merged with built-in variables like dates.*, making them available throughout the configuration.
- When loading an email configuration, the engine first checks for
email.jsand extracts theemailVarsexport - Custom variables are merged with built-in
dates.*variables into a template context - The
email.jsonfile content is loaded as text - Template substitution is applied to the JSON text using the merged variables
- The substituted JSON is parsed and used as the email configuration
- Custom variables can contain template syntax themselves (e.g.,
theYear = '{{dates.lastYear}}') - All variables are substituted in a single pass
- If a custom variable has the same name as a built-in variable, the custom variable takes precedence
- Custom variables are only available in the sibling
email.jsonfile
When you need to send attachments without creating an email.js file, use the CLI attachment flags directly. Pass --attach-file and --attach-path in matching order:
sendEmail \
--send-to client@example.com \
--subject "Your Report" \
--message-html ./report.html \
--attach-file "Report.pdf" \
--attach-path "./attachments/Report.pdf"For inline images via CLI:
sendEmail \
--send-to client@example.com \
--subject "Branded Email" \
--message-html ./branded.html \
--attach-file "logo.png" \
--attach-path "./img/logo.png" \
--attach-cid "logo@company.com" \
--attach-content-disp "inline"Multiple inline images — each flag set corresponds positionally:
sendEmail \
--send-to client@example.com \
--subject "Visual Report" \
--message-html ./visual.html \
--attach-file "logo.png" --attach-path "./img/logo.png" \
--attach-cid "logo@co.com" --attach-content-disp "inline" \
--attach-file "chart.png" --attach-path "./img/chart.png" \
--attach-cid "chart@co.com" --attach-content-disp "inline"HTML body files go in the html/ subfolder. Any filename is accepted.
Use {{variable}} for dynamic substitution:
<p>Hello {{contact.name}},</p>
<p>Your statement for {{dates.lastMonth}} {{dates.year}} is attached.</p>
<p>Sent to: {{contact.email}} on {{date.formatted}}</p>Embed a reusable global block with the {% global %} tag:
<h1>Your Invoice</h1>
<p>Dear {{contact.name}}, please find your invoice attached.</p>
{% global 'footer' %}The tag is replaced at send time with the global's HTML content, and the global's attachments are automatically merged.
Nested global path:
{% global 'footer/billing' %}Reference inline images using the cid: URI — the CID must match the value defined in email.js or a global's global.js:
<img src="cid:logo@company.com" alt="Logo">
<img src="cid:map@company.com" alt="Office Map">Plain-text body files go in the data/ subfolder. Template variables and global tags work the same way as in HTML files.
Hello {{contact.name}},
Your billing statement for {{dates.lastMonth}} {{dates.year}} is attached.
For assistance, reply to this email.
Thanks, and have a great day!
JSON files in the data/ subfolder can serve as text-format data files. They are loaded as plain-text content (not parsed as structured data) unless used by a custom workflow.
These variables are available in subject lines, HTML files, and text files.
| Variable | Description | Source |
|---|---|---|
{{contact.name}} |
Recipient's display name | Email list |
{{contact.email}} |
Recipient's email address | Email list |
{{contact.<field>}} |
Any custom field | Email list |
| Variable | Format | Example |
|---|---|---|
{{date}} |
ISO 8601 | 2026-02-26 |
{{date.formatted}} |
Full English | February 26, 2026 |
{{date.short}} |
Short locale | 2/26/2026 |
Powered by @jhauga/getdate.
| Variable | Description | Example |
|---|---|---|
{{dates.date}} |
MM-DD-YY | 02-26-26 |
{{dates.fullDate}} |
MM-DD-YYYY | 02-26-2026 |
{{dates.slashDate}} |
MM/DD/YY | 02/26/26 |
{{dates.terminalDate}} |
MM/DD/YYYY | 02/26/2026 |
{{dates.isoDate}} |
YYYY-MM-DD | 2026-02-26 |
{{dates.day}} |
Day (two digits) | 26 |
{{dates.monthNumber}} |
Month number (two digits) | 02 |
{{dates.month}} |
Full month name | February |
{{dates.monthShort}} |
Abbreviated month | Feb |
{{dates.lastMonth}} |
Previous month name | January |
{{dates.lastMonthShort}} |
Previous month abbreviated | Jan |
{{dates.quarter}} |
Current fiscal quarter | 1 |
{{dates.lastQuarter}} |
Previous fiscal quarter | 4 |
{{dates.season}} |
Season name | Winter |
{{dates.year}} |
Four-digit year | 2026 |
{{dates.twoDigitYear}} |
Two-digit year | 26 |
{{dates.lastYear}} |
Previous year | 2025 |
{{dates.nextYear}} |
Next year | 2027 |
{{dates.isLeapYear}} |
Leap year (1 or 0) | 0 |
Monthly report subject:
{
"subject": "{{dates.lastMonth}} {{dates.year}} - Monthly Summary"
}Quarterly report subject:
{
"subject": "Q{{dates.lastQuarter}} {{dates.lastYear}} Financial Report"
}Date range in HTML:
<h1>Q{{dates.quarter}} {{dates.year}} Report</h1>
<p>Covering {{dates.lastMonth}} through {{dates.month}}</p>
<p>Generated on {{dates.fullDate}}</p>Available only during repetitive (bulk) sends:
| Variable | Description |
|---|---|
{{list.index}} |
Current recipient index (0-based) |
{{list.count}} |
Total recipient count |
| Placeholder | Equivalent |
|---|---|
CH-EMAILONLIST |
{{contact.name}} |
CHANGE_SEND_TO |
{{contact.email}} |
CH-EMAILTO |
{{contact.email}} |
CH-SUBJECT |
{{subject}} |
CH-DATE |
{{date}} |
CHANGE_MESSAGE_HEADER |
Subject line placeholder |
CHANGE_BCC |
BCC placeholder |
Globals are reusable building blocks: shared footers, signatures, and inline image sets that can be injected into any email template.
config/globals/
footer/
global.js ← required: attachment declarations
html.htm ← optional: HTML content (strict naming at root)
text.txt ← optional: text content (strict naming at root)
example/
global.js
html/
footer.htm ← optional: HTML content (relaxed naming in subfolder)
data/
footer.txt ← optional: text content (relaxed naming in subfolder)
| File / Folder | Required | Purpose |
|---|---|---|
global.js |
Yes | Declares globalAttachments array |
html.htm or html.html |
No | HTML content (root — must be named exactly this) |
text.txt |
No | Text content (root — must be named exactly this) |
html/<any> |
No | HTML content (subfolder — any filename accepted) |
data/<any> |
No | Text content (subfolder — any filename accepted) |
Every global folder must have a global.js that exports a globalAttachments array:
export const globalAttachments = [
{
filename: 'Logo',
path: 'img/logo.jpg',
contentDisposition: 'inline',
cid: 'logo@footer.local',
},
];No attachments (HTML/text-only global):
export const globalAttachments = [];Multiple attachments:
export const globalAttachments = [
{
filename: 'Logo',
path: 'img/logo.jpg',
contentDisposition: 'inline',
cid: 'logo@footer.local',
},
{
filename: 'Map',
path: 'img/map.jpg',
contentDisposition: 'inline',
cid: 'map@footer.local',
},
];Attachment paths in
global.jsare resolved relative to the sendEmail root (or the CWD when using a--copyinstance), not relative to theglobal.jsfile itself.
At the root of the global folder, the HTML file must be named html.htm or html.html:
config/globals/footer/
global.js
html.htm ← must use this exact name at root level
In an html/ subfolder, any filename is accepted:
config/globals/footer/
global.js
html/
footer.htm ← any name works in html/ subfolder
When both exist, the subfolder (html/<file>) takes precedence.
<div style="border-top: 1px solid #ddd; margin-top: 20px; padding-top: 10px;">
<p>Best regards,</p>
<p><strong>The Team</strong></p>
<img src="cid:logo@footer.local" alt="Company Logo" style="height: 40px;">
</div>Template variables work inside global HTML too:
<p>Sent to {{contact.email}} on {{dates.fullDate}}</p>Must be named text.txt at the root:
config/globals/footer/
global.js
text.txt ← must use this exact name
In a data/ subfolder, any filename:
config/globals/footer/
global.js
data/
footer.txt
Thanks, and have a great day!
The Team
company@example.com | https://example.com
Only global.js is a recognized JS file in a global folder. It must export a globalAttachments constant:
// global.js — the only JS file used by sendEmail in a global folder
export const globalAttachments = [ /* attachment objects */ ];A global folder can contain nested sub-globals. Each nested folder is an independent global with its own global.js:
config/globals/
footer/
billing/
global.js ← nested global: 'footer/billing'
html.htm
marketing/
global.js ← nested global: 'footer/marketing'
html/
footer.htm
global.js ← parent global: 'footer'
html.htm
A folder that contains
global.jscannot also serve as a namespace for further nesting. Only folders withoutglobal.jscan act as namespace containers.
Reference nested globals in templates:
{% global 'footer/billing' %}
{% global 'footer/marketing' %}Or in email.json:
{
"globals": ["footer/billing"]
}{
"from": "_default",
"subject": "Hello from sendEmail",
"html": "body"
}{
"to": "CHANGE_SEND_TO",
"bcc": "CHANGE_BCC",
"from": "_default",
"replyTo": "_default",
"subject": "Billing Statement - {{contact.name}}",
"html": "billingStatement",
"attachments": "{email.emailAttachments}",
"globals": ["footer"],
"log": false
}{
"to": "CHANGE_SEND_TO",
"from": "_default",
"subject": "{{dates.lastMonth}} {{dates.year}} - Monthly Report",
"html": "monthlyReport",
"attachments": "{email.emailAttachments}",
"globals": ["footer"],
"log": true
}{
"to": "alice@example.com; bob@example.com; carol@example.com",
"from": "_default",
"subject": "Team Announcement",
"html": "announcement"
}{
"to": "CHANGE_SEND_TO",
"from": "_default",
"subject": "Invoice for {{contact.name}}",
"html": "invoice",
"attachments": "{email.emailAttachments}",
"globals": ["footer"],
"emailList": "clients",
"log": true
}{
"to": "CHANGE_SEND_TO",
"from": "_default",
"subject": "Invoice #{{contact.invoiceNo}} for {{contact.name}}",
"html": "invoice",
"attachments": "{email.emailAttachments}",
"globals": ["footer"],
"email-list": [
{ "email": "alice@example.com", "name": "Alice", "invoiceNo": "INV-001" },
{ "email": "bob@example.com", "name": "Bob", "invoiceNo": "INV-002" }
]
}{
"to": "CHANGE_SEND_TO",
"from": "_default",
"subject": "Team Update — {{dates.month}} {{dates.year}}",
"html": "announcement",
"sendAll": true,
"emailList": "staff"
}{
"to": "CHANGE_SEND_TO",
"from": "_default",
"subject": "CHANGE_MESSAGE_HEADER",
"html": ["html_a", "html_b", "html_c"],
"attachments": "{email.emailAttachments}",
"globals": ["footer"],
"log": false
}{
"to": "CHANGE_SEND_TO",
"bcc": "CHANGE_BCC",
"from": "_default",
"replyTo": "_default",
"subject": "CHANGE_MESSAGE_HEADER",
"html": ["html_a", "html_b"],
"attachments": "{email.emailAttachments}",
"globals": ["footer"],
"log": false,
"dsn": {
"id": "DSN_ID",
"return": "headers",
"notify": ["success", "delay"],
"recipient": "_default"
}
}- TEMPLATING.md — Full template variable reference and global tag details
- CLI-OPTIONS.md —
--config-email,--email-list,--global-config, and all configurable options - EXAMPLES.md — Real-world usage examples including bulk send and inline globals
- API.md — Engine API for library usage