6c81df17ec
The Gmail category rule matched X-GM-LABELS and X-Gmail-Labels against the parsed header block. Neither is a header. X-GM-LABELS is a Gmail FETCH data item and never appears in the message source, and X-Gmail-Labels only exists in a Takeout export, so the branch could not fire against a real mailbox while its doc comment promised a Promotions filter. Its test built the header by hand and therefore asserted the matcher rather than the plumbing. The rule is removed and the comment says what bringing it back would take. multipartText folded a nested multipart's answer into one string, so HTML derived text landed in the plain bucket and a real text/plain sibling later in the message was discarded by the guard on plain being set. The two buckets now stay separate through the recursion. windows-1251 returned an unsupported-charset error and the message degraded to subject only. That is the charset older Russian senders still use, so those mails could never produce a task candidate. It is decoded from a 128 entry table here rather than by vendoring x/text, for the body and for encoded words in the subject. Every other unknown charset still degrades to subject only. Found in review of #63. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TrVSBKe3RFDF4fGYKWYQnX
62 lines
2.2 KiB
Go
62 lines
2.2 KiB
Go
package email
|
|
|
|
import (
|
|
"net/mail"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func headers(t *testing.T, raw string) mail.Header {
|
|
t.Helper()
|
|
m, err := mail.ReadMessage(strings.NewReader(strings.ReplaceAll(raw, "\n", "\r\n") + "\r\n\r\nbody\r\n"))
|
|
if err != nil {
|
|
t.Fatalf("read headers: %v", err)
|
|
}
|
|
return m.Header
|
|
}
|
|
|
|
func TestClassifyJunk(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
raw string
|
|
junk bool
|
|
reason string
|
|
}{
|
|
{"personal", "From: a@b.c\nSubject: привет", false, ""},
|
|
{"list-unsubscribe", "From: a@b.c\nList-Unsubscribe: <mailto:u@b.c>", true, "list-unsubscribe"},
|
|
{"list-id", "From: a@b.c\nList-Id: <golang-nuts.example>", true, "list-id"},
|
|
{"precedence bulk", "From: a@b.c\nPrecedence: bulk", true, "precedence"},
|
|
{"auto-submitted", "From: a@b.c\nAuto-Submitted: auto-generated", true, "auto-submitted"},
|
|
{"auto-submitted no", "From: a@b.c\nAuto-Submitted: no", false, ""},
|
|
{"spam flag", "From: a@b.c\nX-Spam-Flag: YES", true, "x-spam-flag"},
|
|
{"spam status", "From: a@b.c\nX-Spam-Status: Yes, score=9.1", true, "x-spam-status"},
|
|
{"spam status no", "From: a@b.c\nX-Spam-Status: No, score=0.1", false, ""},
|
|
// X-GM-LABELS is a Gmail FETCH data item, not a header, so it never
|
|
// reaches classifyJunk through this client. The rule that matched it was
|
|
// removed rather than left claiming a Promotions filter that never ran.
|
|
{"gmail label header is not a rule", "From: a@b.c\nX-Gmail-Labels: Inbox,CATEGORY_PROMOTIONS", false, ""},
|
|
}
|
|
for _, c := range cases {
|
|
t.Run(c.name, func(t *testing.T) {
|
|
junk, reason := classifyJunk(headers(t, c.raw))
|
|
if junk != c.junk || reason != c.reason {
|
|
t.Errorf("classifyJunk = (%v, %q), want (%v, %q)", junk, reason, c.junk, c.reason)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNewsletterFixtureIsJunk(t *testing.T) {
|
|
msg, err := ParseMessage(9, fixture(t, "newsletter.eml"))
|
|
if err != nil {
|
|
t.Fatalf("parse: %v", err)
|
|
}
|
|
if !msg.Junk {
|
|
t.Fatal("a newsletter with List-Unsubscribe + Precedence: bulk must be junk")
|
|
}
|
|
// The reason is what gets logged, so it must never carry mail content.
|
|
if strings.Contains(msg.JunkReason, "@") || strings.Contains(msg.JunkReason, "Скидки") {
|
|
t.Errorf("junk reason leaks content: %q", msg.JunkReason)
|
|
}
|
|
}
|