aboutsummaryrefslogblamecommitdiffstats
path: root/worker/jmap/push.go
blob: f2a8ad4572d48232f059d5a6b1c3bff53947e6c4 (plain) (tree)
1
2
3
4
5
6
7
8






              
                                        









                                                    

                                



























                                                                          
                                               















                                                                
                                                  


                                              
                                               






                                                            
                                               






























                                                                           

                          

                                                                
                                                  

                                               
                                                     
                                                  







                                                            









                                                            





                                                                      
                                                               

                                                                                        







































































































                                                                                  
                                                                       
                                                        
                                                                           






                                                                         







                                                                                
                                 
 











                                                                                    
                                               
                                                                           
                                 









                                                                               
                                                                           
                                                           

                                               



                                                                           

















                                                                                  


                                                                           















































                                                                               
package jmap

import (
	"fmt"
	"sort"
	"time"

	"git.sr.ht/~rjarry/aerc/lib/log"
	"git.sr.ht/~rjarry/aerc/models"
	"git.sr.ht/~rjarry/aerc/worker/jmap/cache"
	"git.sr.ht/~rjarry/aerc/worker/types"
	"git.sr.ht/~rockorager/go-jmap"
	"git.sr.ht/~rockorager/go-jmap/core/push"
	"git.sr.ht/~rockorager/go-jmap/mail/email"
	"git.sr.ht/~rockorager/go-jmap/mail/mailbox"
)

func (w *JMAPWorker) monitorChanges() {
	defer log.PanicHandler()

	events := push.EventSource{
		Client:  w.client,
		Handler: w.handleChange,
		Ping:    uint(w.config.serverPing.Seconds()),
	}

	w.stop = make(chan struct{})
	go func() {
		defer log.PanicHandler()
		<-w.stop
		w.w.Errorf("listen stopping")
		w.stop = nil
		events.Close()
	}()

	for w.stop != nil {
		w.w.Debugf("listening for changes")
		err := events.Listen()
		if err != nil {
			w.w.PostMessage(&types.Error{
				Error: fmt.Errorf("jmap listen: %w", err),
			}, nil)
			time.Sleep(5 * time.Second)
		}
	}
}

func (w *JMAPWorker) handleChange(s *jmap.StateChange) {
	changed, ok := s.Changed[w.AccountId()]
	if !ok {
		return
	}
	w.w.Debugf("state change %#v", changed)
	w.changes <- changed
}

func (w *JMAPWorker) refresh(newState jmap.TypeState) error {
	var req jmap.Request

	mboxState, err := w.cache.GetMailboxState()
	if err != nil {
		w.w.Debugf("GetMailboxState: %s", err)
	}
	if mboxState != "" && newState["Mailbox"] != mboxState {
		callID := req.Invoke(&mailbox.Changes{
			Account:    w.AccountId(),
			SinceState: mboxState,
		})
		req.Invoke(&mailbox.Get{
			Account: w.AccountId(),
			ReferenceIDs: &jmap.ResultReference{
				ResultOf: callID,
				Name:     "Mailbox/changes",
				Path:     "/created",
			},
		})
		req.Invoke(&mailbox.Get{
			Account: w.AccountId(),
			ReferenceIDs: &jmap.ResultReference{
				ResultOf: callID,
				Name:     "Mailbox/changes",
				Path:     "/updated",
			},
		})
	}

	emailState, err := w.cache.GetEmailState()
	if err != nil {
		w.w.Debugf("GetEmailState: %s", err)
	}
	queryChangesCalls := make(map[string]jmap.ID)
	folderContents := make(map[jmap.ID]*cache.FolderContents)
	ids, _ := w.cache.GetMailboxList()
	mboxes := make(map[jmap.ID]*mailbox.Mailbox)
	for _, id := range ids {
		mbox, err := w.cache.GetMailbox(id)
		if err != nil {
			w.w.Warnf("GetMailbox: %s", err)
			continue
		}
		if mbox.Role == mailbox.RoleArchive && w.config.useLabels {
			mboxes[""] = &mailbox.Mailbox{
				Name: w.config.allMail,
				Role: mailbox.RoleAll,
			}
		} else {
			mboxes[id] = mbox
		}
	}
	emailUpdated := ""
	emailCreated := ""
	if emailState != "" && newState["Email"] != emailState {
		callID := req.Invoke(&email.Changes{
			Account:    w.AccountId(),
			SinceState: emailState,
		})
		emailUpdated = req.Invoke(&email.Get{
			Account:    w.AccountId(),
			Properties: headersProperties,
			ReferenceIDs: &jmap.ResultReference{
				ResultOf: callID,
				Name:     "Email/changes",
				Path:     "/updated",
			},
		})

		emailCreated = req.Invoke(&email.Get{
			Account:    w.AccountId(),
			Properties: headersProperties,
			ReferenceIDs: &jmap.ResultReference{
				ResultOf: callID,
				Name:     "Email/changes",
				Path:     "/created",
			},
		})

		for id := range mboxes {
			contents, err := w.cache.GetFolderContents(id)
			if err != nil {
				continue
			}
			callID = req.Invoke(&email.QueryChanges{
				Account:         w.AccountId(),
				Filter:          w.translateSearch(id, contents.Filter),
				Sort:            translateSort(contents.Sort),
				SinceQueryState: contents.QueryState,
			})
			queryChangesCalls[callID] = id
			folderContents[id] = contents
		}
	}

	if len(req.Calls) == 0 {
		return nil
	}

	resp, err := w.Do(&req)
	if err != nil {
		return err
	}

	var changedMboxIds []jmap.ID
	var labelsChanged bool

	for _, inv := range resp.Responses {
		switch r := inv.Args.(type) {
		case *mailbox.ChangesResponse:
			for _, id := range r.Destroyed {
				dir, ok := w.mbox2dir[id]
				if ok {
					w.w.PostMessage(&types.RemoveDirectory{
						Directory: dir,
					}, nil)
				}
				w.deleteMbox(id)
				err = w.cache.DeleteMailbox(id)
				if err != nil {
					w.w.Warnf("DeleteMailbox: %s", err)
				}
				labelsChanged = true
			}
			err = w.cache.PutMailboxState(r.NewState)
			if err != nil {
				w.w.Warnf("PutMailboxState: %s", err)
			}

		case *mailbox.GetResponse:
			for _, mbox := range r.List {
				changedMboxIds = append(changedMboxIds, mbox.ID)
				mboxes[mbox.ID] = mbox
				err = w.cache.PutMailbox(mbox.ID, mbox)
				if err != nil {
					w.w.Warnf("PutMailbox: %s", err)
				}
			}
			err = w.cache.PutMailboxState(r.State)
			if err != nil {
				w.w.Warnf("PutMailboxState: %s", err)
			}

		case *email.QueryChangesResponse:
			mboxId := queryChangesCalls[inv.CallID]
			contents := folderContents[mboxId]

			removed := make(map[jmap.ID]bool)
			for _, id := range r.Removed {
				removed[id] = true
			}
			added := make(map[int]jmap.ID)
			for _, add := range r.Added {
				added[int(add.Index)] = add.ID
			}
			w.w.Debugf("%q: %d added, %d removed",
				w.mbox2dir[mboxId], len(added), len(removed))
			n := len(contents.MessageIDs) - len(removed) + len(added)
			if n < 0 {
				w.w.Errorf("bug: invalid folder contents state")
				err = w.cache.DeleteFolderContents(mboxId)
				if err != nil {
					w.w.Warnf("DeleteFolderContents: %s", err)
				}
				continue
			}
			ids = make([]jmap.ID, 0, n)
			i := 0
			for _, id := range contents.MessageIDs {
				if removed[id] {
					continue
				}
				if addedId, ok := added[i]; ok {
					ids = append(ids, addedId)
					delete(added, i)
					i += 1
				}
				ids = append(ids, id)
				i += 1
			}
			for _, id := range added {
				ids = append(ids, id)
			}
			contents.MessageIDs = ids
			contents.QueryState = r.NewQueryState

			err = w.cache.PutFolderContents(mboxId, contents)
			if err != nil {
				w.w.Warnf("PutFolderContents: %s", err)
			}

			if w.selectedMbox == mboxId {
				uids := make([]models.UID, 0, len(ids))
				for _, id := range ids {
					uids = append(uids, models.UID(id))
				}
				w.w.PostMessage(&types.DirectoryContents{
					Uids: uids,
				}, nil)
			}

		case *email.GetResponse:
			switch inv.CallID {
			case emailUpdated:
				selectedIds := make(map[jmap.ID]bool)
				contents, ok := folderContents[w.selectedMbox]
				if ok {
					for _, id := range contents.MessageIDs {
						selectedIds[id] = true
					}
				}

				for _, m := range r.List {
					err = w.cache.PutEmail(m.ID, m)
					if err != nil {
						w.w.Warnf("PutEmail: %s", err)
					}
					if selectedIds[m.ID] {
						w.w.PostMessage(&types.MessageInfo{
							Info: w.translateMsgInfo(m),
						}, nil)
					}
				}
				err = w.cache.PutEmailState(r.State)
				if err != nil {
					w.w.Warnf("PutEmailState: %s", err)
				}
			case emailCreated:
				for _, m := range r.List {
					err = w.cache.PutEmail(m.ID, m)
					if err != nil {
						w.w.Warnf("PutEmail: %s", err)
					}
					info := w.translateMsgInfo(m)
					// Set recent on created messages so we
					// get a notification
					info.Flags |= models.RecentFlag
					w.w.PostMessage(&types.MessageInfo{
						Info: info,
					}, nil)
				}
				err = w.cache.PutEmailState(r.State)
				if err != nil {
					w.w.Warnf("PutEmailState: %s", err)
				}
			}

		case *jmap.MethodError:
			w.w.Errorf("%s: %s", wrapMethodError(r))
			if inv.Name == "Email/queryChanges" {
				id := queryChangesCalls[inv.CallID]
				w.w.Infof("flushing %q contents from cache",
					w.mbox2dir[id])
				err := w.cache.DeleteFolderContents(id)
				if err != nil {
					w.w.Warnf("DeleteFolderContents: %s", err)
				}
			}
		}
	}

	for _, id := range changedMboxIds {
		mbox := mboxes[id]
		if mbox.Role == mailbox.RoleArchive && w.config.useLabels {
			continue
		}
		newDir := w.MailboxPath(mbox)
		dir, ok := w.mbox2dir[id]
		if ok {
			// updated
			if newDir == dir {
				w.deleteMbox(id)
				w.addMbox(mbox, dir)
				w.w.PostMessage(&types.DirectoryInfo{
					Info: &models.DirectoryInfo{
						Name:   dir,
						Exists: int(mbox.TotalEmails),
						Unseen: int(mbox.UnreadEmails),
					},
				}, nil)
				continue
			} else {
				// renamed mailbox
				w.deleteMbox(id)
				w.w.PostMessage(&types.RemoveDirectory{
					Directory: dir,
				}, nil)
				dir = newDir
			}
		}
		// new mailbox
		w.addMbox(mbox, dir)
		w.w.PostMessage(&types.Directory{
			Dir: &models.Directory{
				Name:   dir,
				Exists: int(mbox.TotalEmails),
				Unseen: int(mbox.UnreadEmails),
				Role:   jmapRole2aerc[mbox.Role],
			},
		}, nil)
		labelsChanged = true
	}

	if w.config.useLabels && labelsChanged {
		labels := make([]string, 0, len(w.dir2mbox))
		for dir := range w.dir2mbox {
			labels = append(labels, dir)
		}
		sort.Strings(labels)
		w.w.PostMessage(&types.LabelList{Labels: labels}, nil)
	}

	return nil
}