{"id":48963816,"url":"https://github.com/paperfoot/mailing-list-cli","last_synced_at":"2026-04-28T03:00:16.765Z","repository":{"id":349863993,"uuid":"1204258514","full_name":"paperfoot/mailing-list-cli","owner":"paperfoot","description":"Mailing list and newsletter management from the terminal. Built for AI agents on top of Resend.","archived":false,"fork":false,"pushed_at":"2026-04-27T22:45:34.000Z","size":2888,"stargazers_count":0,"open_issues_count":1,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-04-27T23:24:04.412Z","etag":null,"topics":["ab-testing","agent-tools","ai-agents","beehiiv-alternative","broadcast","cli","command-line-tool","devtools","email-campaigns","email-marketing","gdpr","local-first","mailchimp-alternative","mailing-list","mjml","newsletter","resend","rust","segments","unsubscribe"],"latest_commit_sha":null,"homepage":null,"language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/paperfoot.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2026-04-07T20:59:22.000Z","updated_at":"2026-04-27T22:45:38.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/paperfoot/mailing-list-cli","commit_stats":null,"previous_names":["199-biotechnologies/mailing-list-cli","paperfoot/mailing-list-cli"],"tags_count":18,"template":false,"template_full_name":null,"purl":"pkg:github/paperfoot/mailing-list-cli","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/paperfoot%2Fmailing-list-cli","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/paperfoot%2Fmailing-list-cli/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/paperfoot%2Fmailing-list-cli/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/paperfoot%2Fmailing-list-cli/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/paperfoot","download_url":"https://codeload.github.com/paperfoot/mailing-list-cli/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/paperfoot%2Fmailing-list-cli/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32364093,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-27T20:07:02.737Z","status":"online","status_checked_at":"2026-04-28T02:00:07.250Z","response_time":56,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ab-testing","agent-tools","ai-agents","beehiiv-alternative","broadcast","cli","command-line-tool","devtools","email-campaigns","email-marketing","gdpr","local-first","mailchimp-alternative","mailing-list","mjml","newsletter","resend","rust","segments","unsubscribe"],"created_at":"2026-04-18T03:04:22.914Z","updated_at":"2026-04-28T03:00:16.747Z","avatar_url":"https://github.com/paperfoot.png","language":"Rust","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cdiv align=\"center\"\u003e\n\n\u003cimg src=\"./assets/og-card.png\" alt=\"mailing-list-cli — newsletter campaigns from your terminal\" width=\"100%\" /\u003e\n\n# Mailing List CLI\n\n**Newsletter campaigns from your terminal. Built for AI agents.**\n\n\u003cbr /\u003e\n\n[![Star this repo](https://img.shields.io/github/stars/paperfoot/mailing-list-cli?style=for-the-badge\u0026logo=github\u0026label=%E2%AD%90%20Star%20this%20repo\u0026color=yellow)](https://github.com/paperfoot/mailing-list-cli/stargazers)\n\u0026nbsp;\u0026nbsp;\n[![Follow @longevityboris](https://img.shields.io/badge/Follow_%40longevityboris-000000?style=for-the-badge\u0026logo=x\u0026logoColor=white)](https://x.com/longevityboris)\n\n\u003cbr /\u003e\n\n[![License: MIT](https://img.shields.io/badge/License-MIT-blue?style=for-the-badge)](LICENSE)\n[![Rust](https://img.shields.io/badge/Rust-1.85+-orange?style=for-the-badge\u0026logo=rust\u0026logoColor=white)](https://www.rust-lang.org/)\n[![Status: v0.4.5 email-cli v0.6](https://img.shields.io/badge/Status-v0.4.5_email--cli_v0.6-orange?style=for-the-badge)](#status)\n[![Built on Resend](https://img.shields.io/badge/Built_on-Resend-000000?style=for-the-badge)](https://resend.com)\n\n---\n\nA single Rust binary that gives an AI agent (or a human at a terminal) a real mailing list to run. Campaigns, segments, A/B tests, click tracking, double opt-in, hard-bounce auto-suppression, one-click unsubscribe — all driven by JSON-emitting commands the agent can pick up without an MCP server, schema file, or browser dashboard.\n\n`mailing-list-cli` is the orchestration layer. It owns campaigns, segments, templates, suppression, double opt-in, A/B testing, and analytics. It does **not** talk to [Resend](https://resend.com) directly — every send, every audience operation, every webhook event flows through its sister tool [`email-cli`](https://github.com/paperfoot/email-cli), which is the sole Resend API client. Two binaries, one job each.\n\nThink Beehiiv or MailChimp, except it lives at `~/.local/bin/mailing-list-cli` and an agent uses it the same way you'd use `git`.\n\n[Why](#why-this-exists) | [Status](#status) | [Planned Commands](#planned-commands) | [Architecture](#architecture) | [Sister Project](#sister-project) | [Research](#research)\n\n\u003c/div\u003e\n\n## Why This Exists\n\nAI agents can already send single emails. Running a mailing list is a different sport.\n\nSending one newsletter to fifty thousand people involves things one-off email tools never touch: deduplicating against a global suppression list, honoring unsubscribes within minutes, watching the soft-bounce counter, throttling the burst so the ESP doesn't suspend you, A/B testing two subject lines on a five-percent slice and promoting the winner, segmenting by tag and engagement, signing the one-click unsubscribe header per RFC 8058, and writing every send result back to local state so the next campaign knows who not to email.\n\nThe existing options for an agent are bad:\n\n- **MailChimp / Beehiiv / Klaviyo** — browser-first. Their APIs exist but were designed for Zapier and websites, not for an agent shelling out forty times per second.\n- **Resend's own dashboard** — fine for humans, but the Broadcasts API alone doesn't cover the full list-management surface (no bulk import, no programmatic suppression list, no double opt-in workflow, no A/B testing, no segments-by-engagement).\n- **MCP servers wrapping the above** — a 32× context overhead per call versus the same operation as a CLI command, and the agent has to learn a new tool schema for every platform.\n\n`mailing-list-cli` is the missing layer. It owns the campaign / segmentation / template / suppression / opt-in / A/B / analytics surface. For the actual SMTP-side work — sending, audience CRUD, webhook ingestion, Resend API authentication — it shells out to [`email-cli`](https://github.com/paperfoot/email-cli). An agent runs `mailing-list-cli agent-info` once, learns every command, and gets to work.\n\n## Status\n\n\u003e **v0.4.5 — design-gate enforcement on top of v0.4.4.**\n\u003e\n\u003e `template create --from-file` now refuses browser/React/JSX handoffs and\n\u003e lint-error sources by default. The verdict comes from `template inspect`,\n\u003e which used to be advisory only. Override with `--force` for deliberate\n\u003e incremental editing.\n\u003e\n\u003e `broadcast send` re-runs the same design check at preflight and refuses\n\u003e error-level findings (`browser_or_jsx_source`, `browser_script_dependency`)\n\u003e before a single email-cli call. Override with `--allow-design-errors` or\n\u003e set `[guards].block_design_errors = false` in `config.toml`.\n\u003e\n\u003e The JSX heuristic now catches modern frameworks without an explicit React\n\u003e import (Next 13+, Vite, `export default function`, `\u003cCapitalized` component\n\u003e tags) so the gate fires on the handoffs people are actually shipping in\n\u003e 2026, not just `import React from 'react'`.\n\u003e\n\u003e Everything else from v0.4.4 still applies: `--confirm`-gated sends,\n\u003e resumable batch chunks of 100, RFC 8058 one-click unsubscribe headers,\n\u003e body unsubscribe links opt out of UTM rewriting, plain-text alternatives\n\u003e preserve anchor URLs as `Label (URL)`, integrated `event poll` tracking,\n\u003e bundled agent skill via `skill install`, and the explicit email design\n\u003e rules in `agent-info` and the embedded skill.\n\n## Planned Commands\n\nSynthesized from the research swarm. Directional, not final — every entry below is grounded in a feature real list operators rely on day-to-day.\n\n### Lists, Contacts, Tags\n\n| Command | What it does |\n|---|---|\n| `list create \u003cname\u003e` | Create a list (Resend audience) |\n| `list ls` | Show all lists with subscriber counts |\n| `contact add \u003cemail\u003e --list \u003cid\u003e` | Add a contact |\n| `contact import \u003cfile.csv\u003e --list \u003cid\u003e` | Bulk import with rate-limit-aware chunking |\n| `contact tag \u003cemail\u003e \u003ctag\u003e` | Tag a contact |\n| `contact ls --filter \u003cexpr\u003e` | Filter contacts by tag, list, status, engagement |\n| `contact erase \u003cemail\u003e` | GDPR hard-delete (PII removed, suppression entry retained) |\n\n### Segments\n\n| Command | What it does |\n|---|---|\n| `segment create \u003cname\u003e --filter-json \u003cjson\u003e` | Save a dynamic segment from a JSON AST filter |\n| `segment ls` | All segments with live member counts |\n| `segment members \u003cid\u003e` | List currently-matching contacts |\n\nFilter expressions are a JSON AST (v0.2 dropped the string DSL — agents emit JSON directly). Example: `{\"kind\":\"and\",\"children\":[{\"kind\":\"atom\",\"atom\":{\"type\":\"tag\",\"pred\":{\"kind\":\"has\",\"name\":\"vip\"}}},{\"kind\":\"atom\",\"atom\":{\"type\":\"engagement\",\"atom\":{\"kind\":\"opened_last\",\"duration\":{\"value\":30,\"unit\":\"days\"}}}}]}`. See `src/segment/ast.rs` for the full shape. Segments re-evaluate at send time.\n\n### Templates\n\n| Command | What it does |\n|---|---|\n| `template create \u003cname\u003e --subject \"...\" [--from-file \u003cpath\u003e] [--force]` | Create a plain-HTML template (or scaffold). `--from-file` enforces the design + lint gate; `--force` overrides for deliberate non-final imports |\n| `template ls` | List local templates |\n| `template show \u003cname\u003e` | Print the raw HTML source |\n| `template render \u003cname\u003e --with-data \u003cfile\u003e` | Render to a JSON envelope; sendable HTML is in `.data.html` |\n| `template preview \u003cname\u003e --with-data \u003cfile\u003e [--out-dir \u003cpath\u003e] [--open]` | Write preview to disk and optionally open in the browser |\n| `template inspect \u003cname\u003e` / `template inspect --from-file \u003cpath\u003e` | Classify stored templates or design handoff files as email-ready, lint-fixable, or browser/React prototypes that need conversion |\n| `template lint \u003cname\u003e` | 6-rule compliance check (CAN-SPAM + size + XSS allowlist + forbidden tags) |\n\nTemplates are plain HTML with `{{ var }}` merge tags and `{{#if }}` conditionals. Triple-brace `{{{ name }}}` is an allowlisted XSS-safe escape hatch, reserved for `unsubscribe_link` and `physical_address_footer` only. The send pipeline hard-fails on any unresolved placeholder before a single email goes out.\n\n`template render` is for machine inspection and always prints the full CLI JSON envelope. Do not pass its whole stdout to `email-cli --html`; use `template preview` for rendered files, `broadcast preview` for test emails, or extract `jq -r '.data.html'` after checking `lint_errors == 0`.\n\nRendered plain-text alternatives preserve links as `Label (URL)`. Generated unsubscribe anchors include `data-utm=\"off\"` so the compliance link in the body is not rewritten with tracking parameters, while normal CTA links still receive campaign UTM tags.\n\n`template lint` warns on fragile semantic layout tags such as `\u003cmain\u003e` and on\nunstyled text links, because email clients may collapse browser-style layout\nand fall back to default blue/purple hyperlinks.\n\nFor designer handoffs and browser prototypes, run `template inspect --from-file\n\u003cpath\u003e` before importing. It detects React/JSX/Babel/script dependencies,\nexternal CSS, style blocks, flex/grid layout, missing table structure, and\nmissing compliance placeholders. A `browser_prototype_needs_conversion` verdict\nmeans the file is design direction only; convert it into standalone static\nemail HTML before `template create` or any broadcast send.\n\nv0.4.5 enforces the same check at the import boundary and at the send\nboundary. `template create --from-file` refuses imports whose verdict is\n`browser_prototype_needs_conversion` or whose lint reports any errors\n(error codes `template_create_design_blocked` / `template_create_lint_blocked`,\noverride with `--force`). `broadcast send` re-runs the design scanner at\npreflight and refuses error-level findings (error code\n`template_has_design_errors`, override with `--allow-design-errors`). The two\noverride flags exist because capable agents may have a deliberate reason to\nland a half-finished template or to ship something that the heuristic misclassifies;\nthey are not for routine use.\n\n### Broadcasts (Campaigns)\n\n| Command | What it does |\n|---|---|\n| `broadcast create --template \u003cname\u003e --to \u003csegment\u003e` | Stage a broadcast |\n| `broadcast preview \u003cid\u003e --to \u003cemail\u003e` | Send a single test |\n| `broadcast schedule \u003cid\u003e --at \u003ctime\u003e` | Schedule for later |\n| `broadcast send \u003cid\u003e --dry-run [--allow-design-errors]` | Project recipient counts and preflight checks without sending |\n| `broadcast send \u003cid\u003e --confirm [--force-unlock] [--allow-design-errors]` | Send now, after explicit approval |\n| `broadcast cancel \u003cid\u003e` | Cancel a scheduled broadcast |\n| `broadcast ab \u003cid\u003e --vary subject --variants 2 --winner-by opens` | Configure A/B test |\n| `broadcast ls` | Recent broadcasts and their statuses |\n\nLarge broadcasts are sent in chunks of 100 through `email-cli batch send`.\nEach chunk is recorded in `broadcast_send_attempt` before the ESP call and\napplied after acknowledgement, so resume skips already-sent recipients instead\nof repeating them. To test a 1,000-recipient slice, target a list or segment\nwith those 1,000 recipients, run `broadcast send \u003cid\u003e --dry-run`, then send\nthat separate test broadcast with `--confirm`.\n\n### Analytics\n\n| Command | What it does |\n|---|---|\n| `report show \u003cbroadcast-id\u003e` | Opens, clicks, bounces, unsubscribes, complaints, CTR |\n| `report links \u003cbroadcast-id\u003e` | Click count per link |\n| `report engagement --segment \u003cid\u003e` | Engagement scores across a segment |\n| `report deliverability` | Domain health: bounce rate, complaint rate, DMARC pass rate |\n\nClick counting is integrated through `event poll`. Per-link CTA reporting is\nrecorded when the upstream `email-cli email list` row includes `click.link` or\n`link`; if the upstream row only exposes `last_event=clicked`, the aggregate\n`clicked_count` updates but `report links` cannot infer the clicked URL.\n\nTracking is a local mirror, not a direct Resend API call from this binary:\n\n1. `mailing-list-cli webhook poll` (alias: `event poll`) asks `email-cli email list` for recent email rows.\n2. `email-cli` is the only tool that talks to Resend. It returns each email id plus `last_event` and, when available, click payloads such as `click.link`.\n3. `mailing-list-cli` matches the returned Resend email id to `broadcast_recipient.resend_email_id`, writes an idempotent row to the local `event` table, stores CTA link rows in `click` when the URL is present, and updates the broadcast counters.\n4. Agents read the mirror with `report show \u003cbroadcast-id\u003e`, `report links \u003cbroadcast-id\u003e`, `report engagement`, and `report deliverability`.\n\n### Compliance \u0026 Hygiene\n\n| Command | What it does |\n|---|---|\n| `optin start \u003cemail\u003e --list \u003cid\u003e` | Send a double opt-in confirmation |\n| `optin verify \u003ctoken\u003e` | Confirm an opt-in |\n| `unsubscribe \u003cemail\u003e` | Honor an unsubscribe (writes to global suppression) |\n| `suppression ls` | View the global suppression list |\n| `suppression import \u003cfile\u003e` | Import suppressions from another platform |\n| `dnscheck \u003cdomain\u003e` | Verify SPF / DKIM / DMARC alignment before first send |\n\n### Webhook ingestion\n\n| Command | What it does |\n|---|---|\n| `webhook poll` / `event poll` | Poll `email-cli email list` for new delivery/bounce/click events and mirror them locally |\n\nv0.2 dropped the long-running HTTP listener (`tiny_http` + Svix HMAC verifier) — running an inbound HTTP server behind NAT is hostile to a local CLI. Polling via `email-cli email list` covers the same use case without the tunneling requirement.\n\n### Agent tooling\n\n| Command | What it does |\n|---|---|\n| `agent-info` | Self-describing JSON manifest of every command, flag, and exit code |\n| `skill install` | Drop the embedded skill file into Claude / Codex / Gemini paths |\n| `skill status` | Show whether installed skill copies match the binary |\n| `update` | Self-update from GitHub Releases |\n\nRelease automation is documented in [docs/release.md](./docs/release.md). This\nis a Rust binary: `cargo` and Homebrew are the supported package channels; there\nare no `uv` or `bun` artifacts.\n\n## Architecture\n\nThree layers, each replaceable.\n\n```\n┌──────────────────────────────────────────┐\n│             Your Agent / You             │\n│         (Claude, Codex, Gemini)          │\n└────────────────┬─────────────────────────┘\n                 │  CLI commands, JSON in/out\n                 ▼\n┌──────────────────────────────────────────┐\n│             mailing-list-cli             │\n│   campaigns · segments · A/B · opt-in    │\n│   suppression · analytics · templates    │\n└────────────┬─────────────────┬───────────┘\n             │                 │\n             │  shells out     │  reads/writes\n             │  for sending    │  local state\n             ▼                 ▼\n   ┌──────────────────┐  ┌────────────┐\n   │     email-cli    │  │   SQLite   │\n   │ • Resend API     │  │ templates  │\n   │ • send / batch   │  │ campaigns  │\n   │ • audiences      │  │ suppression│\n   │ • contacts       │  │ events     │\n   │ • events / hooks │  │ optin tok. │\n   └─────────┬────────┘  └────────────┘\n             │\n             ▼\n       ┌──────────┐\n       │  Resend  │\n       └──────────┘\n```\n\n- **`mailing-list-cli` is the orchestration layer.** It composes campaigns, computes segments, renders templates, enforces suppression, runs A/B tests, and aggregates analytics. It has zero Resend code.\n- **`email-cli` is the transport layer.** It is the only binary that talks to Resend's API. `mailing-list-cli` shells out to it for every send, every audience operation, and every event read.\n- **Local SQLite** stores the things `email-cli` doesn't track: templates, campaign metadata, the suppression list, double opt-in tokens, segment definitions, engagement aggregates, and a mirror of recent events polled from `email-cli`.\n- **Plain HTML + hand-rolled `{{ var }}` substitution** for templates. v0.2 dropped MJML, Handlebars, css-inline, html2text, and the YAML frontmatter variable schema — all designed-for-humans safety nets that the agent-loop preview renders unnecessary. Merge tags are Mustache-style `{{ first_name }}` (HTML-escaped) with a hard-coded triple-brace allowlist for `{{{ unsubscribe_link }}}` and `{{{ physical_address_footer }}}`. The compile pipeline is ~500 lines of Rust across `src/template/{subst,render}.rs` with 14 runtime crate dependencies total.\n\nBuilt following the [agent-cli-framework](https://github.com/paperfoot/agent-cli-framework) patterns: structured JSON output (auto-detected via `IsTerminal`), semantic exit codes (`0/1/2/3/4`), self-describing `agent-info`, no interactive prompts, ever.\n\n## Sister Project\n\n[`email-cli`](https://github.com/paperfoot/email-cli) — the 1:1 messaging counterpart. Send, reply, draft, sync. Same conventions, same agent-friendly philosophy. Use both: `email-cli` for personal correspondence, `mailing-list-cli` for newsletters and campaigns.\n\n## Research\n\nFive research dossiers ground the design. Read them in [/research](./research):\n\n1. [Modern creator newsletters](./research/01-modern-creator-newsletters.md) — Beehiiv, Buttondown, Substack\n2. [Marketing platforms](./research/02-marketing-platforms.md) — MailChimp, MailerLite, Kit\n3. [Resend native capabilities](./research/03-resend-native.md) — what's already there vs the gap to fill\n4. [Deliverability and compliance](./research/04-deliverability-compliance.md) — the non-negotiables for safe scale\n5. [Email templates for agents](./research/05-templates.md) — format choice, merge syntax, authoring guidelines\n\n## Contributing\n\nThe spec isn't written yet. If you want to shape it, open a discussion or comment on the research files. Once the binary lands, contributions to commands, tests, and docs are welcome.\n\n## License\n\nMIT — see [LICENSE](LICENSE).\n\n---\n\n\u003cdiv align=\"center\"\u003e\n\nBuilt by [Boris Djordjevic](https://github.com/longevityboris) at [Paperfoot AI](https://paperfoot.com)\n\n\u003cbr /\u003e\n\n**If this is useful or interesting:**\n\n[![Star this repo](https://img.shields.io/github/stars/paperfoot/mailing-list-cli?style=for-the-badge\u0026logo=github\u0026label=%E2%AD%90%20Star%20this%20repo\u0026color=yellow)](https://github.com/paperfoot/mailing-list-cli/stargazers)\n\u0026nbsp;\u0026nbsp;\n[![Follow @longevityboris](https://img.shields.io/badge/Follow_%40longevityboris-000000?style=for-the-badge\u0026logo=x\u0026logoColor=white)](https://x.com/longevityboris)\n\n\u003c/div\u003e\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpaperfoot%2Fmailing-list-cli","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpaperfoot%2Fmailing-list-cli","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpaperfoot%2Fmailing-list-cli/lists"}