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")
337const password0 = "te\u0301st \u00a0\u2002\u200a" // NFD and various unicode spaces.
338const password1 = "tést " // PRECIS normalized, with NFC.
340func startArgs(t *testing.T, first, isTLS, allowLoginWithoutTLS, setPassword bool, accname string) *testconn {
341 limitersInit() // Reset rate limiters.
344 os.RemoveAll("../testdata/imap/data")
347 mox.ConfigStaticPath = filepath.FromSlash("../testdata/imap/mox.conf")
348 mox.MustLoadConfig(true, false)
349 acc, err := store.OpenAccount(pkglog, accname)
350 tcheck(t, err, "open account")
352 err = acc.SetPassword(pkglog, password0)
353 tcheck(t, err, "set password")
355 switchStop := func() {}
357 switchStop = store.Switchboard()
360 serverConn, clientConn := net.Pipe()
362 tlsConfig := &tls.Config{
363 Certificates: []tls.Certificate{fakeCert(t)},
366 serverConn = tls.Server(serverConn, tlsConfig)
367 clientConn = tls.Client(clientConn, &tls.Config{InsecureSkipVerify: true})
370 done := make(chan struct{})
374 serve("test", cid, tlsConfig, serverConn, isTLS, allowLoginWithoutTLS)
378 client, err := imapclient.New(clientConn, true)
379 tcheck(t, err, "new client")
380 return &testconn{t: t, conn: clientConn, client: client, done: done, serverConn: serverConn, account: acc}
383func fakeCert(t *testing.T) tls.Certificate {
384 privKey := ed25519.NewKeyFromSeed(make([]byte, ed25519.SeedSize)) // Fake key, don't use this for real!
385 template := &x509.Certificate{
386 SerialNumber: big.NewInt(1), // Required field...
388 localCertBuf, err := x509.CreateCertificate(cryptorand.Reader, template, template, privKey.Public(), privKey)
390 t.Fatalf("making certificate: %s", err)
392 cert, err := x509.ParseCertificate(localCertBuf)
394 t.Fatalf("parsing generated certificate: %s", err)
396 c := tls.Certificate{
397 Certificate: [][]byte{localCertBuf},
404func TestLogin(t *testing.T) {
408 tc.transactf("bad", "login too many args")
409 tc.transactf("bad", "login") // no args
410 tc.transactf("no", "login mjl@mox.example badpass")
411 tc.transactf("no", `login mjl "%s"`, password0) // must use email, not account
412 tc.transactf("no", "login mjl@mox.example test")
413 tc.transactf("no", "login mjl@mox.example testtesttest")
414 tc.transactf("no", `login "mjl@mox.example" "testtesttest"`)
415 tc.transactf("no", "login \"m\xf8x@mox.example\" \"testtesttest\"")
416 tc.transactf("ok", `login mjl@mox.example "%s"`, password0)
420 tc.transactf("ok", `login "mjl@mox.example" "%s"`, password0)
424 tc.transactf("ok", `login "\"\"@mox.example" "%s"`, password0)
427 tc.transactf("bad", "logout badarg")
428 tc.transactf("ok", "logout")
431// Test that commands don't work in the states they are not supposed to.
432func TestState(t *testing.T) {
435 notAuthenticated := []string{"starttls", "authenticate", "login"}
436 authenticatedOrSelected := []string{"enable", "select", "examine", "create", "delete", "rename", "subscribe", "unsubscribe", "list", "namespace", "status", "append", "idle", "lsub"}
437 selected := []string{"close", "unselect", "expunge", "search", "fetch", "store", "copy", "move", "uid expunge"}
440 tc.transactf("ok", "capability")
441 tc.transactf("ok", "noop")
442 tc.transactf("ok", "logout")
447 // Not authenticated, lots of commands not allowed.
448 for _, cmd := range append(append([]string{}, authenticatedOrSelected...), selected...) {
449 tc.transactf("no", "%s", cmd)
452 // Some commands not allowed when authenticated.
453 tc.transactf("ok", `login mjl@mox.example "%s"`, password0)
454 for _, cmd := range append(append([]string{}, notAuthenticated...), selected...) {
455 tc.transactf("no", "%s", cmd)
458 tc.transactf("bad", "boguscommand")
461func TestNonIMAP(t *testing.T) {
465 // imap greeting has already been read, we sidestep the imapclient.
466 _, err := fmt.Fprintf(tc.conn, "bogus\r\n")
467 tc.check(err, "write bogus command")
468 tc.readprefixline("* BYE ")
469 if _, err := tc.conn.Read(make([]byte, 1)); err == nil {
470 t.Fatalf("connection not closed after initial bad command")
474func TestLiterals(t *testing.T) {
478 tc.client.Login("mjl@mox.example", password0)
479 tc.client.Create("tmpbox")
481 tc.transactf("ok", "rename {6+}\r\ntmpbox {7+}\r\nntmpbox")
485 fmt.Fprint(tc.client, "xtag rename ")
486 tc.client.WriteSyncLiteral(from)
487 fmt.Fprint(tc.client, " ")
488 tc.client.WriteSyncLiteral(to)
489 fmt.Fprint(tc.client, "\r\n")
490 tc.client.LastTag = "xtag"
491 tc.last(tc.client.Response())
492 if tc.lastResult.Status != "OK" {
493 tc.t.Fatalf(`got %q, expected "OK"`, tc.lastResult.Status)
497// Test longer scenario with login, lists, subscribes, status, selects, etc.
498func TestScenario(t *testing.T) {
501 tc.transactf("ok", `login mjl@mox.example "%s"`, password0)
503 tc.transactf("bad", " missingcommand")
505 tc.transactf("ok", "examine inbox")
506 tc.transactf("ok", "unselect")
508 tc.transactf("ok", "examine inbox")
509 tc.transactf("ok", "close")
511 tc.transactf("ok", "select inbox")
512 tc.transactf("ok", "close")
514 tc.transactf("ok", "select inbox")
515 tc.transactf("ok", "expunge")
516 tc.transactf("ok", "check")
518 tc.transactf("ok", "subscribe inbox")
519 tc.transactf("ok", "unsubscribe inbox")
520 tc.transactf("ok", "subscribe inbox")
522 tc.transactf("ok", `lsub "" "*"`)
524 tc.transactf("ok", `list "" ""`)
525 tc.transactf("ok", `namespace`)
527 tc.transactf("ok", "enable utf8=accept")
528 tc.transactf("ok", "enable imap4rev2 utf8=accept")
530 tc.transactf("no", "create inbox")
531 tc.transactf("ok", "create tmpbox")
532 tc.transactf("ok", "rename tmpbox ntmpbox")
533 tc.transactf("ok", "delete ntmpbox")
535 tc.transactf("ok", "status inbox (uidnext messages uidvalidity deleted size unseen recent)")
537 tc.transactf("ok", "append inbox (\\seen) {%d+}\r\n%s", len(exampleMsg), exampleMsg)
538 tc.transactf("no", "append bogus () {%d}", len(exampleMsg))
539 tc.cmdf("", "append inbox () {%d}", len(exampleMsg))
540 tc.readprefixline("+ ")
541 _, err := tc.conn.Write([]byte(exampleMsg + "\r\n"))
542 tc.check(err, "write message")
545 tc.transactf("ok", "fetch 1 all")
546 tc.transactf("ok", "fetch 1 body")
547 tc.transactf("ok", "fetch 1 binary[]")
549 tc.transactf("ok", `store 1 flags (\seen \answered)`)
550 tc.transactf("ok", `store 1 +flags ($junk)`) // should train as junk.
551 tc.transactf("ok", `store 1 -flags ($junk)`) // should retrain as non-junk.
552 tc.transactf("ok", `store 1 -flags (\seen)`) // should untrain completely.
553 tc.transactf("ok", `store 1 -flags (\answered)`)
554 tc.transactf("ok", `store 1 +flags (\answered)`)
555 tc.transactf("ok", `store 1 flags.silent (\seen \answered)`)
556 tc.transactf("ok", `store 1 -flags.silent (\answered)`)
557 tc.transactf("ok", `store 1 +flags.silent (\answered)`)
558 tc.transactf("bad", `store 1 flags (\badflag)`)
559 tc.transactf("ok", "noop")
561 tc.transactf("ok", "copy 1 Trash")
562 tc.transactf("ok", "copy 1 Trash")
563 tc.transactf("ok", "move 1 Trash")
565 tc.transactf("ok", "close")
566 tc.transactf("ok", "select Trash")
567 tc.transactf("ok", `store 1 flags (\deleted)`)
568 tc.transactf("ok", "expunge")
569 tc.transactf("ok", "noop")
571 tc.transactf("ok", `store 1 flags (\deleted)`)
572 tc.transactf("ok", "close")
573 tc.transactf("ok", "delete Trash")
576func TestMailbox(t *testing.T) {
579 tc.client.Login("mjl@mox.example", password0)
582 "e\u0301", // é but as e + acute, not unicode-normalized
593 for _, bad := range invalid {
594 tc.transactf("no", "select {%d+}\r\n%s", len(bad), bad)
598func TestMailboxDeleted(t *testing.T) {
601 tc.client.Login("mjl@mox.example", password0)
603 tc2 := startNoSwitchboard(t)
605 tc2.client.Login("mjl@mox.example", password0)
607 tc.client.Create("testbox")
608 tc2.client.Select("testbox")
609 tc.client.Delete("testbox")
611 // Now try to operate on testbox while it has been removed.
612 tc2.transactf("no", "check")
613 tc2.transactf("no", "expunge")
614 tc2.transactf("no", "uid expunge 1")
615 tc2.transactf("no", "search all")
616 tc2.transactf("no", "uid search all")
617 tc2.transactf("no", "fetch 1:* all")
618 tc2.transactf("no", "uid fetch 1 all")
619 tc2.transactf("no", "store 1 flags ()")
620 tc2.transactf("no", "uid store 1 flags ()")
621 tc2.transactf("bad", "copy 1 inbox") // msgseq 1 not available.
622 tc2.transactf("no", "uid copy 1 inbox")
623 tc2.transactf("bad", "move 1 inbox") // msgseq 1 not available.
624 tc2.transactf("no", "uid move 1 inbox")
626 tc2.transactf("ok", "unselect")
628 tc.client.Create("testbox")
629 tc2.client.Select("testbox")
630 tc.client.Delete("testbox")
631 tc2.transactf("ok", "close")
634func TestID(t *testing.T) {
637 tc.client.Login("mjl@mox.example", password0)
639 tc.transactf("ok", "id nil")
640 tc.xuntagged(imapclient.UntaggedID{"name": "mox", "version": moxvar.Version})
642 tc.transactf("ok", `id ("name" "mox" "version" "1.2.3" "other" "test" "test" nil)`)
643 tc.xuntagged(imapclient.UntaggedID{"name": "mox", "version": moxvar.Version})
645 tc.transactf("bad", `id ("name" "mox" "name" "mox")`) // Duplicate field.
648func TestSequence(t *testing.T) {
651 tc.client.Login("mjl@mox.example", password0)
652 tc.client.Select("inbox")
657 tc.transactf("ok", "uid fetch 1 all") // non-existing messages are OK for uids.
658 tc.transactf("ok", "uid fetch * all") // * is like uidnext, a non-existing message.
660 tc.client.Append("inbox", nil, nil, []byte(exampleMsg))
661 tc.client.Append("inbox", nil, nil, []byte(exampleMsg))
662 tc.transactf("ok", "fetch 2:1,1 uid") // We reorder 2:1 to 1:2, but we don't deduplicate numbers.
664 imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1)}},
665 imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(2)}},
666 imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1)}},
669 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.
670 tc.xuntagged(imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(2)}})
673// Test that a message that is expunged by another session can be read as long as a
674// reference is held by a session. New sessions do not see the expunged message.
675// todo: possibly implement the additional reference counting. so far it hasn't been worth the trouble.
676func DisabledTestReference(t *testing.T) {
679 tc.client.Login("mjl@mox.example", password0)
680 tc.client.Select("inbox")
681 tc.client.Append("inbox", nil, nil, []byte(exampleMsg))
683 tc2 := startNoSwitchboard(t)
685 tc2.client.Login("mjl@mox.example", password0)
686 tc2.client.Select("inbox")
688 tc.client.StoreFlagsSet("1", true, `\Deleted`)
691 tc3 := startNoSwitchboard(t)
693 tc3.client.Login("mjl@mox.example", password0)
694 tc3.transactf("ok", `list "" "inbox" return (status (messages))`)
695 tc3.xuntagged(imapclient.UntaggedList{Separator: '/', Mailbox: "Inbox"}, imapclient.UntaggedStatus{Mailbox: "Inbox", Attrs: map[string]int64{"MESSAGES": 0}})
697 tc2.transactf("ok", "fetch 1 rfc822.size")
698 tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchRFC822Size(len(exampleMsg))}})