From af9299bf67f3edf8e4790a884481d2949e503525 Mon Sep 17 00:00:00 2001 From: Simon Ser Date: Mon, 17 Apr 2023 22:07:00 +0200 Subject: [PATCH] imap: turn FetchItem into FetchOptions --- fetch.go | 51 ++---- imapclient/client_test.go | 28 ++-- imapclient/fetch.go | 168 +++++++++++--------- imapserver/fetch.go | 238 ++++++++++++++++------------ imapserver/imapmemserver/mailbox.go | 10 +- imapserver/imapmemserver/message.go | 59 +++---- imapserver/session.go | 2 +- internal/internal.go | 7 - 8 files changed, 286 insertions(+), 277 deletions(-) diff --git a/fetch.go b/fetch.go index 809f3631..db014446 100644 --- a/fetch.go +++ b/fetch.go @@ -7,39 +7,22 @@ import ( ) // FetchOptions contains options for the FETCH command. -type FetchOptions struct{} - -// FetchItem is a message data item which can be requested by a FETCH command. -type FetchItem interface { - fetchItem() +type FetchOptions struct { + // Fields to fetch + BodyStructure *FetchItemBodyStructure + Envelope bool + Flags bool + InternalDate bool + RFC822Size bool + UID bool + BodySection []*FetchItemBodySection + BinarySection []*FetchItemBinarySection // requires IMAP4rev2 or BINARY + BinarySectionSize []*FetchItemBinarySectionSize // requires IMAP4rev2 or BINARY } -var ( - _ FetchItem = FetchItemKeyword("") - _ FetchItem = (*FetchItemBodySection)(nil) - _ FetchItem = (*FetchItemBinarySection)(nil) - _ FetchItem = (*FetchItemBinarySectionSize)(nil) -) - -// FetchItemKeyword is a FETCH item described by a single keyword. -type FetchItemKeyword string - -func (FetchItemKeyword) fetchItem() {} - -var ( - // Macros - FetchItemAll FetchItem = FetchItemKeyword("ALL") - FetchItemFast FetchItem = FetchItemKeyword("FAST") - FetchItemFull FetchItem = FetchItemKeyword("FULL") - - FetchItemBody FetchItem = FetchItemKeyword("BODY") - FetchItemBodyStructure FetchItem = FetchItemKeyword("BODYSTRUCTURE") - FetchItemEnvelope FetchItem = FetchItemKeyword("ENVELOPE") - FetchItemFlags FetchItem = FetchItemKeyword("FLAGS") - FetchItemInternalDate FetchItem = FetchItemKeyword("INTERNALDATE") - FetchItemRFC822Size FetchItem = FetchItemKeyword("RFC822.SIZE") - FetchItemUID FetchItem = FetchItemKeyword("UID") -) +type FetchItemBodyStructure struct { + Extended bool +} type PartSpecifier string @@ -64,8 +47,6 @@ type FetchItemBodySection struct { Peek bool } -func (*FetchItemBodySection) fetchItem() {} - // FetchItemBinarySection is a FETCH BINARY[] data item. type FetchItemBinarySection struct { Part []int @@ -73,15 +54,11 @@ type FetchItemBinarySection struct { Peek bool } -func (*FetchItemBinarySection) fetchItem() {} - // FetchItemBinarySectionSize is a FETCH BINARY.SIZE[] data item. type FetchItemBinarySectionSize struct { Part []int } -func (*FetchItemBinarySectionSize) fetchItem() {} - // Envelope is the envelope structure of a message. type Envelope struct { Date time.Time diff --git a/imapclient/client_test.go b/imapclient/client_test.go index 80400bc2..d4bf2ac5 100644 --- a/imapclient/client_test.go +++ b/imapclient/client_test.go @@ -37,8 +37,8 @@ func ExampleClient() { if selectedMbox.NumMessages > 0 { seqSet := imap.SeqSetNum(1) - fetchItems := []imap.FetchItem{imap.FetchItemEnvelope} - messages, err := c.Fetch(seqSet, fetchItems, nil).Collect() + fetchOptions := &imap.FetchOptions{Envelope: true} + messages, err := c.Fetch(seqSet, fetchOptions).Collect() if err != nil { log.Fatalf("failed to fetch first message in INBOX: %v", err) } @@ -54,12 +54,12 @@ func ExampleClient_pipelining() { var c *imapclient.Client uid := uint32(42) - fetchItems := []imap.FetchItem{imap.FetchItemEnvelope} + fetchOptions := &imap.FetchOptions{Envelope: true} // Login, select and fetch a message in a single roundtrip loginCmd := c.Login("root", "root") selectCmd := c.Select("INBOX", nil) - fetchCmd := c.UIDFetch(imap.SeqSetNum(uid), fetchItems, nil) + fetchCmd := c.UIDFetch(imap.SeqSetNum(uid), fetchOptions) if err := loginCmd.Wait(); err != nil { log.Fatalf("failed to login: %v", err) @@ -142,12 +142,14 @@ func ExampleClient_Fetch() { var c *imapclient.Client seqSet := imap.SeqSetNum(1) - fetchItems := []imap.FetchItem{ - imap.FetchItemFlags, - imap.FetchItemEnvelope, - &imap.FetchItemBodySection{Specifier: imap.PartSpecifierHeader}, + fetchOptions := &imap.FetchOptions{ + Flags: true, + Envelope: true, + BodySection: []*imap.FetchItemBodySection{ + {Specifier: imap.PartSpecifierHeader}, + }, } - messages, err := c.Fetch(seqSet, fetchItems, nil).Collect() + messages, err := c.Fetch(seqSet, fetchOptions).Collect() if err != nil { log.Fatalf("FETCH command failed: %v", err) } @@ -168,11 +170,11 @@ func ExampleClient_Fetch_stream() { var c *imapclient.Client seqSet := imap.SeqSetNum(1) - fetchItems := []imap.FetchItem{ - imap.FetchItemUID, - &imap.FetchItemBodySection{}, + fetchOptions := &imap.FetchOptions{ + UID: true, + BodySection: []*imap.FetchItemBodySection{{}}, } - fetchCmd := c.Fetch(seqSet, fetchItems, nil) + fetchCmd := c.Fetch(seqSet, fetchOptions) for { msg := fetchCmd.Next() if msg == nil { diff --git a/imapclient/fetch.go b/imapclient/fetch.go index 49aea5a6..10abf938 100644 --- a/imapclient/fetch.go +++ b/imapclient/fetch.go @@ -12,28 +12,15 @@ import ( "github.com/emersion/go-imap/v2/internal/imapwire" ) -func (c *Client) fetch(uid bool, seqSet imap.SeqSet, items []imap.FetchItem, options *imap.FetchOptions) *FetchCommand { - // Ensure we request UID as the first data item for UID FETCH, to be safer. - // We want to get it before any literal. - if uid { - itemsWithUID := []imap.FetchItem{imap.FetchItemUID} - for _, item := range items { - if item != imap.FetchItemUID { - itemsWithUID = append(itemsWithUID, item) - } - } - items = itemsWithUID - } - +func (c *Client) fetch(uid bool, seqSet imap.SeqSet, options *imap.FetchOptions) *FetchCommand { cmd := &FetchCommand{ uid: uid, seqSet: seqSet, msgs: make(chan *FetchMessageData, 128), } enc := c.beginCommand(uidCmdName("FETCH", uid), cmd) - enc.SP().SeqSet(seqSet).SP().List(len(items), func(i int) { - writeFetchItem(enc.Encoder, items[i]) - }) + enc.SP().SeqSet(seqSet).SP() + writeFetchItems(enc.Encoder, uid, options) enc.end() return cmd } @@ -44,68 +31,99 @@ func (c *Client) fetch(uid bool, seqSet imap.SeqSet, items []imap.FetchItem, opt // defer a call to FetchCommand.Close. // // A nil options pointer is equivalent to a zero options value. -func (c *Client) Fetch(seqSet imap.SeqSet, items []imap.FetchItem, options *imap.FetchOptions) *FetchCommand { - return c.fetch(false, seqSet, items, options) +func (c *Client) Fetch(seqSet imap.SeqSet, options *imap.FetchOptions) *FetchCommand { + return c.fetch(false, seqSet, options) } // UIDFetch sends a UID FETCH command. // // See Fetch. -func (c *Client) UIDFetch(seqSet imap.SeqSet, items []imap.FetchItem, options *imap.FetchOptions) *FetchCommand { - return c.fetch(true, seqSet, items, options) +func (c *Client) UIDFetch(seqSet imap.SeqSet, options *imap.FetchOptions) *FetchCommand { + return c.fetch(true, seqSet, options) } -func writeFetchItem(enc *imapwire.Encoder, item imap.FetchItem) { - switch item := item.(type) { - case imap.FetchItemKeyword: - enc.Atom(string(item)) - case *imap.FetchItemBodySection: - enc.Atom("BODY") - if item.Peek { - enc.Atom(".PEEK") - } - enc.Special('[') - writeSectionPart(enc, item.Part) - if len(item.Part) > 0 && item.Specifier != imap.PartSpecifierNone { - enc.Special('.') +func writeFetchItems(enc *imapwire.Encoder, uid bool, options *imap.FetchOptions) { + listEnc := enc.BeginList() + + // Ensure we request UID as the first data item for UID FETCH, to be safer. + // We want to get it before any literal. + if options.UID || uid { + listEnc.Item().Atom("UID") + } + + m := map[string]bool{ + "BODY": options.BodyStructure != nil && !options.BodyStructure.Extended, + "BODYSTRUCTURE": options.BodyStructure != nil && options.BodyStructure.Extended, + "ENVELOPE": options.Envelope, + "FLAGS": options.Flags, + "INTERNALDATE": options.InternalDate, + "RFC822.SIZE": options.RFC822Size, + } + for k, req := range m { + if req { + listEnc.Item().Atom(k) } - if item.Specifier != imap.PartSpecifierNone { - enc.Atom(string(item.Specifier)) - - var headerList []string - if len(item.HeaderFields) > 0 { - headerList = item.HeaderFields - enc.Atom(".FIELDS") - } else if len(item.HeaderFieldsNot) > 0 { - headerList = item.HeaderFieldsNot - enc.Atom(".FIELDS.NOT") - } + } - if len(headerList) > 0 { - enc.SP().List(len(headerList), func(i int) { - enc.String(headerList[i]) - }) - } + for _, bs := range options.BodySection { + writeFetchItemBodySection(listEnc.Item(), bs) + } + for _, bs := range options.BinarySection { + writeFetchItemBinarySection(listEnc.Item(), bs) + } + for _, bss := range options.BinarySectionSize { + writeFetchItemBinarySectionSize(listEnc.Item(), bss) + } +} + +func writeFetchItemBodySection(enc *imapwire.Encoder, item *imap.FetchItemBodySection) { + enc.Atom("BODY") + if item.Peek { + enc.Atom(".PEEK") + } + enc.Special('[') + writeSectionPart(enc, item.Part) + if len(item.Part) > 0 && item.Specifier != imap.PartSpecifierNone { + enc.Special('.') + } + if item.Specifier != imap.PartSpecifierNone { + enc.Atom(string(item.Specifier)) + + var headerList []string + if len(item.HeaderFields) > 0 { + headerList = item.HeaderFields + enc.Atom(".FIELDS") + } else if len(item.HeaderFieldsNot) > 0 { + headerList = item.HeaderFieldsNot + enc.Atom(".FIELDS.NOT") } - enc.Special(']') - writeSectionPartial(enc, item.Partial) - case *imap.FetchItemBinarySection: - enc.Atom("BINARY") - if item.Peek { - enc.Atom(".PEEK") + + if len(headerList) > 0 { + enc.SP().List(len(headerList), func(i int) { + enc.String(headerList[i]) + }) } - enc.Special('[') - writeSectionPart(enc, item.Part) - enc.Special(']') - writeSectionPartial(enc, item.Partial) - case *imap.FetchItemBinarySectionSize: - enc.Atom("BINARY.SIZE") - enc.Special('[') - writeSectionPart(enc, item.Part) - enc.Special(']') - default: - panic(fmt.Errorf("imapclient: unknown fetch item type %T", item)) } + enc.Special(']') + writeSectionPartial(enc, item.Partial) +} + +func writeFetchItemBinarySection(enc *imapwire.Encoder, item *imap.FetchItemBinarySection) { + enc.Atom("BINARY") + if item.Peek { + enc.Atom(".PEEK") + } + enc.Special('[') + writeSectionPart(enc, item.Part) + enc.Special(']') + writeSectionPartial(enc, item.Partial) +} + +func writeFetchItemBinarySectionSize(enc *imapwire.Encoder, item *imap.FetchItemBinarySectionSize) { + enc.Atom("BINARY.SIZE") + enc.Special('[') + writeSectionPart(enc, item.Part) + enc.Special(']') } func writeSectionPart(enc *imapwire.Encoder, part []int) { @@ -461,8 +479,8 @@ func (c *Client) handleFetch(seqNum uint32) error { item FetchItemData done chan struct{} ) - switch attName := imap.FetchItemKeyword(attName); attName { - case imap.FetchItemFlags: + switch attName { + case "FLAGS": if !dec.ExpectSP() { return dec.Err() } @@ -473,7 +491,7 @@ func (c *Client) handleFetch(seqNum uint32) error { } item = FetchItemDataFlags{Flags: flags} - case imap.FetchItemEnvelope: + case "ENVELOPE": if !dec.ExpectSP() { return dec.Err() } @@ -484,7 +502,7 @@ func (c *Client) handleFetch(seqNum uint32) error { } item = FetchItemDataEnvelope{Envelope: envelope} - case imap.FetchItemInternalDate: + case "INTERNALDATE": if !dec.ExpectSP() { return dec.Err() } @@ -495,14 +513,14 @@ func (c *Client) handleFetch(seqNum uint32) error { } item = FetchItemDataInternalDate{Time: t} - case imap.FetchItemRFC822Size: + case "RFC822.SIZE": var size int64 if !dec.ExpectSP() || !dec.ExpectNumber64(&size) { return dec.Err() } item = FetchItemDataRFC822Size{Size: size} - case imap.FetchItemUID: + case "UID": if !dec.ExpectSP() || !dec.ExpectNumber(&uid) { return dec.Err() } @@ -510,7 +528,7 @@ func (c *Client) handleFetch(seqNum uint32) error { item = FetchItemDataUID{UID: uid} case "BODY", "BINARY": if dec.Special('[') { - var section imap.FetchItem + var section interface{} switch attName { case "BODY": var err error @@ -570,7 +588,7 @@ func (c *Client) handleFetch(seqNum uint32) error { return dec.Err() } fallthrough - case imap.FetchItemBodyStructure: + case "BODYSTRUCTURE": if !dec.ExpectSP() { return dec.Err() } @@ -582,7 +600,7 @@ func (c *Client) handleFetch(seqNum uint32) error { item = FetchItemDataBodyStructure{ BodyStructure: bodyStruct, - IsExtended: attName == imap.FetchItemBodyStructure, + IsExtended: attName == "BODYSTRUCTURE", } case "BINARY.SIZE": part, dot := readSectionPart(dec) diff --git a/imapserver/fetch.go b/imapserver/fetch.go index f0d7c5a3..cefd4287 100644 --- a/imapserver/fetch.go +++ b/imapserver/fetch.go @@ -15,44 +15,63 @@ import ( const envelopeDateLayout = "Mon, 02 Jan 2006 15:04:05 -0700" +type fetchWriterOptions struct { + bodyStructure struct { + extended bool // BODYSTRUCTURE + nonExtended bool // BODY + } + obsolete map[*imap.FetchItemBodySection]string +} + func (c *Conn) handleFetch(dec *imapwire.Decoder, numKind NumKind) error { var seqSet imap.SeqSet if !dec.ExpectSP() || !dec.ExpectSeqSet(&seqSet) || !dec.ExpectSP() { return dec.Err() } - var items []imap.FetchItem + var options imap.FetchOptions + writerOptions := fetchWriterOptions{obsolete: make(map[*imap.FetchItemBodySection]string)} isList, err := dec.List(func() error { - item, err := readFetchAtt(dec) + name, err := readFetchAttName(dec) if err != nil { return err } - switch item { - case imap.FetchItemAll, imap.FetchItemFast, imap.FetchItemFull: + switch name { + case "ALL", "FAST", "FULL": return newClientBugError("FETCH macros are not allowed in a list") } - items = append(items, item) - return nil + return handleFetchAtt(dec, name, &options, &writerOptions) }) if err != nil { return err } if !isList { - item, err := readFetchAtt(dec) + name, err := readFetchAttName(dec) if err != nil { return err } // Handle macros - switch item { - case imap.FetchItemAll: - items = append(items, imap.FetchItemFlags, imap.FetchItemInternalDate, imap.FetchItemRFC822Size, imap.FetchItemEnvelope) - case imap.FetchItemFast: - items = append(items, imap.FetchItemFlags, imap.FetchItemInternalDate, imap.FetchItemRFC822Size) - case imap.FetchItemFull: - items = append(items, imap.FetchItemFlags, imap.FetchItemInternalDate, imap.FetchItemRFC822Size, imap.FetchItemEnvelope, imap.FetchItemBody) + switch name { + case "ALL": + options.Flags = true + options.InternalDate = true + options.RFC822Size = true + options.Envelope = true + case "FAST": + options.Flags = true + options.InternalDate = true + options.RFC822Size = true + case "FULL": + options.Flags = true + options.InternalDate = true + options.RFC822Size = true + options.Envelope = true + handleFetchBodyStructure(&options, &writerOptions, false) default: - items = append(items, item) + if err := handleFetchAtt(dec, name, &options, &writerOptions); err != nil { + return err + } } } @@ -64,125 +83,122 @@ func (c *Conn) handleFetch(dec *imapwire.Decoder, numKind NumKind) error { return err } - obsolete := make(map[imap.FetchItem]imap.FetchItemKeyword) - for i, item := range items { - var repl imap.FetchItem - switch item { - case internal.FetchItemRFC822: - repl = &imap.FetchItemBodySection{} - case internal.FetchItemRFC822Header: - repl = &imap.FetchItemBodySection{ - Peek: true, - Specifier: imap.PartSpecifierHeader, - } - case internal.FetchItemRFC822Text: - repl = &imap.FetchItemBodySection{ - Specifier: imap.PartSpecifierText, - } - } - if repl != nil { - items[i] = repl - obsolete[repl] = item.(imap.FetchItemKeyword) - } - } - if numKind == NumKindUID { - itemsWithUID := []imap.FetchItem{imap.FetchItemUID} - for _, item := range items { - if item != imap.FetchItemUID { - itemsWithUID = append(itemsWithUID, item) - } - } - items = itemsWithUID + options.UID = true } - w := &FetchWriter{conn: c, obsolete: obsolete} - options := imap.FetchOptions{} - if err := c.session.Fetch(w, numKind, seqSet, items, &options); err != nil { + w := &FetchWriter{conn: c, options: writerOptions} + if err := c.session.Fetch(w, numKind, seqSet, &options); err != nil { return err } return nil } -func readFetchAtt(dec *imapwire.Decoder) (imap.FetchItem, error) { - var attName string - if !dec.Expect(dec.Func(&attName, isMsgAttNameChar), "msg-att name") { - return nil, dec.Err() - } - attName = strings.ToUpper(attName) - - // Keyword fetch items are variables: return the variable so that it can be - // compared directly - keywords := map[imap.FetchItemKeyword]imap.FetchItem{ - imap.FetchItemAll.(imap.FetchItemKeyword): imap.FetchItemAll, - imap.FetchItemFast.(imap.FetchItemKeyword): imap.FetchItemFast, - imap.FetchItemFull.(imap.FetchItemKeyword): imap.FetchItemFull, - imap.FetchItemBodyStructure.(imap.FetchItemKeyword): imap.FetchItemBodyStructure, - imap.FetchItemEnvelope.(imap.FetchItemKeyword): imap.FetchItemEnvelope, - imap.FetchItemFlags.(imap.FetchItemKeyword): imap.FetchItemFlags, - imap.FetchItemInternalDate.(imap.FetchItemKeyword): imap.FetchItemInternalDate, - imap.FetchItemRFC822Size.(imap.FetchItemKeyword): imap.FetchItemRFC822Size, - imap.FetchItemUID.(imap.FetchItemKeyword): imap.FetchItemUID, - internal.FetchItemRFC822.(imap.FetchItemKeyword): internal.FetchItemRFC822, - internal.FetchItemRFC822Header.(imap.FetchItemKeyword): internal.FetchItemRFC822Header, - internal.FetchItemRFC822Text.(imap.FetchItemKeyword): internal.FetchItemRFC822Text, - } - if item, ok := keywords[imap.FetchItemKeyword(attName)]; ok { - return item, nil - } - - switch attName := imap.FetchItemKeyword(attName); attName { +func handleFetchAtt(dec *imapwire.Decoder, attName string, options *imap.FetchOptions, writerOptions *fetchWriterOptions) error { + switch attName { + case "BODYSTRUCTURE": + handleFetchBodyStructure(options, writerOptions, true) + case "ENVELOPE": + options.Envelope = true + case "FLAGS": + options.Flags = true + case "INTERNALDATE": + options.InternalDate = true + case "RFC822.SIZE": + options.RFC822Size = true + case "UID": + options.UID = true + case "RFC822": // equivalent to BODY[] + bs := &imap.FetchItemBodySection{} + writerOptions.obsolete[bs] = attName + options.BodySection = append(options.BodySection, bs) + case "RFC822.HEADER": // equivalent to BODY.PEEK[HEADER] + bs := &imap.FetchItemBodySection{ + Specifier: imap.PartSpecifierHeader, + Peek: true, + } + writerOptions.obsolete[bs] = attName + options.BodySection = append(options.BodySection, bs) + case "RFC822.TEXT": // equivalent to BODY[TEXT] + bs := &imap.FetchItemBodySection{ + Specifier: imap.PartSpecifierText, + } + writerOptions.obsolete[bs] = attName + options.BodySection = append(options.BodySection, bs) case "BINARY", "BINARY.PEEK": part, err := readSectionBinary(dec) if err != nil { - return nil, err + return err } partial, err := maybeReadPartial(dec) if err != nil { - return nil, err + return err } - return &imap.FetchItemBinarySection{ + bs := &imap.FetchItemBinarySection{ Part: part, Partial: partial, Peek: attName == "BINARY.PEEK", - }, nil + } + options.BinarySection = append(options.BinarySection, bs) case "BINARY.SIZE": part, err := readSectionBinary(dec) if err != nil { - return nil, err + return err } - return &imap.FetchItemBinarySectionSize{Part: part}, nil + bss := &imap.FetchItemBinarySectionSize{Part: part} + options.BinarySectionSize = append(options.BinarySectionSize, bss) case "BODY": if !dec.Special('[') { - return attName, nil + handleFetchBodyStructure(options, writerOptions, false) + return nil } section := imap.FetchItemBodySection{} err := readSection(dec, §ion) if err != nil { - return nil, err + return err } section.Partial, err = maybeReadPartial(dec) if err != nil { - return nil, err + return err } - return §ion, nil + options.BodySection = append(options.BodySection, §ion) case "BODY.PEEK": if !dec.ExpectSpecial('[') { - return nil, dec.Err() + return dec.Err() } section := imap.FetchItemBodySection{Peek: true} err := readSection(dec, §ion) if err != nil { - return nil, err + return err } section.Partial, err = maybeReadPartial(dec) if err != nil { - return nil, err + return err } - return §ion, nil + options.BodySection = append(options.BodySection, §ion) default: - return nil, newClientBugError("Unknown FETCH data item") + return newClientBugError("Unknown FETCH data item") } + return nil +} + +func handleFetchBodyStructure(options *imap.FetchOptions, writerOptions *fetchWriterOptions, extended bool) { + if options.BodyStructure == nil || extended { + options.BodyStructure = &imap.FetchItemBodyStructure{Extended: extended} + } + if extended { + writerOptions.bodyStructure.extended = true + } else { + writerOptions.bodyStructure.nonExtended = true + } +} + +func readFetchAttName(dec *imapwire.Decoder) (string, error) { + var attName string + if !dec.Expect(dec.Func(&attName, isMsgAttNameChar), "msg-att name") { + return "", dec.Err() + } + return strings.ToUpper(attName), nil } func isMsgAttNameChar(ch byte) bool { @@ -304,8 +320,8 @@ func maybeReadPartial(dec *imapwire.Decoder) (*imap.SectionPartial, error) { // FetchWriter writes FETCH responses. type FetchWriter struct { - conn *Conn - obsolete map[imap.FetchItem]imap.FetchItemKeyword + conn *Conn + options fetchWriterOptions } // CreateMessage writes a FETCH response for a message. @@ -314,14 +330,15 @@ type FetchWriter struct { func (cmd *FetchWriter) CreateMessage(seqNum uint32) *FetchResponseWriter { enc := newResponseEncoder(cmd.conn) enc.Atom("*").SP().Number(seqNum).SP().Atom("FETCH").SP().Special('(') - return &FetchResponseWriter{enc: enc, obsolete: cmd.obsolete} + return &FetchResponseWriter{enc: enc, options: cmd.options} } // FetchResponseWriter writes a single FETCH response for a message. type FetchResponseWriter struct { - enc *responseEncoder - hasItem bool - obsolete map[imap.FetchItem]imap.FetchItemKeyword + enc *responseEncoder + options fetchWriterOptions + + hasItem bool } func (w *FetchResponseWriter) writeItemSep() { @@ -365,8 +382,8 @@ func (w *FetchResponseWriter) WriteBodySection(section *imap.FetchItemBodySectio w.writeItemSep() enc := w.enc.Encoder - if obs, ok := w.obsolete[section]; ok { - enc.Atom(string(obs)) + if obs, ok := w.options.obsolete[section]; ok { + enc.Atom(obs) } else { writeItemBodySection(enc, section) } @@ -442,14 +459,27 @@ func (w *FetchResponseWriter) WriteEnvelope(envelope *imap.Envelope) { // WriteBodyStructure writes the message's body structure (either BODYSTRUCTURE // or BODY). func (w *FetchResponseWriter) WriteBodyStructure(bs imap.BodyStructure) { - var extended bool - switch bs := bs.(type) { - case *imap.BodyStructureSinglePart: - extended = bs.Extended != nil - case *imap.BodyStructureMultiPart: - extended = bs.Extended != nil + if w.options.bodyStructure.nonExtended { + w.writeBodyStructure(bs, false) } + if w.options.bodyStructure.extended { + var isExtended bool + switch bs := bs.(type) { + case *imap.BodyStructureSinglePart: + isExtended = bs.Extended != nil + case *imap.BodyStructureMultiPart: + isExtended = bs.Extended != nil + } + if !isExtended { + panic("imapserver: client requested extended body structure but a non-extended one is written back") + } + + w.writeBodyStructure(bs, true) + } +} + +func (w *FetchResponseWriter) writeBodyStructure(bs imap.BodyStructure, extended bool) { item := "BODY" if extended { item = "BODYSTRUCTURE" diff --git a/imapserver/imapmemserver/mailbox.go b/imapserver/imapmemserver/mailbox.go index c28efe44..6c3f9091 100644 --- a/imapserver/imapmemserver/mailbox.go +++ b/imapserver/imapmemserver/mailbox.go @@ -282,10 +282,10 @@ func (mbox *MailboxView) Close() { mbox.tracker.Close() } -func (mbox *MailboxView) Fetch(w *imapserver.FetchWriter, numKind imapserver.NumKind, seqSet imap.SeqSet, items []imap.FetchItem, options *imap.FetchOptions) error { +func (mbox *MailboxView) Fetch(w *imapserver.FetchWriter, numKind imapserver.NumKind, seqSet imap.SeqSet, options *imap.FetchOptions) error { markSeen := false - for _, item := range items { - if item, ok := item.(*imap.FetchItemBodySection); ok && !item.Peek { + for _, bs := range options.BodySection { + if !bs.Peek { markSeen = true break } @@ -303,7 +303,7 @@ func (mbox *MailboxView) Fetch(w *imapserver.FetchWriter, numKind imapserver.Num } respWriter := w.CreateMessage(mbox.tracker.EncodeSeqNum(seqNum)) - err = msg.fetch(respWriter, items) + err = msg.fetch(respWriter, options) }) return err } @@ -359,7 +359,7 @@ func (mbox *MailboxView) Store(w *imapserver.FetchWriter, numKind imapserver.Num mbox.Mailbox.tracker.QueueMessageFlags(seqNum, msg.uid, msg.flagList(), mbox.tracker) }) if !flags.Silent { - return mbox.Fetch(w, numKind, seqSet, []imap.FetchItem{imap.FetchItemFlags}, nil) + return mbox.Fetch(w, numKind, seqSet, &imap.FetchOptions{Flags: true}) } return nil } diff --git a/imapserver/imapmemserver/message.go b/imapserver/imapmemserver/message.go index 25427544..00410280 100644 --- a/imapserver/imapmemserver/message.go +++ b/imapserver/imapmemserver/message.go @@ -27,52 +27,41 @@ type message struct { flags map[imap.Flag]struct{} } -func (msg *message) fetch(w *imapserver.FetchResponseWriter, items []imap.FetchItem) error { +func (msg *message) fetch(w *imapserver.FetchResponseWriter, options *imap.FetchOptions) error { w.WriteUID(msg.uid) - for _, item := range items { - if err := msg.fetchItem(w, item); err != nil { - return err - } + if options.Flags { + w.WriteFlags(msg.flagList()) + } + if options.InternalDate { + w.WriteInternalDate(msg.t) + } + if options.RFC822Size { + w.WriteRFC822Size(int64(len(msg.buf))) + } + if options.Envelope { + w.WriteEnvelope(msg.envelope()) + } + if bs := options.BodyStructure; bs != nil { + w.WriteBodyStructure(msg.bodyStructure(bs.Extended)) } - return w.Close() -} - -func (msg *message) fetchItem(w *imapserver.FetchResponseWriter, item imap.FetchItem) error { - switch item := item.(type) { - case *imap.FetchItemBodySection: - buf := msg.bodySection(item) - wc := w.WriteBodySection(item, int64(len(buf))) + for _, bs := range options.BodySection { + buf := msg.bodySection(bs) + wc := w.WriteBodySection(bs, int64(len(buf))) _, writeErr := wc.Write(buf) closeErr := wc.Close() if writeErr != nil { return writeErr } - return closeErr - case *imap.FetchItemBinarySection: - panic("TODO") - case *imap.FetchItemBinarySectionSize: - panic("TODO") + if closeErr != nil { + return closeErr + } } - switch item { - case imap.FetchItemUID: - // always included - case imap.FetchItemFlags: - w.WriteFlags(msg.flagList()) - case imap.FetchItemInternalDate: - w.WriteInternalDate(msg.t) - case imap.FetchItemRFC822Size: - w.WriteRFC822Size(int64(len(msg.buf))) - case imap.FetchItemEnvelope: - w.WriteEnvelope(msg.envelope()) - case imap.FetchItemBodyStructure, imap.FetchItemBody: - w.WriteBodyStructure(msg.bodyStructure(item == imap.FetchItemBodyStructure)) - default: - panic(fmt.Errorf("unknown FETCH item: %#v", item)) - } - return nil + // TODO: BinarySection, BinarySectionSize + + return w.Close() } func (msg *message) envelope() *imap.Envelope { diff --git a/imapserver/session.go b/imapserver/session.go index a9cbb431..b53d8c78 100644 --- a/imapserver/session.go +++ b/imapserver/session.go @@ -66,7 +66,7 @@ type Session interface { Unselect() error Expunge(w *ExpungeWriter, uids *imap.SeqSet) error Search(kind NumKind, criteria *imap.SearchCriteria, options *imap.SearchOptions) (*imap.SearchData, error) - Fetch(w *FetchWriter, kind NumKind, seqSet imap.SeqSet, items []imap.FetchItem, options *imap.FetchOptions) error + Fetch(w *FetchWriter, kind NumKind, seqSet imap.SeqSet, options *imap.FetchOptions) error Store(w *FetchWriter, kind NumKind, seqSet imap.SeqSet, flags *imap.StoreFlags, options *imap.StoreOptions) error Copy(kind NumKind, seqSet imap.SeqSet, dest string) (*imap.CopyData, error) } diff --git a/internal/internal.go b/internal/internal.go index 5be9a5ab..b015d260 100644 --- a/internal/internal.go +++ b/internal/internal.go @@ -15,13 +15,6 @@ const ( DateLayout = "2-Jan-2006" ) -// Fetch items removed in IMAP4rev2. -var ( - FetchItemRFC822 imap.FetchItem = imap.FetchItemKeyword("RFC822") // equivalent to BODY[] - FetchItemRFC822Header imap.FetchItem = imap.FetchItemKeyword("RFC822.HEADER") // equivalent to BODY.PEEK[HEADER] - FetchItemRFC822Text imap.FetchItem = imap.FetchItemKeyword("RFC822.TEXT") // equivalent to BODY[TEXT] -) - const FlagRecent imap.Flag = "\\Recent" // removed in IMAP4rev2 func DecodeDateTime(dec *imapwire.Decoder) (time.Time, error) {