package email import ( "os" "path/filepath" "strings" "testing" ) func fixture(t *testing.T, name string) []byte { t.Helper() b, err := os.ReadFile(filepath.Join("testdata", name)) if err != nil { t.Fatalf("read fixture %s: %v", name, err) } return b } func TestParsePlainRussian(t *testing.T) { msg, err := ParseMessage(7, fixture(t, "plain_ru.eml")) if err != nil { t.Fatalf("parse: %v", err) } if msg.UID != 7 { t.Errorf("uid = %d, want 7", msg.UID) } if want := "Нужно закрыть задачу"; msg.Subject != want { t.Errorf("subject = %q, want %q", msg.Subject, want) } if !strings.Contains(msg.From, "Антон") { t.Errorf("from = %q, want the decoded display name", msg.From) } if !strings.Contains(msg.Body, "Надо отправить акт до пятницы.") { t.Errorf("body = %q, want the quoted-printable text decoded", msg.Body) } if msg.Junk { t.Errorf("a personal mail must not be junk (%s)", msg.JunkReason) } } func TestParseHTMLOnlyIsStripped(t *testing.T) { msg, err := ParseMessage(1, fixture(t, "html_only.eml")) if err != nil { t.Fatalf("parse: %v", err) } if strings.Contains(msg.Body, "<") || strings.Contains(msg.Body, "color:red") || strings.Contains(msg.Body, "x()") { t.Errorf("body still has markup/script/style: %q", msg.Body) } for _, want := range []string{"Счёт за интернет: 700", "Оплатить до 5 августа."} { if !strings.Contains(msg.Body, want) { t.Errorf("body = %q, want it to contain %q", msg.Body, want) } } //   must have become a real space, not vanished into the number. if strings.Contains(msg.Body, " ") { t.Errorf("entity left unescaped: %q", msg.Body) } } func TestParsePrefersPlainAndSkipsAttachments(t *testing.T) { msg, err := ParseMessage(2, fixture(t, "mixed_attachment.eml")) if err != nil { t.Fatalf("parse: %v", err) } if got := strings.TrimSpace(msg.Body); got != "Sign the contract before Monday." { t.Errorf("body = %q, want the text/plain alternative only", got) } if strings.Contains(msg.Body, "PDF") { t.Errorf("attachment bytes leaked into the body: %q", msg.Body) } } // windows-1251 is what older Russian senders still emit. Subject-only for those // mails meant they could never produce a task candidate. func TestParseCP1251(t *testing.T) { msg, err := ParseMessage(3, fixture(t, "cp1251.eml")) if err != nil { t.Fatalf("parse: %v", err) } if want := "Счёт за интернет"; msg.Subject != want { t.Errorf("subject = %q, want %q", msg.Subject, want) } if want := "Оплати счёт до пятницы."; !strings.Contains(msg.Body, want) { t.Errorf("body = %q, want it to contain %q", msg.Body, want) } } // A charset with no table here must degrade to headers-only rather than to // mojibake the model would then extract a task from. func TestParseUnsupportedCharsetKeepsHeaders(t *testing.T) { msg, err := ParseMessage(3, fixture(t, "koi8r.eml")) if err != nil { t.Fatalf("parse: %v", err) } if msg.Subject != "Legacy" { t.Errorf("subject = %q, want Legacy", msg.Subject) } if msg.Body != "" { t.Errorf("body = %q, want empty for an undecodable charset", msg.Body) } } // A nested multipart/alternative that only had HTML must not fill the plain // bucket: a real text/plain sibling later in the message is the better text and // used to be discarded. func TestParseNestedHTMLDoesNotShadowLaterPlain(t *testing.T) { raw := "Subject: nested\r\n" + "Content-Type: multipart/mixed; boundary=OUT\r\n\r\n" + "--OUT\r\n" + "Content-Type: multipart/alternative; boundary=IN\r\n\r\n" + "--IN\r\n" + "Content-Type: text/html; charset=utf-8\r\n\r\n" + "

from the html part

\r\n" + "--IN--\r\n" + "--OUT\r\n" + "Content-Type: text/plain; charset=utf-8\r\n\r\n" + "the real plain text\r\n" + "--OUT--\r\n" msg, err := ParseMessage(5, []byte(raw)) if err != nil { t.Fatalf("parse: %v", err) } if got := strings.TrimSpace(msg.Body); got != "the real plain text" { t.Errorf("body = %q, want the text/plain part to win", got) } } func TestParseTruncatesLongBody(t *testing.T) { var b strings.Builder b.WriteString("Subject: long\r\nContent-Type: text/plain; charset=utf-8\r\n\r\n") for i := 0; i < 2000; i++ { b.WriteString("длинная строка ") } msg, err := ParseMessage(4, []byte(b.String())) if err != nil { t.Fatalf("parse: %v", err) } if len(msg.Body) > MaxBodyBytes+8 { t.Errorf("body kept %d bytes, want ≤ %d", len(msg.Body), MaxBodyBytes) } if !strings.HasSuffix(msg.Body, "…") { t.Errorf("truncated body should be marked: %q", msg.Body[len(msg.Body)-20:]) } } func TestCollapseSqueezesBlankLines(t *testing.T) { got := collapse(" a b \r\n\r\n\r\n\r\n c \r\n") if got != "a b\n\nc" { t.Errorf("collapse = %q, want %q", got, "a b\n\nc") } }