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}