1package config
2
3import (
4 "container/list"
5 "encoding/json"
6 "fmt"
7 "log"
8 "os"
9 "path/filepath"
10 "sort"
11 "sync"
12 "time"
13)
14
15type Node struct {
16 Key string // key = folder:uid:accountID
17 Folder string
18 Body *CachedEmailBody
19}
20
21type LRU struct {
22 threshold int
23 currentSize int
24 cache map[string]*list.Element
25 ll *list.List
26 mu sync.Mutex
27}
28
29var lru *LRU
30var once sync.Once
31
32func GetLRUInstance(threshold int) *LRU {
33 once.Do(
34 func() {
35 lru = &LRU{
36 threshold: threshold,
37 cache: make(map[string]*list.Element),
38 ll: list.New(),
39 }
40
41 if err := lru.LoadFromDisk(); err != nil {
42 log.Printf("Failed to load LRU from disk: %v\n", err)
43 }
44 })
45
46 lru.mu.Lock()
47 defer lru.mu.Unlock()
48
49 if lru.threshold != threshold {
50 lru.threshold = threshold
51 if lru.currentSize > lru.threshold {
52 lru.evict()
53 }
54 }
55
56 return lru
57}
58
59func (lru *LRU) makeKey(folder string, uid uint32, accountID string) string {
60 return fmt.Sprintf("%s:%d:%s", folder, uid, accountID)
61}
62
63func removeBodyFromDisk(folder string, uid uint32, accountID string) error {
64 cache, err := LoadEmailBodyCache(folder)
65
66 if err != nil {
67 return err
68 }
69
70 kept := cache.Bodies[:0]
71 for _, b := range cache.Bodies {
72 if b.UID != uid || b.AccountID != accountID {
73 kept = append(kept, b)
74 }
75 }
76
77 if len(kept) == len(cache.Bodies) {
78 return nil
79 }
80
81 cache.Bodies = kept
82 return saveEmailBodyCache(cache)
83}
84
85func (lru *LRU) evict() {
86 for lru.currentSize > lru.threshold {
87 back := lru.ll.Back()
88
89 if back == nil {
90 break
91 }
92
93 node := back.Value.(*Node)
94
95 lru.ll.Remove(back)
96 delete(lru.cache, node.Key)
97 lru.currentSize -= node.Body.SizeBytes
98
99 _ = removeBodyFromDisk(node.Folder, node.Body.UID, node.Body.AccountID)
100 }
101}
102
103func (lru *LRU) LoadFromDisk() error {
104 dir, err := bodyCacheDir()
105
106 if err != nil {
107 return err
108 }
109
110 entries, err := os.ReadDir(dir)
111 if err != nil {
112 if os.IsNotExist(err) {
113 return nil
114 }
115 return err
116 }
117
118 var caches []EmailBodyCache
119
120 for _, entry := range entries {
121 if entry.IsDir() || filepath.Ext(entry.Name()) != ".json" {
122 continue
123 }
124
125 path := filepath.Join(dir, entry.Name())
126 data, err := SecureReadFile(path)
127 if err != nil {
128 continue
129 }
130
131 var cache EmailBodyCache
132 if err := json.Unmarshal(data, &cache); err != nil {
133 continue
134 }
135
136 for i := range cache.Bodies {
137 if cache.Bodies[i].SizeBytes <= 0 {
138 cache.Bodies[i].SizeBytes = calculateEmailBodySize(&cache.Bodies[i])
139 }
140 }
141
142 caches = append(caches, cache)
143 }
144
145 type bodyWithFolder struct {
146 folder string
147 body CachedEmailBody
148 }
149
150 var allBodies []bodyWithFolder
151
152 for _, cache := range caches {
153 for _, body := range cache.Bodies {
154 allBodies = append(allBodies, bodyWithFolder{
155 folder: cache.FolderName,
156 body: body,
157 })
158 }
159 }
160
161 sort.Slice(allBodies, func(i, j int) bool {
162 ti := allBodies[i].body.LastAccessedAt
163 tj := allBodies[j].body.LastAccessedAt
164 return ti.After(tj)
165 })
166
167 for i := len(allBodies) - 1; i >= 0; i-- {
168 item := allBodies[i]
169
170 if item.body.SizeBytes > lru.threshold {
171 continue
172 }
173
174 key := lru.makeKey(item.folder, item.body.UID, item.body.AccountID)
175
176 bodyCopy := item.body
177 node := &Node{
178 Key: key,
179 Folder: item.folder,
180 Body: &bodyCopy,
181 }
182
183 e := lru.ll.PushFront(node)
184 lru.cache[key] = e
185 lru.currentSize += item.body.SizeBytes
186 }
187
188 if lru.currentSize > lru.threshold {
189 lru.evict()
190 }
191 return nil
192}
193
194func saveEmailBodyToDisk(folder string, body *CachedEmailBody) error {
195 cache, err := LoadEmailBodyCache(folder)
196
197 if err != nil {
198 cache = &EmailBodyCache{FolderName: folder}
199 }
200
201 found := false
202 for i, b := range cache.Bodies {
203 if b.UID == body.UID && b.AccountID == body.AccountID {
204 cache.Bodies[i] = *body
205 found = true
206 break
207 }
208 }
209 if !found {
210 cache.Bodies = append(cache.Bodies, *body)
211 }
212
213 return saveEmailBodyCache(cache)
214}
215
216func (lru *LRU) Get(folder string, uid uint32, accountID string) *CachedEmailBody {
217 lru.mu.Lock()
218 defer lru.mu.Unlock()
219
220 key := lru.makeKey(folder, uid, accountID)
221
222 e, ok := lru.cache[key]
223
224 if !ok {
225 return nil
226 }
227
228 lru.ll.MoveToFront(e)
229
230 node := e.Value.(*Node)
231 node.Body.LastAccessedAt = time.Now()
232
233 _ = saveEmailBodyToDisk(folder, node.Body)
234
235 bodyCopy := *node.Body
236
237 return &bodyCopy
238}
239
240func (lru *LRU) removeKey(key string) {
241 if e, ok := lru.cache[key]; ok {
242 node := e.Value.(*Node)
243
244 lru.currentSize -= node.Body.SizeBytes
245 lru.ll.Remove(e)
246 delete(lru.cache, key)
247 }
248}
249
250func (lru *LRU) Put(folder string, uid uint32, accountID string, body *CachedEmailBody) {
251 lru.mu.Lock()
252 defer lru.mu.Unlock()
253
254 key := lru.makeKey(folder, uid, accountID)
255
256 if body.SizeBytes > lru.threshold {
257 lru.removeKey(key)
258 _ = removeBodyFromDisk(folder, uid, accountID)
259 return
260 }
261
262 body.LastAccessedAt = time.Now()
263
264 if e, ok := lru.cache[key]; ok {
265 node := e.Value.(*Node)
266 lru.currentSize -= node.Body.SizeBytes
267 lru.currentSize += body.SizeBytes
268 node.Body = body
269 lru.ll.MoveToFront(e)
270 } else {
271 node := &Node{
272 Key: key,
273 Folder: folder,
274 Body: body,
275 }
276 e := lru.ll.PushFront(node)
277 lru.cache[key] = e
278 lru.currentSize += body.SizeBytes
279 }
280
281 lru.evict()
282
283 _ = saveEmailBodyToDisk(folder, body)
284}
285
286func (lru *LRU) Delete(folder string, uid uint32, accountID string) {
287 lru.mu.Lock()
288 defer lru.mu.Unlock()
289
290 key := lru.makeKey(folder, uid, accountID)
291 lru.removeKey(key)
292 _ = removeBodyFromDisk(folder, uid, accountID)
293}
294
295func resetLRU() {
296 once = sync.Once{}
297 lru = nil
298}