email: add plaintext MIME part + stable Message-ID hostname

Two deliverability hardening fixes from the email audit:

1. Plaintext (altbody): all campaigns were HTML-only. Listmonk only emits
   multipart/alternative when altbody is set, and HTML-only bulk mail is a
   spam-score signal. New scripts/_email_plaintext.py renders a readable
   text/plain part from the HTML body (dependency-free; preserves Listmonk
   {{ .Subscriber }}/{{ UnsubscribeURL }} template tags, turns links into
   'text (url)'). Wired into the trucking builder (and thus UCR + IFTA, which
   reuse create_and_schedule_campaign) and the healthcare builder.

2. Stable container hostname: Listmonk derived its Message-ID from the random
   docker container id -> @localhost.localdomain (spam-score signal). Pin both
   listmonk + listmonk-hc hostname to perfwest.performancewest.net, matching
   Listmonk's SMTP hello_hostname.

Part of the email-deliverability incident hardening.
This commit is contained in:
justin 2026-06-17 20:09:02 -05:00
parent 2e4388a803
commit a32a3b05a0
4 changed files with 133 additions and 3 deletions

View file

@ -300,6 +300,11 @@ services:
listmonk:
image: listmonk/listmonk:latest
# Stable hostname so the Message-ID Listmonk derives from the container OS
# hostname is perfwest.performancewest.net, NOT the random docker container
# id -> @localhost.localdomain (a spam-score signal; see deliverability
# runbook). Matches Listmonk's SMTP hello_hostname.
hostname: perfwest.performancewest.net
ports:
- "9100:9000"
environment:
@ -335,6 +340,9 @@ services:
# mynetworks 172.16/12). host.docker.internal is mapped for convenience.
listmonk-hc:
image: listmonk/listmonk:latest
# Stable hostname -> Message-ID @perfwest.performancewest.net, not the random
# container id -> @localhost.localdomain (spam-score signal). See listmonk above.
hostname: perfwest.performancewest.net
ports:
- "9101:9000"
extra_hosts: