6 cryptorand "crypto/rand"
19 "golang.org/x/sys/unix"
21 "github.com/mjl-/bstore"
23 "github.com/mjl-/mox/imapclient"
24 "github.com/mjl-/mox/mlog"
25 "github.com/mjl-/mox/mox-"
26 "github.com/mjl-/mox/moxvar"
27 "github.com/mjl-/mox/store"
31var ctxbg = context.Background()
32var pkglog = mlog.New("imapserver", nil)
37 // Don't slow down tests.
44func tocrlf(s string) string {
45 return strings.ReplaceAll(s, "\n", "\r\n")
49var exampleMsg = tocrlf(`Date: Mon, 7 Feb 1994 21:52:25 -0800 (PST)
50From: Fred Foobar <foobar@Blurdybloop.example>
51Subject: afternoon meeting
52To: mooch@owatagu.siam.edu.example
53Message-Id: <B27397-0100000@Blurdybloop.example>
55Content-Type: TEXT/PLAIN; CHARSET=US-ASCII
57Hello Joe, do you think we can meet at 3:30 tomorrow?
66Message - multipart/mixed
67Part 1 - no content-type
69Part 3 - multipart/parallel
70Part 3.1 - audio/basic (base64)
71Part 3.2 - image/jpeg (base64, empty)
73Part 5 - message/rfc822
74Part 5.1 - text/plain (quoted-printable)
76var nestedMessage = tocrlf(`MIME-Version: 1.0
77From: Nathaniel Borenstein <nsb@nsb.fv.com>
78To: Ned Freed <ned@innosoft.com>
79Date: Fri, 07 Oct 1994 16:15:05 -0700 (PDT)
80Subject: A multipart example
81Content-Type: multipart/mixed;
82 boundary=unique-boundary-1
84This is the preamble area of a multipart message.
85Mail readers that understand multipart format
86should ignore this preamble.
88If you are reading this text, you might want to
89consider changing to a mail reader that understands
90how to properly display multipart messages.
94 ... Some text appears here ...
96[Note that the blank between the boundary and the start
97 of the text in this part means no header fields were
98 given and this is text in the US-ASCII character set.
99 It could have been done with explicit typing as in the
103Content-type: text/plain; charset=US-ASCII
105This could have been part of the previous part, but
106illustrates explicit versus implicit typing of body
110Content-Type: multipart/parallel; boundary=unique-boundary-2
113Content-Type: audio/basic
114Content-Transfer-Encoding: base64
119Content-Type: image/jpeg
120Content-Transfer-Encoding: base64
126Content-type: text/enriched
128This is <bold><italic>enriched.</italic></bold>
129<smaller>as defined in RFC 1896</smaller>
132<bigger><bigger>cool?</bigger></bigger>
135Content-Type: message/rfc822
137From: info@mox.example
138To: mox <info@mox.example>
139Subject: (subject in US-ASCII)
140Content-Type: Text/plain; charset=ISO-8859-1
141Content-Transfer-Encoding: Quoted-printable
143 ... Additional text in ISO-8859-1 goes here ...
148func tcheck(t *testing.T, err error, msg string) {
151 t.Fatalf("%s: %s", msg, err)
155func mockUIDValidity() func() {
156 orig := store.InitialUIDValidity
157 store.InitialUIDValidity = func() uint32 {
161 store.InitialUIDValidity = orig
165type testconn struct {
168 client *imapclient.Conn
171 account *store.Account
174 // Result of last command.
175 lastUntagged []imapclient.Untagged
176 lastResult imapclient.Result
180func (tc *testconn) check(err error, msg string) {
183 tc.t.Fatalf("%s: %s", msg, err)
187func (tc *testconn) last(l []imapclient.Untagged, r imapclient.Result, err error) {
193func (tc *testconn) xcode(s string) {
195 if tc.lastResult.Code != s {
196 tc.t.Fatalf("got last code %q, expected %q", tc.lastResult.Code, s)
200func (tc *testconn) xcodeArg(v any) {
202 if !reflect.DeepEqual(tc.lastResult.CodeArg, v) {
203 tc.t.Fatalf("got last code argument %v, expected %v", tc.lastResult.CodeArg, v)
207func (tc *testconn) xuntagged(exps ...imapclient.Untagged) {
209 tc.xuntaggedOpt(true, exps...)
212func (tc *testconn) xuntaggedOpt(all bool, exps ...imapclient.Untagged) {
214 last := slices.Clone(tc.lastUntagged)
217 for ei, exp := range exps {
218 for i, l := range last {
219 if reflect.TypeOf(l) != reflect.TypeOf(exp) {
222 if !reflect.DeepEqual(l, exp) {
226 copy(last[i:], last[i+1:])
227 last = last[:len(last)-1]
231 tc.t.Fatalf("untagged data mismatch, got:\n\t%T %#v\nexpected:\n\t%T %#v", mismatch, mismatch, exp, exp)
234 if len(tc.lastUntagged) > 0 {
235 next = fmt.Sprintf(", next:\n%#v", tc.lastUntagged[0])
237 tc.t.Fatalf("did not find untagged response:\n%#v %T (%d)\nin %v%s", exp, exp, ei, tc.lastUntagged, next)
239 if len(last) > 0 && all {
240 tc.t.Fatalf("leftover untagged responses %v", last)
244func tuntagged(t *testing.T, got imapclient.Untagged, dst any) {
246 gotv := reflect.ValueOf(got)
247 dstv := reflect.ValueOf(dst)
248 if gotv.Type() != dstv.Type().Elem() {
249 t.Fatalf("got %v, expected %v", gotv.Type(), dstv.Type().Elem())
251 dstv.Elem().Set(gotv)
254func (tc *testconn) xnountagged() {
256 if len(tc.lastUntagged) != 0 {
257 tc.t.Fatalf("got %v untagged, expected 0", tc.lastUntagged)
261func (tc *testconn) transactf(status, format string, args ...any) {
263 tc.cmdf("", format, args...)
267func (tc *testconn) response(status string) {
269 tc.lastUntagged, tc.lastResult, tc.lastErr = tc.client.Response()
270 tcheck(tc.t, tc.lastErr, "read imap response")
271 if strings.ToUpper(status) != string(tc.lastResult.Status) {
272 tc.t.Fatalf("got status %q, expected %q", tc.lastResult.Status, status)
276func (tc *testconn) cmdf(tag, format string, args ...any) {
278 err := tc.client.Commandf(tag, format, args...)
279 tcheck(tc.t, err, "writing imap command")
282func (tc *testconn) readstatus(status string) {
287func (tc *testconn) readprefixline(pre string) {
289 line, err := tc.client.Readline()
290 tcheck(tc.t, err, "read line")
291 if !strings.HasPrefix(line, pre) {
292 tc.t.Fatalf("expected prefix %q, got %q", pre, line)
296func (tc *testconn) writelinef(format string, args ...any) {
298 err := tc.client.Writelinef(format, args...)
299 tcheck(tc.t, err, "write line")
302// wait at most 1 second for server to quit.
303func (tc *testconn) waitDone() {
305 t := time.NewTimer(time.Second)
310 tc.t.Fatalf("server not done within 1s")
314func (tc *testconn) close() {
318func (tc *testconn) closeNoWait() {
322func (tc *testconn) close0(waitclose bool) {
324 if unhandledPanics.Swap(0) > 0 {
325 tc.t.Fatalf("handled panic in server")
329 if tc.account == nil {
330 // Already closed, we are not strict about closing multiple times.
333 if tc.client != nil {
336 err := tc.account.Close()
337 tc.check(err, "close account")
339 tc.account.WaitClosed()
342 tc.serverConn.Close()
344 if tc.switchStop != nil {
349func xparseNumSet(s string) imapclient.NumSet {
350 ns, err := imapclient.ParseNumSet(s)
352 panic(fmt.Sprintf("parsing numset %s: %s", s, err))
357func xparseUIDRange(s string) imapclient.NumRange {
358 nr, err := imapclient.ParseUIDRange(s)
360 panic(fmt.Sprintf("parsing uid range %s: %s", s, err))
365func makeAppend(msg string) imapclient.Append {
366 return imapclient.Append{Size: int64(len(msg)), Data: strings.NewReader(msg)}
369func makeAppendTime(msg string, tm time.Time) imapclient.Append {
370 return imapclient.Append{Received: &tm, Size: int64(len(msg)), Data: strings.NewReader(msg)}
375func start(t *testing.T) *testconn {
376 return startArgs(t, true, false, true, true, "mjl")
379func startNoSwitchboard(t *testing.T) *testconn {
380 return startArgs(t, false, false, true, false, "mjl")
383const password0 = "te\u0301st \u00a0\u2002\u200a" // NFD and various unicode spaces.
384const password1 = "tést " // PRECIS normalized, with NFC.
386func startArgs(t *testing.T, first, immediateTLS bool, allowLoginWithoutTLS, setPassword bool, accname string) *testconn {
387 return startArgsMore(t, first, immediateTLS, nil, nil, allowLoginWithoutTLS, setPassword, accname, nil)
390// namedConn wraps a conn so it can return a RemoteAddr with a non-empty name.
391// The TLS resumption test needs a non-empty name, but on BSDs, the unix domain
392// socket pair has an empty peer name.
393type namedConn struct {
397func (c namedConn) RemoteAddr() net.Addr {
398 return &net.TCPAddr{IP: net.ParseIP("127.0.0.10"), Port: 1234}
401// 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.
402func startArgsMore(t *testing.T, first, immediateTLS bool, serverConfig, clientConfig *tls.Config, allowLoginWithoutTLS, setPassword bool, accname string, afterInit func() error) *testconn {
403 limitersInit() // Reset rate limiters.
405 switchStop := func() {}
407 mox.ConfigStaticPath = filepath.FromSlash("../testdata/imap/mox.conf")
408 mox.MustLoadConfig(true, false)
409 store.Close() // May not be open, we ignore error.
410 os.RemoveAll("../testdata/imap/data")
411 err := store.Init(ctxbg)
412 tcheck(t, err, "store init")
413 switchStop = store.Switchboard()
416 acc, err := store.OpenAccount(pkglog, accname, false)
417 tcheck(t, err, "open account")
419 err = acc.SetPassword(pkglog, password0)
420 tcheck(t, err, "set password")
423 // Add a deleted mailbox, may excercise some code paths.
424 err = acc.DB.Write(ctxbg, func(tx *bstore.Tx) error {
425 // todo: add a message to inbox and remove it again. need to change all uids in the tests.
426 // todo: add tests for operating on an expunged mailbox. it should say it doesn't exist.
428 mb, _, _, _, err := acc.MailboxCreate(tx, "expungebox", store.SpecialUse{})
430 return fmt.Errorf("create mailbox: %v", err)
432 if _, _, err := acc.MailboxDelete(ctxbg, pkglog, tx, &mb); err != nil {
433 return fmt.Errorf("delete mailbox: %v", err)
437 tcheck(t, err, "add expunged mailbox")
440 if afterInit != nil {
442 tcheck(t, err, "after init")
445 // We get actual sockets for their buffering behaviour. net.Pipe is synchronous,
446 // and the implementation of the compress extension can write a sync message to an
447 // imap client when that client isn't reading but is trying to write. In the real
448 // world, network buffer will take up those few bytes, so assume the buffer in the
450 fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM, 0)
451 tcheck(t, err, "socketpair")
452 xfdconn := func(fd int, name string) net.Conn {
453 f := os.NewFile(uintptr(fd), name)
454 fc, err := net.FileConn(f)
455 tcheck(t, err, "fileconn")
457 tcheck(t, err, "close file for conn")
459 // Small read/write buffers, for detecting closed/broken connections quickly.
460 uc := fc.(*net.UnixConn)
461 err = uc.SetReadBuffer(512)
462 tcheck(t, err, "set read buffer")
463 uc.SetWriteBuffer(512)
464 tcheck(t, err, "set write buffer")
468 serverConn := xfdconn(fds[0], "server")
469 clientConn := xfdconn(fds[1], "client")
471 if serverConfig == nil {
472 serverConfig = &tls.Config{
473 Certificates: []tls.Certificate{fakeCert(t, false)},
477 if clientConfig == nil {
478 clientConfig = &tls.Config{InsecureSkipVerify: true}
480 clientConn = tls.Client(clientConn, clientConfig)
483 done := make(chan struct{})
485 cid := connCounter - 1
487 const viaHTTPS = false
488 serve("test", cid, serverConfig, serverConn, immediateTLS, allowLoginWithoutTLS, viaHTTPS, "")
491 client, err := imapclient.New(connCounter, clientConn, true)
492 tcheck(t, err, "new client")
493 tc := &testconn{t: t, conn: clientConn, client: client, done: done, serverConn: serverConn, account: acc}
495 tc.switchStop = switchStop
500func fakeCert(t *testing.T, randomkey bool) tls.Certificate {
501 seed := make([]byte, ed25519.SeedSize)
503 cryptorand.Read(seed)
505 privKey := ed25519.NewKeyFromSeed(seed) // Fake key, don't use this for real!
506 template := &x509.Certificate{
507 SerialNumber: big.NewInt(1), // Required field...
508 // Valid period is needed to get session resumption enabled.
509 NotBefore: time.Now().Add(-time.Minute),
510 NotAfter: time.Now().Add(time.Hour),
512 localCertBuf, err := x509.CreateCertificate(cryptorand.Reader, template, template, privKey.Public(), privKey)
514 t.Fatalf("making certificate: %s", err)
516 cert, err := x509.ParseCertificate(localCertBuf)
518 t.Fatalf("parsing generated certificate: %s", err)
520 c := tls.Certificate{
521 Certificate: [][]byte{localCertBuf},
528func TestLogin(t *testing.T) {
532 tc.transactf("bad", "login too many args")
533 tc.transactf("bad", "login") // no args
534 tc.transactf("no", "login mjl@mox.example badpass")
535 tc.transactf("no", `login mjl "%s"`, password0) // must use email, not account
536 tc.transactf("no", "login mjl@mox.example test")
537 tc.transactf("no", "login mjl@mox.example testtesttest")
538 tc.transactf("no", `login "mjl@mox.example" "testtesttest"`)
539 tc.transactf("no", "login \"m\xf8x@mox.example\" \"testtesttest\"")
540 tc.transactf("ok", `login mjl@mox.example "%s"`, password0)
544 tc.transactf("ok", `login "mjl@mox.example" "%s"`, password0)
548 tc.transactf("ok", `login "\"\"@mox.example" "%s"`, password0)
551 tc.transactf("bad", "logout badarg")
552 tc.transactf("ok", "logout")
555// Test that commands don't work in the states they are not supposed to.
556func TestState(t *testing.T) {
559 notAuthenticated := []string{"starttls", "authenticate", "login"}
560 authenticatedOrSelected := []string{"enable", "select", "examine", "create", "delete", "rename", "subscribe", "unsubscribe", "list", "namespace", "status", "append", "idle", "lsub"}
561 selected := []string{"close", "unselect", "expunge", "search", "fetch", "store", "copy", "move", "uid expunge"}
564 tc.transactf("ok", "capability")
565 tc.transactf("ok", "noop")
566 tc.transactf("ok", "logout")
571 // Not authenticated, lots of commands not allowed.
572 for _, cmd := range slices.Concat(authenticatedOrSelected, selected) {
573 tc.transactf("no", "%s", cmd)
576 // Some commands not allowed when authenticated.
577 tc.transactf("ok", `login mjl@mox.example "%s"`, password0)
578 for _, cmd := range slices.Concat(notAuthenticated, selected) {
579 tc.transactf("no", "%s", cmd)
582 tc.transactf("bad", "boguscommand")
585func TestNonIMAP(t *testing.T) {
589 // imap greeting has already been read, we sidestep the imapclient.
590 _, err := fmt.Fprintf(tc.conn, "bogus\r\n")
591 tc.check(err, "write bogus command")
592 tc.readprefixline("* BYE ")
593 if _, err := tc.conn.Read(make([]byte, 1)); err == nil {
594 t.Fatalf("connection not closed after initial bad command")
598func TestLiterals(t *testing.T) {
602 tc.client.Login("mjl@mox.example", password0)
603 tc.client.Create("tmpbox", nil)
605 tc.transactf("ok", "rename {6+}\r\ntmpbox {7+}\r\nntmpbox")
609 fmt.Fprint(tc.client, "xtag rename ")
610 tc.client.WriteSyncLiteral(from)
611 fmt.Fprint(tc.client, " ")
612 tc.client.WriteSyncLiteral(to)
613 fmt.Fprint(tc.client, "\r\n")
614 tc.client.LastTag = "xtag"
615 tc.last(tc.client.Response())
616 if tc.lastResult.Status != "OK" {
617 tc.t.Fatalf(`got %q, expected "OK"`, tc.lastResult.Status)
621// Test longer scenario with login, lists, subscribes, status, selects, etc.
622func TestScenario(t *testing.T) {
625 tc.transactf("ok", `login mjl@mox.example "%s"`, password0)
627 tc.transactf("bad", " missingcommand")
629 tc.transactf("ok", "examine inbox")
630 tc.transactf("ok", "unselect")
632 tc.transactf("ok", "examine inbox")
633 tc.transactf("ok", "close")
635 tc.transactf("ok", "select inbox")
636 tc.transactf("ok", "close")
638 tc.transactf("ok", "select inbox")
639 tc.transactf("ok", "expunge")
640 tc.transactf("ok", "check")
642 tc.transactf("ok", "subscribe inbox")
643 tc.transactf("ok", "unsubscribe inbox")
644 tc.transactf("ok", "subscribe inbox")
646 tc.transactf("ok", `lsub "" "*"`)
648 tc.transactf("ok", `list "" ""`)
649 tc.transactf("ok", `namespace`)
651 tc.transactf("ok", "enable utf8=accept")
652 tc.transactf("ok", "enable imap4rev2 utf8=accept")
654 tc.transactf("no", "create inbox")
655 tc.transactf("ok", "create tmpbox")
656 tc.transactf("ok", "rename tmpbox ntmpbox")
657 tc.transactf("ok", "delete ntmpbox")
659 tc.transactf("ok", "status inbox (uidnext messages uidvalidity deleted size unseen recent)")
661 tc.transactf("ok", "append inbox (\\seen) {%d+}\r\n%s", len(exampleMsg), exampleMsg)
662 tc.transactf("no", "append bogus () {%d}", len(exampleMsg))
663 tc.cmdf("", "append inbox () {%d}", len(exampleMsg))
664 tc.readprefixline("+ ")
665 _, err := tc.conn.Write([]byte(exampleMsg + "\r\n"))
666 tc.check(err, "write message")
669 tc.transactf("ok", "fetch 1 all")
670 tc.transactf("ok", "fetch 1 body")
671 tc.transactf("ok", "fetch 1 binary[]")
673 tc.transactf("ok", `store 1 flags (\seen \answered)`)
674 tc.transactf("ok", `store 1 +flags ($junk)`) // should train as junk.
675 tc.transactf("ok", `store 1 -flags ($junk)`) // should retrain as non-junk.
676 tc.transactf("ok", `store 1 -flags (\seen)`) // should untrain completely.
677 tc.transactf("ok", `store 1 -flags (\answered)`)
678 tc.transactf("ok", `store 1 +flags (\answered)`)
679 tc.transactf("ok", `store 1 flags.silent (\seen \answered)`)
680 tc.transactf("ok", `store 1 -flags.silent (\answered)`)
681 tc.transactf("ok", `store 1 +flags.silent (\answered)`)
682 tc.transactf("bad", `store 1 flags (\badflag)`)
683 tc.transactf("ok", "noop")
685 tc.transactf("ok", "copy 1 Trash")
686 tc.transactf("ok", "copy 1 Trash")
687 tc.transactf("ok", "move 1 Trash")
689 tc.transactf("ok", "close")
690 tc.transactf("ok", "select Trash")
691 tc.transactf("ok", `store 1 flags (\deleted)`)
692 tc.transactf("ok", "expunge")
693 tc.transactf("ok", "noop")
695 tc.transactf("ok", `store 1 flags (\deleted)`)
696 tc.transactf("ok", "close")
697 tc.transactf("ok", "delete Trash")
700func TestMailbox(t *testing.T) {
703 tc.client.Login("mjl@mox.example", password0)
706 "e\u0301", // é but as e + acute, not unicode-normalized
717 for _, bad := range invalid {
718 tc.transactf("no", "select {%d+}\r\n%s", len(bad), bad)
722func TestMailboxDeleted(t *testing.T) {
726 tc2 := startNoSwitchboard(t)
727 defer tc2.closeNoWait()
729 tc.client.Login("mjl@mox.example", password0)
730 tc2.client.Login("mjl@mox.example", password0)
732 tc.client.Create("testbox", nil)
733 tc2.client.Select("testbox")
734 tc.client.Delete("testbox")
736 // Now try to operate on testbox while it has been removed.
737 tc2.transactf("no", "check")
738 tc2.transactf("no", "expunge")
739 tc2.transactf("no", "uid expunge 1")
740 tc2.transactf("no", "search all")
741 tc2.transactf("no", "uid search all")
742 tc2.transactf("no", "fetch 1:* all")
743 tc2.transactf("no", "uid fetch 1 all")
744 tc2.transactf("no", "store 1 flags ()")
745 tc2.transactf("no", "uid store 1 flags ()")
746 tc2.transactf("bad", "copy 1 inbox") // msgseq 1 not available.
747 tc2.transactf("no", "uid copy 1 inbox")
748 tc2.transactf("bad", "move 1 inbox") // msgseq 1 not available.
749 tc2.transactf("no", "uid move 1 inbox")
751 tc2.transactf("ok", "unselect")
753 tc.client.Create("testbox", nil)
754 tc2.client.Select("testbox")
755 tc.client.Delete("testbox")
756 tc2.transactf("ok", "close")
759func TestID(t *testing.T) {
762 tc.client.Login("mjl@mox.example", password0)
764 tc.transactf("ok", "id nil")
765 tc.xuntagged(imapclient.UntaggedID{"name": "mox", "version": moxvar.Version})
767 tc.transactf("ok", `id ("name" "mox" "version" "1.2.3" "other" "test" "test" nil)`)
768 tc.xuntagged(imapclient.UntaggedID{"name": "mox", "version": moxvar.Version})
770 tc.transactf("bad", `id ("name" "mox" "name" "mox")`) // Duplicate field.
773func TestSequence(t *testing.T) {
776 tc.client.Login("mjl@mox.example", password0)
777 tc.client.Select("inbox")
782 tc.transactf("ok", "uid fetch 1 all") // non-existing messages are OK for uids.
783 tc.transactf("ok", "uid fetch * all") // * is like uidnext, a non-existing message.
785 tc.client.Append("inbox", makeAppend(exampleMsg))
786 tc.client.Append("inbox", makeAppend(exampleMsg))
787 tc.transactf("ok", "fetch 2:1,1 uid") // We reorder 2:1 to 1:2, but we don't deduplicate numbers.
789 imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1)}},
790 imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(2)}},
791 imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1)}},
794 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.
795 tc.xuntagged(imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(2)}})
798// Test that a message that is expunged by another session can be read as long as a
799// reference is held by a session. New sessions do not see the expunged message.
800// todo: possibly implement the additional reference counting. so far it hasn't been worth the trouble.
801func DisabledTestReference(t *testing.T) {
804 tc.client.Login("mjl@mox.example", password0)
805 tc.client.Select("inbox")
806 tc.client.Append("inbox", makeAppend(exampleMsg))
808 tc2 := startNoSwitchboard(t)
810 tc2.client.Login("mjl@mox.example", password0)
811 tc2.client.Select("inbox")
813 tc.client.StoreFlagsSet("1", true, `\Deleted`)
816 tc3 := startNoSwitchboard(t)
818 tc3.client.Login("mjl@mox.example", password0)
819 tc3.transactf("ok", `list "" "inbox" return (status (messages))`)
820 tc3.xuntagged(imapclient.UntaggedList{Separator: '/', Mailbox: "Inbox"}, imapclient.UntaggedStatus{Mailbox: "Inbox", Attrs: map[imapclient.StatusAttr]int64{imapclient.StatusMessages: 0}})
822 tc2.transactf("ok", "fetch 1 rfc822.size")
823 tc.xuntagged(imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchRFC822Size(len(exampleMsg))}})