campaignUnchanged compared input.TemplateID (0 when unset) literally
against existing.TemplateID, but listmonk always assigns a real, nonzero
template_id server-side even when create/update omits it. Every campaign
without an explicit template_id was therefore flagged "changed" on every
sync, forever, firing an unwanted extra preview email each time — caught
via reground-campaigns' welcome/campaign.md re-previewing on an unrelated
push. Treat TemplateID 0 as "don't compare" instead. Test fake now mirrors
listmonk's real default-assignment behavior so this class of bug is caught.
Campaign/parseCampaign never fetched altbody, so campaignUnchanged()
compared everything except it -- a campaign synced before v0.1.3
(altbody always null) whose content otherwise hasn't changed would be
judged unchanged forever and never get updated with the new altbody,
even though every subsequent sync now intends to send one.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
listmonk never derives a plaintext part from HTML/Markdown on its own
(models/campaigns.go only compiles AltBodyTpl, and
internal/manager/message.go only emits text/plain, when altbody is
explicitly non-null) -- confirmed against the deployed v6.2.0 source.
Every campaign this tool created was HTML-only as a result, contrary
to the README's now-corrected claim.
Reusing the same raw Markdown+template source as altbody personalizes
it identically to body, since AltBodyTpl compiles whenever the text
contains {{ }} expressions.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Discovered while verifying the TestCampaign fix against real listmonk:
create/update apparently default an omitted messenger to "email"
server-side, but the test-send endpoint validates the raw request body
without that same defaulting, failing with "Unknown messenger .".
eec-campaigns only ever targets email, so send it explicitly instead
of depending on an inconsistency between endpoints.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
listmonk's /api/campaigns/:id/test handler binds the request into the
same campReq struct create/update use and validates it in full, so a
request with only {"subscribers": [...]} fails with "Invalid length
for name" before subscribers is ever read. TestCampaign now sends the
campaign's full CampaignInput alongside subscribers.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Campaign content and listmonk credentials now live in separate
per-domain repos that install this tool at a pinned version, so a
client with access to one domain's content repo has no path to
another domain's secrets. Renames the module to the actual Gitea
host path so `go install` works without vanity-import redirects,
drops the four workflows that assumed campaigns/ content lived here,
adds real go-test CI, and rewrites the README for the tool's new
audience.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Answers "can the unsubscribe template be added via git+API too": commit
listmonk's own stock campaign template (already unsubscribe-capable) as
email-templates/campaign.html, add a small find-or-create-or-update
Template client (confirmed against knadh/listmonk's actual model/handlers),
a new `template` subcommand, and a manual-dispatch-only workflow to push it
in as the default campaign template. Unlike campaign sync there's no
draft/live status to protect, so this is always a safe overwrite.
Read knadh/listmonk's actual Go source (cmd/campaigns.go, cmd/subscribers.go)
instead of relying on incomplete public docs. Two corrections: the media
attach field is "media" (plain IDs), not "media_ids" as originally guessed;
and there's a single query-based bulk list action
(PUT /api/subscribers/query/lists) that applies a segment_query server-side,
so resolveSegment no longer fetches subscriber IDs client-side before
bulk-adding them. Create-status-defaults-to-draft and the test-send
"subscribers" field are now confirmed rather than flagged TODO(verify).
Drives listmonk's real Campaign API from git-authored Markdown+frontmatter,
so broadcast/segment emails get listmonk's mature unsubscribe/bulk-send/
attachment handling instead of reimplementing it. sync only ever creates or
updates a draft (idempotent, diff-based, refuses to touch a non-draft
campaign); a pushed send/<slug> tag or manual workflow run is the only way
to actually trigger a send. Includes list-name resolution, segment_query
materialization into managed lists, content-hash-deduped attachment
uploads, and an automatic post-sync preview email.