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
206	// Add mailing lists to matches if they match the query
207	cfg, err := LoadConfig()
208	if err == nil {
209		for _, list := range cfg.MailingLists {
210			if strings.Contains(strings.ToLower(list.Name), query) {
211				// Convert mailing list to a virtual contact
212				matches = append(matches, Contact{
213					Name:     list.Name,
214					Email:    strings.Join(list.Addresses, ", "),
215					UseCount: 9999, // Ensure lists appear at the top
216					LastUsed: time.Now(),
217				})
218			}
219		}
220	}
221
222	for _, c := range cache.Contacts {
223		if strings.Contains(strings.ToLower(c.Email), query) ||
224			strings.Contains(strings.ToLower(c.Name), query) {
225			matches = append(matches, c)
226		}
227	}
228
229	// Sort by use count (most used first), then by last used
230	sort.Slice(matches, func(i, j int) bool {
231		if matches[i].UseCount != matches[j].UseCount {
232			return matches[i].UseCount > matches[j].UseCount
233		}
234		return matches[i].LastUsed.After(matches[j].LastUsed)
235	})
236
237	// Limit to 5 suggestions
238	if len(matches) > 5 {
239		matches = matches[:5]
240	}
241
242	return matches
243}
244
245// --- Drafts Cache ---
246
247// Draft stores a saved email draft.
248type Draft struct {
249	ID             string    `json:"id"`
250	To             string    `json:"to"`
251	Cc             string    `json:"cc,omitempty"`
252	Bcc            string    `json:"bcc,omitempty"`
253	Subject        string    `json:"subject"`
254	Body           string    `json:"body"`
255	AttachmentPath string    `json:"attachment_path,omitempty"`
256	AccountID      string    `json:"account_id"`
257	InReplyTo      string    `json:"in_reply_to,omitempty"`
258	References     []string  `json:"references,omitempty"`
259	QuotedText     string    `json:"quoted_text,omitempty"`
260	CreatedAt      time.Time `json:"created_at"`
261	UpdatedAt      time.Time `json:"updated_at"`
262}
263
264// DraftsCache stores all saved drafts.
265type DraftsCache struct {
266	Drafts    []Draft   `json:"drafts"`
267	UpdatedAt time.Time `json:"updated_at"`
268}
269
270// draftsFile returns the full path to the drafts cache file.
271func draftsFile() (string, error) {
272	dir, err := configDir()
273	if err != nil {
274		return "", err
275	}
276	return filepath.Join(dir, "drafts.json"), nil
277}
278
279// SaveDraftsCache saves drafts to the cache file.
280func SaveDraftsCache(cache *DraftsCache) error {
281	path, err := draftsFile()
282	if err != nil {
283		return err
284	}
285	if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil {
286		return err
287	}
288	cache.UpdatedAt = time.Now()
289	data, err := json.MarshalIndent(cache, "", "  ")
290	if err != nil {
291		return err
292	}
293	return os.WriteFile(path, data, 0600)
294}
295
296// LoadDraftsCache loads drafts from the cache file.
297func LoadDraftsCache() (*DraftsCache, error) {
298	path, err := draftsFile()
299	if err != nil {
300		return nil, err
301	}
302	data, err := os.ReadFile(path)
303	if err != nil {
304		return nil, err
305	}
306	var cache DraftsCache
307	if err := json.Unmarshal(data, &cache); err != nil {
308		return nil, err
309	}
310	return &cache, nil
311}
312
313// SaveDraft saves or updates a draft.
314func SaveDraft(draft Draft) error {
315	cache, err := LoadDraftsCache()
316	if err != nil {
317		cache = &DraftsCache{Drafts: []Draft{}}
318	}
319
320	draft.UpdatedAt = time.Now()
321
322	// Check if draft exists (update) or is new
323	found := false
324	for i, d := range cache.Drafts {
325		if d.ID == draft.ID {
326			cache.Drafts[i] = draft
327			found = true
328			break
329		}
330	}
331
332	if !found {
333		if draft.CreatedAt.IsZero() {
334			draft.CreatedAt = time.Now()
335		}
336		cache.Drafts = append(cache.Drafts, draft)
337	}
338
339	return SaveDraftsCache(cache)
340}
341
342// DeleteDraft removes a draft by ID.
343func DeleteDraft(id string) error {
344	cache, err := LoadDraftsCache()
345	if err != nil {
346		return nil // No cache, nothing to delete
347	}
348
349	var filtered []Draft
350	for _, d := range cache.Drafts {
351		if d.ID != id {
352			filtered = append(filtered, d)
353		}
354	}
355	cache.Drafts = filtered
356
357	return SaveDraftsCache(cache)
358}
359
360// GetDraft retrieves a draft by ID.
361func GetDraft(id string) *Draft {
362	cache, err := LoadDraftsCache()
363	if err != nil {
364		return nil
365	}
366
367	for _, d := range cache.Drafts {
368		if d.ID == id {
369			return &d
370		}
371	}
372	return nil
373}
374
375// GetAllDrafts retrieves all drafts sorted by update time (newest first).
376func GetAllDrafts() []Draft {
377	cache, err := LoadDraftsCache()
378	if err != nil {
379		return nil
380	}
381
382	drafts := cache.Drafts
383	sort.Slice(drafts, func(i, j int) bool {
384		return drafts[i].UpdatedAt.After(drafts[j].UpdatedAt)
385	})
386
387	return drafts
388}
389
390// HasDrafts checks if there are any saved drafts.
391func HasDrafts() bool {
392	cache, err := LoadDraftsCache()
393	if err != nil {
394		return false
395	}
396	return len(cache.Drafts) > 0
397}