feat: per-message extra headers in the mail envelope

This commit is contained in:
2026-09-11 13:33:12 +00:00
parent 91d10a7650
commit a901482e5d
2 changed files with 41 additions and 0 deletions
+14
View File
@@ -20,6 +20,7 @@ import (
"net/smtp"
"net/textproto"
"os"
"sort"
"strings"
"time"
)
@@ -76,6 +77,11 @@ type message struct {
Subject string
HTML string
Text string
// Headers are extra header lines for this one message, such as an
// announcement's List-Unsubscribe pair. Keys and values are CR/LF-stripped
// like every other header.
Headers map[string]string
}
// sendTemplate renders name against data and delivers the result.
@@ -226,6 +232,14 @@ func (s Sender) envelope(m message) ([]byte, error) {
b.WriteString("Message-ID: " + messageID(s.From) + "\r\n")
b.WriteString("Subject: " + mime.QEncoding.Encode("utf-8", sanitizeHeader(m.Subject)) + "\r\n")
b.WriteString("MIME-Version: 1.0\r\n")
keys := make([]string, 0, len(m.Headers))
for k := range m.Headers {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
b.WriteString(sanitizeHeader(k) + ": " + sanitizeHeader(m.Headers[k]) + "\r\n")
}
b.WriteString("Content-Type: multipart/alternative; boundary=" + w.Boundary() + "\r\n")
b.WriteString("\r\n")
b.WriteString(parts.String())
+27
View File
@@ -83,3 +83,30 @@ func TestEnvelopePartsAreQuotedPrintable(t *testing.T) {
}
}
}
func TestEnvelopeWritesExtraHeadersSanitised(t *testing.T) {
s := Sender{From: "Vantage <updates@example.com>"}
raw, err := s.envelope(message{
To: "a@example.com", Subject: "s", Text: "t", HTML: "<p>h</p>",
Headers: map[string]string{
"List-Unsubscribe-Post": "List-Unsubscribe=One-Click",
"List-Unsubscribe": "<https://x.example/u?t=1>\r\nBcc: evil@example.com",
},
})
if err != nil {
t.Fatal(err)
}
msg, err := mail.ReadMessage(bytes.NewReader(raw))
if err != nil {
t.Fatal(err)
}
if got := msg.Header.Get("List-Unsubscribe-Post"); got != "List-Unsubscribe=One-Click" {
t.Fatalf("List-Unsubscribe-Post = %q", got)
}
if got := msg.Header.Get("Bcc"); got != "" {
t.Fatalf("header injection: Bcc = %q", got)
}
if !strings.HasPrefix(msg.Header.Get("List-Unsubscribe"), "<https://x.example/u?t=1>") {
t.Fatalf("List-Unsubscribe = %q", msg.Header.Get("List-Unsubscribe"))
}
}