feat(mail): RFC822 message parsing (headers, body, attachments)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-21 23:51:29 +01:00
parent b9d0b57f84
commit d73aabca96
5 changed files with 220 additions and 0 deletions
+98
View File
@@ -0,0 +1,98 @@
// Package mail provides IMAP reading and RFC822 message parsing.
package mail
import (
"bytes"
"io"
"strings"
"github.com/emersion/go-message/mail"
)
type Header struct {
UID uint32
From string
To string
Subject string
Date string
MessageID string
HasAttachments bool
}
type Attachment struct {
Name string
Size int
MIME string
Content []byte
}
type Message struct {
Header Header
BodyText string
Attachments []Attachment
}
func readHeader(mr *mail.Reader, uid uint32) Header {
h := Header{UID: uid}
hd := mr.Header
h.Subject, _ = hd.Subject()
if addrs, err := hd.AddressList("From"); err == nil && len(addrs) > 0 {
h.From = addrs[0].String()
}
if addrs, err := hd.AddressList("To"); err == nil && len(addrs) > 0 {
h.To = addrs[0].String()
}
if d, err := hd.Date(); err == nil {
h.Date = d.UTC().Format("Mon, 02 Jan 2006 15:04:05 -0700")
}
if msgID, err := hd.MessageID(); err == nil {
h.MessageID = msgID
} else {
h.MessageID = strings.Trim(hd.Get("Message-Id"), "<> ")
}
return h
}
// ParseMessage decodes the full message including attachment contents.
func ParseMessage(uid uint32, raw []byte) (Message, error) {
mr, err := mail.CreateReader(bytes.NewReader(raw))
if err != nil {
return Message{}, err
}
m := Message{Header: readHeader(mr, uid)}
for {
part, err := mr.NextPart()
if err == io.EOF {
break
}
if err != nil {
return Message{}, err
}
switch hdr := part.Header.(type) {
case *mail.InlineHeader:
ct, _, _ := hdr.ContentType()
if strings.HasPrefix(ct, "text/plain") && m.BodyText == "" {
b, _ := io.ReadAll(part.Body)
m.BodyText = string(b)
}
case *mail.AttachmentHeader:
name, _ := hdr.Filename()
ct, _, _ := hdr.ContentType()
b, _ := io.ReadAll(part.Body)
m.Attachments = append(m.Attachments, Attachment{
Name: name, Size: len(b), MIME: ct, Content: b,
})
}
}
m.Header.HasAttachments = len(m.Attachments) > 0
return m, nil
}
// ParseHeaderOnly decodes headers and detects attachments without keeping bodies.
func ParseHeaderOnly(uid uint32, raw []byte) (Header, error) {
m, err := ParseMessage(uid, raw)
if err != nil {
return Header{}, err
}
return m.Header, nil
}