6 cryptorand "crypto/rand"
19 "golang.org/x/sys/unix"
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"
28var ctxbg = context.Background()
29var pkglog = mlog.New("imapserver", nil)
34 // Don't slow down tests.
41func tocrlf(s string) string {
42 return strings.ReplaceAll(s, "\n", "\r\n")
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>
52Content-Type: TEXT/PLAIN; CHARSET=US-ASCII
54Hello Joe, do you think we can meet at 3:30 tomorrow?
63Message - multipart/mixed
64Part 1 - no content-type
66Part 3 - multipart/parallel
67Part 3.1 - audio/basic (base64)
68Part 3.2 - image/jpeg (base64, empty)
70Part 5 - message/rfc822
71Part 5.1 - text/plain (quoted-printable)
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
81This is the preamble area of a multipart message.
82Mail readers that understand multipart format
83should ignore this preamble.
85If you are reading this text, you might want to
86consider changing to a mail reader that understands
87how to properly display multipart messages.
91 ... Some text appears here ...
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
100Content-type: text/plain; charset=US-ASCII
102This could have been part of the previous part, but
103illustrates explicit versus implicit typing of body
107Content-Type: multipart/parallel; boundary=unique-boundary-2
110Content-Type: audio/basic
111Content-Transfer-Encoding: base64
116Content-Type: image/jpeg
117Content-Transfer-Encoding: base64
123Content-type: text/enriched
125This is <bold><italic>enriched.</italic></bold>
126<smaller>as defined in RFC 1896</smaller>
129<bigger><bigger>cool?</bigger></bigger>
132Content-Type: message/rfc822
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
140 ... Additional text in ISO-8859-1 goes here ...
145func tcheck(t *testing.T, err error, msg string) {
148 t.Fatalf("%s: %s", msg, err)
152func mockUIDValidity() func() {
153 orig := store.InitialUIDValidity
154 store.InitialUIDValidity = func() uint32 {
158 store.InitialUIDValidity = orig
162type testconn struct {
165 client *imapclient.Conn
168 account *store.Account
171 // Result of last command.
172 lastUntagged []imapclient.Untagged
173 lastResult imapclient.Result
177func (tc *testconn) check(err error, msg string) {
180 tc.t.Fatalf("%s: %s", msg, err)
184func (tc *testconn) last(l []imapclient.Untagged, r imapclient.Result, err error) {
190func (tc *testconn) xcode(s string) {
192 if tc.lastResult.Code != s {
193 tc.t.Fatalf("got last code %q, expected %q", tc.lastResult.Code, s)
197func (tc *testconn) xcodeArg(v any) {
199 if !reflect.DeepEqual(tc.lastResult.CodeArg, v) {
200 tc.t.Fatalf("got last code argument %v, expected %v", tc.lastResult.CodeArg, v)
204func (tc *testconn) xuntagged(exps ...imapclient.Untagged) {
206 tc.xuntaggedOpt(true, exps...)
209func (tc *testconn) xuntaggedOpt(all bool, exps ...imapclient.Untagged) {
211 last := append([]imapclient.Untagged{}, tc.lastUntagged...)
214 for ei, exp := range exps {
215 for i, l := range last {
216 if reflect.TypeOf(l) != reflect.TypeOf(exp) {
219 if !reflect.DeepEqual(l, exp) {
223 copy(last[i:], last[i+1:])
224 last = last[:len(last)-1]
228 tc.t.Fatalf("untagged data mismatch, got:\n\t%T %#v\nexpected:\n\t%T %#v", mismatch, mismatch, exp, exp)
231 if len(tc.lastUntagged) > 0 {
232 next = fmt.Sprintf(", next %#v", tc.lastUntagged[0])
234 tc.t.Fatalf("did not find untagged response %#v %T (%d) in %v%s", exp, exp, ei, tc.lastUntagged, next)
236 if len(last) > 0 && all {
237 tc.t.Fatalf("leftover untagged responses %v", last)
241func tuntagged(t *testing.T, got imapclient.Untagged, dst any) {
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())
248 dstv.Elem().Set(gotv)
251func (tc *testconn) xnountagged() {
253 if len(tc.lastUntagged) != 0 {
254 tc.t.Fatalf("got %v untagged, expected 0", tc.lastUntagged)
258func (tc *testconn) transactf(status, format string, args ...any) {
260 tc.cmdf("", format, args...)
264func (tc *testconn) response(status string) {
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)
273func (tc *testconn) cmdf(tag, format string, args ...any) {
275 err := tc.client.Commandf(tag, format, args...)
276 tcheck(tc.t, err, "writing imap command")
279func (tc *testconn) readstatus(status string) {
284func (tc *testconn) readprefixline(pre string) {
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)
293func (tc *testconn) writelinef(format string, args ...any) {
295 err := tc.client.Writelinef(format, args...)
296 tcheck(tc.t, err, "write line")
299// wait at most 1 second for server to quit.
300func (tc *testconn) waitDone() {
302 t := time.NewTimer(time.Second)
307 tc.t.Fatalf("server not done within 1s")
311func (tc *testconn) close() {
312 if tc.account == nil {
313 // Already closed, we are not strict about closing multiple times.
316 err := tc.account.Close()
317 tc.check(err, "close account")
318 // no account.CheckClosed(), the tests open accounts multiple times.
321 tc.serverConn.Close()
323 if tc.switchStop != nil {
328func xparseNumSet(s string) imapclient.NumSet {
329 ns, err := imapclient.ParseNumSet(s)
331 panic(fmt.Sprintf("parsing numset %s: %s", s, err))
338func start(t *testing.T) *testconn {
339 return startArgs(t, true, false, true, true, "mjl")
342func startNoSwitchboard(t *testing.T) *testconn {
343 return startArgs(t, false, false, true, false, "mjl")
346const password0 = "te\u0301st \u00a0\u2002\u200a" // NFD and various unicode spaces.
347const password1 = "tést " // PRECIS normalized, with NFC.
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)
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 {
360func (c namedConn) RemoteAddr() net.Addr {
361 return &net.TCPAddr{IP: net.ParseIP("127.0.0.10"), Port: 1234}
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.
368 mox.ConfigStaticPath = filepath.FromSlash("../testdata/imap/mox.conf")
369 mox.MustLoadConfig(true, false)
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")
376 acc, err := store.OpenAccount(pkglog, accname, false)
377 tcheck(t, err, "open account")
379 err = acc.SetPassword(pkglog, password0)
380 tcheck(t, err, "set password")
382 switchStop := func() {}
384 switchStop = store.Switchboard()
387 if afterInit != nil {
389 tcheck(t, err, "after init")
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
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")
404 tcheck(t, err, "close file for conn")
407 serverConn := xfdconn(fds[0], "server")
408 clientConn := xfdconn(fds[1], "client")
410 if serverConfig == nil {
411 serverConfig = &tls.Config{
412 Certificates: []tls.Certificate{fakeCert(t, false)},
416 if clientConfig == nil {
417 clientConfig = &tls.Config{InsecureSkipVerify: true}
419 clientConn = tls.Client(clientConn, clientConfig)
422 done := make(chan struct{})
424 cid := connCounter - 1
426 const viaHTTPS = false
427 serve("test", cid, serverConfig, serverConn, immediateTLS, allowLoginWithoutTLS, viaHTTPS, "")
428 if !noCloseSwitchboard {
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
442func fakeCert(t *testing.T, randomkey bool) tls.Certificate {
443 seed := make([]byte, ed25519.SeedSize)
445 cryptorand.Read(seed)
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),
454 localCertBuf, err := x509.CreateCertificate(cryptorand.Reader, template, template, privKey.Public(), privKey)
456 t.Fatalf("making certificate: %s", err)
458 cert, err := x509.ParseCertificate(localCertBuf)
460 t.Fatalf("parsing generated certificate: %s", err)
462 c := tls.Certificate{
463 Certificate: [][]byte{localCertBuf},
470func TestLogin(t *testing.T) {
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)
486 tc.transactf("ok", `login "mjl@mox.example" "%s"`, password0)
490 tc.transactf("ok", `login "\"\"@mox.example" "%s"`, password0)
493 tc.transactf("bad", "logout badarg")
494 tc.transactf("ok", "logout")
497// Test that commands don't work in the states they are not supposed to.
498func TestState(t *testing.T) {
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"}
506 tc.transactf("ok", "capability")
507 tc.transactf("ok", "noop")
508 tc.transactf("ok", "logout")
513 // Not authenticated, lots of commands not allowed.
514 for _, cmd := range append(append([]string{}, authenticatedOrSelected...), selected...) {
515 tc.transactf("no", "%s", cmd)
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)
524 tc.transactf("bad", "boguscommand")
527func TestNonIMAP(t *testing.T) {
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")
540func TestLiterals(t *testing.T) {
544 tc.client.Login("mjl@mox.example", password0)
545 tc.client.Create("tmpbox", nil)
547 tc.transactf("ok", "rename {6+}\r\ntmpbox {7+}\r\nntmpbox")
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)
563// Test longer scenario with login, lists, subscribes, status, selects, etc.
564func TestScenario(t *testing.T) {
567 tc.transactf("ok", `login mjl@mox.example "%s"`, password0)
569 tc.transactf("bad", " missingcommand")
571 tc.transactf("ok", "examine inbox")
572 tc.transactf("ok", "unselect")
574 tc.transactf("ok", "examine inbox")
575 tc.transactf("ok", "close")
577 tc.transactf("ok", "select inbox")
578 tc.transactf("ok", "close")
580 tc.transactf("ok", "select inbox")
581 tc.transactf("ok", "expunge")
582 tc.transactf("ok", "check")
584 tc.transactf("ok", "subscribe inbox")
585 tc.transactf("ok", "unsubscribe inbox")
586 tc.transactf("ok", "subscribe inbox")
588 tc.transactf("ok", `lsub "" "*"`)
590 tc.transactf("ok", `list "" ""`)
591 tc.transactf("ok", `namespace`)
593 tc.transactf("ok", "enable utf8=accept")
594 tc.transactf("ok", "enable imap4rev2 utf8=accept")
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")
601 tc.transactf("ok", "status inbox (uidnext messages uidvalidity deleted size unseen recent)")
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")
611 tc.transactf("ok", "fetch 1 all")
612 tc.transactf("ok", "fetch 1 body")
613 tc.transactf("ok", "fetch 1 binary[]")
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")
627 tc.transactf("ok", "copy 1 Trash")
628 tc.transactf("ok", "copy 1 Trash")
629 tc.transactf("ok", "move 1 Trash")
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")
637 tc.transactf("ok", `store 1 flags (\deleted)`)
638 tc.transactf("ok", "close")
639 tc.transactf("ok", "delete Trash")
642func TestMailbox(t *testing.T) {
645 tc.client.Login("mjl@mox.example", password0)
648 "e\u0301", // é but as e + acute, not unicode-normalized
659 for _, bad := range invalid {
660 tc.transactf("no", "select {%d+}\r\n%s", len(bad), bad)
664func TestMailboxDeleted(t *testing.T) {
668 tc2 := startNoSwitchboard(t)
671 tc.client.Login("mjl@mox.example", password0)
672 tc2.client.Login("mjl@mox.example", password0)
674 tc.client.Create("testbox", nil)
675 tc2.client.Select("testbox")
676 tc.client.Delete("testbox")
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")
693 tc2.transactf("ok", "unselect")
695 tc.client.Create("testbox", nil)
696 tc2.client.Select("testbox")
697 tc.client.Delete("testbox")
698 tc2.transactf("ok", "close")
701func TestID(t *testing.T) {
704 tc.client.Login("mjl@mox.example", password0)
706 tc.transactf("ok", "id nil")
707 tc.xuntagged(imapclient.UntaggedID{"name": "mox", "version": moxvar.Version})
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})
712 tc.transactf("bad", `id ("name" "mox" "name" "mox")`) // Duplicate field.
715func TestSequence(t *testing.T) {
718 tc.client.Login("mjl@mox.example", password0)
719 tc.client.Select("inbox")
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.
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.
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)}},
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)}})
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) {
746 tc.client.Login("mjl@mox.example", password0)
747 tc.client.Select("inbox")
748 tc.client.Append("inbox", nil, nil, []byte(exampleMsg))
750 tc2 := startNoSwitchboard(t)
752 tc2.client.Login("mjl@mox.example", password0)
753 tc2.client.Select("inbox")
755 tc.client.StoreFlagsSet("1", true, `\Deleted`)
758 tc3 := startNoSwitchboard(t)
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}})
764 tc2.transactf("ok", "fetch 1 rfc822.size")
765 tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchRFC822Size(len(exampleMsg))}})