cache.go

  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}