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:
parent
2e4388a803
commit
a32a3b05a0
4 changed files with 133 additions and 3 deletions
|
|
@ -41,6 +41,7 @@ if ROOT not in sys.path:
|
|||
sys.path.insert(0, ROOT)
|
||||
|
||||
from scripts._email_exclusions import BLOCKED_EMAIL_DOMAINS
|
||||
from scripts._email_plaintext import html_to_text
|
||||
|
||||
LOG = logging.getLogger("build_trucking_campaigns")
|
||||
|
||||
|
|
@ -551,6 +552,21 @@ def import_subscribers(list_id: int, subscribers: list[dict]) -> int:
|
|||
return added
|
||||
|
||||
|
||||
def _altbody_for(base: dict, body: str | None = None) -> str:
|
||||
"""Plaintext (text/plain) part for a campaign.
|
||||
|
||||
Listmonk only emits multipart/alternative when altbody is set; HTML-only
|
||||
mail is a spam-score signal. The source/base campaigns have no altbody, so
|
||||
derive one from the HTML body. `body` overrides base["body"] for test sends
|
||||
where merge fields were already substituted.
|
||||
"""
|
||||
existing = (base.get("altbody") or "").strip()
|
||||
if existing:
|
||||
return existing
|
||||
html = body if body is not None else base.get("body", "")
|
||||
return html_to_text(html)
|
||||
|
||||
|
||||
def create_and_schedule_campaign(
|
||||
base: dict,
|
||||
list_id: int,
|
||||
|
|
@ -566,7 +582,7 @@ def create_and_schedule_campaign(
|
|||
"type": "regular",
|
||||
"content_type": base["content_type"],
|
||||
"body": base["body"],
|
||||
"altbody": base.get("altbody"),
|
||||
"altbody": _altbody_for(base),
|
||||
"template_id": base["template_id"],
|
||||
"tags": base.get("tags") or [],
|
||||
"messenger": base.get("messenger") or "email",
|
||||
|
|
@ -611,7 +627,7 @@ def send_test(base: dict, campaign_id: int, sample_row: tuple, label: str, tz: s
|
|||
"name": base.get("name", "Test"), "subject": subj,
|
||||
"lists": list_ids, "from_email": base["from_email"],
|
||||
"type": "regular", "content_type": base["content_type"],
|
||||
"body": body, "altbody": base.get("altbody"),
|
||||
"body": body, "altbody": _altbody_for(base, body),
|
||||
"template_id": base["template_id"],
|
||||
"tags": base.get("tags") or [], "messenger": base.get("messenger") or "email",
|
||||
"headers": base.get("headers") or REPLY_TO_HEADERS,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue