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
166 // Result of last command.
167 lastUntagged []imapclient.Untagged
168 lastResult imapclient.Result
172func (tc *testconn) check(err error, msg string) {
175 tc.t.Fatalf("%s: %s", msg, err)
179func (tc *testconn) last(l []imapclient.Untagged, r imapclient.Result, err error) {
185func (tc *testconn) xcode(s string) {
187 if tc.lastResult.Code != s {
188 tc.t.Fatalf("got last code %q, expected %q", tc.lastResult.Code, s)
192func (tc *testconn) xcodeArg(v any) {
194 if !reflect.DeepEqual(tc.lastResult.CodeArg, v) {
195 tc.t.Fatalf("got last code argument %v, expected %v", tc.lastResult.CodeArg, v)
199func (tc *testconn) xuntagged(exps ...imapclient.Untagged) {
201 tc.xuntaggedOpt(true, exps...)
204func (tc *testconn) xuntaggedOpt(all bool, exps ...imapclient.Untagged) {
206 last := append([]imapclient.Untagged{}, tc.lastUntagged...)
209 for ei, exp := range exps {
210 for i, l := range last {
211 if reflect.TypeOf(l) != reflect.TypeOf(exp) {
214 if !reflect.DeepEqual(l, exp) {
218 copy(last[i:], last[i+1:])
219 last = last[:len(last)-1]
223 tc.t.Fatalf("untagged data mismatch, got:\n\t%T %#v\nexpected:\n\t%T %#v", mismatch, mismatch, exp, exp)
226 if len(tc.lastUntagged) > 0 {
227 next = fmt.Sprintf(", next %#v", tc.lastUntagged[0])
229 tc.t.Fatalf("did not find untagged response %#v %T (%d) in %v%s", exp, exp, ei, tc.lastUntagged, next)
231 if len(last) > 0 && all {
232 tc.t.Fatalf("leftover untagged responses %v", last)
236func tuntagged(t *testing.T, got imapclient.Untagged, dst any) {
238 gotv := reflect.ValueOf(got)
239 dstv := reflect.ValueOf(dst)
240 if gotv.Type() != dstv.Type().Elem() {
241 t.Fatalf("got %v, expected %v", gotv.Type(), dstv.Type().Elem())
243 dstv.Elem().Set(gotv)
246func (tc *testconn) xnountagged() {
248 if len(tc.lastUntagged) != 0 {
249 tc.t.Fatalf("got %v untagged, expected 0", tc.lastUntagged)
253func (tc *testconn) transactf(status, format string, args ...any) {
255 tc.cmdf("", format, args...)
259func (tc *testconn) response(status string) {
261 tc.lastUntagged, tc.lastResult, tc.lastErr = tc.client.Response()
262 tcheck(tc.t, tc.lastErr, "read imap response")
263 if strings.ToUpper(status) != string(tc.lastResult.Status) {
264 tc.t.Fatalf("got status %q, expected %q", tc.lastResult.Status, status)
268func (tc *testconn) cmdf(tag, format string, args ...any) {
270 err := tc.client.Commandf(tag, format, args...)
271 tcheck(tc.t, err, "writing imap command")
274func (tc *testconn) readstatus(status string) {
279func (tc *testconn) readprefixline(pre string) {
281 line, err := tc.client.Readline()
282 tcheck(tc.t, err, "read line")
283 if !strings.HasPrefix(line, pre) {
284 tc.t.Fatalf("expected prefix %q, got %q", pre, line)
288func (tc *testconn) writelinef(format string, args ...any) {
290 err := tc.client.Writelinef(format, args...)
291 tcheck(tc.t, err, "write line")
294// wait at most 1 second for server to quit.
295func (tc *testconn) waitDone() {
297 t := time.NewTimer(time.Second)
302 tc.t.Fatalf("server not done within 1s")
306func (tc *testconn) close() {
307 if tc.account == nil {
308 // Already closed, we are not strict about closing multiple times.
311 err := tc.account.Close()
312 tc.check(err, "close account")
315 tc.serverConn.Close()
319func xparseNumSet(s string) imapclient.NumSet {
320 ns, err := imapclient.ParseNumSet(s)
322 panic(fmt.Sprintf("parsing numset %s: %s", s, err))
329func start(t *testing.T) *testconn {
330 return startArgs(t, true, false, true, true, "mjl")
333func startNoSwitchboard(t *testing.T) *testconn {
334 return startArgs(t, false, false, true, false, "mjl")
337func startArgs(t *testing.T, first, isTLS, allowLoginWithoutTLS, setPassword bool, accname string) *testconn {
338 limitersInit() // Reset rate limiters.
341 os.RemoveAll("../testdata/imap/data")
344 mox.ConfigStaticPath = filepath.FromSlash("../testdata/imap/mox.conf")
345 mox.MustLoadConfig(true, false)
346 acc, err := store.OpenAccount(pkglog, accname)
347 tcheck(t, err, "open account")
349 err = acc.SetPassword(pkglog, "testtest")
350 tcheck(t, err, "set password")
352 switchStop := func() {}
354 switchStop = store.Switchboard()
357 serverConn, clientConn := net.Pipe()
359 tlsConfig := &tls.Config{
360 Certificates: []tls.Certificate{fakeCert(t)},
363 serverConn = tls.Server(serverConn, tlsConfig)
364 clientConn = tls.Client(clientConn, &tls.Config{InsecureSkipVerify: true})
367 done := make(chan struct{})
371 serve("test", cid, tlsConfig, serverConn, isTLS, allowLoginWithoutTLS)
375 client, err := imapclient.New(clientConn, true)
376 tcheck(t, err, "new client")
377 return &testconn{t: t, conn: clientConn, client: client, done: done, serverConn: serverConn, account: acc}
380func fakeCert(t *testing.T) tls.Certificate {
381 privKey := ed25519.NewKeyFromSeed(make([]byte, ed25519.SeedSize)) // Fake key, don't use this for real!
382 template := &x509.Certificate{
383 SerialNumber: big.NewInt(1), // Required field...
385 localCertBuf, err := x509.CreateCertificate(cryptorand.Reader, template, template, privKey.Public(), privKey)
387 t.Fatalf("making certificate: %s", err)
389 cert, err := x509.ParseCertificate(localCertBuf)
391 t.Fatalf("parsing generated certificate: %s", err)
393 c := tls.Certificate{
394 Certificate: [][]byte{localCertBuf},
401func TestLogin(t *testing.T) {
405 tc.transactf("bad", "login too many args")
406 tc.transactf("bad", "login") // no args
407 tc.transactf("no", "login mjl@mox.example badpass")
408 tc.transactf("no", "login mjl testtest") // must use email, not account
409 tc.transactf("no", "login mjl@mox.example test")
410 tc.transactf("no", "login mjl@mox.example testtesttest")
411 tc.transactf("no", `login "mjl@mox.example" "testtesttest"`)
412 tc.transactf("no", "login \"m\xf8x@mox.example\" \"testtesttest\"")
413 tc.transactf("ok", "login mjl@mox.example testtest")
417 tc.transactf("ok", `login "mjl@mox.example" "testtest"`)
421 tc.transactf("ok", `login "\"\"@mox.example" "testtest"`)
424 tc.transactf("bad", "logout badarg")
425 tc.transactf("ok", "logout")
428// Test that commands don't work in the states they are not supposed to.
429func TestState(t *testing.T) {
432 notAuthenticated := []string{"starttls", "authenticate", "login"}
433 authenticatedOrSelected := []string{"enable", "select", "examine", "create", "delete", "rename", "subscribe", "unsubscribe", "list", "namespace", "status", "append", "idle", "lsub"}
434 selected := []string{"close", "unselect", "expunge", "search", "fetch", "store", "copy", "move", "uid expunge"}
437 tc.transactf("ok", "capability")
438 tc.transactf("ok", "noop")
439 tc.transactf("ok", "logout")
444 // Not authenticated, lots of commands not allowed.
445 for _, cmd := range append(append([]string{}, authenticatedOrSelected...), selected...) {
446 tc.transactf("no", "%s", cmd)
449 // Some commands not allowed when authenticated.
450 tc.transactf("ok", "login mjl@mox.example testtest")
451 for _, cmd := range append(append([]string{}, notAuthenticated...), selected...) {
452 tc.transactf("no", "%s", cmd)
455 tc.transactf("bad", "boguscommand")
458func TestNonIMAP(t *testing.T) {
462 // imap greeting has already been read, we sidestep the imapclient.
463 _, err := fmt.Fprintf(tc.conn, "bogus\r\n")
464 tc.check(err, "write bogus command")
465 tc.readprefixline("* BYE ")
466 if _, err := tc.conn.Read(make([]byte, 1)); err == nil {
467 t.Fatalf("connection not closed after initial bad command")
471func TestLiterals(t *testing.T) {
475 tc.client.Login("mjl@mox.example", "testtest")
476 tc.client.Create("tmpbox")
478 tc.transactf("ok", "rename {6+}\r\ntmpbox {7+}\r\nntmpbox")
482 fmt.Fprint(tc.client, "xtag rename ")
483 tc.client.WriteSyncLiteral(from)
484 fmt.Fprint(tc.client, " ")
485 tc.client.WriteSyncLiteral(to)
486 fmt.Fprint(tc.client, "\r\n")
487 tc.client.LastTag = "xtag"
488 tc.last(tc.client.Response())
489 if tc.lastResult.Status != "OK" {
490 tc.t.Fatalf(`got %q, expected "OK"`, tc.lastResult.Status)
494// Test longer scenario with login, lists, subscribes, status, selects, etc.
495func TestScenario(t *testing.T) {
498 tc.transactf("ok", "login mjl@mox.example testtest")
500 tc.transactf("bad", " missingcommand")
502 tc.transactf("ok", "examine inbox")
503 tc.transactf("ok", "unselect")
505 tc.transactf("ok", "examine inbox")
506 tc.transactf("ok", "close")
508 tc.transactf("ok", "select inbox")
509 tc.transactf("ok", "close")
511 tc.transactf("ok", "select inbox")
512 tc.transactf("ok", "expunge")
513 tc.transactf("ok", "check")
515 tc.transactf("ok", "subscribe inbox")
516 tc.transactf("ok", "unsubscribe inbox")
517 tc.transactf("ok", "subscribe inbox")
519 tc.transactf("ok", `lsub "" "*"`)
521 tc.transactf("ok", `list "" ""`)
522 tc.transactf("ok", `namespace`)
524 tc.transactf("ok", "enable utf8=accept")
525 tc.transactf("ok", "enable imap4rev2 utf8=accept")
527 tc.transactf("no", "create inbox")
528 tc.transactf("ok", "create tmpbox")
529 tc.transactf("ok", "rename tmpbox ntmpbox")
530 tc.transactf("ok", "delete ntmpbox")
532 tc.transactf("ok", "status inbox (uidnext messages uidvalidity deleted size unseen recent)")
534 tc.transactf("ok", "append inbox (\\seen) {%d+}\r\n%s", len(exampleMsg), exampleMsg)
535 tc.transactf("no", "append bogus () {%d}", len(exampleMsg))
536 tc.cmdf("", "append inbox () {%d}", len(exampleMsg))
537 tc.readprefixline("+ ")
538 _, err := tc.conn.Write([]byte(exampleMsg + "\r\n"))
539 tc.check(err, "write message")
542 tc.transactf("ok", "fetch 1 all")
543 tc.transactf("ok", "fetch 1 body")
544 tc.transactf("ok", "fetch 1 binary[]")
546 tc.transactf("ok", `store 1 flags (\seen \answered)`)
547 tc.transactf("ok", `store 1 +flags ($junk)`) // should train as junk.
548 tc.transactf("ok", `store 1 -flags ($junk)`) // should retrain as non-junk.
549 tc.transactf("ok", `store 1 -flags (\seen)`) // should untrain completely.
550 tc.transactf("ok", `store 1 -flags (\answered)`)
551 tc.transactf("ok", `store 1 +flags (\answered)`)
552 tc.transactf("ok", `store 1 flags.silent (\seen \answered)`)
553 tc.transactf("ok", `store 1 -flags.silent (\answered)`)
554 tc.transactf("ok", `store 1 +flags.silent (\answered)`)
555 tc.transactf("bad", `store 1 flags (\badflag)`)
556 tc.transactf("ok", "noop")
558 tc.transactf("ok", "copy 1 Trash")
559 tc.transactf("ok", "copy 1 Trash")
560 tc.transactf("ok", "move 1 Trash")
562 tc.transactf("ok", "close")
563 tc.transactf("ok", "select Trash")
564 tc.transactf("ok", `store 1 flags (\deleted)`)
565 tc.transactf("ok", "expunge")
566 tc.transactf("ok", "noop")
568 tc.transactf("ok", `store 1 flags (\deleted)`)
569 tc.transactf("ok", "close")
570 tc.transactf("ok", "delete Trash")
573func TestMailbox(t *testing.T) {
576 tc.client.Login("mjl@mox.example", "testtest")
579 "e\u0301", // é but as e + acute, not unicode-normalized
590 for _, bad := range invalid {
591 tc.transactf("no", "select {%d+}\r\n%s", len(bad), bad)
595func TestMailboxDeleted(t *testing.T) {
598 tc.client.Login("mjl@mox.example", "testtest")
600 tc2 := startNoSwitchboard(t)
602 tc2.client.Login("mjl@mox.example", "testtest")
604 tc.client.Create("testbox")
605 tc2.client.Select("testbox")
606 tc.client.Delete("testbox")
608 // Now try to operate on testbox while it has been removed.
609 tc2.transactf("no", "check")
610 tc2.transactf("no", "expunge")
611 tc2.transactf("no", "uid expunge 1")
612 tc2.transactf("no", "search all")
613 tc2.transactf("no", "uid search all")
614 tc2.transactf("no", "fetch 1:* all")
615 tc2.transactf("no", "uid fetch 1 all")
616 tc2.transactf("no", "store 1 flags ()")
617 tc2.transactf("no", "uid store 1 flags ()")
618 tc2.transactf("bad", "copy 1 inbox") // msgseq 1 not available.
619 tc2.transactf("no", "uid copy 1 inbox")
620 tc2.transactf("bad", "move 1 inbox") // msgseq 1 not available.
621 tc2.transactf("no", "uid move 1 inbox")
623 tc2.transactf("ok", "unselect")
625 tc.client.Create("testbox")
626 tc2.client.Select("testbox")
627 tc.client.Delete("testbox")
628 tc2.transactf("ok", "close")
631func TestID(t *testing.T) {
634 tc.client.Login("mjl@mox.example", "testtest")
636 tc.transactf("ok", "id nil")
637 tc.xuntagged(imapclient.UntaggedID{"name": "mox", "version": moxvar.Version})
639 tc.transactf("ok", `id ("name" "mox" "version" "1.2.3" "other" "test" "test" nil)`)
640 tc.xuntagged(imapclient.UntaggedID{"name": "mox", "version": moxvar.Version})
642 tc.transactf("bad", `id ("name" "mox" "name" "mox")`) // Duplicate field.
645func TestSequence(t *testing.T) {
648 tc.client.Login("mjl@mox.example", "testtest")
649 tc.client.Select("inbox")
654 tc.transactf("ok", "uid fetch 1 all") // non-existing messages are OK for uids.
655 tc.transactf("ok", "uid fetch * all") // * is like uidnext, a non-existing message.
657 tc.client.Append("inbox", nil, nil, []byte(exampleMsg))
658 tc.client.Append("inbox", nil, nil, []byte(exampleMsg))
659 tc.transactf("ok", "fetch 2:1,1 uid") // We reorder 2:1 to 1:2, but we don't deduplicate numbers.
661 imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1)}},
662 imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(2)}},
663 imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1)}},
666 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.
667 tc.xuntagged(imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(2)}})
670// Test that a message that is expunged by another session can be read as long as a
671// reference is held by a session. New sessions do not see the expunged message.
672// todo: possibly implement the additional reference counting. so far it hasn't been worth the trouble.
673func DisabledTestReference(t *testing.T) {
676 tc.client.Login("mjl@mox.example", "testtest")
677 tc.client.Select("inbox")
678 tc.client.Append("inbox", nil, nil, []byte(exampleMsg))
680 tc2 := startNoSwitchboard(t)
682 tc2.client.Login("mjl@mox.example", "testtest")
683 tc2.client.Select("inbox")
685 tc.client.StoreFlagsSet("1", true, `\Deleted`)
688 tc3 := startNoSwitchboard(t)
690 tc3.client.Login("mjl@mox.example", "testtest")
691 tc3.transactf("ok", `list "" "inbox" return (status (messages))`)
692 tc3.xuntagged(imapclient.UntaggedList{Separator: '/', Mailbox: "Inbox"}, imapclient.UntaggedStatus{Mailbox: "Inbox", Attrs: map[string]int64{"MESSAGES": 0}})
694 tc2.transactf("ok", "fetch 1 rfc822.size")
695 tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchRFC822Size(len(exampleMsg))}})