Forked the emersion/go-imap v1 project.
This commit is contained in:
336
imapserver/message.go
Normal file
336
imapserver/message.go
Normal file
@@ -0,0 +1,336 @@
|
||||
package imapserver
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
gomessage "github.com/emersion/go-message"
|
||||
"github.com/emersion/go-message/mail"
|
||||
"github.com/emersion/go-message/textproto"
|
||||
|
||||
"github.com/emersion/go-imap/v2"
|
||||
)
|
||||
|
||||
// ExtractBodySection extracts a section of a message body.
|
||||
//
|
||||
// It can be used by server backends to implement Session.Fetch.
|
||||
func ExtractBodySection(r io.Reader, item *imap.FetchItemBodySection) []byte {
|
||||
var (
|
||||
header textproto.Header
|
||||
body io.Reader
|
||||
)
|
||||
|
||||
br := bufio.NewReader(r)
|
||||
header, err := textproto.ReadHeader(br)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
body = br
|
||||
|
||||
parentMediaType, header, body := findMessagePart(header, body, item.Part)
|
||||
if body == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(item.Part) > 0 {
|
||||
switch item.Specifier {
|
||||
case imap.PartSpecifierHeader, imap.PartSpecifierText:
|
||||
header, body = openMessagePart(header, body, parentMediaType)
|
||||
}
|
||||
}
|
||||
|
||||
// Filter header fields
|
||||
if len(item.HeaderFields) > 0 {
|
||||
keep := make(map[string]struct{})
|
||||
for _, k := range item.HeaderFields {
|
||||
keep[strings.ToLower(k)] = struct{}{}
|
||||
}
|
||||
for field := header.Fields(); field.Next(); {
|
||||
if _, ok := keep[strings.ToLower(field.Key())]; !ok {
|
||||
field.Del()
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, k := range item.HeaderFieldsNot {
|
||||
header.Del(k)
|
||||
}
|
||||
|
||||
// Write the requested data to a buffer
|
||||
var buf bytes.Buffer
|
||||
|
||||
writeHeader := true
|
||||
switch item.Specifier {
|
||||
case imap.PartSpecifierNone:
|
||||
writeHeader = len(item.Part) == 0
|
||||
case imap.PartSpecifierText:
|
||||
writeHeader = false
|
||||
}
|
||||
if writeHeader {
|
||||
if err := textproto.WriteHeader(&buf, header); err != nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
switch item.Specifier {
|
||||
case imap.PartSpecifierNone, imap.PartSpecifierText:
|
||||
if _, err := io.Copy(&buf, body); err != nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return extractPartial(buf.Bytes(), item.Partial)
|
||||
}
|
||||
|
||||
func findMessagePart(header textproto.Header, body io.Reader, partPath []int) (string, textproto.Header, io.Reader) {
|
||||
// First part of non-multipart message refers to the message itself
|
||||
msgHeader := gomessage.Header{header}
|
||||
mediaType, _, _ := msgHeader.ContentType()
|
||||
if !strings.HasPrefix(mediaType, "multipart/") && len(partPath) > 0 && partPath[0] == 1 {
|
||||
partPath = partPath[1:]
|
||||
}
|
||||
|
||||
var parentMediaType string
|
||||
for i := 0; i < len(partPath); i++ {
|
||||
partNum := partPath[i]
|
||||
|
||||
header, body = openMessagePart(header, body, parentMediaType)
|
||||
|
||||
msgHeader := gomessage.Header{header}
|
||||
mediaType, typeParams, _ := msgHeader.ContentType()
|
||||
if !strings.HasPrefix(mediaType, "multipart/") {
|
||||
if partNum != 1 {
|
||||
return "", textproto.Header{}, nil
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
mr := textproto.NewMultipartReader(body, typeParams["boundary"])
|
||||
found := false
|
||||
for j := 1; j <= partNum; j++ {
|
||||
p, err := mr.NextPart()
|
||||
if err != nil {
|
||||
return "", textproto.Header{}, nil
|
||||
}
|
||||
|
||||
if j == partNum {
|
||||
parentMediaType = mediaType
|
||||
header = p.Header
|
||||
body = p
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return "", textproto.Header{}, nil
|
||||
}
|
||||
}
|
||||
|
||||
return parentMediaType, header, body
|
||||
}
|
||||
|
||||
func openMessagePart(header textproto.Header, body io.Reader, parentMediaType string) (textproto.Header, io.Reader) {
|
||||
msgHeader := gomessage.Header{header}
|
||||
mediaType, _, _ := msgHeader.ContentType()
|
||||
if !msgHeader.Has("Content-Type") && parentMediaType == "multipart/digest" {
|
||||
mediaType = "message/rfc822"
|
||||
}
|
||||
if mediaType == "message/rfc822" || mediaType == "message/global" {
|
||||
br := bufio.NewReader(body)
|
||||
header, _ = textproto.ReadHeader(br)
|
||||
return header, br
|
||||
}
|
||||
return header, body
|
||||
}
|
||||
|
||||
func extractPartial(b []byte, partial *imap.SectionPartial) []byte {
|
||||
if partial == nil {
|
||||
return b
|
||||
}
|
||||
|
||||
end := partial.Offset + partial.Size
|
||||
if partial.Offset > int64(len(b)) {
|
||||
return nil
|
||||
}
|
||||
if end > int64(len(b)) {
|
||||
end = int64(len(b))
|
||||
}
|
||||
return b[partial.Offset:end]
|
||||
}
|
||||
|
||||
func ExtractBinarySection(r io.Reader, item *imap.FetchItemBinarySection) []byte {
|
||||
var (
|
||||
header textproto.Header
|
||||
body io.Reader
|
||||
)
|
||||
|
||||
br := bufio.NewReader(r)
|
||||
header, err := textproto.ReadHeader(br)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
body = br
|
||||
|
||||
_, header, body = findMessagePart(header, body, item.Part)
|
||||
if body == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
part, err := gomessage.New(gomessage.Header{header}, body)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Write the requested data to a buffer
|
||||
var buf bytes.Buffer
|
||||
|
||||
if len(item.Part) == 0 {
|
||||
if err := textproto.WriteHeader(&buf, part.Header.Header); err != nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := io.Copy(&buf, part.Body); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return extractPartial(buf.Bytes(), item.Partial)
|
||||
}
|
||||
|
||||
func ExtractBinarySectionSize(r io.Reader, item *imap.FetchItemBinarySectionSize) uint32 {
|
||||
// TODO: optimize
|
||||
b := ExtractBinarySection(r, &imap.FetchItemBinarySection{Part: item.Part})
|
||||
return uint32(len(b))
|
||||
}
|
||||
|
||||
// ExtractEnvelope returns a message envelope from its header.
|
||||
//
|
||||
// It can be used by server backends to implement Session.Fetch.
|
||||
func ExtractEnvelope(h textproto.Header) *imap.Envelope {
|
||||
mh := mail.Header{gomessage.Header{h}}
|
||||
date, _ := mh.Date()
|
||||
subject, _ := mh.Subject()
|
||||
inReplyTo, _ := mh.MsgIDList("In-Reply-To")
|
||||
messageID, _ := mh.MessageID()
|
||||
return &imap.Envelope{
|
||||
Date: date,
|
||||
Subject: subject,
|
||||
From: parseAddressList(mh, "From"),
|
||||
Sender: parseAddressList(mh, "Sender"),
|
||||
ReplyTo: parseAddressList(mh, "Reply-To"),
|
||||
To: parseAddressList(mh, "To"),
|
||||
Cc: parseAddressList(mh, "Cc"),
|
||||
Bcc: parseAddressList(mh, "Bcc"),
|
||||
InReplyTo: inReplyTo,
|
||||
MessageID: messageID,
|
||||
}
|
||||
}
|
||||
|
||||
func parseAddressList(mh mail.Header, k string) []imap.Address {
|
||||
// TODO: handle groups
|
||||
addrs, _ := mh.AddressList(k)
|
||||
var l []imap.Address
|
||||
for _, addr := range addrs {
|
||||
mailbox, host, ok := strings.Cut(addr.Address, "@")
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
l = append(l, imap.Address{
|
||||
Name: addr.Name,
|
||||
Mailbox: mailbox,
|
||||
Host: host,
|
||||
})
|
||||
}
|
||||
return l
|
||||
}
|
||||
|
||||
// ExtractBodyStructure extracts the structure of a message body.
|
||||
//
|
||||
// It can be used by server backends to implement Session.Fetch.
|
||||
func ExtractBodyStructure(r io.Reader) imap.BodyStructure {
|
||||
br := bufio.NewReader(r)
|
||||
header, _ := textproto.ReadHeader(br)
|
||||
return extractBodyStructure(header, br)
|
||||
}
|
||||
|
||||
func extractBodyStructure(rawHeader textproto.Header, r io.Reader) imap.BodyStructure {
|
||||
header := gomessage.Header{rawHeader}
|
||||
|
||||
mediaType, typeParams, _ := header.ContentType()
|
||||
primaryType, subType, _ := strings.Cut(mediaType, "/")
|
||||
|
||||
if primaryType == "multipart" {
|
||||
bs := &imap.BodyStructureMultiPart{Subtype: subType}
|
||||
mr := textproto.NewMultipartReader(r, typeParams["boundary"])
|
||||
for {
|
||||
part, _ := mr.NextPart()
|
||||
if part == nil {
|
||||
break
|
||||
}
|
||||
bs.Children = append(bs.Children, extractBodyStructure(part.Header, part))
|
||||
}
|
||||
bs.Extended = &imap.BodyStructureMultiPartExt{
|
||||
Params: typeParams,
|
||||
Disposition: getContentDisposition(header),
|
||||
Language: getContentLanguage(header),
|
||||
Location: header.Get("Content-Location"),
|
||||
}
|
||||
return bs
|
||||
} else {
|
||||
body, _ := io.ReadAll(r) // TODO: optimize
|
||||
bs := &imap.BodyStructureSinglePart{
|
||||
Type: primaryType,
|
||||
Subtype: subType,
|
||||
Params: typeParams,
|
||||
ID: header.Get("Content-Id"),
|
||||
Description: header.Get("Content-Description"),
|
||||
Encoding: header.Get("Content-Transfer-Encoding"),
|
||||
Size: uint32(len(body)),
|
||||
}
|
||||
if mediaType == "message/rfc822" || mediaType == "message/global" {
|
||||
br := bufio.NewReader(bytes.NewReader(body))
|
||||
childHeader, _ := textproto.ReadHeader(br)
|
||||
bs.MessageRFC822 = &imap.BodyStructureMessageRFC822{
|
||||
Envelope: ExtractEnvelope(childHeader),
|
||||
BodyStructure: extractBodyStructure(childHeader, br),
|
||||
NumLines: int64(bytes.Count(body, []byte("\n"))),
|
||||
}
|
||||
}
|
||||
if primaryType == "text" {
|
||||
bs.Text = &imap.BodyStructureText{
|
||||
NumLines: int64(bytes.Count(body, []byte("\n"))),
|
||||
}
|
||||
}
|
||||
bs.Extended = &imap.BodyStructureSinglePartExt{
|
||||
Disposition: getContentDisposition(header),
|
||||
Language: getContentLanguage(header),
|
||||
Location: header.Get("Content-Location"),
|
||||
}
|
||||
return bs
|
||||
}
|
||||
}
|
||||
|
||||
func getContentDisposition(header gomessage.Header) *imap.BodyStructureDisposition {
|
||||
disp, dispParams, _ := header.ContentDisposition()
|
||||
if disp == "" {
|
||||
return nil
|
||||
}
|
||||
return &imap.BodyStructureDisposition{
|
||||
Value: disp,
|
||||
Params: dispParams,
|
||||
}
|
||||
}
|
||||
|
||||
func getContentLanguage(header gomessage.Header) []string {
|
||||
v := header.Get("Content-Language")
|
||||
if v == "" {
|
||||
return nil
|
||||
}
|
||||
// TODO: handle CFWS
|
||||
l := strings.Split(v, ",")
|
||||
for i, lang := range l {
|
||||
l[i] = strings.TrimSpace(lang)
|
||||
}
|
||||
return l
|
||||
}
|
||||
Reference in New Issue
Block a user