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