Mail Merge, From the Command Line

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.

See Also