internal/mailin/body_test.go

v1.41.0
gitbay/internal/mailin/body_test.go history · blame · raw

100 lines · 4400 bytes

  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}