1package config
2
3import (
4 "encoding/json"
5 "os"
6 "path/filepath"
7 "sort"
8 "strings"
9 "time"
10)
11
12// CachedEmail stores essential email data for caching.
13type CachedEmail struct {
14 UID uint32 `json:"uid"`
15 From string `json:"from"`
16 To []string `json:"to"`
17 Subject string `json:"subject"`
18 Date time.Time `json:"date"`
19 MessageID string `json:"message_id"`
20 AccountID string `json:"account_id"`
21 IsRead bool `json:"is_read"`
22}
23
24// EmailCache stores cached emails for all accounts.
25type EmailCache struct {
26 Emails []CachedEmail `json:"emails"`
27 UpdatedAt time.Time `json:"updated_at"`
28}
29
30// cacheFile returns the full path to the email cache file.
31func cacheFile() (string, error) {
32 dir, err := cacheDir()
33 if err != nil {
34 return "", err
35 }
36 return filepath.Join(dir, "email_cache.json"), nil
37}
38
39// SaveEmailCache saves emails to the cache file.
40func SaveEmailCache(cache *EmailCache) error {
41 path, err := cacheFile()
42 if err != nil {
43 return err
44 }
45 if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil {
46 return err
47 }
48 cache.UpdatedAt = time.Now()
49 data, err := json.MarshalIndent(cache, "", " ")
50 if err != nil {
51 return err
52 }
53 return SecureWriteFile(path, data, 0600)
54}
55
56// LoadEmailCache loads emails from the cache file.
57func LoadEmailCache() (*EmailCache, error) {
58 path, err := cacheFile()
59 if err != nil {
60 return nil, err
61 }
62 data, err := SecureReadFile(path)
63 if err != nil {
64 return nil, err
65 }
66 var cache EmailCache
67 if err := json.Unmarshal(data, &cache); err != nil {
68 return nil, err
69 }
70 return &cache, nil
71}
72
73// HasEmailCache checks if a cache file exists.
74func HasEmailCache() bool {
75 path, err := cacheFile()
76 if err != nil {
77 return false
78 }
79 _, err = os.Stat(path)
80 return err == nil
81}
82
83// ClearEmailCache removes the cache file.
84func ClearEmailCache() error {
85 path, err := cacheFile()
86 if err != nil {
87 return err
88 }
89 return os.Remove(path)
90}
91
92// --- Contacts Cache ---
93
94// Contact stores a contact's name and email address.
95type Contact struct {
96 Name string `json:"name"`
97 Email string `json:"email"`
98 LastUsed time.Time `json:"last_used"`
99 UseCount int `json:"use_count"`
100}
101
102// ContactsCache stores all known contacts.
103type ContactsCache struct {
104 Contacts []Contact `json:"contacts"`
105 UpdatedAt time.Time `json:"updated_at"`
106}
107
108// contactsFile returns the full path to the contacts cache file.
109func contactsFile() (string, error) {
110 dir, err := cacheDir()
111 if err != nil {
112 return "", err
113 }
114 return filepath.Join(dir, "contacts.json"), nil
115}
116
117// SaveContactsCache saves contacts to the cache file.
118func SaveContactsCache(cache *ContactsCache) error {
119 path, err := contactsFile()
120 if err != nil {
121 return err
122 }
123 if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil {
124 return err
125 }
126 cache.UpdatedAt = time.Now()
127 data, err := json.MarshalIndent(cache, "", " ")
128 if err != nil {
129 return err
130 }
131 return SecureWriteFile(path, data, 0600)
132}
133
134// LoadContactsCache loads contacts from the cache file.
135func LoadContactsCache() (*ContactsCache, error) {
136 path, err := contactsFile()
137 if err != nil {
138 return nil, err
139 }
140 data, err := SecureReadFile(path)
141 if err != nil {
142 return nil, err
143 }
144 var cache ContactsCache
145 if err := json.Unmarshal(data, &cache); err != nil {
146 return nil, err
147 }
148 return &cache, nil
149}
150
151// AddContact adds or updates a contact in the cache.
152func AddContact(name, email string) error {
153 if email == "" {
154 return nil
155 }
156
157 email = strings.ToLower(strings.Trim(strings.TrimSpace(email), ","))
158 name = strings.TrimSpace(name)
159
160 cache, err := LoadContactsCache()
161 if err != nil {
162 cache = &ContactsCache{Contacts: []Contact{}}
163 }
164
165 // Check if contact exists
166 found := false
167 for i, c := range cache.Contacts {
168 if strings.EqualFold(c.Email, email) {
169 // Update existing contact
170 cache.Contacts[i].UseCount++
171 cache.Contacts[i].LastUsed = time.Now()
172 // Update name if we have a better one
173 if name != "" && (c.Name == "" || c.Name == email) {
174 cache.Contacts[i].Name = name
175 }
176 found = true
177 break
178 }
179 }
180
181 if !found {
182 cache.Contacts = append(cache.Contacts, Contact{
183 Name: name,
184 Email: email,
185 LastUsed: time.Now(),
186 UseCount: 1,
187 })
188 }
189
190 return SaveContactsCache(cache)
191}
192
193// SearchContacts searches for contacts matching the query.
194func SearchContacts(query string) []Contact {
195 cache, err := LoadContactsCache()
196 if err != nil {
197 return nil
198 }
199
200 query = strings.ToLower(strings.TrimSpace(query))
201 if query == "" {
202 return nil
203 }
204
205 var matches []Contact
206
207 // Add mailing lists to matches if they match the query
208 cfg, err := LoadConfig()
209 if err == nil {
210 for _, list := range cfg.MailingLists {
211 if strings.Contains(strings.ToLower(list.Name), query) {
212 // Convert mailing list to a virtual contact
213 matches = append(matches, Contact{
214 Name: list.Name,
215 Email: strings.Join(list.Addresses, ", "),
216 UseCount: 9999, // Ensure lists appear at the top
217 LastUsed: time.Now(),
218 })
219 }
220 }
221 }
222
223 for _, c := range cache.Contacts {
224 if strings.Contains(strings.ToLower(c.Email), query) ||
225 strings.Contains(strings.ToLower(c.Name), query) {
226 matches = append(matches, c)
227 }
228 }
229
230 // Sort by use count (most used first), then by last used
231 sort.Slice(matches, func(i, j int) bool {
232 if matches[i].UseCount != matches[j].UseCount {
233 return matches[i].UseCount > matches[j].UseCount
234 }
235 return matches[i].LastUsed.After(matches[j].LastUsed)
236 })
237
238 // Limit to 5 suggestions
239 if len(matches) > 5 {
240 matches = matches[:5]
241 }
242
243 return matches
244}
245
246// --- Drafts Cache ---
247
248// Draft stores a saved email draft.
249type Draft struct {
250 ID string `json:"id"`
251 To string `json:"to"`
252 Cc string `json:"cc,omitempty"`
253 Bcc string `json:"bcc,omitempty"`
254 Subject string `json:"subject"`
255 Body string `json:"body"`
256 AttachmentPaths []string `json:"attachment_paths,omitempty"`
257 AccountID string `json:"account_id"`
258 InReplyTo string `json:"in_reply_to,omitempty"`
259 References []string `json:"references,omitempty"`
260 QuotedText string `json:"quoted_text,omitempty"`
261 CreatedAt time.Time `json:"created_at"`
262 UpdatedAt time.Time `json:"updated_at"`
263}
264
265// DraftsCache stores all saved drafts.
266type DraftsCache struct {
267 Drafts []Draft `json:"drafts"`
268 UpdatedAt time.Time `json:"updated_at"`
269}
270
271// draftsFile returns the full path to the drafts cache file.
272func draftsFile() (string, error) {
273 dir, err := cacheDir()
274 if err != nil {
275 return "", err
276 }
277 return filepath.Join(dir, "drafts.json"), nil
278}
279
280// SaveDraftsCache saves drafts to the cache file.
281func SaveDraftsCache(cache *DraftsCache) error {
282 path, err := draftsFile()
283 if err != nil {
284 return err
285 }
286 if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil {
287 return err
288 }
289 cache.UpdatedAt = time.Now()
290 data, err := json.MarshalIndent(cache, "", " ")
291 if err != nil {
292 return err
293 }
294 return SecureWriteFile(path, data, 0600)
295}
296
297// LoadDraftsCache loads drafts from the cache file.
298func LoadDraftsCache() (*DraftsCache, error) {
299 path, err := draftsFile()
300 if err != nil {
301 return nil, err
302 }
303 data, err := SecureReadFile(path)
304 if err != nil {
305 return nil, err
306 }
307 var cache DraftsCache
308 if err := json.Unmarshal(data, &cache); err != nil {
309 return nil, err
310 }
311 return &cache, nil
312}
313
314// SaveDraft saves or updates a draft.
315func SaveDraft(draft Draft) error {
316 cache, err := LoadDraftsCache()
317 if err != nil {
318 cache = &DraftsCache{Drafts: []Draft{}}
319 }
320
321 draft.UpdatedAt = time.Now()
322
323 // Check if draft exists (update) or is new
324 found := false
325 for i, d := range cache.Drafts {
326 if d.ID == draft.ID {
327 cache.Drafts[i] = draft
328 found = true
329 break
330 }
331 }
332
333 if !found {
334 if draft.CreatedAt.IsZero() {
335 draft.CreatedAt = time.Now()
336 }
337 cache.Drafts = append(cache.Drafts, draft)
338 }
339
340 return SaveDraftsCache(cache)
341}
342
343// DeleteDraft removes a draft by ID.
344func DeleteDraft(id string) error {
345 cache, err := LoadDraftsCache()
346 if err != nil {
347 return nil // No cache, nothing to delete
348 }
349
350 var filtered []Draft
351 for _, d := range cache.Drafts {
352 if d.ID != id {
353 filtered = append(filtered, d)
354 }
355 }
356 cache.Drafts = filtered
357
358 return SaveDraftsCache(cache)
359}
360
361// GetDraft retrieves a draft by ID.
362func GetDraft(id string) *Draft {
363 cache, err := LoadDraftsCache()
364 if err != nil {
365 return nil
366 }
367
368 for _, d := range cache.Drafts {
369 if d.ID == id {
370 return &d
371 }
372 }
373 return nil
374}
375
376// GetAllDrafts retrieves all drafts sorted by update time (newest first).
377func GetAllDrafts() []Draft {
378 cache, err := LoadDraftsCache()
379 if err != nil {
380 return nil
381 }
382
383 drafts := cache.Drafts
384 sort.Slice(drafts, func(i, j int) bool {
385 return drafts[i].UpdatedAt.After(drafts[j].UpdatedAt)
386 })
387
388 return drafts
389}
390
391// HasDrafts checks if there are any saved drafts.
392func HasDrafts() bool {
393 cache, err := LoadDraftsCache()
394 if err != nil {
395 return false
396 }
397 return len(cache.Drafts) > 0
398}
399
400// --- Email Body Cache ---
401
402// CachedAttachment stores attachment metadata (not the binary data).
403type CachedAttachment struct {
404 Filename string `json:"filename"`
405 PartID string `json:"part_id"`
406 Encoding string `json:"encoding,omitempty"`
407 MIMEType string `json:"mime_type,omitempty"`
408 ContentID string `json:"content_id,omitempty"`
409 Inline bool `json:"inline,omitempty"`
410 IsSMIMESignature bool `json:"is_smime_signature,omitempty"`
411 SMIMEVerified bool `json:"smime_verified,omitempty"`
412 IsSMIMEEncrypted bool `json:"is_smime_encrypted,omitempty"`
413}
414
415// CachedEmailBody stores the body and attachment metadata for a single email.
416type CachedEmailBody struct {
417 UID uint32 `json:"uid"`
418 AccountID string `json:"account_id"`
419 Body string `json:"body"`
420 Attachments []CachedAttachment `json:"attachments,omitempty"`
421 CachedAt time.Time `json:"cached_at"`
422}
423
424// EmailBodyCache stores cached email bodies for a folder.
425type EmailBodyCache struct {
426 FolderName string `json:"folder_name"`
427 Bodies []CachedEmailBody `json:"bodies"`
428 UpdatedAt time.Time `json:"updated_at"`
429}
430
431// bodyCacheDir returns the directory for body cache files.
432func bodyCacheDir() (string, error) {
433 dir, err := cacheDir()
434 if err != nil {
435 return "", err
436 }
437 return filepath.Join(dir, "email_bodies"), nil
438}
439
440// bodyBacheFile returns the file path for a folder's body cache.
441func bodyCacheFile(folderName string) (string, error) {
442 dir, err := bodyCacheDir()
443 if err != nil {
444 return "", err
445 }
446 safe := strings.NewReplacer("/", "_", "\\", "_", ":", "_", " ", "_").Replace(folderName)
447 return filepath.Join(dir, safe+".json"), nil
448}
449
450// LoadEmailBodyCache loads the body cache for a folder.
451func LoadEmailBodyCache(folderName string) (*EmailBodyCache, error) {
452 path, err := bodyCacheFile(folderName)
453 if err != nil {
454 return nil, err
455 }
456 data, err := SecureReadFile(path)
457 if err != nil {
458 return nil, err
459 }
460 var cache EmailBodyCache
461 if err := json.Unmarshal(data, &cache); err != nil {
462 return nil, err
463 }
464 return &cache, nil
465}
466
467// saveEmailBodyCache writes the body cache for a folder.
468func saveEmailBodyCache(cache *EmailBodyCache) error {
469 path, err := bodyCacheFile(cache.FolderName)
470 if err != nil {
471 return err
472 }
473 if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil {
474 return err
475 }
476 cache.UpdatedAt = time.Now()
477 data, err := json.Marshal(cache)
478 if err != nil {
479 return err
480 }
481 return SecureWriteFile(path, data, 0600)
482}
483
484// GetCachedEmailBody returns the cached body for a specific email, or nil if not cached.
485func GetCachedEmailBody(folderName string, uid uint32, accountID string) *CachedEmailBody {
486 cache, err := LoadEmailBodyCache(folderName)
487 if err != nil {
488 return nil
489 }
490 for _, b := range cache.Bodies {
491 if b.UID == uid && b.AccountID == accountID {
492 return &b
493 }
494 }
495 return nil
496}
497
498// SaveEmailBody saves or updates a cached email body for a folder.
499func SaveEmailBody(folderName string, body CachedEmailBody) error {
500 cache, err := LoadEmailBodyCache(folderName)
501 if err != nil {
502 cache = &EmailBodyCache{FolderName: folderName}
503 }
504
505 body.CachedAt = time.Now()
506
507 // Replace existing or append
508 found := false
509 for i, b := range cache.Bodies {
510 if b.UID == body.UID && b.AccountID == body.AccountID {
511 cache.Bodies[i] = body
512 found = true
513 break
514 }
515 }
516 if !found {
517 cache.Bodies = append(cache.Bodies, body)
518 }
519
520 return saveEmailBodyCache(cache)
521}
522
523// PruneEmailBodyCache removes cached bodies for emails that are no longer in the folder.
524// validUIDs is a map of UID -> AccountID for emails still present.
525func PruneEmailBodyCache(folderName string, validUIDs map[uint32]string) error {
526 cache, err := LoadEmailBodyCache(folderName)
527 if err != nil {
528 return nil // No cache to prune
529 }
530
531 var kept []CachedEmailBody
532 for _, b := range cache.Bodies {
533 if accID, ok := validUIDs[b.UID]; ok && accID == b.AccountID {
534 kept = append(kept, b)
535 }
536 }
537
538 if len(kept) == len(cache.Bodies) {
539 return nil // Nothing pruned
540 }
541
542 cache.Bodies = kept
543 return saveEmailBodyCache(cache)
544}