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