Skip to content

Commit

Permalink
imap: turn FetchItem into FetchOptions
Browse files Browse the repository at this point in the history
  • Loading branch information
emersion committed Jun 21, 2023
1 parent b9ded2a commit af9299b
Show file tree
Hide file tree
Showing 8 changed files with 286 additions and 277 deletions.
51 changes: 14 additions & 37 deletions fetch.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -64,24 +47,18 @@ type FetchItemBodySection struct {
Peek bool
}

func (*FetchItemBodySection) fetchItem() {}

// FetchItemBinarySection is a FETCH BINARY[] data item.
type FetchItemBinarySection struct {
Part []int
Partial *SectionPartial
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
Expand Down
28 changes: 15 additions & 13 deletions imapclient/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand All @@ -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)
Expand Down Expand Up @@ -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)
}
Expand All @@ -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 {
Expand Down
168 changes: 93 additions & 75 deletions imapclient/fetch.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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) {
Expand Down Expand Up @@ -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()
}
Expand All @@ -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()
}
Expand All @@ -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()
}
Expand All @@ -495,22 +513,22 @@ 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()
}

item = FetchItemDataUID{UID: uid}
case "BODY", "BINARY":
if dec.Special('[') {
var section imap.FetchItem
var section interface{}
switch attName {
case "BODY":
var err error
Expand Down Expand Up @@ -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()
}
Expand All @@ -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)
Expand Down
Loading

0 comments on commit af9299b

Please sign in to comment.