6 cryptorand "crypto/rand"
18 "github.com/mjl-/mox/imapclient"
19 "github.com/mjl-/mox/mox-"
20 "github.com/mjl-/mox/moxvar"
21 "github.com/mjl-/mox/store"
24var ctxbg = context.Background()
29 // Don't slow down tests.
34func tocrlf(s string) string {
35 return strings.ReplaceAll(s, "\n", "\r\n")
39var exampleMsg = tocrlf(`Date: Mon, 7 Feb 1994 21:52:25 -0800 (PST)
40From: Fred Foobar <foobar@Blurdybloop.example>
41Subject: afternoon meeting
42To: mooch@owatagu.siam.edu.example
43Message-Id: <B27397-0100000@Blurdybloop.example>
45Content-Type: TEXT/PLAIN; CHARSET=US-ASCII
47Hello Joe, do you think we can meet at 3:30 tomorrow?
56Message - multipart/mixed
57Part 1 - no content-type
59Part 3 - multipart/parallel
60Part 3.1 - audio/basic (base64)
61Part 3.2 - image/jpeg (base64, empty)
63Part 5 - message/rfc822
64Part 5.1 - text/plain (quoted-printable)
66var nestedMessage = tocrlf(`MIME-Version: 1.0
67From: Nathaniel Borenstein <nsb@nsb.fv.com>
68To: Ned Freed <ned@innosoft.com>
69Date: Fri, 07 Oct 1994 16:15:05 -0700 (PDT)
70Subject: A multipart example
71Content-Type: multipart/mixed;
72 boundary=unique-boundary-1
74This is the preamble area of a multipart message.
75Mail readers that understand multipart format
76should ignore this preamble.
78If you are reading this text, you might want to
79consider changing to a mail reader that understands
80how to properly display multipart messages.
84 ... Some text appears here ...
86[Note that the blank between the boundary and the start
87 of the text in this part means no header fields were
88 given and this is text in the US-ASCII character set.
89 It could have been done with explicit typing as in the
93Content-type: text/plain; charset=US-ASCII
95This could have been part of the previous part, but
96illustrates explicit versus implicit typing of body
100Content-Type: multipart/parallel; boundary=unique-boundary-2
103Content-Type: audio/basic
104Content-Transfer-Encoding: base64
109Content-Type: image/jpeg
110Content-Transfer-Encoding: base64
116Content-type: text/enriched
118This is <bold><italic>enriched.</italic></bold>
119<smaller>as defined in RFC 1896</smaller>
122<bigger><bigger>cool?</bigger></bigger>
125Content-Type: message/rfc822
127From: info@mox.example
128To: mox <info@mox.example>
129Subject: (subject in US-ASCII)
130Content-Type: Text/plain; charset=ISO-8859-1
131Content-Transfer-Encoding: Quoted-printable
133 ... Additional text in ISO-8859-1 goes here ...
138func tcheck(t *testing.T, err error, msg string) {
141 t.Fatalf("%s: %s", msg, err)
145func mockUIDValidity() func() {
146 orig := store.InitialUIDValidity
147 store.InitialUIDValidity = func() uint32 {
151 store.InitialUIDValidity = orig
155type testconn struct {
158 client *imapclient.Conn
161 account *store.Account
163 // Result of last command.
164 lastUntagged []imapclient.Untagged
165 lastResult imapclient.Result
169func (tc *testconn) check(err error, msg string) {
172 tc.t.Fatalf("%s: %s", msg, err)
176func (tc *testconn) last(l []imapclient.Untagged, r imapclient.Result, err error) {
182func (tc *testconn) xcode(s string) {
184 if tc.lastResult.Code != s {
185 tc.t.Fatalf("got last code %q, expected %q", tc.lastResult.Code, s)
189func (tc *testconn) xcodeArg(v any) {
191 if !reflect.DeepEqual(tc.lastResult.CodeArg, v) {
192 tc.t.Fatalf("got last code argument %v, expected %v", tc.lastResult.CodeArg, v)
196func (tc *testconn) xuntagged(exps ...imapclient.Untagged) {
198 tc.xuntaggedOpt(true, exps...)
201func (tc *testconn) xuntaggedOpt(all bool, exps ...imapclient.Untagged) {
203 last := append([]imapclient.Untagged{}, tc.lastUntagged...)
206 for ei, exp := range exps {
207 for i, l := range last {
208 if reflect.TypeOf(l) != reflect.TypeOf(exp) {
211 if !reflect.DeepEqual(l, exp) {
215 copy(last[i:], last[i+1:])
216 last = last[:len(last)-1]
220 tc.t.Fatalf("untagged data mismatch, got:\n\t%T %#v\nexpected:\n\t%T %#v", mismatch, mismatch, exp, exp)
223 if len(tc.lastUntagged) > 0 {
224 next = fmt.Sprintf(", next %#v", tc.lastUntagged[0])
226 tc.t.Fatalf("did not find untagged response %#v %T (%d) in %v%s", exp, exp, ei, tc.lastUntagged, next)
228 if len(last) > 0 && all {
229 tc.t.Fatalf("leftover untagged responses %v", last)
233func tuntagged(t *testing.T, got imapclient.Untagged, dst any) {
235 gotv := reflect.ValueOf(got)
236 dstv := reflect.ValueOf(dst)
237 if gotv.Type() != dstv.Type().Elem() {
238 t.Fatalf("got %v, expected %v", gotv.Type(), dstv.Type().Elem())
240 dstv.Elem().Set(gotv)
243func (tc *testconn) xnountagged() {
245 if len(tc.lastUntagged) != 0 {
246 tc.t.Fatalf("got %v untagged, expected 0", tc.lastUntagged)
250func (tc *testconn) transactf(status, format string, args ...any) {
252 tc.cmdf("", format, args...)
256func (tc *testconn) response(status string) {
258 tc.lastUntagged, tc.lastResult, tc.lastErr = tc.client.Response()
259 tcheck(tc.t, tc.lastErr, "read imap response")
260 if strings.ToUpper(status) != string(tc.lastResult.Status) {
261 tc.t.Fatalf("got status %q, expected %q", tc.lastResult.Status, status)
265func (tc *testconn) cmdf(tag, format string, args ...any) {
267 err := tc.client.Commandf(tag, format, args...)
268 tcheck(tc.t, err, "writing imap command")
271func (tc *testconn) readstatus(status string) {
276func (tc *testconn) readprefixline(pre string) {
278 line, err := tc.client.Readline()
279 tcheck(tc.t, err, "read line")
280 if !strings.HasPrefix(line, pre) {
281 tc.t.Fatalf("expected prefix %q, got %q", pre, line)
285func (tc *testconn) writelinef(format string, args ...any) {
287 err := tc.client.Writelinef(format, args...)
288 tcheck(tc.t, err, "write line")
291// wait at most 1 second for server to quit.
292func (tc *testconn) waitDone() {
294 t := time.NewTimer(time.Second)
299 tc.t.Fatalf("server not done within 1s")
303func (tc *testconn) close() {
304 if tc.account == nil {
305 // Already closed, we are not strict about closing multiple times.
308 err := tc.account.Close()
309 tc.check(err, "close account")
312 tc.serverConn.Close()
316func xparseNumSet(s string) imapclient.NumSet {
317 ns, err := imapclient.ParseNumSet(s)
319 panic(fmt.Sprintf("parsing numset %s: %s", s, err))
326func start(t *testing.T) *testconn {
327 return startArgs(t, true, false, true)
330func startNoSwitchboard(t *testing.T) *testconn {
331 return startArgs(t, false, false, true)
334func startArgs(t *testing.T, first, isTLS, allowLoginWithoutTLS bool) *testconn {
335 limitersInit() // Reset rate limiters.
338 os.RemoveAll("../testdata/imap/data")
341 mox.ConfigStaticPath = "../testdata/imap/mox.conf"
342 mox.MustLoadConfig(true, false)
343 acc, err := store.OpenAccount("mjl")
344 tcheck(t, err, "open account")
346 err = acc.SetPassword("testtest")
347 tcheck(t, err, "set password")
349 switchStop := func() {}
351 switchStop = store.Switchboard()
354 serverConn, clientConn := net.Pipe()
356 tlsConfig := &tls.Config{
357 Certificates: []tls.Certificate{fakeCert(t)},
360 serverConn = tls.Server(serverConn, tlsConfig)
361 clientConn = tls.Client(clientConn, &tls.Config{InsecureSkipVerify: true})
364 done := make(chan struct{})
368 serve("test", cid, tlsConfig, serverConn, isTLS, allowLoginWithoutTLS)
372 client, err := imapclient.New(clientConn, true)
373 tcheck(t, err, "new client")
374 return &testconn{t: t, conn: clientConn, client: client, done: done, serverConn: serverConn, account: acc}
377func fakeCert(t *testing.T) tls.Certificate {
378 privKey := ed25519.NewKeyFromSeed(make([]byte, ed25519.SeedSize)) // Fake key, don't use this for real!
379 template := &x509.Certificate{
380 SerialNumber: big.NewInt(1), // Required field...
382 localCertBuf, err := x509.CreateCertificate(cryptorand.Reader, template, template, privKey.Public(), privKey)
384 t.Fatalf("making certificate: %s", err)
386 cert, err := x509.ParseCertificate(localCertBuf)
388 t.Fatalf("parsing generated certificate: %s", err)
390 c := tls.Certificate{
391 Certificate: [][]byte{localCertBuf},
398func TestLogin(t *testing.T) {
402 tc.transactf("bad", "login too many args")
403 tc.transactf("bad", "login") // no args
404 tc.transactf("no", "login mjl@mox.example badpass")
405 tc.transactf("no", "login mjl testtest") // must use email, not account
406 tc.transactf("no", "login mjl@mox.example test")
407 tc.transactf("no", "login mjl@mox.example testtesttest")
408 tc.transactf("no", `login "mjl@mox.example" "testtesttest"`)
409 tc.transactf("no", "login \"m\xf8x@mox.example\" \"testtesttest\"")
410 tc.transactf("ok", "login mjl@mox.example testtest")
414 tc.transactf("ok", `login "mjl@mox.example" "testtest"`)
418 tc.transactf("ok", `login "\"\"@mox.example" "testtest"`)
421 tc.transactf("bad", "logout badarg")
422 tc.transactf("ok", "logout")
425// Test that commands don't work in the states they are not supposed to.
426func TestState(t *testing.T) {
429 notAuthenticated := []string{"starttls", "authenticate", "login"}
430 authenticatedOrSelected := []string{"enable", "select", "examine", "create", "delete", "rename", "subscribe", "unsubscribe", "list", "namespace", "status", "append", "idle", "lsub"}
431 selected := []string{"close", "unselect", "expunge", "search", "fetch", "store", "copy", "move", "uid expunge"}
434 tc.transactf("ok", "capability")
435 tc.transactf("ok", "noop")
436 tc.transactf("ok", "logout")
441 // Not authenticated, lots of commands not allowed.
442 for _, cmd := range append(append([]string{}, authenticatedOrSelected...), selected...) {
443 tc.transactf("no", "%s", cmd)
446 // Some commands not allowed when authenticated.
447 tc.transactf("ok", "login mjl@mox.example testtest")
448 for _, cmd := range append(append([]string{}, notAuthenticated...), selected...) {
449 tc.transactf("no", "%s", cmd)
452 tc.transactf("bad", "boguscommand")
455func TestNonIMAP(t *testing.T) {
459 // imap greeting has already been read, we sidestep the imapclient.
460 _, err := fmt.Fprintf(tc.conn, "bogus\r\n")
461 tc.check(err, "write bogus command")
462 tc.readprefixline("* BYE ")
463 if _, err := tc.conn.Read(make([]byte, 1)); err == nil {
464 t.Fatalf("connection not closed after initial bad command")
468func TestLiterals(t *testing.T) {
472 tc.client.Login("mjl@mox.example", "testtest")
473 tc.client.Create("tmpbox")
475 tc.transactf("ok", "rename {6+}\r\ntmpbox {7+}\r\nntmpbox")
479 fmt.Fprint(tc.client, "xtag rename ")
480 tc.client.WriteSyncLiteral(from)
481 fmt.Fprint(tc.client, " ")
482 tc.client.WriteSyncLiteral(to)
483 fmt.Fprint(tc.client, "\r\n")
484 tc.client.LastTag = "xtag"
485 tc.last(tc.client.Response())
486 if tc.lastResult.Status != "OK" {
487 tc.t.Fatalf(`got %q, expected "OK"`, tc.lastResult.Status)
491// Test longer scenario with login, lists, subscribes, status, selects, etc.
492func TestScenario(t *testing.T) {
495 tc.transactf("ok", "login mjl@mox.example testtest")
497 tc.transactf("bad", " missingcommand")
499 tc.transactf("ok", "examine inbox")
500 tc.transactf("ok", "unselect")
502 tc.transactf("ok", "examine inbox")
503 tc.transactf("ok", "close")
505 tc.transactf("ok", "select inbox")
506 tc.transactf("ok", "close")
508 tc.transactf("ok", "select inbox")
509 tc.transactf("ok", "expunge")
510 tc.transactf("ok", "check")
512 tc.transactf("ok", "subscribe inbox")
513 tc.transactf("ok", "unsubscribe inbox")
514 tc.transactf("ok", "subscribe inbox")
516 tc.transactf("ok", `lsub "" "*"`)
518 tc.transactf("ok", `list "" ""`)
519 tc.transactf("ok", `namespace`)
521 tc.transactf("ok", "enable utf8=accept")
522 tc.transactf("ok", "enable imap4rev2 utf8=accept")
524 tc.transactf("no", "create inbox")
525 tc.transactf("ok", "create tmpbox")
526 tc.transactf("ok", "rename tmpbox ntmpbox")
527 tc.transactf("ok", "delete ntmpbox")
529 tc.transactf("ok", "status inbox (uidnext messages uidvalidity deleted size unseen recent)")
531 tc.transactf("ok", "append inbox (\\seen) {%d+}\r\n%s", len(exampleMsg), exampleMsg)
532 tc.transactf("no", "append bogus () {%d}", len(exampleMsg))
533 tc.cmdf("", "append inbox () {%d}", len(exampleMsg))
534 tc.readprefixline("+ ")
535 _, err := tc.conn.Write([]byte(exampleMsg + "\r\n"))
536 tc.check(err, "write message")
539 tc.transactf("ok", "fetch 1 all")
540 tc.transactf("ok", "fetch 1 body")
541 tc.transactf("ok", "fetch 1 binary[]")
543 tc.transactf("ok", `store 1 flags (\seen \answered)`)
544 tc.transactf("ok", `store 1 +flags ($junk)`) // should train as junk.
545 tc.transactf("ok", `store 1 -flags ($junk)`) // should retrain as non-junk.
546 tc.transactf("ok", `store 1 -flags (\seen)`) // should untrain completely.
547 tc.transactf("ok", `store 1 -flags (\answered)`)
548 tc.transactf("ok", `store 1 +flags (\answered)`)
549 tc.transactf("ok", `store 1 flags.silent (\seen \answered)`)
550 tc.transactf("ok", `store 1 -flags.silent (\answered)`)
551 tc.transactf("ok", `store 1 +flags.silent (\answered)`)
552 tc.transactf("bad", `store 1 flags (\badflag)`)
553 tc.transactf("ok", "noop")
555 tc.transactf("ok", "copy 1 Trash")
556 tc.transactf("ok", "copy 1 Trash")
557 tc.transactf("ok", "move 1 Trash")
559 tc.transactf("ok", "close")
560 tc.transactf("ok", "select Trash")
561 tc.transactf("ok", `store 1 flags (\deleted)`)
562 tc.transactf("ok", "expunge")
563 tc.transactf("ok", "noop")
565 tc.transactf("ok", `store 1 flags (\deleted)`)
566 tc.transactf("ok", "close")
567 tc.transactf("ok", "delete Trash")
570func TestMailbox(t *testing.T) {
573 tc.client.Login("mjl@mox.example", "testtest")
576 "e\u0301", // é but as e + acute, not unicode-normalized
587 for _, bad := range invalid {
588 tc.transactf("no", "select {%d+}\r\n%s", len(bad), bad)
592func TestMailboxDeleted(t *testing.T) {
595 tc.client.Login("mjl@mox.example", "testtest")
597 tc2 := startNoSwitchboard(t)
599 tc2.client.Login("mjl@mox.example", "testtest")
601 tc.client.Create("testbox")
602 tc2.client.Select("testbox")
603 tc.client.Delete("testbox")
605 // Now try to operate on testbox while it has been removed.
606 tc2.transactf("no", "check")
607 tc2.transactf("no", "expunge")
608 tc2.transactf("no", "uid expunge 1")
609 tc2.transactf("no", "search all")
610 tc2.transactf("no", "uid search all")
611 tc2.transactf("no", "fetch 1:* all")
612 tc2.transactf("no", "uid fetch 1 all")
613 tc2.transactf("no", "store 1 flags ()")
614 tc2.transactf("no", "uid store 1 flags ()")
615 tc2.transactf("bad", "copy 1 inbox") // msgseq 1 not available.
616 tc2.transactf("no", "uid copy 1 inbox")
617 tc2.transactf("bad", "move 1 inbox") // msgseq 1 not available.
618 tc2.transactf("no", "uid move 1 inbox")
620 tc2.transactf("ok", "unselect")
622 tc.client.Create("testbox")
623 tc2.client.Select("testbox")
624 tc.client.Delete("testbox")
625 tc2.transactf("ok", "close")
628func TestID(t *testing.T) {
631 tc.client.Login("mjl@mox.example", "testtest")
633 tc.transactf("ok", "id nil")
634 tc.xuntagged(imapclient.UntaggedID{"name": "mox", "version": moxvar.Version})
636 tc.transactf("ok", `id ("name" "mox" "version" "1.2.3" "other" "test" "test" nil)`)
637 tc.xuntagged(imapclient.UntaggedID{"name": "mox", "version": moxvar.Version})
639 tc.transactf("bad", `id ("name" "mox" "name" "mox")`) // Duplicate field.
642func TestSequence(t *testing.T) {
645 tc.client.Login("mjl@mox.example", "testtest")
646 tc.client.Select("inbox")
651 tc.transactf("ok", "uid fetch 1 all") // non-existing messages are OK for uids.
652 tc.transactf("ok", "uid fetch * all") // * is like uidnext, a non-existing message.
654 tc.client.Append("inbox", nil, nil, []byte(exampleMsg))
655 tc.client.Append("inbox", nil, nil, []byte(exampleMsg))
656 tc.transactf("ok", "fetch 2:1,1 uid") // We reorder 2:1 to 1:2, but we don't deduplicate numbers.
658 imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1)}},
659 imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(2)}},
660 imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1)}},
663 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.
664 tc.xuntagged(imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(2)}})
667// Test that a message that is expunged by another session can be read as long as a
668// reference is held by a session. New sessions do not see the expunged message.
669// todo: possibly implement the additional reference counting. so far it hasn't been worth the trouble.
670func DisabledTestReference(t *testing.T) {
673 tc.client.Login("mjl@mox.example", "testtest")
674 tc.client.Select("inbox")
675 tc.client.Append("inbox", nil, nil, []byte(exampleMsg))
677 tc2 := startNoSwitchboard(t)
679 tc2.client.Login("mjl@mox.example", "testtest")
680 tc2.client.Select("inbox")
682 tc.client.StoreFlagsSet("1", true, `\Deleted`)
685 tc3 := startNoSwitchboard(t)
687 tc3.client.Login("mjl@mox.example", "testtest")
688 tc3.transactf("ok", `list "" "inbox" return (status (messages))`)
689 tc3.xuntagged(imapclient.UntaggedList{Separator: '/', Mailbox: "Inbox"}, imapclient.UntaggedStatus{Mailbox: "Inbox", Attrs: map[string]int64{"MESSAGES": 0}})
691 tc2.transactf("ok", "fetch 1 rfc822.size")
692 tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchRFC822Size(len(exampleMsg))}})