1package config
2
3import (
4 "encoding/json"
5 "errors"
6 "os"
7 "path/filepath"
8 "sort"
9 "strings"
10 "time"
11)
12
13// CachedEmail stores essential email data for caching.
14type CachedEmail struct {
15 UID uint32 `json:"uid"`
16 From string `json:"from"`
17 To []string `json:"to"`
18 Subject string `json:"subject"`
19 Date time.Time `json:"date"`
20 MessageID string `json:"message_id"`
21 InReplyTo string `json:"in_reply_to,omitempty"`
22 References []string `json:"references,omitempty"`
23 AccountID string `json:"account_id"`
24 IsRead bool `json:"is_read"`
25}
26
27// EmailCache stores cached emails for all accounts.
28type EmailCache struct {
29 Emails []CachedEmail `json:"emails"`
30 UpdatedAt time.Time `json:"updated_at"`
31}
32
33// cacheFile returns the full path to the email cache file.
34func cacheFile() (string, error) {
35 dir, err := cacheDir()
36 if err != nil {
37 return "", err
38 }
39 return filepath.Join(dir, "email_cache.json"), nil
40}
41
42// SaveEmailCache saves emails to the cache file.
43func SaveEmailCache(cache *EmailCache) error {
44 path, err := cacheFile()
45 if err != nil {
46 return err
47 }
48 if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil {
49 return err
50 }
51 cache.UpdatedAt = time.Now()
52 data, err := json.MarshalIndent(cache, "", " ")
53 if err != nil {
54 return err
55 }
56 return SecureWriteFile(path, data, 0600)
57}
58
59// LoadEmailCache loads emails from the cache file.
60func LoadEmailCache() (*EmailCache, error) {
61 path, err := cacheFile()
62 if err != nil {
63 return nil, err
64 }
65 data, err := SecureReadFile(path)
66 if err != nil {
67 return nil, err
68 }
69 var cache EmailCache
70 if err := json.Unmarshal(data, &cache); err != nil {
71 return nil, err
72 }
73 return &cache, nil
74}
75
76// HasEmailCache checks if a cache file exists.
77func HasEmailCache() bool {
78 path, err := cacheFile()
79 if err != nil {
80 return false
81 }
82 _, err = os.Stat(path)
83 return err == nil
84}
85
86// ClearEmailCache removes the cache file.
87func ClearEmailCache() error {
88 path, err := cacheFile()
89 if err != nil {
90 return err
91 }
92 return os.Remove(path)
93}
94
95func removeAccountFromEmailCache(accountID string) error {
96 cache, err := LoadEmailCache()
97 if err != nil {
98 if os.IsNotExist(err) {
99 return nil
100 }
101 return err
102 }
103 filtered := cache.Emails[:0]
104 for _, email := range cache.Emails {
105 if email.AccountID != accountID {
106 filtered = append(filtered, email)
107 }
108 }
109 if len(filtered) == len(cache.Emails) {
110 return nil
111 }
112 cache.Emails = filtered
113 return SaveEmailCache(cache)
114}
115
116// --- Contacts Cache ---
117
118const legacyContactUsageKey = "__legacy__"
119
120// ContactUsage stores per-account contact usage metadata.
121type ContactUsage struct {
122 LastUsed time.Time `json:"last_used"`
123 UseCount int `json:"use_count"`
124}
125
126// Contact stores a contact's name, email address, and per-account usage.
127//
128// For regular contacts, Email holds a single address and Addresses is empty.
129// For mailing-list virtual contacts emitted by SearchContacts, Email is empty
130// and Addresses holds the expanded list of recipients. Callers that need to
131// distinguish the two cases should check len(Addresses) > 0.
132type Contact struct {
133 Name string `json:"name"`
134 Email string `json:"email"`
135 Addresses []string `json:"addresses,omitempty"`
136 Usage map[string]ContactUsage `json:"usage_by_account"`
137}
138
139// UnmarshalJSON accepts both the current usage_by_account format and the
140// legacy last_used/use_count fields so old contacts can be migrated.
141func (c *Contact) UnmarshalJSON(data []byte) error {
142 type contactAlias Contact
143 aux := struct {
144 *contactAlias
145 LastUsed time.Time `json:"last_used"`
146 UseCount int `json:"use_count"`
147 }{
148 contactAlias: (*contactAlias)(c),
149 }
150 if err := json.Unmarshal(data, &aux); err != nil {
151 return err
152 }
153 if c.Usage == nil {
154 c.Usage = make(map[string]ContactUsage)
155 }
156 if len(c.Usage) == 0 && (!aux.LastUsed.IsZero() || aux.UseCount > 0) {
157 c.Usage[legacyContactUsageKey] = ContactUsage{
158 LastUsed: aux.LastUsed,
159 UseCount: aux.UseCount,
160 }
161 }
162 return nil
163}
164
165// ContactsCache stores all known contacts.
166type ContactsCache struct {
167 Contacts []Contact `json:"contacts"`
168 UpdatedAt time.Time `json:"updated_at"`
169}
170
171// GetContactsCachePath returns the full path to the contacts cache file.
172func GetContactsCachePath() (string, error) {
173 dir, err := cacheDir()
174 if err != nil {
175 return "", err
176 }
177 return filepath.Join(dir, "contacts.json"), nil
178}
179
180// SaveContactsCache saves contacts to the cache file.
181func SaveContactsCache(cache *ContactsCache) error {
182 path, err := GetContactsCachePath()
183 if err != nil {
184 return err
185 }
186 if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil {
187 return err
188 }
189 for i := range cache.Contacts {
190 if cache.Contacts[i].Usage == nil {
191 cache.Contacts[i].Usage = make(map[string]ContactUsage)
192 }
193 }
194 cache.UpdatedAt = time.Now()
195 data, err := json.MarshalIndent(cache, "", " ")
196 if err != nil {
197 return err
198 }
199 return SecureWriteFile(path, data, 0600)
200}
201
202// LoadContactsCache loads contacts from the cache file.
203func LoadContactsCache() (*ContactsCache, error) {
204 path, err := GetContactsCachePath()
205 if err != nil {
206 return nil, err
207 }
208 data, err := SecureReadFile(path)
209 if err != nil {
210 return nil, err
211 }
212 var cache ContactsCache
213 if err := json.Unmarshal(data, &cache); err != nil {
214 return nil, err
215 }
216 return &cache, nil
217}
218
219func normalizeContactEmail(email string) string {
220 return strings.ToLower(strings.Trim(strings.TrimSpace(email), ",<>"))
221}
222
223// AddContact adds or updates a global contact in the cache.
224func AddContact(name, email string) error {
225 return AddContactForAccount(name, email, "")
226}
227
228// AddContactForAccount adds or updates a contact in the cache for an account.
229func AddContactForAccount(name, email, accountID string) error {
230 if email == "" {
231 return nil
232 }
233
234 email = normalizeContactEmail(email)
235 name = strings.TrimSpace(name)
236
237 cache, err := LoadContactsCache()
238 if err != nil {
239 cache = &ContactsCache{Contacts: []Contact{}}
240 }
241
242 // Check if contact exists
243 found := false
244 for i, c := range cache.Contacts {
245 if strings.EqualFold(c.Email, email) {
246 // Normalize the stored email to a canonical lowercase form.
247 cache.Contacts[i].Email = email
248 if cache.Contacts[i].Usage == nil {
249 cache.Contacts[i].Usage = make(map[string]ContactUsage)
250 }
251 usage := cache.Contacts[i].Usage[accountID]
252 usage.UseCount++
253 usage.LastUsed = time.Now()
254 cache.Contacts[i].Usage[accountID] = usage
255 // Update name if we have a better one
256 if name != "" && (c.Name == "" || c.Name == email) {
257 cache.Contacts[i].Name = name
258 }
259 found = true
260 break
261 }
262 }
263
264 if !found {
265 cache.Contacts = append(cache.Contacts, Contact{
266 Name: name,
267 Email: email,
268 Usage: map[string]ContactUsage{
269 accountID: {
270 LastUsed: time.Now(),
271 UseCount: 1,
272 },
273 },
274 })
275 }
276
277 return SaveContactsCache(cache)
278}
279
280func contactUsageForAccount(c Contact, accountID string) (ContactUsage, bool) {
281 if len(c.Usage) == 0 {
282 return ContactUsage{}, accountID == ""
283 }
284 if accountID != "" {
285 if usage, ok := c.Usage[legacyContactUsageKey]; ok {
286 return usage, true
287 }
288 usage, ok := c.Usage[accountID]
289 return usage, ok
290 }
291 var aggregate ContactUsage
292 for _, usage := range c.Usage {
293 aggregate.UseCount += usage.UseCount
294 if usage.LastUsed.After(aggregate.LastUsed) {
295 aggregate.LastUsed = usage.LastUsed
296 }
297 }
298 return aggregate, true
299}
300
301// ContactAggregateUsage returns a contact's total usage across accounts.
302func ContactAggregateUsage(c Contact) ContactUsage {
303 usage, _ := contactUsageForAccount(c, "")
304 return usage
305}
306
307// SearchContacts searches for contacts matching the query across all accounts.
308func SearchContacts(query string) []Contact {
309 return SearchContactsForAccount(query, "")
310}
311
312// SearchContactsForAccount searches for contacts matching the query for an account.
313func SearchContactsForAccount(query, accountID string) []Contact {
314 cache, err := LoadContactsCache()
315 if err != nil {
316 return nil
317 }
318
319 query = strings.ToLower(strings.TrimSpace(query))
320 if query == "" {
321 return nil
322 }
323
324 var matches []Contact
325
326 // Add mailing lists to matches if they match the query
327 cfg, err := LoadConfig()
328 if err == nil {
329 for _, list := range cfg.MailingLists {
330 if strings.Contains(strings.ToLower(list.Name), query) {
331 // Convert mailing list to a virtual contact. Addresses are
332 // stored in a dedicated slice so the Email field keeps its
333 // single-address invariant -- avoids corruption by
334 // normalizeContactEmail and exact-match lookups in callers.
335 addresses := append([]string(nil), list.Addresses...)
336 matches = append(matches, Contact{
337 Name: list.Name,
338 Addresses: addresses,
339 Usage: map[string]ContactUsage{
340 accountID: {
341 UseCount: 9999, // Ensure lists appear at the top
342 LastUsed: time.Now(),
343 },
344 },
345 })
346 }
347 }
348 }
349
350 for _, c := range cache.Contacts {
351 if strings.Contains(strings.ToLower(c.Email), query) ||
352 strings.Contains(strings.ToLower(c.Name), query) {
353 if _, ok := contactUsageForAccount(c, accountID); ok {
354 matches = append(matches, c)
355 }
356 }
357 }
358
359 // Sort by use count (most used first), then by last used
360 sort.Slice(matches, func(i, j int) bool {
361 left, _ := contactUsageForAccount(matches[i], accountID)
362 right, _ := contactUsageForAccount(matches[j], accountID)
363 if left.UseCount != right.UseCount {
364 return left.UseCount > right.UseCount
365 }
366 return left.LastUsed.After(right.LastUsed)
367 })
368
369 // Limit to 5 suggestions
370 if len(matches) > 5 {
371 matches = matches[:5]
372 }
373
374 return matches
375}
376
377// MigrateContactsCacheUsage expands legacy global contact usage to all accounts.
378func MigrateContactsCacheUsage(accountIDs []string) error {
379 cache, err := LoadContactsCache()
380 if err != nil {
381 return err
382 }
383
384 changed := false
385 for i := range cache.Contacts {
386 if cache.Contacts[i].Usage == nil {
387 cache.Contacts[i].Usage = make(map[string]ContactUsage)
388 changed = true
389 }
390 legacyUsage, hasLegacy := cache.Contacts[i].Usage[legacyContactUsageKey]
391 if !hasLegacy {
392 continue
393 }
394 delete(cache.Contacts[i].Usage, legacyContactUsageKey)
395 for _, accountID := range accountIDs {
396 if accountID == "" {
397 continue
398 }
399 if _, ok := cache.Contacts[i].Usage[accountID]; !ok {
400 cache.Contacts[i].Usage[accountID] = legacyUsage
401 }
402 }
403 changed = true
404 }
405 if !changed {
406 return nil
407 }
408 return SaveContactsCache(cache)
409}
410
411func removeAccountFromContactsCache(accountID string) error {
412 cache, err := LoadContactsCache()
413 if err != nil {
414 if os.IsNotExist(err) {
415 return nil
416 }
417 return err
418 }
419
420 changed := false
421 filtered := cache.Contacts[:0]
422 for _, contact := range cache.Contacts {
423 if _, ok := contact.Usage[accountID]; ok {
424 delete(contact.Usage, accountID)
425 changed = true
426 }
427 if len(contact.Usage) > 0 {
428 filtered = append(filtered, contact)
429 } else {
430 changed = true
431 }
432 }
433 if !changed {
434 return nil
435 }
436 cache.Contacts = filtered
437 return SaveContactsCache(cache)
438}
439
440// --- Drafts Cache ---
441
442// Draft stores a saved email draft.
443type Draft struct {
444 ID string `json:"id"`
445 To string `json:"to"`
446 Cc string `json:"cc,omitempty"`
447 Bcc string `json:"bcc,omitempty"`
448 Subject string `json:"subject"`
449 Body string `json:"body"`
450 AttachmentPaths []string `json:"attachment_paths,omitempty"`
451 AccountID string `json:"account_id"`
452 FromOverride string `json:"from_override,omitempty"`
453 InReplyTo string `json:"in_reply_to,omitempty"`
454 References []string `json:"references,omitempty"`
455 QuotedText string `json:"quoted_text,omitempty"`
456 CreatedAt time.Time `json:"created_at"`
457 UpdatedAt time.Time `json:"updated_at"`
458}
459
460// DraftsCache stores all saved drafts.
461type DraftsCache struct {
462 Drafts []Draft `json:"drafts"`
463 UpdatedAt time.Time `json:"updated_at"`
464}
465
466// draftsFile returns the full path to the drafts cache file.
467func draftsFile() (string, error) {
468 dir, err := cacheDir()
469 if err != nil {
470 return "", err
471 }
472 return filepath.Join(dir, "drafts.json"), nil
473}
474
475// SaveDraftsCache saves drafts to the cache file.
476func SaveDraftsCache(cache *DraftsCache) error {
477 path, err := draftsFile()
478 if err != nil {
479 return err
480 }
481 if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil {
482 return err
483 }
484 cache.UpdatedAt = time.Now()
485 data, err := json.MarshalIndent(cache, "", " ")
486 if err != nil {
487 return err
488 }
489 return SecureWriteFile(path, data, 0600)
490}
491
492// LoadDraftsCache loads drafts from the cache file.
493func LoadDraftsCache() (*DraftsCache, error) {
494 path, err := draftsFile()
495 if err != nil {
496 return nil, err
497 }
498 data, err := SecureReadFile(path)
499 if err != nil {
500 return nil, err
501 }
502 var cache DraftsCache
503 if err := json.Unmarshal(data, &cache); err != nil {
504 return nil, err
505 }
506 return &cache, nil
507}
508
509// SaveDraft saves or updates a draft.
510func SaveDraft(draft Draft) error {
511 cache, err := LoadDraftsCache()
512 if err != nil {
513 cache = &DraftsCache{Drafts: []Draft{}}
514 }
515
516 draft.UpdatedAt = time.Now()
517
518 // Check if draft exists (update) or is new
519 found := false
520 for i, d := range cache.Drafts {
521 if d.ID == draft.ID {
522 cache.Drafts[i] = draft
523 found = true
524 break
525 }
526 }
527
528 if !found {
529 if draft.CreatedAt.IsZero() {
530 draft.CreatedAt = time.Now()
531 }
532 cache.Drafts = append(cache.Drafts, draft)
533 }
534
535 return SaveDraftsCache(cache)
536}
537
538// DeleteDraft removes a draft by ID.
539func DeleteDraft(id string) error {
540 cache, err := LoadDraftsCache()
541 if err != nil {
542 return err
543 }
544
545 var filtered []Draft
546 for _, d := range cache.Drafts {
547 if d.ID != id {
548 filtered = append(filtered, d)
549 }
550 }
551 cache.Drafts = filtered
552
553 return SaveDraftsCache(cache)
554}
555
556// GetDraft retrieves a draft by ID.
557func GetDraft(id string) *Draft {
558 cache, err := LoadDraftsCache()
559 if err != nil {
560 return nil
561 }
562
563 for _, d := range cache.Drafts {
564 if d.ID == id {
565 return &d
566 }
567 }
568 return nil
569}
570
571// GetAllDrafts retrieves all drafts sorted by update time (newest first).
572func GetAllDrafts() []Draft {
573 cache, err := LoadDraftsCache()
574 if err != nil {
575 return nil
576 }
577
578 drafts := cache.Drafts
579 sort.Slice(drafts, func(i, j int) bool {
580 return drafts[i].UpdatedAt.After(drafts[j].UpdatedAt)
581 })
582
583 return drafts
584}
585
586// HasDrafts checks if there are any saved drafts.
587func HasDrafts() bool {
588 cache, err := LoadDraftsCache()
589 if err != nil {
590 return false
591 }
592 return len(cache.Drafts) > 0
593}
594
595func removeAccountFromDraftsCache(accountID string) error {
596 cache, err := LoadDraftsCache()
597 if err != nil {
598 if os.IsNotExist(err) {
599 return nil
600 }
601 return err
602 }
603 filtered := cache.Drafts[:0]
604 for _, draft := range cache.Drafts {
605 if draft.AccountID != accountID {
606 filtered = append(filtered, draft)
607 }
608 }
609 if len(filtered) == len(cache.Drafts) {
610 return nil
611 }
612 cache.Drafts = filtered
613 return SaveDraftsCache(cache)
614}
615
616// --- Email Body Cache ---
617
618// CachedAttachment stores attachment metadata (not the binary data).
619type CachedAttachment struct {
620 Filename string `json:"filename"`
621 PartID string `json:"part_id"`
622 Encoding string `json:"encoding,omitempty"`
623 MIMEType string `json:"mime_type,omitempty"`
624 ContentID string `json:"content_id,omitempty"`
625 Inline bool `json:"inline,omitempty"`
626 IsSMIMESignature bool `json:"is_smime_signature,omitempty"`
627 SMIMEVerified bool `json:"smime_verified,omitempty"`
628 IsSMIMEEncrypted bool `json:"is_smime_encrypted,omitempty"`
629 IsCalendarInvite bool `json:"is_calendar_invite,omitempty"`
630 CalendarData []byte `json:"calendar_data,omitempty"` // Raw .ics data for calendar invites
631}
632
633// CachedEmailBody stores the body and attachment metadata for a single email.
634type CachedEmailBody struct {
635 UID uint32 `json:"uid"`
636 AccountID string `json:"account_id"`
637 Body string `json:"body"`
638 BodyMIMEType string `json:"body_mime_type,omitempty"` // empty for cache rows written before MIME-type tracking; renderer falls back to legacy markdown→HTML pre-pass
639 Attachments []CachedAttachment `json:"attachments,omitempty"`
640 CachedAt time.Time `json:"cached_at"`
641 LastAccessedAt time.Time `json:"last_accessed_at"`
642 SizeBytes int `json:"size_bytes"`
643}
644
645// EmailBodyCache stores cached email bodies for a folder.
646type EmailBodyCache struct {
647 FolderName string `json:"folder_name"`
648 Bodies []CachedEmailBody `json:"bodies"`
649 UpdatedAt time.Time `json:"updated_at"`
650}
651
652// bodyCacheDir returns the directory for body cache files.
653func bodyCacheDir() (string, error) {
654 dir, err := cacheDir()
655 if err != nil {
656 return "", err
657 }
658 return filepath.Join(dir, "email_bodies"), nil
659}
660
661// bodyBacheFile returns the file path for a folder's body cache.
662func bodyCacheFile(folderName string) (string, error) {
663 dir, err := bodyCacheDir()
664 if err != nil {
665 return "", err
666 }
667 safe := strings.NewReplacer("/", "_", "\\", "_", ":", "_", " ", "_").Replace(folderName)
668 return filepath.Join(dir, safe+".json"), nil
669}
670
671// LoadEmailBodyCache loads the body cache for a folder.
672func LoadEmailBodyCache(folderName string) (*EmailBodyCache, error) {
673 path, err := bodyCacheFile(folderName)
674 if err != nil {
675 return nil, err
676 }
677 data, err := SecureReadFile(path)
678 if err != nil {
679 return nil, err
680 }
681 var cache EmailBodyCache
682 if err := json.Unmarshal(data, &cache); err != nil {
683 return nil, err
684 }
685 return &cache, nil
686}
687
688// saveEmailBodyCache writes the body cache for a folder.
689func saveEmailBodyCache(cache *EmailBodyCache) error {
690 path, err := bodyCacheFile(cache.FolderName)
691 if err != nil {
692 return err
693 }
694 if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil {
695 return err
696 }
697 cache.UpdatedAt = time.Now()
698 data, err := json.Marshal(cache)
699 if err != nil {
700 return err
701 }
702 return SecureWriteFile(path, data, 0600)
703}
704
705// GetCachedEmailBody returns the cached body for a specific email, or nil if not cached.
706// LastAccessedAt is updated by SaveEmailBody, not here -- a read should not
707// mutate cache state.
708func GetCachedEmailBody(folderName string, uid uint32, accountID string, threshold int) *CachedEmailBody {
709 lru := GetLRUInstance(threshold)
710 return lru.Get(folderName, uid, accountID)
711}
712
713func calculateEmailBodySize(body *CachedEmailBody) int {
714 size := len(body.Body)
715 for _, att := range body.Attachments {
716 size += len(att.Filename)
717 size += len(att.PartID)
718 size += len(att.Encoding)
719 size += len(att.MIMEType)
720 size += len(att.ContentID)
721 size += len(att.CalendarData)
722 }
723 return size
724}
725
726// SaveEmailBody saves or updates a cached email body for a folder.
727func SaveEmailBody(folderName string, body CachedEmailBody, threshold int) error {
728 body.CachedAt = time.Now()
729 body.SizeBytes = calculateEmailBodySize(&body)
730
731 lru := GetLRUInstance(threshold)
732 lru.Put(folderName, body.UID, body.AccountID, &body)
733
734 return nil
735}
736
737// PruneEmailBodyCache removes cached bodies for emails that are no longer in the folder.
738// validUIDs is a map of UID -> AccountID for emails still present.
739func PruneEmailBodyCache(folderName string, validUIDs map[uint32]string, threshold int) error {
740 cache, err := LoadEmailBodyCache(folderName)
741
742 if err != nil {
743 return err
744 }
745
746 lru := GetLRUInstance(threshold)
747
748 var kept []CachedEmailBody
749 for _, b := range cache.Bodies {
750 if accID, ok := validUIDs[b.UID]; ok && accID == b.AccountID {
751 kept = append(kept, b)
752 } else {
753 lru.Delete(folderName, b.UID, b.AccountID)
754 }
755 }
756
757 if len(kept) == len(cache.Bodies) {
758 return nil
759 }
760
761 cache.Bodies = kept
762 return saveEmailBodyCache(cache)
763}
764
765func removeAccountFromEmailBodyCaches(accountID string) error {
766 dir, err := bodyCacheDir()
767 if err != nil {
768 return err
769 }
770 entries, err := os.ReadDir(dir)
771 if err != nil {
772 if os.IsNotExist(err) {
773 return nil
774 }
775 return err
776 }
777
778 var errs []error
779 for _, entry := range entries {
780 if entry.IsDir() {
781 continue
782 }
783 path := filepath.Join(dir, entry.Name())
784 data, err := SecureReadFile(path)
785 if err != nil {
786 errs = append(errs, err)
787 continue
788 }
789 var cache EmailBodyCache
790 if err := json.Unmarshal(data, &cache); err != nil {
791 errs = append(errs, err)
792 continue
793 }
794
795 filtered := cache.Bodies[:0]
796 for _, body := range cache.Bodies {
797 if body.AccountID != accountID {
798 filtered = append(filtered, body)
799 }
800 }
801 if len(filtered) == len(cache.Bodies) {
802 continue
803 }
804 if len(filtered) == 0 {
805 if err := os.Remove(path); err != nil && !os.IsNotExist(err) {
806 errs = append(errs, err)
807 }
808 continue
809 }
810 cache.Bodies = filtered
811 cache.UpdatedAt = time.Now()
812 data, err = json.Marshal(cache)
813 if err != nil {
814 errs = append(errs, err)
815 continue
816 }
817 if err := SecureWriteFile(path, data, 0600); err != nil {
818 errs = append(errs, err)
819 }
820 }
821 return errors.Join(errs...)
822}
823
824// CleanupAccountCache removes cached data associated with an account.
825func CleanupAccountCache(accountID string) error {
826 if accountID == "" {
827 return nil
828 }
829
830 return errors.Join(
831 removeAccountFromEmailCache(accountID),
832 removeAccountFromFolderCache(accountID),
833 removeAccountFromFolderEmailCaches(accountID),
834 removeAccountFromEmailBodyCaches(accountID),
835 removeAccountFromContactsCache(accountID),
836 removeAccountFromDraftsCache(accountID),
837 )
838}