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}
 22
 23// EmailCache stores cached emails for all accounts.
 24type EmailCache struct {
 25	Emails    []CachedEmail `json:"emails"`
 26	UpdatedAt time.Time     `json:"updated_at"`
 27}
 28
 29// cacheFile returns the full path to the email cache file.
 30func cacheFile() (string, error) {
 31	dir, err := configDir()
 32	if err != nil {
 33		return "", err
 34	}
 35	return filepath.Join(dir, "email_cache.json"), nil
 36}
 37
 38// SaveEmailCache saves emails to the cache file.
 39func SaveEmailCache(cache *EmailCache) error {
 40	path, err := cacheFile()
 41	if err != nil {
 42		return err
 43	}
 44	if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil {
 45		return err
 46	}
 47	cache.UpdatedAt = time.Now()
 48	data, err := json.MarshalIndent(cache, "", "  ")
 49	if err != nil {
 50		return err
 51	}
 52	return os.WriteFile(path, data, 0600)
 53}
 54
 55// LoadEmailCache loads emails from the cache file.
 56func LoadEmailCache() (*EmailCache, error) {
 57	path, err := cacheFile()
 58	if err != nil {
 59		return nil, err
 60	}
 61	data, err := os.ReadFile(path)
 62	if err != nil {
 63		return nil, err
 64	}
 65	var cache EmailCache
 66	if err := json.Unmarshal(data, &cache); err != nil {
 67		return nil, err
 68	}
 69	return &cache, nil
 70}
 71
 72// HasEmailCache checks if a cache file exists.
 73func HasEmailCache() bool {
 74	path, err := cacheFile()
 75	if err != nil {
 76		return false
 77	}
 78	_, err = os.Stat(path)
 79	return err == nil
 80}
 81
 82// ClearEmailCache removes the cache file.
 83func ClearEmailCache() error {
 84	path, err := cacheFile()
 85	if err != nil {
 86		return err
 87	}
 88	return os.Remove(path)
 89}
 90
 91// --- Contacts Cache ---
 92
 93// Contact stores a contact's name and email address.
 94type Contact struct {
 95	Name     string    `json:"name"`
 96	Email    string    `json:"email"`
 97	LastUsed time.Time `json:"last_used"`
 98	UseCount int       `json:"use_count"`
 99}
100
101// ContactsCache stores all known contacts.
102type ContactsCache struct {
103	Contacts  []Contact `json:"contacts"`
104	UpdatedAt time.Time `json:"updated_at"`
105}
106
107// contactsFile returns the full path to the contacts cache file.
108func contactsFile() (string, error) {
109	dir, err := configDir()
110	if err != nil {
111		return "", err
112	}
113	return filepath.Join(dir, "contacts.json"), nil
114}
115
116// SaveContactsCache saves contacts to the cache file.
117func SaveContactsCache(cache *ContactsCache) error {
118	path, err := contactsFile()
119	if err != nil {
120		return err
121	}
122	if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil {
123		return err
124	}
125	cache.UpdatedAt = time.Now()
126	data, err := json.MarshalIndent(cache, "", "  ")
127	if err != nil {
128		return err
129	}
130	return os.WriteFile(path, data, 0600)
131}
132
133// LoadContactsCache loads contacts from the cache file.
134func LoadContactsCache() (*ContactsCache, error) {
135	path, err := contactsFile()
136	if err != nil {
137		return nil, err
138	}
139	data, err := os.ReadFile(path)
140	if err != nil {
141		return nil, err
142	}
143	var cache ContactsCache
144	if err := json.Unmarshal(data, &cache); err != nil {
145		return nil, err
146	}
147	return &cache, nil
148}
149
150// AddContact adds or updates a contact in the cache.
151func AddContact(name, email string) error {
152	if email == "" {
153		return nil
154	}
155
156	email = strings.ToLower(strings.TrimSpace(email))
157	name = strings.TrimSpace(name)
158
159	cache, err := LoadContactsCache()
160	if err != nil {
161		cache = &ContactsCache{Contacts: []Contact{}}
162	}
163
164	// Check if contact exists
165	found := false
166	for i, c := range cache.Contacts {
167		if strings.EqualFold(c.Email, email) {
168			// Update existing contact
169			cache.Contacts[i].UseCount++
170			cache.Contacts[i].LastUsed = time.Now()
171			// Update name if we have a better one
172			if name != "" && (c.Name == "" || c.Name == email) {
173				cache.Contacts[i].Name = name
174			}
175			found = true
176			break
177		}
178	}
179
180	if !found {
181		cache.Contacts = append(cache.Contacts, Contact{
182			Name:     name,
183			Email:    email,
184			LastUsed: time.Now(),
185			UseCount: 1,
186		})
187	}
188
189	return SaveContactsCache(cache)
190}
191
192// SearchContacts searches for contacts matching the query.
193func SearchContacts(query string) []Contact {
194	cache, err := LoadContactsCache()
195	if err != nil {
196		return nil
197	}
198
199	query = strings.ToLower(strings.TrimSpace(query))
200	if query == "" {
201		return nil
202	}
203
204	var matches []Contact
205	for _, c := range cache.Contacts {
206		if strings.Contains(strings.ToLower(c.Email), query) ||
207			strings.Contains(strings.ToLower(c.Name), query) {
208			matches = append(matches, c)
209		}
210	}
211
212	// Sort by use count (most used first), then by last used
213	sort.Slice(matches, func(i, j int) bool {
214		if matches[i].UseCount != matches[j].UseCount {
215			return matches[i].UseCount > matches[j].UseCount
216		}
217		return matches[i].LastUsed.After(matches[j].LastUsed)
218	})
219
220	// Limit to 5 suggestions
221	if len(matches) > 5 {
222		matches = matches[:5]
223	}
224
225	return matches
226}
227
228// --- Drafts Cache ---
229
230// Draft stores a saved email draft.
231type Draft struct {
232	ID             string    `json:"id"`
233	To             string    `json:"to"`
234	Subject        string    `json:"subject"`
235	Body           string    `json:"body"`
236	AttachmentPath string    `json:"attachment_path,omitempty"`
237	AccountID      string    `json:"account_id"`
238	InReplyTo      string    `json:"in_reply_to,omitempty"`
239	References     []string  `json:"references,omitempty"`
240	CreatedAt      time.Time `json:"created_at"`
241	UpdatedAt      time.Time `json:"updated_at"`
242}
243
244// DraftsCache stores all saved drafts.
245type DraftsCache struct {
246	Drafts    []Draft   `json:"drafts"`
247	UpdatedAt time.Time `json:"updated_at"`
248}
249
250// draftsFile returns the full path to the drafts cache file.
251func draftsFile() (string, error) {
252	dir, err := configDir()
253	if err != nil {
254		return "", err
255	}
256	return filepath.Join(dir, "drafts.json"), nil
257}
258
259// SaveDraftsCache saves drafts to the cache file.
260func SaveDraftsCache(cache *DraftsCache) error {
261	path, err := draftsFile()
262	if err != nil {
263		return err
264	}
265	if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil {
266		return err
267	}
268	cache.UpdatedAt = time.Now()
269	data, err := json.MarshalIndent(cache, "", "  ")
270	if err != nil {
271		return err
272	}
273	return os.WriteFile(path, data, 0600)
274}
275
276// LoadDraftsCache loads drafts from the cache file.
277func LoadDraftsCache() (*DraftsCache, error) {
278	path, err := draftsFile()
279	if err != nil {
280		return nil, err
281	}
282	data, err := os.ReadFile(path)
283	if err != nil {
284		return nil, err
285	}
286	var cache DraftsCache
287	if err := json.Unmarshal(data, &cache); err != nil {
288		return nil, err
289	}
290	return &cache, nil
291}
292
293// SaveDraft saves or updates a draft.
294func SaveDraft(draft Draft) error {
295	cache, err := LoadDraftsCache()
296	if err != nil {
297		cache = &DraftsCache{Drafts: []Draft{}}
298	}
299
300	draft.UpdatedAt = time.Now()
301
302	// Check if draft exists (update) or is new
303	found := false
304	for i, d := range cache.Drafts {
305		if d.ID == draft.ID {
306			cache.Drafts[i] = draft
307			found = true
308			break
309		}
310	}
311
312	if !found {
313		if draft.CreatedAt.IsZero() {
314			draft.CreatedAt = time.Now()
315		}
316		cache.Drafts = append(cache.Drafts, draft)
317	}
318
319	return SaveDraftsCache(cache)
320}
321
322// DeleteDraft removes a draft by ID.
323func DeleteDraft(id string) error {
324	cache, err := LoadDraftsCache()
325	if err != nil {
326		return nil // No cache, nothing to delete
327	}
328
329	var filtered []Draft
330	for _, d := range cache.Drafts {
331		if d.ID != id {
332			filtered = append(filtered, d)
333		}
334	}
335	cache.Drafts = filtered
336
337	return SaveDraftsCache(cache)
338}
339
340// GetDraft retrieves a draft by ID.
341func GetDraft(id string) *Draft {
342	cache, err := LoadDraftsCache()
343	if err != nil {
344		return nil
345	}
346
347	for _, d := range cache.Drafts {
348		if d.ID == id {
349			return &d
350		}
351	}
352	return nil
353}
354
355// GetAllDrafts retrieves all drafts sorted by update time (newest first).
356func GetAllDrafts() []Draft {
357	cache, err := LoadDraftsCache()
358	if err != nil {
359		return nil
360	}
361
362	drafts := cache.Drafts
363	sort.Slice(drafts, func(i, j int) bool {
364		return drafts[i].UpdatedAt.After(drafts[j].UpdatedAt)
365	})
366
367	return drafts
368}
369
370// HasDrafts checks if there are any saved drafts.
371func HasDrafts() bool {
372	cache, err := LoadDraftsCache()
373	if err != nil {
374		return false
375	}
376	return len(cache.Drafts) > 0
377}