diff --git a/app/app.go b/app/app.go index 40567ec8..1f2556ed 100644 --- a/app/app.go +++ b/app/app.go @@ -434,6 +434,9 @@ func (m *Model) cachedAttachmentsToFetcher(cached []config.CachedAttachment) []f if ca.IsCalendarInvite && len(ca.CalendarData) > 0 { att.Data = ca.CalendarData } + if ca.Inline && len(ca.Data) > 0 { + att.Data = ca.Data + } attachments = append(attachments, att) } return attachments @@ -455,6 +458,9 @@ func cachedAttachmentToConfig(a fetcher.Attachment) config.CachedAttachment { if a.IsCalendarInvite && len(a.Data) > 0 { ca.CalendarData = a.Data } + if a.Inline && len(a.Data) > 0 { + ca.Data = a.Data + } return ca } diff --git a/config/cache.go b/config/cache.go index a4f5d507..a9d72927 100644 --- a/config/cache.go +++ b/config/cache.go @@ -722,7 +722,8 @@ func removeAccountFromDraftsCache(accountID string) error { // --- Email Body Cache --- -// CachedAttachment stores attachment metadata (not the binary data). +// CachedAttachment stores attachment metadata and, for inline attachments, +// the decoded payload needed to render CID images. type CachedAttachment struct { Filename string `json:"filename"` PartID string `json:"part_id"` @@ -735,6 +736,7 @@ type CachedAttachment struct { IsSMIMEEncrypted bool `json:"is_smime_encrypted,omitempty"` IsCalendarInvite bool `json:"is_calendar_invite,omitempty"` CalendarData []byte `json:"calendar_data,omitempty"` // Raw .ics data for calendar invites + Data []byte `json:"data,omitempty"` // Decoded payload for inline attachments (e.g. CID images) } // CachedEmailBody stores the body and attachment metadata for a single email. @@ -826,6 +828,7 @@ func calculateEmailBodySize(body *CachedEmailBody) int { size += len(att.MIMEType) size += len(att.ContentID) size += len(att.CalendarData) + size += len(att.Data) } return size } diff --git a/config/cache_test.go b/config/cache_test.go index 4d97d9f0..00f0be6a 100644 --- a/config/cache_test.go +++ b/config/cache_test.go @@ -462,6 +462,73 @@ func TestEmailBodyCache_AttachmentsPreserved(t *testing.T) { } } +func TestEmailBodyCache_InlineImageDataPreserved(t *testing.T) { + setup(t) + + inlineData := []byte{0x89, 0x50, 0x4E, 0x47, 0x00, 0x01, 0x02, 0x03} + a1 := CachedAttachment{ + Filename: "logo.png", + PartID: "2", + MIMEType: "image/png", + ContentID: "logo@example.com", + Inline: true, + Data: inlineData, + } + + a2 := CachedAttachment{ + Filename: "report.pdf", + PartID: "3", + MIMEType: "application/pdf", + } + + body := CachedEmailBody{ + UID: 1, + AccountID: "account", + Body: "inline image body", + Attachments: []CachedAttachment{a1, a2}, + } + + threshold := 100 * 1024 * 1024 + + if err := SaveEmailBody("INBOX", body, threshold); err != nil { + t.Fatalf("SaveEmailBody: %v", err) + } + + output := GetCachedEmailBody("INBOX", 1, "account", threshold) + if output == nil { + t.Fatal("GetCachedEmailBody returned nil") + } + + if len(output.Attachments) != 2 { + t.Fatalf("expected 2 attachments, got %d", len(output.Attachments)) + } + + if got := output.Attachments[0].Data; !slices.Equal(got, inlineData) { + t.Errorf("inline attachment Data: got %v, want %v", got, inlineData) + } + + if output.Attachments[1].Data != nil { + t.Errorf("non-inline attachment should not carry Data, got %v", output.Attachments[1].Data) + } +} + +func TestEmailBodyCache_InlineImageDataCountsTowardSize(t *testing.T) { + body := CachedEmailBody{ + UID: 1, + AccountID: "account", + Body: "hello", + Attachments: []CachedAttachment{ + {Data: []byte("0123456789")}, + {Data: []byte("ABCDE")}, + }, + } + + got := calculateEmailBodySize(&body) + if got != len(body.Body)+10+5 { + t.Errorf("calculateEmailBodySize: got %d, want %d", got, len(body.Body)+10+5) + } +} + func TestLRU_EvictsLeastRecentlyUsed(t *testing.T) { setup(t)