1327 lines
30 KiB
Go
1327 lines
30 KiB
Go
package imapclient
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
netmail "net/mail"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/emersion/go-imap/v2"
|
|
"github.com/emersion/go-imap/v2/internal"
|
|
"github.com/emersion/go-imap/v2/internal/imapwire"
|
|
"github.com/emersion/go-message/mail"
|
|
)
|
|
|
|
// Fetch sends a FETCH command.
|
|
//
|
|
// The caller must fully consume the FetchCommand. A simple way to do so is to
|
|
// defer a call to FetchCommand.Close.
|
|
//
|
|
// A nil options pointer is equivalent to a zero options value.
|
|
func (c *Client) Fetch(numSet imap.NumSet, options *imap.FetchOptions) *FetchCommand {
|
|
if options == nil {
|
|
options = new(imap.FetchOptions)
|
|
}
|
|
|
|
numKind := imapwire.NumSetKind(numSet)
|
|
|
|
cmd := &FetchCommand{
|
|
numSet: numSet,
|
|
msgs: make(chan *FetchMessageData, 128),
|
|
}
|
|
enc := c.beginCommand(uidCmdName("FETCH", numKind), cmd)
|
|
enc.SP().NumSet(numSet).SP()
|
|
writeFetchItems(enc.Encoder, numKind, options)
|
|
if options.ChangedSince != 0 {
|
|
enc.SP().Special('(').Atom("CHANGEDSINCE").SP().ModSeq(options.ChangedSince).Special(')')
|
|
}
|
|
enc.end()
|
|
return cmd
|
|
}
|
|
|
|
func writeFetchItems(enc *imapwire.Encoder, numKind imapwire.NumKind, 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 || numKind == imapwire.NumKindUID {
|
|
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,
|
|
"MODSEQ": options.ModSeq,
|
|
}
|
|
for k, req := range m {
|
|
if req {
|
|
listEnc.Item().Atom(k)
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
listEnc.End()
|
|
}
|
|
|
|
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")
|
|
}
|
|
|
|
if len(headerList) > 0 {
|
|
enc.SP().List(len(headerList), func(i int) {
|
|
enc.String(headerList[i])
|
|
})
|
|
}
|
|
}
|
|
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) {
|
|
if len(part) == 0 {
|
|
return
|
|
}
|
|
|
|
var l []string
|
|
for _, num := range part {
|
|
l = append(l, fmt.Sprintf("%v", num))
|
|
}
|
|
enc.Atom(strings.Join(l, "."))
|
|
}
|
|
|
|
func writeSectionPartial(enc *imapwire.Encoder, partial *imap.SectionPartial) {
|
|
if partial == nil {
|
|
return
|
|
}
|
|
enc.Special('<').Number64(partial.Offset).Special('.').Number64(partial.Size).Special('>')
|
|
}
|
|
|
|
// FetchCommand is a FETCH command.
|
|
type FetchCommand struct {
|
|
commandBase
|
|
|
|
numSet imap.NumSet
|
|
recvSeqSet imap.SeqSet
|
|
recvUIDSet imap.UIDSet
|
|
|
|
msgs chan *FetchMessageData
|
|
prev *FetchMessageData
|
|
}
|
|
|
|
func (cmd *FetchCommand) recvSeqNum(seqNum uint32) bool {
|
|
set, ok := cmd.numSet.(imap.SeqSet)
|
|
if !ok || !set.Contains(seqNum) {
|
|
return false
|
|
}
|
|
|
|
if cmd.recvSeqSet.Contains(seqNum) {
|
|
return false
|
|
}
|
|
|
|
cmd.recvSeqSet.AddNum(seqNum)
|
|
return true
|
|
}
|
|
|
|
func (cmd *FetchCommand) recvUID(uid imap.UID) bool {
|
|
set, ok := cmd.numSet.(imap.UIDSet)
|
|
if !ok || !set.Contains(uid) {
|
|
return false
|
|
}
|
|
|
|
if cmd.recvUIDSet.Contains(uid) {
|
|
return false
|
|
}
|
|
|
|
cmd.recvUIDSet.AddNum(uid)
|
|
return true
|
|
}
|
|
|
|
// Next advances to the next message.
|
|
//
|
|
// On success, the message is returned. On error or if there are no more
|
|
// messages, nil is returned. To check the error value, use Close.
|
|
func (cmd *FetchCommand) Next() *FetchMessageData {
|
|
if cmd.prev != nil {
|
|
cmd.prev.discard()
|
|
}
|
|
cmd.prev = <-cmd.msgs
|
|
return cmd.prev
|
|
}
|
|
|
|
// Close releases the command.
|
|
//
|
|
// Calling Close unblocks the IMAP client decoder and lets it read the next
|
|
// responses. Next will always return nil after Close.
|
|
func (cmd *FetchCommand) Close() error {
|
|
for cmd.Next() != nil {
|
|
// ignore
|
|
}
|
|
return cmd.wait()
|
|
}
|
|
|
|
// Collect accumulates message data into a list.
|
|
//
|
|
// This method will read and store message contents in memory. This is
|
|
// acceptable when the message contents have a reasonable size, but may not be
|
|
// suitable when fetching e.g. attachments.
|
|
//
|
|
// This is equivalent to calling Next repeatedly and then Close.
|
|
func (cmd *FetchCommand) Collect() ([]*FetchMessageBuffer, error) {
|
|
defer cmd.Close()
|
|
|
|
var l []*FetchMessageBuffer
|
|
for {
|
|
msg := cmd.Next()
|
|
if msg == nil {
|
|
break
|
|
}
|
|
|
|
buf, err := msg.Collect()
|
|
if err != nil {
|
|
return l, err
|
|
}
|
|
|
|
l = append(l, buf)
|
|
}
|
|
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
|
|
|
|
items chan FetchItemData
|
|
prev FetchItemData
|
|
}
|
|
|
|
// Next advances to the next data item for this message.
|
|
//
|
|
// If there is one or more data items left, the next item is returned.
|
|
// Otherwise nil is returned.
|
|
func (data *FetchMessageData) Next() FetchItemData {
|
|
if d, ok := data.prev.(discarder); ok {
|
|
d.discard()
|
|
}
|
|
|
|
item := <-data.items
|
|
data.prev = item
|
|
return item
|
|
}
|
|
|
|
func (data *FetchMessageData) discard() {
|
|
for {
|
|
if item := data.Next(); item == nil {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
// Collect accumulates message data into a struct.
|
|
//
|
|
// This method will read and store message contents in memory. This is
|
|
// acceptable when the message contents have a reasonable size, but may not be
|
|
// suitable when fetching e.g. attachments.
|
|
func (data *FetchMessageData) Collect() (*FetchMessageBuffer, error) {
|
|
defer data.discard()
|
|
|
|
buf := &FetchMessageBuffer{SeqNum: data.SeqNum}
|
|
for {
|
|
item := data.Next()
|
|
if item == nil {
|
|
break
|
|
}
|
|
if err := buf.populateItemData(item); err != nil {
|
|
return buf, err
|
|
}
|
|
}
|
|
return buf, nil
|
|
}
|
|
|
|
// FetchItemData contains a message's FETCH item data.
|
|
type FetchItemData interface {
|
|
fetchItemData()
|
|
}
|
|
|
|
var (
|
|
_ FetchItemData = FetchItemDataBodySection{}
|
|
_ FetchItemData = FetchItemDataBinarySection{}
|
|
_ FetchItemData = FetchItemDataFlags{}
|
|
_ FetchItemData = FetchItemDataEnvelope{}
|
|
_ FetchItemData = FetchItemDataInternalDate{}
|
|
_ FetchItemData = FetchItemDataRFC822Size{}
|
|
_ FetchItemData = FetchItemDataUID{}
|
|
_ FetchItemData = FetchItemDataBodyStructure{}
|
|
)
|
|
|
|
type discarder interface {
|
|
discard()
|
|
}
|
|
|
|
var (
|
|
_ discarder = FetchItemDataBodySection{}
|
|
_ discarder = FetchItemDataBinarySection{}
|
|
)
|
|
|
|
// FetchItemDataBodySection holds data returned by FETCH BODY[].
|
|
//
|
|
// Literal might be nil.
|
|
type FetchItemDataBodySection struct {
|
|
Section *imap.FetchItemBodySection
|
|
Literal imap.LiteralReader
|
|
}
|
|
|
|
func (FetchItemDataBodySection) fetchItemData() {}
|
|
|
|
func (item FetchItemDataBodySection) discard() {
|
|
if item.Literal != nil {
|
|
io.Copy(io.Discard, item.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[].
|
|
//
|
|
// Literal might be nil.
|
|
type FetchItemDataBinarySection struct {
|
|
Section *imap.FetchItemBinarySection
|
|
Literal imap.LiteralReader
|
|
}
|
|
|
|
func (FetchItemDataBinarySection) fetchItemData() {}
|
|
|
|
func (item FetchItemDataBinarySection) discard() {
|
|
if item.Literal != nil {
|
|
io.Copy(io.Discard, item.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.
|
|
type FetchItemDataFlags struct {
|
|
Flags []imap.Flag
|
|
}
|
|
|
|
func (FetchItemDataFlags) fetchItemData() {}
|
|
|
|
// FetchItemDataEnvelope holds data returned by FETCH ENVELOPE.
|
|
type FetchItemDataEnvelope struct {
|
|
Envelope *imap.Envelope
|
|
}
|
|
|
|
func (FetchItemDataEnvelope) fetchItemData() {}
|
|
|
|
// FetchItemDataInternalDate holds data returned by FETCH INTERNALDATE.
|
|
type FetchItemDataInternalDate struct {
|
|
Time time.Time
|
|
}
|
|
|
|
func (FetchItemDataInternalDate) fetchItemData() {}
|
|
|
|
// FetchItemDataRFC822Size holds data returned by FETCH RFC822.SIZE.
|
|
type FetchItemDataRFC822Size struct {
|
|
Size int64
|
|
}
|
|
|
|
func (FetchItemDataRFC822Size) fetchItemData() {}
|
|
|
|
// FetchItemDataUID holds data returned by FETCH UID.
|
|
type FetchItemDataUID struct {
|
|
UID imap.UID
|
|
}
|
|
|
|
func (FetchItemDataUID) fetchItemData() {}
|
|
|
|
// FetchItemDataBodyStructure holds data returned by FETCH BODYSTRUCTURE or
|
|
// FETCH BODY.
|
|
type FetchItemDataBodyStructure struct {
|
|
BodyStructure imap.BodyStructure
|
|
IsExtended bool // True if BODYSTRUCTURE, false if BODY
|
|
}
|
|
|
|
func (FetchItemDataBodyStructure) fetchItemData() {}
|
|
|
|
// FetchItemDataBinarySectionSize holds data returned by FETCH BINARY.SIZE[].
|
|
type FetchItemDataBinarySectionSize struct {
|
|
Part []int
|
|
Size uint32
|
|
}
|
|
|
|
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.
|
|
type FetchItemDataModSeq struct {
|
|
ModSeq uint64
|
|
}
|
|
|
|
func (FetchItemDataModSeq) fetchItemData() {}
|
|
|
|
// FetchBodySectionBuffer is a buffer for the data returned by
|
|
// FetchItemBodySection.
|
|
type FetchBodySectionBuffer struct {
|
|
Section *imap.FetchItemBodySection
|
|
Bytes []byte
|
|
}
|
|
|
|
// FetchBinarySectionBuffer is a buffer for the data returned by
|
|
// FetchItemBinarySection.
|
|
type FetchBinarySectionBuffer struct {
|
|
Section *imap.FetchItemBinarySection
|
|
Bytes []byte
|
|
}
|
|
|
|
// FetchMessageBuffer is a buffer for the data returned by FetchMessageData.
|
|
//
|
|
// The SeqNum field is always populated. All remaining fields are optional.
|
|
type FetchMessageBuffer struct {
|
|
SeqNum uint32
|
|
Flags []imap.Flag
|
|
Envelope *imap.Envelope
|
|
InternalDate time.Time
|
|
RFC822Size int64
|
|
UID imap.UID
|
|
BodyStructure imap.BodyStructure
|
|
BodySection []FetchBodySectionBuffer
|
|
BinarySection []FetchBinarySectionBuffer
|
|
BinarySectionSize []FetchItemDataBinarySectionSize
|
|
ModSeq uint64 // requires CONDSTORE
|
|
}
|
|
|
|
func (buf *FetchMessageBuffer) populateItemData(item FetchItemData) error {
|
|
switch item := item.(type) {
|
|
case FetchItemDataBodySection:
|
|
var b []byte
|
|
if item.Literal != nil {
|
|
var err error
|
|
b, err = io.ReadAll(item.Literal)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
buf.BodySection = append(buf.BodySection, FetchBodySectionBuffer{
|
|
Section: item.Section,
|
|
Bytes: b,
|
|
})
|
|
case FetchItemDataBinarySection:
|
|
var b []byte
|
|
if item.Literal != nil {
|
|
var err error
|
|
b, err = io.ReadAll(item.Literal)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
buf.BinarySection = append(buf.BinarySection, FetchBinarySectionBuffer{
|
|
Section: item.Section,
|
|
Bytes: b,
|
|
})
|
|
case FetchItemDataFlags:
|
|
buf.Flags = item.Flags
|
|
case FetchItemDataEnvelope:
|
|
buf.Envelope = item.Envelope
|
|
case FetchItemDataInternalDate:
|
|
buf.InternalDate = item.Time
|
|
case FetchItemDataRFC822Size:
|
|
buf.RFC822Size = item.Size
|
|
case FetchItemDataUID:
|
|
buf.UID = item.UID
|
|
case FetchItemDataBodyStructure:
|
|
buf.BodyStructure = item.BodyStructure
|
|
case FetchItemDataBinarySectionSize:
|
|
buf.BinarySectionSize = append(buf.BinarySectionSize, item)
|
|
case FetchItemDataModSeq:
|
|
buf.ModSeq = item.ModSeq
|
|
default:
|
|
panic(fmt.Errorf("unsupported fetch item data %T", item))
|
|
}
|
|
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 := range buf.BodySection {
|
|
if matchFetchItemBodySection(section, s.Section) {
|
|
return s.Bytes
|
|
}
|
|
}
|
|
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 := range buf.BinarySection {
|
|
if matchFetchItemBinarySection(section, s.Section) {
|
|
return s.Bytes
|
|
}
|
|
}
|
|
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
|
|
|
|
items := make(chan FetchItemData, 32)
|
|
defer close(items)
|
|
|
|
msg := &FetchMessageData{SeqNum: seqNum, items: items}
|
|
|
|
// We're in a tricky situation: to know whether this FETCH response needs
|
|
// to be handled by a pending command, we may need to look at the UID in
|
|
// the response data. But the response data comes in in a streaming
|
|
// fashion: it can contain literals. Assume that the UID will be returned
|
|
// before any literal.
|
|
var uid imap.UID
|
|
handled := false
|
|
handleMsg := func() {
|
|
if handled {
|
|
return
|
|
}
|
|
|
|
cmd := c.findPendingCmdFunc(func(anyCmd command) bool {
|
|
cmd, ok := anyCmd.(*FetchCommand)
|
|
if !ok {
|
|
return false
|
|
}
|
|
|
|
// Skip if we haven't requested or already handled this message
|
|
if _, ok := cmd.numSet.(imap.UIDSet); ok {
|
|
return uid != 0 && cmd.recvUID(uid)
|
|
} else {
|
|
return seqNum != 0 && cmd.recvSeqNum(seqNum)
|
|
}
|
|
})
|
|
if cmd != nil {
|
|
cmd := cmd.(*FetchCommand)
|
|
cmd.msgs <- msg
|
|
} else if handler := c.options.unilateralDataHandler().Fetch; handler != nil {
|
|
go handler(msg)
|
|
} else {
|
|
go msg.discard()
|
|
}
|
|
|
|
handled = true
|
|
}
|
|
defer handleMsg()
|
|
|
|
numAtts := 0
|
|
return dec.ExpectList(func() error {
|
|
var attName string
|
|
if !dec.Expect(dec.Func(&attName, isMsgAttNameChar), "msg-att name") {
|
|
return dec.Err()
|
|
}
|
|
attName = strings.ToUpper(attName)
|
|
|
|
var (
|
|
item FetchItemData
|
|
done chan struct{}
|
|
)
|
|
switch attName {
|
|
case "FLAGS":
|
|
if !dec.ExpectSP() {
|
|
return dec.Err()
|
|
}
|
|
|
|
flags, err := internal.ExpectFlagList(dec)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
item = FetchItemDataFlags{Flags: flags}
|
|
case "ENVELOPE":
|
|
if !dec.ExpectSP() {
|
|
return dec.Err()
|
|
}
|
|
|
|
envelope, err := readEnvelope(dec, &c.options)
|
|
if err != nil {
|
|
return fmt.Errorf("in envelope: %v", err)
|
|
}
|
|
|
|
item = FetchItemDataEnvelope{Envelope: envelope}
|
|
case "INTERNALDATE":
|
|
if !dec.ExpectSP() {
|
|
return dec.Err()
|
|
}
|
|
|
|
t, err := internal.ExpectDateTime(dec)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
item = FetchItemDataInternalDate{Time: t}
|
|
case "RFC822.SIZE":
|
|
var size int64
|
|
if !dec.ExpectSP() || !dec.ExpectNumber64(&size) {
|
|
return dec.Err()
|
|
}
|
|
|
|
item = FetchItemDataRFC822Size{Size: size}
|
|
case "UID":
|
|
if !dec.ExpectSP() || !dec.ExpectUID(&uid) {
|
|
return dec.Err()
|
|
}
|
|
|
|
item = FetchItemDataUID{UID: uid}
|
|
case "BODY", "BINARY":
|
|
if dec.Special('[') {
|
|
var section interface{}
|
|
switch attName {
|
|
case "BODY":
|
|
var err error
|
|
section, err = readSectionSpec(dec)
|
|
if err != nil {
|
|
return fmt.Errorf("in section-spec: %v", err)
|
|
}
|
|
case "BINARY":
|
|
part, dot := readSectionPart(dec)
|
|
if dot {
|
|
return fmt.Errorf("in section-binary: expected number after dot")
|
|
}
|
|
if !dec.ExpectSpecial(']') {
|
|
return dec.Err()
|
|
}
|
|
section = &imap.FetchItemBinarySection{Part: part}
|
|
}
|
|
|
|
if !dec.ExpectSP() {
|
|
return dec.Err()
|
|
}
|
|
|
|
// Ignore literal8 marker, if any
|
|
if attName == "BINARY" {
|
|
dec.Special('~')
|
|
}
|
|
|
|
lit, _, ok := dec.ExpectNStringReader()
|
|
if !ok {
|
|
return dec.Err()
|
|
}
|
|
|
|
var fetchLit imap.LiteralReader
|
|
if lit != nil {
|
|
done = make(chan struct{})
|
|
fetchLit = &fetchLiteralReader{
|
|
LiteralReader: lit,
|
|
ch: done,
|
|
}
|
|
}
|
|
|
|
switch section := section.(type) {
|
|
case *imap.FetchItemBodySection:
|
|
item = FetchItemDataBodySection{
|
|
Section: section,
|
|
Literal: fetchLit,
|
|
}
|
|
case *imap.FetchItemBinarySection:
|
|
item = FetchItemDataBinarySection{
|
|
Section: section,
|
|
Literal: fetchLit,
|
|
}
|
|
}
|
|
break
|
|
}
|
|
if !dec.Expect(attName == "BODY", "'['") {
|
|
return dec.Err()
|
|
}
|
|
fallthrough
|
|
case "BODYSTRUCTURE":
|
|
if !dec.ExpectSP() {
|
|
return dec.Err()
|
|
}
|
|
|
|
bodyStruct, err := readBody(dec, &c.options)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
item = FetchItemDataBodyStructure{
|
|
BodyStructure: bodyStruct,
|
|
IsExtended: attName == "BODYSTRUCTURE",
|
|
}
|
|
case "BINARY.SIZE":
|
|
if !dec.ExpectSpecial('[') {
|
|
return dec.Err()
|
|
}
|
|
part, dot := readSectionPart(dec)
|
|
if dot {
|
|
return fmt.Errorf("in section-binary: expected number after dot")
|
|
}
|
|
|
|
var size uint32
|
|
if !dec.ExpectSpecial(']') || !dec.ExpectSP() || !dec.ExpectNumber(&size) {
|
|
return dec.Err()
|
|
}
|
|
|
|
item = FetchItemDataBinarySectionSize{
|
|
Part: part,
|
|
Size: size,
|
|
}
|
|
case "MODSEQ":
|
|
var modSeq uint64
|
|
if !dec.ExpectSP() || !dec.ExpectSpecial('(') || !dec.ExpectModSeq(&modSeq) || !dec.ExpectSpecial(')') {
|
|
return dec.Err()
|
|
}
|
|
item = FetchItemDataModSeq{ModSeq: modSeq}
|
|
default:
|
|
return fmt.Errorf("unsupported msg-att name: %q", attName)
|
|
}
|
|
|
|
numAtts++
|
|
if numAtts > cap(items) || done != nil {
|
|
// To avoid deadlocking we need to ask the message handler to
|
|
// consume the data
|
|
handleMsg()
|
|
}
|
|
|
|
if done != nil {
|
|
c.setReadTimeout(literalReadTimeout)
|
|
}
|
|
items <- item
|
|
if done != nil {
|
|
<-done
|
|
c.setReadTimeout(respReadTimeout)
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func isMsgAttNameChar(ch byte) bool {
|
|
return ch != '[' && imapwire.IsAtomChar(ch)
|
|
}
|
|
|
|
func readEnvelope(dec *imapwire.Decoder, options *Options) (*imap.Envelope, error) {
|
|
var envelope imap.Envelope
|
|
|
|
if !dec.ExpectSpecial('(') {
|
|
return nil, dec.Err()
|
|
}
|
|
|
|
var date, subject string
|
|
if !dec.ExpectNString(&date) || !dec.ExpectSP() || !dec.ExpectNString(&subject) || !dec.ExpectSP() {
|
|
return nil, dec.Err()
|
|
}
|
|
// TODO: handle error
|
|
envelope.Date, _ = netmail.ParseDate(date)
|
|
envelope.Subject, _ = options.decodeText(subject)
|
|
|
|
addrLists := []struct {
|
|
name string
|
|
out *[]imap.Address
|
|
}{
|
|
{"env-from", &envelope.From},
|
|
{"env-sender", &envelope.Sender},
|
|
{"env-reply-to", &envelope.ReplyTo},
|
|
{"env-to", &envelope.To},
|
|
{"env-cc", &envelope.Cc},
|
|
{"env-bcc", &envelope.Bcc},
|
|
}
|
|
for _, addrList := range addrLists {
|
|
l, err := readAddressList(dec, options)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("in %v: %v", addrList.name, err)
|
|
} else if !dec.ExpectSP() {
|
|
return nil, dec.Err()
|
|
}
|
|
*addrList.out = l
|
|
}
|
|
|
|
var inReplyTo, messageID string
|
|
if !dec.ExpectNString(&inReplyTo) || !dec.ExpectSP() || !dec.ExpectNString(&messageID) {
|
|
return nil, dec.Err()
|
|
}
|
|
// TODO: handle errors
|
|
envelope.InReplyTo, _ = parseMsgIDList(inReplyTo)
|
|
envelope.MessageID, _ = parseMsgID(messageID)
|
|
|
|
if !dec.ExpectSpecial(')') {
|
|
return nil, dec.Err()
|
|
}
|
|
return &envelope, nil
|
|
}
|
|
|
|
func readAddressList(dec *imapwire.Decoder, options *Options) ([]imap.Address, error) {
|
|
var l []imap.Address
|
|
err := dec.ExpectNList(func() error {
|
|
addr, err := readAddress(dec, options)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
l = append(l, *addr)
|
|
return nil
|
|
})
|
|
return l, err
|
|
}
|
|
|
|
func readAddress(dec *imapwire.Decoder, options *Options) (*imap.Address, error) {
|
|
var (
|
|
addr imap.Address
|
|
name string
|
|
obsRoute string
|
|
)
|
|
ok := dec.ExpectSpecial('(') &&
|
|
dec.ExpectNString(&name) && dec.ExpectSP() &&
|
|
dec.ExpectNString(&obsRoute) && dec.ExpectSP() &&
|
|
dec.ExpectNString(&addr.Mailbox) && dec.ExpectSP() &&
|
|
dec.ExpectNString(&addr.Host) && dec.ExpectSpecial(')')
|
|
if !ok {
|
|
return nil, fmt.Errorf("in address: %v", dec.Err())
|
|
}
|
|
// TODO: handle error
|
|
addr.Name, _ = options.decodeText(name)
|
|
return &addr, nil
|
|
}
|
|
|
|
func parseMsgID(s string) (string, error) {
|
|
var h mail.Header
|
|
h.Set("Message-Id", s)
|
|
return h.MessageID()
|
|
}
|
|
|
|
func parseMsgIDList(s string) ([]string, error) {
|
|
var h mail.Header
|
|
h.Set("In-Reply-To", s)
|
|
return h.MsgIDList("In-Reply-To")
|
|
}
|
|
|
|
func readBody(dec *imapwire.Decoder, options *Options) (imap.BodyStructure, error) {
|
|
if !dec.ExpectSpecial('(') {
|
|
return nil, dec.Err()
|
|
}
|
|
|
|
var (
|
|
mediaType string
|
|
token string
|
|
bs imap.BodyStructure
|
|
err error
|
|
)
|
|
if dec.String(&mediaType) {
|
|
token = "body-type-1part"
|
|
bs, err = readBodyType1part(dec, mediaType, options)
|
|
} else {
|
|
token = "body-type-mpart"
|
|
bs, err = readBodyTypeMpart(dec, options)
|
|
}
|
|
if err != nil {
|
|
return nil, fmt.Errorf("in %v: %v", token, err)
|
|
}
|
|
|
|
for dec.SP() {
|
|
if !dec.DiscardValue() {
|
|
return nil, dec.Err()
|
|
}
|
|
}
|
|
|
|
if !dec.ExpectSpecial(')') {
|
|
return nil, dec.Err()
|
|
}
|
|
|
|
return bs, nil
|
|
}
|
|
|
|
func readBodyType1part(dec *imapwire.Decoder, typ string, options *Options) (*imap.BodyStructureSinglePart, error) {
|
|
bs := imap.BodyStructureSinglePart{Type: typ}
|
|
|
|
if !dec.ExpectSP() || !dec.ExpectString(&bs.Subtype) || !dec.ExpectSP() {
|
|
return nil, dec.Err()
|
|
}
|
|
var err error
|
|
bs.Params, err = readBodyFldParam(dec, options)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var description string
|
|
if !dec.ExpectSP() || !dec.ExpectNString(&bs.ID) || !dec.ExpectSP() || !dec.ExpectNString(&description) || !dec.ExpectSP() || !dec.ExpectNString(&bs.Encoding) || !dec.ExpectSP() || !dec.ExpectBodyFldOctets(&bs.Size) {
|
|
return nil, dec.Err()
|
|
}
|
|
|
|
// Content-Transfer-Encoding should always be set, but some non-standard
|
|
// servers leave it NIL. Default to 7BIT.
|
|
if bs.Encoding == "" {
|
|
bs.Encoding = "7BIT"
|
|
}
|
|
|
|
// TODO: handle errors
|
|
bs.Description, _ = options.decodeText(description)
|
|
|
|
// Some servers don't include the extra fields for message and text
|
|
// (see https://github.com/emersion/go-imap/issues/557)
|
|
hasSP := dec.SP()
|
|
if !hasSP {
|
|
return &bs, nil
|
|
}
|
|
|
|
if strings.EqualFold(bs.Type, "message") && (strings.EqualFold(bs.Subtype, "rfc822") || strings.EqualFold(bs.Subtype, "global")) {
|
|
var msg imap.BodyStructureMessageRFC822
|
|
|
|
msg.Envelope, err = readEnvelope(dec, options)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if !dec.ExpectSP() {
|
|
return nil, dec.Err()
|
|
}
|
|
|
|
msg.BodyStructure, err = readBody(dec, options)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if !dec.ExpectSP() || !dec.ExpectNumber64(&msg.NumLines) {
|
|
return nil, dec.Err()
|
|
}
|
|
|
|
bs.MessageRFC822 = &msg
|
|
hasSP = false
|
|
} else if strings.EqualFold(bs.Type, "text") {
|
|
var text imap.BodyStructureText
|
|
|
|
if !dec.ExpectNumber64(&text.NumLines) {
|
|
return nil, dec.Err()
|
|
}
|
|
|
|
bs.Text = &text
|
|
hasSP = false
|
|
}
|
|
|
|
if !hasSP {
|
|
hasSP = dec.SP()
|
|
}
|
|
if hasSP {
|
|
bs.Extended, err = readBodyExt1part(dec, options)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("in body-ext-1part: %v", err)
|
|
}
|
|
}
|
|
|
|
return &bs, nil
|
|
}
|
|
|
|
func readBodyExt1part(dec *imapwire.Decoder, options *Options) (*imap.BodyStructureSinglePartExt, error) {
|
|
var ext imap.BodyStructureSinglePartExt
|
|
|
|
var md5 string
|
|
if !dec.ExpectNString(&md5) {
|
|
return nil, dec.Err()
|
|
}
|
|
|
|
if !dec.SP() {
|
|
return &ext, nil
|
|
}
|
|
|
|
var err error
|
|
ext.Disposition, err = readBodyFldDsp(dec, options)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("in body-fld-dsp: %v", err)
|
|
}
|
|
|
|
if !dec.SP() {
|
|
return &ext, nil
|
|
}
|
|
|
|
ext.Language, err = readBodyFldLang(dec)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("in body-fld-lang: %v", err)
|
|
}
|
|
|
|
if !dec.SP() {
|
|
return &ext, nil
|
|
}
|
|
|
|
if !dec.ExpectNString(&ext.Location) {
|
|
return nil, dec.Err()
|
|
}
|
|
|
|
return &ext, nil
|
|
}
|
|
|
|
func readBodyTypeMpart(dec *imapwire.Decoder, options *Options) (*imap.BodyStructureMultiPart, error) {
|
|
var bs imap.BodyStructureMultiPart
|
|
|
|
for {
|
|
child, err := readBody(dec, options)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
bs.Children = append(bs.Children, child)
|
|
|
|
if dec.SP() && dec.String(&bs.Subtype) {
|
|
break
|
|
}
|
|
}
|
|
|
|
if dec.SP() {
|
|
var err error
|
|
bs.Extended, err = readBodyExtMpart(dec, options)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("in body-ext-mpart: %v", err)
|
|
}
|
|
}
|
|
|
|
return &bs, nil
|
|
}
|
|
|
|
func readBodyExtMpart(dec *imapwire.Decoder, options *Options) (*imap.BodyStructureMultiPartExt, error) {
|
|
var ext imap.BodyStructureMultiPartExt
|
|
|
|
var err error
|
|
ext.Params, err = readBodyFldParam(dec, options)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("in body-fld-param: %v", err)
|
|
}
|
|
|
|
if !dec.SP() {
|
|
return &ext, nil
|
|
}
|
|
|
|
ext.Disposition, err = readBodyFldDsp(dec, options)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("in body-fld-dsp: %v", err)
|
|
}
|
|
|
|
if !dec.SP() {
|
|
return &ext, nil
|
|
}
|
|
|
|
ext.Language, err = readBodyFldLang(dec)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("in body-fld-lang: %v", err)
|
|
}
|
|
|
|
if !dec.SP() {
|
|
return &ext, nil
|
|
}
|
|
|
|
if !dec.ExpectNString(&ext.Location) {
|
|
return nil, dec.Err()
|
|
}
|
|
|
|
return &ext, nil
|
|
}
|
|
|
|
func readBodyFldDsp(dec *imapwire.Decoder, options *Options) (*imap.BodyStructureDisposition, error) {
|
|
if !dec.Special('(') {
|
|
if !dec.ExpectNIL() {
|
|
return nil, dec.Err()
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
var disp imap.BodyStructureDisposition
|
|
if !dec.ExpectString(&disp.Value) || !dec.ExpectSP() {
|
|
return nil, dec.Err()
|
|
}
|
|
|
|
var err error
|
|
disp.Params, err = readBodyFldParam(dec, options)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !dec.ExpectSpecial(')') {
|
|
return nil, dec.Err()
|
|
}
|
|
return &disp, nil
|
|
}
|
|
|
|
func readBodyFldParam(dec *imapwire.Decoder, options *Options) (map[string]string, error) {
|
|
var (
|
|
params map[string]string
|
|
k string
|
|
)
|
|
err := dec.ExpectNList(func() error {
|
|
var s string
|
|
if !dec.ExpectString(&s) {
|
|
return dec.Err()
|
|
}
|
|
|
|
if k == "" {
|
|
k = s
|
|
} else {
|
|
if params == nil {
|
|
params = make(map[string]string)
|
|
}
|
|
decoded, _ := options.decodeText(s)
|
|
// TODO: handle error
|
|
|
|
params[strings.ToLower(k)] = decoded
|
|
k = ""
|
|
}
|
|
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
} else if k != "" {
|
|
return nil, fmt.Errorf("in body-fld-param: key without value")
|
|
}
|
|
return params, nil
|
|
}
|
|
|
|
func readBodyFldLang(dec *imapwire.Decoder) ([]string, error) {
|
|
var l []string
|
|
isList, err := dec.List(func() error {
|
|
var s string
|
|
if !dec.ExpectString(&s) {
|
|
return dec.Err()
|
|
}
|
|
l = append(l, s)
|
|
return nil
|
|
})
|
|
if err != nil || isList {
|
|
return l, err
|
|
}
|
|
|
|
var s string
|
|
if !dec.ExpectNString(&s) {
|
|
return nil, dec.Err()
|
|
}
|
|
if s != "" {
|
|
return []string{s}, nil
|
|
} else {
|
|
return nil, nil
|
|
}
|
|
}
|
|
|
|
func readSectionSpec(dec *imapwire.Decoder) (*imap.FetchItemBodySection, error) {
|
|
var section imap.FetchItemBodySection
|
|
|
|
var dot bool
|
|
section.Part, dot = readSectionPart(dec)
|
|
if dot || len(section.Part) == 0 {
|
|
var specifier string
|
|
if dot {
|
|
if !dec.ExpectAtom(&specifier) {
|
|
return nil, dec.Err()
|
|
}
|
|
} else {
|
|
dec.Atom(&specifier)
|
|
}
|
|
specifier = strings.ToUpper(specifier)
|
|
section.Specifier = imap.PartSpecifier(specifier)
|
|
|
|
if specifier == "HEADER.FIELDS" || specifier == "HEADER.FIELDS.NOT" {
|
|
if !dec.ExpectSP() {
|
|
return nil, dec.Err()
|
|
}
|
|
var err error
|
|
headerList, err := readHeaderList(dec)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
section.Specifier = imap.PartSpecifierHeader
|
|
if specifier == "HEADER.FIELDS" {
|
|
section.HeaderFields = headerList
|
|
} else {
|
|
section.HeaderFieldsNot = headerList
|
|
}
|
|
}
|
|
}
|
|
|
|
if !dec.ExpectSpecial(']') {
|
|
return nil, dec.Err()
|
|
}
|
|
|
|
offset, err := readPartialOffset(dec)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if offset != nil {
|
|
section.Partial = &imap.SectionPartial{Offset: int64(*offset)}
|
|
}
|
|
|
|
return §ion, nil
|
|
}
|
|
|
|
func readPartialOffset(dec *imapwire.Decoder) (*uint32, error) {
|
|
if !dec.Special('<') {
|
|
return nil, nil
|
|
}
|
|
var offset uint32
|
|
if !dec.ExpectNumber(&offset) || !dec.ExpectSpecial('>') {
|
|
return nil, dec.Err()
|
|
}
|
|
return &offset, nil
|
|
}
|
|
|
|
func readHeaderList(dec *imapwire.Decoder) ([]string, error) {
|
|
var l []string
|
|
err := dec.ExpectList(func() error {
|
|
var s string
|
|
if !dec.ExpectAString(&s) {
|
|
return dec.Err()
|
|
}
|
|
l = append(l, s)
|
|
return nil
|
|
})
|
|
return l, err
|
|
}
|
|
|
|
func readSectionPart(dec *imapwire.Decoder) (part []int, dot bool) {
|
|
for {
|
|
dot = len(part) > 0
|
|
if dot && !dec.Special('.') {
|
|
return part, false
|
|
}
|
|
|
|
var num uint32
|
|
if !dec.Number(&num) {
|
|
return part, dot
|
|
}
|
|
part = append(part, int(num))
|
|
}
|
|
}
|
|
|
|
type fetchLiteralReader struct {
|
|
*imapwire.LiteralReader
|
|
ch chan<- struct{}
|
|
}
|
|
|
|
func (lit *fetchLiteralReader) Read(b []byte) (int, error) {
|
|
n, err := lit.LiteralReader.Read(b)
|
|
if err == io.EOF && lit.ch != nil {
|
|
close(lit.ch)
|
|
lit.ch = nil
|
|
}
|
|
return n, err
|
|
}
|