aboutsummaryrefslogtreecommitdiffstats
path: root/worker/jmap/directories.go
diff options
context:
space:
mode:
Diffstat (limited to 'worker/jmap/directories.go')
-rw-r--r--worker/jmap/directories.go360
1 files changed, 360 insertions, 0 deletions
diff --git a/worker/jmap/directories.go b/worker/jmap/directories.go
new file mode 100644
index 00000000..b3297169
--- /dev/null
+++ b/worker/jmap/directories.go
@@ -0,0 +1,360 @@
+package jmap
+
+import (
+ "errors"
+ "fmt"
+ "path"
+ "sort"
+
+ "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/mail/email"
+ "git.sr.ht/~rockorager/go-jmap/mail/mailbox"
+)
+
+func (w *JMAPWorker) handleListDirectories(msg *types.ListDirectories) error {
+ var ids, missing []jmap.ID
+ var labels []string
+ var mboxes map[jmap.ID]*mailbox.Mailbox
+
+ mboxes = make(map[jmap.ID]*mailbox.Mailbox)
+
+ mboxIds, err := w.cache.GetMailboxList()
+ if err == nil {
+ for _, id := range mboxIds {
+ mbox, err := w.cache.GetMailbox(id)
+ if err != nil {
+ w.w.Warnf("GetMailbox: %s", err)
+ missing = append(missing, id)
+ continue
+ }
+ mboxes[id] = mbox
+ ids = append(ids, id)
+ }
+ }
+
+ if err != nil || len(missing) > 0 {
+ var req jmap.Request
+
+ req.Invoke(&mailbox.Get{Account: w.accountId})
+ resp, err := w.Do(&req)
+ if err != nil {
+ return err
+ }
+
+ mboxes = make(map[jmap.ID]*mailbox.Mailbox)
+ ids = make([]jmap.ID, 0)
+
+ for _, inv := range resp.Responses {
+ switch r := inv.Args.(type) {
+ case *mailbox.GetResponse:
+ for _, mbox := range r.List {
+ mboxes[mbox.ID] = mbox
+ ids = append(ids, mbox.ID)
+ err = w.cache.PutMailbox(mbox.ID, mbox)
+ if err != nil {
+ w.w.Warnf("PutMailbox: %s", err)
+ }
+ }
+ err = w.cache.PutMailboxList(ids)
+ if err != nil {
+ w.w.Warnf("PutMailboxList: %s", err)
+ }
+ err = w.cache.PutMailboxState(r.State)
+ if err != nil {
+ w.w.Warnf("PutMailboxState: %s", err)
+ }
+ case *jmap.MethodError:
+ return wrapMethodError(r)
+ }
+ }
+ }
+
+ if len(mboxes) == 0 {
+ return errors.New("no mailboxes")
+ }
+
+ for _, mbox := range mboxes {
+ dir := w.MailboxPath(mbox)
+ w.addMbox(mbox, dir)
+ labels = append(labels, dir)
+ }
+ if w.config.useLabels {
+ sort.Strings(labels)
+ w.w.PostMessage(&types.LabelList{Labels: labels}, nil)
+ }
+
+ for _, id := range ids {
+ mbox := mboxes[id]
+ if mbox.Role == mailbox.RoleArchive && w.config.useLabels {
+ // replace archive with virtual all-mail folder
+ mbox = &mailbox.Mailbox{
+ Name: w.config.allMail,
+ Role: mailbox.RoleAll,
+ }
+ w.addMbox(mbox, mbox.Name)
+ }
+ w.w.PostMessage(&types.Directory{
+ Message: types.RespondTo(msg),
+ Dir: &models.Directory{
+ Name: w.mbox2dir[mbox.ID],
+ Exists: int(mbox.TotalEmails),
+ Unseen: int(mbox.UnreadEmails),
+ Role: jmapRole2aerc[mbox.Role],
+ },
+ }, nil)
+ }
+
+ go w.monitorChanges()
+
+ return nil
+}
+
+func (w *JMAPWorker) handleOpenDirectory(msg *types.OpenDirectory) error {
+ id, ok := w.dir2mbox[msg.Directory]
+ if !ok {
+ return fmt.Errorf("unknown directory: %s", msg.Directory)
+ }
+ w.selectedMbox = id
+ return nil
+}
+
+func (w *JMAPWorker) handleFetchDirectoryContents(msg *types.FetchDirectoryContents) error {
+ contents, err := w.cache.GetFolderContents(w.selectedMbox)
+ if err != nil {
+ contents = &cache.FolderContents{
+ MailboxID: w.selectedMbox,
+ Filter: &email.FilterCondition{},
+ }
+ }
+
+ filter, err := parseSearch(msg.FilterCriteria)
+ if err != nil {
+ return err
+ }
+ filter.InMailbox = w.selectedMbox
+
+ sort := translateSort(msg.SortCriteria)
+
+ if contents.NeedsRefresh(filter, sort) {
+ var req jmap.Request
+
+ req.Invoke(&email.Query{
+ Account: w.accountId,
+ Filter: filter,
+ Sort: sort,
+ })
+ resp, err := w.Do(&req)
+ if err != nil {
+ return err
+ }
+ var canCalculateChanges bool
+ for _, inv := range resp.Responses {
+ switch r := inv.Args.(type) {
+ case *email.QueryResponse:
+ contents.Sort = sort
+ contents.Filter = filter
+ contents.QueryState = r.QueryState
+ contents.MessageIDs = r.IDs
+ canCalculateChanges = r.CanCalculateChanges
+ case *jmap.MethodError:
+ return wrapMethodError(r)
+ }
+ }
+ if canCalculateChanges {
+ err = w.cache.PutFolderContents(w.selectedMbox, contents)
+ if err != nil {
+ w.w.Warnf("PutFolderContents: %s", err)
+ }
+ } else {
+ w.w.Debugf("%q: server cannot calculate changes, flushing cache",
+ w.mbox2dir[w.selectedMbox])
+ err = w.cache.DeleteFolderContents(w.selectedMbox)
+ if err != nil {
+ w.w.Warnf("DeleteFolderContents: %s", err)
+ }
+ }
+ }
+
+ uids := make([]uint32, 0, len(contents.MessageIDs))
+ for _, id := range contents.MessageIDs {
+ uids = append(uids, w.uidStore.GetOrInsert(string(id)))
+ }
+ w.w.PostMessage(&types.DirectoryContents{
+ Message: types.RespondTo(msg),
+ Uids: uids,
+ }, nil)
+
+ return nil
+}
+
+func (w *JMAPWorker) handleSearchDirectory(msg *types.SearchDirectory) error {
+ var req jmap.Request
+
+ filter, err := parseSearch(msg.Argv)
+ if err != nil {
+ return err
+ }
+ if w.selectedMbox == "" {
+ // all mail virtual folder: display all but trash and spam
+ var mboxes []jmap.ID
+ if id, ok := w.roles[mailbox.RoleJunk]; ok {
+ mboxes = append(mboxes, id)
+ }
+ if id, ok := w.roles[mailbox.RoleTrash]; ok {
+ mboxes = append(mboxes, id)
+ }
+ filter.InMailboxOtherThan = mboxes
+ } else {
+ filter.InMailbox = w.selectedMbox
+ }
+
+ req.Invoke(&email.Query{
+ Account: w.accountId,
+ Filter: filter,
+ })
+
+ resp, err := w.Do(&req)
+ if err != nil {
+ return err
+ }
+
+ for _, inv := range resp.Responses {
+ switch r := inv.Args.(type) {
+ case *email.QueryResponse:
+ var uids []uint32
+ for _, id := range r.IDs {
+ uids = append(uids, w.uidStore.GetOrInsert(string(id)))
+ }
+ w.w.PostMessage(&types.SearchResults{
+ Message: types.RespondTo(msg),
+ Uids: uids,
+ }, nil)
+ case *jmap.MethodError:
+ return wrapMethodError(r)
+ }
+ }
+
+ return nil
+}
+
+func (w *JMAPWorker) handleCreateDirectory(msg *types.CreateDirectory) error {
+ var req jmap.Request
+ var parentId, id jmap.ID
+
+ if _, ok := w.dir2mbox[msg.Directory]; ok {
+ // directory already exists
+ return nil
+ }
+ if parent := path.Dir(msg.Directory); parent != "" && parent != "." {
+ var ok bool
+ if parentId, ok = w.dir2mbox[parent]; !ok {
+ return fmt.Errorf(
+ "parent mailbox %q does not exist", parent)
+ }
+ }
+ name := path.Base(msg.Directory)
+ id = jmap.ID(msg.Directory)
+
+ req.Invoke(&mailbox.Set{
+ Account: w.accountId,
+ Create: map[jmap.ID]*mailbox.Mailbox{
+ id: {
+ ParentID: parentId,
+ Name: name,
+ },
+ },
+ })
+
+ resp, err := w.Do(&req)
+ if err != nil {
+ return err
+ }
+ for _, inv := range resp.Responses {
+ switch r := inv.Args.(type) {
+ case *mailbox.SetResponse:
+ if err := r.NotCreated[id]; err != nil {
+ e := wrapSetError(err)
+ if msg.Quiet {
+ w.w.Warnf("mailbox creation failed: %s", e)
+ } else {
+ return e
+ }
+ }
+ case *jmap.MethodError:
+ return wrapMethodError(r)
+ }
+ }
+
+ return nil
+}
+
+func (w *JMAPWorker) handleRemoveDirectory(msg *types.RemoveDirectory) error {
+ var req jmap.Request
+
+ id, ok := w.dir2mbox[msg.Directory]
+ if !ok {
+ return fmt.Errorf("unknown mailbox: %s", msg.Directory)
+ }
+
+ req.Invoke(&mailbox.Set{
+ Account: w.accountId,
+ Destroy: []jmap.ID{id},
+ OnDestroyRemoveEmails: msg.Quiet,
+ })
+
+ resp, err := w.Do(&req)
+ if err != nil {
+ return err
+ }
+ for _, inv := range resp.Responses {
+ switch r := inv.Args.(type) {
+ case *mailbox.SetResponse:
+ if err := r.NotDestroyed[id]; err != nil {
+ return wrapSetError(err)
+ }
+ case *jmap.MethodError:
+ return wrapMethodError(r)
+ }
+ }
+
+ return nil
+}
+
+func translateSort(criteria []*types.SortCriterion) []*email.SortComparator {
+ sort := make([]*email.SortComparator, 0, len(criteria))
+ if len(criteria) == 0 {
+ criteria = []*types.SortCriterion{
+ {Field: types.SortArrival, Reverse: true},
+ }
+ }
+ for _, s := range criteria {
+ var cmp email.SortComparator
+ switch s.Field {
+ case types.SortArrival:
+ cmp.Property = "receivedAt"
+ case types.SortCc:
+ cmp.Property = "cc"
+ case types.SortDate:
+ cmp.Property = "receivedAt"
+ case types.SortFrom:
+ cmp.Property = "from"
+ case types.SortRead:
+ cmp.Keyword = "$seen"
+ case types.SortSize:
+ cmp.Property = "size"
+ case types.SortSubject:
+ cmp.Property = "subject"
+ case types.SortTo:
+ cmp.Property = "to"
+ default:
+ continue
+ }
+ cmp.IsAscending = !s.Reverse
+ sort = append(sort, &cmp)
+ }
+
+ return sort
+}