Detailed changes
@@ -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.
@@ -495,22 +497,57 @@ 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 {
+ 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 {
+func SaveEmailBody(folderName string, body CachedEmailBody, threshold int) error {
cache, err := LoadEmailBodyCache(folderName)
if err != nil {
cache = &EmailBodyCache{FolderName: folderName}
}
body.CachedAt = time.Now()
+ body.LastAccessedAt = time.Now()
+ body.SizeBytes = calculateEmailBodySize(&body)
// Replace existing or append
found := false
@@ -522,7 +559,13 @@ func SaveEmailBody(folderName string, body CachedEmailBody) error {
}
}
if !found {
- cache.Bodies = append(cache.Bodies, body)
+ if body.SizeBytes <= threshold {
+ if calculateTotalCacheSize(cache)+body.SizeBytes > threshold {
+ evict(cache, body.SizeBytes, threshold)
+ }
+
+ cache.Bodies = append(cache.Bodies, body)
+ }
}
return saveEmailBodyCache(cache)
@@ -95,6 +95,16 @@ 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 c.BodyCacheThresholdMB * 1024 * 1024
}
// GetDateFormat returns the Go time reference layout translated from the
@@ -537,6 +547,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 +583,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,
@@ -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/`:
@@ -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)