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