internal/imapc/imapc_test.go
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}