From e238bde03663c3d2a79f8a644741132d47009b94 Mon Sep 17 00:00:00 2001 From: Will Estes Date: Thu, 23 Jul 2026 22:05:49 -0400 Subject: [PATCH] Send altbody so campaigns actually go out multipart/alternative 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 --- README.md | 7 +++++-- internal/listmonk/listmonk.go | 12 +++++++++++- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index a1a4f3a..81f34cd 100644 --- a/README.md +++ b/README.md @@ -54,8 +54,11 @@ attachments: --- Campaign body goes here, in Markdown. It's sent to listmonk with -content_type "markdown" — listmonk renders the HTML *and* derives the -plaintext alternative itself. +content_type "markdown" for the HTML part; listmonk never derives a +plaintext alternative on its own, so this same source is also sent +verbatim as `altbody` — recipients on plaintext-only clients see the +raw Markdown (and personalization still resolves, since the `{{ }}` +expressions are still there), rather than getting an HTML-only email. ``` - **`subject`** — required. diff --git a/internal/listmonk/listmonk.go b/internal/listmonk/listmonk.go index 8387a14..9dae237 100644 --- a/internal/listmonk/listmonk.go +++ b/internal/listmonk/listmonk.go @@ -335,7 +335,17 @@ func (in CampaignInput) payload() map[string]any { "lists": in.ListIDs, "content_type": "markdown", "body": in.Body, - "type": in.Type, + // listmonk never derives a plaintext alternative from body/HTML on its + // own (models/campaigns.go only compiles AltBodyTpl, and + // internal/manager/message.go only emits a text/plain part, when + // altbody is explicitly non-null) — confirmed against the deployed + // v6.2.0 source. Reusing the same raw Markdown+template source as the + // altbody gets it personalized identically to body (AltBodyTpl compiles + // whenever the text contains {{ }} expressions), at the cost of + // Markdown syntax like *emphasis* showing up literally in plaintext + // clients — an acceptable, well-established tradeoff for Markdown. + "altbody": in.Body, + "type": in.Type, // listmonk's create/update handlers default an omitted messenger to // "email" before validating, but the test-send handler validates the // raw request body as-is — an omitted messenger there fails with