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
}