feat: per-message extra headers in the mail envelope
This commit is contained in:
@@ -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())
|
||||
|
||||
@@ -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"))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user