8379fddbb2
list and search now fetch BODY.PEEK[HEADER] + BODYSTRUCTURE instead of the whole RFC822 message, so listing a large mailbox no longer downloads every message body and attachment. Header parsing reuses the same go-message path (RFC2047 decoding/formatting preserved); has_attachments is derived from the BODYSTRUCTURE tree. FetchFull keeps fetching the full message for get. Validated end-to-end against a live IMAP account: list/search/get output identical to the prior full-fetch behaviour, has_attachments correct. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
102 lines
2.8 KiB
Go
102 lines
2.8 KiB
Go
package mail
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
)
|
|
|
|
func loadFixture(t *testing.T, name string) []byte {
|
|
t.Helper()
|
|
b, err := os.ReadFile(filepath.Join("testdata", name))
|
|
if err != nil {
|
|
t.Fatalf("read fixture: %v", err)
|
|
}
|
|
return b
|
|
}
|
|
|
|
func TestParseMessage(t *testing.T) {
|
|
raw := loadFixture(t, "with_attachment.eml")
|
|
m, err := ParseMessage(42, raw)
|
|
if err != nil {
|
|
t.Fatalf("ParseMessage: %v", err)
|
|
}
|
|
if m.Header.UID != 42 {
|
|
t.Fatalf("uid: %d", m.Header.UID)
|
|
}
|
|
if m.Header.Subject != "Your Invoice #5" {
|
|
t.Fatalf("subject: %q", m.Header.Subject)
|
|
}
|
|
if m.Header.From != `"Bob" <bob@trusted.com>` && m.Header.From != "Bob <bob@trusted.com>" {
|
|
t.Fatalf("from: %q", m.Header.From)
|
|
}
|
|
if m.Header.MessageID != "abc123@trusted.com" && m.Header.MessageID != "<abc123@trusted.com>" {
|
|
t.Fatalf("message-id: %q", m.Header.MessageID)
|
|
}
|
|
if want := "Hello, here is your invoice."; !contains(m.BodyText, want) {
|
|
t.Fatalf("body=%q want contains %q", m.BodyText, want)
|
|
}
|
|
if !m.Header.HasAttachments {
|
|
t.Fatal("HasAttachments should be true")
|
|
}
|
|
if len(m.Attachments) != 1 || m.Attachments[0].Name != "invoice.txt" {
|
|
t.Fatalf("attachments: %+v", m.Attachments)
|
|
}
|
|
if !contains(string(m.Attachments[0].Content), "LINE-ITEM 1") {
|
|
t.Fatalf("attachment content: %q", m.Attachments[0].Content)
|
|
}
|
|
}
|
|
|
|
func TestParseHeaderOnly(t *testing.T) {
|
|
raw := loadFixture(t, "with_attachment.eml")
|
|
h, err := ParseHeaderOnly(7, raw)
|
|
if err != nil {
|
|
t.Fatalf("ParseHeaderOnly: %v", err)
|
|
}
|
|
if h.Subject != "Your Invoice #5" || !h.HasAttachments {
|
|
t.Fatalf("header: %+v", h)
|
|
}
|
|
}
|
|
|
|
func TestParseHeaderBytes(t *testing.T) {
|
|
// Header-only bytes (no body), as returned by an IMAP BODY[HEADER] fetch,
|
|
// including an RFC2047-encoded subject to confirm decoding is preserved.
|
|
raw := []byte("From: \"Bob\" <bob@trusted.com>\r\n" +
|
|
"To: me@example.com\r\n" +
|
|
"Subject: =?UTF-8?Q?Caf=C3=A9=20Invoice?=\r\n" +
|
|
"Date: Sat, 20 Jun 2026 10:00:00 +0000\r\n" +
|
|
"Message-ID: <abc123@trusted.com>\r\n" +
|
|
"\r\n")
|
|
h, err := ParseHeaderBytes(9, raw)
|
|
if err != nil {
|
|
t.Fatalf("ParseHeaderBytes: %v", err)
|
|
}
|
|
if h.UID != 9 {
|
|
t.Fatalf("uid: %d", h.UID)
|
|
}
|
|
if h.Subject != "Café Invoice" {
|
|
t.Fatalf("subject not RFC2047-decoded: %q", h.Subject)
|
|
}
|
|
if h.From != `"Bob" <bob@trusted.com>` {
|
|
t.Fatalf("from: %q", h.From)
|
|
}
|
|
if h.MessageID != "abc123@trusted.com" && h.MessageID != "<abc123@trusted.com>" {
|
|
t.Fatalf("message-id: %q", h.MessageID)
|
|
}
|
|
// HasAttachments is left false — the IMAP layer sets it from BODYSTRUCTURE.
|
|
if h.HasAttachments {
|
|
t.Fatal("ParseHeaderBytes must not set HasAttachments")
|
|
}
|
|
}
|
|
|
|
func contains(s, sub string) bool {
|
|
return len(s) >= len(sub) && (func() bool {
|
|
for i := 0; i+len(sub) <= len(s); i++ {
|
|
if s[i:i+len(sub)] == sub {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}())
|
|
}
|