From a612fd0a4984c62719706c8130830ce2265df43e Mon Sep 17 00:00:00 2001 From: Engineer Date: Mon, 28 Sep 2026 16:07:14 +0200 Subject: [PATCH] fix: cache inline image payloads CachedAttachment previously stored only metadata, dropping the decoded payload of inline (cid:) attachments. On a cache hit, attachments were rebuilt with empty Data, so inlineImagesFromAttachments always skipped them and CID lookup resolved against an empty map. Persist the decoded payload for inline attachments on cache write and restore it on read so cid: images render from cache. --- app/app.go | 6 ++++ config/cache.go | 5 +++- config/cache_test.go | 67 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 77 insertions(+), 1 deletion(-) 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)