diff --git a/config/cache.go b/config/cache.go index 5ffad48..2025027 100644 --- a/config/cache.go +++ b/config/cache.go @@ -707,12 +707,7 @@ func saveEmailBodyCache(cache *EmailBodyCache) error { // mutate cache state. func GetCachedEmailBody(folderName string, uid uint32, accountID string, threshold int) *CachedEmailBody { lru := GetLRUInstance(threshold) - - if node := lru.Get(folderName, uid, accountID); node != nil { - return node.Body - } - - return nil + return lru.Get(folderName, uid, accountID) } func calculateEmailBodySize(body *CachedEmailBody) int { diff --git a/config/cache_test.go b/config/cache_test.go index b405de2..9c74909 100644 --- a/config/cache_test.go +++ b/config/cache_test.go @@ -1,162 +1,643 @@ package config import ( - "reflect" + "os" + "path/filepath" + "slices" "strings" + "sync" "testing" - "time" ) -func TestSaveEmailBodyEvictsLeastRecentlyAccessedAcrossFolders(t *testing.T) { - folderCacheTestSetup(t) - - oldTime := time.Now().Add(-2 * time.Hour) - recentTime := time.Now().Add(-1 * time.Hour) - - if err := saveEmailBodyCache(&EmailBodyCache{ - FolderName: "INBOX", - Bodies: []CachedEmailBody{ - { - UID: 1, - AccountID: "acct", - Body: strings.Repeat("a", 10), - SizeBytes: 10, - CachedAt: oldTime, - LastAccessedAt: oldTime, - }, - }, - }); err != nil { - t.Fatalf("save old cache: %v", err) - } - - if err := saveEmailBodyCache(&EmailBodyCache{ - FolderName: "Archive", - Bodies: []CachedEmailBody{ - { - UID: 2, - AccountID: "acct", - Body: strings.Repeat("b", 10), - SizeBytes: 10, - CachedAt: recentTime, - LastAccessedAt: recentTime, - }, - }, - }); err != nil { - t.Fatalf("save recent cache: %v", err) - } - - if err := SaveEmailBody("Sent", CachedEmailBody{ - UID: 3, - AccountID: "acct", - Body: strings.Repeat("c", 10), - }, 20); err != nil { - t.Fatalf("SaveEmailBody: %v", err) +func setup(t *testing.T) { + t.Helper() + dir := t.TempDir() + t.Setenv("HOME", dir) + t.Setenv("USERPROFILE", dir) + resetLRU() +} + +func TestEmailCache_SaveLoadRoundTrip(t *testing.T) { + setup(t) + + e1 := CachedEmail{ + UID: 1, + From: "t1@e.com", + To: []string{"t2@e.com"}, + Subject: "Hello", } - inbox, err := LoadEmailBodyCache("INBOX") + e2 := CachedEmail{ + UID: 2, + From: "t2@e.com", + To: []string{"t1@e.com"}, + Subject: "Hello", + } + + input := &EmailCache{Emails: []CachedEmail{e1, e2}} + + if err := SaveEmailCache(input); err != nil { + t.Fatalf("SaveEmailCache: %v", err) + } + + output, err := LoadEmailCache() if err != nil { - t.Fatalf("LoadEmailBodyCache(INBOX): %v", err) + t.Fatalf("LoadEmailCache: %v", err) + } + + if len(output.Emails) != len(input.Emails) { + t.Fatalf("email count: got %d, want %d", len(output.Emails), len(input.Emails)) + } + + for i := range output.Emails { + IN := input.Emails[i] + OU := output.Emails[i] + if IN.UID != OU.UID || IN.From != OU.From || !slices.Equal(IN.To, OU.To) || IN.Subject != OU.Subject { + t.Errorf("email[%d] mismatch: got %+v, want %+v", i, OU, IN) + } + } +} + +func TestEmailCache_HasEmailCache_FalseWhenMissing(t *testing.T) { + setup(t) + if HasEmailCache() { + t.Error("HasEmailCache should be false before any save") + } +} + +func TestEmailCache_HasEmailCache_TrueAfterSave(t *testing.T) { + setup(t) + + if err := SaveEmailCache(&EmailCache{}); err != nil { + t.Fatalf("SaveEmailCache: %v", err) + } + + if !HasEmailCache() { + t.Error("HasEmailCache should be true after save") } - if len(inbox.Bodies) != 0 { - t.Fatalf("oldest INBOX body should be evicted, got %d bodies", len(inbox.Bodies)) +} + +func TestEmailCache_ClearEmailCache(t *testing.T) { + setup(t) + + e := CachedEmail{ + UID: 1, + AccountID: "account", + From: "t1@e.com", + To: []string{"t2@e.com"}, + Subject: "Hello", + } + + if err := SaveEmailCache(&EmailCache{Emails: []CachedEmail{e}}); err != nil { + t.Fatalf("SaveEmailCache: %v", err) + } + + if err := ClearEmailCache(); err != nil { + t.Fatalf("ClearEmailCache: %v", err) } - archive, err := LoadEmailBodyCache("Archive") + if HasEmailCache() { + t.Error("HasEmailCache should be false after clear") + } +} + +func TestEmailCache_RemoveAccount(t *testing.T) { + setup(t) + + e1 := CachedEmail{UID: 1, AccountID: "a1"} + e2 := CachedEmail{UID: 2, AccountID: "a2"} + e3 := CachedEmail{UID: 3, AccountID: "a3"} + + if err := SaveEmailCache(&EmailCache{Emails: []CachedEmail{e1, e2, e3}}); err != nil { + t.Fatalf("SaveEmailCache: %v", err) + } + + if err := removeAccountFromEmailCache("a2"); err != nil { + t.Fatalf("removeAccountFromEmailCache: %v", err) + } + + output, err := LoadEmailCache() if err != nil { - t.Fatalf("LoadEmailBodyCache(Archive): %v", err) + t.Fatalf("LoadEmailCache: %v", err) } - if len(archive.Bodies) != 1 || archive.Bodies[0].UID != 2 { - t.Fatalf("recent Archive body should remain, got %+v", archive.Bodies) + + for _, e := range output.Emails { + if e.AccountID == "a2" { + t.Errorf("found email belonging to removed account AC2: %+v", e) + } } +} + +func TestEmailCache_LoadCorruptFile(t *testing.T) { + setup(t) - sent, err := LoadEmailBodyCache("Sent") + if err := SaveEmailCache(&EmailCache{}); err != nil { + t.Fatalf("SaveEmailCache: %v", err) + } + + path, err := cacheFile() if err != nil { - t.Fatalf("LoadEmailBodyCache(Sent): %v", err) + t.Fatalf("cacheFile: %v", err) } - if len(sent.Bodies) != 1 || sent.Bodies[0].UID != 3 { - t.Fatalf("new Sent body should remain, got %+v", sent.Bodies) + + if err := os.WriteFile(path, []byte("{corrupted json}"), 0600); err != nil { + t.Fatalf("WriteFile: %v", err) + } + + if _, err = LoadEmailCache(); err == nil { + t.Error("LoadEmailCache should return an error for corrupt JSON") } } -func TestSaveEmailBodyEvictsMultipleEntriesUntilUnderLimit(t *testing.T) { - folderCacheTestSetup(t) +func TestContacts_SaveLoadRoundTrip(t *testing.T) { + setup(t) - now := time.Now() - bodies := make([]CachedEmailBody, 0, 4) - for i := 1; i <= 4; i++ { - accessedAt := now.Add(-time.Duration(5-i) * time.Minute) - bodies = append(bodies, CachedEmailBody{ - UID: uint32(i), - AccountID: "acct", - Body: strings.Repeat(string(rune('a'+i-1)), 10), - SizeBytes: 10, - CachedAt: accessedAt, - LastAccessedAt: accessedAt, - }) + c := Contact{ + Name: "t", + Email: "t@e.com", + Addresses: []string{"address 1, address 2"}, } - if err := saveEmailBodyCache(&EmailBodyCache{ - FolderName: "INBOX", - Bodies: bodies, - }); err != nil { - t.Fatalf("save cache: %v", err) + input := &ContactsCache{Contacts: []Contact{c}} + + if err := SaveContactsCache(input); err != nil { + t.Fatalf("SaveContactsCache: %v", err) } - if err := SaveEmailBody("Archive", CachedEmailBody{ - UID: 5, - AccountID: "acct", - Body: strings.Repeat("e", 30), - }, 50); err != nil { - t.Fatalf("SaveEmailBody: %v", err) + output, err := LoadContactsCache() + if err != nil { + t.Fatalf("LoadContactsCache: %v", err) + } + + if len(output.Contacts) != len(input.Contacts) { + t.Fatalf("contacts count mismatch:\n got: %d\n want: %d", len(output.Contacts), len(input.Contacts)) } - inbox, err := LoadEmailBodyCache("INBOX") + for i := range output.Contacts { + IN := input.Contacts[i] + OU := output.Contacts[i] + if IN.Name != OU.Name || IN.Email != OU.Email || !slices.Equal(IN.Addresses, OU.Addresses) { + t.Errorf("contact[%d] mismatch: got %+v, want %+v", i, OU, IN) + } + } +} + +func TestContacts_SearchEmpty(t *testing.T) { + setup(t) + if results := SearchContacts(""); len(results) != 0 { + t.Errorf("SearchContacts(\"\") should return nil, got %d results", len(results)) + } +} + +func TestContacts_LoadCorruptFile(t *testing.T) { + setup(t) + + path, err := GetContactsCachePath() if err != nil { - t.Fatalf("LoadEmailBodyCache(INBOX): %v", err) + t.Fatalf("GetContactsCachePath: %v", err) } - gotUIDs := make([]uint32, 0, len(inbox.Bodies)) - for _, body := range inbox.Bodies { - gotUIDs = append(gotUIDs, body.UID) + if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil { + t.Fatalf("MkdirAll: %v", err) } - wantUIDs := []uint32{3, 4} - if !reflect.DeepEqual(gotUIDs, wantUIDs) { - t.Fatalf("remaining INBOX UIDs = %v, want %v", gotUIDs, wantUIDs) + + if err := os.WriteFile(path, []byte("{corrupted json}"), 0600); err != nil { + t.Fatalf("WriteFile: %v", err) + } + + if _, err = LoadContactsCache(); err == nil { + t.Error("LoadContactsCache should error on corrupt JSON") + } +} + +func TestDrafts_SaveLoadRoundTrip(t *testing.T) { + setup(t) + + d := Draft{ + ID: "draft 1", + To: "d@e.com", + Subject: "Hello World", + AccountID: "a1", + } + + if err := SaveDraft(d); err != nil { + t.Fatalf("SaveDraft: %v", err) + } + + output := GetDraft("draft 1") + if output == nil { + t.Fatal("GetDraft returned nil") + } + + if output.ID != d.ID || output.To != d.To || output.Subject != d.Subject || output.AccountID != d.AccountID { + t.Errorf("draft mismatch: got %+v, want %+v", output, d) + } +} + +func TestDrafts_UpdateExisting(t *testing.T) { + setup(t) + + d := Draft{ + ID: "draft 1", + To: "d@e.com", + Subject: "Hello World", + AccountID: "a1", + } + + if err := SaveDraft(d); err != nil { + t.Fatalf("SaveDraft: %v", err) + } + + d.Subject = "Hello" + if err := SaveDraft(d); err != nil { + t.Fatalf("SaveDraft (update): %v", err) + } + + output := GetAllDrafts() + if len(output) != 1 { + t.Fatalf("expected 1 draft after update, got %d", len(output)) + } + + if output[0].Subject != "Hello" { + t.Errorf("subject: got %q, want %q", output[0].Subject, "Hello") + } +} + +func TestDrafts_Delete(t *testing.T) { + setup(t) + + d := Draft{ + ID: "draft 1", + To: "d@e.com", + Subject: "Hello World", + AccountID: "a1", } - archive, err := LoadEmailBodyCache("Archive") + if err := SaveDraft(d); err != nil { + t.Fatalf("SaveDraft: %v", err) + } + + if err := DeleteDraft("draft 1"); err != nil { + t.Fatalf("DeleteDraft: %v", err) + } + + if GetDraft("draft 1") != nil { + t.Error("deleted draft should return nil") + } + + if HasDrafts() { + t.Error("HasDrafts should be false after all drafts deleted") + } +} + +func TestDrafts_LoadCorruptFile(t *testing.T) { + setup(t) + + path, err := draftsFile() if err != nil { - t.Fatalf("LoadEmailBodyCache(Archive): %v", err) + t.Fatalf("draftsFile: %v", err) + } + + if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil { + t.Fatalf("MkdirAll: %v", err) } - if len(archive.Bodies) != 1 || archive.Bodies[0].UID != 5 { - t.Fatalf("new Archive body should remain, got %+v", archive.Bodies) + + if err := os.WriteFile(path, []byte("{corrupted json}"), 0600); err != nil { + t.Fatalf("WriteFile: %v", err) + } + + if _, err = LoadDraftsCache(); err == nil { + t.Error("LoadDraftsCache should error on corrupt JSON") } } -func TestSaveEmailBodyDropsOversizedReplacement(t *testing.T) { - folderCacheTestSetup(t) +func TestEmailBody_SaveLoadRoundTrip(t *testing.T) { + setup(t) - if err := SaveEmailBody("INBOX", CachedEmailBody{ + body := CachedEmailBody{ UID: 1, - AccountID: "acct", - Body: strings.Repeat("a", 10), - }, 20); err != nil { - t.Fatalf("initial SaveEmailBody: %v", err) + AccountID: "account", + Body: "Hello World", } - if err := SaveEmailBody("INBOX", CachedEmailBody{ + 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 output.Body != body.Body { + t.Errorf("body text: got %q, want %q", output.Body, body.Body) + } +} + +func TestEmailBody_FolderIsolation(t *testing.T) { + setup(t) + + b1 := CachedEmailBody{ UID: 1, - AccountID: "acct", - Body: strings.Repeat("b", 25), - }, 20); err != nil { - t.Fatalf("oversized SaveEmailBody: %v", err) + AccountID: "account 1", + Body: "Hello INBOX", + } + + b2 := CachedEmailBody{ + UID: 2, + AccountID: "account 2", + Body: "Hello Sent", + } + + threshold := 100 * 1024 * 1024 + + _ = SaveEmailBody("INBOX", b1, threshold) + _ = SaveEmailBody("Sent", b2, threshold) + + outputInbox := GetCachedEmailBody("INBOX", 1, "account 1", threshold) + outputSent := GetCachedEmailBody("Sent", 2, "account 2", threshold) + + if outputInbox == nil || outputInbox.Body != "Hello INBOX" { + t.Errorf("INBOX body: got %v", outputInbox) + } + if outputSent == nil || outputSent.Body != "Hello Sent" { + t.Errorf("Sent body: got %v", outputSent) + } +} + +func TestEmailBody_PruneRemovesStaleUIDs(t *testing.T) { + setup(t) + + b1 := CachedEmailBody{UID: 1, AccountID: "account 1", Body: "body 1"} + b2 := CachedEmailBody{UID: 2, AccountID: "account 1", Body: "body 2"} + b3 := CachedEmailBody{UID: 3, AccountID: "account 1", Body: "body 3"} + + threshold := 100 * 1024 * 1024 + + _ = SaveEmailBody("INBOX", b1, threshold) + _ = SaveEmailBody("INBOX", b2, threshold) + _ = SaveEmailBody("INBOX", b3, threshold) + + if err := PruneEmailBodyCache("INBOX", map[uint32]string{2: "account 1"}, threshold); err != nil { + t.Fatalf("PruneEmailBodyCache: %v", err) + } + + if GetCachedEmailBody("INBOX", 1, "account 1", threshold) != nil { + t.Error("UID 1 should have been pruned") + } + + if GetCachedEmailBody("INBOX", 3, "account 1", threshold) != nil { + t.Error("UID 3 should have been pruned") } - cache, err := LoadEmailBodyCache("INBOX") + if GetCachedEmailBody("INBOX", 2, "account 1", threshold) == nil { + t.Error("UID 2 should still be cached") + } +} + +func TestEmailBody_CorruptBodyCacheFile(t *testing.T) { + setup(t) + + b := CachedEmailBody{UID: 1, AccountID: "account", Body: "Hello World"} + + _ = SaveEmailBody("INBOX", b, 100*1024*1024) + + path, err := bodyCacheFile("INBOX") if err != nil { - t.Fatalf("LoadEmailBodyCache: %v", err) + t.Fatalf("bodyCacheFile: %v", err) + } + + if err := os.WriteFile(path, []byte("{corrupted json}"), 0600); err != nil { + t.Fatalf("WriteFile: %v", err) } - if len(cache.Bodies) != 0 { - t.Fatalf("oversized replacement should not remain cached, got %+v", cache.Bodies) + + if _, err = LoadEmailBodyCache("INBOX"); err == nil { + t.Error("LoadEmailBodyCache should error on corrupt JSON") + } +} + +func TestEmailBodyCache_AttachmentsPreserved(t *testing.T) { + setup(t) + + a1 := CachedAttachment{ + Filename: "invoice.pdf", + PartID: "2", + MIMEType: "application/pdf", + } + + a2 := CachedAttachment{ + Filename: "meeting.ics", + PartID: "3", + MIMEType: "text/calendar", + } + + body := CachedEmailBody{ + UID: 1, + AccountID: "account", + Body: "attachment", + Attachments: []CachedAttachment{a1, a2}, + } + + threshold := 100 * 1024 * 1024 + + _ = SaveEmailBody("INBOX", body, threshold) + + 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 output.Attachments[0].Filename != "invoice.pdf" { + t.Errorf("attachment[0].Filename: got %q", output.Attachments[0].Filename) + } + + if output.Attachments[1].Filename != "meeting.ics" { + t.Errorf("attachment[1].Filename: got %q", output.Attachments[1].Filename) + } +} + +func TestLRU_EvictsLeastRecentlyUsed(t *testing.T) { + setup(t) + + body := strings.Repeat("a", 100) + + threshold := 250 + + b1 := CachedEmailBody{UID: 1, AccountID: "account", Body: body, SizeBytes: len(body)} + b2 := CachedEmailBody{UID: 2, AccountID: "account", Body: body, SizeBytes: len(body)} + b3 := CachedEmailBody{UID: 3, AccountID: "account", Body: body, SizeBytes: len(body)} + + _ = SaveEmailBody("INBOX", b1, threshold) + _ = SaveEmailBody("INBOX", b2, threshold) + _ = SaveEmailBody("INBOX", b3, threshold) + + if GetCachedEmailBody("INBOX", 1, "account", threshold) != nil { + t.Error("UID 1 should have been evicted (LRU)") + } + + if GetCachedEmailBody("INBOX", 2, "account", threshold) == nil { + t.Error("UID 2 should still be cached") + } + + if GetCachedEmailBody("INBOX", 3, "account", threshold) == nil { + t.Error("UID 3 should still be cached") + } +} + +func TestLRU_OversizedBodyRejected(t *testing.T) { + setup(t) + + body := CachedEmailBody{ + UID: 1, + AccountID: "account", + Body: strings.Repeat("a", 100), + } + + _ = SaveEmailBody("INBOX", body, 50) + + if GetCachedEmailBody("INBOX", 1, "account", 50) != nil { + t.Error("oversized body should not be stored in LRU") + } +} + +func TestLRU_GetPromotesToFront(t *testing.T) { + setup(t) + + b1 := CachedEmailBody{UID: 1, AccountID: "account", Body: strings.Repeat("a", 50)} + b2 := CachedEmailBody{UID: 2, AccountID: "account", Body: strings.Repeat("a", 50)} + + threshold := 100 + + _ = SaveEmailBody("INBOX", b1, threshold) + _ = SaveEmailBody("INBOX", b2, threshold) + + GetCachedEmailBody("INBOX", 1, "account", threshold) + + b3 := CachedEmailBody{UID: 3, AccountID: "account", Body: strings.Repeat("a", 50)} + _ = SaveEmailBody("INBOX", b3, threshold) + + if GetCachedEmailBody("INBOX", 2, "account", threshold) != nil { + t.Error("UID 2 should have been evicted (LRU after promotion of UID 1)") + } + + if GetCachedEmailBody("INBOX", 1, "account", threshold) == nil { + t.Error("UID 1 should still be cached (was promoted)") + } +} + +func TestLRU_DeleteRemovesEntry(t *testing.T) { + setup(t) + + b := CachedEmailBody{UID: 1, AccountID: "account", Body: strings.Repeat("a", 50)} + + threshold := 100 + + _ = SaveEmailBody("INBOX", b, threshold) + + GetLRUInstance(threshold).Delete("INBOX", 1, "account") + + if GetCachedEmailBody("INBOX", 1, "account", threshold) != nil { + t.Error("deleted entry should not be retrievable") + } +} + +func TestLRU_ThresholdUpdate(t *testing.T) { + setup(t) + + lru1 := GetLRUInstance(100) + if lru1.threshold != 100 { + t.Errorf("threshold: got %d, want %d", lru1.threshold, 100) + } + + lru2 := GetLRUInstance(50) + if lru2.threshold != 50 { + t.Errorf("updated threshold: got %d, want %d", lru2.threshold, 50) + } + + if lru1 != lru2 { + t.Error("GetLRUInstance should always return the same pointer") + } +} + +func TestEmailBody_EvictsLeastRecentlyAccessedAcrossFolders(t *testing.T) { + setup(t) + + b1 := CachedEmailBody{UID: 1, AccountID: "account", Body: strings.Repeat("a", 50)} + b2 := CachedEmailBody{UID: 2, AccountID: "account", Body: strings.Repeat("a", 50)} + b3 := CachedEmailBody{UID: 3, AccountID: "account", Body: strings.Repeat("a", 50)} + + _ = SaveEmailBody("INBOX", b1, 100) + _ = SaveEmailBody("Sent", b2, 100) + _ = SaveEmailBody("Trash", b3, 100) + + if got := GetCachedEmailBody("INBOX", 1, "account", 100); got != nil { + t.Error("oldest INBOX body should be evicted from LRU") + } + + if got := GetCachedEmailBody("Sent", 2, "account", 100); got == nil { + t.Error("recent Archive body should still be cached") + } + + if got := GetCachedEmailBody("Trash", 3, "account", 100); got == nil { + t.Error("new Sent body should be cached") + } +} + +func TestEmailBody_EvictsMultipleEntriesUntilUnderLimit(t *testing.T) { + setup(t) + + b1 := CachedEmailBody{UID: 1, AccountID: "account", Body: strings.Repeat("a", 50)} + b2 := CachedEmailBody{UID: 2, AccountID: "account", Body: strings.Repeat("a", 50)} + b3 := CachedEmailBody{UID: 3, AccountID: "account", Body: strings.Repeat("a", 50)} + b4 := CachedEmailBody{UID: 4, AccountID: "account", Body: strings.Repeat("a", 150)} + + _ = SaveEmailBody("INBOX", b1, 150) + _ = SaveEmailBody("INBOX", b2, 150) + _ = SaveEmailBody("INBOX", b3, 150) + _ = SaveEmailBody("INBOX", b4, 150) + + if got := GetCachedEmailBody("INBOX", 1, "account", 150); got != nil { + t.Error("UID 1 should have been evicted") + } + + if got := GetCachedEmailBody("INBOX", 2, "account", 150); got != nil { + t.Error("UID 2 should have been evicted") + } + + if got := GetCachedEmailBody("INBOX", 3, "account", 150); got != nil { + t.Error("UID 3 should have been evicted") + } + + if got := GetCachedEmailBody("INBOX", 4, "account", 150); got == nil { + t.Error("new Archive body should be cached") + } +} + +func TestLRU_ConcurrentReadWrite(t *testing.T) { + setup(t) + + var wg sync.WaitGroup + wg.Add(20) + + for i := range 20 { + go func(i int) { + defer wg.Done() + uid := uint32(i % 5) + b := CachedEmailBody{ + UID: uid, + AccountID: "account", + Body: "Hello World", + } + + _ = SaveEmailBody("INBOX", b, 1000000) + _ = GetCachedEmailBody("INBOX", uid, "account", 1000000) + }(i) } + wg.Wait() } diff --git a/config/lru.go b/config/lru.go index 3d4c20d..cfc95f4 100644 --- a/config/lru.go +++ b/config/lru.go @@ -213,7 +213,7 @@ func saveEmailBodyToDisk(folder string, body *CachedEmailBody) error { return saveEmailBodyCache(cache) } -func (lru *LRU) Get(folder string, uid uint32, accountID string) *Node { +func (lru *LRU) Get(folder string, uid uint32, accountID string) *CachedEmailBody { lru.mu.Lock() defer lru.mu.Unlock() @@ -232,7 +232,9 @@ func (lru *LRU) Get(folder string, uid uint32, accountID string) *Node { _ = saveEmailBodyToDisk(folder, node.Body) - return node + bodyCopy := *node.Body + + return &bodyCopy } func (lru *LRU) removeKey(key string) {