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