Introduction
You have a list of people and one message that should read as if it were written for each of them — a welcome note, a plan activation, an event invite. That's mail merge, and it usually means a marketing SaaS or a fragile spreadsheet macro. This post does it from the command line: aux4/template personalizes the body per recipient, aux4/json fans it out over the list, and aux4/email sends — with a full preview before anything leaves your outbox.
Install the packages
aux4 aux4 pkger install aux4/template aux4/json aux4/email
Step 1: the list
Your recipients live in one file — contacts.json, with an address and whatever fields you want to personalize on:
[
{ "name": "Sally", "email": "sally@example.com", "plan": "Pro", "trial": false },
{ "name": "Marco", "email": "marco@example.com", "plan": "Free", "trial": true },
{ "name": "Priya", "email": "priya@example.com", "plan": "Team", "trial": false }
]
Step 2: the message, once
Write the body one time as a Handlebars template. Fields fill in per recipient, and {{#if}} adds a line only when it applies. Create body.hbs — note (bool trial), which turns the flag value into a real boolean:
Hi {{name}},
Your {{plan}} plan is now active.{{#if (bool trial)}} You're on a 14-day free trial — nothing is charged until it ends.{{/if}}
Thanks for joining,
The Acme Team
Render it for one person to check the shape:
aux4 template --file body.hbs --name Sally --plan Pro --trial false
Hi Sally,
Your Pro plan is now active.
Thanks for joining,
The Acme Team
Step 3: preview every email before sending
This is the step that saves you. Fan the template out over the whole list with aux4 json exec — ${item.field} is the current recipient — and print each message with its recipient. Nothing is sent; you're just reading what would go out:
cat contacts.json | aux4 json exec 'echo "--- To: ${item.email} ---"; aux4 template --file body.hbs --name "${item.name}" --plan "${item.plan}" --trial "${item.trial}"'
--- To: sally@example.com ---
Hi Sally,
Your Pro plan is now active.
Thanks for joining,
The Acme Team
--- To: marco@example.com ---
Hi Marco,
Your Free plan is now active. You're on a 14-day free trial — nothing is charged until it ends.
Thanks for joining,
The Acme Team
--- To: priya@example.com ---
Hi Priya,
Your Team plan is now active.
Thanks for joining,
The Acme Team
Each message is personalized, and Marco — the only one on a trial — is the only one who gets the trial line. Read the whole batch, catch the awkward wording now, then send.
Step 4: send the batch
Same fan-out, but the per-recipient command is now aux4 email send. The body is rendered by template for each person and handed to --body:
cat contacts.json | aux4 json exec 'aux4 email send --to "${item.email}" --subject "Your ${item.plan} plan is ready" --body "$(aux4 template --file body.hbs --name "${item.name}" --plan "${item.plan}" --trial "${item.trial}")"'
aux4 email send requires --to, --subject, and --body; it also takes --cc, --bcc, and --attachment. The sender and SMTP settings come from your ~/.config/aux4/email.yaml, so the address people see is the user configured there.
This one actually sends, once per recipient — which is exactly why Step 3 exists. Preview the batch, and only run this when the output reads the way you want.
Conclusion
Mail merge, without the marketing stack: aux4/template writes the message once and personalizes it per recipient, aux4/json runs it across your whole list, and aux4/email delivers — with a dry-run preview so you see every message before one is sent. Swap the template and the list and you have welcome emails, renewal notices, or event invites, all from a single command.