diff --git a/imap.go b/imap.go index 704c1f3e..5eebf92a 100644 --- a/imap.go +++ b/imap.go @@ -36,6 +36,8 @@ const ( // Items FetchBody FetchItem = "BODY" + FetchBodyPeek FetchItem = "BODY.PEEK[]" + FetchXGmMsgID FetchItem = "X-GM-MSGID" FetchBodyStructure FetchItem = "BODYSTRUCTURE" FetchEnvelope FetchItem = "ENVELOPE" FetchFlags FetchItem = "FLAGS" diff --git a/message.go b/message.go index bd28325d..e1a18db6 100644 --- a/message.go +++ b/message.go @@ -181,6 +181,9 @@ type Message struct { // because some bad IMAP clients (looking at you, Outlook!) refuse responses // containing items in a different order. itemsOrder []FetchItem + + // Google specific unique identifier + XGmMsgID uint64 } // Create a new empty message that will contain the specified items. @@ -259,6 +262,8 @@ func (m *Message) Parse(fields []interface{}) error { m.Size, _ = ParseNumber(f) case FetchUid: m.Uid, _ = ParseNumber(f) + case FetchXGmMsgID: + m.XGmMsgID, _ = ParseInt64(f) default: // Likely to be a section of the body // First check that the section name is correct diff --git a/read.go b/read.go index 112ee28b..8ce135e7 100644 --- a/read.go +++ b/read.go @@ -87,6 +87,31 @@ func ParseNumber(f interface{}) (uint32, error) { return uint32(nbr), nil } +// ParseInt64 parses a number. +func ParseInt64(f interface{}) (uint64, error) { + // Useful for tests + if n, ok := f.(uint64); ok { + return n, nil + } + + var s string + switch f := f.(type) { + case RawString: + s = string(f) + case string: + s = f + default: + return 0, newParseError("expected a number, got a non-atom") + } + + nbr, err := strconv.ParseUint(s, 10, 64) + if err != nil { + return 0, &parseError{err} + } + + return nbr, nil +} + // ParseString parses a string, which is either a literal, a quoted string or an // atom. func ParseString(f interface{}) (string, error) { diff --git a/search.go b/search.go index 0ecb24d2..afbba9bd 100644 --- a/search.go +++ b/search.go @@ -56,6 +56,23 @@ func popSearchField(fields []interface{}) (interface{}, []interface{}, error) { return fields[0], fields[1:], nil } +func parseRawField(fields []interface{}) (interface{}, []interface{}) { + if len(fields) == 0 { + return nil, fields + } + + if _, ok := fields[0].([]interface{}); ok { + return nil, fields + } + + return fields[0], fields[1:] +} + +type Raw struct { + Key string + Value interface{} +} + // SearchCriteria is a search criteria. A message matches the criteria if and // only if it matches each one of its fields. type SearchCriteria struct { @@ -80,6 +97,8 @@ type SearchCriteria struct { Not []*SearchCriteria // Each criteria doesn't match Or [][2]*SearchCriteria // Each criteria pair has at least one match of two + + Raw []Raw } // NewSearchCriteria creates a new search criteria. @@ -250,8 +269,12 @@ func (c *SearchCriteria) parseField(fields []interface{}, charsetReader func(io. c.WithoutFlags = append(c.WithoutFlags, CanonicalFlag(maybeString(f))) } default: // Try to parse a sequence set - if c.SeqNum, err = ParseSeqSet(key); err != nil { - return nil, err + if seqNum, err := ParseSeqSet(key); err == nil { + c.SeqNum = seqNum + } else { + var rawValue interface{} + rawValue, fields = parseRawField(fields) + c.Raw = append(c.Raw, Raw{Key: key, Value: rawValue}) } } @@ -362,6 +385,14 @@ func (c *SearchCriteria) Format() []interface{} { fields = append(fields, RawString("OR"), or[0].Format(), or[1].Format()) } + for _, raw := range c.Raw { + field := []interface{}{RawString(raw.Key)} + if raw.Value != nil { + field = append(field, raw.Value) + } + fields = append(fields, field) + } + // Not a single criteria given, add ALL criteria as fallback if len(fields) == 0 { fields = append(fields, RawString("ALL")) diff --git a/search_test.go b/search_test.go index 81c3e5e0..a72b580a 100644 --- a/search_test.go +++ b/search_test.go @@ -2,6 +2,7 @@ package imap import ( "bytes" + "fmt" "io" "net/textproto" "reflect" @@ -65,6 +66,26 @@ var searchCriteriaTests = []struct { }}, }, }, + { + expected: `((X-GM-THRID) (X-GM-RAW "has:attachment") (X-GM-MSGID))`, + criteria: &SearchCriteria{ + Raw: []Raw{ + {"X-GM-THRID", nil}, + {"X-GM-RAW", "has:attachment"}, + {"X-GM-MSGID", nil}, + }, + }, + }, + { + expected: `((X-GM-THRID) (X-GM-MSGID) (X-GM-RAW "has:attachment"))`, + criteria: &SearchCriteria{ + Raw: []Raw{ + {"X-GM-THRID", nil}, + {"X-GM-MSGID", nil}, + {"X-GM-RAW", "has:attachment"}, + }, + }, + }, { expected: "(ALL)", criteria: &SearchCriteria{}, @@ -93,6 +114,7 @@ func TestSearchCriteria_Parse(t *testing.T) { b := bytes.NewBuffer([]byte(test.expected)) r := NewReader(b) fields, _ := r.ReadFields() + fmt.Println(fields) if err := criteria.ParseWithCharset(fields[0].([]interface{}), nil); err != nil { t.Errorf("Cannot parse search criteria for #%v: %v", i+1, err)