Forked the emersion/go-imap v1 project.

This commit is contained in:
2025-05-01 11:58:18 +03:00
commit bcc3f95e8e
107 changed files with 16268 additions and 0 deletions

336
imapserver/message.go Normal file
View 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
}