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