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 switchStop func()
166
167 // Result of last command.
168 lastUntagged []imapclient.Untagged
169 lastResult imapclient.Result
170 lastErr error
171}
172
173func (tc *testconn) check(err error, msg string) {
174 tc.t.Helper()
175 if err != nil {
176 tc.t.Fatalf("%s: %s", msg, err)
177 }
178}
179
180func (tc *testconn) last(l []imapclient.Untagged, r imapclient.Result, err error) {
181 tc.lastUntagged = l
182 tc.lastResult = r
183 tc.lastErr = err
184}
185
186func (tc *testconn) xcode(s string) {
187 tc.t.Helper()
188 if tc.lastResult.Code != s {
189 tc.t.Fatalf("got last code %q, expected %q", tc.lastResult.Code, s)
190 }
191}
192
193func (tc *testconn) xcodeArg(v any) {
194 tc.t.Helper()
195 if !reflect.DeepEqual(tc.lastResult.CodeArg, v) {
196 tc.t.Fatalf("got last code argument %v, expected %v", tc.lastResult.CodeArg, v)
197 }
198}
199
200func (tc *testconn) xuntagged(exps ...imapclient.Untagged) {
201 tc.t.Helper()
202 tc.xuntaggedOpt(true, exps...)
203}
204
205func (tc *testconn) xuntaggedOpt(all bool, exps ...imapclient.Untagged) {
206 tc.t.Helper()
207 last := append([]imapclient.Untagged{}, tc.lastUntagged...)
208 var mismatch any
209next:
210 for ei, exp := range exps {
211 for i, l := range last {
212 if reflect.TypeOf(l) != reflect.TypeOf(exp) {
213 continue
214 }
215 if !reflect.DeepEqual(l, exp) {
216 mismatch = l
217 continue
218 }
219 copy(last[i:], last[i+1:])
220 last = last[:len(last)-1]
221 continue next
222 }
223 if mismatch != nil {
224 tc.t.Fatalf("untagged data mismatch, got:\n\t%T %#v\nexpected:\n\t%T %#v", mismatch, mismatch, exp, exp)
225 }
226 var next string
227 if len(tc.lastUntagged) > 0 {
228 next = fmt.Sprintf(", next %#v", tc.lastUntagged[0])
229 }
230 tc.t.Fatalf("did not find untagged response %#v %T (%d) in %v%s", exp, exp, ei, tc.lastUntagged, next)
231 }
232 if len(last) > 0 && all {
233 tc.t.Fatalf("leftover untagged responses %v", last)
234 }
235}
236
237func tuntagged(t *testing.T, got imapclient.Untagged, dst any) {
238 t.Helper()
239 gotv := reflect.ValueOf(got)
240 dstv := reflect.ValueOf(dst)
241 if gotv.Type() != dstv.Type().Elem() {
242 t.Fatalf("got %v, expected %v", gotv.Type(), dstv.Type().Elem())
243 }
244 dstv.Elem().Set(gotv)
245}
246
247func (tc *testconn) xnountagged() {
248 tc.t.Helper()
249 if len(tc.lastUntagged) != 0 {
250 tc.t.Fatalf("got %v untagged, expected 0", tc.lastUntagged)
251 }
252}
253
254func (tc *testconn) transactf(status, format string, args ...any) {
255 tc.t.Helper()
256 tc.cmdf("", format, args...)
257 tc.response(status)
258}
259
260func (tc *testconn) response(status string) {
261 tc.t.Helper()
262 tc.lastUntagged, tc.lastResult, tc.lastErr = tc.client.Response()
263 tcheck(tc.t, tc.lastErr, "read imap response")
264 if strings.ToUpper(status) != string(tc.lastResult.Status) {
265 tc.t.Fatalf("got status %q, expected %q", tc.lastResult.Status, status)
266 }
267}
268
269func (tc *testconn) cmdf(tag, format string, args ...any) {
270 tc.t.Helper()
271 err := tc.client.Commandf(tag, format, args...)
272 tcheck(tc.t, err, "writing imap command")
273}
274
275func (tc *testconn) readstatus(status string) {
276 tc.t.Helper()
277 tc.response(status)
278}
279
280func (tc *testconn) readprefixline(pre string) {
281 tc.t.Helper()
282 line, err := tc.client.Readline()
283 tcheck(tc.t, err, "read line")
284 if !strings.HasPrefix(line, pre) {
285 tc.t.Fatalf("expected prefix %q, got %q", pre, line)
286 }
287}
288
289func (tc *testconn) writelinef(format string, args ...any) {
290 tc.t.Helper()
291 err := tc.client.Writelinef(format, args...)
292 tcheck(tc.t, err, "write line")
293}
294
295// wait at most 1 second for server to quit.
296func (tc *testconn) waitDone() {
297 tc.t.Helper()
298 t := time.NewTimer(time.Second)
299 select {
300 case <-tc.done:
301 t.Stop()
302 case <-t.C:
303 tc.t.Fatalf("server not done within 1s")
304 }
305}
306
307func (tc *testconn) close() {
308 if tc.account == nil {
309 // Already closed, we are not strict about closing multiple times.
310 return
311 }
312 err := tc.account.Close()
313 tc.check(err, "close account")
314 // no account.CheckClosed(), the tests open accounts multiple times.
315 tc.account = nil
316 tc.client.Close()
317 tc.serverConn.Close()
318 tc.waitDone()
319 if tc.switchStop != nil {
320 tc.switchStop()
321 }
322}
323
324func xparseNumSet(s string) imapclient.NumSet {
325 ns, err := imapclient.ParseNumSet(s)
326 if err != nil {
327 panic(fmt.Sprintf("parsing numset %s: %s", s, err))
328 }
329 return ns
330}
331
332var connCounter int64
333
334func start(t *testing.T) *testconn {
335 return startArgs(t, true, false, true, true, "mjl")
336}
337
338func startNoSwitchboard(t *testing.T) *testconn {
339 return startArgs(t, false, false, true, false, "mjl")
340}
341
342const password0 = "te\u0301st \u00a0\u2002\u200a" // NFD and various unicode spaces.
343const password1 = "tést " // PRECIS normalized, with NFC.
344
345func startArgs(t *testing.T, first, immediateTLS bool, allowLoginWithoutTLS, setPassword bool, accname string) *testconn {
346 return startArgsMore(t, first, immediateTLS, nil, nil, allowLoginWithoutTLS, false, setPassword, accname, nil)
347}
348
349// todo: the parameters and usage are too much now. change to scheme similar to smtpserver, with params in a struct, and a separate method for init and making a connection.
350func startArgsMore(t *testing.T, first, immediateTLS bool, serverConfig, clientConfig *tls.Config, allowLoginWithoutTLS, noCloseSwitchboard, setPassword bool, accname string, afterInit func() error) *testconn {
351 limitersInit() // Reset rate limiters.
352
353 mox.Context = ctxbg
354 mox.ConfigStaticPath = filepath.FromSlash("../testdata/imap/mox.conf")
355 mox.MustLoadConfig(true, false)
356 if first {
357 store.Close() // May not be open, we ignore error.
358 os.RemoveAll("../testdata/imap/data")
359 err := store.Init(ctxbg)
360 tcheck(t, err, "store init")
361 }
362 acc, err := store.OpenAccount(pkglog, accname)
363 tcheck(t, err, "open account")
364 if setPassword {
365 err = acc.SetPassword(pkglog, password0)
366 tcheck(t, err, "set password")
367 }
368 switchStop := func() {}
369 if first {
370 switchStop = store.Switchboard()
371 }
372
373 if afterInit != nil {
374 err := afterInit()
375 tcheck(t, err, "after init")
376 }
377
378 serverConn, clientConn := net.Pipe()
379
380 if serverConfig == nil {
381 serverConfig = &tls.Config{
382 Certificates: []tls.Certificate{fakeCert(t, false)},
383 }
384 }
385 if immediateTLS {
386 if clientConfig == nil {
387 clientConfig = &tls.Config{InsecureSkipVerify: true}
388 }
389 clientConn = tls.Client(clientConn, clientConfig)
390 }
391
392 done := make(chan struct{})
393 connCounter++
394 cid := connCounter
395 go func() {
396 serve("test", cid, serverConfig, serverConn, immediateTLS, allowLoginWithoutTLS)
397 if !noCloseSwitchboard {
398 switchStop()
399 }
400 close(done)
401 }()
402 client, err := imapclient.New(clientConn, true)
403 tcheck(t, err, "new client")
404 tc := &testconn{t: t, conn: clientConn, client: client, done: done, serverConn: serverConn, account: acc}
405 if first && noCloseSwitchboard {
406 tc.switchStop = switchStop
407 }
408 return tc
409}
410
411func fakeCert(t *testing.T, randomkey bool) tls.Certificate {
412 seed := make([]byte, ed25519.SeedSize)
413 if randomkey {
414 cryptorand.Read(seed)
415 }
416 privKey := ed25519.NewKeyFromSeed(seed) // Fake key, don't use this for real!
417 template := &x509.Certificate{
418 SerialNumber: big.NewInt(1), // Required field...
419 // Valid period is needed to get session resumption enabled.
420 NotBefore: time.Now().Add(-time.Minute),
421 NotAfter: time.Now().Add(time.Hour),
422 }
423 localCertBuf, err := x509.CreateCertificate(cryptorand.Reader, template, template, privKey.Public(), privKey)
424 if err != nil {
425 t.Fatalf("making certificate: %s", err)
426 }
427 cert, err := x509.ParseCertificate(localCertBuf)
428 if err != nil {
429 t.Fatalf("parsing generated certificate: %s", err)
430 }
431 c := tls.Certificate{
432 Certificate: [][]byte{localCertBuf},
433 PrivateKey: privKey,
434 Leaf: cert,
435 }
436 return c
437}
438
439func TestLogin(t *testing.T) {
440 tc := start(t)
441 defer tc.close()
442
443 tc.transactf("bad", "login too many args")
444 tc.transactf("bad", "login") // no args
445 tc.transactf("no", "login mjl@mox.example badpass")
446 tc.transactf("no", `login mjl "%s"`, password0) // must use email, not account
447 tc.transactf("no", "login mjl@mox.example test")
448 tc.transactf("no", "login mjl@mox.example testtesttest")
449 tc.transactf("no", `login "mjl@mox.example" "testtesttest"`)
450 tc.transactf("no", "login \"m\xf8x@mox.example\" \"testtesttest\"")
451 tc.transactf("ok", `login mjl@mox.example "%s"`, password0)
452 tc.close()
453
454 tc = start(t)
455 tc.transactf("ok", `login "mjl@mox.example" "%s"`, password0)
456 tc.close()
457
458 tc = start(t)
459 tc.transactf("ok", `login "\"\"@mox.example" "%s"`, password0)
460 defer tc.close()
461
462 tc.transactf("bad", "logout badarg")
463 tc.transactf("ok", "logout")
464}
465
466// Test that commands don't work in the states they are not supposed to.
467func TestState(t *testing.T) {
468 tc := start(t)
469
470 notAuthenticated := []string{"starttls", "authenticate", "login"}
471 authenticatedOrSelected := []string{"enable", "select", "examine", "create", "delete", "rename", "subscribe", "unsubscribe", "list", "namespace", "status", "append", "idle", "lsub"}
472 selected := []string{"close", "unselect", "expunge", "search", "fetch", "store", "copy", "move", "uid expunge"}
473
474 // Always allowed.
475 tc.transactf("ok", "capability")
476 tc.transactf("ok", "noop")
477 tc.transactf("ok", "logout")
478 tc.close()
479 tc = start(t)
480 defer tc.close()
481
482 // Not authenticated, lots of commands not allowed.
483 for _, cmd := range append(append([]string{}, authenticatedOrSelected...), selected...) {
484 tc.transactf("no", "%s", cmd)
485 }
486
487 // Some commands not allowed when authenticated.
488 tc.transactf("ok", `login mjl@mox.example "%s"`, password0)
489 for _, cmd := range append(append([]string{}, notAuthenticated...), selected...) {
490 tc.transactf("no", "%s", cmd)
491 }
492
493 tc.transactf("bad", "boguscommand")
494}
495
496func TestNonIMAP(t *testing.T) {
497 tc := start(t)
498 defer tc.close()
499
500 // imap greeting has already been read, we sidestep the imapclient.
501 _, err := fmt.Fprintf(tc.conn, "bogus\r\n")
502 tc.check(err, "write bogus command")
503 tc.readprefixline("* BYE ")
504 if _, err := tc.conn.Read(make([]byte, 1)); err == nil {
505 t.Fatalf("connection not closed after initial bad command")
506 }
507}
508
509func TestLiterals(t *testing.T) {
510 tc := start(t)
511 defer tc.close()
512
513 tc.client.Login("mjl@mox.example", password0)
514 tc.client.Create("tmpbox")
515
516 tc.transactf("ok", "rename {6+}\r\ntmpbox {7+}\r\nntmpbox")
517
518 from := "ntmpbox"
519 to := "tmpbox"
520 fmt.Fprint(tc.client, "xtag rename ")
521 tc.client.WriteSyncLiteral(from)
522 fmt.Fprint(tc.client, " ")
523 tc.client.WriteSyncLiteral(to)
524 fmt.Fprint(tc.client, "\r\n")
525 tc.client.LastTag = "xtag"
526 tc.last(tc.client.Response())
527 if tc.lastResult.Status != "OK" {
528 tc.t.Fatalf(`got %q, expected "OK"`, tc.lastResult.Status)
529 }
530}
531
532// Test longer scenario with login, lists, subscribes, status, selects, etc.
533func TestScenario(t *testing.T) {
534 tc := start(t)
535 defer tc.close()
536 tc.transactf("ok", `login mjl@mox.example "%s"`, password0)
537
538 tc.transactf("bad", " missingcommand")
539
540 tc.transactf("ok", "examine inbox")
541 tc.transactf("ok", "unselect")
542
543 tc.transactf("ok", "examine inbox")
544 tc.transactf("ok", "close")
545
546 tc.transactf("ok", "select inbox")
547 tc.transactf("ok", "close")
548
549 tc.transactf("ok", "select inbox")
550 tc.transactf("ok", "expunge")
551 tc.transactf("ok", "check")
552
553 tc.transactf("ok", "subscribe inbox")
554 tc.transactf("ok", "unsubscribe inbox")
555 tc.transactf("ok", "subscribe inbox")
556
557 tc.transactf("ok", `lsub "" "*"`)
558
559 tc.transactf("ok", `list "" ""`)
560 tc.transactf("ok", `namespace`)
561
562 tc.transactf("ok", "enable utf8=accept")
563 tc.transactf("ok", "enable imap4rev2 utf8=accept")
564
565 tc.transactf("no", "create inbox")
566 tc.transactf("ok", "create tmpbox")
567 tc.transactf("ok", "rename tmpbox ntmpbox")
568 tc.transactf("ok", "delete ntmpbox")
569
570 tc.transactf("ok", "status inbox (uidnext messages uidvalidity deleted size unseen recent)")
571
572 tc.transactf("ok", "append inbox (\\seen) {%d+}\r\n%s", len(exampleMsg), exampleMsg)
573 tc.transactf("no", "append bogus () {%d}", len(exampleMsg))
574 tc.cmdf("", "append inbox () {%d}", len(exampleMsg))
575 tc.readprefixline("+ ")
576 _, err := tc.conn.Write([]byte(exampleMsg + "\r\n"))
577 tc.check(err, "write message")
578 tc.response("ok")
579
580 tc.transactf("ok", "fetch 1 all")
581 tc.transactf("ok", "fetch 1 body")
582 tc.transactf("ok", "fetch 1 binary[]")
583
584 tc.transactf("ok", `store 1 flags (\seen \answered)`)
585 tc.transactf("ok", `store 1 +flags ($junk)`) // should train as junk.
586 tc.transactf("ok", `store 1 -flags ($junk)`) // should retrain as non-junk.
587 tc.transactf("ok", `store 1 -flags (\seen)`) // should untrain completely.
588 tc.transactf("ok", `store 1 -flags (\answered)`)
589 tc.transactf("ok", `store 1 +flags (\answered)`)
590 tc.transactf("ok", `store 1 flags.silent (\seen \answered)`)
591 tc.transactf("ok", `store 1 -flags.silent (\answered)`)
592 tc.transactf("ok", `store 1 +flags.silent (\answered)`)
593 tc.transactf("bad", `store 1 flags (\badflag)`)
594 tc.transactf("ok", "noop")
595
596 tc.transactf("ok", "copy 1 Trash")
597 tc.transactf("ok", "copy 1 Trash")
598 tc.transactf("ok", "move 1 Trash")
599
600 tc.transactf("ok", "close")
601 tc.transactf("ok", "select Trash")
602 tc.transactf("ok", `store 1 flags (\deleted)`)
603 tc.transactf("ok", "expunge")
604 tc.transactf("ok", "noop")
605
606 tc.transactf("ok", `store 1 flags (\deleted)`)
607 tc.transactf("ok", "close")
608 tc.transactf("ok", "delete Trash")
609}
610
611func TestMailbox(t *testing.T) {
612 tc := start(t)
613 defer tc.close()
614 tc.client.Login("mjl@mox.example", password0)
615
616 invalid := []string{
617 "e\u0301", // é but as e + acute, not unicode-normalized
618 "/leadingslash",
619 "a//b",
620 "Inbox/",
621 "\x01",
622 " ",
623 "\x7f",
624 "\x80",
625 "\u2028",
626 "\u2029",
627 }
628 for _, bad := range invalid {
629 tc.transactf("no", "select {%d+}\r\n%s", len(bad), bad)
630 }
631}
632
633func TestMailboxDeleted(t *testing.T) {
634 tc := start(t)
635 defer tc.close()
636
637 tc2 := startNoSwitchboard(t)
638 defer tc2.close()
639
640 tc.client.Login("mjl@mox.example", password0)
641 tc2.client.Login("mjl@mox.example", password0)
642
643 tc.client.Create("testbox")
644 tc2.client.Select("testbox")
645 tc.client.Delete("testbox")
646
647 // Now try to operate on testbox while it has been removed.
648 tc2.transactf("no", "check")
649 tc2.transactf("no", "expunge")
650 tc2.transactf("no", "uid expunge 1")
651 tc2.transactf("no", "search all")
652 tc2.transactf("no", "uid search all")
653 tc2.transactf("no", "fetch 1:* all")
654 tc2.transactf("no", "uid fetch 1 all")
655 tc2.transactf("no", "store 1 flags ()")
656 tc2.transactf("no", "uid store 1 flags ()")
657 tc2.transactf("bad", "copy 1 inbox") // msgseq 1 not available.
658 tc2.transactf("no", "uid copy 1 inbox")
659 tc2.transactf("bad", "move 1 inbox") // msgseq 1 not available.
660 tc2.transactf("no", "uid move 1 inbox")
661
662 tc2.transactf("ok", "unselect")
663
664 tc.client.Create("testbox")
665 tc2.client.Select("testbox")
666 tc.client.Delete("testbox")
667 tc2.transactf("ok", "close")
668}
669
670func TestID(t *testing.T) {
671 tc := start(t)
672 defer tc.close()
673 tc.client.Login("mjl@mox.example", password0)
674
675 tc.transactf("ok", "id nil")
676 tc.xuntagged(imapclient.UntaggedID{"name": "mox", "version": moxvar.Version})
677
678 tc.transactf("ok", `id ("name" "mox" "version" "1.2.3" "other" "test" "test" nil)`)
679 tc.xuntagged(imapclient.UntaggedID{"name": "mox", "version": moxvar.Version})
680
681 tc.transactf("bad", `id ("name" "mox" "name" "mox")`) // Duplicate field.
682}
683
684func TestSequence(t *testing.T) {
685 tc := start(t)
686 defer tc.close()
687 tc.client.Login("mjl@mox.example", password0)
688 tc.client.Select("inbox")
689
690 tc.transactf("bad", "fetch * all") // ../rfc/9051:7018
691 tc.transactf("bad", "fetch 1 all") // ../rfc/9051:7018
692
693 tc.transactf("ok", "uid fetch 1 all") // non-existing messages are OK for uids.
694 tc.transactf("ok", "uid fetch * all") // * is like uidnext, a non-existing message.
695
696 tc.client.Append("inbox", nil, nil, []byte(exampleMsg))
697 tc.client.Append("inbox", nil, nil, []byte(exampleMsg))
698 tc.transactf("ok", "fetch 2:1,1 uid") // We reorder 2:1 to 1:2, but we don't deduplicate numbers.
699 tc.xuntagged(
700 imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1)}},
701 imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(2)}},
702 imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1)}},
703 )
704
705 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.
706 tc.xuntagged(imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(2)}})
707}
708
709// Test that a message that is expunged by another session can be read as long as a
710// reference is held by a session. New sessions do not see the expunged message.
711// todo: possibly implement the additional reference counting. so far it hasn't been worth the trouble.
712func DisabledTestReference(t *testing.T) {
713 tc := start(t)
714 defer tc.close()
715 tc.client.Login("mjl@mox.example", password0)
716 tc.client.Select("inbox")
717 tc.client.Append("inbox", nil, nil, []byte(exampleMsg))
718
719 tc2 := startNoSwitchboard(t)
720 defer tc2.close()
721 tc2.client.Login("mjl@mox.example", password0)
722 tc2.client.Select("inbox")
723
724 tc.client.StoreFlagsSet("1", true, `\Deleted`)
725 tc.client.Expunge()
726
727 tc3 := startNoSwitchboard(t)
728 defer tc3.close()
729 tc3.client.Login("mjl@mox.example", password0)
730 tc3.transactf("ok", `list "" "inbox" return (status (messages))`)
731 tc3.xuntagged(imapclient.UntaggedList{Separator: '/', Mailbox: "Inbox"}, imapclient.UntaggedStatus{Mailbox: "Inbox", Attrs: map[imapclient.StatusAttr]int64{imapclient.StatusMessages: 0}})
732
733 tc2.transactf("ok", "fetch 1 rfc822.size")
734 tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchRFC822Size(len(exampleMsg))}})
735}
736