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 AttachmentPaths []string `json:"attachment_paths,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}