1package imapserver
2
3import (
4 "context"
5 "crypto/ed25519"
6 cryptorand "crypto/rand"
7 "crypto/tls"
8 "crypto/x509"
9 "fmt"
10 "math/big"
11 "net"
12 "os"
13 "path/filepath"
14 "reflect"
15 "strings"
16 "testing"
17 "time"
18
19 "github.com/mjl-/mox/imapclient"
20 "github.com/mjl-/mox/mox-"
21 "github.com/mjl-/mox/moxvar"
22 "github.com/mjl-/mox/store"
23)
24
25var ctxbg = context.Background()
26
27func init() {
28 sanityChecks = true
29
30 // Don't slow down tests.
31 badClientDelay = 0
32 authFailDelay = 0
33}
34
35func tocrlf(s string) string {
36 return strings.ReplaceAll(s, "\n", "\r\n")
37}
38
39// From ../rfc/3501:2589
40var exampleMsg = tocrlf(`Date: Mon, 7 Feb 1994 21:52:25 -0800 (PST)
41From: Fred Foobar <foobar@Blurdybloop.example>
42Subject: afternoon meeting
43To: mooch@owatagu.siam.edu.example
44Message-Id: <B27397-0100000@Blurdybloop.example>
45MIME-Version: 1.0
46Content-Type: TEXT/PLAIN; CHARSET=US-ASCII
47
48Hello Joe, do you think we can meet at 3:30 tomorrow?
49
50`)
51
52/*
53From ../rfc/2049:801
54
55Message structure:
56
57Message - multipart/mixed
58Part 1 - no content-type
59Part 2 - text/plain
60Part 3 - multipart/parallel
61Part 3.1 - audio/basic (base64)
62Part 3.2 - image/jpeg (base64, empty)
63Part 4 - text/enriched
64Part 5 - message/rfc822
65Part 5.1 - text/plain (quoted-printable)
66*/
67var nestedMessage = tocrlf(`MIME-Version: 1.0
68From: Nathaniel Borenstein <nsb@nsb.fv.com>
69To: Ned Freed <ned@innosoft.com>
70Date: Fri, 07 Oct 1994 16:15:05 -0700 (PDT)
71Subject: A multipart example
72Content-Type: multipart/mixed;
73 boundary=unique-boundary-1
74
75This is the preamble area of a multipart message.
76Mail readers that understand multipart format
77should ignore this preamble.
78
79If you are reading this text, you might want to
80consider changing to a mail reader that understands
81how to properly display multipart messages.
82
83--unique-boundary-1
84
85 ... Some text appears here ...
86
87[Note that the blank between the boundary and the start
88 of the text in this part means no header fields were
89 given and this is text in the US-ASCII character set.
90 It could have been done with explicit typing as in the
91 next part.]
92
93--unique-boundary-1
94Content-type: text/plain; charset=US-ASCII
95
96This could have been part of the previous part, but
97illustrates explicit versus implicit typing of body
98parts.
99
100--unique-boundary-1
101Content-Type: multipart/parallel; boundary=unique-boundary-2
102
103--unique-boundary-2
104Content-Type: audio/basic
105Content-Transfer-Encoding: base64
106
107aGVsbG8NCndvcmxkDQo=
108
109--unique-boundary-2
110Content-Type: image/jpeg
111Content-Transfer-Encoding: base64
112
113
114--unique-boundary-2--
115
116--unique-boundary-1
117Content-type: text/enriched
118
119This is <bold><italic>enriched.</italic></bold>
120<smaller>as defined in RFC 1896</smaller>
121
122Isn't it
123<bigger><bigger>cool?</bigger></bigger>
124
125--unique-boundary-1
126Content-Type: message/rfc822
127
128From: info@mox.example
129To: mox <info@mox.example>
130Subject: (subject in US-ASCII)
131Content-Type: Text/plain; charset=ISO-8859-1
132Content-Transfer-Encoding: Quoted-printable
133
134 ... Additional text in ISO-8859-1 goes here ...
135
136--unique-boundary-1--
137`)
138
139func tcheck(t *testing.T, err error, msg string) {
140 t.Helper()
141 if err != nil {
142 t.Fatalf("%s: %s", msg, err)
143 }
144}
145
146func mockUIDValidity() func() {
147 orig := store.InitialUIDValidity
148 store.InitialUIDValidity = func() uint32 {
149 return 1
150 }
151 return func() {
152 store.InitialUIDValidity = orig
153 }
154}
155
156type testconn struct {
157 t *testing.T
158 conn net.Conn
159 client *imapclient.Conn
160 done chan struct{}
161 serverConn net.Conn
162 account *store.Account
163
164 // Result of last command.
165 lastUntagged []imapclient.Untagged
166 lastResult imapclient.Result
167 lastErr error
168}
169
170func (tc *testconn) check(err error, msg string) {
171 tc.t.Helper()
172 if err != nil {
173 tc.t.Fatalf("%s: %s", msg, err)
174 }
175}
176
177func (tc *testconn) last(l []imapclient.Untagged, r imapclient.Result, err error) {
178 tc.lastUntagged = l
179 tc.lastResult = r
180 tc.lastErr = err
181}
182
183func (tc *testconn) xcode(s string) {
184 tc.t.Helper()
185 if tc.lastResult.Code != s {
186 tc.t.Fatalf("got last code %q, expected %q", tc.lastResult.Code, s)
187 }
188}
189
190func (tc *testconn) xcodeArg(v any) {
191 tc.t.Helper()
192 if !reflect.DeepEqual(tc.lastResult.CodeArg, v) {
193 tc.t.Fatalf("got last code argument %v, expected %v", tc.lastResult.CodeArg, v)
194 }
195}
196
197func (tc *testconn) xuntagged(exps ...imapclient.Untagged) {
198 tc.t.Helper()
199 tc.xuntaggedOpt(true, exps...)
200}
201
202func (tc *testconn) xuntaggedOpt(all bool, exps ...imapclient.Untagged) {
203 tc.t.Helper()
204 last := append([]imapclient.Untagged{}, tc.lastUntagged...)
205 var mismatch any
206next:
207 for ei, exp := range exps {
208 for i, l := range last {
209 if reflect.TypeOf(l) != reflect.TypeOf(exp) {
210 continue
211 }
212 if !reflect.DeepEqual(l, exp) {
213 mismatch = l
214 continue
215 }
216 copy(last[i:], last[i+1:])
217 last = last[:len(last)-1]
218 continue next
219 }
220 if mismatch != nil {
221 tc.t.Fatalf("untagged data mismatch, got:\n\t%T %#v\nexpected:\n\t%T %#v", mismatch, mismatch, exp, exp)
222 }
223 var next string
224 if len(tc.lastUntagged) > 0 {
225 next = fmt.Sprintf(", next %#v", tc.lastUntagged[0])
226 }
227 tc.t.Fatalf("did not find untagged response %#v %T (%d) in %v%s", exp, exp, ei, tc.lastUntagged, next)
228 }
229 if len(last) > 0 && all {
230 tc.t.Fatalf("leftover untagged responses %v", last)
231 }
232}
233
234func tuntagged(t *testing.T, got imapclient.Untagged, dst any) {
235 t.Helper()
236 gotv := reflect.ValueOf(got)
237 dstv := reflect.ValueOf(dst)
238 if gotv.Type() != dstv.Type().Elem() {
239 t.Fatalf("got %v, expected %v", gotv.Type(), dstv.Type().Elem())
240 }
241 dstv.Elem().Set(gotv)
242}
243
244func (tc *testconn) xnountagged() {
245 tc.t.Helper()
246 if len(tc.lastUntagged) != 0 {
247 tc.t.Fatalf("got %v untagged, expected 0", tc.lastUntagged)
248 }
249}
250
251func (tc *testconn) transactf(status, format string, args ...any) {
252 tc.t.Helper()
253 tc.cmdf("", format, args...)
254 tc.response(status)
255}
256
257func (tc *testconn) response(status string) {
258 tc.t.Helper()
259 tc.lastUntagged, tc.lastResult, tc.lastErr = tc.client.Response()
260 tcheck(tc.t, tc.lastErr, "read imap response")
261 if strings.ToUpper(status) != string(tc.lastResult.Status) {
262 tc.t.Fatalf("got status %q, expected %q", tc.lastResult.Status, status)
263 }
264}
265
266func (tc *testconn) cmdf(tag, format string, args ...any) {
267 tc.t.Helper()
268 err := tc.client.Commandf(tag, format, args...)
269 tcheck(tc.t, err, "writing imap command")
270}
271
272func (tc *testconn) readstatus(status string) {
273 tc.t.Helper()
274 tc.response(status)
275}
276
277func (tc *testconn) readprefixline(pre string) {
278 tc.t.Helper()
279 line, err := tc.client.Readline()
280 tcheck(tc.t, err, "read line")
281 if !strings.HasPrefix(line, pre) {
282 tc.t.Fatalf("expected prefix %q, got %q", pre, line)
283 }
284}
285
286func (tc *testconn) writelinef(format string, args ...any) {
287 tc.t.Helper()
288 err := tc.client.Writelinef(format, args...)
289 tcheck(tc.t, err, "write line")
290}
291
292// wait at most 1 second for server to quit.
293func (tc *testconn) waitDone() {
294 tc.t.Helper()
295 t := time.NewTimer(time.Second)
296 select {
297 case <-tc.done:
298 t.Stop()
299 case <-t.C:
300 tc.t.Fatalf("server not done within 1s")
301 }
302}
303
304func (tc *testconn) close() {
305 if tc.account == nil {
306 // Already closed, we are not strict about closing multiple times.
307 return
308 }
309 err := tc.account.Close()
310 tc.check(err, "close account")
311 tc.account = nil
312 tc.client.Close()
313 tc.serverConn.Close()
314 tc.waitDone()
315}
316
317func xparseNumSet(s string) imapclient.NumSet {
318 ns, err := imapclient.ParseNumSet(s)
319 if err != nil {
320 panic(fmt.Sprintf("parsing numset %s: %s", s, err))
321 }
322 return ns
323}
324
325var connCounter int64
326
327func start(t *testing.T) *testconn {
328 return startArgs(t, true, false, true)
329}
330
331func startNoSwitchboard(t *testing.T) *testconn {
332 return startArgs(t, false, false, true)
333}
334
335func startArgs(t *testing.T, first, isTLS, allowLoginWithoutTLS bool) *testconn {
336 limitersInit() // Reset rate limiters.
337
338 if first {
339 os.RemoveAll("../testdata/imap/data")
340 }
341 mox.Context = ctxbg
342 mox.ConfigStaticPath = filepath.FromSlash("../testdata/imap/mox.conf")
343 mox.MustLoadConfig(true, false)
344 acc, err := store.OpenAccount("mjl")
345 tcheck(t, err, "open account")
346 if first {
347 err = acc.SetPassword("testtest")
348 tcheck(t, err, "set password")
349 }
350 switchStop := func() {}
351 if first {
352 switchStop = store.Switchboard()
353 }
354
355 serverConn, clientConn := net.Pipe()
356
357 tlsConfig := &tls.Config{
358 Certificates: []tls.Certificate{fakeCert(t)},
359 }
360 if isTLS {
361 serverConn = tls.Server(serverConn, tlsConfig)
362 clientConn = tls.Client(clientConn, &tls.Config{InsecureSkipVerify: true})
363 }
364
365 done := make(chan struct{})
366 connCounter++
367 cid := connCounter
368 go func() {
369 serve("test", cid, tlsConfig, serverConn, isTLS, allowLoginWithoutTLS)
370 switchStop()
371 close(done)
372 }()
373 client, err := imapclient.New(clientConn, true)
374 tcheck(t, err, "new client")
375 return &testconn{t: t, conn: clientConn, client: client, done: done, serverConn: serverConn, account: acc}
376}
377
378func fakeCert(t *testing.T) tls.Certificate {
379 privKey := ed25519.NewKeyFromSeed(make([]byte, ed25519.SeedSize)) // Fake key, don't use this for real!
380 template := &x509.Certificate{
381 SerialNumber: big.NewInt(1), // Required field...
382 }
383 localCertBuf, err := x509.CreateCertificate(cryptorand.Reader, template, template, privKey.Public(), privKey)
384 if err != nil {
385 t.Fatalf("making certificate: %s", err)
386 }
387 cert, err := x509.ParseCertificate(localCertBuf)
388 if err != nil {
389 t.Fatalf("parsing generated certificate: %s", err)
390 }
391 c := tls.Certificate{
392 Certificate: [][]byte{localCertBuf},
393 PrivateKey: privKey,
394 Leaf: cert,
395 }
396 return c
397}
398
399func TestLogin(t *testing.T) {
400 tc := start(t)
401 defer tc.close()
402
403 tc.transactf("bad", "login too many args")
404 tc.transactf("bad", "login") // no args
405 tc.transactf("no", "login mjl@mox.example badpass")
406 tc.transactf("no", "login mjl testtest") // must use email, not account
407 tc.transactf("no", "login mjl@mox.example test")
408 tc.transactf("no", "login mjl@mox.example testtesttest")
409 tc.transactf("no", `login "mjl@mox.example" "testtesttest"`)
410 tc.transactf("no", "login \"m\xf8x@mox.example\" \"testtesttest\"")
411 tc.transactf("ok", "login mjl@mox.example testtest")
412 tc.close()
413
414 tc = start(t)
415 tc.transactf("ok", `login "mjl@mox.example" "testtest"`)
416 tc.close()
417
418 tc = start(t)
419 tc.transactf("ok", `login "\"\"@mox.example" "testtest"`)
420 defer tc.close()
421
422 tc.transactf("bad", "logout badarg")
423 tc.transactf("ok", "logout")
424}
425
426// Test that commands don't work in the states they are not supposed to.
427func TestState(t *testing.T) {
428 tc := start(t)
429
430 notAuthenticated := []string{"starttls", "authenticate", "login"}
431 authenticatedOrSelected := []string{"enable", "select", "examine", "create", "delete", "rename", "subscribe", "unsubscribe", "list", "namespace", "status", "append", "idle", "lsub"}
432 selected := []string{"close", "unselect", "expunge", "search", "fetch", "store", "copy", "move", "uid expunge"}
433
434 // Always allowed.
435 tc.transactf("ok", "capability")
436 tc.transactf("ok", "noop")
437 tc.transactf("ok", "logout")
438 tc.close()
439 tc = start(t)
440 defer tc.close()
441
442 // Not authenticated, lots of commands not allowed.
443 for _, cmd := range append(append([]string{}, authenticatedOrSelected...), selected...) {
444 tc.transactf("no", "%s", cmd)
445 }
446
447 // Some commands not allowed when authenticated.
448 tc.transactf("ok", "login mjl@mox.example testtest")
449 for _, cmd := range append(append([]string{}, notAuthenticated...), selected...) {
450 tc.transactf("no", "%s", cmd)
451 }
452
453 tc.transactf("bad", "boguscommand")
454}
455
456func TestNonIMAP(t *testing.T) {
457 tc := start(t)
458 defer tc.close()
459
460 // imap greeting has already been read, we sidestep the imapclient.
461 _, err := fmt.Fprintf(tc.conn, "bogus\r\n")
462 tc.check(err, "write bogus command")
463 tc.readprefixline("* BYE ")
464 if _, err := tc.conn.Read(make([]byte, 1)); err == nil {
465 t.Fatalf("connection not closed after initial bad command")
466 }
467}
468
469func TestLiterals(t *testing.T) {
470 tc := start(t)
471 defer tc.close()
472
473 tc.client.Login("mjl@mox.example", "testtest")
474 tc.client.Create("tmpbox")
475
476 tc.transactf("ok", "rename {6+}\r\ntmpbox {7+}\r\nntmpbox")
477
478 from := "ntmpbox"
479 to := "tmpbox"
480 fmt.Fprint(tc.client, "xtag rename ")
481 tc.client.WriteSyncLiteral(from)
482 fmt.Fprint(tc.client, " ")
483 tc.client.WriteSyncLiteral(to)
484 fmt.Fprint(tc.client, "\r\n")
485 tc.client.LastTag = "xtag"
486 tc.last(tc.client.Response())
487 if tc.lastResult.Status != "OK" {
488 tc.t.Fatalf(`got %q, expected "OK"`, tc.lastResult.Status)
489 }
490}
491
492// Test longer scenario with login, lists, subscribes, status, selects, etc.
493func TestScenario(t *testing.T) {
494 tc := start(t)
495 defer tc.close()
496 tc.transactf("ok", "login mjl@mox.example testtest")
497
498 tc.transactf("bad", " missingcommand")
499
500 tc.transactf("ok", "examine inbox")
501 tc.transactf("ok", "unselect")
502
503 tc.transactf("ok", "examine inbox")
504 tc.transactf("ok", "close")
505
506 tc.transactf("ok", "select inbox")
507 tc.transactf("ok", "close")
508
509 tc.transactf("ok", "select inbox")
510 tc.transactf("ok", "expunge")
511 tc.transactf("ok", "check")
512
513 tc.transactf("ok", "subscribe inbox")
514 tc.transactf("ok", "unsubscribe inbox")
515 tc.transactf("ok", "subscribe inbox")
516
517 tc.transactf("ok", `lsub "" "*"`)
518
519 tc.transactf("ok", `list "" ""`)
520 tc.transactf("ok", `namespace`)
521
522 tc.transactf("ok", "enable utf8=accept")
523 tc.transactf("ok", "enable imap4rev2 utf8=accept")
524
525 tc.transactf("no", "create inbox")
526 tc.transactf("ok", "create tmpbox")
527 tc.transactf("ok", "rename tmpbox ntmpbox")
528 tc.transactf("ok", "delete ntmpbox")
529
530 tc.transactf("ok", "status inbox (uidnext messages uidvalidity deleted size unseen recent)")
531
532 tc.transactf("ok", "append inbox (\\seen) {%d+}\r\n%s", len(exampleMsg), exampleMsg)
533 tc.transactf("no", "append bogus () {%d}", len(exampleMsg))
534 tc.cmdf("", "append inbox () {%d}", len(exampleMsg))
535 tc.readprefixline("+ ")
536 _, err := tc.conn.Write([]byte(exampleMsg + "\r\n"))
537 tc.check(err, "write message")
538 tc.response("ok")
539
540 tc.transactf("ok", "fetch 1 all")
541 tc.transactf("ok", "fetch 1 body")
542 tc.transactf("ok", "fetch 1 binary[]")
543
544 tc.transactf("ok", `store 1 flags (\seen \answered)`)
545 tc.transactf("ok", `store 1 +flags ($junk)`) // should train as junk.
546 tc.transactf("ok", `store 1 -flags ($junk)`) // should retrain as non-junk.
547 tc.transactf("ok", `store 1 -flags (\seen)`) // should untrain completely.
548 tc.transactf("ok", `store 1 -flags (\answered)`)
549 tc.transactf("ok", `store 1 +flags (\answered)`)
550 tc.transactf("ok", `store 1 flags.silent (\seen \answered)`)
551 tc.transactf("ok", `store 1 -flags.silent (\answered)`)
552 tc.transactf("ok", `store 1 +flags.silent (\answered)`)
553 tc.transactf("bad", `store 1 flags (\badflag)`)
554 tc.transactf("ok", "noop")
555
556 tc.transactf("ok", "copy 1 Trash")
557 tc.transactf("ok", "copy 1 Trash")
558 tc.transactf("ok", "move 1 Trash")
559
560 tc.transactf("ok", "close")
561 tc.transactf("ok", "select Trash")
562 tc.transactf("ok", `store 1 flags (\deleted)`)
563 tc.transactf("ok", "expunge")
564 tc.transactf("ok", "noop")
565
566 tc.transactf("ok", `store 1 flags (\deleted)`)
567 tc.transactf("ok", "close")
568 tc.transactf("ok", "delete Trash")
569}
570
571func TestMailbox(t *testing.T) {
572 tc := start(t)
573 defer tc.close()
574 tc.client.Login("mjl@mox.example", "testtest")
575
576 invalid := []string{
577 "e\u0301", // é but as e + acute, not unicode-normalized
578 "/leadingslash",
579 "a//b",
580 "Inbox/",
581 "\x01",
582 " ",
583 "\x7f",
584 "\x80",
585 "\u2028",
586 "\u2029",
587 }
588 for _, bad := range invalid {
589 tc.transactf("no", "select {%d+}\r\n%s", len(bad), bad)
590 }
591}
592
593func TestMailboxDeleted(t *testing.T) {
594 tc := start(t)
595 defer tc.close()
596 tc.client.Login("mjl@mox.example", "testtest")
597
598 tc2 := startNoSwitchboard(t)
599 defer tc2.close()
600 tc2.client.Login("mjl@mox.example", "testtest")
601
602 tc.client.Create("testbox")
603 tc2.client.Select("testbox")
604 tc.client.Delete("testbox")
605
606 // Now try to operate on testbox while it has been removed.
607 tc2.transactf("no", "check")
608 tc2.transactf("no", "expunge")
609 tc2.transactf("no", "uid expunge 1")
610 tc2.transactf("no", "search all")
611 tc2.transactf("no", "uid search all")
612 tc2.transactf("no", "fetch 1:* all")
613 tc2.transactf("no", "uid fetch 1 all")
614 tc2.transactf("no", "store 1 flags ()")
615 tc2.transactf("no", "uid store 1 flags ()")
616 tc2.transactf("bad", "copy 1 inbox") // msgseq 1 not available.
617 tc2.transactf("no", "uid copy 1 inbox")
618 tc2.transactf("bad", "move 1 inbox") // msgseq 1 not available.
619 tc2.transactf("no", "uid move 1 inbox")
620
621 tc2.transactf("ok", "unselect")
622
623 tc.client.Create("testbox")
624 tc2.client.Select("testbox")
625 tc.client.Delete("testbox")
626 tc2.transactf("ok", "close")
627}
628
629func TestID(t *testing.T) {
630 tc := start(t)
631 defer tc.close()
632 tc.client.Login("mjl@mox.example", "testtest")
633
634 tc.transactf("ok", "id nil")
635 tc.xuntagged(imapclient.UntaggedID{"name": "mox", "version": moxvar.Version})
636
637 tc.transactf("ok", `id ("name" "mox" "version" "1.2.3" "other" "test" "test" nil)`)
638 tc.xuntagged(imapclient.UntaggedID{"name": "mox", "version": moxvar.Version})
639
640 tc.transactf("bad", `id ("name" "mox" "name" "mox")`) // Duplicate field.
641}
642
643func TestSequence(t *testing.T) {
644 tc := start(t)
645 defer tc.close()
646 tc.client.Login("mjl@mox.example", "testtest")
647 tc.client.Select("inbox")
648
649 tc.transactf("bad", "fetch * all") // ../rfc/9051:7018
650 tc.transactf("bad", "fetch 1 all") // ../rfc/9051:7018
651
652 tc.transactf("ok", "uid fetch 1 all") // non-existing messages are OK for uids.
653 tc.transactf("ok", "uid fetch * all") // * is like uidnext, a non-existing message.
654
655 tc.client.Append("inbox", nil, nil, []byte(exampleMsg))
656 tc.client.Append("inbox", nil, nil, []byte(exampleMsg))
657 tc.transactf("ok", "fetch 2:1,1 uid") // We reorder 2:1 to 1:2, but we don't deduplicate numbers.
658 tc.xuntagged(
659 imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1)}},
660 imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(2)}},
661 imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1)}},
662 )
663
664 tc.transactf("ok", "uid fetch 3:* uid") // Because * is the last message, which is 2, the range becomes 3:2, which matches the last message.
665 tc.xuntagged(imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(2)}})
666}
667
668// Test that a message that is expunged by another session can be read as long as a
669// reference is held by a session. New sessions do not see the expunged message.
670// todo: possibly implement the additional reference counting. so far it hasn't been worth the trouble.
671func DisabledTestReference(t *testing.T) {
672 tc := start(t)
673 defer tc.close()
674 tc.client.Login("mjl@mox.example", "testtest")
675 tc.client.Select("inbox")
676 tc.client.Append("inbox", nil, nil, []byte(exampleMsg))
677
678 tc2 := startNoSwitchboard(t)
679 defer tc2.close()
680 tc2.client.Login("mjl@mox.example", "testtest")
681 tc2.client.Select("inbox")
682
683 tc.client.StoreFlagsSet("1", true, `\Deleted`)
684 tc.client.Expunge()
685
686 tc3 := startNoSwitchboard(t)
687 defer tc3.close()
688 tc3.client.Login("mjl@mox.example", "testtest")
689 tc3.transactf("ok", `list "" "inbox" return (status (messages))`)
690 tc3.xuntagged(imapclient.UntaggedList{Separator: '/', Mailbox: "Inbox"}, imapclient.UntaggedStatus{Mailbox: "Inbox", Attrs: map[string]int64{"MESSAGES": 0}})
691
692 tc2.transactf("ok", "fetch 1 rfc822.size")
693 tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchRFC822Size(len(exampleMsg))}})
694}
695