feat(notification): RFC 8058 one-click List-Unsubscribe, From-domain Message-ID, legal footer links#1020
Merged
Merged
Conversation
…Message-ID, legal footer links (#1018) Notification emails that carry the footer unsubscribe link now emit List-Unsubscribe and List-Unsubscribe-Post headers via go-mail's SetListUnsubscribeOneClick; a non-https portal base URL advertises the plain List-Unsubscribe URI alone rather than a non-conformant one-click pair. The unsubscribe endpoint accepts the RFC 8058 provider POST and records the opt-out with a bare status and no page; GET keeps the branded confirmation page. Transactional sends (guest links, admin SMTP tests) carry neither footer nor headers. Message-ID is now set explicitly with a random left-hand side and the domain of the configured From address, replacing go-mail's hostname default that leaked pod names in containerized deployments. New optional portal.terms_url and portal.privacy_url render as small footer links in notification emails when set.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #1020 +/- ##
==========================================
+ Coverage 89.29% 89.31% +0.01%
==========================================
Files 458 458
Lines 50726 50757 +31
==========================================
+ Hits 45298 45332 +34
+ Misses 3616 3614 -2
+ Partials 1812 1811 -1 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #1018
Notification email now satisfies the RFC 8058 one-click unsubscribe requirement Gmail and Yahoo enforce for bulk senders, stamps Message-ID with the sending domain instead of the container hostname, and can carry operator legal links in the footer.
List-Unsubscribe / List-Unsubscribe-Post headers
Emailcarries the recipient's no-login unsubscribe URL (UnsubURL) whenever the message renders the footer opt-out link, andbuildMessageemits the RFC 8058 header pair from it via go-mail'sSetListUnsubscribeOneClick, which validates the https scheme the RFC requires and writes both headers preformatted. A non-https portal base URL advertises the plainList-UnsubscribeURI alone and drops the-Postheader, so mail never ships a non-conformant one-click pair. Header presence tracks the footer link exactly: transactional sends (one-time guest view links, admin SMTP tests) set noUnsubURLand therefore carry neither footer nor headers, mirroring the existingUnsubURLpresence logic inrender.go.UnsubscribeHandlernow serves both methods on/portal/notifications/unsubscribe. GET keeps the branded confirmation page for a human following the footer link. POST is the RFC 8058 one-click path a mail provider calls on the recipient's behalf (bodyList-Unsubscribe=One-Click): it verifies the same HMAC token from the query string, records delivery modeoff, and returns a bare status code with no page and no further interaction, as the RFC requires. Both routes are mounted inmountNotificationUnsubscribe.Message-ID from the From-address domain
buildMessagepreviously left Message-ID generation to go-mail, which derives the right-hand side fromos.Hostname(). In containerized deployments that is the pod name, producing IDs like<...@notification-worker-abc123>whose domain never resolves, which trips content-filter heuristics and leaks internal naming. The message now gets an explicit ID viaSetMessageIDWithValue: a random left-hand side fromcrypto/rand.Text()(~130 bits) and the domain parsed from the configured From address (messageIDDomain, which handles both bare and name-addr forms). If no domain can be derived the code falls back to go-mail's default; in practice this branch is unreachable because the From address is validated earlier inbuildMessage.Operator legal links in the footer
New optional
portal.terms_urlandportal.privacy_urlconfig fields thread throughnotification.Brandinginto both email templates. When set, they render as small Terms of Service / Privacy Policy footer links (with a separator only when both are present); unset fields are omitted entirely. When the portal runs on a different domain than the mail From address, these give recipients and content filters body links that associate with the sending identity.Tests
msg.WriteTo, unfolding RFC 5322 header continuation lines): header pair present with the exact URL and fixed POST value, absent withoutUnsubURL, and the non-https fallback emitsList-UnsubscribewithoutList-Unsubscribe-Post.messageIDDomaintable test covers bare, name-addr, and malformed inputs.Email.UnsubURLpopulated for notifications and empty for guest-link and test sends; legal links render with both, one, or neither URL configured, including separator presence and absence.internal/httpserver.Docs
docs/server/notifications.md(one-click flow, Message-ID behavior, legal links),docs/server/configuration.mdandconfigs/platform.yaml(new portal fields), and bothdocs/llms.txt/docs/llms-full.txt.make verifypasses.