internal/imapc/imapc_test.go

v1.43.0
gitbay/internal/imapc/imapc_test.go history · blame · raw

353 lines · 9881 bytes

  1package imapc
  2
  3import (
  4	"bufio"
  5	"crypto/tls"
  6	"crypto/x509"
  7	"errors"
  8	"fmt"
  9	"net"
 10	"net/http"
 11	"net/http/httptest"
 12	"strings"
 13	"testing"
 14	"time"
 15)
 16
 17// fakeServer answers the commands this client sends from a map of UID to
 18// message. It records the commands it saw.
 19type fakeServer struct {
 20	msgs map[uint32]string
 21	seen map[uint32]bool
 22	cmds []string
 23	// raw, when set, answers a command in place of the default: it
 24	// writes whatever it likes and reports whether it handled it.
 25	raw func(conn net.Conn, tag, cmd string) bool
 26}
 27
 28func (f *fakeServer) serve(conn net.Conn) {
 29	defer conn.Close()
 30	r := bufio.NewReader(conn)
 31	fmt.Fprint(conn, "* OK fake ready\r\n")
 32	for {
 33		line, err := r.ReadString('\n')
 34		if err != nil {
 35			return
 36		}
 37		line = strings.TrimRight(line, "\r\n")
 38		tag, cmd, _ := strings.Cut(line, " ")
 39		f.cmds = append(f.cmds, cmd)
 40		up := strings.ToUpper(cmd)
 41		if f.raw != nil && f.raw(conn, tag, cmd) {
 42			continue
 43		}
 44		switch {
 45		case strings.HasPrefix(up, "STARTTLS"):
 46			fmt.Fprintf(conn, "%s OK begin\r\n", tag)
 47			tc := tls.Server(conn, serverTLS)
 48			if err := tc.Handshake(); err != nil {
 49				return
 50			}
 51			conn, r = tc, bufio.NewReader(tc)
 52		case strings.HasPrefix(up, "LOGIN"):
 53			if cmd != `LOGIN "u" "p\"w"` {
 54				fmt.Fprintf(conn, "%s NO [AUTHENTICATIONFAILED] %s\r\n", tag, cmd)
 55				continue
 56			}
 57			fmt.Fprintf(conn, "* CAPABILITY IMAP4rev1\r\n%s OK logged in\r\n", tag)
 58		case strings.HasPrefix(up, "SELECT"), strings.HasPrefix(up, "EXAMINE"):
 59			fmt.Fprintf(conn, "* %d EXISTS\r\n* 0 RECENT\r\n%s OK done\r\n", len(f.msgs), tag)
 60		case up == "UID SEARCH UNSEEN":
 61			var ids []string
 62			for uid := range f.msgs {
 63				if !f.seen[uid] {
 64					ids = append(ids, fmt.Sprint(uid))
 65				}
 66			}
 67			fmt.Fprintf(conn, "* SEARCH %s\r\n%s OK done\r\n", strings.Join(ids, " "), tag)
 68		case strings.HasPrefix(up, "UID FETCH") && strings.HasSuffix(up, "RFC822.SIZE"):
 69			var uid uint32
 70			fmt.Sscanf(cmd, "UID FETCH %d", &uid)
 71			fmt.Fprintf(conn, "* 1 FETCH (UID %d RFC822.SIZE %d)\r\n%s OK done\r\n", uid, len(f.msgs[uid]), tag)
 72		case strings.HasPrefix(up, "UID FETCH"):
 73			var uid uint32
 74			fmt.Sscanf(cmd, "UID FETCH %d", &uid)
 75			m := f.msgs[uid]
 76			// FLAGS ahead of the body and UID after it, as some servers order them.
 77			fmt.Fprintf(conn, "* 1 FETCH (FLAGS () BODY[] {%d}\r\n%s UID %d)\r\n%s OK done\r\n", len(m), m, uid, tag)
 78		case strings.HasPrefix(up, "UID STORE"):
 79			var uid uint32
 80			fmt.Sscanf(cmd, "UID STORE %d", &uid)
 81			f.seen[uid] = true
 82			fmt.Fprintf(conn, "%s OK done\r\n", tag)
 83		case up == "LOGOUT":
 84			fmt.Fprintf(conn, "* BYE\r\n%s OK bye\r\n", tag)
 85			return
 86		default:
 87			fmt.Fprintf(conn, "%s BAD unknown\r\n", tag)
 88		}
 89	}
 90}
 91
 92var serverTLS *tls.Config
 93
 94func setupTLS(t *testing.T) {
 95	ts := httptest.NewTLSServer(http.NotFoundHandler())
 96	t.Cleanup(ts.Close)
 97	pool := x509.NewCertPool()
 98	pool.AddCert(ts.Certificate())
 99	prev := rootCAs
100	rootCAs = pool
101	t.Cleanup(func() { rootCAs = prev })
102	serverTLS = &tls.Config{Certificates: ts.TLS.Certificates}
103}
104
105func listen(t *testing.T, f *fakeServer, implicit bool) string {
106	t.Helper()
107	ln, err := net.Listen("tcp", "127.0.0.1:0")
108	if err != nil {
109		t.Fatal(err)
110	}
111	if implicit {
112		ln = tls.NewListener(ln, serverTLS)
113	}
114	t.Cleanup(func() { ln.Close() })
115	go func() {
116		for {
117			conn, err := ln.Accept()
118			if err != nil {
119				return
120			}
121			go f.serve(conn)
122		}
123	}()
124	// The test certificate is for example.com and 127.0.0.1.
125	return ln.Addr().String()
126}
127
128func TestSession(t *testing.T) {
129	setupTLS(t)
130	body := "From: a@example.test\r\nSubject: x\r\n\r\nhello {3}\r\n"
131	for _, starttls := range []bool{false, true} {
132		f := &fakeServer{msgs: map[uint32]string{7: body, 9: "other"}, seen: map[uint32]bool{9: true}}
133		c, err := Dial(listen(t, f, !starttls), starttls, 5*time.Second)
134		if err != nil {
135			t.Fatal(err)
136		}
137		if err := c.Login("u", `p"w`); err != nil {
138			t.Fatal(err)
139		}
140		n, err := c.Select("INBOX")
141		if err != nil || n != 2 {
142			t.Fatalf("Select = %d, %v", n, err)
143		}
144		uids, err := c.Unseen()
145		if err != nil || len(uids) != 1 || uids[0] != 7 {
146			t.Fatalf("Unseen = %v, %v", uids, err)
147		}
148		got, err := c.Fetch(7)
149		if err != nil || string(got) != body {
150			t.Fatalf("Fetch = %q, %v", got, err)
151		}
152		if err := c.MarkSeen(7); err != nil {
153			t.Fatal(err)
154		}
155		if uids, _ := c.Unseen(); len(uids) != 0 {
156			t.Fatalf("still unseen: %v", uids)
157		}
158		c.Close()
159		if !strings.Contains(strings.Join(f.cmds, "\n"), "UID FETCH 7 BODY.PEEK[]") {
160			t.Fatalf("fetch did not peek: %v", f.cmds)
161		}
162		if starttls && f.cmds[0] != "STARTTLS" {
163			t.Fatalf("first command %q, want STARTTLS before LOGIN", f.cmds[0])
164		}
165	}
166}
167
168func TestLoginRefusedHidesServerText(t *testing.T) {
169	setupTLS(t)
170	f := &fakeServer{msgs: map[uint32]string{}, seen: map[uint32]bool{}}
171	c, err := Dial(listen(t, f, true), false, 5*time.Second)
172	if err != nil {
173		t.Fatal(err)
174	}
175	defer c.Close()
176	err = c.Login("u", "secret")
177	if err == nil || strings.Contains(err.Error(), "secret") {
178		t.Fatalf("Login = %v", err)
179	}
180	if err := c.Login("u", "bad\r\npass"); err == nil {
181		t.Fatal("a line break in the password was sent")
182	}
183}
184
185// A server with a certificate the client does not trust is refused.
186func TestUntrustedCertificate(t *testing.T) {
187	setupTLS(t)
188	f := &fakeServer{msgs: map[uint32]string{}, seen: map[uint32]bool{}}
189	addr := listen(t, f, true)
190	rootCAs = x509.NewCertPool()
191	if _, err := Dial(addr, false, 5*time.Second); err == nil {
192		t.Fatal("dialled a server with an untrusted certificate")
193	}
194}
195
196func TestLiteralSize(t *testing.T) {
197	for line, want := range map[string]int64{
198		"* 1 FETCH (BODY[] {12}": 12,
199		"* 1 FETCH (BODY[] {5+}": 5,
200		"* OK {x}":               -1,
201		"* OK done":              -1,
202	} {
203		n, ok := literalSize(line)
204		if (want < 0) == ok || (ok && n != want) {
205			t.Errorf("literalSize(%q) = %d, %v", line, n, ok)
206		}
207	}
208}
209
210// session dials f over implicit TLS and logs in.
211func session(t *testing.T, f *fakeServer) *Client {
212	t.Helper()
213	setupTLS(t)
214	if f.msgs == nil {
215		f.msgs, f.seen = map[uint32]string{}, map[uint32]bool{}
216	}
217	c, err := Dial(listen(t, f, true), false, 10*time.Second)
218	if err != nil {
219		t.Fatal(err)
220	}
221	t.Cleanup(func() { c.Close() })
222	if err := c.Login("u", `p"w`); err != nil {
223		t.Fatal(err)
224	}
225	return c
226}
227
228// A server that answers one FETCH with literal after literal is cut off
229// at the command's byte budget, however it labels them.
230func TestHostileRepeatedLiterals(t *testing.T) {
231	chunk := strings.Repeat("x", 10<<20)
232	f := &fakeServer{raw: func(conn net.Conn, tag, cmd string) bool {
233		if !strings.HasPrefix(cmd, "UID FETCH 1 BODY") {
234			return false
235		}
236		for i := 0; i < 5; i++ {
237			if _, err := fmt.Fprintf(conn, "* 1 FETCH (FLAGS {%d}\r\n%s)\r\n", len(chunk), chunk); err != nil {
238				return true
239			}
240		}
241		fmt.Fprintf(conn, "%s OK done\r\n", tag)
242		return true
243	}}
244	c := session(t, f)
245	f.msgs[1] = "small"
246	if _, err := c.Fetch(1); !errors.Is(err, ErrLimit) {
247		t.Fatalf("Fetch = %v, want ErrLimit", err)
248	}
249}
250
251// A body literal over MaxMessage is refused even when RFC822.SIZE lied.
252func TestLyingSize(t *testing.T) {
253	big := strings.Repeat("x", MaxMessage+10)
254	f := &fakeServer{raw: func(conn net.Conn, tag, cmd string) bool {
255		if !strings.HasPrefix(cmd, "UID FETCH 1 BODY") {
256			return false
257		}
258		fmt.Fprintf(conn, "* 1 FETCH (BODY[] {%d}\r\n%s)\r\n%s OK done\r\n", len(big), big, tag)
259		return true
260	}}
261	c := session(t, f)
262	f.msgs[1] = "small"
263	if _, err := c.Fetch(1); !errors.Is(err, ErrTooLarge) {
264		t.Fatalf("Fetch = %v, want ErrTooLarge", err)
265	}
266}
267
268func TestSizeRefusedBeforeFetch(t *testing.T) {
269	f := &fakeServer{}
270	c := session(t, f)
271	f.msgs[1] = strings.Repeat("x", MaxMessage+1)
272	if _, err := c.Fetch(1); !errors.Is(err, ErrTooLarge) {
273		t.Fatalf("Fetch = %v, want ErrTooLarge", err)
274	}
275	for _, cmd := range f.cmds {
276		if strings.Contains(cmd, "BODY.PEEK") {
277			t.Fatal("fetched the body of an oversized message")
278		}
279	}
280}
281
282func TestHugeSearch(t *testing.T) {
283	var b strings.Builder
284	for i := 1; i <= 20000; i++ {
285		fmt.Fprintf(&b, " %d", i)
286	}
287	f := &fakeServer{raw: func(conn net.Conn, tag, cmd string) bool {
288		if cmd != "UID SEARCH UNSEEN" {
289			return false
290		}
291		fmt.Fprintf(conn, "* SEARCH%s\r\n%s OK done\r\n", b.String(), tag)
292		return true
293	}}
294	c := session(t, f)
295	uids, err := c.Unseen()
296	if err != nil || len(uids) != MaxUnseen {
297		t.Fatalf("Unseen = %d uids, %v", len(uids), err)
298	}
299}
300
301func TestTooManyUntagged(t *testing.T) {
302	f := &fakeServer{raw: func(conn net.Conn, tag, cmd string) bool {
303		if cmd != "UID SEARCH UNSEEN" {
304			return false
305		}
306		for i := 0; i < maxUntagged+10; i++ {
307			fmt.Fprint(conn, "* OK noise\r\n")
308		}
309		fmt.Fprintf(conn, "%s OK done\r\n", tag)
310		return true
311	}}
312	c := session(t, f)
313	if _, err := c.Unseen(); !errors.Is(err, ErrLimit) {
314		t.Fatalf("Unseen = %v, want ErrLimit", err)
315	}
316}
317
318func TestEmptyBody(t *testing.T) {
319	for _, form := range []string{"NIL", `""`} {
320		f := &fakeServer{raw: func(conn net.Conn, tag, cmd string) bool {
321			if !strings.HasPrefix(cmd, "UID FETCH 1 BODY") {
322				return false
323			}
324			fmt.Fprintf(conn, "* 1 FETCH (UID 1 BODY[] %s)\r\n%s OK done\r\n", form, tag)
325			return true
326		}}
327		c := session(t, f)
328		f.msgs[1] = ""
329		if b, err := c.Fetch(1); err != nil || len(b) != 0 {
330			t.Fatalf("%s: Fetch = %q, %v", form, b, err)
331		}
332	}
333}
334
335// A NO to one FETCH is a RefusedError; the session goes on.
336func TestFetchRefused(t *testing.T) {
337	f := &fakeServer{raw: func(conn net.Conn, tag, cmd string) bool {
338		if !strings.HasPrefix(cmd, "UID FETCH 1 ") {
339			return false
340		}
341		fmt.Fprintf(conn, "%s NO [UNAVAILABLE] try later\r\n", tag)
342		return true
343	}}
344	c := session(t, f)
345	f.msgs[1], f.msgs[2] = "a", "b"
346	var re *RefusedError
347	if _, err := c.Fetch(1); !errors.As(err, &re) {
348		t.Fatalf("Fetch = %v, want a RefusedError", err)
349	}
350	if b, err := c.Fetch(2); err != nil || string(b) != "b" {
351		t.Fatalf("next Fetch = %q, %v", b, err)
352	}
353}