6 cryptorand "crypto/rand"
19 "github.com/mjl-/mox/imapclient"
20 "github.com/mjl-/mox/mlog"
21 "github.com/mjl-/mox/mox-"
22 "github.com/mjl-/mox/moxvar"
23 "github.com/mjl-/mox/store"
26var ctxbg = context.Background()
27var pkglog = mlog.New("imapserver", nil)
32 // Don't slow down tests.
37func tocrlf(s string) string {
38 return strings.ReplaceAll(s, "\n", "\r\n")
42var exampleMsg = tocrlf(`Date: Mon, 7 Feb 1994 21:52:25 -0800 (PST)
43From: Fred Foobar <foobar@Blurdybloop.example>
44Subject: afternoon meeting
45To: mooch@owatagu.siam.edu.example
46Message-Id: <B27397-0100000@Blurdybloop.example>
48Content-Type: TEXT/PLAIN; CHARSET=US-ASCII
50Hello Joe, do you think we can meet at 3:30 tomorrow?
59Message - multipart/mixed
60Part 1 - no content-type
62Part 3 - multipart/parallel
63Part 3.1 - audio/basic (base64)
64Part 3.2 - image/jpeg (base64, empty)
66Part 5 - message/rfc822
67Part 5.1 - text/plain (quoted-printable)
69var nestedMessage = tocrlf(`MIME-Version: 1.0
70From: Nathaniel Borenstein <nsb@nsb.fv.com>
71To: Ned Freed <ned@innosoft.com>
72Date: Fri, 07 Oct 1994 16:15:05 -0700 (PDT)
73Subject: A multipart example
74Content-Type: multipart/mixed;
75 boundary=unique-boundary-1
77This is the preamble area of a multipart message.
78Mail readers that understand multipart format
79should ignore this preamble.
81If you are reading this text, you might want to
82consider changing to a mail reader that understands
83how to properly display multipart messages.
87 ... Some text appears here ...
89[Note that the blank between the boundary and the start
90 of the text in this part means no header fields were
91 given and this is text in the US-ASCII character set.
92 It could have been done with explicit typing as in the
96Content-type: text/plain; charset=US-ASCII
98This could have been part of the previous part, but
99illustrates explicit versus implicit typing of body
103Content-Type: multipart/parallel; boundary=unique-boundary-2
106Content-Type: audio/basic
107Content-Transfer-Encoding: base64
112Content-Type: image/jpeg
113Content-Transfer-Encoding: base64
119Content-type: text/enriched
121This is <bold><italic>enriched.</italic></bold>
122<smaller>as defined in RFC 1896</smaller>
125<bigger><bigger>cool?</bigger></bigger>
128Content-Type: message/rfc822
130From: info@mox.example
131To: mox <info@mox.example>
132Subject: (subject in US-ASCII)
133Content-Type: Text/plain; charset=ISO-8859-1
134Content-Transfer-Encoding: Quoted-printable
136 ... Additional text in ISO-8859-1 goes here ...
141func tcheck(t *testing.T, err error, msg string) {
144 t.Fatalf("%s: %s", msg, err)
148func mockUIDValidity() func() {
149 orig := store.InitialUIDValidity
150 store.InitialUIDValidity = func() uint32 {
154 store.InitialUIDValidity = orig
158type testconn struct {
161 client *imapclient.Conn
164 account *store.Account
167 // Result of last command.
168 lastUntagged []imapclient.Untagged
169 lastResult imapclient.Result
173func (tc *testconn) check(err error, msg string) {
176 tc.t.Fatalf("%s: %s", msg, err)
180func (tc *testconn) last(l []imapclient.Untagged, r imapclient.Result, err error) {
186func (tc *testconn) xcode(s string) {
188 if tc.lastResult.Code != s {
189 tc.t.Fatalf("got last code %q, expected %q", tc.lastResult.Code, s)
193func (tc *testconn) xcodeArg(v any) {
195 if !reflect.DeepEqual(tc.lastResult.CodeArg, v) {
196 tc.t.Fatalf("got last code argument %v, expected %v", tc.lastResult.CodeArg, v)
200func (tc *testconn) xuntagged(exps ...imapclient.Untagged) {
202 tc.xuntaggedOpt(true, exps...)
205func (tc *testconn) xuntaggedOpt(all bool, exps ...imapclient.Untagged) {
207 last := append([]imapclient.Untagged{}, tc.lastUntagged...)
210 for ei, exp := range exps {
211 for i, l := range last {
212 if reflect.TypeOf(l) != reflect.TypeOf(exp) {
215 if !reflect.DeepEqual(l, exp) {
219 copy(last[i:], last[i+1:])
220 last = last[:len(last)-1]
224 tc.t.Fatalf("untagged data mismatch, got:\n\t%T %#v\nexpected:\n\t%T %#v", mismatch, mismatch, exp, exp)
227 if len(tc.lastUntagged) > 0 {
228 next = fmt.Sprintf(", next %#v", tc.lastUntagged[0])
230 tc.t.Fatalf("did not find untagged response %#v %T (%d) in %v%s", exp, exp, ei, tc.lastUntagged, next)
232 if len(last) > 0 && all {
233 tc.t.Fatalf("leftover untagged responses %v", last)
237func tuntagged(t *testing.T, got imapclient.Untagged, dst any) {
239 gotv := reflect.ValueOf(got)
240 dstv := reflect.ValueOf(dst)
241 if gotv.Type() != dstv.Type().Elem() {
242 t.Fatalf("got %v, expected %v", gotv.Type(), dstv.Type().Elem())
244 dstv.Elem().Set(gotv)
247func (tc *testconn) xnountagged() {
249 if len(tc.lastUntagged) != 0 {
250 tc.t.Fatalf("got %v untagged, expected 0", tc.lastUntagged)
254func (tc *testconn) transactf(status, format string, args ...any) {
256 tc.cmdf("", format, args...)
260func (tc *testconn) response(status string) {
262 tc.lastUntagged, tc.lastResult, tc.lastErr = tc.client.Response()
263 tcheck(tc.t, tc.lastErr, "read imap response")
264 if strings.ToUpper(status) != string(tc.lastResult.Status) {
265 tc.t.Fatalf("got status %q, expected %q", tc.lastResult.Status, status)
269func (tc *testconn) cmdf(tag, format string, args ...any) {
271 err := tc.client.Commandf(tag, format, args...)
272 tcheck(tc.t, err, "writing imap command")
275func (tc *testconn) readstatus(status string) {
280func (tc *testconn) readprefixline(pre string) {
282 line, err := tc.client.Readline()
283 tcheck(tc.t, err, "read line")
284 if !strings.HasPrefix(line, pre) {
285 tc.t.Fatalf("expected prefix %q, got %q", pre, line)
289func (tc *testconn) writelinef(format string, args ...any) {
291 err := tc.client.Writelinef(format, args...)
292 tcheck(tc.t, err, "write line")
295// wait at most 1 second for server to quit.
296func (tc *testconn) waitDone() {
298 t := time.NewTimer(time.Second)
303 tc.t.Fatalf("server not done within 1s")
307func (tc *testconn) close() {
308 if tc.account == nil {
309 // Already closed, we are not strict about closing multiple times.
312 err := tc.account.Close()
313 tc.check(err, "close account")
314 // no account.CheckClosed(), the tests open accounts multiple times.
317 tc.serverConn.Close()
319 if tc.switchStop != nil {
324func xparseNumSet(s string) imapclient.NumSet {
325 ns, err := imapclient.ParseNumSet(s)
327 panic(fmt.Sprintf("parsing numset %s: %s", s, err))
334func start(t *testing.T) *testconn {
335 return startArgs(t, true, false, true, true, "mjl")
338func startNoSwitchboard(t *testing.T) *testconn {
339 return startArgs(t, false, false, true, false, "mjl")
342const password0 = "te\u0301st \u00a0\u2002\u200a" // NFD and various unicode spaces.
343const password1 = "tést " // PRECIS normalized, with NFC.
345func startArgs(t *testing.T, first, immediateTLS bool, allowLoginWithoutTLS, setPassword bool, accname string) *testconn {
346 return startArgsMore(t, first, immediateTLS, nil, nil, allowLoginWithoutTLS, false, setPassword, accname, nil)
349// todo: the parameters and usage are too much now. change to scheme similar to smtpserver, with params in a struct, and a separate method for init and making a connection.
350func startArgsMore(t *testing.T, first, immediateTLS bool, serverConfig, clientConfig *tls.Config, allowLoginWithoutTLS, noCloseSwitchboard, setPassword bool, accname string, afterInit func() error) *testconn {
351 limitersInit() // Reset rate limiters.
354 mox.ConfigStaticPath = filepath.FromSlash("../testdata/imap/mox.conf")
355 mox.MustLoadConfig(true, false)
357 store.Close() // May not be open, we ignore error.
358 os.RemoveAll("../testdata/imap/data")
359 err := store.Init(ctxbg)
360 tcheck(t, err, "store init")
362 acc, err := store.OpenAccount(pkglog, accname)
363 tcheck(t, err, "open account")
365 err = acc.SetPassword(pkglog, password0)
366 tcheck(t, err, "set password")
368 switchStop := func() {}
370 switchStop = store.Switchboard()
373 if afterInit != nil {
375 tcheck(t, err, "after init")
378 serverConn, clientConn := net.Pipe()
380 if serverConfig == nil {
381 serverConfig = &tls.Config{
382 Certificates: []tls.Certificate{fakeCert(t, false)},
386 if clientConfig == nil {
387 clientConfig = &tls.Config{InsecureSkipVerify: true}
389 clientConn = tls.Client(clientConn, clientConfig)
392 done := make(chan struct{})
396 serve("test", cid, serverConfig, serverConn, immediateTLS, allowLoginWithoutTLS)
397 if !noCloseSwitchboard {
402 client, err := imapclient.New(clientConn, true)
403 tcheck(t, err, "new client")
404 tc := &testconn{t: t, conn: clientConn, client: client, done: done, serverConn: serverConn, account: acc}
405 if first && noCloseSwitchboard {
406 tc.switchStop = switchStop
411func fakeCert(t *testing.T, randomkey bool) tls.Certificate {
412 seed := make([]byte, ed25519.SeedSize)
414 cryptorand.Read(seed)
416 privKey := ed25519.NewKeyFromSeed(seed) // Fake key, don't use this for real!
417 template := &x509.Certificate{
418 SerialNumber: big.NewInt(1), // Required field...
419 // Valid period is needed to get session resumption enabled.
420 NotBefore: time.Now().Add(-time.Minute),
421 NotAfter: time.Now().Add(time.Hour),
423 localCertBuf, err := x509.CreateCertificate(cryptorand.Reader, template, template, privKey.Public(), privKey)
425 t.Fatalf("making certificate: %s", err)
427 cert, err := x509.ParseCertificate(localCertBuf)
429 t.Fatalf("parsing generated certificate: %s", err)
431 c := tls.Certificate{
432 Certificate: [][]byte{localCertBuf},
439func TestLogin(t *testing.T) {
443 tc.transactf("bad", "login too many args")
444 tc.transactf("bad", "login") // no args
445 tc.transactf("no", "login mjl@mox.example badpass")
446 tc.transactf("no", `login mjl "%s"`, password0) // must use email, not account
447 tc.transactf("no", "login mjl@mox.example test")
448 tc.transactf("no", "login mjl@mox.example testtesttest")
449 tc.transactf("no", `login "mjl@mox.example" "testtesttest"`)
450 tc.transactf("no", "login \"m\xf8x@mox.example\" \"testtesttest\"")
451 tc.transactf("ok", `login mjl@mox.example "%s"`, password0)
455 tc.transactf("ok", `login "mjl@mox.example" "%s"`, password0)
459 tc.transactf("ok", `login "\"\"@mox.example" "%s"`, password0)
462 tc.transactf("bad", "logout badarg")
463 tc.transactf("ok", "logout")
466// Test that commands don't work in the states they are not supposed to.
467func TestState(t *testing.T) {
470 notAuthenticated := []string{"starttls", "authenticate", "login"}
471 authenticatedOrSelected := []string{"enable", "select", "examine", "create", "delete", "rename", "subscribe", "unsubscribe", "list", "namespace", "status", "append", "idle", "lsub"}
472 selected := []string{"close", "unselect", "expunge", "search", "fetch", "store", "copy", "move", "uid expunge"}
475 tc.transactf("ok", "capability")
476 tc.transactf("ok", "noop")
477 tc.transactf("ok", "logout")
482 // Not authenticated, lots of commands not allowed.
483 for _, cmd := range append(append([]string{}, authenticatedOrSelected...), selected...) {
484 tc.transactf("no", "%s", cmd)
487 // Some commands not allowed when authenticated.
488 tc.transactf("ok", `login mjl@mox.example "%s"`, password0)
489 for _, cmd := range append(append([]string{}, notAuthenticated...), selected...) {
490 tc.transactf("no", "%s", cmd)
493 tc.transactf("bad", "boguscommand")
496func TestNonIMAP(t *testing.T) {
500 // imap greeting has already been read, we sidestep the imapclient.
501 _, err := fmt.Fprintf(tc.conn, "bogus\r\n")
502 tc.check(err, "write bogus command")
503 tc.readprefixline("* BYE ")
504 if _, err := tc.conn.Read(make([]byte, 1)); err == nil {
505 t.Fatalf("connection not closed after initial bad command")
509func TestLiterals(t *testing.T) {
513 tc.client.Login("mjl@mox.example", password0)
514 tc.client.Create("tmpbox")
516 tc.transactf("ok", "rename {6+}\r\ntmpbox {7+}\r\nntmpbox")
520 fmt.Fprint(tc.client, "xtag rename ")
521 tc.client.WriteSyncLiteral(from)
522 fmt.Fprint(tc.client, " ")
523 tc.client.WriteSyncLiteral(to)
524 fmt.Fprint(tc.client, "\r\n")
525 tc.client.LastTag = "xtag"
526 tc.last(tc.client.Response())
527 if tc.lastResult.Status != "OK" {
528 tc.t.Fatalf(`got %q, expected "OK"`, tc.lastResult.Status)
532// Test longer scenario with login, lists, subscribes, status, selects, etc.
533func TestScenario(t *testing.T) {
536 tc.transactf("ok", `login mjl@mox.example "%s"`, password0)
538 tc.transactf("bad", " missingcommand")
540 tc.transactf("ok", "examine inbox")
541 tc.transactf("ok", "unselect")
543 tc.transactf("ok", "examine inbox")
544 tc.transactf("ok", "close")
546 tc.transactf("ok", "select inbox")
547 tc.transactf("ok", "close")
549 tc.transactf("ok", "select inbox")
550 tc.transactf("ok", "expunge")
551 tc.transactf("ok", "check")
553 tc.transactf("ok", "subscribe inbox")
554 tc.transactf("ok", "unsubscribe inbox")
555 tc.transactf("ok", "subscribe inbox")
557 tc.transactf("ok", `lsub "" "*"`)
559 tc.transactf("ok", `list "" ""`)
560 tc.transactf("ok", `namespace`)
562 tc.transactf("ok", "enable utf8=accept")
563 tc.transactf("ok", "enable imap4rev2 utf8=accept")
565 tc.transactf("no", "create inbox")
566 tc.transactf("ok", "create tmpbox")
567 tc.transactf("ok", "rename tmpbox ntmpbox")
568 tc.transactf("ok", "delete ntmpbox")
570 tc.transactf("ok", "status inbox (uidnext messages uidvalidity deleted size unseen recent)")
572 tc.transactf("ok", "append inbox (\\seen) {%d+}\r\n%s", len(exampleMsg), exampleMsg)
573 tc.transactf("no", "append bogus () {%d}", len(exampleMsg))
574 tc.cmdf("", "append inbox () {%d}", len(exampleMsg))
575 tc.readprefixline("+ ")
576 _, err := tc.conn.Write([]byte(exampleMsg + "\r\n"))
577 tc.check(err, "write message")
580 tc.transactf("ok", "fetch 1 all")
581 tc.transactf("ok", "fetch 1 body")
582 tc.transactf("ok", "fetch 1 binary[]")
584 tc.transactf("ok", `store 1 flags (\seen \answered)`)
585 tc.transactf("ok", `store 1 +flags ($junk)`) // should train as junk.
586 tc.transactf("ok", `store 1 -flags ($junk)`) // should retrain as non-junk.
587 tc.transactf("ok", `store 1 -flags (\seen)`) // should untrain completely.
588 tc.transactf("ok", `store 1 -flags (\answered)`)
589 tc.transactf("ok", `store 1 +flags (\answered)`)
590 tc.transactf("ok", `store 1 flags.silent (\seen \answered)`)
591 tc.transactf("ok", `store 1 -flags.silent (\answered)`)
592 tc.transactf("ok", `store 1 +flags.silent (\answered)`)
593 tc.transactf("bad", `store 1 flags (\badflag)`)
594 tc.transactf("ok", "noop")
596 tc.transactf("ok", "copy 1 Trash")
597 tc.transactf("ok", "copy 1 Trash")
598 tc.transactf("ok", "move 1 Trash")
600 tc.transactf("ok", "close")
601 tc.transactf("ok", "select Trash")
602 tc.transactf("ok", `store 1 flags (\deleted)`)
603 tc.transactf("ok", "expunge")
604 tc.transactf("ok", "noop")
606 tc.transactf("ok", `store 1 flags (\deleted)`)
607 tc.transactf("ok", "close")
608 tc.transactf("ok", "delete Trash")
611func TestMailbox(t *testing.T) {
614 tc.client.Login("mjl@mox.example", password0)
617 "e\u0301", // é but as e + acute, not unicode-normalized
628 for _, bad := range invalid {
629 tc.transactf("no", "select {%d+}\r\n%s", len(bad), bad)
633func TestMailboxDeleted(t *testing.T) {
637 tc2 := startNoSwitchboard(t)
640 tc.client.Login("mjl@mox.example", password0)
641 tc2.client.Login("mjl@mox.example", password0)
643 tc.client.Create("testbox")
644 tc2.client.Select("testbox")
645 tc.client.Delete("testbox")
647 // Now try to operate on testbox while it has been removed.
648 tc2.transactf("no", "check")
649 tc2.transactf("no", "expunge")
650 tc2.transactf("no", "uid expunge 1")
651 tc2.transactf("no", "search all")
652 tc2.transactf("no", "uid search all")
653 tc2.transactf("no", "fetch 1:* all")
654 tc2.transactf("no", "uid fetch 1 all")
655 tc2.transactf("no", "store 1 flags ()")
656 tc2.transactf("no", "uid store 1 flags ()")
657 tc2.transactf("bad", "copy 1 inbox") // msgseq 1 not available.
658 tc2.transactf("no", "uid copy 1 inbox")
659 tc2.transactf("bad", "move 1 inbox") // msgseq 1 not available.
660 tc2.transactf("no", "uid move 1 inbox")
662 tc2.transactf("ok", "unselect")
664 tc.client.Create("testbox")
665 tc2.client.Select("testbox")
666 tc.client.Delete("testbox")
667 tc2.transactf("ok", "close")
670func TestID(t *testing.T) {
673 tc.client.Login("mjl@mox.example", password0)
675 tc.transactf("ok", "id nil")
676 tc.xuntagged(imapclient.UntaggedID{"name": "mox", "version": moxvar.Version})
678 tc.transactf("ok", `id ("name" "mox" "version" "1.2.3" "other" "test" "test" nil)`)
679 tc.xuntagged(imapclient.UntaggedID{"name": "mox", "version": moxvar.Version})
681 tc.transactf("bad", `id ("name" "mox" "name" "mox")`) // Duplicate field.
684func TestSequence(t *testing.T) {
687 tc.client.Login("mjl@mox.example", password0)
688 tc.client.Select("inbox")
693 tc.transactf("ok", "uid fetch 1 all") // non-existing messages are OK for uids.
694 tc.transactf("ok", "uid fetch * all") // * is like uidnext, a non-existing message.
696 tc.client.Append("inbox", nil, nil, []byte(exampleMsg))
697 tc.client.Append("inbox", nil, nil, []byte(exampleMsg))
698 tc.transactf("ok", "fetch 2:1,1 uid") // We reorder 2:1 to 1:2, but we don't deduplicate numbers.
700 imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1)}},
701 imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(2)}},
702 imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1)}},
705 tc.transactf("ok", "uid fetch 3:* uid") // Because * is the last message, which is 2, the range becomes 3:2, which matches the last message.
706 tc.xuntagged(imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(2)}})
709// Test that a message that is expunged by another session can be read as long as a
710// reference is held by a session. New sessions do not see the expunged message.
711// todo: possibly implement the additional reference counting. so far it hasn't been worth the trouble.
712func DisabledTestReference(t *testing.T) {
715 tc.client.Login("mjl@mox.example", password0)
716 tc.client.Select("inbox")
717 tc.client.Append("inbox", nil, nil, []byte(exampleMsg))
719 tc2 := startNoSwitchboard(t)
721 tc2.client.Login("mjl@mox.example", password0)
722 tc2.client.Select("inbox")
724 tc.client.StoreFlagsSet("1", true, `\Deleted`)
727 tc3 := startNoSwitchboard(t)
729 tc3.client.Login("mjl@mox.example", password0)
730 tc3.transactf("ok", `list "" "inbox" return (status (messages))`)
731 tc3.xuntagged(imapclient.UntaggedList{Separator: '/', Mailbox: "Inbox"}, imapclient.UntaggedStatus{Mailbox: "Inbox", Attrs: map[imapclient.StatusAttr]int64{imapclient.StatusMessages: 0}})
733 tc2.transactf("ok", "fetch 1 rfc822.size")
734 tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchRFC822Size(len(exampleMsg))}})