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}