Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
}

Expand Down
5 changes: 4 additions & 1 deletion config/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand All @@ -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.
Expand Down Expand Up @@ -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
}
Expand Down
67 changes: 67 additions & 0 deletions config/cache_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
Loading