mailing-list-cli is a single Rust binary for running a mailing list from the
terminal. Campaigns, segments, templates, suppression, analytics, webhook
ingestion — all exposed as JSON-emitting subcommands so an agent can drive
them without an MCP server, schema file, or browser dashboard.
- Version: v0.4.5 (design-gate enforcement on top of v0.4.4)
- Hosted unsubscribe companion:
web/is SharpClap, a Next.js app on Vercel that serves the public/u/<token>unsubscribe page and the RFC 8058 one-click POST endpoint a local CLI cannot.unsubscribe syncmirrors its recorded opt-outs into local SQLite suppression (--endpoint,--api-key-env,--after,--limit,--max-pages,--dry-run). Token secret:MLC_UNSUBSCRIBE_SECRET; sync key read fromMLC_UNSUBSCRIBE_SYNC_KEY, falling back toSYNC_API_KEY. See web/README.md. - Research: see /research for the five dossiers that informed the original design
- Recent plans:
- v0.2 rearchitecture (shipped as v0.2.0)
- v0.3 production-grade 10k (shipped as v0.3.0)
- v0.3.1 emergency hardening (shipped as v0.3.1)
- v0.4 operator superpowers (shipped as v0.4.0)
- v0.4.1 patch: explicit broadcast approval, safer template-render guidance, click payload passthrough, release automation
- v0.4.2 patch: unsubscribe body links opt out of UTM rewriting, plain-text alternatives preserve anchor URLs
- v0.4.3 patch:
skill install/skill statusinstall the embedded Codex/Claude/Gemini skill - v0.4.4 patch: embedded skill and
agent-infoinclude explicit email design rules plustemplate inspectfor browser/design handoffs - v0.4.5 patch:
template create --from-fileandbroadcast sendpreflight enforce the design + lint gate (override with--force/--allow-design-errors); JSX heuristic catches modern frameworks without an explicit React import; single design-rule scanner shared by inspect/create/send - v0.5 deliverability guard — planned, not shipped: send-ramp governor + bounce/complaint circuit breaker
What "production-grade" means in this codebase:
- Send pipeline reliability: 429/5xx retry with exponential backoff [500ms, 1s, 2s, 4s], up to 4 retries; per-chunk DB transactions; preloaded suppression
HashSetfor O(1) lookups; resumable sends with atomic broadcast lock CAS (no double-send race even on concurrent invocation); v0.3.2 write-aheadbroadcast_send_attempttable that records ESP acceptance BEFORE the local recipient UPDATE, so a crash between the two cannot cause duplicate sends on resume — it reconciles from the stored response. - Explicit send approval: real
broadcast sendandbroadcast resumerequire--confirm. Usebroadcast send <id> --dry-runfirst for recipient counts and preflight checks;--dry-runnever sends and does not require confirmation. - Large-send behavior:
broadcast sendusesemail-cli batch sendchunks of 100 recipients. For a 1,000-recipient trial, target a separate 1,000-member list/segment; do not reuse the final full-list broadcast as a partial-send test. - Subprocess safety: every
email-clicall has a 120-second default timeout (MLC_EMAIL_CLI_TIMEOUT_SECenv var to override). Hung subprocesses are killed via SIGKILL and surfaced asemail_cli_timeouttransient errors that feed the existing retry path. - Schema safety:
Db::openfails fast (exit code 2,db_schema_too_new) when the on-disk schema version is newer than what this binary supports — no more silent column-mismatch errors after a binary downgrade. - Unsubscribe link security:
MLC_UNSUBSCRIBE_SECRETis required (v0.3.2) —broadcast sendrefuses to sign tokens with a dev fallback. HMAC-SHA256. - GDPR compliance:
contact erase --confirmwrites agdpr_erasuresuppression tombstone before deleting the contact row (atomic transaction; the email is never momentarily absent from both). - Operator escape hatches:
broadcast send <id> --confirm --force-unlockoverrides a held send lock when the previous process is confirmed dead (use only afterps aux | grep mailing-list-cli). - Honest limitations: the 30-day complaint/bounce rate guards in
broadcast sendpreflight are computed from the localeventtable, which is populated bywebhook pollpaginatingemail-cli email listby email ID and reading onlylast_eventper row. The guards still fire and remain useful safety nets, but the exact percentages are best-effort approximations — seeagent-info → known_limitationsand the docstring onhistorical_send_rates. Proper fix is v0.5+ pending an upstream change to email-cli or a rolling-window snapshot diff. Source: GPT Pro F3.2 from 2026-04-09 hardening review. - Click reporting limitation:
report showcan count clicked emails fromlast_event=clicked.report linksneeds link payload fromemail-cli(click.linkorlink); the poll path stores it when present, but cannot infer the CTA URL fromlast_eventalone. - Tracking workflow: run
webhook poll/event pollafter sends. The CLI shells out toemail-cli email list, mapslast_eventinto localeventrows, storesclickrows when link payload exists, then reports from SQLite viareport show/report links. - Deliverability footer behavior: the send pipeline emits
List-UnsubscribeandList-Unsubscribe-Postheaders, and the body unsubscribe anchor is rendered with inline styling plusdata-utm="off"so tracking parameters are not added to compliance links. The plain-text MIME alternative preserves anchor destinations asLabel (URL)instead of dropping URLs. - Template quality warnings:
template lintwarns on unstyled<a href>links and fragile semantic layout tags such as<main>, because Gmail and other clients do not behave like full browsers. - Skill distribution:
mailing-list-cli skill installwrites the embeddedmailing-list-cliskill to Codex, Claude, Gemini, and.agentsskill roots.MLC_SKILL_ROOTScan override the install roots for tests or custom setups. - Agent-facing design guidance:
agent-info.template_design_rulesand the embedded skill tell agents to use table wrappers, visible margins, inline link styles, restrained typography, plain-text inspection, and broadcast preview before real sends. - Design handoff inspection: run
template inspect --from-file <path>on browser/React/JSX/design-canvas handoffs beforetemplate create. Abrowser_prototype_needs_conversionverdict means the file is design direction only; convert it into standalone table-based inline HTML before linting, previewing, or sending. - Design + lint gate (v0.4.5):
template create --from-filerefuses imports whose verdict isbrowser_prototype_needs_conversionor whose lint reports any errors — error codestemplate_create_design_blocked/template_create_lint_blocked, override with--force.broadcast sendre-runs the design check at preflight and refuses error-level findings — error codetemplate_has_design_errors, override with--allow-design-errorsor set[guards].block_design_errors = falseinconfig.toml. The two error codes use distinct names so an agent can route a JSX handoff through conversion without confusing it with a substantive lint failure. - Hosted unsubscribe sync: unsubscribe clicks land on the SharpClap companion and stay in its Postgres until pulled. Run
unsubscribe syncbefore every real send so hosted opt-outs reach local suppression;--dry-runverifies the endpoint and key without writing suppression or cursor state.
This project follows the agent-cli-framework patterns:
- Structured JSON output, auto-detected via
IsTerminal - Semantic exit codes:
0success,1transient (retry),2config (fix setup),3bad input,4rate limited - Self-describing via
agent-info— one command returns the full capability manifest - No interactive prompts, ever. v0.2 removed the v0.1
template editcommand because it violated this invariant; agents useWrite/Edittools directly on files passed viatemplate create --from-file. - Local-first state under
~/.local/share/mailing-list-cli/ - Config under
~/.config/mailing-list-cli/config.toml - Cache under
~/.cache/mailing-list-cli/(always safe torm -rf) - Integrated preview.
template preview <name>writes rendered HTML to disk and optionally opens it in the default browser. This is the core iteration primitive — it replaces every "catch the mistake upfront" safety net the v0.1 system had. - Release automation. Pushing a
vX.Y.Ztag runs.github/workflows/release.yml, verifies the tag matchesCargo.toml, publishes crates.io, updates the Homebrew tap, and creates/updates a GitHub release. Required repo secrets are listed indocs/release.md. - Template render is not send-ready stdout.
template render <name>emits the full JSON envelope. If you need the rendered HTML for custom tooling, extract.data.htmlafter checkingstatus == "success"anddata.lint_errors == 0; never pass the whole stdout toemail-cli --html. Preferbroadcast preview <id> --to <email>for inbox tests because it runs the same strict render/compliance path as a real send. - Template inspect is the handoff gate.
template inspect <name>ortemplate inspect --from-file <path>classifies HTML/JSX/browser prototypes asemail_ready,email_candidate_with_warnings,not_send_ready, orbrowser_prototype_needs_conversion, and returns concrete conversion steps for agents.
mailing-list-cli agent-infoReturns a JSON manifest of every subcommand, every flag, every exit code. No documentation drift, no MCP server, no schema file an agent has to load up front.
mailing-list-cli does not talk to Resend directly. Every send, every audience operation, every event read goes through email-cli, which is the sole Resend API client. Both binaries must be on $PATH.
This split exists so neither tool has to do the other's job:
email-cliowns the Resend API surface, accounts, profiles, transports, the inbox, the webhook listener.mailing-list-cliowns campaigns, segmentation, templates, suppression, analytics, revenue attribution, and the hosted-unsubscribe sync.
For an agent: use email-cli for personal correspondence, mailing-list-cli for newsletters and campaigns. They cooperate on the same Resend account but each one stays in its lane.