Send a plain-text part and the headers spam filters expect (1.2.0) #7

Merged
joeyr merged 1 commit from feat/deliverability into main 2026-10-05 20:26:05 -05:00
Owner

Spam filters count a missing Date or Message-ID and an HTML-only body against a message (upstream issues #168 and #169). This adds them, plus a proper plain-text part.

  • smtp/mime_message.go (moved out of sender.go): multipart/alternative with text/plain and then text/html, Date, a random Message-ID on the sender's domain, and List-Unsubscribe (a mailto: to unsubscribe_email, left out when that is empty). The subject and display names are RFC 2047-encoded. Both parts are quoted-printable, which also keeps every line under SMTP's 998-character limit; the HTML used to go out unencoded.
  • template/text_email.go and plaintext/newsletter.txt: the text is rendered from the same data as the HTML theme. Line endings are normalised on load (a Windows checkout embeds CRLF).
  • newsletter.go builds the text and passes it to SendEmailToAllRecipients.
  • Tests: parse the built message back (headers, a decoded subject, both parts decoded), no line over 998 with a 5,000-character HTML line, no List-Unsubscribe without an address, unique Message-IDs, and the exact text output. The text test also passes with a CRLF template.
  • VERSION 1.2.0, CHANGELOG, CLAUDE.md.

🤖 Generated with Claude Code

Spam filters count a missing `Date` or `Message-ID` and an HTML-only body against a message (upstream issues #168 and #169). This adds them, plus a proper plain-text part. - `smtp/mime_message.go` (moved out of `sender.go`): multipart/alternative with text/plain and then text/html, `Date`, a random `Message-ID` on the sender's domain, and `List-Unsubscribe` (a `mailto:` to `unsubscribe_email`, left out when that is empty). The subject and display names are RFC 2047-encoded. Both parts are quoted-printable, which also keeps every line under SMTP's 998-character limit; the HTML used to go out unencoded. - `template/text_email.go` and `plaintext/newsletter.txt`: the text is rendered from the same data as the HTML theme. Line endings are normalised on load (a Windows checkout embeds CRLF). - `newsletter.go` builds the text and passes it to `SendEmailToAllRecipients`. - Tests: parse the built message back (headers, a decoded subject, both parts decoded), no line over 998 with a 5,000-character HTML line, no `List-Unsubscribe` without an address, unique Message-IDs, and the exact text output. The text test also passes with a CRLF template. - VERSION 1.2.0, CHANGELOG, CLAUDE.md. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Send a plain-text part and the headers spam filters expect (1.2.0)
All checks were successful
CI / gate (pull_request) Successful in 1m0s
CI / lint (pull_request) Successful in 57s
CI / image (pull_request) Has been skipped
45b202f96a
The email was HTML only, with From/To/Subject/Content-Type and nothing
else. Spam filters count a missing Date or Message-ID and an HTML-only
body against a message (upstream issues #168 and #169).

smtp/mime_message.go (moved out of sender.go) now writes
multipart/alternative: a text/plain part, then the HTML. It adds Date,
a random Message-ID on the sender's domain, and List-Unsubscribe as a
mailto: to unsubscribe_email when that is set. The subject and address
display names are RFC 2047-encoded, since headers may only carry ASCII.
Both parts are quoted-printable, which also fixes a real limit: SMTP
lines may not exceed 998 characters, and the HTML went out unencoded.

The text comes from template/plaintext/newsletter.txt, rendered from the
same data as the HTML theme: titles, Jellyfin links, overviews when the
theme would show them, "and N more", totals and the footer. Its line
endings are normalised on load, since a Windows checkout embeds CRLF;
the exact-output test passes with either.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
joeyr merged commit b7ea4ae030 into main 2026-10-05 20:26:05 -05:00
joeyr deleted branch feat/deliverability 2026-10-05 20:26:05 -05:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
joeyr/jellyfin-newsletter!7
No description provided.