86 lines
2.5 KiB
Go
86 lines
2.5 KiB
Go
package mail
|
|
|
|
import (
|
|
"bytes"
|
|
"io"
|
|
"mime"
|
|
"mime/multipart"
|
|
"mime/quotedprintable"
|
|
"net/mail"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// The envelope carries the bare address. "Vantage <x@y>" as MAIL FROM left
|
|
// rspamd with no envelope sender, and mailcow skipped DKIM signing.
|
|
func TestAddrSpecStripsDisplayName(t *testing.T) {
|
|
for in, want := range map[string]string{
|
|
"Vantage <support@example.com>": "support@example.com",
|
|
"support@example.com": "support@example.com",
|
|
" a@example.com ": "a@example.com",
|
|
"not an address": "not an address",
|
|
} {
|
|
if got := addrSpec(in); got != want {
|
|
t.Errorf("addrSpec(%q) = %q, want %q", in, got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Every part must declare quoted-printable and put only 7-bit bytes on the
|
|
// wire. Raw UTF-8 under an implied 7bit encoding scored R_BAD_CTE_7BIT in
|
|
// rspamd.
|
|
func TestEnvelopePartsAreQuotedPrintable(t *testing.T) {
|
|
s := Sender{From: "Vantage <support@example.com>"}
|
|
m := message{
|
|
To: "a@example.com",
|
|
Subject: "Smith & Sons · restored",
|
|
Text: "VANTAGE · Account\nBilling has resumed.",
|
|
HTML: "<p>VANTAGE · Account</p><p>" + strings.Repeat("x", 2000) + "</p>",
|
|
}
|
|
raw, err := s.envelope(m)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for i, c := range raw {
|
|
if c > 0x7e && c != '\r' && c != '\n' {
|
|
t.Fatalf("byte %d is 0x%x: the wire must be 7-bit", i, c)
|
|
}
|
|
}
|
|
for _, line := range strings.Split(string(raw), "\r\n") {
|
|
if len(line) > 998 {
|
|
t.Fatalf("line of %d bytes exceeds SMTP's 998", len(line))
|
|
}
|
|
}
|
|
|
|
msg, err := mail.ReadMessage(bytes.NewReader(raw))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_, params, err := mime.ParseMediaType(msg.Header.Get("Content-Type"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
r := multipart.NewReader(msg.Body, params["boundary"])
|
|
want := []string{m.Text, m.HTML}
|
|
for i := range want {
|
|
p, err := r.NextRawPart()
|
|
if err != nil {
|
|
t.Fatalf("part %d: %v", i, err)
|
|
}
|
|
if cte := p.Header.Get("Content-Transfer-Encoding"); cte != "quoted-printable" {
|
|
t.Fatalf("part %d Content-Transfer-Encoding = %q", i, cte)
|
|
}
|
|
// NextPart would decode for us; NextRawPart keeps the check honest by
|
|
// decoding here, so a missing header cannot pass by accident.
|
|
body, err := io.ReadAll(quotedprintable.NewReader(p))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// Quoted-printable text mode writes line breaks as CRLF, which is the
|
|
// canonical form on the wire.
|
|
if strings.ReplaceAll(string(body), "\r\n", "\n") != want[i] {
|
|
t.Fatalf("part %d decodes to %q", i, body)
|
|
}
|
|
}
|
|
}
|