1package daemon
2
3import (
4 "context"
5 "encoding/json"
6 "fmt"
7 "log"
8 "os"
9 "time"
10
11 "github.com/floatpane/matcha/daemonrpc"
12)
13
14func (d *Daemon) handleRequest(conn *daemonrpc.Conn, req *daemonrpc.Request) {
15 switch req.Method {
16 case daemonrpc.MethodPing:
17 d.handlePing(conn, req)
18 case daemonrpc.MethodGetStatus:
19 d.handleGetStatus(conn, req)
20 case daemonrpc.MethodGetAccounts:
21 d.handleGetAccounts(conn, req)
22 case daemonrpc.MethodReloadConfig:
23 d.handleReloadConfig(conn, req)
24 case daemonrpc.MethodFetchEmails:
25 d.handleFetchEmails(conn, req)
26 case daemonrpc.MethodFetchEmailBody:
27 d.handleFetchEmailBody(conn, req)
28 case daemonrpc.MethodDeleteEmails:
29 d.handleDeleteEmails(conn, req)
30 case daemonrpc.MethodArchiveEmails:
31 d.handleArchiveEmails(conn, req)
32 case daemonrpc.MethodMoveEmails:
33 d.handleMoveEmails(conn, req)
34 case daemonrpc.MethodMarkRead:
35 d.handleMarkRead(conn, req)
36 case daemonrpc.MethodFetchFolders:
37 d.handleFetchFolders(conn, req)
38 case daemonrpc.MethodRefreshFolder:
39 d.handleRefreshFolder(conn, req)
40 case daemonrpc.MethodSubscribe:
41 d.handleSubscribe(conn, req)
42 case daemonrpc.MethodUnsubscribe:
43 d.handleUnsubscribe(conn, req)
44 default:
45 conn.SendError(req.ID, daemonrpc.ErrCodeNotFound, fmt.Sprintf("unknown method: %s", req.Method))
46 }
47}
48
49func decodeParams[T any](req *daemonrpc.Request) (T, error) {
50 var params T
51 if req.Params != nil {
52 if err := json.Unmarshal(req.Params, ¶ms); err != nil {
53 return params, err
54 }
55 }
56 return params, nil
57}
58
59func (d *Daemon) handlePing(conn *daemonrpc.Conn, req *daemonrpc.Request) {
60 conn.SendResponse(req.ID, daemonrpc.PingResult{Pong: true})
61}
62
63func (d *Daemon) handleGetStatus(conn *daemonrpc.Conn, req *daemonrpc.Request) {
64 d.mu.RLock()
65 var accounts []string
66 for _, acct := range d.config.Accounts {
67 accounts = append(accounts, acct.Email)
68 }
69 d.mu.RUnlock()
70
71 conn.SendResponse(req.ID, daemonrpc.StatusResult{
72 Running: true,
73 Uptime: int64(time.Since(d.startTime).Seconds()),
74 Accounts: accounts,
75 PID: os.Getpid(),
76 })
77}
78
79func (d *Daemon) handleGetAccounts(conn *daemonrpc.Conn, req *daemonrpc.Request) {
80 d.mu.RLock()
81 defer d.mu.RUnlock()
82
83 var infos []daemonrpc.AccountInfo
84 for _, acct := range d.config.Accounts {
85 protocol := acct.Protocol
86 if protocol == "" {
87 protocol = "imap"
88 }
89 infos = append(infos, daemonrpc.AccountInfo{
90 ID: acct.ID,
91 Name: acct.Name,
92 Email: acct.Email,
93 Protocol: protocol,
94 })
95 }
96 conn.SendResponse(req.ID, infos)
97}
98
99func (d *Daemon) handleReloadConfig(conn *daemonrpc.Conn, req *daemonrpc.Request) {
100 if err := d.ReloadConfig(); err != nil {
101 conn.SendError(req.ID, daemonrpc.ErrCodeInternal, err.Error())
102 return
103 }
104 conn.SendResponse(req.ID, true)
105}
106
107func (d *Daemon) handleFetchEmails(conn *daemonrpc.Conn, req *daemonrpc.Request) {
108 params, err := decodeParams[daemonrpc.FetchEmailsParams](req)
109 if err != nil {
110 conn.SendError(req.ID, daemonrpc.ErrCodeParse, err.Error())
111 return
112 }
113
114 p, err := d.getProvider(params.AccountID)
115 if err != nil {
116 conn.SendError(req.ID, daemonrpc.ErrCodeInternal, err.Error())
117 return
118 }
119
120 ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
121 defer cancel()
122
123 emails, err := p.FetchEmails(ctx, params.Folder, params.Limit, params.Offset)
124 if err != nil {
125 conn.SendError(req.ID, daemonrpc.ErrCodeInternal, err.Error())
126 return
127 }
128
129 conn.SendResponse(req.ID, emails)
130}
131
132func (d *Daemon) handleFetchEmailBody(conn *daemonrpc.Conn, req *daemonrpc.Request) {
133 params, err := decodeParams[daemonrpc.FetchEmailBodyParams](req)
134 if err != nil {
135 conn.SendError(req.ID, daemonrpc.ErrCodeParse, err.Error())
136 return
137 }
138
139 p, err := d.getProvider(params.AccountID)
140 if err != nil {
141 conn.SendError(req.ID, daemonrpc.ErrCodeInternal, err.Error())
142 return
143 }
144
145 ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
146 defer cancel()
147
148 body, attachments, err := p.FetchEmailBody(ctx, params.Folder, params.UID)
149 if err != nil {
150 conn.SendError(req.ID, daemonrpc.ErrCodeInternal, err.Error())
151 return
152 }
153
154 // Convert backend.Attachment to daemonrpc.AttachmentInfo for wire transfer.
155 var attInfos []daemonrpc.AttachmentInfo
156 for _, att := range attachments {
157 attInfos = append(attInfos, daemonrpc.AttachmentInfo{
158 Filename: att.Filename,
159 PartID: att.PartID,
160 Encoding: att.Encoding,
161 MIMEType: att.MIMEType,
162 })
163 }
164
165 conn.SendResponse(req.ID, daemonrpc.FetchEmailBodyResult{
166 Body: body,
167 Attachments: attInfos,
168 })
169}
170
171func (d *Daemon) handleDeleteEmails(conn *daemonrpc.Conn, req *daemonrpc.Request) {
172 params, err := decodeParams[daemonrpc.DeleteEmailsParams](req)
173 if err != nil {
174 conn.SendError(req.ID, daemonrpc.ErrCodeParse, err.Error())
175 return
176 }
177
178 p, err := d.getProvider(params.AccountID)
179 if err != nil {
180 conn.SendError(req.ID, daemonrpc.ErrCodeInternal, err.Error())
181 return
182 }
183
184 ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
185 defer cancel()
186
187 if err := p.DeleteEmails(ctx, params.Folder, params.UIDs); err != nil {
188 conn.SendError(req.ID, daemonrpc.ErrCodeInternal, err.Error())
189 return
190 }
191 conn.SendResponse(req.ID, true)
192}
193
194func (d *Daemon) handleArchiveEmails(conn *daemonrpc.Conn, req *daemonrpc.Request) {
195 params, err := decodeParams[daemonrpc.ArchiveEmailsParams](req)
196 if err != nil {
197 conn.SendError(req.ID, daemonrpc.ErrCodeParse, err.Error())
198 return
199 }
200
201 p, err := d.getProvider(params.AccountID)
202 if err != nil {
203 conn.SendError(req.ID, daemonrpc.ErrCodeInternal, err.Error())
204 return
205 }
206
207 ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
208 defer cancel()
209
210 if err := p.ArchiveEmails(ctx, params.Folder, params.UIDs); err != nil {
211 conn.SendError(req.ID, daemonrpc.ErrCodeInternal, err.Error())
212 return
213 }
214 conn.SendResponse(req.ID, true)
215}
216
217func (d *Daemon) handleMoveEmails(conn *daemonrpc.Conn, req *daemonrpc.Request) {
218 params, err := decodeParams[daemonrpc.MoveEmailsParams](req)
219 if err != nil {
220 conn.SendError(req.ID, daemonrpc.ErrCodeParse, err.Error())
221 return
222 }
223
224 p, err := d.getProvider(params.AccountID)
225 if err != nil {
226 conn.SendError(req.ID, daemonrpc.ErrCodeInternal, err.Error())
227 return
228 }
229
230 ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
231 defer cancel()
232
233 if err := p.MoveEmails(ctx, params.UIDs, params.SourceFolder, params.DestFolder); err != nil {
234 conn.SendError(req.ID, daemonrpc.ErrCodeInternal, err.Error())
235 return
236 }
237 conn.SendResponse(req.ID, true)
238}
239
240func (d *Daemon) handleMarkRead(conn *daemonrpc.Conn, req *daemonrpc.Request) {
241 params, err := decodeParams[daemonrpc.MarkReadParams](req)
242 if err != nil {
243 conn.SendError(req.ID, daemonrpc.ErrCodeParse, err.Error())
244 return
245 }
246
247 p, err := d.getProvider(params.AccountID)
248 if err != nil {
249 conn.SendError(req.ID, daemonrpc.ErrCodeInternal, err.Error())
250 return
251 }
252
253 ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
254 defer cancel()
255
256 // MarkAsRead only supports one UID at a time in the Provider interface.
257 for _, uid := range params.UIDs {
258 if err := p.MarkAsRead(ctx, params.Folder, uid); err != nil {
259 log.Printf("daemon: mark read %d failed: %v", uid, err)
260 }
261 }
262 conn.SendResponse(req.ID, true)
263}
264
265func (d *Daemon) handleFetchFolders(conn *daemonrpc.Conn, req *daemonrpc.Request) {
266 params, err := decodeParams[daemonrpc.FetchFoldersParams](req)
267 if err != nil {
268 conn.SendError(req.ID, daemonrpc.ErrCodeParse, err.Error())
269 return
270 }
271
272 p, err := d.getProvider(params.AccountID)
273 if err != nil {
274 conn.SendError(req.ID, daemonrpc.ErrCodeInternal, err.Error())
275 return
276 }
277
278 ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
279 defer cancel()
280
281 folders, err := p.FetchFolders(ctx)
282 if err != nil {
283 conn.SendError(req.ID, daemonrpc.ErrCodeInternal, err.Error())
284 return
285 }
286 conn.SendResponse(req.ID, folders)
287}
288
289func (d *Daemon) handleRefreshFolder(conn *daemonrpc.Conn, req *daemonrpc.Request) {
290 params, err := decodeParams[daemonrpc.RefreshFolderParams](req)
291 if err != nil {
292 conn.SendError(req.ID, daemonrpc.ErrCodeParse, err.Error())
293 return
294 }
295
296 // Async: fetch in background, push events when done.
297 go func() {
298 p, err := d.getProvider(params.AccountID)
299 if err != nil {
300 log.Printf("daemon: refresh provider error: %v", err)
301 return
302 }
303
304 d.broadcastToSubscribers(params.AccountID, params.Folder, daemonrpc.EventSyncStarted, daemonrpc.SyncStartedEvent{
305 AccountID: params.AccountID,
306 Folder: params.Folder,
307 })
308
309 ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
310 defer cancel()
311
312 emails, err := p.FetchEmails(ctx, params.Folder, 50, 0)
313 if err != nil {
314 d.broadcastToSubscribers(params.AccountID, params.Folder, daemonrpc.EventSyncError, daemonrpc.SyncErrorEvent{
315 AccountID: params.AccountID,
316 Folder: params.Folder,
317 Error: err.Error(),
318 })
319 return
320 }
321
322 d.broadcastToSubscribers(params.AccountID, params.Folder, daemonrpc.EventSyncComplete, daemonrpc.SyncCompleteEvent{
323 AccountID: params.AccountID,
324 Folder: params.Folder,
325 EmailCount: len(emails),
326 })
327 }()
328
329 conn.SendResponse(req.ID, true)
330}
331
332func (d *Daemon) handleSubscribe(conn *daemonrpc.Conn, req *daemonrpc.Request) {
333 params, err := decodeParams[daemonrpc.SubscribeParams](req)
334 if err != nil {
335 conn.SendError(req.ID, daemonrpc.ErrCodeParse, err.Error())
336 return
337 }
338
339 key := params.AccountID + ":" + params.Folder
340
341 d.subMu.Lock()
342 if d.subscriptions[conn] == nil {
343 d.subscriptions[conn] = make(map[string]struct{})
344 }
345 d.subscriptions[conn][key] = struct{}{}
346 d.subMu.Unlock()
347
348 log.Printf("daemon: client subscribed to %s", key)
349 conn.SendResponse(req.ID, true)
350}
351
352func (d *Daemon) handleUnsubscribe(conn *daemonrpc.Conn, req *daemonrpc.Request) {
353 params, err := decodeParams[daemonrpc.UnsubscribeParams](req)
354 if err != nil {
355 conn.SendError(req.ID, daemonrpc.ErrCodeParse, err.Error())
356 return
357 }
358
359 key := params.AccountID + ":" + params.Folder
360
361 d.subMu.Lock()
362 if subs, ok := d.subscriptions[conn]; ok {
363 delete(subs, key)
364 }
365 d.subMu.Unlock()
366
367 conn.SendResponse(req.ID, true)
368}