1package config
2
3import (
4 "encoding/json"
5 "os"
6 "path/filepath"
7 "strconv"
8 "strings"
9 "time"
10
11 "github.com/floatpane/matcha/internal/threading"
12)
13
14// CachedFolders stores folder names for a single account.
15type CachedFolders struct {
16 AccountID string `json:"account_id"`
17 Folders []string `json:"folders"`
18 UpdatedAt time.Time `json:"updated_at"`
19}
20
21// FolderCache stores cached folders for all accounts.
22type FolderCache struct {
23 Accounts []CachedFolders `json:"accounts"`
24 ThreadedFolders map[string]bool `json:"threaded_folders,omitempty"`
25 UpdatedAt time.Time `json:"updated_at"`
26}
27
28// folderCacheFile returns the full path to the folder cache file.
29func folderCacheFile() (string, error) {
30 dir, err := cacheDir()
31 if err != nil {
32 return "", err
33 }
34 return filepath.Join(dir, "folder_cache.json"), nil
35}
36
37// SaveFolderCache saves the folder cache to disk.
38func SaveFolderCache(cache *FolderCache) error {
39 path, err := folderCacheFile()
40 if err != nil {
41 return err
42 }
43 if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil {
44 return err
45 }
46 cache.UpdatedAt = time.Now()
47 data, err := json.MarshalIndent(cache, "", " ")
48 if err != nil {
49 return err
50 }
51 return SecureWriteFile(path, data, 0600)
52}
53
54// LoadFolderCache loads the folder cache from disk.
55func LoadFolderCache() (*FolderCache, error) {
56 path, err := folderCacheFile()
57 if err != nil {
58 return nil, err
59 }
60 data, err := SecureReadFile(path)
61 if err != nil {
62 return nil, err
63 }
64 var cache FolderCache
65 if err := json.Unmarshal(data, &cache); err != nil {
66 return nil, err
67 }
68 return &cache, nil
69}
70
71// GetCachedFolders returns cached folder names for a specific account.
72func GetCachedFolders(accountID string) []string {
73 cache, err := LoadFolderCache()
74 if err != nil {
75 return nil
76 }
77 for _, acc := range cache.Accounts {
78 if acc.AccountID == accountID {
79 return acc.Folders
80 }
81 }
82 return nil
83}
84
85// SaveAccountFolders saves folder names for a specific account, merging into the existing cache.
86func SaveAccountFolders(accountID string, folders []string) error {
87 cache, err := LoadFolderCache()
88 if err != nil {
89 cache = &FolderCache{}
90 }
91
92 found := false
93 for i, acc := range cache.Accounts {
94 if acc.AccountID == accountID {
95 cache.Accounts[i].Folders = folders
96 cache.Accounts[i].UpdatedAt = time.Now()
97 found = true
98 break
99 }
100 }
101
102 if !found {
103 cache.Accounts = append(cache.Accounts, CachedFolders{
104 AccountID: accountID,
105 Folders: folders,
106 UpdatedAt: time.Now(),
107 })
108 }
109
110 return SaveFolderCache(cache)
111}
112
113// --- Per-folder email cache ---
114
115// FolderEmailCache stores cached emails for a specific folder.
116type FolderEmailCache struct {
117 FolderName string `json:"folder_name"`
118 Emails []CachedEmail `json:"emails"`
119 UpdatedAt time.Time `json:"updated_at"`
120}
121
122// folderEmailCacheDir returns the directory for folder email cache files.
123func folderEmailCacheDir() (string, error) {
124 dir, err := cacheDir()
125 if err != nil {
126 return "", err
127 }
128 return filepath.Join(dir, "folder_emails"), nil
129}
130
131// folderEmailCacheFile returns the file path for a folder's email cache.
132// Uses a sanitized folder name to avoid filesystem issues.
133func folderEmailCacheFile(folderName string) (string, error) {
134 dir, err := folderEmailCacheDir()
135 if err != nil {
136 return "", err
137 }
138 // Sanitize folder name for use as filename
139 safe := sanitizeFolderName(folderName)
140 return filepath.Join(dir, safe+".json"), nil
141}
142
143func sanitizeFolderName(name string) string {
144 // Replace path separators and other problematic chars
145 replacer := strings.NewReplacer("/", "_", "\\", "_", ":", "_", " ", "_")
146 return replacer.Replace(name)
147}
148
149// SaveFolderEmailCache saves emails for a folder to disk.
150func SaveFolderEmailCache(folderName string, emails []CachedEmail) error {
151 path, err := folderEmailCacheFile(folderName)
152 if err != nil {
153 return err
154 }
155 if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil {
156 return err
157 }
158 cache := FolderEmailCache{
159 FolderName: folderName,
160 Emails: emails,
161 UpdatedAt: time.Now(),
162 }
163 data, err := json.Marshal(cache)
164 if err != nil {
165 return err
166 }
167 return SecureWriteFile(path, data, 0600)
168}
169
170// LoadFolderEmailCache loads cached emails for a folder from disk.
171func LoadFolderEmailCache(folderName string) ([]CachedEmail, error) {
172 path, err := folderEmailCacheFile(folderName)
173 if err != nil {
174 return nil, err
175 }
176 data, err := SecureReadFile(path)
177 if err != nil {
178 return nil, err
179 }
180 var cache FolderEmailCache
181 if err := json.Unmarshal(data, &cache); err != nil {
182 return nil, err
183 }
184 return cache.Emails, nil
185}
186
187func LoadFolderEmailHeaders(folderName string) ([]threading.EmailHeader, error) {
188 emails, err := LoadFolderEmailCache(folderName)
189 if err != nil {
190 return nil, err
191 }
192 headers := make([]threading.EmailHeader, 0, len(emails))
193 for _, email := range emails {
194 headers = append(headers, threading.EmailHeader{
195 ID: email.MessageID,
196 InReplyTo: email.InReplyTo,
197 References: email.References,
198 Subject: email.Subject,
199 Date: email.Date,
200 EmailID: cachedEmailID(email),
201 Sender: email.From,
202 })
203 }
204 return headers, nil
205}
206
207// IsFolderThreaded returns the threading state for a folder. If the user has
208// explicitly toggled threading for this folder, that override is returned.
209// Otherwise defaultEnabled (from Config.EnableThreaded) is used.
210func IsFolderThreaded(folderName string, defaultEnabled bool) bool {
211 cache, err := LoadFolderCache()
212 if err != nil || cache.ThreadedFolders == nil {
213 return defaultEnabled
214 }
215 v, ok := cache.ThreadedFolders[folderName]
216 if !ok {
217 return defaultEnabled
218 }
219 return v
220}
221
222// SetFolderThreaded stores an explicit per-folder threading override.
223func SetFolderThreaded(folderName string, threaded bool) error {
224 cache, err := LoadFolderCache()
225 if err != nil {
226 cache = &FolderCache{}
227 }
228 if cache.ThreadedFolders == nil {
229 cache.ThreadedFolders = make(map[string]bool)
230 }
231 cache.ThreadedFolders[folderName] = threaded
232 return SaveFolderCache(cache)
233}
234
235func cachedEmailID(email CachedEmail) string {
236 return email.AccountID + ":" + formatUID(email.UID)
237}
238
239func formatUID(uid uint32) string {
240 return strconv.FormatUint(uint64(uid), 10)
241}