I saved you $4,200/year and it'll bankrupt Mailchimp
Gmail binned my newsletter while every check that proves a sender is genuine passed. The first of three failures, and every one reported success.
The invoice I stopped paying
Mailchimp Premium is widely reported to start near $350 a month. That is $4,200 a year to send email. I run the same job on a Hetzner box I already pay for, with Listmonk, which is free.
The software was the easy part. It took an afternoon. What Mailchimp actually sells is not the sending. It is the promise that you never meet the three failures below, and not one of them announced itself. Every one returned a success code.
Every spammer on the internet ships my email template
The first confirmation email landed in spam. Gmail’s stated reason: “similar to messages that were identified as spam in the past.”
The obvious suspect is authentication. Three checks cover it, all of them records in DNS, the public directory listing which servers stand behind a domain. SPF, Sender Policy Framework, names the servers allowed to send mail for yours. DKIM, DomainKeys Identified Mail, signs each message so the receiver can prove it was not altered on the way. DMARC, Domain-based Message Authentication, Reporting and Conformance, checks the sender a human sees matches the domain the other two verified.
Mine passed all three:
dkim=pass # signed by news.leblonlabs.com, unaltered in transit
spf=pass # sent from a server that domain authorises
dmarc=pass # the visible sender matches both, so: aligned
That is as clean as email authentication gets. The cause was the message itself. Listmonk’s stock templates ship byte-identical with every install on the internet, spammers included, so my email carried a fingerprint Gmail has spent years learning to distrust: the button, alt="listmonk", and a Powered by listmonk footer sitting on the last screen of my own signup funnel.
Three strings. I replaced the templates with my own design and no third-party branding.
Authentication proves who sent the mail. It does not prove that anyone wanted it.
The comment that turned every email into raw HTML
The new templates fixed the spam problem and broke the email. Subscribers stopped getting a confirmation message and started getting its HTML source, tags and all. It shipped to production that way.
The cause was a comment. I had put a header block at the top of the new template explaining what it was and why. Listmonk reads the first 256 bytes of a template, looks for the literal string <!doctype html, and sends plain text when it cannot find one:
ln := min(len(html), 256)
if !bytes.Contains(bytes.ToLower(h), []byte("<!doctype html")) {
contentType = models.CampaignContentTypePlain
My comment pushed the doctype past byte 256. Nothing errored and nothing retried. The only trace anywhere was one line in the startup log:
init.go:810: system e-mail templates are plaintext
The workflow now refuses to deploy when the doctype falls outside that window. That check is the cheap one. The check that matters greps the boot log for that line afterwards, because it asserts what Listmonk concluded rather than what I meant.
The fix that deployed green and changed nothing
Fixing it took two attempts, for a reason that had nothing to do with the doctype.
The templates are bind-mounted, and Listmonk parses them once at boot. docker compose up -d compared the service definition, found it unchanged, and kept the running container. The corrected files sat on disk unread. Right files, clean deploy, stale behaviour. The app is force-recreated on every run now.
A spam filter can subscribe on your reader’s behalf
Listmonk has a setting, app.show_optin_page, that drops the confirmation page and lets the GET from the email confirm the subscription on its own. It saves a click, and plenty of guides recommend it.
My readers open mail at work, in finance. Proofpoint, Mimecast and Safe Links open every link in an incoming message before the recipient sees it. With that page off, a scanner’s GET confirms a subscription its owner never asked for. That is the exact thing double opt-in exists to prevent, and it leaves no evidence of consent under PECR or GDPR.
Auto-submitting the form with JavaScript was the obvious workaround. Detonation sandboxes render JavaScript. It shrinks the exposure without removing it, in exchange for the whole protection.
So the third click stays. Stock Listmonk puts “Confirm subscription” in the heading and on the button, right after an email button reading the same thing: three identical labels in a row, which reads as a loop rather than progress. I forked the page to give each element a distinct job. Keep the click, make it cheap.
The Takeaways
- Assert on the log line, not on the file. A guard that reads your source proves your intent. A guard that greps the boot log proves what the program decided. The doctype bug passed the first kind.
- Assume a machine opens your reader’s mail first. Corporate filters click every link before a human does. Any flow where a bare GET changes state is a flow those filters can complete on somebody’s behalf.
- Paying does not buy you deliverability. I used Mailchimp before this, and newsletters still landed in spam at times. Most accounts sit on shared IP pools, so your inbox placement rides on strangers you cannot see. That is the same failure as the stock template, one layer up: a reputation you inherit and cannot control. Three strings in your own template, at least, are yours to delete.
Get the next episode first
Every week: the progress unfiltered, plus what I can't post publicly. The build, the number, green or red.