internal/mailin/body_test.go
100 lines · 4400 bytes
3 symbols in this file
1package mailin
2
3import (
4 "bytes"
5 "errors"
6 "net/mail"
7 "net/textproto"
8 "strings"
9 "testing"
10)
11
12func TestStripQuoted(t *testing.T) {
13 for _, tc := range []struct{ name, in, want string }{
14 {"gmail",
15 "Agreed, ship it.\n\nOn Mon, Sep 28, 2026 at 9:00 AM gitbay <reply+abc@gitbay.example> wrote:\n\n> alice commented on #1\n>\n> looks fine\n",
16 "Agreed, ship it."},
17 {"gmail, attribution wrapped",
18 "Agreed.\n\nOn Mon, Sep 28, 2026 at 9:00 AM gitbay <\nreply+abc@gitbay.example> wrote:\n\n> alice commented\n",
19 "Agreed."},
20 {"apple mail",
21 "Fixed in the next push.\n\nSent from my iPhone\n\n> On Sep 28, 2026, at 09:00, gitbay <reply+abc@gitbay.example> wrote:\n> \n> alice commented on #1\n",
22 "Fixed in the next push."},
23 {"apple mail, unquoted attribution",
24 "Yes.\n\nOn 28 Sep 2026, at 09:00, gitbay <reply+abc@gitbay.example> wrote:\n\n> alice commented\n",
25 "Yes."},
26 {"outlook",
27 "Will do.\r\n\r\n________________________________\r\nFrom: gitbay <reply+abc@gitbay.example>\r\nSent: Monday, September 28, 2026 9:00 AM\r\nTo: Bob\r\nSubject: [alice/app] #1: title\r\n\r\nalice commented on #1\r\n",
28 "Will do."},
29 {"outlook, no rule",
30 "Will do.\n\nFrom: gitbay <reply+abc@gitbay.example>\nSent: Monday, September 28, 2026 9:00 AM\nTo: Bob\n\nalice commented on #1\n",
31 "Will do."},
32 {"outlook, original message",
33 "Noted.\n\n-----Original Message-----\nFrom: gitbay\nalice commented\n",
34 "Noted."},
35 {"thunderbird",
36 "Thanks, merged.\n\n-- \nBob Example\nExample Corp\n\nOn 9/28/26 09:00, gitbay wrote:\n> alice commented on #1\n",
37 "Thanks, merged."},
38 {"thunderbird, quote first",
39 "On 9/28/26 09:00, gitbay wrote:\n> alice commented on #1\n\nThat was me.\n",
40 ""},
41 {"inline answers keep the answers",
42 "> does this build?\nYes, on main.\n> and the tests?\nAll green.\n",
43 "Yes, on main.\nAll green."},
44 {"a From: line in prose is kept",
45 "From: the log it looks like a timeout.\nRetrying.\n",
46 "From: the log it looks like a timeout.\nRetrying."},
47 {"markdown rule is not a signature",
48 "one\n\n---\n\ntwo\n",
49 "one\n\n---\n\ntwo"},
50 } {
51 t.Run(tc.name, func(t *testing.T) {
52 if got := stripQuoted(tc.in); got != tc.want {
53 t.Errorf("got %q\nwant %q", got, tc.want)
54 }
55 })
56 }
57}
58
59func body(t *testing.T, raw string) (string, error) {
60 t.Helper()
61 msg, err := mail.ReadMessage(strings.NewReader(raw))
62 if err != nil {
63 t.Fatal(err)
64 }
65 return textBody(textproto.MIMEHeader(msg.Header), msg.Body, 1<<16)
66}
67
68func TestTextBody(t *testing.T) {
69 for _, tc := range []struct{ name, raw, want string }{
70 {"plain, no content type", "Subject: x\r\n\r\nhello\r\n", "hello\r\n"},
71 {"quoted-printable", "Content-Type: text/plain; charset=utf-8\r\nContent-Transfer-Encoding: quoted-printable\r\n\r\ncaf=C3=A9 =\r\nau lait\r\n", "café au lait\r\n"},
72 {"base64", "Content-Type: text/plain\r\nContent-Transfer-Encoding: base64\r\n\r\naGVs\r\nbG8=\r\n", "hello"},
73 {"latin-1", "Content-Type: text/plain; charset=iso-8859-1\r\nContent-Transfer-Encoding: quoted-printable\r\n\r\ncaf=E9\r\n", "café\r\n"},
74 {"alternative prefers plain",
75 "Content-Type: multipart/alternative; boundary=b\r\n\r\n--b\r\nContent-Type: text/html\r\n\r\n<p>html</p>\r\n--b\r\nContent-Type: text/plain\r\n\r\nplain\r\n--b--\r\n",
76 "plain"},
77 {"mixed with nested alternative and an attachment",
78 "Content-Type: multipart/mixed; boundary=m\r\n\r\n--m\r\nContent-Type: text/plain\r\nContent-Disposition: attachment; filename=a.txt\r\n\r\nattached\r\n--m\r\nContent-Type: multipart/alternative; boundary=a\r\n\r\n--a\r\nContent-Type: text/plain\r\n\r\nbody\r\n--a--\r\n--m--\r\n",
79 "body"},
80 } {
81 t.Run(tc.name, func(t *testing.T) {
82 got, err := body(t, tc.raw)
83 if err != nil || got != tc.want {
84 t.Errorf("got %q, %v; want %q", got, err, tc.want)
85 }
86 })
87 }
88 for name, raw := range map[string]string{
89 "html only": "Content-Type: text/html\r\n\r\n<p>hi</p>\r\n",
90 "unknown charset": "Content-Type: text/plain; charset=x-nonesuch\r\n\r\nhi\r\n",
91 "unknown encoding": "Content-Type: text/plain\r\nContent-Transfer-Encoding: x-uuencode\r\n\r\nhi\r\n",
92 } {
93 if _, err := body(t, raw); !errors.Is(err, errNoText) {
94 t.Errorf("%s: %v, want errNoText", name, err)
95 }
96 }
97 if _, err := body(t, "Subject: x\r\n\r\n"+string(bytes.Repeat([]byte("a"), 8<<16+1))); !errors.Is(err, errTooLong) {
98 t.Errorf("oversized body: %v", err)
99 }
100}