1package imapserver
2
3import (
4 "context"
5 "crypto/ed25519"
6 cryptorand "crypto/rand"
7 "crypto/tls"
8 "crypto/x509"
9 "fmt"
10 "log/slog"
11 "math/big"
12 "net"
13 "os"
14 "path/filepath"
15 "reflect"
16 "strings"
17 "testing"
18 "time"
19
20 "golang.org/x/sys/unix"
21
22 "github.com/mjl-/bstore"
23
24 "github.com/mjl-/mox/imapclient"
25 "github.com/mjl-/mox/mlog"
26 "github.com/mjl-/mox/mox-"
27 "github.com/mjl-/mox/moxvar"
28 "github.com/mjl-/mox/store"
29 "slices"
30)
31
32var ctxbg = context.Background()
33var pkglog = mlog.New("imapserver", nil)
34
35func init() {
36 sanityChecks = true
37
38 // Don't slow down tests.
39 badClientDelay = 0
40 authFailDelay = 0
41
42 mox.Context = ctxbg
43}
44
45func ptr[T any](v T) *T {
46 return &v
47}
48
49func tocrlf(s string) string {
50 return strings.ReplaceAll(s, "\n", "\r\n")
51}
52
53// From ../rfc/3501:2589
54var exampleMsg = tocrlf(`Date: Mon, 7 Feb 1994 21:52:25 -0800 (PST)
55From: Fred Foobar <foobar@Blurdybloop.example>
56Subject: afternoon meeting
57To: mooch@owatagu.siam.edu.example
58Message-Id: <B27397-0100000@Blurdybloop.example>
59MIME-Version: 1.0
60Content-Type: TEXT/PLAIN; CHARSET=US-ASCII
61
62Hello Joe, do you think we can meet at 3:30 tomorrow?
63
64`)
65
66/*
67From ../rfc/2049:801
68
69Message structure:
70
71Message - multipart/mixed
72Part 1 - no content-type
73Part 2 - text/plain
74Part 3 - multipart/parallel
75Part 3.1 - audio/basic (base64)
76Part 3.2 - image/jpeg (base64, empty)
77Part 4 - text/enriched
78Part 5 - message/rfc822
79Part 5.1 - text/plain (quoted-printable)
80*/
81var nestedMessage = tocrlf(`MIME-Version: 1.0
82From: Nathaniel Borenstein <nsb@nsb.fv.com>
83To: Ned Freed <ned@innosoft.com>
84Date: Fri, 07 Oct 1994 16:15:05 -0700 (PDT)
85Subject: A multipart example
86Content-Type: multipart/mixed;
87 boundary=unique-boundary-1
88
89This is the preamble area of a multipart message.
90Mail readers that understand multipart format
91should ignore this preamble.
92
93If you are reading this text, you might want to
94consider changing to a mail reader that understands
95how to properly display multipart messages.
96
97--unique-boundary-1
98
99 ... Some text appears here ...
100
101[Note that the blank between the boundary and the start
102 of the text in this part means no header fields were
103 given and this is text in the US-ASCII character set.
104 It could have been done with explicit typing as in the
105 next part.]
106
107--unique-boundary-1
108Content-type: text/plain; charset=US-ASCII
109
110This could have been part of the previous part, but
111illustrates explicit versus implicit typing of body
112parts.
113
114--unique-boundary-1
115Content-Type: multipart/parallel; boundary=unique-boundary-2
116
117--unique-boundary-2
118Content-Type: audio/basic
119Content-Transfer-Encoding: base64
120
121aGVsbG8NCndvcmxkDQo=
122
123--unique-boundary-2
124Content-Type: image/jpeg
125Content-Transfer-Encoding: base64
126Content-Disposition: inline; filename=image.jpg
127
128
129--unique-boundary-2--
130
131--unique-boundary-1
132Content-type: text/enriched
133
134This is <bold><italic>enriched.</italic></bold>
135<smaller>as defined in RFC 1896</smaller>
136
137Isn't it
138<bigger><bigger>cool?</bigger></bigger>
139
140--unique-boundary-1
141Content-Type: message/rfc822
142Content-MD5: MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=
143Content-Language: en,de
144Content-Location: http://localhost
145
146From: info@mox.example
147To: mox <info@mox.example>
148Subject: (subject in US-ASCII)
149Content-Type: Text/plain; charset=ISO-8859-1
150Content-Transfer-Encoding: Quoted-printable
151
152 ... Additional text in ISO-8859-1 goes here ...
153
154--unique-boundary-1--
155`)
156
157func tcheck(t *testing.T, err error, msg string) {
158 t.Helper()
159 if err != nil {
160 t.Fatalf("%s: %s", msg, err)
161 }
162}
163
164func mustParseUntagged(s string) imapclient.Untagged {
165 r, err := imapclient.ParseUntagged(s + "\r\n")
166 if err != nil {
167 panic(err)
168 }
169 return r
170}
171
172func mustParseCode(s string) imapclient.Code {
173 r, err := imapclient.ParseCode(s)
174 if err != nil {
175 panic(err)
176 }
177 return r
178}
179
180func mockUIDValidity() func() {
181 orig := store.InitialUIDValidity
182 store.InitialUIDValidity = func() uint32 {
183 return 1
184 }
185 return func() {
186 store.InitialUIDValidity = orig
187 }
188}
189
190type testconn struct {
191 t *testing.T
192 conn net.Conn
193 client *imapclient.Conn
194 uidonly bool
195 done chan struct{}
196 serverConn net.Conn
197 account *store.Account
198 switchStop func()
199 clientPanic bool
200
201 // Result of last command.
202 lastResponse imapclient.Response
203 lastErr error
204}
205
206func (tc *testconn) check(err error, msg string) {
207 tc.t.Helper()
208 if err != nil {
209 tc.t.Fatalf("%s: %s", msg, err)
210 }
211}
212
213func (tc *testconn) last(resp imapclient.Response, err error) {
214 tc.lastResponse = resp
215 tc.lastErr = err
216}
217
218func (tc *testconn) xcode(c imapclient.Code) {
219 tc.t.Helper()
220 if !reflect.DeepEqual(tc.lastResponse.Code, c) {
221 tc.t.Fatalf("got last code %#v, expected %#v", tc.lastResponse.Code, c)
222 }
223}
224
225func (tc *testconn) xcodeWord(s string) {
226 tc.t.Helper()
227 tc.xcode(imapclient.CodeWord(s))
228}
229
230func (tc *testconn) xuntagged(exps ...imapclient.Untagged) {
231 tc.t.Helper()
232 tc.xuntaggedOpt(true, exps...)
233}
234
235func (tc *testconn) xuntaggedOpt(all bool, exps ...imapclient.Untagged) {
236 tc.t.Helper()
237 last := slices.Clone(tc.lastResponse.Untagged)
238 var mismatch any
239next:
240 for ei, exp := range exps {
241 for i, l := range last {
242 if reflect.TypeOf(l) != reflect.TypeOf(exp) {
243 continue
244 }
245 if !reflect.DeepEqual(l, exp) {
246 mismatch = l
247 continue
248 }
249 copy(last[i:], last[i+1:])
250 last = last[:len(last)-1]
251 continue next
252 }
253 if mismatch != nil {
254 tc.t.Fatalf("untagged data mismatch, got:\n\t%T %#v\nexpected:\n\t%T %#v", mismatch, mismatch, exp, exp)
255 }
256 var next string
257 if len(tc.lastResponse.Untagged) > 0 {
258 next = fmt.Sprintf(", next:\n%#v", tc.lastResponse.Untagged[0])
259 }
260 tc.t.Fatalf("did not find untagged response:\n%#v %T (%d)\nin %v%s", exp, exp, ei, tc.lastResponse.Untagged, next)
261 }
262 if len(last) > 0 && all {
263 tc.t.Fatalf("leftover untagged responses %v", last)
264 }
265}
266
267func tuntagged(t *testing.T, got imapclient.Untagged, dst any) {
268 t.Helper()
269 gotv := reflect.ValueOf(got)
270 dstv := reflect.ValueOf(dst)
271 if gotv.Type() != dstv.Type().Elem() {
272 t.Fatalf("got %#v, expected %#v", got, dstv.Elem().Interface())
273 }
274 dstv.Elem().Set(gotv)
275}
276
277func (tc *testconn) xnountagged() {
278 tc.t.Helper()
279 if len(tc.lastResponse.Untagged) != 0 {
280 tc.t.Fatalf("got %v untagged, expected 0", tc.lastResponse.Untagged)
281 }
282}
283
284func (tc *testconn) readuntagged(exps ...imapclient.Untagged) {
285 tc.t.Helper()
286 for i, exp := range exps {
287 tc.conn.SetReadDeadline(time.Now().Add(3 * time.Second))
288 v, err := tc.client.ReadUntagged()
289 tcheck(tc.t, err, "reading untagged")
290 if !reflect.DeepEqual(v, exp) {
291 tc.t.Fatalf("got %#v, expected %#v, response %d/%d", v, exp, i+1, len(exps))
292 }
293 }
294}
295
296func (tc *testconn) transactf(status, format string, args ...any) {
297 tc.t.Helper()
298 tc.cmdf("", format, args...)
299 tc.response(status)
300}
301
302func (tc *testconn) response(status string) {
303 tc.t.Helper()
304 tc.lastResponse, tc.lastErr = tc.client.ReadResponse()
305 if tc.lastErr != nil {
306 if resp, ok := tc.lastErr.(imapclient.Response); ok {
307 if !reflect.DeepEqual(resp, tc.lastResponse) {
308 tc.t.Fatalf("response error %#v != returned response %#v", tc.lastErr, tc.lastResponse)
309 }
310 } else {
311 tcheck(tc.t, tc.lastErr, "read imap response")
312 }
313 }
314 if strings.ToUpper(status) != string(tc.lastResponse.Status) {
315 tc.t.Fatalf("got status %q, expected %q", tc.lastResponse.Status, status)
316 }
317}
318
319func (tc *testconn) cmdf(tag, format string, args ...any) {
320 tc.t.Helper()
321 err := tc.client.WriteCommandf(tag, format, args...)
322 tcheck(tc.t, err, "writing imap command")
323}
324
325func (tc *testconn) readstatus(status string) {
326 tc.t.Helper()
327 tc.response(status)
328}
329
330func (tc *testconn) readprefixline(pre string) {
331 tc.t.Helper()
332 line, err := tc.client.Readline()
333 tcheck(tc.t, err, "read line")
334 if !strings.HasPrefix(line, pre) {
335 tc.t.Fatalf("expected prefix %q, got %q", pre, line)
336 }
337}
338
339func (tc *testconn) writelinef(format string, args ...any) {
340 tc.t.Helper()
341 err := tc.client.Writelinef(format, args...)
342 tcheck(tc.t, err, "write line")
343}
344
345// wait at most 1 second for server to quit.
346func (tc *testconn) waitDone() {
347 tc.t.Helper()
348 t := time.NewTimer(time.Second)
349 select {
350 case <-tc.done:
351 t.Stop()
352 case <-t.C:
353 tc.t.Fatalf("server not done within 1s")
354 }
355}
356
357func (tc *testconn) login(username, password string) {
358 tc.client.Login(username, password)
359 if tc.uidonly {
360 tc.transactf("ok", "enable uidonly")
361 }
362}
363
364// untaggedFetch returns an imapclient.UntaggedFetch or
365// imapclient.UntaggedUIDFetch, depending on whether uidonly is enabled for the
366// connection.
367func (tc *testconn) untaggedFetch(seq, uid uint32, attrs ...imapclient.FetchAttr) any {
368 if tc.uidonly {
369 return imapclient.UntaggedUIDFetch{UID: uid, Attrs: attrs}
370 }
371 attrs = append([]imapclient.FetchAttr{imapclient.FetchUID(uid)}, attrs...)
372 return imapclient.UntaggedFetch{Seq: seq, Attrs: attrs}
373}
374
375// like untaggedFetch, but with explicit UID fetch attribute in case of uidonly.
376func (tc *testconn) untaggedFetchUID(seq, uid uint32, attrs ...imapclient.FetchAttr) any {
377 attrs = append([]imapclient.FetchAttr{imapclient.FetchUID(uid)}, attrs...)
378 if tc.uidonly {
379 return imapclient.UntaggedUIDFetch{UID: uid, Attrs: attrs}
380 }
381 return imapclient.UntaggedFetch{Seq: seq, Attrs: attrs}
382}
383
384func (tc *testconn) close() {
385 tc.close0(true)
386}
387
388func (tc *testconn) closeNoWait() {
389 tc.close0(false)
390}
391
392func (tc *testconn) close0(waitclose bool) {
393 defer func() {
394 if unhandledPanics.Swap(0) > 0 {
395 tc.t.Fatalf("unhandled panic in server")
396 }
397 }()
398
399 if tc.account == nil {
400 // Already closed, we are not strict about closing multiple times.
401 return
402 }
403 if tc.client != nil {
404 tc.clientPanic = false // Ignore errors writing to TLS connection the server also closed.
405 tc.client.Close()
406 }
407 err := tc.account.Close()
408 tc.check(err, "close account")
409 if waitclose {
410 tc.account.WaitClosed()
411 }
412 tc.account = nil
413 tc.serverConn.Close()
414 tc.waitDone()
415 if tc.switchStop != nil {
416 tc.switchStop()
417 }
418}
419
420func xparseNumSet(s string) imapclient.NumSet {
421 ns, err := imapclient.ParseNumSet(s)
422 if err != nil {
423 panic(fmt.Sprintf("parsing numset %s: %s", s, err))
424 }
425 return ns
426}
427
428func xparseUIDRange(s string) imapclient.NumRange {
429 nr, err := imapclient.ParseUIDRange(s)
430 if err != nil {
431 panic(fmt.Sprintf("parsing uid range %s: %s", s, err))
432 }
433 return nr
434}
435
436func makeAppend(msg string) imapclient.Append {
437 return imapclient.Append{Size: int64(len(msg)), Data: strings.NewReader(msg)}
438}
439
440func makeAppendTime(msg string, tm time.Time) imapclient.Append {
441 return imapclient.Append{Received: &tm, Size: int64(len(msg)), Data: strings.NewReader(msg)}
442}
443
444var connCounter int64
445
446func start(t *testing.T, uidonly bool) *testconn {
447 return startArgs(t, uidonly, true, false, true, true, "mjl")
448}
449
450func startNoSwitchboard(t *testing.T, uidonly bool) *testconn {
451 return startArgs(t, uidonly, false, false, true, false, "mjl")
452}
453
454const password0 = "te\u0301st \u00a0\u2002\u200a" // NFD and various unicode spaces.
455const password1 = "tést " // PRECIS normalized, with NFC.
456
457func startArgs(t *testing.T, uidonly, first, immediateTLS bool, allowLoginWithoutTLS, setPassword bool, accname string) *testconn {
458 return startArgsMore(t, uidonly, first, immediateTLS, nil, nil, allowLoginWithoutTLS, setPassword, accname, nil)
459}
460
461// namedConn wraps a conn so it can return a RemoteAddr with a non-empty name.
462// The TLS resumption test needs a non-empty name, but on BSDs, the unix domain
463// socket pair has an empty peer name.
464type namedConn struct {
465 net.Conn
466}
467
468func (c namedConn) RemoteAddr() net.Addr {
469 return &net.TCPAddr{IP: net.ParseIP("127.0.0.10"), Port: 1234}
470}
471
472// todo: the parameters and usage are too much now. change to scheme similar to smtpserver, with params in a struct, and a separate method for init and making a connection.
473func startArgsMore(t *testing.T, uidonly, first, immediateTLS bool, serverConfig, clientConfig *tls.Config, allowLoginWithoutTLS, setPassword bool, accname string, afterInit func() error) *testconn {
474 limitersInit() // Reset rate limiters.
475
476 switchStop := func() {}
477 if first {
478 mox.ConfigStaticPath = filepath.FromSlash("../testdata/imap/mox.conf")
479 mox.MustLoadConfig(true, false)
480 store.Close() // May not be open, we ignore error.
481 os.RemoveAll("../testdata/imap/data")
482 err := store.Init(ctxbg)
483 tcheck(t, err, "store init")
484 switchStop = store.Switchboard()
485 }
486
487 acc, err := store.OpenAccount(pkglog, accname, false)
488 tcheck(t, err, "open account")
489 if setPassword {
490 err = acc.SetPassword(pkglog, password0)
491 tcheck(t, err, "set password")
492 }
493 if first {
494 // Add a deleted mailbox, may excercise some code paths.
495 err = acc.DB.Write(ctxbg, func(tx *bstore.Tx) error {
496 // todo: add a message to inbox and remove it again. need to change all uids in the tests.
497 // todo: add tests for operating on an expunged mailbox. it should say it doesn't exist.
498
499 mb, _, _, _, err := acc.MailboxCreate(tx, "expungebox", store.SpecialUse{})
500 if err != nil {
501 return fmt.Errorf("create mailbox: %v", err)
502 }
503 if _, _, err := acc.MailboxDelete(ctxbg, pkglog, tx, &mb); err != nil {
504 return fmt.Errorf("delete mailbox: %v", err)
505 }
506 return nil
507 })
508 tcheck(t, err, "add expunged mailbox")
509 }
510
511 if afterInit != nil {
512 err := afterInit()
513 tcheck(t, err, "after init")
514 }
515
516 // We get actual sockets for their buffering behaviour. net.Pipe is synchronous,
517 // and the implementation of the compress extension can write a sync message to an
518 // imap client when that client isn't reading but is trying to write. In the real
519 // world, network buffer will take up those few bytes, so assume the buffer in the
520 // test too.
521 fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM, 0)
522 tcheck(t, err, "socketpair")
523 xfdconn := func(fd int, name string) net.Conn {
524 f := os.NewFile(uintptr(fd), name)
525 fc, err := net.FileConn(f)
526 tcheck(t, err, "fileconn")
527 err = f.Close()
528 tcheck(t, err, "close file for conn")
529
530 // Small read/write buffers, for detecting closed/broken connections quickly.
531 uc := fc.(*net.UnixConn)
532 err = uc.SetReadBuffer(512)
533 tcheck(t, err, "set read buffer")
534 uc.SetWriteBuffer(512)
535 tcheck(t, err, "set write buffer")
536
537 return namedConn{uc}
538 }
539 serverConn := xfdconn(fds[0], "server")
540 clientConn := xfdconn(fds[1], "client")
541
542 if serverConfig == nil {
543 serverConfig = &tls.Config{
544 Certificates: []tls.Certificate{fakeCert(t, false)},
545 }
546 }
547 if immediateTLS {
548 if clientConfig == nil {
549 clientConfig = &tls.Config{InsecureSkipVerify: true}
550 }
551 clientConn = tls.Client(clientConn, clientConfig)
552 }
553
554 done := make(chan struct{})
555 connCounter += 2
556 cid := connCounter - 1
557 go func() {
558 const viaHTTPS = false
559 serve("test", cid, serverConfig, serverConn, immediateTLS, allowLoginWithoutTLS, viaHTTPS, "")
560 close(done)
561 }()
562 var tc *testconn
563 var opts imapclient.Opts
564 opts = imapclient.Opts{
565 Logger: slog.Default().With("cid", connCounter),
566 Error: func(err error) {
567 if tc.clientPanic {
568 panic(err)
569 } else {
570 opts.Logger.Error("imapclient error", "err", err)
571 }
572 },
573 }
574 client, _ := imapclient.New(clientConn, &opts)
575 tc = &testconn{t: t, conn: clientConn, client: client, uidonly: uidonly, done: done, serverConn: serverConn, account: acc, clientPanic: true}
576 if first {
577 tc.switchStop = switchStop
578 }
579 return tc
580}
581
582func fakeCert(t *testing.T, randomkey bool) tls.Certificate {
583 seed := make([]byte, ed25519.SeedSize)
584 if randomkey {
585 cryptorand.Read(seed)
586 }
587 privKey := ed25519.NewKeyFromSeed(seed) // Fake key, don't use this for real!
588 template := &x509.Certificate{
589 SerialNumber: big.NewInt(1), // Required field...
590 // Valid period is needed to get session resumption enabled.
591 NotBefore: time.Now().Add(-time.Minute),
592 NotAfter: time.Now().Add(time.Hour),
593 }
594 localCertBuf, err := x509.CreateCertificate(cryptorand.Reader, template, template, privKey.Public(), privKey)
595 if err != nil {
596 t.Fatalf("making certificate: %s", err)
597 }
598 cert, err := x509.ParseCertificate(localCertBuf)
599 if err != nil {
600 t.Fatalf("parsing generated certificate: %s", err)
601 }
602 c := tls.Certificate{
603 Certificate: [][]byte{localCertBuf},
604 PrivateKey: privKey,
605 Leaf: cert,
606 }
607 return c
608}
609
610func TestLogin(t *testing.T) {
611 tc := start(t, false)
612 defer tc.close()
613
614 tc.transactf("bad", "login too many args")
615 tc.transactf("bad", "login") // no args
616 tc.transactf("no", "login mjl@mox.example badpass")
617 tc.transactf("no", `login mjl "%s"`, password0) // must use email, not account
618 tc.transactf("no", "login mjl@mox.example test")
619 tc.transactf("no", "login mjl@mox.example testtesttest")
620 tc.transactf("no", `login "mjl@mox.example" "testtesttest"`)
621 tc.transactf("no", "login \"m\xf8x@mox.example\" \"testtesttest\"")
622 tc.transactf("ok", `login mjl@mox.example "%s"`, password0)
623 tc.close()
624
625 tc = start(t, false)
626 tc.transactf("ok", `login "mjl@mox.example" "%s"`, password0)
627 tc.close()
628
629 tc = start(t, false)
630 tc.transactf("ok", `login "\"\"@mox.example" "%s"`, password0)
631 defer tc.close()
632
633 tc.transactf("bad", "logout badarg")
634 tc.transactf("ok", "logout")
635}
636
637// Test that commands don't work in the states they are not supposed to.
638func TestState(t *testing.T) {
639 tc := start(t, false)
640
641 notAuthenticated := []string{"starttls", "authenticate", "login"}
642 authenticatedOrSelected := []string{"enable", "select", "examine", "create", "delete", "rename", "subscribe", "unsubscribe", "list", "namespace", "status", "append", "idle", "lsub"}
643 selected := []string{"close", "unselect", "expunge", "search", "fetch", "store", "copy", "move", "uid expunge"}
644
645 // Always allowed.
646 tc.transactf("ok", "capability")
647 tc.transactf("ok", "noop")
648 tc.transactf("ok", "logout")
649 tc.close()
650 tc = start(t, false)
651 defer tc.close()
652
653 // Not authenticated, lots of commands not allowed.
654 for _, cmd := range slices.Concat(authenticatedOrSelected, selected) {
655 tc.transactf("no", "%s", cmd)
656 }
657
658 // Some commands not allowed when authenticated.
659 tc.transactf("ok", `login mjl@mox.example "%s"`, password0)
660 for _, cmd := range slices.Concat(notAuthenticated, selected) {
661 tc.transactf("no", "%s", cmd)
662 }
663
664 tc.transactf("bad", "boguscommand")
665}
666
667func TestNonIMAP(t *testing.T) {
668 tc := start(t, false)
669 defer tc.close()
670
671 // imap greeting has already been read, we sidestep the imapclient.
672 _, err := fmt.Fprintf(tc.conn, "bogus\r\n")
673 tc.check(err, "write bogus command")
674 tc.readprefixline("* BYE ")
675 if _, err := tc.conn.Read(make([]byte, 1)); err == nil {
676 t.Fatalf("connection not closed after initial bad command")
677 }
678}
679
680func TestLiterals(t *testing.T) {
681 tc := start(t, false)
682 defer tc.close()
683
684 tc.login("mjl@mox.example", password0)
685 tc.client.Create("tmpbox", nil)
686
687 tc.transactf("ok", "rename {6+}\r\ntmpbox {7+}\r\nntmpbox")
688
689 from := "ntmpbox"
690 to := "tmpbox"
691 tc.client.LastTagSet("xtag")
692 fmt.Fprint(tc.client, "xtag rename ")
693 tc.client.WriteSyncLiteral(from)
694 fmt.Fprint(tc.client, " ")
695 tc.client.WriteSyncLiteral(to)
696 fmt.Fprint(tc.client, "\r\n")
697 tc.lastResponse, tc.lastErr = tc.client.ReadResponse()
698 if tc.lastResponse.Status != "OK" {
699 tc.t.Fatalf(`got %q, expected "OK"`, tc.lastResponse.Status)
700 }
701}
702
703// Test longer scenario with login, lists, subscribes, status, selects, etc.
704func TestScenario(t *testing.T) {
705 testScenario(t, false)
706}
707
708func TestScenarioUIDOnly(t *testing.T) {
709 testScenario(t, true)
710}
711
712func testScenario(t *testing.T, uidonly bool) {
713 tc := start(t, uidonly)
714 defer tc.close()
715 tc.login("mjl@mox.example", password0)
716
717 tc.transactf("bad", " missingcommand")
718
719 tc.transactf("ok", "examine inbox")
720 tc.transactf("ok", "unselect")
721
722 tc.transactf("ok", "examine inbox")
723 tc.transactf("ok", "close")
724
725 tc.transactf("ok", "select inbox")
726 tc.transactf("ok", "close")
727
728 tc.transactf("ok", "select inbox")
729 tc.transactf("ok", "expunge")
730 tc.transactf("ok", "check")
731
732 tc.transactf("ok", "subscribe inbox")
733 tc.transactf("ok", "unsubscribe inbox")
734 tc.transactf("ok", "subscribe inbox")
735
736 tc.transactf("ok", `lsub "" "*"`)
737
738 tc.transactf("ok", `list "" ""`)
739 tc.transactf("ok", `namespace`)
740
741 tc.transactf("ok", "enable utf8=accept")
742 tc.transactf("ok", "enable imap4rev2 utf8=accept")
743
744 tc.transactf("no", "create inbox")
745 tc.transactf("ok", "create tmpbox")
746 tc.transactf("ok", "rename tmpbox ntmpbox")
747 tc.transactf("ok", "delete ntmpbox")
748
749 tc.transactf("ok", "status inbox (uidnext messages uidvalidity deleted size unseen recent)")
750
751 tc.transactf("ok", "append inbox (\\seen) {%d+}\r\n%s", len(exampleMsg), exampleMsg)
752 tc.transactf("no", "append bogus () {%d}", len(exampleMsg))
753 tc.cmdf("", "append inbox () {%d}", len(exampleMsg))
754 tc.readprefixline("+ ")
755 _, err := tc.conn.Write([]byte(exampleMsg + "\r\n"))
756 tc.check(err, "write message")
757 tc.response("ok")
758
759 tc.transactf("ok", "uid fetch 1 all")
760 tc.transactf("ok", "uid fetch 1 body")
761 tc.transactf("ok", "uid fetch 1 binary[]")
762
763 tc.transactf("ok", `uid store 1 flags (\seen \answered)`)
764 tc.transactf("ok", `uid store 1 +flags ($junk)`) // should train as junk.
765 tc.transactf("ok", `uid store 1 -flags ($junk)`) // should retrain as non-junk.
766 tc.transactf("ok", `uid store 1 -flags (\seen)`) // should untrain completely.
767 tc.transactf("ok", `uid store 1 -flags (\answered)`)
768 tc.transactf("ok", `uid store 1 +flags (\answered)`)
769 tc.transactf("ok", `uid store 1 flags.silent (\seen \answered)`)
770 tc.transactf("ok", `uid store 1 -flags.silent (\answered)`)
771 tc.transactf("ok", `uid store 1 +flags.silent (\answered)`)
772 tc.transactf("bad", `uid store 1 flags (\badflag)`)
773 tc.transactf("ok", "noop")
774
775 tc.transactf("ok", "uid copy 1 Trash")
776 tc.transactf("ok", "uid copy 1 Trash")
777 tc.transactf("ok", "uid move 1 Trash")
778
779 tc.transactf("ok", "close")
780 tc.transactf("ok", "select Trash")
781 tc.transactf("ok", `uid store 1 flags (\deleted)`)
782 tc.transactf("ok", "expunge")
783 tc.transactf("ok", "noop")
784
785 tc.transactf("ok", `uid store 1 flags (\deleted)`)
786 tc.transactf("ok", "close")
787 tc.transactf("ok", "delete Trash")
788
789 if uidonly {
790 return
791 }
792
793 tc.transactf("ok", "create Trash")
794 tc.transactf("ok", "append inbox (\\seen) {%d+}\r\n%s", len(exampleMsg), exampleMsg)
795 tc.transactf("ok", "select inbox")
796
797 tc.transactf("ok", "fetch 1 all")
798 tc.transactf("ok", "fetch 1 body")
799 tc.transactf("ok", "fetch 1 binary[]")
800
801 tc.transactf("ok", `store 1 flags (\seen \answered)`)
802 tc.transactf("ok", `store 1 +flags ($junk)`) // should train as junk.
803 tc.transactf("ok", `store 1 -flags ($junk)`) // should retrain as non-junk.
804 tc.transactf("ok", `store 1 -flags (\seen)`) // should untrain completely.
805 tc.transactf("ok", `store 1 -flags (\answered)`)
806 tc.transactf("ok", `store 1 +flags (\answered)`)
807 tc.transactf("ok", `store 1 flags.silent (\seen \answered)`)
808 tc.transactf("ok", `store 1 -flags.silent (\answered)`)
809 tc.transactf("ok", `store 1 +flags.silent (\answered)`)
810 tc.transactf("bad", `store 1 flags (\badflag)`)
811 tc.transactf("ok", "noop")
812
813 tc.transactf("ok", "copy 1 Trash")
814 tc.transactf("ok", "copy 1 Trash")
815 tc.transactf("ok", "move 1 Trash")
816
817 tc.transactf("ok", "close")
818 tc.transactf("ok", "select Trash")
819 tc.transactf("ok", `store 1 flags (\deleted)`)
820 tc.transactf("ok", "expunge")
821 tc.transactf("ok", "noop")
822
823 tc.transactf("ok", `store 1 flags (\deleted)`)
824 tc.transactf("ok", "close")
825 tc.transactf("ok", "delete Trash")
826}
827
828func TestMailbox(t *testing.T) {
829 tc := start(t, false)
830 defer tc.close()
831 tc.login("mjl@mox.example", password0)
832
833 invalid := []string{
834 "e\u0301", // é but as e + acute, not unicode-normalized
835 "/leadingslash",
836 "a//b",
837 "Inbox/",
838 "\x01",
839 " ",
840 "\x7f",
841 "\x80",
842 "\u2028",
843 "\u2029",
844 }
845 for _, bad := range invalid {
846 tc.transactf("no", "select {%d+}\r\n%s", len(bad), bad)
847 }
848}
849
850func TestMailboxDeleted(t *testing.T) {
851 tc := start(t, false)
852 defer tc.close()
853
854 tc2 := startNoSwitchboard(t, false)
855 defer tc2.closeNoWait()
856
857 tc.login("mjl@mox.example", password0)
858 tc2.login("mjl@mox.example", password0)
859
860 tc.client.Create("testbox", nil)
861 tc2.client.Select("testbox")
862 tc.client.Delete("testbox")
863
864 // Now try to operate on testbox while it has been removed.
865 tc2.transactf("no", "check")
866 tc2.transactf("no", "expunge")
867 tc2.transactf("no", "uid expunge 1")
868 tc2.transactf("no", "search all")
869 tc2.transactf("no", "uid search all")
870 tc2.transactf("no", "fetch 1:* all")
871 tc2.transactf("no", "uid fetch 1 all")
872 tc2.transactf("no", "store 1 flags ()")
873 tc2.transactf("no", "uid store 1 flags ()")
874 tc2.transactf("no", "copy 1 inbox")
875 tc2.transactf("no", "uid copy 1 inbox")
876 tc2.transactf("no", "move 1 inbox")
877 tc2.transactf("no", "uid move 1 inbox")
878
879 tc2.transactf("ok", "unselect")
880
881 tc.client.Create("testbox", nil)
882 tc2.client.Select("testbox")
883 tc.client.Delete("testbox")
884 tc2.transactf("ok", "close")
885}
886
887func TestID(t *testing.T) {
888 tc := start(t, false)
889 defer tc.close()
890 tc.login("mjl@mox.example", password0)
891
892 tc.transactf("ok", "id nil")
893 tc.xuntagged(imapclient.UntaggedID{"name": "mox", "version": moxvar.Version})
894
895 tc.transactf("ok", `id ("name" "mox" "version" "1.2.3" "other" "test" "test" nil)`)
896 tc.xuntagged(imapclient.UntaggedID{"name": "mox", "version": moxvar.Version})
897
898 tc.transactf("bad", `id ("name" "mox" "name" "mox")`) // Duplicate field.
899}
900
901func TestSequence(t *testing.T) {
902 testSequence(t, false)
903}
904
905func TestSequenceUIDOnly(t *testing.T) {
906 testSequence(t, true)
907}
908
909func testSequence(t *testing.T, uidonly bool) {
910 tc := start(t, uidonly)
911 defer tc.close()
912 tc.login("mjl@mox.example", password0)
913 tc.client.Select("inbox")
914
915 tc.transactf("bad", "fetch * all") // ../rfc/9051:7018
916 tc.transactf("bad", "fetch 1:* all")
917 tc.transactf("bad", "fetch 1:2 all")
918 tc.transactf("bad", "fetch 1 all") // ../rfc/9051:7018
919
920 tc.transactf("ok", "uid fetch 1 all") // non-existing messages are OK for uids.
921 tc.transactf("ok", "uid fetch * all") // * is like uidnext, a non-existing message.
922
923 tc.transactf("ok", "uid search return (save) all") // Empty result.
924 tc.transactf("ok", "uid fetch $ uid")
925 tc.xuntagged()
926
927 tc.client.Append("inbox", makeAppend(exampleMsg))
928 tc.client.Append("inbox", makeAppend(exampleMsg))
929 if !uidonly {
930 tc.transactf("ok", "fetch 2:1,1 uid") // We reorder 2:1 to 1:2, and we deduplicate numbers.
931 tc.xuntagged(
932 tc.untaggedFetch(1, 1),
933 tc.untaggedFetch(2, 2),
934 )
935
936 tc.transactf("bad", "fetch 1:3 all")
937 }
938
939 tc.transactf("ok", "uid fetch * flags")
940 tc.xuntagged(tc.untaggedFetch(2, 2, imapclient.FetchFlags(nil)))
941
942 tc.transactf("ok", "uid fetch 3:* flags") // Because * is the last message, which is 2, the range becomes 3:2, which matches the last message.
943 tc.xuntagged(tc.untaggedFetch(2, 2, imapclient.FetchFlags(nil)))
944
945 tc.transactf("ok", "uid fetch *:3 flags")
946 tc.xuntagged(tc.untaggedFetch(2, 2, imapclient.FetchFlags(nil)))
947
948 tc.transactf("ok", "uid search return (save) all") // Empty result.
949 tc.transactf("ok", "uid fetch $ flags")
950 tc.xuntagged(
951 tc.untaggedFetch(1, 1, imapclient.FetchFlags(nil)),
952 tc.untaggedFetch(2, 2, imapclient.FetchFlags(nil)),
953 )
954}
955
956// Test that a message that is expunged by another session can be read as long as a
957// reference is held by a session. New sessions do not see the expunged message.
958func TestReference(t *testing.T) {
959 tc := start(t, false)
960 defer tc.close()
961 tc.login("mjl@mox.example", password0)
962 tc.client.Select("inbox")
963 tc.client.Append("inbox", makeAppend(exampleMsg))
964
965 tc2 := startNoSwitchboard(t, false)
966 defer tc2.closeNoWait()
967 tc2.login("mjl@mox.example", password0)
968 tc2.client.Select("inbox")
969
970 tc.client.MSNStoreFlagsSet("1", true, `\Deleted`)
971 tc.client.Expunge()
972
973 tc3 := startNoSwitchboard(t, false)
974 defer tc3.closeNoWait()
975 tc3.login("mjl@mox.example", password0)
976 tc3.transactf("ok", `list "" "inbox" return (status (messages))`)
977 tc3.xuntagged(
978 mustParseUntagged(`* LIST () "/" Inbox`),
979 imapclient.UntaggedStatus{Mailbox: "Inbox", Attrs: map[imapclient.StatusAttr]int64{imapclient.StatusMessages: 0}},
980 )
981
982 tc2.transactf("ok", "fetch 1 rfc822.size")
983 tc2.xuntagged(tc.untaggedFetch(1, 1, imapclient.FetchRFC822Size(len(exampleMsg))))
984}
985