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