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 // no account.CheckClosed(), the tests open accounts multiple times.
314 tc.account = nil
315 tc.client.Close()
316 tc.serverConn.Close()
317 tc.waitDone()
318}
319
320func xparseNumSet(s string) imapclient.NumSet {
321 ns, err := imapclient.ParseNumSet(s)
322 if err != nil {
323 panic(fmt.Sprintf("parsing numset %s: %s", s, err))
324 }
325 return ns
326}
327
328var connCounter int64
329
330func start(t *testing.T) *testconn {
331 return startArgs(t, true, false, true, true, "mjl")
332}
333
334func startNoSwitchboard(t *testing.T) *testconn {
335 return startArgs(t, false, false, true, false, "mjl")
336}
337
338const password0 = "te\u0301st \u00a0\u2002\u200a" // NFD and various unicode spaces.
339const password1 = "tést " // PRECIS normalized, with NFC.
340
341func startArgs(t *testing.T, first, isTLS, allowLoginWithoutTLS, setPassword bool, accname string) *testconn {
342 limitersInit() // Reset rate limiters.
343
344 if first {
345 os.RemoveAll("../testdata/imap/data")
346 }
347 mox.Context = ctxbg
348 mox.ConfigStaticPath = filepath.FromSlash("../testdata/imap/mox.conf")
349 mox.MustLoadConfig(true, false)
350 acc, err := store.OpenAccount(pkglog, accname)
351 tcheck(t, err, "open account")
352 if setPassword {
353 err = acc.SetPassword(pkglog, password0)
354 tcheck(t, err, "set password")
355 }
356 switchStop := func() {}
357 if first {
358 switchStop = store.Switchboard()
359 }
360
361 serverConn, clientConn := net.Pipe()
362
363 tlsConfig := &tls.Config{
364 Certificates: []tls.Certificate{fakeCert(t)},
365 }
366 if isTLS {
367 serverConn = tls.Server(serverConn, tlsConfig)
368 clientConn = tls.Client(clientConn, &tls.Config{InsecureSkipVerify: true})
369 }
370
371 done := make(chan struct{})
372 connCounter++
373 cid := connCounter
374 go func() {
375 serve("test", cid, tlsConfig, serverConn, isTLS, allowLoginWithoutTLS)
376 switchStop()
377 close(done)
378 }()
379 client, err := imapclient.New(clientConn, true)
380 tcheck(t, err, "new client")
381 return &testconn{t: t, conn: clientConn, client: client, done: done, serverConn: serverConn, account: acc}
382}
383
384func fakeCert(t *testing.T) tls.Certificate {
385 privKey := ed25519.NewKeyFromSeed(make([]byte, ed25519.SeedSize)) // Fake key, don't use this for real!
386 template := &x509.Certificate{
387 SerialNumber: big.NewInt(1), // Required field...
388 }
389 localCertBuf, err := x509.CreateCertificate(cryptorand.Reader, template, template, privKey.Public(), privKey)
390 if err != nil {
391 t.Fatalf("making certificate: %s", err)
392 }
393 cert, err := x509.ParseCertificate(localCertBuf)
394 if err != nil {
395 t.Fatalf("parsing generated certificate: %s", err)
396 }
397 c := tls.Certificate{
398 Certificate: [][]byte{localCertBuf},
399 PrivateKey: privKey,
400 Leaf: cert,
401 }
402 return c
403}
404
405func TestLogin(t *testing.T) {
406 tc := start(t)
407 defer tc.close()
408
409 tc.transactf("bad", "login too many args")
410 tc.transactf("bad", "login") // no args
411 tc.transactf("no", "login mjl@mox.example badpass")
412 tc.transactf("no", `login mjl "%s"`, password0) // must use email, not account
413 tc.transactf("no", "login mjl@mox.example test")
414 tc.transactf("no", "login mjl@mox.example testtesttest")
415 tc.transactf("no", `login "mjl@mox.example" "testtesttest"`)
416 tc.transactf("no", "login \"m\xf8x@mox.example\" \"testtesttest\"")
417 tc.transactf("ok", `login mjl@mox.example "%s"`, password0)
418 tc.close()
419
420 tc = start(t)
421 tc.transactf("ok", `login "mjl@mox.example" "%s"`, password0)
422 tc.close()
423
424 tc = start(t)
425 tc.transactf("ok", `login "\"\"@mox.example" "%s"`, password0)
426 defer tc.close()
427
428 tc.transactf("bad", "logout badarg")
429 tc.transactf("ok", "logout")
430}
431
432// Test that commands don't work in the states they are not supposed to.
433func TestState(t *testing.T) {
434 tc := start(t)
435
436 notAuthenticated := []string{"starttls", "authenticate", "login"}
437 authenticatedOrSelected := []string{"enable", "select", "examine", "create", "delete", "rename", "subscribe", "unsubscribe", "list", "namespace", "status", "append", "idle", "lsub"}
438 selected := []string{"close", "unselect", "expunge", "search", "fetch", "store", "copy", "move", "uid expunge"}
439
440 // Always allowed.
441 tc.transactf("ok", "capability")
442 tc.transactf("ok", "noop")
443 tc.transactf("ok", "logout")
444 tc.close()
445 tc = start(t)
446 defer tc.close()
447
448 // Not authenticated, lots of commands not allowed.
449 for _, cmd := range append(append([]string{}, authenticatedOrSelected...), selected...) {
450 tc.transactf("no", "%s", cmd)
451 }
452
453 // Some commands not allowed when authenticated.
454 tc.transactf("ok", `login mjl@mox.example "%s"`, password0)
455 for _, cmd := range append(append([]string{}, notAuthenticated...), selected...) {
456 tc.transactf("no", "%s", cmd)
457 }
458
459 tc.transactf("bad", "boguscommand")
460}
461
462func TestNonIMAP(t *testing.T) {
463 tc := start(t)
464 defer tc.close()
465
466 // imap greeting has already been read, we sidestep the imapclient.
467 _, err := fmt.Fprintf(tc.conn, "bogus\r\n")
468 tc.check(err, "write bogus command")
469 tc.readprefixline("* BYE ")
470 if _, err := tc.conn.Read(make([]byte, 1)); err == nil {
471 t.Fatalf("connection not closed after initial bad command")
472 }
473}
474
475func TestLiterals(t *testing.T) {
476 tc := start(t)
477 defer tc.close()
478
479 tc.client.Login("mjl@mox.example", password0)
480 tc.client.Create("tmpbox")
481
482 tc.transactf("ok", "rename {6+}\r\ntmpbox {7+}\r\nntmpbox")
483
484 from := "ntmpbox"
485 to := "tmpbox"
486 fmt.Fprint(tc.client, "xtag rename ")
487 tc.client.WriteSyncLiteral(from)
488 fmt.Fprint(tc.client, " ")
489 tc.client.WriteSyncLiteral(to)
490 fmt.Fprint(tc.client, "\r\n")
491 tc.client.LastTag = "xtag"
492 tc.last(tc.client.Response())
493 if tc.lastResult.Status != "OK" {
494 tc.t.Fatalf(`got %q, expected "OK"`, tc.lastResult.Status)
495 }
496}
497
498// Test longer scenario with login, lists, subscribes, status, selects, etc.
499func TestScenario(t *testing.T) {
500 tc := start(t)
501 defer tc.close()
502 tc.transactf("ok", `login mjl@mox.example "%s"`, password0)
503
504 tc.transactf("bad", " missingcommand")
505
506 tc.transactf("ok", "examine inbox")
507 tc.transactf("ok", "unselect")
508
509 tc.transactf("ok", "examine inbox")
510 tc.transactf("ok", "close")
511
512 tc.transactf("ok", "select inbox")
513 tc.transactf("ok", "close")
514
515 tc.transactf("ok", "select inbox")
516 tc.transactf("ok", "expunge")
517 tc.transactf("ok", "check")
518
519 tc.transactf("ok", "subscribe inbox")
520 tc.transactf("ok", "unsubscribe inbox")
521 tc.transactf("ok", "subscribe inbox")
522
523 tc.transactf("ok", `lsub "" "*"`)
524
525 tc.transactf("ok", `list "" ""`)
526 tc.transactf("ok", `namespace`)
527
528 tc.transactf("ok", "enable utf8=accept")
529 tc.transactf("ok", "enable imap4rev2 utf8=accept")
530
531 tc.transactf("no", "create inbox")
532 tc.transactf("ok", "create tmpbox")
533 tc.transactf("ok", "rename tmpbox ntmpbox")
534 tc.transactf("ok", "delete ntmpbox")
535
536 tc.transactf("ok", "status inbox (uidnext messages uidvalidity deleted size unseen recent)")
537
538 tc.transactf("ok", "append inbox (\\seen) {%d+}\r\n%s", len(exampleMsg), exampleMsg)
539 tc.transactf("no", "append bogus () {%d}", len(exampleMsg))
540 tc.cmdf("", "append inbox () {%d}", len(exampleMsg))
541 tc.readprefixline("+ ")
542 _, err := tc.conn.Write([]byte(exampleMsg + "\r\n"))
543 tc.check(err, "write message")
544 tc.response("ok")
545
546 tc.transactf("ok", "fetch 1 all")
547 tc.transactf("ok", "fetch 1 body")
548 tc.transactf("ok", "fetch 1 binary[]")
549
550 tc.transactf("ok", `store 1 flags (\seen \answered)`)
551 tc.transactf("ok", `store 1 +flags ($junk)`) // should train as junk.
552 tc.transactf("ok", `store 1 -flags ($junk)`) // should retrain as non-junk.
553 tc.transactf("ok", `store 1 -flags (\seen)`) // should untrain completely.
554 tc.transactf("ok", `store 1 -flags (\answered)`)
555 tc.transactf("ok", `store 1 +flags (\answered)`)
556 tc.transactf("ok", `store 1 flags.silent (\seen \answered)`)
557 tc.transactf("ok", `store 1 -flags.silent (\answered)`)
558 tc.transactf("ok", `store 1 +flags.silent (\answered)`)
559 tc.transactf("bad", `store 1 flags (\badflag)`)
560 tc.transactf("ok", "noop")
561
562 tc.transactf("ok", "copy 1 Trash")
563 tc.transactf("ok", "copy 1 Trash")
564 tc.transactf("ok", "move 1 Trash")
565
566 tc.transactf("ok", "close")
567 tc.transactf("ok", "select Trash")
568 tc.transactf("ok", `store 1 flags (\deleted)`)
569 tc.transactf("ok", "expunge")
570 tc.transactf("ok", "noop")
571
572 tc.transactf("ok", `store 1 flags (\deleted)`)
573 tc.transactf("ok", "close")
574 tc.transactf("ok", "delete Trash")
575}
576
577func TestMailbox(t *testing.T) {
578 tc := start(t)
579 defer tc.close()
580 tc.client.Login("mjl@mox.example", password0)
581
582 invalid := []string{
583 "e\u0301", // é but as e + acute, not unicode-normalized
584 "/leadingslash",
585 "a//b",
586 "Inbox/",
587 "\x01",
588 " ",
589 "\x7f",
590 "\x80",
591 "\u2028",
592 "\u2029",
593 }
594 for _, bad := range invalid {
595 tc.transactf("no", "select {%d+}\r\n%s", len(bad), bad)
596 }
597}
598
599func TestMailboxDeleted(t *testing.T) {
600 tc := start(t)
601 defer tc.close()
602 tc.client.Login("mjl@mox.example", password0)
603
604 tc2 := startNoSwitchboard(t)
605 defer tc2.close()
606 tc2.client.Login("mjl@mox.example", password0)
607
608 tc.client.Create("testbox")
609 tc2.client.Select("testbox")
610 tc.client.Delete("testbox")
611
612 // Now try to operate on testbox while it has been removed.
613 tc2.transactf("no", "check")
614 tc2.transactf("no", "expunge")
615 tc2.transactf("no", "uid expunge 1")
616 tc2.transactf("no", "search all")
617 tc2.transactf("no", "uid search all")
618 tc2.transactf("no", "fetch 1:* all")
619 tc2.transactf("no", "uid fetch 1 all")
620 tc2.transactf("no", "store 1 flags ()")
621 tc2.transactf("no", "uid store 1 flags ()")
622 tc2.transactf("bad", "copy 1 inbox") // msgseq 1 not available.
623 tc2.transactf("no", "uid copy 1 inbox")
624 tc2.transactf("bad", "move 1 inbox") // msgseq 1 not available.
625 tc2.transactf("no", "uid move 1 inbox")
626
627 tc2.transactf("ok", "unselect")
628
629 tc.client.Create("testbox")
630 tc2.client.Select("testbox")
631 tc.client.Delete("testbox")
632 tc2.transactf("ok", "close")
633}
634
635func TestID(t *testing.T) {
636 tc := start(t)
637 defer tc.close()
638 tc.client.Login("mjl@mox.example", password0)
639
640 tc.transactf("ok", "id nil")
641 tc.xuntagged(imapclient.UntaggedID{"name": "mox", "version": moxvar.Version})
642
643 tc.transactf("ok", `id ("name" "mox" "version" "1.2.3" "other" "test" "test" nil)`)
644 tc.xuntagged(imapclient.UntaggedID{"name": "mox", "version": moxvar.Version})
645
646 tc.transactf("bad", `id ("name" "mox" "name" "mox")`) // Duplicate field.
647}
648
649func TestSequence(t *testing.T) {
650 tc := start(t)
651 defer tc.close()
652 tc.client.Login("mjl@mox.example", password0)
653 tc.client.Select("inbox")
654
655 tc.transactf("bad", "fetch * all") // ../rfc/9051:7018
656 tc.transactf("bad", "fetch 1 all") // ../rfc/9051:7018
657
658 tc.transactf("ok", "uid fetch 1 all") // non-existing messages are OK for uids.
659 tc.transactf("ok", "uid fetch * all") // * is like uidnext, a non-existing message.
660
661 tc.client.Append("inbox", nil, nil, []byte(exampleMsg))
662 tc.client.Append("inbox", nil, nil, []byte(exampleMsg))
663 tc.transactf("ok", "fetch 2:1,1 uid") // We reorder 2:1 to 1:2, but we don't deduplicate numbers.
664 tc.xuntagged(
665 imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1)}},
666 imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(2)}},
667 imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1)}},
668 )
669
670 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.
671 tc.xuntagged(imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(2)}})
672}
673
674// Test that a message that is expunged by another session can be read as long as a
675// reference is held by a session. New sessions do not see the expunged message.
676// todo: possibly implement the additional reference counting. so far it hasn't been worth the trouble.
677func DisabledTestReference(t *testing.T) {
678 tc := start(t)
679 defer tc.close()
680 tc.client.Login("mjl@mox.example", password0)
681 tc.client.Select("inbox")
682 tc.client.Append("inbox", nil, nil, []byte(exampleMsg))
683
684 tc2 := startNoSwitchboard(t)
685 defer tc2.close()
686 tc2.client.Login("mjl@mox.example", password0)
687 tc2.client.Select("inbox")
688
689 tc.client.StoreFlagsSet("1", true, `\Deleted`)
690 tc.client.Expunge()
691
692 tc3 := startNoSwitchboard(t)
693 defer tc3.close()
694 tc3.client.Login("mjl@mox.example", password0)
695 tc3.transactf("ok", `list "" "inbox" return (status (messages))`)
696 tc3.xuntagged(imapclient.UntaggedList{Separator: '/', Mailbox: "Inbox"}, imapclient.UntaggedStatus{Mailbox: "Inbox", Attrs: map[imapclient.StatusAttr]int64{imapclient.StatusMessages: 0}})
697
698 tc2.transactf("ok", "fetch 1 rfc822.size")
699 tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchRFC822Size(len(exampleMsg))}})
700}
701