diff options
author | Jordan <me@jordan.im> | 2023-02-04 23:54:03 -0700 |
---|---|---|
committer | Jordan <me@jordan.im> | 2023-02-04 23:54:03 -0700 |
commit | c4159d895ac399ca55326f7b4ff8bfbf8402e654 (patch) | |
tree | 45340ca429c16f683b375695d01e03d65ebf22b0 /vendor/github.com/emersion/go-imap/message.go | |
download | pigeon-c4159d895ac399ca55326f7b4ff8bfbf8402e654.tar.gz pigeon-c4159d895ac399ca55326f7b4ff8bfbf8402e654.zip |
initial commit
Diffstat (limited to 'vendor/github.com/emersion/go-imap/message.go')
-rw-r--r-- | vendor/github.com/emersion/go-imap/message.go | 1186 |
1 files changed, 1186 insertions, 0 deletions
diff --git a/vendor/github.com/emersion/go-imap/message.go b/vendor/github.com/emersion/go-imap/message.go new file mode 100644 index 0000000..bd28325 --- /dev/null +++ b/vendor/github.com/emersion/go-imap/message.go @@ -0,0 +1,1186 @@ +package imap + +import ( + "bytes" + "errors" + "fmt" + "io" + "mime" + "strconv" + "strings" + "time" +) + +// System message flags, defined in RFC 3501 section 2.3.2. +const ( + SeenFlag = "\\Seen" + AnsweredFlag = "\\Answered" + FlaggedFlag = "\\Flagged" + DeletedFlag = "\\Deleted" + DraftFlag = "\\Draft" + RecentFlag = "\\Recent" +) + +// ImportantFlag is a message flag to signal that a message is likely important +// to the user. This flag is defined in RFC 8457 section 2. +const ImportantFlag = "$Important" + +// TryCreateFlag is a special flag in MailboxStatus.PermanentFlags indicating +// that it is possible to create new keywords by attempting to store those +// flags in the mailbox. +const TryCreateFlag = "\\*" + +var flags = []string{ + SeenFlag, + AnsweredFlag, + FlaggedFlag, + DeletedFlag, + DraftFlag, + RecentFlag, +} + +// A PartSpecifier specifies which parts of the MIME entity should be returned. +type PartSpecifier string + +// Part specifiers described in RFC 3501 page 55. +const ( + // Refers to the entire part, including headers. + EntireSpecifier PartSpecifier = "" + // Refers to the header of the part. Must include the final CRLF delimiting + // the header and the body. + HeaderSpecifier = "HEADER" + // Refers to the text body of the part, omitting the header. + TextSpecifier = "TEXT" + // Refers to the MIME Internet Message Body header. Must include the final + // CRLF delimiting the header and the body. + MIMESpecifier = "MIME" +) + +// CanonicalFlag returns the canonical form of a flag. Flags are case-insensitive. +// +// If the flag is defined in RFC 3501, it returns the flag with the case of the +// RFC. Otherwise, it returns the lowercase version of the flag. +func CanonicalFlag(flag string) string { + for _, f := range flags { + if strings.EqualFold(f, flag) { + return f + } + } + return strings.ToLower(flag) +} + +func ParseParamList(fields []interface{}) (map[string]string, error) { + params := make(map[string]string) + + var k string + for i, f := range fields { + p, err := ParseString(f) + if err != nil { + return nil, errors.New("Parameter list contains a non-string: " + err.Error()) + } + + if i%2 == 0 { + k = p + } else { + params[k] = p + k = "" + } + } + + if k != "" { + return nil, errors.New("Parameter list contains a key without a value") + } + return params, nil +} + +func FormatParamList(params map[string]string) []interface{} { + var fields []interface{} + for key, value := range params { + fields = append(fields, key, value) + } + return fields +} + +var wordDecoder = &mime.WordDecoder{ + CharsetReader: func(charset string, input io.Reader) (io.Reader, error) { + if CharsetReader != nil { + return CharsetReader(charset, input) + } + return nil, fmt.Errorf("imap: unhandled charset %q", charset) + }, +} + +func decodeHeader(s string) (string, error) { + dec, err := wordDecoder.DecodeHeader(s) + if err != nil { + return s, err + } + return dec, nil +} + +func encodeHeader(s string) string { + return mime.QEncoding.Encode("utf-8", s) +} + +func stringLowered(i interface{}) (string, bool) { + s, ok := i.(string) + return strings.ToLower(s), ok +} + +func parseHeaderParamList(fields []interface{}) (map[string]string, error) { + params, err := ParseParamList(fields) + if err != nil { + return nil, err + } + + for k, v := range params { + if lower := strings.ToLower(k); lower != k { + delete(params, k) + k = lower + } + + params[k], _ = decodeHeader(v) + } + + return params, nil +} + +func formatHeaderParamList(params map[string]string) []interface{} { + encoded := make(map[string]string) + for k, v := range params { + encoded[k] = encodeHeader(v) + } + return FormatParamList(encoded) +} + +// A message. +type Message struct { + // The message sequence number. It must be greater than or equal to 1. + SeqNum uint32 + // The mailbox items that are currently filled in. This map's values + // should not be used directly, they must only be used by libraries + // implementing extensions of the IMAP protocol. + Items map[FetchItem]interface{} + + // The message envelope. + Envelope *Envelope + // The message body structure (either BODYSTRUCTURE or BODY). + BodyStructure *BodyStructure + // The message flags. + Flags []string + // The date the message was received by the server. + InternalDate time.Time + // The message size. + Size uint32 + // The message unique identifier. It must be greater than or equal to 1. + Uid uint32 + // The message body sections. + Body map[*BodySectionName]Literal + + // The order in which items were requested. This order must be preserved + // because some bad IMAP clients (looking at you, Outlook!) refuse responses + // containing items in a different order. + itemsOrder []FetchItem +} + +// Create a new empty message that will contain the specified items. +func NewMessage(seqNum uint32, items []FetchItem) *Message { + msg := &Message{ + SeqNum: seqNum, + Items: make(map[FetchItem]interface{}), + Body: make(map[*BodySectionName]Literal), + itemsOrder: items, + } + + for _, k := range items { + msg.Items[k] = nil + } + + return msg +} + +// Parse a message from fields. +func (m *Message) Parse(fields []interface{}) error { + m.Items = make(map[FetchItem]interface{}) + m.Body = map[*BodySectionName]Literal{} + m.itemsOrder = nil + + var k FetchItem + for i, f := range fields { + if i%2 == 0 { // It's a key + switch f := f.(type) { + case string: + k = FetchItem(strings.ToUpper(f)) + case RawString: + k = FetchItem(strings.ToUpper(string(f))) + default: + return fmt.Errorf("cannot parse message: key is not a string, but a %T", f) + } + } else { // It's a value + m.Items[k] = nil + m.itemsOrder = append(m.itemsOrder, k) + + switch k { + case FetchBody, FetchBodyStructure: + bs, ok := f.([]interface{}) + if !ok { + return fmt.Errorf("cannot parse message: BODYSTRUCTURE is not a list, but a %T", f) + } + + m.BodyStructure = &BodyStructure{Extended: k == FetchBodyStructure} + if err := m.BodyStructure.Parse(bs); err != nil { + return err + } + case FetchEnvelope: + env, ok := f.([]interface{}) + if !ok { + return fmt.Errorf("cannot parse message: ENVELOPE is not a list, but a %T", f) + } + + m.Envelope = &Envelope{} + if err := m.Envelope.Parse(env); err != nil { + return err + } + case FetchFlags: + flags, ok := f.([]interface{}) + if !ok { + return fmt.Errorf("cannot parse message: FLAGS is not a list, but a %T", f) + } + + m.Flags = make([]string, len(flags)) + for i, flag := range flags { + s, _ := ParseString(flag) + m.Flags[i] = CanonicalFlag(s) + } + case FetchInternalDate: + date, _ := f.(string) + m.InternalDate, _ = time.Parse(DateTimeLayout, date) + case FetchRFC822Size: + m.Size, _ = ParseNumber(f) + case FetchUid: + m.Uid, _ = ParseNumber(f) + default: + // Likely to be a section of the body + // First check that the section name is correct + if section, err := ParseBodySectionName(k); err != nil { + // Not a section name, maybe an attribute defined in an IMAP extension + m.Items[k] = f + } else { + m.Body[section], _ = f.(Literal) + } + } + } + } + + return nil +} + +func (m *Message) formatItem(k FetchItem) []interface{} { + v := m.Items[k] + var kk interface{} = RawString(k) + + switch k { + case FetchBody, FetchBodyStructure: + // Extension data is only returned with the BODYSTRUCTURE fetch + m.BodyStructure.Extended = k == FetchBodyStructure + v = m.BodyStructure.Format() + case FetchEnvelope: + v = m.Envelope.Format() + case FetchFlags: + flags := make([]interface{}, len(m.Flags)) + for i, flag := range m.Flags { + flags[i] = RawString(flag) + } + v = flags + case FetchInternalDate: + v = m.InternalDate + case FetchRFC822Size: + v = m.Size + case FetchUid: + v = m.Uid + default: + for section, literal := range m.Body { + if section.value == k { + // This can contain spaces, so we can't pass it as a string directly + kk = section.resp() + v = literal + break + } + } + } + + return []interface{}{kk, v} +} + +func (m *Message) Format() []interface{} { + var fields []interface{} + + // First send ordered items + processed := make(map[FetchItem]bool) + for _, k := range m.itemsOrder { + if _, ok := m.Items[k]; ok { + fields = append(fields, m.formatItem(k)...) + processed[k] = true + } + } + + // Then send other remaining items + for k := range m.Items { + if !processed[k] { + fields = append(fields, m.formatItem(k)...) + } + } + + return fields +} + +// GetBody gets the body section with the specified name. Returns nil if it's not found. +func (m *Message) GetBody(section *BodySectionName) Literal { + section = section.resp() + + for s, body := range m.Body { + if section.Equal(s) { + if body == nil { + // Server can return nil, we need to treat as empty string per RFC 3501 + body = bytes.NewReader(nil) + } + return body + } + } + return nil +} + +// A body section name. +// See RFC 3501 page 55. +type BodySectionName struct { + BodyPartName + + // If set to true, do not implicitly set the \Seen flag. + Peek bool + // The substring of the section requested. The first value is the position of + // the first desired octet and the second value is the maximum number of + // octets desired. + Partial []int + + value FetchItem +} + +func (section *BodySectionName) parse(s string) error { + section.value = FetchItem(s) + + if s == "RFC822" { + s = "BODY[]" + } + if s == "RFC822.HEADER" { + s = "BODY.PEEK[HEADER]" + } + if s == "RFC822.TEXT" { + s = "BODY[TEXT]" + } + + partStart := strings.Index(s, "[") + if partStart == -1 { + return errors.New("Invalid body section name: must contain an open bracket") + } + + partEnd := strings.LastIndex(s, "]") + if partEnd == -1 { + return errors.New("Invalid body section name: must contain a close bracket") + } + + name := s[:partStart] + part := s[partStart+1 : partEnd] + partial := s[partEnd+1:] + + if name == "BODY.PEEK" { + section.Peek = true + } else if name != "BODY" { + return errors.New("Invalid body section name") + } + + b := bytes.NewBufferString(part + string(cr) + string(lf)) + r := NewReader(b) + fields, err := r.ReadFields() + if err != nil { + return err + } + + if err := section.BodyPartName.parse(fields); err != nil { + return err + } + + if len(partial) > 0 { + if !strings.HasPrefix(partial, "<") || !strings.HasSuffix(partial, ">") { + return errors.New("Invalid body section name: invalid partial") + } + partial = partial[1 : len(partial)-1] + + partialParts := strings.SplitN(partial, ".", 2) + + var from, length int + if from, err = strconv.Atoi(partialParts[0]); err != nil { + return errors.New("Invalid body section name: invalid partial: invalid from: " + err.Error()) + } + section.Partial = []int{from} + + if len(partialParts) == 2 { + if length, err = strconv.Atoi(partialParts[1]); err != nil { + return errors.New("Invalid body section name: invalid partial: invalid length: " + err.Error()) + } + section.Partial = append(section.Partial, length) + } + } + + return nil +} + +func (section *BodySectionName) FetchItem() FetchItem { + if section.value != "" { + return section.value + } + + s := "BODY" + if section.Peek { + s += ".PEEK" + } + + s += "[" + section.BodyPartName.string() + "]" + + if len(section.Partial) > 0 { + s += "<" + s += strconv.Itoa(section.Partial[0]) + + if len(section.Partial) > 1 { + s += "." + s += strconv.Itoa(section.Partial[1]) + } + + s += ">" + } + + return FetchItem(s) +} + +// Equal checks whether two sections are equal. +func (section *BodySectionName) Equal(other *BodySectionName) bool { + if section.Peek != other.Peek { + return false + } + if len(section.Partial) != len(other.Partial) { + return false + } + if len(section.Partial) > 0 && section.Partial[0] != other.Partial[0] { + return false + } + if len(section.Partial) > 1 && section.Partial[1] != other.Partial[1] { + return false + } + return section.BodyPartName.Equal(&other.BodyPartName) +} + +func (section *BodySectionName) resp() *BodySectionName { + resp := *section // Copy section + if resp.Peek { + resp.Peek = false + } + if len(resp.Partial) == 2 { + resp.Partial = []int{resp.Partial[0]} + } + if !strings.HasPrefix(string(resp.value), string(FetchRFC822)) { + resp.value = "" + } + return &resp +} + +// ExtractPartial returns a subset of the specified bytes matching the partial requested in the +// section name. +func (section *BodySectionName) ExtractPartial(b []byte) []byte { + if len(section.Partial) != 2 { + return b + } + + from := section.Partial[0] + length := section.Partial[1] + to := from + length + if from > len(b) { + return nil + } + if to > len(b) { + to = len(b) + } + return b[from:to] +} + +// ParseBodySectionName parses a body section name. +func ParseBodySectionName(s FetchItem) (*BodySectionName, error) { + section := new(BodySectionName) + err := section.parse(string(s)) + return section, err +} + +// A body part name. +type BodyPartName struct { + // The specifier of the requested part. + Specifier PartSpecifier + // The part path. Parts indexes start at 1. + Path []int + // If Specifier is HEADER, contains header fields that will/won't be returned, + // depending of the value of NotFields. + Fields []string + // If set to true, Fields is a blacklist of fields instead of a whitelist. + NotFields bool +} + +func (part *BodyPartName) parse(fields []interface{}) error { + if len(fields) == 0 { + return nil + } + + name, ok := fields[0].(string) + if !ok { + return errors.New("Invalid body section name: part name must be a string") + } + + args := fields[1:] + + path := strings.Split(strings.ToUpper(name), ".") + + end := 0 +loop: + for i, node := range path { + switch PartSpecifier(node) { + case EntireSpecifier, HeaderSpecifier, MIMESpecifier, TextSpecifier: + part.Specifier = PartSpecifier(node) + end = i + 1 + break loop + } + + index, err := strconv.Atoi(node) + if err != nil { + return errors.New("Invalid body part name: " + err.Error()) + } + if index <= 0 { + return errors.New("Invalid body part name: index <= 0") + } + + part.Path = append(part.Path, index) + } + + if part.Specifier == HeaderSpecifier && len(path) > end && path[end] == "FIELDS" && len(args) > 0 { + end++ + if len(path) > end && path[end] == "NOT" { + part.NotFields = true + } + + names, ok := args[0].([]interface{}) + if !ok { + return errors.New("Invalid body part name: HEADER.FIELDS must have a list argument") + } + + for _, namei := range names { + if name, ok := namei.(string); ok { + part.Fields = append(part.Fields, name) + } + } + } + + return nil +} + +func (part *BodyPartName) string() string { + path := make([]string, len(part.Path)) + for i, index := range part.Path { + path[i] = strconv.Itoa(index) + } + + if part.Specifier != EntireSpecifier { + path = append(path, string(part.Specifier)) + } + + if part.Specifier == HeaderSpecifier && len(part.Fields) > 0 { + path = append(path, "FIELDS") + + if part.NotFields { + path = append(path, "NOT") + } + } + + s := strings.Join(path, ".") + + if len(part.Fields) > 0 { + s += " (" + strings.Join(part.Fields, " ") + ")" + } + + return s +} + +// Equal checks whether two body part names are equal. +func (part *BodyPartName) Equal(other *BodyPartName) bool { + if part.Specifier != other.Specifier { + return false + } + if part.NotFields != other.NotFields { + return false + } + if len(part.Path) != len(other.Path) { + return false + } + for i, node := range part.Path { + if node != other.Path[i] { + return false + } + } + if len(part.Fields) != len(other.Fields) { + return false + } + for _, field := range part.Fields { + found := false + for _, f := range other.Fields { + if strings.EqualFold(field, f) { + found = true + break + } + } + if !found { + return false + } + } + return true +} + +// An address. +type Address struct { + // The personal name. + PersonalName string + // The SMTP at-domain-list (source route). + AtDomainList string + // The mailbox name. + MailboxName string + // The host name. + HostName string +} + +// Address returns the mailbox address (e.g. "foo@example.org"). +func (addr *Address) Address() string { + return addr.MailboxName + "@" + addr.HostName +} + +// Parse an address from fields. +func (addr *Address) Parse(fields []interface{}) error { + if len(fields) < 4 { + return errors.New("Address doesn't contain 4 fields") + } + + if s, err := ParseString(fields[0]); err == nil { + addr.PersonalName, _ = decodeHeader(s) + } + if s, err := ParseString(fields[1]); err == nil { + addr.AtDomainList, _ = decodeHeader(s) + } + + s, err := ParseString(fields[2]) + if err != nil { + return errors.New("Mailbox name could not be parsed") + } + addr.MailboxName, _ = decodeHeader(s) + + s, err = ParseString(fields[3]) + if err != nil { + return errors.New("Host name could not be parsed") + } + addr.HostName, _ = decodeHeader(s) + + return nil +} + +// Format an address to fields. +func (addr *Address) Format() []interface{} { + fields := make([]interface{}, 4) + + if addr.PersonalName != "" { + fields[0] = encodeHeader(addr.PersonalName) + } + if addr.AtDomainList != "" { + fields[1] = addr.AtDomainList + } + if addr.MailboxName != "" { + fields[2] = addr.MailboxName + } + if addr.HostName != "" { + fields[3] = addr.HostName + } + + return fields +} + +// Parse an address list from fields. +func ParseAddressList(fields []interface{}) (addrs []*Address) { + for _, f := range fields { + if addrFields, ok := f.([]interface{}); ok { + addr := &Address{} + if err := addr.Parse(addrFields); err == nil { + addrs = append(addrs, addr) + } + } + } + + return +} + +// Format an address list to fields. +func FormatAddressList(addrs []*Address) interface{} { + if len(addrs) == 0 { + return nil + } + + fields := make([]interface{}, len(addrs)) + + for i, addr := range addrs { + fields[i] = addr.Format() + } + + return fields +} + +// A message envelope, ie. message metadata from its headers. +// See RFC 3501 page 77. +type Envelope struct { + // The message date. + Date time.Time + // The message subject. + Subject string + // The From header addresses. + From []*Address + // The message senders. + Sender []*Address + // The Reply-To header addresses. + ReplyTo []*Address + // The To header addresses. + To []*Address + // The Cc header addresses. + Cc []*Address + // The Bcc header addresses. + Bcc []*Address + // The In-Reply-To header. Contains the parent Message-Id. + InReplyTo string + // The Message-Id header. + MessageId string +} + +// Parse an envelope from fields. +func (e *Envelope) Parse(fields []interface{}) error { + if len(fields) < 10 { + return errors.New("ENVELOPE doesn't contain 10 fields") + } + + if date, ok := fields[0].(string); ok { + e.Date, _ = parseMessageDateTime(date) + } + if subject, err := ParseString(fields[1]); err == nil { + e.Subject, _ = decodeHeader(subject) + } + if from, ok := fields[2].([]interface{}); ok { + e.From = ParseAddressList(from) + } + if sender, ok := fields[3].([]interface{}); ok { + e.Sender = ParseAddressList(sender) + } + if replyTo, ok := fields[4].([]interface{}); ok { + e.ReplyTo = ParseAddressList(replyTo) + } + if to, ok := fields[5].([]interface{}); ok { + e.To = ParseAddressList(to) + } + if cc, ok := fields[6].([]interface{}); ok { + e.Cc = ParseAddressList(cc) + } + if bcc, ok := fields[7].([]interface{}); ok { + e.Bcc = ParseAddressList(bcc) + } + if inReplyTo, ok := fields[8].(string); ok { + e.InReplyTo = inReplyTo + } + if msgId, ok := fields[9].(string); ok { + e.MessageId = msgId + } + + return nil +} + +// Format an envelope to fields. +func (e *Envelope) Format() (fields []interface{}) { + fields = make([]interface{}, 0, 10) + fields = append(fields, envelopeDateTime(e.Date)) + if e.Subject != "" { + fields = append(fields, encodeHeader(e.Subject)) + } else { + fields = append(fields, nil) + } + fields = append(fields, + FormatAddressList(e.From), + FormatAddressList(e.Sender), + FormatAddressList(e.ReplyTo), + FormatAddressList(e.To), + FormatAddressList(e.Cc), + FormatAddressList(e.Bcc), + ) + if e.InReplyTo != "" { + fields = append(fields, e.InReplyTo) + } else { + fields = append(fields, nil) + } + if e.MessageId != "" { + fields = append(fields, e.MessageId) + } else { + fields = append(fields, nil) + } + return fields +} + +// A body structure. +// See RFC 3501 page 74. +type BodyStructure struct { + // Basic fields + + // The MIME type (e.g. "text", "image") + MIMEType string + // The MIME subtype (e.g. "plain", "png") + MIMESubType string + // The MIME parameters. + Params map[string]string + + // The Content-Id header. + Id string + // The Content-Description header. + Description string + // The Content-Encoding header. + Encoding string + // The Content-Length header. + Size uint32 + + // Type-specific fields + + // The children parts, if multipart. + Parts []*BodyStructure + // The envelope, if message/rfc822. + Envelope *Envelope + // The body structure, if message/rfc822. + BodyStructure *BodyStructure + // The number of lines, if text or message/rfc822. + Lines uint32 + + // Extension data + + // True if the body structure contains extension data. + Extended bool + + // The Content-Disposition header field value. + Disposition string + // The Content-Disposition header field parameters. + DispositionParams map[string]string + // The Content-Language header field, if multipart. + Language []string + // The content URI, if multipart. + Location []string + + // The MD5 checksum. + MD5 string +} + +func (bs *BodyStructure) Parse(fields []interface{}) error { + if len(fields) == 0 { + return nil + } + + // Initialize params map + bs.Params = make(map[string]string) + + switch fields[0].(type) { + case []interface{}: // A multipart body part + bs.MIMEType = "multipart" + + end := 0 + for i, fi := range fields { + switch f := fi.(type) { + case []interface{}: // A part + part := new(BodyStructure) + if err := part.Parse(f); err != nil { + return err + } + bs.Parts = append(bs.Parts, part) + case string: + end = i + } + + if end > 0 { + break + } + } + + bs.MIMESubType, _ = fields[end].(string) + end++ + + // GMail seems to return only 3 extension data fields. Parse as many fields + // as we can. + if len(fields) > end { + bs.Extended = true // Contains extension data + + params, _ := fields[end].([]interface{}) + bs.Params, _ = parseHeaderParamList(params) + end++ + } + if len(fields) > end { + if disp, ok := fields[end].([]interface{}); ok && len(disp) >= 2 { + if s, ok := disp[0].(string); ok { + bs.Disposition, _ = decodeHeader(s) + bs.Disposition = strings.ToLower(bs.Disposition) + } + if params, ok := disp[1].([]interface{}); ok { + bs.DispositionParams, _ = parseHeaderParamList(params) + } + } + end++ + } + if len(fields) > end { + switch langs := fields[end].(type) { + case string: + bs.Language = []string{langs} + case []interface{}: + bs.Language, _ = ParseStringList(langs) + default: + bs.Language = nil + } + end++ + } + if len(fields) > end { + location, _ := fields[end].([]interface{}) + bs.Location, _ = ParseStringList(location) + end++ + } + case string: // A non-multipart body part + if len(fields) < 7 { + return errors.New("Non-multipart body part doesn't have 7 fields") + } + + bs.MIMEType, _ = stringLowered(fields[0]) + bs.MIMESubType, _ = stringLowered(fields[1]) + + params, _ := fields[2].([]interface{}) + bs.Params, _ = parseHeaderParamList(params) + + bs.Id, _ = fields[3].(string) + if desc, err := ParseString(fields[4]); err == nil { + bs.Description, _ = decodeHeader(desc) + } + bs.Encoding, _ = stringLowered(fields[5]) + bs.Size, _ = ParseNumber(fields[6]) + + end := 7 + + // Type-specific fields + if strings.EqualFold(bs.MIMEType, "message") && strings.EqualFold(bs.MIMESubType, "rfc822") { + if len(fields)-end < 3 { + return errors.New("Missing type-specific fields for message/rfc822") + } + + envelope, _ := fields[end].([]interface{}) + bs.Envelope = new(Envelope) + bs.Envelope.Parse(envelope) + + structure, _ := fields[end+1].([]interface{}) + bs.BodyStructure = new(BodyStructure) + bs.BodyStructure.Parse(structure) + + bs.Lines, _ = ParseNumber(fields[end+2]) + + end += 3 + } + if strings.EqualFold(bs.MIMEType, "text") { + if len(fields)-end < 1 { + return errors.New("Missing type-specific fields for text/*") + } + + bs.Lines, _ = ParseNumber(fields[end]) + end++ + } + + // GMail seems to return only 3 extension data fields. Parse as many fields + // as we can. + if len(fields) > end { + bs.Extended = true // Contains extension data + + bs.MD5, _ = fields[end].(string) + end++ + } + if len(fields) > end { + if disp, ok := fields[end].([]interface{}); ok && len(disp) >= 2 { + if s, ok := disp[0].(string); ok { + bs.Disposition, _ = decodeHeader(s) + bs.Disposition = strings.ToLower(bs.Disposition) + } + if params, ok := disp[1].([]interface{}); ok { + bs.DispositionParams, _ = parseHeaderParamList(params) + } + } + end++ + } + if len(fields) > end { + switch langs := fields[end].(type) { + case string: + bs.Language = []string{langs} + case []interface{}: + bs.Language, _ = ParseStringList(langs) + default: + bs.Language = nil + } + end++ + } + if len(fields) > end { + location, _ := fields[end].([]interface{}) + bs.Location, _ = ParseStringList(location) + end++ + } + } + + return nil +} + +func (bs *BodyStructure) Format() (fields []interface{}) { + if strings.EqualFold(bs.MIMEType, "multipart") { + for _, part := range bs.Parts { + fields = append(fields, part.Format()) + } + + fields = append(fields, bs.MIMESubType) + + if bs.Extended { + extended := make([]interface{}, 4) + + if bs.Params != nil { + extended[0] = formatHeaderParamList(bs.Params) + } + if bs.Disposition != "" { + extended[1] = []interface{}{ + encodeHeader(bs.Disposition), + formatHeaderParamList(bs.DispositionParams), + } + } + if bs.Language != nil { + extended[2] = FormatStringList(bs.Language) + } + if bs.Location != nil { + extended[3] = FormatStringList(bs.Location) + } + + fields = append(fields, extended...) + } + } else { + fields = make([]interface{}, 7) + fields[0] = bs.MIMEType + fields[1] = bs.MIMESubType + fields[2] = formatHeaderParamList(bs.Params) + + if bs.Id != "" { + fields[3] = bs.Id + } + if bs.Description != "" { + fields[4] = encodeHeader(bs.Description) + } + if bs.Encoding != "" { + fields[5] = bs.Encoding + } + + fields[6] = bs.Size + + // Type-specific fields + if strings.EqualFold(bs.MIMEType, "message") && strings.EqualFold(bs.MIMESubType, "rfc822") { + var env interface{} + if bs.Envelope != nil { + env = bs.Envelope.Format() + } + + var bsbs interface{} + if bs.BodyStructure != nil { + bsbs = bs.BodyStructure.Format() + } + + fields = append(fields, env, bsbs, bs.Lines) + } + if strings.EqualFold(bs.MIMEType, "text") { + fields = append(fields, bs.Lines) + } + + // Extension data + if bs.Extended { + extended := make([]interface{}, 4) + + if bs.MD5 != "" { + extended[0] = bs.MD5 + } + if bs.Disposition != "" { + extended[1] = []interface{}{ + encodeHeader(bs.Disposition), + formatHeaderParamList(bs.DispositionParams), + } + } + if bs.Language != nil { + extended[2] = FormatStringList(bs.Language) + } + if bs.Location != nil { + extended[3] = FormatStringList(bs.Location) + } + + fields = append(fields, extended...) + } + } + + return +} + +// Filename parses the body structure's filename, if it's an attachment. An +// empty string is returned if the filename isn't specified. An error is +// returned if and only if a charset error occurs, in which case the undecoded +// filename is returned too. +func (bs *BodyStructure) Filename() (string, error) { + raw, ok := bs.DispositionParams["filename"] + if !ok { + // Using "name" in Content-Type is discouraged + raw = bs.Params["name"] + } + return decodeHeader(raw) +} + +// BodyStructureWalkFunc is the type of the function called for each body +// structure visited by BodyStructure.Walk. The path argument contains the IMAP +// part path (see BodyPartName). +// +// The function should return true to visit all of the part's children or false +// to skip them. +type BodyStructureWalkFunc func(path []int, part *BodyStructure) (walkChildren bool) + +// Walk walks the body structure tree, calling f for each part in the tree, +// including bs itself. The parts are visited in DFS pre-order. +func (bs *BodyStructure) Walk(f BodyStructureWalkFunc) { + // Non-multipart messages only have part 1 + if len(bs.Parts) == 0 { + f([]int{1}, bs) + return + } + + bs.walk(f, nil) +} + +func (bs *BodyStructure) walk(f BodyStructureWalkFunc, path []int) { + if !f(path, bs) { + return + } + + for i, part := range bs.Parts { + num := i + 1 + + partPath := append([]int(nil), path...) + partPath = append(partPath, num) + + part.walk(f, partPath) + } +} |