6 cryptorand "crypto/rand"
20 "golang.org/x/sys/unix"
22 "github.com/mjl-/bstore"
24 "github.com/mjl-/mox/imapclient"
25 "github.com/mjl-/mox/mlog"
26 "github.com/mjl-/mox/mox-"
27 "github.com/mjl-/mox/moxvar"
28 "github.com/mjl-/mox/store"
32var ctxbg = context.Background()
33var pkglog = mlog.New("imapserver", nil)
38 // Don't slow down tests.
45func ptr[T any](v T) *T {
49func tocrlf(s string) string {
50 return strings.ReplaceAll(s, "\n", "\r\n")
54var exampleMsg = tocrlf(`Date: Mon, 7 Feb 1994 21:52:25 -0800 (PST)
55From: Fred Foobar <foobar@Blurdybloop.example>
56Subject: afternoon meeting
57To: mooch@owatagu.siam.edu.example
58Message-Id: <B27397-0100000@Blurdybloop.example>
60Content-Type: TEXT/PLAIN; CHARSET=US-ASCII
62Hello Joe, do you think we can meet at 3:30 tomorrow?
71Message - multipart/mixed
72Part 1 - no content-type
74Part 3 - multipart/parallel
75Part 3.1 - audio/basic (base64)
76Part 3.2 - image/jpeg (base64, empty)
78Part 5 - message/rfc822
79Part 5.1 - text/plain (quoted-printable)
81var nestedMessage = tocrlf(`MIME-Version: 1.0
82From: Nathaniel Borenstein <nsb@nsb.fv.com>
83To: Ned Freed <ned@innosoft.com>
84Date: Fri, 07 Oct 1994 16:15:05 -0700 (PDT)
85Subject: A multipart example
86Content-Type: multipart/mixed;
87 boundary=unique-boundary-1
89This is the preamble area of a multipart message.
90Mail readers that understand multipart format
91should ignore this preamble.
93If you are reading this text, you might want to
94consider changing to a mail reader that understands
95how to properly display multipart messages.
99 ... Some text appears here ...
101[Note that the blank between the boundary and the start
102 of the text in this part means no header fields were
103 given and this is text in the US-ASCII character set.
104 It could have been done with explicit typing as in the
108Content-type: text/plain; charset=US-ASCII
110This could have been part of the previous part, but
111illustrates explicit versus implicit typing of body
115Content-Type: multipart/parallel; boundary=unique-boundary-2
118Content-Type: audio/basic
119Content-Transfer-Encoding: base64
124Content-Type: image/jpeg
125Content-Transfer-Encoding: base64
126Content-Disposition: inline; filename=image.jpg
132Content-type: text/enriched
134This is <bold><italic>enriched.</italic></bold>
135<smaller>as defined in RFC 1896</smaller>
138<bigger><bigger>cool?</bigger></bigger>
141Content-Type: message/rfc822
142Content-MD5: MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=
143Content-Language: en,de
144Content-Location: http://localhost
146From: info@mox.example
147To: mox <info@mox.example>
148Subject: (subject in US-ASCII)
149Content-Type: Text/plain; charset=ISO-8859-1
150Content-Transfer-Encoding: Quoted-printable
152 ... Additional text in ISO-8859-1 goes here ...
157func tcheck(t *testing.T, err error, msg string) {
160 t.Fatalf("%s: %s", msg, err)
164func mustParseUntagged(s string) imapclient.Untagged {
165 r, err := imapclient.ParseUntagged(s + "\r\n")
172func mustParseCode(s string) imapclient.Code {
173 r, err := imapclient.ParseCode(s)
180func mockUIDValidity() func() {
181 orig := store.InitialUIDValidity
182 store.InitialUIDValidity = func() uint32 {
186 store.InitialUIDValidity = orig
190type testconn struct {
193 client *imapclient.Conn
197 account *store.Account
201 // Result of last command.
202 lastResponse imapclient.Response
206func (tc *testconn) check(err error, msg string) {
209 tc.t.Fatalf("%s: %s", msg, err)
213func (tc *testconn) last(resp imapclient.Response, err error) {
214 tc.lastResponse = resp
218func (tc *testconn) xcode(c imapclient.Code) {
220 if !reflect.DeepEqual(tc.lastResponse.Code, c) {
221 tc.t.Fatalf("got last code %#v, expected %#v", tc.lastResponse.Code, c)
225func (tc *testconn) xcodeWord(s string) {
227 tc.xcode(imapclient.CodeWord(s))
230func (tc *testconn) xuntagged(exps ...imapclient.Untagged) {
232 tc.xuntaggedOpt(true, exps...)
235func (tc *testconn) xuntaggedOpt(all bool, exps ...imapclient.Untagged) {
237 last := slices.Clone(tc.lastResponse.Untagged)
240 for ei, exp := range exps {
241 for i, l := range last {
242 if reflect.TypeOf(l) != reflect.TypeOf(exp) {
245 if !reflect.DeepEqual(l, exp) {
249 copy(last[i:], last[i+1:])
250 last = last[:len(last)-1]
254 tc.t.Fatalf("untagged data mismatch, got:\n\t%T %#v\nexpected:\n\t%T %#v", mismatch, mismatch, exp, exp)
257 if len(tc.lastResponse.Untagged) > 0 {
258 next = fmt.Sprintf(", next:\n%#v", tc.lastResponse.Untagged[0])
260 tc.t.Fatalf("did not find untagged response:\n%#v %T (%d)\nin %v%s", exp, exp, ei, tc.lastResponse.Untagged, next)
262 if len(last) > 0 && all {
263 tc.t.Fatalf("leftover untagged responses %v", last)
267func tuntagged(t *testing.T, got imapclient.Untagged, dst any) {
269 gotv := reflect.ValueOf(got)
270 dstv := reflect.ValueOf(dst)
271 if gotv.Type() != dstv.Type().Elem() {
272 t.Fatalf("got %#v, expected %#v", got, dstv.Elem().Interface())
274 dstv.Elem().Set(gotv)
277func (tc *testconn) xnountagged() {
279 if len(tc.lastResponse.Untagged) != 0 {
280 tc.t.Fatalf("got %v untagged, expected 0", tc.lastResponse.Untagged)
284func (tc *testconn) readuntagged(exps ...imapclient.Untagged) {
286 for i, exp := range exps {
287 tc.conn.SetReadDeadline(time.Now().Add(3 * time.Second))
288 v, err := tc.client.ReadUntagged()
289 tcheck(tc.t, err, "reading untagged")
290 if !reflect.DeepEqual(v, exp) {
291 tc.t.Fatalf("got %#v, expected %#v, response %d/%d", v, exp, i+1, len(exps))
296func (tc *testconn) transactf(status, format string, args ...any) {
298 tc.cmdf("", format, args...)
302func (tc *testconn) response(status string) {
304 tc.lastResponse, tc.lastErr = tc.client.ReadResponse()
305 if tc.lastErr != nil {
306 if resp, ok := tc.lastErr.(imapclient.Response); ok {
307 if !reflect.DeepEqual(resp, tc.lastResponse) {
308 tc.t.Fatalf("response error %#v != returned response %#v", tc.lastErr, tc.lastResponse)
311 tcheck(tc.t, tc.lastErr, "read imap response")
314 if strings.ToUpper(status) != string(tc.lastResponse.Status) {
315 tc.t.Fatalf("got status %q, expected %q", tc.lastResponse.Status, status)
319func (tc *testconn) cmdf(tag, format string, args ...any) {
321 err := tc.client.WriteCommandf(tag, format, args...)
322 tcheck(tc.t, err, "writing imap command")
325func (tc *testconn) readstatus(status string) {
330func (tc *testconn) readprefixline(pre string) {
332 line, err := tc.client.Readline()
333 tcheck(tc.t, err, "read line")
334 if !strings.HasPrefix(line, pre) {
335 tc.t.Fatalf("expected prefix %q, got %q", pre, line)
339func (tc *testconn) writelinef(format string, args ...any) {
341 err := tc.client.Writelinef(format, args...)
342 tcheck(tc.t, err, "write line")
345// wait at most 1 second for server to quit.
346func (tc *testconn) waitDone() {
348 t := time.NewTimer(time.Second)
353 tc.t.Fatalf("server not done within 1s")
357func (tc *testconn) login(username, password string) {
358 tc.client.Login(username, password)
360 tc.transactf("ok", "enable uidonly")
364// untaggedFetch returns an imapclient.UntaggedFetch or
365// imapclient.UntaggedUIDFetch, depending on whether uidonly is enabled for the
367func (tc *testconn) untaggedFetch(seq, uid uint32, attrs ...imapclient.FetchAttr) any {
369 return imapclient.UntaggedUIDFetch{UID: uid, Attrs: attrs}
371 attrs = append([]imapclient.FetchAttr{imapclient.FetchUID(uid)}, attrs...)
372 return imapclient.UntaggedFetch{Seq: seq, Attrs: attrs}
375// like untaggedFetch, but with explicit UID fetch attribute in case of uidonly.
376func (tc *testconn) untaggedFetchUID(seq, uid uint32, attrs ...imapclient.FetchAttr) any {
377 attrs = append([]imapclient.FetchAttr{imapclient.FetchUID(uid)}, attrs...)
379 return imapclient.UntaggedUIDFetch{UID: uid, Attrs: attrs}
381 return imapclient.UntaggedFetch{Seq: seq, Attrs: attrs}
384func (tc *testconn) close() {
388func (tc *testconn) closeNoWait() {
392func (tc *testconn) close0(waitclose bool) {
394 if unhandledPanics.Swap(0) > 0 {
395 tc.t.Fatalf("unhandled panic in server")
399 if tc.account == nil {
400 // Already closed, we are not strict about closing multiple times.
403 if tc.client != nil {
404 tc.clientPanic = false // Ignore errors writing to TLS connection the server also closed.
407 err := tc.account.Close()
408 tc.check(err, "close account")
410 tc.account.WaitClosed()
413 tc.serverConn.Close()
415 if tc.switchStop != nil {
420func xparseNumSet(s string) imapclient.NumSet {
421 ns, err := imapclient.ParseNumSet(s)
423 panic(fmt.Sprintf("parsing numset %s: %s", s, err))
428func xparseUIDRange(s string) imapclient.NumRange {
429 nr, err := imapclient.ParseUIDRange(s)
431 panic(fmt.Sprintf("parsing uid range %s: %s", s, err))
436func makeAppend(msg string) imapclient.Append {
437 return imapclient.Append{Size: int64(len(msg)), Data: strings.NewReader(msg)}
440func makeAppendTime(msg string, tm time.Time) imapclient.Append {
441 return imapclient.Append{Received: &tm, Size: int64(len(msg)), Data: strings.NewReader(msg)}
446func start(t *testing.T, uidonly bool) *testconn {
447 return startArgs(t, uidonly, true, false, true, true, "mjl")
450func startNoSwitchboard(t *testing.T, uidonly bool) *testconn {
451 return startArgs(t, uidonly, false, false, true, false, "mjl")
454const password0 = "te\u0301st \u00a0\u2002\u200a" // NFD and various unicode spaces.
455const password1 = "tést " // PRECIS normalized, with NFC.
457func startArgs(t *testing.T, uidonly, first, immediateTLS bool, allowLoginWithoutTLS, setPassword bool, accname string) *testconn {
458 return startArgsMore(t, uidonly, first, immediateTLS, nil, nil, allowLoginWithoutTLS, setPassword, accname, nil)
461// namedConn wraps a conn so it can return a RemoteAddr with a non-empty name.
462// The TLS resumption test needs a non-empty name, but on BSDs, the unix domain
463// socket pair has an empty peer name.
464type namedConn struct {
468func (c namedConn) RemoteAddr() net.Addr {
469 return &net.TCPAddr{IP: net.ParseIP("127.0.0.10"), Port: 1234}
472// 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.
473func startArgsMore(t *testing.T, uidonly, first, immediateTLS bool, serverConfig, clientConfig *tls.Config, allowLoginWithoutTLS, setPassword bool, accname string, afterInit func() error) *testconn {
474 limitersInit() // Reset rate limiters.
476 switchStop := func() {}
478 mox.ConfigStaticPath = filepath.FromSlash("../testdata/imap/mox.conf")
479 mox.MustLoadConfig(true, false)
480 store.Close() // May not be open, we ignore error.
481 os.RemoveAll("../testdata/imap/data")
482 err := store.Init(ctxbg)
483 tcheck(t, err, "store init")
484 switchStop = store.Switchboard()
487 acc, err := store.OpenAccount(pkglog, accname, false)
488 tcheck(t, err, "open account")
490 err = acc.SetPassword(pkglog, password0)
491 tcheck(t, err, "set password")
494 // Add a deleted mailbox, may excercise some code paths.
495 err = acc.DB.Write(ctxbg, func(tx *bstore.Tx) error {
496 // todo: add a message to inbox and remove it again. need to change all uids in the tests.
497 // todo: add tests for operating on an expunged mailbox. it should say it doesn't exist.
499 mb, _, _, _, err := acc.MailboxCreate(tx, "expungebox", store.SpecialUse{})
501 return fmt.Errorf("create mailbox: %v", err)
503 if _, _, err := acc.MailboxDelete(ctxbg, pkglog, tx, &mb); err != nil {
504 return fmt.Errorf("delete mailbox: %v", err)
508 tcheck(t, err, "add expunged mailbox")
511 if afterInit != nil {
513 tcheck(t, err, "after init")
516 // We get actual sockets for their buffering behaviour. net.Pipe is synchronous,
517 // and the implementation of the compress extension can write a sync message to an
518 // imap client when that client isn't reading but is trying to write. In the real
519 // world, network buffer will take up those few bytes, so assume the buffer in the
521 fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM, 0)
522 tcheck(t, err, "socketpair")
523 xfdconn := func(fd int, name string) net.Conn {
524 f := os.NewFile(uintptr(fd), name)
525 fc, err := net.FileConn(f)
526 tcheck(t, err, "fileconn")
528 tcheck(t, err, "close file for conn")
530 // Small read/write buffers, for detecting closed/broken connections quickly.
531 uc := fc.(*net.UnixConn)
532 err = uc.SetReadBuffer(512)
533 tcheck(t, err, "set read buffer")
534 uc.SetWriteBuffer(512)
535 tcheck(t, err, "set write buffer")
539 serverConn := xfdconn(fds[0], "server")
540 clientConn := xfdconn(fds[1], "client")
542 if serverConfig == nil {
543 serverConfig = &tls.Config{
544 Certificates: []tls.Certificate{fakeCert(t, false)},
548 if clientConfig == nil {
549 clientConfig = &tls.Config{InsecureSkipVerify: true}
551 clientConn = tls.Client(clientConn, clientConfig)
554 done := make(chan struct{})
556 cid := connCounter - 1
558 const viaHTTPS = false
559 serve("test", cid, serverConfig, serverConn, immediateTLS, allowLoginWithoutTLS, viaHTTPS, "")
563 var opts imapclient.Opts
564 opts = imapclient.Opts{
565 Logger: slog.Default().With("cid", connCounter),
566 Error: func(err error) {
570 opts.Logger.Error("imapclient error", "err", err)
574 client, _ := imapclient.New(clientConn, &opts)
575 tc = &testconn{t: t, conn: clientConn, client: client, uidonly: uidonly, done: done, serverConn: serverConn, account: acc, clientPanic: true}
577 tc.switchStop = switchStop
582func fakeCert(t *testing.T, randomkey bool) tls.Certificate {
583 seed := make([]byte, ed25519.SeedSize)
585 cryptorand.Read(seed)
587 privKey := ed25519.NewKeyFromSeed(seed) // Fake key, don't use this for real!
588 template := &x509.Certificate{
589 SerialNumber: big.NewInt(1), // Required field...
590 // Valid period is needed to get session resumption enabled.
591 NotBefore: time.Now().Add(-time.Minute),
592 NotAfter: time.Now().Add(time.Hour),
594 localCertBuf, err := x509.CreateCertificate(cryptorand.Reader, template, template, privKey.Public(), privKey)
596 t.Fatalf("making certificate: %s", err)
598 cert, err := x509.ParseCertificate(localCertBuf)
600 t.Fatalf("parsing generated certificate: %s", err)
602 c := tls.Certificate{
603 Certificate: [][]byte{localCertBuf},
610func TestLogin(t *testing.T) {
611 tc := start(t, false)
614 tc.transactf("bad", "login too many args")
615 tc.transactf("bad", "login") // no args
616 tc.transactf("no", "login mjl@mox.example badpass")
617 tc.transactf("no", `login mjl "%s"`, password0) // must use email, not account
618 tc.transactf("no", "login mjl@mox.example test")
619 tc.transactf("no", "login mjl@mox.example testtesttest")
620 tc.transactf("no", `login "mjl@mox.example" "testtesttest"`)
621 tc.transactf("no", "login \"m\xf8x@mox.example\" \"testtesttest\"")
622 tc.transactf("ok", `login mjl@mox.example "%s"`, password0)
626 tc.transactf("ok", `login "mjl@mox.example" "%s"`, password0)
630 tc.transactf("ok", `login "\"\"@mox.example" "%s"`, password0)
633 tc.transactf("bad", "logout badarg")
634 tc.transactf("ok", "logout")
637// Test that commands don't work in the states they are not supposed to.
638func TestState(t *testing.T) {
639 tc := start(t, false)
641 notAuthenticated := []string{"starttls", "authenticate", "login"}
642 authenticatedOrSelected := []string{"enable", "select", "examine", "create", "delete", "rename", "subscribe", "unsubscribe", "list", "namespace", "status", "append", "idle", "lsub"}
643 selected := []string{"close", "unselect", "expunge", "search", "fetch", "store", "copy", "move", "uid expunge"}
646 tc.transactf("ok", "capability")
647 tc.transactf("ok", "noop")
648 tc.transactf("ok", "logout")
653 // Not authenticated, lots of commands not allowed.
654 for _, cmd := range slices.Concat(authenticatedOrSelected, selected) {
655 tc.transactf("no", "%s", cmd)
658 // Some commands not allowed when authenticated.
659 tc.transactf("ok", `login mjl@mox.example "%s"`, password0)
660 for _, cmd := range slices.Concat(notAuthenticated, selected) {
661 tc.transactf("no", "%s", cmd)
664 tc.transactf("bad", "boguscommand")
667func TestNonIMAP(t *testing.T) {
668 tc := start(t, false)
671 // imap greeting has already been read, we sidestep the imapclient.
672 _, err := fmt.Fprintf(tc.conn, "bogus\r\n")
673 tc.check(err, "write bogus command")
674 tc.readprefixline("* BYE ")
675 if _, err := tc.conn.Read(make([]byte, 1)); err == nil {
676 t.Fatalf("connection not closed after initial bad command")
680func TestLiterals(t *testing.T) {
681 tc := start(t, false)
684 tc.login("mjl@mox.example", password0)
685 tc.client.Create("tmpbox", nil)
687 tc.transactf("ok", "rename {6+}\r\ntmpbox {7+}\r\nntmpbox")
691 tc.client.LastTagSet("xtag")
692 fmt.Fprint(tc.client, "xtag rename ")
693 tc.client.WriteSyncLiteral(from)
694 fmt.Fprint(tc.client, " ")
695 tc.client.WriteSyncLiteral(to)
696 fmt.Fprint(tc.client, "\r\n")
697 tc.lastResponse, tc.lastErr = tc.client.ReadResponse()
698 if tc.lastResponse.Status != "OK" {
699 tc.t.Fatalf(`got %q, expected "OK"`, tc.lastResponse.Status)
703// Test longer scenario with login, lists, subscribes, status, selects, etc.
704func TestScenario(t *testing.T) {
705 testScenario(t, false)
708func TestScenarioUIDOnly(t *testing.T) {
709 testScenario(t, true)
712func testScenario(t *testing.T, uidonly bool) {
713 tc := start(t, uidonly)
715 tc.login("mjl@mox.example", password0)
717 tc.transactf("bad", " missingcommand")
719 tc.transactf("ok", "examine inbox")
720 tc.transactf("ok", "unselect")
722 tc.transactf("ok", "examine inbox")
723 tc.transactf("ok", "close")
725 tc.transactf("ok", "select inbox")
726 tc.transactf("ok", "close")
728 tc.transactf("ok", "select inbox")
729 tc.transactf("ok", "expunge")
730 tc.transactf("ok", "check")
732 tc.transactf("ok", "subscribe inbox")
733 tc.transactf("ok", "unsubscribe inbox")
734 tc.transactf("ok", "subscribe inbox")
736 tc.transactf("ok", `lsub "" "*"`)
738 tc.transactf("ok", `list "" ""`)
739 tc.transactf("ok", `namespace`)
741 tc.transactf("ok", "enable utf8=accept")
742 tc.transactf("ok", "enable imap4rev2 utf8=accept")
744 tc.transactf("no", "create inbox")
745 tc.transactf("ok", "create tmpbox")
746 tc.transactf("ok", "rename tmpbox ntmpbox")
747 tc.transactf("ok", "delete ntmpbox")
749 tc.transactf("ok", "status inbox (uidnext messages uidvalidity deleted size unseen recent)")
751 tc.transactf("ok", "append inbox (\\seen) {%d+}\r\n%s", len(exampleMsg), exampleMsg)
752 tc.transactf("no", "append bogus () {%d}", len(exampleMsg))
753 tc.cmdf("", "append inbox () {%d}", len(exampleMsg))
754 tc.readprefixline("+ ")
755 _, err := tc.conn.Write([]byte(exampleMsg + "\r\n"))
756 tc.check(err, "write message")
759 tc.transactf("ok", "uid fetch 1 all")
760 tc.transactf("ok", "uid fetch 1 body")
761 tc.transactf("ok", "uid fetch 1 binary[]")
763 tc.transactf("ok", `uid store 1 flags (\seen \answered)`)
764 tc.transactf("ok", `uid store 1 +flags ($junk)`) // should train as junk.
765 tc.transactf("ok", `uid store 1 -flags ($junk)`) // should retrain as non-junk.
766 tc.transactf("ok", `uid store 1 -flags (\seen)`) // should untrain completely.
767 tc.transactf("ok", `uid store 1 -flags (\answered)`)
768 tc.transactf("ok", `uid store 1 +flags (\answered)`)
769 tc.transactf("ok", `uid store 1 flags.silent (\seen \answered)`)
770 tc.transactf("ok", `uid store 1 -flags.silent (\answered)`)
771 tc.transactf("ok", `uid store 1 +flags.silent (\answered)`)
772 tc.transactf("bad", `uid store 1 flags (\badflag)`)
773 tc.transactf("ok", "noop")
775 tc.transactf("ok", "uid copy 1 Trash")
776 tc.transactf("ok", "uid copy 1 Trash")
777 tc.transactf("ok", "uid move 1 Trash")
779 tc.transactf("ok", "close")
780 tc.transactf("ok", "select Trash")
781 tc.transactf("ok", `uid store 1 flags (\deleted)`)
782 tc.transactf("ok", "expunge")
783 tc.transactf("ok", "noop")
785 tc.transactf("ok", `uid store 1 flags (\deleted)`)
786 tc.transactf("ok", "close")
787 tc.transactf("ok", "delete Trash")
793 tc.transactf("ok", "create Trash")
794 tc.transactf("ok", "append inbox (\\seen) {%d+}\r\n%s", len(exampleMsg), exampleMsg)
795 tc.transactf("ok", "select inbox")
797 tc.transactf("ok", "fetch 1 all")
798 tc.transactf("ok", "fetch 1 body")
799 tc.transactf("ok", "fetch 1 binary[]")
801 tc.transactf("ok", `store 1 flags (\seen \answered)`)
802 tc.transactf("ok", `store 1 +flags ($junk)`) // should train as junk.
803 tc.transactf("ok", `store 1 -flags ($junk)`) // should retrain as non-junk.
804 tc.transactf("ok", `store 1 -flags (\seen)`) // should untrain completely.
805 tc.transactf("ok", `store 1 -flags (\answered)`)
806 tc.transactf("ok", `store 1 +flags (\answered)`)
807 tc.transactf("ok", `store 1 flags.silent (\seen \answered)`)
808 tc.transactf("ok", `store 1 -flags.silent (\answered)`)
809 tc.transactf("ok", `store 1 +flags.silent (\answered)`)
810 tc.transactf("bad", `store 1 flags (\badflag)`)
811 tc.transactf("ok", "noop")
813 tc.transactf("ok", "copy 1 Trash")
814 tc.transactf("ok", "copy 1 Trash")
815 tc.transactf("ok", "move 1 Trash")
817 tc.transactf("ok", "close")
818 tc.transactf("ok", "select Trash")
819 tc.transactf("ok", `store 1 flags (\deleted)`)
820 tc.transactf("ok", "expunge")
821 tc.transactf("ok", "noop")
823 tc.transactf("ok", `store 1 flags (\deleted)`)
824 tc.transactf("ok", "close")
825 tc.transactf("ok", "delete Trash")
828func TestMailbox(t *testing.T) {
829 tc := start(t, false)
831 tc.login("mjl@mox.example", password0)
834 "e\u0301", // é but as e + acute, not unicode-normalized
845 for _, bad := range invalid {
846 tc.transactf("no", "select {%d+}\r\n%s", len(bad), bad)
850func TestMailboxDeleted(t *testing.T) {
851 tc := start(t, false)
854 tc2 := startNoSwitchboard(t, false)
855 defer tc2.closeNoWait()
857 tc.login("mjl@mox.example", password0)
858 tc2.login("mjl@mox.example", password0)
860 tc.client.Create("testbox", nil)
861 tc2.client.Select("testbox")
862 tc.client.Delete("testbox")
864 // Now try to operate on testbox while it has been removed.
865 tc2.transactf("no", "check")
866 tc2.transactf("no", "expunge")
867 tc2.transactf("no", "uid expunge 1")
868 tc2.transactf("no", "search all")
869 tc2.transactf("no", "uid search all")
870 tc2.transactf("no", "fetch 1:* all")
871 tc2.transactf("no", "uid fetch 1 all")
872 tc2.transactf("no", "store 1 flags ()")
873 tc2.transactf("no", "uid store 1 flags ()")
874 tc2.transactf("no", "copy 1 inbox")
875 tc2.transactf("no", "uid copy 1 inbox")
876 tc2.transactf("no", "move 1 inbox")
877 tc2.transactf("no", "uid move 1 inbox")
879 tc2.transactf("ok", "unselect")
881 tc.client.Create("testbox", nil)
882 tc2.client.Select("testbox")
883 tc.client.Delete("testbox")
884 tc2.transactf("ok", "close")
887func TestID(t *testing.T) {
888 tc := start(t, false)
890 tc.login("mjl@mox.example", password0)
892 tc.transactf("ok", "id nil")
893 tc.xuntagged(imapclient.UntaggedID{"name": "mox", "version": moxvar.Version})
895 tc.transactf("ok", `id ("name" "mox" "version" "1.2.3" "other" "test" "test" nil)`)
896 tc.xuntagged(imapclient.UntaggedID{"name": "mox", "version": moxvar.Version})
898 tc.transactf("bad", `id ("name" "mox" "name" "mox")`) // Duplicate field.
901func TestSequence(t *testing.T) {
902 testSequence(t, false)
905func TestSequenceUIDOnly(t *testing.T) {
906 testSequence(t, true)
909func testSequence(t *testing.T, uidonly bool) {
910 tc := start(t, uidonly)
912 tc.login("mjl@mox.example", password0)
913 tc.client.Select("inbox")
916 tc.transactf("bad", "fetch 1:* all")
917 tc.transactf("bad", "fetch 1:2 all")
920 tc.transactf("ok", "uid fetch 1 all") // non-existing messages are OK for uids.
921 tc.transactf("ok", "uid fetch * all") // * is like uidnext, a non-existing message.
923 tc.transactf("ok", "uid search return (save) all") // Empty result.
924 tc.transactf("ok", "uid fetch $ uid")
927 tc.client.Append("inbox", makeAppend(exampleMsg))
928 tc.client.Append("inbox", makeAppend(exampleMsg))
930 tc.transactf("ok", "fetch 2:1,1 uid") // We reorder 2:1 to 1:2, and we deduplicate numbers.
932 tc.untaggedFetch(1, 1),
933 tc.untaggedFetch(2, 2),
936 tc.transactf("bad", "fetch 1:3 all")
939 tc.transactf("ok", "uid fetch * flags")
940 tc.xuntagged(tc.untaggedFetch(2, 2, imapclient.FetchFlags(nil)))
942 tc.transactf("ok", "uid fetch 3:* flags") // Because * is the last message, which is 2, the range becomes 3:2, which matches the last message.
943 tc.xuntagged(tc.untaggedFetch(2, 2, imapclient.FetchFlags(nil)))
945 tc.transactf("ok", "uid fetch *:3 flags")
946 tc.xuntagged(tc.untaggedFetch(2, 2, imapclient.FetchFlags(nil)))
948 tc.transactf("ok", "uid search return (save) all") // Empty result.
949 tc.transactf("ok", "uid fetch $ flags")
951 tc.untaggedFetch(1, 1, imapclient.FetchFlags(nil)),
952 tc.untaggedFetch(2, 2, imapclient.FetchFlags(nil)),
956// Test that a message that is expunged by another session can be read as long as a
957// reference is held by a session. New sessions do not see the expunged message.
958func TestReference(t *testing.T) {
959 tc := start(t, false)
961 tc.login("mjl@mox.example", password0)
962 tc.client.Select("inbox")
963 tc.client.Append("inbox", makeAppend(exampleMsg))
965 tc2 := startNoSwitchboard(t, false)
966 defer tc2.closeNoWait()
967 tc2.login("mjl@mox.example", password0)
968 tc2.client.Select("inbox")
970 tc.client.MSNStoreFlagsSet("1", true, `\Deleted`)
973 tc3 := startNoSwitchboard(t, false)
974 defer tc3.closeNoWait()
975 tc3.login("mjl@mox.example", password0)
976 tc3.transactf("ok", `list "" "inbox" return (status (messages))`)
978 mustParseUntagged(`* LIST () "/" Inbox`),
979 imapclient.UntaggedStatus{Mailbox: "Inbox", Attrs: map[imapclient.StatusAttr]int64{imapclient.StatusMessages: 0}},
982 tc2.transactf("ok", "fetch 1 rfc822.size")
983 tc2.xuntagged(tc.untaggedFetch(1, 1, imapclient.FetchRFC822Size(len(exampleMsg))))