6 cryptorand "crypto/rand"
19 "github.com/mjl-/mox/imapclient"
20 "github.com/mjl-/mox/mox-"
21 "github.com/mjl-/mox/moxvar"
22 "github.com/mjl-/mox/store"
25var ctxbg = context.Background()
30 // Don't slow down tests.
35func tocrlf(s string) string {
36 return strings.ReplaceAll(s, "\n", "\r\n")
40var exampleMsg = tocrlf(`Date: Mon, 7 Feb 1994 21:52:25 -0800 (PST)
41From: Fred Foobar <foobar@Blurdybloop.example>
42Subject: afternoon meeting
43To: mooch@owatagu.siam.edu.example
44Message-Id: <B27397-0100000@Blurdybloop.example>
46Content-Type: TEXT/PLAIN; CHARSET=US-ASCII
48Hello Joe, do you think we can meet at 3:30 tomorrow?
57Message - multipart/mixed
58Part 1 - no content-type
60Part 3 - multipart/parallel
61Part 3.1 - audio/basic (base64)
62Part 3.2 - image/jpeg (base64, empty)
64Part 5 - message/rfc822
65Part 5.1 - text/plain (quoted-printable)
67var nestedMessage = tocrlf(`MIME-Version: 1.0
68From: Nathaniel Borenstein <nsb@nsb.fv.com>
69To: Ned Freed <ned@innosoft.com>
70Date: Fri, 07 Oct 1994 16:15:05 -0700 (PDT)
71Subject: A multipart example
72Content-Type: multipart/mixed;
73 boundary=unique-boundary-1
75This is the preamble area of a multipart message.
76Mail readers that understand multipart format
77should ignore this preamble.
79If you are reading this text, you might want to
80consider changing to a mail reader that understands
81how to properly display multipart messages.
85 ... Some text appears here ...
87[Note that the blank between the boundary and the start
88 of the text in this part means no header fields were
89 given and this is text in the US-ASCII character set.
90 It could have been done with explicit typing as in the
94Content-type: text/plain; charset=US-ASCII
96This could have been part of the previous part, but
97illustrates explicit versus implicit typing of body
101Content-Type: multipart/parallel; boundary=unique-boundary-2
104Content-Type: audio/basic
105Content-Transfer-Encoding: base64
110Content-Type: image/jpeg
111Content-Transfer-Encoding: base64
117Content-type: text/enriched
119This is <bold><italic>enriched.</italic></bold>
120<smaller>as defined in RFC 1896</smaller>
123<bigger><bigger>cool?</bigger></bigger>
126Content-Type: message/rfc822
128From: info@mox.example
129To: mox <info@mox.example>
130Subject: (subject in US-ASCII)
131Content-Type: Text/plain; charset=ISO-8859-1
132Content-Transfer-Encoding: Quoted-printable
134 ... Additional text in ISO-8859-1 goes here ...
139func tcheck(t *testing.T, err error, msg string) {
142 t.Fatalf("%s: %s", msg, err)
146func mockUIDValidity() func() {
147 orig := store.InitialUIDValidity
148 store.InitialUIDValidity = func() uint32 {
152 store.InitialUIDValidity = orig
156type testconn struct {
159 client *imapclient.Conn
162 account *store.Account
164 // Result of last command.
165 lastUntagged []imapclient.Untagged
166 lastResult imapclient.Result
170func (tc *testconn) check(err error, msg string) {
173 tc.t.Fatalf("%s: %s", msg, err)
177func (tc *testconn) last(l []imapclient.Untagged, r imapclient.Result, err error) {
183func (tc *testconn) xcode(s string) {
185 if tc.lastResult.Code != s {
186 tc.t.Fatalf("got last code %q, expected %q", tc.lastResult.Code, s)
190func (tc *testconn) xcodeArg(v any) {
192 if !reflect.DeepEqual(tc.lastResult.CodeArg, v) {
193 tc.t.Fatalf("got last code argument %v, expected %v", tc.lastResult.CodeArg, v)
197func (tc *testconn) xuntagged(exps ...imapclient.Untagged) {
199 tc.xuntaggedOpt(true, exps...)
202func (tc *testconn) xuntaggedOpt(all bool, exps ...imapclient.Untagged) {
204 last := append([]imapclient.Untagged{}, tc.lastUntagged...)
207 for ei, exp := range exps {
208 for i, l := range last {
209 if reflect.TypeOf(l) != reflect.TypeOf(exp) {
212 if !reflect.DeepEqual(l, exp) {
216 copy(last[i:], last[i+1:])
217 last = last[:len(last)-1]
221 tc.t.Fatalf("untagged data mismatch, got:\n\t%T %#v\nexpected:\n\t%T %#v", mismatch, mismatch, exp, exp)
224 if len(tc.lastUntagged) > 0 {
225 next = fmt.Sprintf(", next %#v", tc.lastUntagged[0])
227 tc.t.Fatalf("did not find untagged response %#v %T (%d) in %v%s", exp, exp, ei, tc.lastUntagged, next)
229 if len(last) > 0 && all {
230 tc.t.Fatalf("leftover untagged responses %v", last)
234func tuntagged(t *testing.T, got imapclient.Untagged, dst any) {
236 gotv := reflect.ValueOf(got)
237 dstv := reflect.ValueOf(dst)
238 if gotv.Type() != dstv.Type().Elem() {
239 t.Fatalf("got %v, expected %v", gotv.Type(), dstv.Type().Elem())
241 dstv.Elem().Set(gotv)
244func (tc *testconn) xnountagged() {
246 if len(tc.lastUntagged) != 0 {
247 tc.t.Fatalf("got %v untagged, expected 0", tc.lastUntagged)
251func (tc *testconn) transactf(status, format string, args ...any) {
253 tc.cmdf("", format, args...)
257func (tc *testconn) response(status string) {
259 tc.lastUntagged, tc.lastResult, tc.lastErr = tc.client.Response()
260 tcheck(tc.t, tc.lastErr, "read imap response")
261 if strings.ToUpper(status) != string(tc.lastResult.Status) {
262 tc.t.Fatalf("got status %q, expected %q", tc.lastResult.Status, status)
266func (tc *testconn) cmdf(tag, format string, args ...any) {
268 err := tc.client.Commandf(tag, format, args...)
269 tcheck(tc.t, err, "writing imap command")
272func (tc *testconn) readstatus(status string) {
277func (tc *testconn) readprefixline(pre string) {
279 line, err := tc.client.Readline()
280 tcheck(tc.t, err, "read line")
281 if !strings.HasPrefix(line, pre) {
282 tc.t.Fatalf("expected prefix %q, got %q", pre, line)
286func (tc *testconn) writelinef(format string, args ...any) {
288 err := tc.client.Writelinef(format, args...)
289 tcheck(tc.t, err, "write line")
292// wait at most 1 second for server to quit.
293func (tc *testconn) waitDone() {
295 t := time.NewTimer(time.Second)
300 tc.t.Fatalf("server not done within 1s")
304func (tc *testconn) close() {
305 if tc.account == nil {
306 // Already closed, we are not strict about closing multiple times.
309 err := tc.account.Close()
310 tc.check(err, "close account")
313 tc.serverConn.Close()
317func xparseNumSet(s string) imapclient.NumSet {
318 ns, err := imapclient.ParseNumSet(s)
320 panic(fmt.Sprintf("parsing numset %s: %s", s, err))
327func start(t *testing.T) *testconn {
328 return startArgs(t, true, false, true)
331func startNoSwitchboard(t *testing.T) *testconn {
332 return startArgs(t, false, false, true)
335func startArgs(t *testing.T, first, isTLS, allowLoginWithoutTLS bool) *testconn {
336 limitersInit() // Reset rate limiters.
339 os.RemoveAll("../testdata/imap/data")
342 mox.ConfigStaticPath = filepath.FromSlash("../testdata/imap/mox.conf")
343 mox.MustLoadConfig(true, false)
344 acc, err := store.OpenAccount("mjl")
345 tcheck(t, err, "open account")
347 err = acc.SetPassword("testtest")
348 tcheck(t, err, "set password")
350 switchStop := func() {}
352 switchStop = store.Switchboard()
355 serverConn, clientConn := net.Pipe()
357 tlsConfig := &tls.Config{
358 Certificates: []tls.Certificate{fakeCert(t)},
361 serverConn = tls.Server(serverConn, tlsConfig)
362 clientConn = tls.Client(clientConn, &tls.Config{InsecureSkipVerify: true})
365 done := make(chan struct{})
369 serve("test", cid, tlsConfig, serverConn, isTLS, allowLoginWithoutTLS)
373 client, err := imapclient.New(clientConn, true)
374 tcheck(t, err, "new client")
375 return &testconn{t: t, conn: clientConn, client: client, done: done, serverConn: serverConn, account: acc}
378func fakeCert(t *testing.T) tls.Certificate {
379 privKey := ed25519.NewKeyFromSeed(make([]byte, ed25519.SeedSize)) // Fake key, don't use this for real!
380 template := &x509.Certificate{
381 SerialNumber: big.NewInt(1), // Required field...
383 localCertBuf, err := x509.CreateCertificate(cryptorand.Reader, template, template, privKey.Public(), privKey)
385 t.Fatalf("making certificate: %s", err)
387 cert, err := x509.ParseCertificate(localCertBuf)
389 t.Fatalf("parsing generated certificate: %s", err)
391 c := tls.Certificate{
392 Certificate: [][]byte{localCertBuf},
399func TestLogin(t *testing.T) {
403 tc.transactf("bad", "login too many args")
404 tc.transactf("bad", "login") // no args
405 tc.transactf("no", "login mjl@mox.example badpass")
406 tc.transactf("no", "login mjl testtest") // must use email, not account
407 tc.transactf("no", "login mjl@mox.example test")
408 tc.transactf("no", "login mjl@mox.example testtesttest")
409 tc.transactf("no", `login "mjl@mox.example" "testtesttest"`)
410 tc.transactf("no", "login \"m\xf8x@mox.example\" \"testtesttest\"")
411 tc.transactf("ok", "login mjl@mox.example testtest")
415 tc.transactf("ok", `login "mjl@mox.example" "testtest"`)
419 tc.transactf("ok", `login "\"\"@mox.example" "testtest"`)
422 tc.transactf("bad", "logout badarg")
423 tc.transactf("ok", "logout")
426// Test that commands don't work in the states they are not supposed to.
427func TestState(t *testing.T) {
430 notAuthenticated := []string{"starttls", "authenticate", "login"}
431 authenticatedOrSelected := []string{"enable", "select", "examine", "create", "delete", "rename", "subscribe", "unsubscribe", "list", "namespace", "status", "append", "idle", "lsub"}
432 selected := []string{"close", "unselect", "expunge", "search", "fetch", "store", "copy", "move", "uid expunge"}
435 tc.transactf("ok", "capability")
436 tc.transactf("ok", "noop")
437 tc.transactf("ok", "logout")
442 // Not authenticated, lots of commands not allowed.
443 for _, cmd := range append(append([]string{}, authenticatedOrSelected...), selected...) {
444 tc.transactf("no", "%s", cmd)
447 // Some commands not allowed when authenticated.
448 tc.transactf("ok", "login mjl@mox.example testtest")
449 for _, cmd := range append(append([]string{}, notAuthenticated...), selected...) {
450 tc.transactf("no", "%s", cmd)
453 tc.transactf("bad", "boguscommand")
456func TestNonIMAP(t *testing.T) {
460 // imap greeting has already been read, we sidestep the imapclient.
461 _, err := fmt.Fprintf(tc.conn, "bogus\r\n")
462 tc.check(err, "write bogus command")
463 tc.readprefixline("* BYE ")
464 if _, err := tc.conn.Read(make([]byte, 1)); err == nil {
465 t.Fatalf("connection not closed after initial bad command")
469func TestLiterals(t *testing.T) {
473 tc.client.Login("mjl@mox.example", "testtest")
474 tc.client.Create("tmpbox")
476 tc.transactf("ok", "rename {6+}\r\ntmpbox {7+}\r\nntmpbox")
480 fmt.Fprint(tc.client, "xtag rename ")
481 tc.client.WriteSyncLiteral(from)
482 fmt.Fprint(tc.client, " ")
483 tc.client.WriteSyncLiteral(to)
484 fmt.Fprint(tc.client, "\r\n")
485 tc.client.LastTag = "xtag"
486 tc.last(tc.client.Response())
487 if tc.lastResult.Status != "OK" {
488 tc.t.Fatalf(`got %q, expected "OK"`, tc.lastResult.Status)
492// Test longer scenario with login, lists, subscribes, status, selects, etc.
493func TestScenario(t *testing.T) {
496 tc.transactf("ok", "login mjl@mox.example testtest")
498 tc.transactf("bad", " missingcommand")
500 tc.transactf("ok", "examine inbox")
501 tc.transactf("ok", "unselect")
503 tc.transactf("ok", "examine inbox")
504 tc.transactf("ok", "close")
506 tc.transactf("ok", "select inbox")
507 tc.transactf("ok", "close")
509 tc.transactf("ok", "select inbox")
510 tc.transactf("ok", "expunge")
511 tc.transactf("ok", "check")
513 tc.transactf("ok", "subscribe inbox")
514 tc.transactf("ok", "unsubscribe inbox")
515 tc.transactf("ok", "subscribe inbox")
517 tc.transactf("ok", `lsub "" "*"`)
519 tc.transactf("ok", `list "" ""`)
520 tc.transactf("ok", `namespace`)
522 tc.transactf("ok", "enable utf8=accept")
523 tc.transactf("ok", "enable imap4rev2 utf8=accept")
525 tc.transactf("no", "create inbox")
526 tc.transactf("ok", "create tmpbox")
527 tc.transactf("ok", "rename tmpbox ntmpbox")
528 tc.transactf("ok", "delete ntmpbox")
530 tc.transactf("ok", "status inbox (uidnext messages uidvalidity deleted size unseen recent)")
532 tc.transactf("ok", "append inbox (\\seen) {%d+}\r\n%s", len(exampleMsg), exampleMsg)
533 tc.transactf("no", "append bogus () {%d}", len(exampleMsg))
534 tc.cmdf("", "append inbox () {%d}", len(exampleMsg))
535 tc.readprefixline("+ ")
536 _, err := tc.conn.Write([]byte(exampleMsg + "\r\n"))
537 tc.check(err, "write message")
540 tc.transactf("ok", "fetch 1 all")
541 tc.transactf("ok", "fetch 1 body")
542 tc.transactf("ok", "fetch 1 binary[]")
544 tc.transactf("ok", `store 1 flags (\seen \answered)`)
545 tc.transactf("ok", `store 1 +flags ($junk)`) // should train as junk.
546 tc.transactf("ok", `store 1 -flags ($junk)`) // should retrain as non-junk.
547 tc.transactf("ok", `store 1 -flags (\seen)`) // should untrain completely.
548 tc.transactf("ok", `store 1 -flags (\answered)`)
549 tc.transactf("ok", `store 1 +flags (\answered)`)
550 tc.transactf("ok", `store 1 flags.silent (\seen \answered)`)
551 tc.transactf("ok", `store 1 -flags.silent (\answered)`)
552 tc.transactf("ok", `store 1 +flags.silent (\answered)`)
553 tc.transactf("bad", `store 1 flags (\badflag)`)
554 tc.transactf("ok", "noop")
556 tc.transactf("ok", "copy 1 Trash")
557 tc.transactf("ok", "copy 1 Trash")
558 tc.transactf("ok", "move 1 Trash")
560 tc.transactf("ok", "close")
561 tc.transactf("ok", "select Trash")
562 tc.transactf("ok", `store 1 flags (\deleted)`)
563 tc.transactf("ok", "expunge")
564 tc.transactf("ok", "noop")
566 tc.transactf("ok", `store 1 flags (\deleted)`)
567 tc.transactf("ok", "close")
568 tc.transactf("ok", "delete Trash")
571func TestMailbox(t *testing.T) {
574 tc.client.Login("mjl@mox.example", "testtest")
577 "e\u0301", // é but as e + acute, not unicode-normalized
588 for _, bad := range invalid {
589 tc.transactf("no", "select {%d+}\r\n%s", len(bad), bad)
593func TestMailboxDeleted(t *testing.T) {
596 tc.client.Login("mjl@mox.example", "testtest")
598 tc2 := startNoSwitchboard(t)
600 tc2.client.Login("mjl@mox.example", "testtest")
602 tc.client.Create("testbox")
603 tc2.client.Select("testbox")
604 tc.client.Delete("testbox")
606 // Now try to operate on testbox while it has been removed.
607 tc2.transactf("no", "check")
608 tc2.transactf("no", "expunge")
609 tc2.transactf("no", "uid expunge 1")
610 tc2.transactf("no", "search all")
611 tc2.transactf("no", "uid search all")
612 tc2.transactf("no", "fetch 1:* all")
613 tc2.transactf("no", "uid fetch 1 all")
614 tc2.transactf("no", "store 1 flags ()")
615 tc2.transactf("no", "uid store 1 flags ()")
616 tc2.transactf("bad", "copy 1 inbox") // msgseq 1 not available.
617 tc2.transactf("no", "uid copy 1 inbox")
618 tc2.transactf("bad", "move 1 inbox") // msgseq 1 not available.
619 tc2.transactf("no", "uid move 1 inbox")
621 tc2.transactf("ok", "unselect")
623 tc.client.Create("testbox")
624 tc2.client.Select("testbox")
625 tc.client.Delete("testbox")
626 tc2.transactf("ok", "close")
629func TestID(t *testing.T) {
632 tc.client.Login("mjl@mox.example", "testtest")
634 tc.transactf("ok", "id nil")
635 tc.xuntagged(imapclient.UntaggedID{"name": "mox", "version": moxvar.Version})
637 tc.transactf("ok", `id ("name" "mox" "version" "1.2.3" "other" "test" "test" nil)`)
638 tc.xuntagged(imapclient.UntaggedID{"name": "mox", "version": moxvar.Version})
640 tc.transactf("bad", `id ("name" "mox" "name" "mox")`) // Duplicate field.
643func TestSequence(t *testing.T) {
646 tc.client.Login("mjl@mox.example", "testtest")
647 tc.client.Select("inbox")
652 tc.transactf("ok", "uid fetch 1 all") // non-existing messages are OK for uids.
653 tc.transactf("ok", "uid fetch * all") // * is like uidnext, a non-existing message.
655 tc.client.Append("inbox", nil, nil, []byte(exampleMsg))
656 tc.client.Append("inbox", nil, nil, []byte(exampleMsg))
657 tc.transactf("ok", "fetch 2:1,1 uid") // We reorder 2:1 to 1:2, but we don't deduplicate numbers.
659 imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1)}},
660 imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(2)}},
661 imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1)}},
664 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.
665 tc.xuntagged(imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(2)}})
668// Test that a message that is expunged by another session can be read as long as a
669// reference is held by a session. New sessions do not see the expunged message.
670// todo: possibly implement the additional reference counting. so far it hasn't been worth the trouble.
671func DisabledTestReference(t *testing.T) {
674 tc.client.Login("mjl@mox.example", "testtest")
675 tc.client.Select("inbox")
676 tc.client.Append("inbox", nil, nil, []byte(exampleMsg))
678 tc2 := startNoSwitchboard(t)
680 tc2.client.Login("mjl@mox.example", "testtest")
681 tc2.client.Select("inbox")
683 tc.client.StoreFlagsSet("1", true, `\Deleted`)
686 tc3 := startNoSwitchboard(t)
688 tc3.client.Login("mjl@mox.example", "testtest")
689 tc3.transactf("ok", `list "" "inbox" return (status (messages))`)
690 tc3.xuntagged(imapclient.UntaggedList{Separator: '/', Mailbox: "Inbox"}, imapclient.UntaggedStatus{Mailbox: "Inbox", Attrs: map[string]int64{"MESSAGES": 0}})
692 tc2.transactf("ok", "fetch 1 rfc822.size")
693 tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchRFC822Size(len(exampleMsg))}})