package jmap
import (
"errors"
"net/url"
"time"
"git.sr.ht/~rjarry/aerc/config"
"git.sr.ht/~rjarry/aerc/lib/uidstore"
"git.sr.ht/~rjarry/aerc/models"
"git.sr.ht/~rjarry/aerc/worker/handlers"
"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/identity"
"git.sr.ht/~rockorager/go-jmap/mail/mailbox"
)
func init() {
handlers.RegisterWorkerFactory("jmap", NewJMAPWorker)
}
var (
errNoop error = errors.New("noop")
errUnsupported error = errors.New("unsupported")
)
type JMAPWorker struct {
config struct {
account *config.AccountConfig
endpoint string
oauth bool
user *url.Userinfo
cacheState bool
cacheBlobs bool
serverPing time.Duration
useLabels bool
allMail string
}
w *types.Worker
client *jmap.Client
cache *cache.JMAPCache
accountId jmap.ID
selectedMbox jmap.ID
dir2mbox map[string]jmap.ID
mbox2dir map[jmap.ID]string
roles map[mailbox.Role]jmap.ID
identities map[string]*identity.Identity
uidStore *uidstore.Store
changes chan jmap.TypeState
stop chan struct{}
}
func NewJMAPWorker(worker *types.Worker) (types.Backend, error) {
return &JMAPWorker{
w: worker,
uidStore: uidstore.NewStore(),
roles: make(map[mailbox.Role]jmap.ID),
dir2mbox: make(map[string]jmap.ID),
mbox2dir: make(map[jmap.ID]string),
identities: make(map[string]*identity.Identity),
changes: make(chan jmap.TypeState),
}, nil
}
func (w *JMAPWorker) addMbox(mbox *mailbox.Mailbox, dir string) {
w.mbox2dir[mbox.ID] = dir
w.dir2mbox[dir] = mbox.ID
w.roles[mbox.Role] = mbox.ID
}
func (w *JMAPWorker) deleteMbox(id jmap.ID) {
var dir string
var role mailbox.Role
delete(w.mbox2dir, id)
for d, i := range w.dir2mbox {
if i == id {
dir = d
break
}
}
delete(w.dir2mbox, dir)
for r, i := range w.roles {
if i == id {
role = r
break
}
}
delete(w.roles, role)
}
var capas = models.Capabilities{Sort: true, Thread: false}
func (w *JMAPWorker) Capabilities() *models.Capabilities {
return &capas
}
func (w *JMAPWorker) PathSeparator() string {
return "/"
}
func (w *JMAPWorker) handleMessage(msg types.WorkerMessage) error {
switch msg := msg.(type) {
case *types.Configure:
return w.handleConfigure(msg)
case *types.Connect:
if w.stop != nil {
return errors.New("already connected")
}
return w.handleConnect(msg)
case *types.Reconnect:
if w.stop == nil {
return errors.New("not connected")
}
close(w.stop)
return w.handleConnect(&types.Connect{Message: msg.Message})
case *types.Disconnect:
if w.stop == nil {
return errors.New("not connected")
}
close(w.stop)
return nil
case *types.ListDirectories:
return w.handleListDirectories(msg)
case *types.OpenDirectory:
return w.handleOpenDirectory(msg)
case *types.FetchDirectoryContents:
return w.handleFetchDirectoryContents(msg)
case *types.SearchDirectory:
return w.handleSearchDirectory(msg)
case *types.CreateDirectory:
return w.handleCreateDirectory(msg)
case *types.RemoveDirectory:
return w.handleRemoveDirectory(msg)
case *types.FetchMessageHeaders:
return w.handleFetchMessageHeaders(msg)
case *types.FetchMessageBodyPart:
return w.handleFetchMessageBodyPart(msg)
case *types.FetchFullMessages:
return w.handleFetchFullMessages(msg)
case *types.FlagMessages:
return w.updateFlags(msg.Uids, msg.Flags, msg.Enable)
case *types.AnsweredMessages:
return w.updateFlags(msg.Uids, models.AnsweredFlag, msg.Answered)
case *types.DeleteMessages:
return w.moveCopy(msg.Uids, "", true)
case *types.CopyMessages:
return w.moveCopy(msg.Uids, msg.Destination, false)
case *types.MoveMessages:
return w.moveCopy(msg.Uids, msg.Destination, true)
case *types.ModifyLabels:
if w.config.useLabels {
return w.handleModifyLabels(msg)
}
case *types.AppendMessage:
return w.handleAppendMessage(msg)
case *types.StartSendingMessage:
return w.handleStartSend(msg)
}
return errUnsupported
}
func (w *JMAPWorker) Run() {
for {
select {
case change := <-w.changes:
err := w.refresh(change)
if err != nil {
w.w.Errorf("refresh: %s", err)
}
case msg := <-w.w.Actions():
msg = w.w.ProcessAction(msg)
err := w.handleMessage(msg)
switch {
case errors.Is(err, errNoop):
// Operation did not have any effect.
// Do *NOT* send a Done message.
break
case errors.Is(err, errUnsupported):
w.w.PostMessage(&types.Unsupported{
Message: types.RespondTo(msg),
}, nil)
case err != nil:
w.w.PostMessage(&types.Error{
Message: types.RespondTo(msg),
Error: err,
}, nil)
default: // err == nil
// Operation is finished.
// Send a Done message.
w.w.PostMessage(&types.Done{
Message: types.RespondTo(msg),
}, nil)
}
}
}
}