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