From 342288799b9b0515a37102b36e46201a803ad935 Mon Sep 17 00:00:00 2001 From: Haroka-74 Date: Sun, 3 May 2026 23:46:44 +0300 Subject: [PATCH 1/4] feat(config): add LRU eviction to email body cache --- config/cache.go | 39 ++++++++++++++++++++++++++++++++++----- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/config/cache.go b/config/cache.go index 13a941f..cebab77 100644 --- a/config/cache.go +++ b/config/cache.go @@ -422,11 +422,13 @@ type CachedAttachment struct { // CachedEmailBody stores the body and attachment metadata for a single email. type CachedEmailBody struct { - UID uint32 `json:"uid"` - AccountID string `json:"account_id"` - Body string `json:"body"` - Attachments []CachedAttachment `json:"attachments,omitempty"` - CachedAt time.Time `json:"cached_at"` + UID uint32 `json:"uid"` + AccountID string `json:"account_id"` + Body string `json:"body"` + Attachments []CachedAttachment `json:"attachments,omitempty"` + CachedAt time.Time `json:"cached_at"` + LastAccessedAt time.Time `json:"last_accessed_at"` + SizeBytes int `json:"size_bytes"` } // EmailBodyCache stores cached email bodies for a folder. @@ -503,6 +505,24 @@ func GetCachedEmailBody(folderName string, uid uint32, accountID string) *Cached return nil } +func calculateTotalCacheSize(cache *EmailBodyCache) int { + total := 0 + for _, b := range cache.Bodies { + total += b.SizeBytes + } + return total +} + +func evict(cache *EmailBodyCache, newSize int, threshold int) { + sort.Slice(cache.Bodies, func(i, j int) bool { + return cache.Bodies[i].LastAccessedAt.Before(cache.Bodies[j].LastAccessedAt) + }) + + for len(cache.Bodies) > 0 && calculateTotalCacheSize(cache)+newSize > threshold { + cache.Bodies = cache.Bodies[1:] + } +} + // SaveEmailBody saves or updates a cached email body for a folder. func SaveEmailBody(folderName string, body CachedEmailBody) error { cache, err := LoadEmailBodyCache(folderName) @@ -510,7 +530,11 @@ func SaveEmailBody(folderName string, body CachedEmailBody) error { cache = &EmailBodyCache{FolderName: folderName} } + const threshold = 500 * 1024 * 1024 // 500MB + body.CachedAt = time.Now() + body.LastAccessedAt = time.Now() + body.SizeBytes = len(body.Body) // Replace existing or append found := false @@ -522,6 +546,11 @@ func SaveEmailBody(folderName string, body CachedEmailBody) error { } } if !found { + + if calculateTotalCacheSize(cache)+body.SizeBytes > threshold { + evict(cache, body.SizeBytes, threshold) + } + cache.Bodies = append(cache.Bodies, body) } From 263f7ae7c012e7f12d4263e32fe1572f67a13a9a Mon Sep 17 00:00:00 2001 From: Haroka-74 Date: Mon, 4 May 2026 21:54:27 +0300 Subject: [PATCH 2/4] feat(config): make email body cache threshold configurable --- config/cache.go | 4 +--- config/config.go | 15 +++++++++++++++ docs/docs/Configuration.md | 5 ++++- main.go | 4 ++-- 4 files changed, 22 insertions(+), 6 deletions(-) diff --git a/config/cache.go b/config/cache.go index cebab77..55903ff 100644 --- a/config/cache.go +++ b/config/cache.go @@ -524,14 +524,12 @@ func evict(cache *EmailBodyCache, newSize int, threshold int) { } // SaveEmailBody saves or updates a cached email body for a folder. -func SaveEmailBody(folderName string, body CachedEmailBody) error { +func SaveEmailBody(folderName string, body CachedEmailBody, threshold int) error { cache, err := LoadEmailBodyCache(folderName) if err != nil { cache = &EmailBodyCache{FolderName: folderName} } - const threshold = 500 * 1024 * 1024 // 500MB - body.CachedAt = time.Now() body.LastAccessedAt = time.Now() body.SizeBytes = len(body.Body) diff --git a/config/config.go b/config/config.go index 0e6bb58..5a05ab1 100644 --- a/config/config.go +++ b/config/config.go @@ -95,6 +95,18 @@ type Config struct { MailingLists []MailingList `json:"mailing_lists,omitempty"` DateFormat string `json:"date_format,omitempty"` Language string `json:"language,omitempty"` // Language code (e.g., "en", "es", "de") + BodyCacheThresholdMB int `json:"body_cache_threshold_mb,omitempty"` +} + +// GetBodyCacheThreshold returns the email body cache threshold in bytes. +// It defaults to 500MB if unset or zero. +func (c *Config) GetBodyCacheThreshold() int { + if c.BodyCacheThresholdMB <= 0 { + // return 500 * 1024 * 1024 + return 100 * 1024 + } + // return c.BodyCacheThresholdMB * 1024 * 1024 + return c.BodyCacheThresholdMB * 1024 } // GetDateFormat returns the Go time reference layout translated from the @@ -537,6 +549,7 @@ func LoadConfig() (*Config, error) { MailingLists []MailingList `json:"mailing_lists,omitempty"` DateFormat string `json:"date_format,omitempty"` Language string `json:"language,omitempty"` + BodyCacheThresholdMB int `json:"body_cache_threshold_mb,omitempty"` } var raw diskConfig @@ -572,6 +585,8 @@ func LoadConfig() (*Config, error) { config.MailingLists = raw.MailingLists config.DateFormat = raw.DateFormat config.Language = raw.Language + config.BodyCacheThresholdMB = raw.BodyCacheThresholdMB + for _, rawAcc := range raw.Accounts { acc := Account{ ID: rawAcc.ID, diff --git a/docs/docs/Configuration.md b/docs/docs/Configuration.md index d350c34..497512b 100644 --- a/docs/docs/Configuration.md +++ b/docs/docs/Configuration.md @@ -45,7 +45,8 @@ Configuration is stored in `~/.config/matcha/config.json`. "theme": "Matcha", "enable_split_pane": true, "disable_images": true, - "hide_tips": true + "hide_tips": true, + "body_cache_threshold_mb": 500 } ``` @@ -53,6 +54,8 @@ Configuration is stored in `~/.config/matcha/config.json`. `enable_split_pane` enables a side-by-side view where the email list and the selected email are shown on the same screen. +`body_cache_threshold_mb` sets the maximum size (in megabytes) for the local email body cache. When this limit is reached, older cached emails are evicted to make room for new ones. Defaults to `500` MB if not specified. + ## Data Locations Configuration and persistent data are stored in `~/.config/matcha/`: diff --git a/main.go b/main.go index 87a83b0..682bc16 100644 --- a/main.go +++ b/main.go @@ -762,7 +762,7 @@ func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { AccountID: msg.AccountID, Body: msg.Body, Attachments: cachedAttachments, - }) + }, m.config.GetBodyCacheThreshold()) if err != nil { log.Printf("debug: error caching email body fails (disk full, permission denied) for UID: %d: %v", msg.UID, err) } @@ -1313,7 +1313,7 @@ func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { AccountID: msg.AccountID, Body: msg.Body, Attachments: cachedAttachments, - }) + }, m.config.GetBodyCacheThreshold()) if err != nil { log.Printf("debug: error caching email body fails (disk full, permission denied) for UID: %d: %v", msg.UID, err) From 72f210b1e2e888fc6dcacf85084c9fa52f9eb6d4 Mon Sep 17 00:00:00 2001 From: Haroka-74 Date: Mon, 4 May 2026 21:58:25 +0300 Subject: [PATCH 3/4] fix: restore correct cache threshold units --- config/config.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/config/config.go b/config/config.go index 5a05ab1..edcf41d 100644 --- a/config/config.go +++ b/config/config.go @@ -102,11 +102,9 @@ type Config struct { // It defaults to 500MB if unset or zero. func (c *Config) GetBodyCacheThreshold() int { if c.BodyCacheThresholdMB <= 0 { - // return 500 * 1024 * 1024 - return 100 * 1024 + return 500 * 1024 * 1024 } - // return c.BodyCacheThresholdMB * 1024 * 1024 - return c.BodyCacheThresholdMB * 1024 + return c.BodyCacheThresholdMB * 1024 * 1024 } // GetDateFormat returns the Go time reference layout translated from the From afc114c5e1dee23e265187a4a5eb101c2a5cb53b Mon Sep 17 00:00:00 2001 From: Haroka-74 Date: Tue, 5 May 2026 00:04:15 +0300 Subject: [PATCH 4/4] fix(config): fix LRU eviction correctness --- config/cache.go | 30 +++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/config/cache.go b/config/cache.go index 55903ff..72ee7fe 100644 --- a/config/cache.go +++ b/config/cache.go @@ -497,14 +497,29 @@ func GetCachedEmailBody(folderName string, uid uint32, accountID string) *Cached if err != nil { return nil } - for _, b := range cache.Bodies { + for i, b := range cache.Bodies { if b.UID == uid && b.AccountID == accountID { - return &b + cache.Bodies[i].LastAccessedAt = time.Now() + _ = saveEmailBodyCache(cache) + return &cache.Bodies[i] } } return nil } +func calculateEmailBodySize(body *CachedEmailBody) int { + size := len(body.Body) + for _, att := range body.Attachments { + size += len(att.Filename) + size += len(att.PartID) + size += len(att.Encoding) + size += len(att.MIMEType) + size += len(att.ContentID) + size += len(att.CalendarData) + } + return size +} + func calculateTotalCacheSize(cache *EmailBodyCache) int { total := 0 for _, b := range cache.Bodies { @@ -532,7 +547,7 @@ func SaveEmailBody(folderName string, body CachedEmailBody, threshold int) error body.CachedAt = time.Now() body.LastAccessedAt = time.Now() - body.SizeBytes = len(body.Body) + body.SizeBytes = calculateEmailBodySize(&body) // Replace existing or append found := false @@ -544,12 +559,13 @@ func SaveEmailBody(folderName string, body CachedEmailBody, threshold int) error } } if !found { + if body.SizeBytes <= threshold { + if calculateTotalCacheSize(cache)+body.SizeBytes > threshold { + evict(cache, body.SizeBytes, threshold) + } - if calculateTotalCacheSize(cache)+body.SizeBytes > threshold { - evict(cache, body.SizeBytes, threshold) + cache.Bodies = append(cache.Bodies, body) } - - cache.Bodies = append(cache.Bodies, body) } return saveEmailBodyCache(cache)