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	QuotedText     string    `json:"quoted_text,omitempty"`
241	CreatedAt      time.Time `json:"created_at"`
242	UpdatedAt      time.Time `json:"updated_at"`
243}
244
245// DraftsCache stores all saved drafts.
246type DraftsCache struct {
247	Drafts    []Draft   `json:"drafts"`
248	UpdatedAt time.Time `json:"updated_at"`
249}
250
251// draftsFile returns the full path to the drafts cache file.
252func draftsFile() (string, error) {
253	dir, err := configDir()
254	if err != nil {
255		return "", err
256	}
257	return filepath.Join(dir, "drafts.json"), nil
258}
259
260// SaveDraftsCache saves drafts to the cache file.
261func SaveDraftsCache(cache *DraftsCache) error {
262	path, err := draftsFile()
263	if err != nil {
264		return err
265	}
266	if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil {
267		return err
268	}
269	cache.UpdatedAt = time.Now()
270	data, err := json.MarshalIndent(cache, "", "  ")
271	if err != nil {
272		return err
273	}
274	return os.WriteFile(path, data, 0600)
275}
276
277// LoadDraftsCache loads drafts from the cache file.
278func LoadDraftsCache() (*DraftsCache, error) {
279	path, err := draftsFile()
280	if err != nil {
281		return nil, err
282	}
283	data, err := os.ReadFile(path)
284	if err != nil {
285		return nil, err
286	}
287	var cache DraftsCache
288	if err := json.Unmarshal(data, &cache); err != nil {
289		return nil, err
290	}
291	return &cache, nil
292}
293
294// SaveDraft saves or updates a draft.
295func SaveDraft(draft Draft) error {
296	cache, err := LoadDraftsCache()
297	if err != nil {
298		cache = &DraftsCache{Drafts: []Draft{}}
299	}
300
301	draft.UpdatedAt = time.Now()
302
303	// Check if draft exists (update) or is new
304	found := false
305	for i, d := range cache.Drafts {
306		if d.ID == draft.ID {
307			cache.Drafts[i] = draft
308			found = true
309			break
310		}
311	}
312
313	if !found {
314		if draft.CreatedAt.IsZero() {
315			draft.CreatedAt = time.Now()
316		}
317		cache.Drafts = append(cache.Drafts, draft)
318	}
319
320	return SaveDraftsCache(cache)
321}
322
323// DeleteDraft removes a draft by ID.
324func DeleteDraft(id string) error {
325	cache, err := LoadDraftsCache()
326	if err != nil {
327		return nil // No cache, nothing to delete
328	}
329
330	var filtered []Draft
331	for _, d := range cache.Drafts {
332		if d.ID != id {
333			filtered = append(filtered, d)
334		}
335	}
336	cache.Drafts = filtered
337
338	return SaveDraftsCache(cache)
339}
340
341// GetDraft retrieves a draft by ID.
342func GetDraft(id string) *Draft {
343	cache, err := LoadDraftsCache()
344	if err != nil {
345		return nil
346	}
347
348	for _, d := range cache.Drafts {
349		if d.ID == id {
350			return &d
351		}
352	}
353	return nil
354}
355
356// GetAllDrafts retrieves all drafts sorted by update time (newest first).
357func GetAllDrafts() []Draft {
358	cache, err := LoadDraftsCache()
359	if err != nil {
360		return nil
361	}
362
363	drafts := cache.Drafts
364	sort.Slice(drafts, func(i, j int) bool {
365		return drafts[i].UpdatedAt.After(drafts[j].UpdatedAt)
366	})
367
368	return drafts
369}
370
371// HasDrafts checks if there are any saved drafts.
372func HasDrafts() bool {
373	cache, err := LoadDraftsCache()
374	if err != nil {
375		return false
376	}
377	return len(cache.Drafts) > 0
378}