2024-12-10 19:54:22 +00:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bufio"
|
2025-01-17 18:13:01 +00:00
|
|
|
"bytes"
|
|
|
|
"fmt"
|
|
|
|
"github.com/google/uuid"
|
2024-12-10 19:54:22 +00:00
|
|
|
"io"
|
2025-01-17 18:13:01 +00:00
|
|
|
"mime"
|
|
|
|
"strings"
|
2024-12-10 19:54:22 +00:00
|
|
|
"time"
|
|
|
|
|
2025-01-17 18:13:01 +00:00
|
|
|
"github.com/emersion/go-imap/v2"
|
|
|
|
"github.com/emersion/go-imap/v2/imapserver"
|
|
|
|
gomessage "github.com/emersion/go-message"
|
|
|
|
"github.com/emersion/go-message/mail"
|
2024-12-10 19:54:22 +00:00
|
|
|
"github.com/emersion/go-message/textproto"
|
|
|
|
)
|
|
|
|
|
2025-01-17 18:13:01 +00:00
|
|
|
type message struct {
|
|
|
|
// immutable
|
|
|
|
uid imap.UID
|
|
|
|
buf io.ReadCloser
|
|
|
|
len int64
|
|
|
|
t time.Time
|
|
|
|
id uuid.UUID
|
|
|
|
lines int64
|
|
|
|
|
|
|
|
// mutable, protected by Mailbox.mutex
|
|
|
|
flags map[imap.Flag]struct{}
|
2024-12-10 19:54:22 +00:00
|
|
|
}
|
|
|
|
|
2025-01-17 18:13:01 +00:00
|
|
|
func (msg *message) fetch(w *imapserver.FetchResponseWriter, options *imap.FetchOptions) error {
|
|
|
|
w.WriteUID(msg.uid)
|
|
|
|
|
|
|
|
if options.Flags {
|
|
|
|
fmt.Println("FLAGS: ", msg.flagList())
|
|
|
|
w.WriteFlags(msg.flagList())
|
|
|
|
}
|
|
|
|
if options.InternalDate {
|
|
|
|
w.WriteInternalDate(msg.t)
|
|
|
|
}
|
|
|
|
if options.RFC822Size {
|
|
|
|
fmt.Println("RFC822SIZE: ", msg.len)
|
|
|
|
w.WriteRFC822Size(msg.len)
|
|
|
|
}
|
|
|
|
if options.Envelope {
|
|
|
|
w.WriteEnvelope(msg.envelope())
|
|
|
|
}
|
|
|
|
bs := options.BodyStructure
|
|
|
|
if bs != nil {
|
|
|
|
structure, err := msg.bodyStructure(bs.Extended)
|
|
|
|
if err != nil {
|
|
|
|
println("BODYSTRUCTURE ERROR: ", err.Error())
|
|
|
|
return err
|
2024-12-10 19:54:22 +00:00
|
|
|
}
|
2025-01-17 18:13:01 +00:00
|
|
|
println("BODYSTRUCTURE: ", structure.Disposition().Value)
|
|
|
|
w.WriteBodyStructure(structure)
|
2024-12-10 19:54:22 +00:00
|
|
|
}
|
|
|
|
|
2025-01-17 18:13:01 +00:00
|
|
|
for _, bs := range options.BodySection {
|
|
|
|
buf := msg.bodySection(bs)
|
|
|
|
wc := w.WriteBodySection(bs, msg.len)
|
|
|
|
_, writeErr := wc.Write(buf)
|
|
|
|
closeErr := wc.Close()
|
|
|
|
if writeErr != nil {
|
|
|
|
return writeErr
|
|
|
|
}
|
|
|
|
if closeErr != nil {
|
|
|
|
return closeErr
|
|
|
|
}
|
|
|
|
}
|
2024-12-10 19:54:22 +00:00
|
|
|
|
2025-01-17 18:13:01 +00:00
|
|
|
return w.Close()
|
2024-12-10 19:54:22 +00:00
|
|
|
}
|
|
|
|
|
2025-01-17 18:13:01 +00:00
|
|
|
func (msg *message) envelope() *imap.Envelope {
|
|
|
|
br := bufio.NewReader(msg.buf)
|
|
|
|
header, err := textproto.ReadHeader(br)
|
|
|
|
if err != nil {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
return getEnvelope(header)
|
2024-12-10 19:54:22 +00:00
|
|
|
}
|
|
|
|
|
2025-01-17 18:13:01 +00:00
|
|
|
func (msg *message) bodyStructure(extended bool) (imap.BodyStructure, error) {
|
|
|
|
br := bufio.NewReader(msg.buf)
|
|
|
|
header, err := textproto.ReadHeader(br)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return getBodyStructure(header, br, extended, uint32(msg.len), msg.lines)
|
2024-12-10 19:54:22 +00:00
|
|
|
}
|
|
|
|
|
2025-01-17 18:13:01 +00:00
|
|
|
func openMessagePart(header textproto.Header, body io.Reader, parentMediaType string) (textproto.Header, io.Reader) {
|
|
|
|
msgHeader := gomessage.Header{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
|
2024-12-10 19:54:22 +00:00
|
|
|
}
|
2025-01-17 18:13:01 +00:00
|
|
|
return header, body
|
2024-12-10 19:54:22 +00:00
|
|
|
}
|
|
|
|
|
2025-01-17 18:13:01 +00:00
|
|
|
func (msg *message) bodySection(item *imap.FetchItemBodySection) []byte {
|
|
|
|
var (
|
|
|
|
header textproto.Header
|
|
|
|
body io.Reader
|
|
|
|
)
|
2024-12-10 19:54:22 +00:00
|
|
|
|
2025-01-17 18:13:01 +00:00
|
|
|
br := bufio.NewReader(msg.buf)
|
|
|
|
header, err := textproto.ReadHeader(br)
|
|
|
|
if err != nil {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
body = br
|
|
|
|
|
|
|
|
// First part of non-multipart message refers to the message itself
|
|
|
|
msgHeader := gomessage.Header{Header: header}
|
|
|
|
mediaType, _, _ := msgHeader.ContentType()
|
|
|
|
partPath := item.Part
|
|
|
|
if !strings.HasPrefix(mediaType, "multipart/") && len(partPath) > 0 && partPath[0] == 1 {
|
|
|
|
partPath = partPath[1:]
|
|
|
|
}
|
|
|
|
|
|
|
|
// Find the requested part using the provided path
|
|
|
|
var parentMediaType string
|
|
|
|
for i := 0; i < len(partPath); i++ {
|
|
|
|
partNum := partPath[i]
|
|
|
|
|
|
|
|
header, body = openMessagePart(header, body, parentMediaType)
|
|
|
|
|
|
|
|
msgHeader := gomessage.Header{Header: header}
|
|
|
|
mediaType, typeParams, _ := msgHeader.ContentType()
|
|
|
|
if !strings.HasPrefix(mediaType, "multipart/") {
|
|
|
|
if partNum != 1 {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
continue
|
|
|
|
}
|
2024-12-10 19:54:22 +00:00
|
|
|
|
2025-01-17 18:13:01 +00:00
|
|
|
mr := textproto.NewMultipartReader(body, typeParams["boundary"])
|
|
|
|
found := false
|
|
|
|
for j := 1; j <= partNum; j++ {
|
|
|
|
p, err := mr.NextPart()
|
2024-12-10 19:54:22 +00:00
|
|
|
if err != nil {
|
2025-01-17 18:13:01 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
if j == partNum {
|
|
|
|
parentMediaType = mediaType
|
|
|
|
header = p.Header
|
|
|
|
body = p
|
|
|
|
found = true
|
2024-12-10 19:54:22 +00:00
|
|
|
break
|
|
|
|
}
|
2025-01-17 18:13:01 +00:00
|
|
|
}
|
|
|
|
if !found {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
2024-12-10 19:54:22 +00:00
|
|
|
|
2025-01-17 18:13:01 +00:00
|
|
|
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()
|
2024-12-10 19:54:22 +00:00
|
|
|
}
|
2025-01-17 18:13:01 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
for _, k := range item.HeaderFieldsNot {
|
|
|
|
header.Del(k)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Write the requested data to a buffer
|
|
|
|
var buf bytes.Buffer
|
2024-12-10 19:54:22 +00:00
|
|
|
|
2025-01-17 18:13:01 +00:00
|
|
|
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
|
2024-12-10 19:54:22 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-01-17 18:13:01 +00:00
|
|
|
switch item.Specifier {
|
|
|
|
case imap.PartSpecifierNone, imap.PartSpecifierText:
|
|
|
|
if _, err := io.Copy(&buf, body); err != nil {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Extract partial if any
|
|
|
|
b := buf.Bytes()
|
|
|
|
if partial := item.Partial; partial != nil {
|
|
|
|
end := partial.Offset + partial.Size
|
|
|
|
if partial.Offset > int64(len(b)) {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
if end > int64(len(b)) {
|
|
|
|
end = int64(len(b))
|
|
|
|
}
|
|
|
|
b = b[partial.Offset:end]
|
|
|
|
}
|
|
|
|
return b
|
2024-12-10 19:54:22 +00:00
|
|
|
}
|
|
|
|
|
2025-01-17 18:13:01 +00:00
|
|
|
func (msg *message) flagList() []imap.Flag {
|
|
|
|
var flags []imap.Flag
|
|
|
|
for flag := range msg.flags {
|
|
|
|
flags = append(flags, flag)
|
|
|
|
}
|
|
|
|
return flags
|
2024-12-10 19:54:22 +00:00
|
|
|
}
|
|
|
|
|
2025-01-17 18:13:01 +00:00
|
|
|
func (msg *message) store(store *imap.StoreFlags) {
|
|
|
|
switch store.Op {
|
|
|
|
case imap.StoreFlagsSet:
|
|
|
|
msg.flags = make(map[imap.Flag]struct{})
|
|
|
|
fallthrough
|
|
|
|
case imap.StoreFlagsAdd:
|
|
|
|
for _, flag := range store.Flags {
|
|
|
|
msg.flags[canonicalFlag(flag)] = struct{}{}
|
|
|
|
}
|
|
|
|
case imap.StoreFlagsDel:
|
|
|
|
for _, flag := range store.Flags {
|
|
|
|
delete(msg.flags, canonicalFlag(flag))
|
|
|
|
}
|
|
|
|
default:
|
|
|
|
panic(fmt.Errorf("unknown STORE flag operation: %v", store.Op))
|
2024-12-10 19:54:22 +00:00
|
|
|
}
|
2025-01-17 18:13:01 +00:00
|
|
|
}
|
2024-12-10 19:54:22 +00:00
|
|
|
|
2025-01-17 18:13:01 +00:00
|
|
|
func (msg *message) reader() *gomessage.Entity {
|
|
|
|
r, _ := gomessage.Read(msg.buf)
|
|
|
|
if r == nil {
|
|
|
|
r, _ = gomessage.New(gomessage.Header{}, bytes.NewReader(nil))
|
|
|
|
}
|
|
|
|
return r
|
|
|
|
}
|
2024-12-10 19:54:22 +00:00
|
|
|
|
2025-01-17 18:13:01 +00:00
|
|
|
func (msg *message) search(seqNum uint32, criteria *imap.SearchCriteria) bool {
|
|
|
|
for _, seqSet := range criteria.SeqNum {
|
|
|
|
if seqNum == 0 || !seqSet.Contains(seqNum) {
|
|
|
|
println("DOES NOT CONTAIN 1")
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for _, uidSet := range criteria.UID {
|
|
|
|
if !uidSet.Contains(msg.uid) {
|
|
|
|
println("DOES NOT CONTAIN 2")
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if !matchDate(msg.t, criteria.Since, criteria.Before) {
|
|
|
|
println("DOES NOT CONTAIN 3")
|
|
|
|
return false
|
2024-12-10 19:54:22 +00:00
|
|
|
}
|
|
|
|
|
2025-01-17 18:13:01 +00:00
|
|
|
for _, flag := range criteria.Flag {
|
|
|
|
if _, ok := msg.flags[canonicalFlag(flag)]; !ok {
|
|
|
|
println("DOES NOT CONTAIN 4")
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for _, flag := range criteria.NotFlag {
|
|
|
|
if _, ok := msg.flags[canonicalFlag(flag)]; ok {
|
|
|
|
println("DOES NOT CONTAIN 5")
|
|
|
|
return false
|
|
|
|
}
|
2024-12-10 19:54:22 +00:00
|
|
|
}
|
|
|
|
|
2025-01-17 18:13:01 +00:00
|
|
|
if criteria.Larger != 0 && msg.len <= criteria.Larger {
|
|
|
|
println("DOES NOT CONTAIN 6")
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
if criteria.Smaller != 0 && msg.len >= criteria.Smaller {
|
|
|
|
println("DOES NOT CONTAIN 7")
|
|
|
|
return false
|
2024-12-10 19:54:22 +00:00
|
|
|
}
|
|
|
|
|
2025-01-17 18:13:01 +00:00
|
|
|
header := mail.Header{Header: msg.reader().Header}
|
|
|
|
|
|
|
|
for _, fieldCriteria := range criteria.Header {
|
|
|
|
if !matchHeaderFields(header.FieldsByKey(fieldCriteria.Key), fieldCriteria.Value) {
|
|
|
|
println("DOES NOT CONTAIN 8")
|
|
|
|
return false
|
|
|
|
}
|
2024-12-10 19:54:22 +00:00
|
|
|
}
|
|
|
|
|
2025-01-17 18:13:01 +00:00
|
|
|
if !criteria.SentSince.IsZero() || !criteria.SentBefore.IsZero() {
|
|
|
|
t, err := header.Date()
|
|
|
|
if err != nil {
|
|
|
|
println("DOES NOT CONTAIN 9")
|
|
|
|
return false
|
|
|
|
} else if !matchDate(t, criteria.SentSince, criteria.SentBefore) {
|
|
|
|
println("DOES NOT CONTAIN 10")
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, text := range criteria.Text {
|
|
|
|
if !matchEntity(msg.reader(), text, true) {
|
|
|
|
println("DOES NOT CONTAIN 11")
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for _, body := range criteria.Body {
|
|
|
|
if !matchEntity(msg.reader(), body, false) {
|
|
|
|
println("DOES NOT CONTAIN 12")
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, not := range criteria.Not {
|
|
|
|
if msg.search(seqNum, ¬) {
|
|
|
|
println("DOES NOT CONTAIN 13")
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for _, or := range criteria.Or {
|
|
|
|
if !msg.search(seqNum, &or[0]) && !msg.search(seqNum, &or[1]) {
|
|
|
|
println("DOES NOT CONTAIN 14")
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
func matchDate(t, since, before time.Time) bool {
|
|
|
|
// We discard time zone information by setting it to UTC.
|
|
|
|
// RFC 3501 explicitly requires zone unaware date comparison.
|
|
|
|
t = time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, time.UTC)
|
|
|
|
|
|
|
|
if !since.IsZero() && t.Before(since) {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
if !before.IsZero() && !t.Before(before) {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
func matchHeaderFields(fields gomessage.HeaderFields, pattern string) bool {
|
|
|
|
if pattern == "" {
|
|
|
|
return fields.Len() > 0
|
|
|
|
}
|
|
|
|
|
|
|
|
pattern = strings.ToLower(pattern)
|
|
|
|
for fields.Next() {
|
|
|
|
v, _ := fields.Text()
|
|
|
|
if strings.Contains(strings.ToLower(v), pattern) {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
func matchEntity(e *gomessage.Entity, pattern string, includeHeader bool) bool {
|
|
|
|
if pattern == "" {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
if includeHeader && matchHeaderFields(e.Header.Fields(), pattern) {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
if mr := e.MultipartReader(); mr != nil {
|
|
|
|
for {
|
|
|
|
part, err := mr.NextPart()
|
|
|
|
if err == io.EOF {
|
|
|
|
break
|
|
|
|
} else if err != nil {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
if matchEntity(part, pattern, includeHeader) {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return false
|
|
|
|
} else {
|
|
|
|
t, _, err := e.Header.ContentType()
|
|
|
|
if err != nil {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
if !strings.HasPrefix(t, "text/") && !strings.HasPrefix(t, "message/") {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
buf, err := io.ReadAll(e.Body)
|
|
|
|
if err != nil {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
return bytes.Contains(bytes.ToLower(buf), bytes.ToLower([]byte(pattern)))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func getEnvelope(h textproto.Header) *imap.Envelope {
|
|
|
|
mh := mail.Header{Header: gomessage.Header{Header: h}}
|
|
|
|
date, _ := mh.Date()
|
|
|
|
inReplyTo, _ := mh.MsgIDList("In-Reply-To")
|
|
|
|
messageID, _ := mh.MessageID()
|
|
|
|
return &imap.Envelope{
|
|
|
|
Date: date,
|
|
|
|
Subject: h.Get("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: leave the quoted words unchanged
|
|
|
|
// 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: mime.QEncoding.Encode("utf-8", addr.Name),
|
|
|
|
Mailbox: mailbox,
|
|
|
|
Host: host,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
return l
|
|
|
|
}
|
|
|
|
|
|
|
|
func getBodyStructure(rawHeader textproto.Header, r io.Reader, extended bool, length uint32, lines int64) (imap.BodyStructure, error) {
|
|
|
|
header := gomessage.Header{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
|
|
|
|
}
|
|
|
|
structure, err := getBodyStructure(part.Header, part, extended, length, lines)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
bs.Children = append(bs.Children, structure)
|
|
|
|
}
|
|
|
|
if extended {
|
|
|
|
bs.Extended = &imap.BodyStructureMultiPartExt{
|
|
|
|
Params: typeParams,
|
|
|
|
Disposition: getContentDisposition(header),
|
|
|
|
Language: getContentLanguage(header),
|
|
|
|
Location: header.Get("Content-Location"),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return bs, nil
|
|
|
|
} else {
|
|
|
|
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: length,
|
|
|
|
}
|
|
|
|
if mediaType == "message/rfc822" || mediaType == "message/global" {
|
|
|
|
br := bufio.NewReader(r)
|
|
|
|
childHeader, err := textproto.ReadHeader(br)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
structure, err := getBodyStructure(childHeader, br, extended, length, lines)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
bs.MessageRFC822 = &imap.BodyStructureMessageRFC822{
|
|
|
|
Envelope: getEnvelope(childHeader),
|
|
|
|
BodyStructure: structure,
|
|
|
|
NumLines: lines,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if primaryType == "text" {
|
|
|
|
bs.Text = &imap.BodyStructureText{
|
|
|
|
NumLines: lines,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if extended {
|
|
|
|
bs.Extended = &imap.BodyStructureSinglePartExt{
|
|
|
|
Disposition: getContentDisposition(header),
|
|
|
|
Language: getContentLanguage(header),
|
|
|
|
Location: header.Get("Content-Location"),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return bs, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
}
|
2024-12-10 19:54:22 +00:00
|
|
|
|
2025-01-17 18:13:01 +00:00
|
|
|
func canonicalFlag(flag imap.Flag) imap.Flag {
|
|
|
|
return imap.Flag(strings.ToLower(string(flag)))
|
2024-12-10 19:54:22 +00:00
|
|
|
}
|