Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

imapclient: add helpers to match FETCH response sections #635

Open
wants to merge 1 commit into
base: v2
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 12 additions & 15 deletions imapclient/example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,24 +145,19 @@ func ExampleClient_Fetch() {
var c *imapclient.Client

seqSet := imap.SeqSetNum(1)
bodySection := &imap.FetchItemBodySection{Specifier: imap.PartSpecifierHeader}
fetchOptions := &imap.FetchOptions{
Flags: true,
Envelope: true,
BodySection: []*imap.FetchItemBodySection{
{Specifier: imap.PartSpecifierHeader},
},
Flags: true,
Envelope: true,
BodySection: []*imap.FetchItemBodySection{bodySection},
}
messages, err := c.Fetch(seqSet, fetchOptions).Collect()
if err != nil {
log.Fatalf("FETCH command failed: %v", err)
}

msg := messages[0]
var header []byte
for _, buf := range msg.BodySection {
header = buf
break
}
header := msg.FindBodySection(bodySection)

log.Printf("Flags: %v", msg.Flags)
log.Printf("Subject: %v", msg.Envelope.Subject)
Expand All @@ -173,9 +168,10 @@ func ExampleClient_Fetch_streamBody() {
var c *imapclient.Client

seqSet := imap.SeqSetNum(1)
bodySection := &imap.FetchItemBodySection{}
fetchOptions := &imap.FetchOptions{
UID: true,
BodySection: []*imap.FetchItemBodySection{{}},
BodySection: []*imap.FetchItemBodySection{bodySection},
}
fetchCmd := c.Fetch(seqSet, fetchOptions)
defer fetchCmd.Close()
Expand Down Expand Up @@ -215,8 +211,9 @@ func ExampleClient_Fetch_parseBody() {

// Send a FETCH command to fetch the message body
seqSet := imap.SeqSetNum(1)
bodySection := &imap.FetchItemBodySection{}
fetchOptions := &imap.FetchOptions{
BodySection: []*imap.FetchItemBodySection{{}},
BodySection: []*imap.FetchItemBodySection{bodySection},
}
fetchCmd := c.Fetch(seqSet, fetchOptions)
defer fetchCmd.Close()
Expand All @@ -227,14 +224,14 @@ func ExampleClient_Fetch_parseBody() {
}

// Find the body section in the response
var bodySection imapclient.FetchItemDataBodySection
var bodySectionData imapclient.FetchItemDataBodySection
ok := false
for {
item := msg.Next()
if item == nil {
break
}
bodySection, ok = item.(imapclient.FetchItemDataBodySection)
bodySectionData, ok = item.(imapclient.FetchItemDataBodySection)
if ok {
break
}
Expand All @@ -244,7 +241,7 @@ func ExampleClient_Fetch_parseBody() {
}

// Read the message via the go-message library
mr, err := mail.CreateReader(bodySection.Literal)
mr, err := mail.CreateReader(bodySectionData.Literal)
if err != nil {
log.Fatalf("failed to create mail reader: %v", err)
}
Expand Down
118 changes: 114 additions & 4 deletions imapclient/fetch.go
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,61 @@ func (cmd *FetchCommand) Collect() ([]*FetchMessageBuffer, error) {
return l, cmd.Close()
}

func matchFetchItemBodySection(cmd, resp *imap.FetchItemBodySection) bool {
if cmd.Specifier != resp.Specifier {
return false
}

if !intSliceEqual(cmd.Part, resp.Part) {
return false
}
if !stringSliceEqualFold(cmd.HeaderFields, resp.HeaderFields) {
return false
}
if !stringSliceEqualFold(cmd.HeaderFieldsNot, resp.HeaderFieldsNot) {
return false
}

if (cmd.Partial == nil) != (resp.Partial == nil) {
return false
}
if cmd.Partial != nil && cmd.Partial.Offset != resp.Partial.Offset {
return false
}

// Ignore Partial.Size and Peek: these are not echoed back by the server
return true
}

func matchFetchItemBinarySection(cmd, resp *imap.FetchItemBinarySection) bool {
// Ignore Partial and Peek: these are not echoed back by the server
return intSliceEqual(cmd.Part, resp.Part)
}

func intSliceEqual(a, b []int) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}

func stringSliceEqualFold(a, b []string) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if !strings.EqualFold(a[i], b[i]) {
return false
}
}
return true
}

// FetchMessageData contains a message's FETCH data.
type FetchMessageData struct {
SeqNum uint32
Expand Down Expand Up @@ -320,8 +375,14 @@ type FetchItemDataBodySection struct {

func (FetchItemDataBodySection) fetchItemData() {}

func (item FetchItemDataBodySection) discard() {
io.Copy(io.Discard, item.Literal)
func (dataItem FetchItemDataBodySection) discard() {
io.Copy(io.Discard, dataItem.Literal)
}

// MatchCommand checks whether a section returned by the server in a response
// is compatible with a section requested by the client in a command.
func (dataItem *FetchItemDataBodySection) MatchCommand(item *imap.FetchItemBodySection) bool {
return matchFetchItemBodySection(item, dataItem.Section)
}

// FetchItemDataBinarySection holds data returned by FETCH BINARY[].
Expand All @@ -332,8 +393,14 @@ type FetchItemDataBinarySection struct {

func (FetchItemDataBinarySection) fetchItemData() {}

func (item FetchItemDataBinarySection) discard() {
io.Copy(io.Discard, item.Literal)
func (dataItem FetchItemDataBinarySection) discard() {
io.Copy(io.Discard, dataItem.Literal)
}

// MatchCommand checks whether a section returned by the server in a response
// is compatible with a section requested by the client in a command.
func (dataItem *FetchItemDataBinarySection) MatchCommand(item *imap.FetchItemBinarySection) bool {
return matchFetchItemBinarySection(item, dataItem.Section)
}

// FetchItemDataFlags holds data returned by FETCH FLAGS.
Expand Down Expand Up @@ -388,6 +455,13 @@ type FetchItemDataBinarySectionSize struct {

func (FetchItemDataBinarySectionSize) fetchItemData() {}

// MatchCommand checks whether a section size returned by the server in a
// response is compatible with a section size requested by the client in a
// command.
func (data *FetchItemDataBinarySectionSize) MatchCommand(item *imap.FetchItemBinarySectionSize) bool {
return intSliceEqual(item.Part, data.Part)
}

// FetchItemDataModSeq holds data returned by FETCH MODSEQ.
//
// This requires the CONDSTORE extension.
Expand Down Expand Up @@ -456,6 +530,42 @@ func (buf *FetchMessageBuffer) populateItemData(item FetchItemData) error {
return nil
}

// FindBodySection returns the contents of a requested body section.
//
// If the body section is not found, nil is returned.
func (buf *FetchMessageBuffer) FindBodySection(section *imap.FetchItemBodySection) []byte {
for s, b := range buf.BodySection {
if matchFetchItemBodySection(section, s) {
return b
}
}
return nil
}

// FindBinarySection returns the contents of a requested binary section.
//
// If the binary section is not found, nil is returned.
func (buf *FetchMessageBuffer) FindBinarySection(section *imap.FetchItemBinarySection) []byte {
for s, b := range buf.BinarySection {
if matchFetchItemBinarySection(section, s) {
return b
}
}
return nil
}

// FindBinarySectionSize returns a requested binary section size.
//
// If the binary section size is not found, false is returned.
func (buf *FetchMessageBuffer) FindBinarySectionSize(part []int) (uint32, bool) {
for _, s := range buf.BinarySectionSize {
if intSliceEqual(part, s.Part) {
return s.Size, true
}
}
return 0, false
}

func (c *Client) handleFetch(seqNum uint32) error {
dec := c.dec

Expand Down