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")
313 // no account.CheckClosed(), the tests open accounts multiple times.
316 tc.serverConn.Close()
320func xparseNumSet(s string) imapclient.NumSet {
321 ns, err := imapclient.ParseNumSet(s)
323 panic(fmt.Sprintf("parsing numset %s: %s", s, err))
330func start(t *testing.T) *testconn {
331 return startArgs(t, true, false, true, true, "mjl")
334func startNoSwitchboard(t *testing.T) *testconn {
335 return startArgs(t, false, false, true, false, "mjl")
338const password0 = "te\u0301st \u00a0\u2002\u200a" // NFD and various unicode spaces.
339const password1 = "tést " // PRECIS normalized, with NFC.
341func startArgs(t *testing.T, first, isTLS, allowLoginWithoutTLS, setPassword bool, accname string) *testconn {
342 limitersInit() // Reset rate limiters.
345 os.RemoveAll("../testdata/imap/data")
348 mox.ConfigStaticPath = filepath.FromSlash("../testdata/imap/mox.conf")
349 mox.MustLoadConfig(true, false)
350 acc, err := store.OpenAccount(pkglog, accname)
351 tcheck(t, err, "open account")
353 err = acc.SetPassword(pkglog, password0)
354 tcheck(t, err, "set password")
356 switchStop := func() {}
358 switchStop = store.Switchboard()
361 serverConn, clientConn := net.Pipe()
363 tlsConfig := &tls.Config{
364 Certificates: []tls.Certificate{fakeCert(t)},
367 serverConn = tls.Server(serverConn, tlsConfig)
368 clientConn = tls.Client(clientConn, &tls.Config{InsecureSkipVerify: true})
371 done := make(chan struct{})
375 serve("test", cid, tlsConfig, serverConn, isTLS, allowLoginWithoutTLS)
379 client, err := imapclient.New(clientConn, true)
380 tcheck(t, err, "new client")
381 return &testconn{t: t, conn: clientConn, client: client, done: done, serverConn: serverConn, account: acc}
384func fakeCert(t *testing.T) tls.Certificate {
385 privKey := ed25519.NewKeyFromSeed(make([]byte, ed25519.SeedSize)) // Fake key, don't use this for real!
386 template := &x509.Certificate{
387 SerialNumber: big.NewInt(1), // Required field...
389 localCertBuf, err := x509.CreateCertificate(cryptorand.Reader, template, template, privKey.Public(), privKey)
391 t.Fatalf("making certificate: %s", err)
393 cert, err := x509.ParseCertificate(localCertBuf)
395 t.Fatalf("parsing generated certificate: %s", err)
397 c := tls.Certificate{
398 Certificate: [][]byte{localCertBuf},
405func TestLogin(t *testing.T) {
409 tc.transactf("bad", "login too many args")
410 tc.transactf("bad", "login") // no args
411 tc.transactf("no", "login mjl@mox.example badpass")
412 tc.transactf("no", `login mjl "%s"`, password0) // must use email, not account
413 tc.transactf("no", "login mjl@mox.example test")
414 tc.transactf("no", "login mjl@mox.example testtesttest")
415 tc.transactf("no", `login "mjl@mox.example" "testtesttest"`)
416 tc.transactf("no", "login \"m\xf8x@mox.example\" \"testtesttest\"")
417 tc.transactf("ok", `login mjl@mox.example "%s"`, password0)
421 tc.transactf("ok", `login "mjl@mox.example" "%s"`, password0)
425 tc.transactf("ok", `login "\"\"@mox.example" "%s"`, password0)
428 tc.transactf("bad", "logout badarg")
429 tc.transactf("ok", "logout")
432// Test that commands don't work in the states they are not supposed to.
433func TestState(t *testing.T) {
436 notAuthenticated := []string{"starttls", "authenticate", "login"}
437 authenticatedOrSelected := []string{"enable", "select", "examine", "create", "delete", "rename", "subscribe", "unsubscribe", "list", "namespace", "status", "append", "idle", "lsub"}
438 selected := []string{"close", "unselect", "expunge", "search", "fetch", "store", "copy", "move", "uid expunge"}
441 tc.transactf("ok", "capability")
442 tc.transactf("ok", "noop")
443 tc.transactf("ok", "logout")
448 // Not authenticated, lots of commands not allowed.
449 for _, cmd := range append(append([]string{}, authenticatedOrSelected...), selected...) {
450 tc.transactf("no", "%s", cmd)
453 // Some commands not allowed when authenticated.
454 tc.transactf("ok", `login mjl@mox.example "%s"`, password0)
455 for _, cmd := range append(append([]string{}, notAuthenticated...), selected...) {
456 tc.transactf("no", "%s", cmd)
459 tc.transactf("bad", "boguscommand")
462func TestNonIMAP(t *testing.T) {
466 // imap greeting has already been read, we sidestep the imapclient.
467 _, err := fmt.Fprintf(tc.conn, "bogus\r\n")
468 tc.check(err, "write bogus command")
469 tc.readprefixline("* BYE ")
470 if _, err := tc.conn.Read(make([]byte, 1)); err == nil {
471 t.Fatalf("connection not closed after initial bad command")
475func TestLiterals(t *testing.T) {
479 tc.client.Login("mjl@mox.example", password0)
480 tc.client.Create("tmpbox")
482 tc.transactf("ok", "rename {6+}\r\ntmpbox {7+}\r\nntmpbox")
486 fmt.Fprint(tc.client, "xtag rename ")
487 tc.client.WriteSyncLiteral(from)
488 fmt.Fprint(tc.client, " ")
489 tc.client.WriteSyncLiteral(to)
490 fmt.Fprint(tc.client, "\r\n")
491 tc.client.LastTag = "xtag"
492 tc.last(tc.client.Response())
493 if tc.lastResult.Status != "OK" {
494 tc.t.Fatalf(`got %q, expected "OK"`, tc.lastResult.Status)
498// Test longer scenario with login, lists, subscribes, status, selects, etc.
499func TestScenario(t *testing.T) {
502 tc.transactf("ok", `login mjl@mox.example "%s"`, password0)
504 tc.transactf("bad", " missingcommand")
506 tc.transactf("ok", "examine inbox")
507 tc.transactf("ok", "unselect")
509 tc.transactf("ok", "examine inbox")
510 tc.transactf("ok", "close")
512 tc.transactf("ok", "select inbox")
513 tc.transactf("ok", "close")
515 tc.transactf("ok", "select inbox")
516 tc.transactf("ok", "expunge")
517 tc.transactf("ok", "check")
519 tc.transactf("ok", "subscribe inbox")
520 tc.transactf("ok", "unsubscribe inbox")
521 tc.transactf("ok", "subscribe inbox")
523 tc.transactf("ok", `lsub "" "*"`)
525 tc.transactf("ok", `list "" ""`)
526 tc.transactf("ok", `namespace`)
528 tc.transactf("ok", "enable utf8=accept")
529 tc.transactf("ok", "enable imap4rev2 utf8=accept")
531 tc.transactf("no", "create inbox")
532 tc.transactf("ok", "create tmpbox")
533 tc.transactf("ok", "rename tmpbox ntmpbox")
534 tc.transactf("ok", "delete ntmpbox")
536 tc.transactf("ok", "status inbox (uidnext messages uidvalidity deleted size unseen recent)")
538 tc.transactf("ok", "append inbox (\\seen) {%d+}\r\n%s", len(exampleMsg), exampleMsg)
539 tc.transactf("no", "append bogus () {%d}", len(exampleMsg))
540 tc.cmdf("", "append inbox () {%d}", len(exampleMsg))
541 tc.readprefixline("+ ")
542 _, err := tc.conn.Write([]byte(exampleMsg + "\r\n"))
543 tc.check(err, "write message")
546 tc.transactf("ok", "fetch 1 all")
547 tc.transactf("ok", "fetch 1 body")
548 tc.transactf("ok", "fetch 1 binary[]")
550 tc.transactf("ok", `store 1 flags (\seen \answered)`)
551 tc.transactf("ok", `store 1 +flags ($junk)`) // should train as junk.
552 tc.transactf("ok", `store 1 -flags ($junk)`) // should retrain as non-junk.
553 tc.transactf("ok", `store 1 -flags (\seen)`) // should untrain completely.
554 tc.transactf("ok", `store 1 -flags (\answered)`)
555 tc.transactf("ok", `store 1 +flags (\answered)`)
556 tc.transactf("ok", `store 1 flags.silent (\seen \answered)`)
557 tc.transactf("ok", `store 1 -flags.silent (\answered)`)
558 tc.transactf("ok", `store 1 +flags.silent (\answered)`)
559 tc.transactf("bad", `store 1 flags (\badflag)`)
560 tc.transactf("ok", "noop")
562 tc.transactf("ok", "copy 1 Trash")
563 tc.transactf("ok", "copy 1 Trash")
564 tc.transactf("ok", "move 1 Trash")
566 tc.transactf("ok", "close")
567 tc.transactf("ok", "select Trash")
568 tc.transactf("ok", `store 1 flags (\deleted)`)
569 tc.transactf("ok", "expunge")
570 tc.transactf("ok", "noop")
572 tc.transactf("ok", `store 1 flags (\deleted)`)
573 tc.transactf("ok", "close")
574 tc.transactf("ok", "delete Trash")
577func TestMailbox(t *testing.T) {
580 tc.client.Login("mjl@mox.example", password0)
583 "e\u0301", // é but as e + acute, not unicode-normalized
594 for _, bad := range invalid {
595 tc.transactf("no", "select {%d+}\r\n%s", len(bad), bad)
599func TestMailboxDeleted(t *testing.T) {
602 tc.client.Login("mjl@mox.example", password0)
604 tc2 := startNoSwitchboard(t)
606 tc2.client.Login("mjl@mox.example", password0)
608 tc.client.Create("testbox")
609 tc2.client.Select("testbox")
610 tc.client.Delete("testbox")
612 // Now try to operate on testbox while it has been removed.
613 tc2.transactf("no", "check")
614 tc2.transactf("no", "expunge")
615 tc2.transactf("no", "uid expunge 1")
616 tc2.transactf("no", "search all")
617 tc2.transactf("no", "uid search all")
618 tc2.transactf("no", "fetch 1:* all")
619 tc2.transactf("no", "uid fetch 1 all")
620 tc2.transactf("no", "store 1 flags ()")
621 tc2.transactf("no", "uid store 1 flags ()")
622 tc2.transactf("bad", "copy 1 inbox") // msgseq 1 not available.
623 tc2.transactf("no", "uid copy 1 inbox")
624 tc2.transactf("bad", "move 1 inbox") // msgseq 1 not available.
625 tc2.transactf("no", "uid move 1 inbox")
627 tc2.transactf("ok", "unselect")
629 tc.client.Create("testbox")
630 tc2.client.Select("testbox")
631 tc.client.Delete("testbox")
632 tc2.transactf("ok", "close")
635func TestID(t *testing.T) {
638 tc.client.Login("mjl@mox.example", password0)
640 tc.transactf("ok", "id nil")
641 tc.xuntagged(imapclient.UntaggedID{"name": "mox", "version": moxvar.Version})
643 tc.transactf("ok", `id ("name" "mox" "version" "1.2.3" "other" "test" "test" nil)`)
644 tc.xuntagged(imapclient.UntaggedID{"name": "mox", "version": moxvar.Version})
646 tc.transactf("bad", `id ("name" "mox" "name" "mox")`) // Duplicate field.
649func TestSequence(t *testing.T) {
652 tc.client.Login("mjl@mox.example", password0)
653 tc.client.Select("inbox")
658 tc.transactf("ok", "uid fetch 1 all") // non-existing messages are OK for uids.
659 tc.transactf("ok", "uid fetch * all") // * is like uidnext, a non-existing message.
661 tc.client.Append("inbox", nil, nil, []byte(exampleMsg))
662 tc.client.Append("inbox", nil, nil, []byte(exampleMsg))
663 tc.transactf("ok", "fetch 2:1,1 uid") // We reorder 2:1 to 1:2, but we don't deduplicate numbers.
665 imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1)}},
666 imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(2)}},
667 imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1)}},
670 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.
671 tc.xuntagged(imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(2)}})
674// Test that a message that is expunged by another session can be read as long as a
675// reference is held by a session. New sessions do not see the expunged message.
676// todo: possibly implement the additional reference counting. so far it hasn't been worth the trouble.
677func DisabledTestReference(t *testing.T) {
680 tc.client.Login("mjl@mox.example", password0)
681 tc.client.Select("inbox")
682 tc.client.Append("inbox", nil, nil, []byte(exampleMsg))
684 tc2 := startNoSwitchboard(t)
686 tc2.client.Login("mjl@mox.example", password0)
687 tc2.client.Select("inbox")
689 tc.client.StoreFlagsSet("1", true, `\Deleted`)
692 tc3 := startNoSwitchboard(t)
694 tc3.client.Login("mjl@mox.example", password0)
695 tc3.transactf("ok", `list "" "inbox" return (status (messages))`)
696 tc3.xuntagged(imapclient.UntaggedList{Separator: '/', Mailbox: "Inbox"}, imapclient.UntaggedStatus{Mailbox: "Inbox", Attrs: map[imapclient.StatusAttr]int64{imapclient.StatusMessages: 0}})
698 tc2.transactf("ok", "fetch 1 rfc822.size")
699 tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchRFC822Size(len(exampleMsg))}})