aboutsummaryrefslogblamecommitdiffstats
path: root/worker/jmap/directories.go
blob: 07bb0762e938fc241666f562b0fa6ede93765263 (plain) (tree)






















                                                                              








                                                                           
                                                     
















                                                                           
                         


                 
                                                        

                                    
                                                                






















































































                                                                                            


                 
                                                                


                                        
                                               

                                                                               








                                                     

                                                                




































                                                                                         
                                
                                       
                                                                         





























                                                                                       
                                                    
                                           






                                                                           












                                                                             
                                       







































                                                                                   
                                                     


















































                                                                             
                                           




                                         
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)

	currentMailboxState, err := w.getMailboxState()
	if err != nil {
		return err
	}

	// If we can't get the cached mailbox state, at worst, we will just
	// query information we might already know
	cachedMailboxState, err := w.cache.GetMailboxState()
	if err != nil {
		w.w.Warnf("GetMailboxState: %s", err)
	}

	consistentMailboxState := currentMailboxState == cachedMailboxState

	// If we have a consistent state, check the cache
	if consistentMailboxState {
		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 !consistentMailboxState || 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,
		}
	}

	if contents.NeedsRefresh(msg.Filter, msg.SortCriteria) {
		var req jmap.Request

		req.Invoke(&email.Query{
			Account: w.AccountId(),
			Filter:  w.translateSearch(w.selectedMbox, msg.Filter),
			Sort:    translateSort(msg.SortCriteria),
		})
		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 = msg.SortCriteria
				contents.Filter = msg.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

	req.Invoke(&email.Query{
		Account: w.AccountId(),
		Filter:  w.translateSearch(w.selectedMbox, msg.Criteria),
	})

	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 id, ok := w.dir2mbox[msg.Directory]; ok {
		// directory already exists
		mbox, err := w.cache.GetMailbox(id)
		if err != nil {
			return err
		}
		if mbox.Role == mailbox.RoleArchive && w.config.useLabels {
			return errNoop
		}
		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
}