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