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