fix(email): add text/plain part to every transactional + telecom email
All transactional/worker senders built multipart/alternative (or mixed) messages with ONLY an HTML part. A single-part multipart/alternative is malformed and HTML-only mail is a spam-score signal -- the same class of deliverability bug that hurt the campaign pipeline, but on the telecom / filing / customer-transactional path (499-Q reminders, RMD/FCC filing review links, intake/completion/delivery emails, commissions, etc). - worker_email.send_worker_email: auto-derive plaintext from HTML when caller omits text= (fixes the shared helper for all current+future use) - 16 rolled-their-own senders in scripts/workers/** + scripts/formation/ document_delivery.py: attach html_to_text(...) plaintext sibling before the HTML part (job_server + document_delivery wrap text+html in an alternative sub-part so PDFs still attach to the mixed root) - api/src/email.ts: add dependency-free htmlToText() and default sendEmail text to it (fixes checkout/webhook HTML-only sends) Verified: all py files compile + import at runtime, api tsc passes, htmlToText handles hrefs/lists/entities, 11 plaintext unit tests pass. Telecom campaign 407 (Jun 8) was HTML-only + sent in the DKIM-broken window -> 384 sent / 0 clicks (same junked-mail signature).
This commit is contained in:
parent
899b880e7f
commit
b375385efd
19 changed files with 114 additions and 8 deletions
|
|
@ -17,10 +17,21 @@ from __future__ import annotations
|
|||
import logging
|
||||
import os
|
||||
import smtplib
|
||||
import sys
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.text import MIMEText
|
||||
from email.mime.application import MIMEApplication
|
||||
|
||||
# Shared HTML -> plaintext converter (scripts/_email_plaintext.py). Adding a
|
||||
# text/plain part to every message is a deliverability win: a multipart/
|
||||
# alternative with ONLY an HTML part is malformed, and HTML-only mail is a
|
||||
# spam-score signal. Import is path-tolerant (module run from /app or scripts/).
|
||||
try:
|
||||
from scripts._email_plaintext import html_to_text
|
||||
except ImportError: # pragma: no cover - fallback when scripts/ is on sys.path
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
from _email_plaintext import html_to_text # type: ignore
|
||||
|
||||
LOG = logging.getLogger("workers.email")
|
||||
|
||||
SMTP_HOST = os.getenv("SMTP_HOST", "co.carrierone.com")
|
||||
|
|
@ -53,8 +64,13 @@ def send_worker_email(
|
|||
msg["Subject"] = subject
|
||||
|
||||
alt = MIMEMultipart("alternative")
|
||||
if text:
|
||||
alt.attach(MIMEText(text, "plain"))
|
||||
# Always include a text/plain part. If the caller did not supply one,
|
||||
# derive it from the HTML so the message is a well-formed multipart/
|
||||
# alternative (HTML-only is malformed + a spam signal). RFC 2046: list
|
||||
# the plain part first (least->most preferred).
|
||||
plain = text if text else html_to_text(html)
|
||||
if plain:
|
||||
alt.attach(MIMEText(plain, "plain"))
|
||||
alt.attach(MIMEText(html, "html"))
|
||||
msg.attach(alt)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue