handler.go

  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, &params); 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}