init: pristine aerc 0.20.0 source
This commit is contained in:
Vendored
+45
@@ -0,0 +1,45 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"git.sr.ht/~rockorager/go-jmap"
|
||||
)
|
||||
|
||||
func (c *JMAPCache) GetBlob(id jmap.ID) ([]byte, error) {
|
||||
fpath := c.blobPath(id)
|
||||
if fpath == "" {
|
||||
return nil, notfound
|
||||
}
|
||||
return os.ReadFile(fpath)
|
||||
}
|
||||
|
||||
func (c *JMAPCache) PutBlob(id jmap.ID, buf []byte) error {
|
||||
fpath := c.blobPath(id)
|
||||
if fpath == "" {
|
||||
return nil
|
||||
}
|
||||
_ = os.MkdirAll(path.Dir(fpath), 0o700)
|
||||
return os.WriteFile(fpath, buf, 0o600)
|
||||
}
|
||||
|
||||
func (c *JMAPCache) DeleteBlob(id jmap.ID) error {
|
||||
fpath := c.blobPath(id)
|
||||
if fpath == "" {
|
||||
return nil
|
||||
}
|
||||
defer func() {
|
||||
_ = os.Remove(path.Dir(fpath))
|
||||
}()
|
||||
return os.Remove(fpath)
|
||||
}
|
||||
|
||||
func (c *JMAPCache) blobPath(id jmap.ID) string {
|
||||
if c.blobsDir == "" || id == "" {
|
||||
return ""
|
||||
}
|
||||
name := string(id)
|
||||
sub := name[len(name)-2:]
|
||||
return path.Join(c.blobsDir, sub, name)
|
||||
}
|
||||
Vendored
+109
@@ -0,0 +1,109 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"git.sr.ht/~rjarry/aerc/lib/log"
|
||||
"git.sr.ht/~rjarry/aerc/lib/xdg"
|
||||
"github.com/syndtr/goleveldb/leveldb"
|
||||
"github.com/syndtr/goleveldb/leveldb/util"
|
||||
)
|
||||
|
||||
type JMAPCache struct {
|
||||
mem map[string][]byte
|
||||
file *leveldb.DB
|
||||
blobsDir string
|
||||
}
|
||||
|
||||
func NewJMAPCache(state, blobs bool, accountName string) *JMAPCache {
|
||||
c := new(JMAPCache)
|
||||
cacheDir := xdg.CachePath()
|
||||
if state && cacheDir != "" {
|
||||
var err error
|
||||
dir := path.Join(cacheDir, "aerc", accountName, "state")
|
||||
_ = os.MkdirAll(dir, 0o700)
|
||||
c.file, err = leveldb.OpenFile(dir, nil)
|
||||
if err != nil {
|
||||
log.Errorf("failed to open goleveldb: %s", err)
|
||||
c.mem = make(map[string][]byte)
|
||||
}
|
||||
} else {
|
||||
c.mem = make(map[string][]byte)
|
||||
}
|
||||
if blobs && cacheDir != "" {
|
||||
c.blobsDir = path.Join(cacheDir, "aerc", accountName, "blobs")
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
var notfound = errors.New("key not found")
|
||||
|
||||
func (c *JMAPCache) get(key string) ([]byte, error) {
|
||||
switch {
|
||||
case c.file != nil:
|
||||
return c.file.Get([]byte(key), nil)
|
||||
case c.mem != nil:
|
||||
value, ok := c.mem[key]
|
||||
if !ok {
|
||||
return nil, notfound
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
panic("jmap cache with no backend")
|
||||
}
|
||||
|
||||
func (c *JMAPCache) put(key string, value []byte) error {
|
||||
switch {
|
||||
case c.file != nil:
|
||||
return c.file.Put([]byte(key), value, nil)
|
||||
case c.mem != nil:
|
||||
c.mem[key] = value
|
||||
return nil
|
||||
}
|
||||
panic("jmap cache with no backend")
|
||||
}
|
||||
|
||||
func (c *JMAPCache) delete(key string) error {
|
||||
switch {
|
||||
case c.file != nil:
|
||||
return c.file.Delete([]byte(key), nil)
|
||||
case c.mem != nil:
|
||||
delete(c.mem, key)
|
||||
return nil
|
||||
}
|
||||
panic("jmap cache with no backend")
|
||||
}
|
||||
|
||||
func (c *JMAPCache) purge(prefix string) error {
|
||||
switch {
|
||||
case c.file != nil:
|
||||
txn, err := c.file.OpenTransaction()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
iter := txn.NewIterator(util.BytesPrefix([]byte(prefix)), nil)
|
||||
for iter.Next() {
|
||||
err = txn.Delete(iter.Key(), nil)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
iter.Release()
|
||||
if err != nil {
|
||||
txn.Discard()
|
||||
return err
|
||||
}
|
||||
return txn.Commit()
|
||||
case c.mem != nil:
|
||||
for key := range c.mem {
|
||||
if strings.HasPrefix(key, prefix) {
|
||||
delete(c.mem, key)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
panic("jmap cache with no backend")
|
||||
}
|
||||
Vendored
+40
@@ -0,0 +1,40 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"git.sr.ht/~rockorager/go-jmap"
|
||||
"git.sr.ht/~rockorager/go-jmap/mail/email"
|
||||
)
|
||||
|
||||
func (c *JMAPCache) HasEmail(id jmap.ID) bool {
|
||||
_, err := c.get(emailKey(id))
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (c *JMAPCache) GetEmail(id jmap.ID) (*email.Email, error) {
|
||||
buf, err := c.get(emailKey(id))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
e := new(email.Email)
|
||||
err = unmarshal(buf, e)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return e, nil
|
||||
}
|
||||
|
||||
func (c *JMAPCache) PutEmail(id jmap.ID, e *email.Email) error {
|
||||
buf, err := marshal(e)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.put(emailKey(id), buf)
|
||||
}
|
||||
|
||||
func (c *JMAPCache) DeleteEmail(id jmap.ID) error {
|
||||
return c.delete(emailKey(id))
|
||||
}
|
||||
|
||||
func emailKey(id jmap.ID) string {
|
||||
return "email/" + string(id)
|
||||
}
|
||||
Vendored
+59
@@ -0,0 +1,59 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
|
||||
"git.sr.ht/~rjarry/aerc/lib/log"
|
||||
"git.sr.ht/~rjarry/aerc/worker/types"
|
||||
"git.sr.ht/~rockorager/go-jmap"
|
||||
)
|
||||
|
||||
type FolderContents struct {
|
||||
MailboxID jmap.ID
|
||||
QueryState string
|
||||
Filter *types.SearchCriteria
|
||||
Sort []*types.SortCriterion
|
||||
MessageIDs []jmap.ID
|
||||
}
|
||||
|
||||
func (c *JMAPCache) GetFolderContents(mailboxId jmap.ID) (*FolderContents, error) {
|
||||
key := folderContentsKey(mailboxId)
|
||||
buf, err := c.get(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m := new(FolderContents)
|
||||
err = unmarshal(buf, m)
|
||||
if err != nil {
|
||||
log.Debugf("cache format has changed, purging foldercontents")
|
||||
if e := c.purge("foldercontents/"); e != nil {
|
||||
log.Errorf("foldercontents cache purge: %s", e)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (c *JMAPCache) PutFolderContents(mailboxId jmap.ID, m *FolderContents) error {
|
||||
buf, err := marshal(m)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.put(folderContentsKey(mailboxId), buf)
|
||||
}
|
||||
|
||||
func (c *JMAPCache) DeleteFolderContents(mailboxId jmap.ID) error {
|
||||
return c.delete(folderContentsKey(mailboxId))
|
||||
}
|
||||
|
||||
func folderContentsKey(mailboxId jmap.ID) string {
|
||||
return "foldercontents/" + string(mailboxId)
|
||||
}
|
||||
|
||||
func (f *FolderContents) NeedsRefresh(
|
||||
filter *types.SearchCriteria, sort []*types.SortCriterion,
|
||||
) bool {
|
||||
return f.QueryState == "" ||
|
||||
!reflect.DeepEqual(f.Sort, sort) ||
|
||||
!reflect.DeepEqual(f.Filter, filter)
|
||||
}
|
||||
Vendored
+33
@@ -0,0 +1,33 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/gob"
|
||||
|
||||
"git.sr.ht/~rockorager/go-jmap/mail/email"
|
||||
"git.sr.ht/~rockorager/go-jmap/mail/mailbox"
|
||||
)
|
||||
|
||||
type jmapObject interface {
|
||||
*email.Email |
|
||||
*email.QueryResponse |
|
||||
*mailbox.Mailbox |
|
||||
*FolderContents |
|
||||
*IDList
|
||||
}
|
||||
|
||||
func marshal[T jmapObject](obj T) ([]byte, error) {
|
||||
buf := bytes.NewBuffer(nil)
|
||||
encoder := gob.NewEncoder(buf)
|
||||
err := encoder.Encode(obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func unmarshal[T jmapObject](data []byte, obj T) error {
|
||||
buf := bytes.NewBuffer(data)
|
||||
decoder := gob.NewDecoder(buf)
|
||||
return decoder.Decode(obj)
|
||||
}
|
||||
Vendored
+35
@@ -0,0 +1,35 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"git.sr.ht/~rockorager/go-jmap"
|
||||
"git.sr.ht/~rockorager/go-jmap/mail/mailbox"
|
||||
)
|
||||
|
||||
func (c *JMAPCache) GetMailbox(id jmap.ID) (*mailbox.Mailbox, error) {
|
||||
buf, err := c.get(mailboxKey(id))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m := new(mailbox.Mailbox)
|
||||
err = unmarshal(buf, m)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (c *JMAPCache) PutMailbox(id jmap.ID, m *mailbox.Mailbox) error {
|
||||
buf, err := marshal(m)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.put(mailboxKey(id), buf)
|
||||
}
|
||||
|
||||
func (c *JMAPCache) DeleteMailbox(id jmap.ID) error {
|
||||
return c.delete(mailboxKey(id))
|
||||
}
|
||||
|
||||
func mailboxKey(id jmap.ID) string {
|
||||
return "mailbox/" + string(id)
|
||||
}
|
||||
Vendored
+32
@@ -0,0 +1,32 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"git.sr.ht/~rockorager/go-jmap"
|
||||
)
|
||||
|
||||
type IDList struct {
|
||||
IDs []jmap.ID
|
||||
}
|
||||
|
||||
func (c *JMAPCache) GetMailboxList() ([]jmap.ID, error) {
|
||||
buf, err := c.get(mailboxListKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var list IDList
|
||||
err = unmarshal(buf, &list)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return list.IDs, nil
|
||||
}
|
||||
|
||||
func (c *JMAPCache) PutMailboxList(list []jmap.ID) error {
|
||||
buf, err := marshal(&IDList{IDs: list})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.put(mailboxListKey, buf)
|
||||
}
|
||||
|
||||
const mailboxListKey = "mailbox/list"
|
||||
Vendored
+34
@@ -0,0 +1,34 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"git.sr.ht/~rockorager/go-jmap"
|
||||
)
|
||||
|
||||
func (c *JMAPCache) GetSession() (*jmap.Session, error) {
|
||||
buf, err := c.get(sessionKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s := new(jmap.Session)
|
||||
err = json.Unmarshal(buf, s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (c *JMAPCache) PutSession(s *jmap.Session) error {
|
||||
buf, err := json.Marshal(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.put(sessionKey, buf)
|
||||
}
|
||||
|
||||
func (c *JMAPCache) DeleteSession() error {
|
||||
return c.delete(sessionKey)
|
||||
}
|
||||
|
||||
const sessionKey = "session"
|
||||
Vendored
+43
@@ -0,0 +1,43 @@
|
||||
package cache
|
||||
|
||||
func (c *JMAPCache) GetMailboxState() (string, error) {
|
||||
buf, err := c.get(mailboxStateKey)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(buf), nil
|
||||
}
|
||||
|
||||
func (c *JMAPCache) PutMailboxState(state string) error {
|
||||
return c.put(mailboxStateKey, []byte(state))
|
||||
}
|
||||
|
||||
func (c *JMAPCache) GetEmailState() (string, error) {
|
||||
buf, err := c.get(emailStateKey)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(buf), nil
|
||||
}
|
||||
|
||||
func (c *JMAPCache) PutEmailState(state string) error {
|
||||
return c.put(emailStateKey, []byte(state))
|
||||
}
|
||||
|
||||
func (c *JMAPCache) GetThreadState() (string, error) {
|
||||
buf, err := c.get(threadStateKey)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(buf), nil
|
||||
}
|
||||
|
||||
func (c *JMAPCache) PutThreadState(state string) error {
|
||||
return c.put(threadStateKey, []byte(state))
|
||||
}
|
||||
|
||||
const (
|
||||
mailboxStateKey = "state/mailbox"
|
||||
emailStateKey = "state/email"
|
||||
threadStateKey = "state/thread"
|
||||
)
|
||||
Vendored
+34
@@ -0,0 +1,34 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"git.sr.ht/~rockorager/go-jmap"
|
||||
)
|
||||
|
||||
func (c *JMAPCache) GetThread(id jmap.ID) ([]jmap.ID, error) {
|
||||
buf, err := c.get(threadKey(id))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var list IDList
|
||||
err = unmarshal(buf, &list)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return list.IDs, nil
|
||||
}
|
||||
|
||||
func (c *JMAPCache) PutThread(id jmap.ID, list []jmap.ID) error {
|
||||
buf, err := marshal(&IDList{IDs: list})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.put(threadKey(id), buf)
|
||||
}
|
||||
|
||||
func (c *JMAPCache) DeleteThread(id jmap.ID) error {
|
||||
return c.delete(mailboxKey(id))
|
||||
}
|
||||
|
||||
func threadKey(id jmap.ID) string {
|
||||
return "thread/" + string(id)
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package jmap
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.sr.ht/~rjarry/aerc/worker/jmap/cache"
|
||||
"git.sr.ht/~rjarry/aerc/worker/types"
|
||||
)
|
||||
|
||||
func (w *JMAPWorker) handleConfigure(msg *types.Configure) error {
|
||||
w.config.cacheState = parseBool(msg.Config.Params["cache-state"])
|
||||
w.config.cacheBlobs = parseBool(msg.Config.Params["cache-blobs"])
|
||||
w.config.useLabels = parseBool(msg.Config.Params["use-labels"])
|
||||
w.cache = cache.NewJMAPCache(
|
||||
w.config.cacheState, w.config.cacheBlobs, msg.Config.Name)
|
||||
|
||||
u, err := url.Parse(msg.Config.Source)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if strings.HasSuffix(u.Scheme, "+oauthbearer") {
|
||||
w.config.oauth = true
|
||||
} else {
|
||||
if u.User == nil {
|
||||
return fmt.Errorf("user:password not specified")
|
||||
} else if u.User.Username() == "" {
|
||||
return fmt.Errorf("username not specified")
|
||||
} else if _, ok := u.User.Password(); !ok {
|
||||
return fmt.Errorf("password not specified")
|
||||
}
|
||||
}
|
||||
|
||||
u.RawQuery = ""
|
||||
u.Fragment = ""
|
||||
w.config.user = u.User
|
||||
u.User = nil
|
||||
u.Scheme = "https"
|
||||
|
||||
w.config.endpoint = u.String()
|
||||
w.config.account = msg.Config
|
||||
w.config.allMail = msg.Config.Params["all-mail"]
|
||||
if w.config.allMail == "" {
|
||||
w.config.allMail = "All mail"
|
||||
}
|
||||
if ping, ok := msg.Config.Params["server-ping"]; ok {
|
||||
dur, err := time.ParseDuration(ping)
|
||||
if err != nil {
|
||||
return fmt.Errorf("server-ping: %w", err)
|
||||
}
|
||||
w.config.serverPing = dur
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseBool(val string) bool {
|
||||
switch strings.ToLower(val) {
|
||||
case "1", "t", "true", "yes", "y", "on":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
package jmap
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
|
||||
"git.sr.ht/~rjarry/aerc/worker/types"
|
||||
"git.sr.ht/~rockorager/go-jmap"
|
||||
"git.sr.ht/~rockorager/go-jmap/mail"
|
||||
"git.sr.ht/~rockorager/go-jmap/mail/identity"
|
||||
)
|
||||
|
||||
func (w *JMAPWorker) handleConnect(msg *types.Connect) error {
|
||||
w.client = &jmap.Client{SessionEndpoint: w.config.endpoint}
|
||||
|
||||
if w.config.oauth {
|
||||
pass, _ := w.config.user.Password()
|
||||
w.client.WithAccessToken(pass)
|
||||
} else {
|
||||
user := w.config.user.Username()
|
||||
pass, _ := w.config.user.Password()
|
||||
w.client.WithBasicAuth(user, pass)
|
||||
}
|
||||
|
||||
if session, err := w.cache.GetSession(); err == nil {
|
||||
w.client.Session = session
|
||||
}
|
||||
if w.client.Session == nil {
|
||||
if err := w.UpdateSession(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
go w.monitorChanges()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *JMAPWorker) AccountId() jmap.ID {
|
||||
switch {
|
||||
case w.client == nil:
|
||||
fallthrough
|
||||
case w.client.Session == nil:
|
||||
fallthrough
|
||||
case w.client.Session.PrimaryAccounts == nil:
|
||||
return ""
|
||||
default:
|
||||
return w.client.Session.PrimaryAccounts[mail.URI]
|
||||
}
|
||||
}
|
||||
|
||||
func (w *JMAPWorker) UpdateSession() error {
|
||||
if err := w.client.Authenticate(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := w.cache.PutSession(w.client.Session); err != nil {
|
||||
w.w.Warnf("PutSession: %s", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *JMAPWorker) GetIdentities() error {
|
||||
var req jmap.Request
|
||||
|
||||
req.Invoke(&identity.Get{Account: w.AccountId()})
|
||||
resp, err := w.Do(&req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, inv := range resp.Responses {
|
||||
switch r := inv.Args.(type) {
|
||||
case *identity.GetResponse:
|
||||
for _, ident := range r.List {
|
||||
w.identities[ident.Email] = ident
|
||||
}
|
||||
case *jmap.MethodError:
|
||||
return wrapMethodError(r)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var seqnum uint64
|
||||
|
||||
func (w *JMAPWorker) Do(req *jmap.Request) (*jmap.Response, error) {
|
||||
seq := atomic.AddUint64(&seqnum, 1)
|
||||
body, _ := json.Marshal(req.Calls)
|
||||
w.w.Debugf(">%d> POST %s", seq, body)
|
||||
resp, err := w.client.Do(req)
|
||||
if err != nil {
|
||||
w.w.Debugf("<%d< %s", seq, err)
|
||||
// Try to update session in case an endpoint changed
|
||||
err := w.UpdateSession()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// And try again if we succeeded
|
||||
resp, err = w.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if resp.SessionState != w.client.Session.State {
|
||||
if err := w.UpdateSession(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
w.w.Debugf("<%d< done", seq)
|
||||
return resp, err
|
||||
}
|
||||
|
||||
func (w *JMAPWorker) Download(blobID jmap.ID) (io.ReadCloser, error) {
|
||||
seq := atomic.AddUint64(&seqnum, 1)
|
||||
replacer := strings.NewReplacer(
|
||||
"{accountId}", string(w.AccountId()),
|
||||
"{blobId}", string(blobID),
|
||||
"{type}", "application/octet-stream",
|
||||
"{name}", "filename",
|
||||
)
|
||||
url := replacer.Replace(w.client.Session.DownloadURL)
|
||||
w.w.Debugf(">%d> GET %s", seq, url)
|
||||
rd, err := w.client.Download(w.AccountId(), blobID)
|
||||
if err == nil {
|
||||
w.w.Debugf("<%d< 200 OK", seq)
|
||||
} else {
|
||||
w.w.Debugf("<%d< %s", seq, err)
|
||||
}
|
||||
return rd, err
|
||||
}
|
||||
|
||||
func (w *JMAPWorker) Upload(reader io.Reader) (*jmap.UploadResponse, error) {
|
||||
seq := atomic.AddUint64(&seqnum, 1)
|
||||
url := strings.ReplaceAll(w.client.Session.UploadURL,
|
||||
"{accountId}", string(w.AccountId()))
|
||||
w.w.Debugf(">%d> POST %s", seq, url)
|
||||
resp, err := w.client.Upload(w.AccountId(), reader)
|
||||
if err == nil {
|
||||
w.w.Debugf("<%d< 200 OK", seq)
|
||||
} else {
|
||||
w.w.Debugf("<%d< %s", seq, err)
|
||||
}
|
||||
return resp, err
|
||||
}
|
||||
@@ -0,0 +1,345 @@
|
||||
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)
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
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 cachedMailboxState == "" || 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)
|
||||
}
|
||||
|
||||
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([]models.UID, 0, len(contents.MessageIDs))
|
||||
for _, id := range contents.MessageIDs {
|
||||
uids = append(uids, models.UID(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 []models.UID
|
||||
for _, id := range r.IDs {
|
||||
uids = append(uids, models.UID(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
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
package jmap
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"git.sr.ht/~rjarry/aerc/models"
|
||||
"git.sr.ht/~rjarry/aerc/worker/types"
|
||||
"git.sr.ht/~rockorager/go-jmap"
|
||||
"git.sr.ht/~rockorager/go-jmap/mail/email"
|
||||
"github.com/emersion/go-message/charset"
|
||||
)
|
||||
|
||||
var headersProperties = []string{
|
||||
"id",
|
||||
"blobId",
|
||||
"threadId",
|
||||
"mailboxIds",
|
||||
"keywords",
|
||||
"size",
|
||||
"receivedAt",
|
||||
"headers",
|
||||
"messageId",
|
||||
"inReplyTo",
|
||||
"references",
|
||||
"from",
|
||||
"to",
|
||||
"cc",
|
||||
"bcc",
|
||||
"replyTo",
|
||||
"subject",
|
||||
"bodyStructure",
|
||||
}
|
||||
|
||||
func (w *JMAPWorker) handleFetchMessageHeaders(msg *types.FetchMessageHeaders) error {
|
||||
emailIdsToFetch := make([]jmap.ID, 0, len(msg.Uids))
|
||||
currentEmails := make([]*email.Email, 0, len(msg.Uids))
|
||||
for _, uid := range msg.Uids {
|
||||
jid := jmap.ID(uid)
|
||||
m, err := w.cache.GetEmail(jid)
|
||||
if err != nil {
|
||||
// Message wasn't in cache; fetch it
|
||||
emailIdsToFetch = append(emailIdsToFetch, jid)
|
||||
continue
|
||||
}
|
||||
currentEmails = append(currentEmails, m)
|
||||
// Get the UI updated immediately
|
||||
w.w.PostMessage(&types.MessageInfo{
|
||||
Message: types.RespondTo(msg),
|
||||
Info: w.translateMsgInfo(m),
|
||||
}, nil)
|
||||
}
|
||||
|
||||
if len(emailIdsToFetch) > 0 {
|
||||
var req jmap.Request
|
||||
|
||||
req.Invoke(&email.Get{
|
||||
Account: w.AccountId(),
|
||||
IDs: emailIdsToFetch,
|
||||
Properties: headersProperties,
|
||||
})
|
||||
|
||||
resp, err := w.Do(&req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, inv := range resp.Responses {
|
||||
switch r := inv.Args.(type) {
|
||||
case *email.GetResponse:
|
||||
if err = w.cache.PutEmailState(r.State); err != nil {
|
||||
w.w.Warnf("PutEmailState: %s", err)
|
||||
}
|
||||
currentEmails = append(currentEmails, r.List...)
|
||||
case *jmap.MethodError:
|
||||
return wrapMethodError(r)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var threadsToFetch []jmap.ID
|
||||
for _, eml := range currentEmails {
|
||||
thread, err := w.cache.GetThread(eml.ThreadID)
|
||||
if err != nil {
|
||||
threadsToFetch = append(threadsToFetch, eml.ThreadID)
|
||||
continue
|
||||
}
|
||||
for _, id := range thread {
|
||||
m, err := w.cache.GetEmail(id)
|
||||
if err != nil {
|
||||
// This should never happen. If we have the
|
||||
// thread in cache, we will have fetched it
|
||||
// already or updated it from the update loop
|
||||
w.w.Warnf("Email ID %s from Thread %s not in cache", id, eml.ThreadID)
|
||||
continue
|
||||
}
|
||||
currentEmails = append(currentEmails, m)
|
||||
// Get the UI updated immediately
|
||||
w.w.PostMessage(&types.MessageInfo{
|
||||
Message: types.RespondTo(msg),
|
||||
Info: w.translateMsgInfo(m),
|
||||
}, nil)
|
||||
}
|
||||
}
|
||||
|
||||
threadEmails, err := w.fetchEntireThreads(threadsToFetch)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, m := range threadEmails {
|
||||
w.w.PostMessage(&types.MessageInfo{
|
||||
Message: types.RespondTo(msg),
|
||||
Info: w.translateMsgInfo(m),
|
||||
}, nil)
|
||||
if err := w.cache.PutEmail(m.ID, m); err != nil {
|
||||
w.w.Warnf("PutEmail: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *JMAPWorker) handleFetchMessageBodyPart(msg *types.FetchMessageBodyPart) error {
|
||||
mail, err := w.cache.GetEmail(jmap.ID(msg.Uid))
|
||||
if err != nil {
|
||||
return fmt.Errorf("bug: unknown message id %s: %w", msg.Uid, err)
|
||||
}
|
||||
|
||||
part := mail.BodyStructure
|
||||
for i, index := range msg.Part {
|
||||
index -= 1 // convert to zero based offset
|
||||
if index < len(part.SubParts) {
|
||||
part = part.SubParts[index]
|
||||
} else {
|
||||
return fmt.Errorf(
|
||||
"bug: invalid part index[%d]: %v", i, msg.Part)
|
||||
}
|
||||
}
|
||||
|
||||
buf, err := w.cache.GetBlob(part.BlobID)
|
||||
if err != nil {
|
||||
rd, err := w.Download(part.BlobID)
|
||||
if err != nil {
|
||||
return w.wrapDownloadError("part", part.BlobID, err)
|
||||
}
|
||||
buf, err = io.ReadAll(rd)
|
||||
rd.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = w.cache.PutBlob(part.BlobID, buf); err != nil {
|
||||
w.w.Warnf("PutBlob: %s", err)
|
||||
}
|
||||
}
|
||||
var reader io.Reader = bytes.NewReader(buf)
|
||||
if strings.HasPrefix(part.Type, "text/") && part.Charset != "" {
|
||||
r, err := charset.Reader(part.Charset, reader)
|
||||
if err != nil {
|
||||
w.w.Warnf("charset.Reader: %v", err)
|
||||
} else {
|
||||
reader = r
|
||||
}
|
||||
}
|
||||
w.w.PostMessage(&types.MessageBodyPart{
|
||||
Message: types.RespondTo(msg),
|
||||
Part: &models.MessageBodyPart{
|
||||
Reader: reader,
|
||||
Uid: msg.Uid,
|
||||
},
|
||||
}, nil)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *JMAPWorker) handleFetchFullMessages(msg *types.FetchFullMessages) error {
|
||||
for _, uid := range msg.Uids {
|
||||
mail, err := w.cache.GetEmail(jmap.ID(uid))
|
||||
if err != nil {
|
||||
return fmt.Errorf("bug: unknown message id %s: %w", uid, err)
|
||||
}
|
||||
buf, err := w.cache.GetBlob(mail.BlobID)
|
||||
if err != nil {
|
||||
rd, err := w.Download(mail.BlobID)
|
||||
if err != nil {
|
||||
return w.wrapDownloadError("full", mail.BlobID, err)
|
||||
}
|
||||
buf, err = io.ReadAll(rd)
|
||||
rd.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = w.cache.PutBlob(mail.BlobID, buf); err != nil {
|
||||
w.w.Warnf("PutBlob: %s", err)
|
||||
}
|
||||
}
|
||||
w.w.PostMessage(&types.FullMessage{
|
||||
Message: types.RespondTo(msg),
|
||||
Content: &models.FullMessage{
|
||||
Reader: bytes.NewReader(buf),
|
||||
Uid: uid,
|
||||
},
|
||||
}, nil)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *JMAPWorker) wrapDownloadError(prefix string, blobId jmap.ID, err error) error {
|
||||
urlRepl := strings.NewReplacer(
|
||||
"{accountId}", string(w.AccountId()),
|
||||
"{blobId}", string(blobId),
|
||||
"{type}", "application/octet-stream",
|
||||
"{name}", "filename",
|
||||
)
|
||||
url := urlRepl.Replace(w.client.Session.DownloadURL)
|
||||
return fmt.Errorf("%s: %q %w", prefix, url, err)
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
package jmap
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"git.sr.ht/~rjarry/aerc/models"
|
||||
"git.sr.ht/~rockorager/go-jmap"
|
||||
"git.sr.ht/~rockorager/go-jmap/mail"
|
||||
"git.sr.ht/~rockorager/go-jmap/mail/email"
|
||||
"git.sr.ht/~rockorager/go-jmap/mail/mailbox"
|
||||
msgmail "github.com/emersion/go-message/mail"
|
||||
)
|
||||
|
||||
func (w *JMAPWorker) translateMsgInfo(m *email.Email) *models.MessageInfo {
|
||||
env := &models.Envelope{
|
||||
Date: *m.ReceivedAt,
|
||||
Subject: m.Subject,
|
||||
From: translateAddrList(m.From),
|
||||
ReplyTo: translateAddrList(m.ReplyTo),
|
||||
To: translateAddrList(m.To),
|
||||
Cc: translateAddrList(m.CC),
|
||||
Bcc: translateAddrList(m.BCC),
|
||||
MessageId: firstString(m.MessageID),
|
||||
InReplyTo: firstString(m.InReplyTo),
|
||||
}
|
||||
labels := make([]string, 0, len(m.MailboxIDs))
|
||||
for id := range m.MailboxIDs {
|
||||
if dir, ok := w.mbox2dir[id]; ok {
|
||||
labels = append(labels, dir)
|
||||
}
|
||||
}
|
||||
sort.Strings(labels)
|
||||
|
||||
return &models.MessageInfo{
|
||||
Envelope: env,
|
||||
Flags: keywordsToFlags(m.Keywords),
|
||||
Uid: models.UID(m.ID),
|
||||
BodyStructure: translateBodyStructure(m.BodyStructure),
|
||||
RFC822Headers: translateJMAPHeader(m.Headers),
|
||||
Refs: m.References,
|
||||
Labels: labels,
|
||||
Size: uint32(m.Size),
|
||||
InternalDate: *m.ReceivedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func translateJMAPHeader(headers []*email.Header) *msgmail.Header {
|
||||
hdr := new(msgmail.Header)
|
||||
for _, h := range headers {
|
||||
raw := fmt.Sprintf("%s:%s\r\n", h.Name, h.Value)
|
||||
hdr.AddRaw([]byte(raw))
|
||||
}
|
||||
return hdr
|
||||
}
|
||||
|
||||
func flagsToKeywords(flags models.Flags) map[string]bool {
|
||||
kw := make(map[string]bool)
|
||||
if flags.Has(models.SeenFlag) {
|
||||
kw["$seen"] = true
|
||||
}
|
||||
if flags.Has(models.AnsweredFlag) {
|
||||
kw["$answered"] = true
|
||||
}
|
||||
if flags.Has(models.FlaggedFlag) {
|
||||
kw["$flagged"] = true
|
||||
}
|
||||
if flags.Has(models.DraftFlag) {
|
||||
kw["$draft"] = true
|
||||
}
|
||||
return kw
|
||||
}
|
||||
|
||||
func keywordsToFlags(kw map[string]bool) models.Flags {
|
||||
var f models.Flags
|
||||
for k, v := range kw {
|
||||
if v {
|
||||
switch k {
|
||||
case "$seen":
|
||||
f |= models.SeenFlag
|
||||
case "$answered":
|
||||
f |= models.AnsweredFlag
|
||||
case "$flagged":
|
||||
f |= models.FlaggedFlag
|
||||
case "$draft":
|
||||
f |= models.DraftFlag
|
||||
}
|
||||
}
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
func (w *JMAPWorker) MailboxPath(mbox *mailbox.Mailbox) string {
|
||||
if mbox == nil {
|
||||
return ""
|
||||
}
|
||||
if mbox.ParentID == "" {
|
||||
return mbox.Name
|
||||
}
|
||||
parent, err := w.cache.GetMailbox(mbox.ParentID)
|
||||
if err != nil {
|
||||
w.w.Warnf("MailboxPath/GetMailbox: %s", err)
|
||||
return mbox.Name
|
||||
}
|
||||
return w.MailboxPath(parent) + "/" + mbox.Name
|
||||
}
|
||||
|
||||
var jmapRole2aerc = map[mailbox.Role]models.Role{
|
||||
mailbox.RoleAll: models.AllRole,
|
||||
mailbox.RoleArchive: models.ArchiveRole,
|
||||
mailbox.RoleDrafts: models.DraftsRole,
|
||||
mailbox.RoleInbox: models.InboxRole,
|
||||
mailbox.RoleJunk: models.JunkRole,
|
||||
mailbox.RoleSent: models.SentRole,
|
||||
mailbox.RoleTrash: models.TrashRole,
|
||||
}
|
||||
|
||||
func firstString(s []string) string {
|
||||
if len(s) == 0 {
|
||||
return ""
|
||||
}
|
||||
return s[0]
|
||||
}
|
||||
|
||||
func translateAddrList(addrs []*mail.Address) []*msgmail.Address {
|
||||
res := make([]*msgmail.Address, 0, len(addrs))
|
||||
for _, a := range addrs {
|
||||
res = append(res, &msgmail.Address{Name: a.Name, Address: a.Email})
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func translateBodyStructure(part *email.BodyPart) *models.BodyStructure {
|
||||
bs := &models.BodyStructure{
|
||||
Description: part.Name,
|
||||
Encoding: part.Charset,
|
||||
Params: map[string]string{
|
||||
"name": part.Name,
|
||||
"charset": part.Charset,
|
||||
},
|
||||
Disposition: part.Disposition,
|
||||
DispositionParams: map[string]string{
|
||||
"filename": part.Name,
|
||||
},
|
||||
}
|
||||
bs.MIMEType, bs.MIMESubType, _ = strings.Cut(part.Type, "/")
|
||||
for _, sub := range part.SubParts {
|
||||
bs.Parts = append(bs.Parts, translateBodyStructure(sub))
|
||||
}
|
||||
return bs
|
||||
}
|
||||
|
||||
func wrapSetError(err *jmap.SetError) error {
|
||||
var s string
|
||||
if err.Description != nil {
|
||||
s = *err.Description
|
||||
} else {
|
||||
s = err.Type
|
||||
if err.Properties != nil {
|
||||
s += fmt.Sprintf(" %v", *err.Properties)
|
||||
}
|
||||
if s == "invalidProperties: [mailboxIds]" {
|
||||
s = "a message must belong to one or more mailboxes"
|
||||
}
|
||||
}
|
||||
return errors.New(s)
|
||||
}
|
||||
|
||||
func wrapMethodError(err *jmap.MethodError) error {
|
||||
var s string
|
||||
if err.Description != nil {
|
||||
s = *err.Description
|
||||
} else {
|
||||
s = err.Type
|
||||
}
|
||||
return errors.New(s)
|
||||
}
|
||||
@@ -0,0 +1,484 @@
|
||||
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"
|
||||
"git.sr.ht/~rockorager/go-jmap/mail/thread"
|
||||
)
|
||||
|
||||
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)
|
||||
}
|
||||
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",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
threadState, err := w.cache.GetThreadState()
|
||||
if err != nil {
|
||||
w.w.Debugf("GetThreadState: %s", err)
|
||||
}
|
||||
if threadState != "" && newState["Thread"] != threadState {
|
||||
callID := req.Invoke(&thread.Changes{
|
||||
Account: w.AccountId(),
|
||||
SinceState: threadState,
|
||||
})
|
||||
req.Invoke(&thread.Get{
|
||||
Account: w.AccountId(),
|
||||
ReferenceIDs: &jmap.ResultReference{
|
||||
ResultOf: callID,
|
||||
Name: "Thread/changes",
|
||||
Path: "/created",
|
||||
},
|
||||
})
|
||||
req.Invoke(&thread.Get{
|
||||
Account: w.AccountId(),
|
||||
ReferenceIDs: &jmap.ResultReference{
|
||||
ResultOf: callID,
|
||||
Name: "Thread/changes",
|
||||
Path: "/updated",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
if len(req.Calls) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
resp, err := w.Do(&req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var changedMboxIds []jmap.ID
|
||||
var labelsChanged bool
|
||||
// threadEmails are email IDs from threads which changed or were
|
||||
// created
|
||||
var threadEmails []jmap.ID
|
||||
|
||||
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 *thread.ChangesResponse:
|
||||
for _, id := range r.Destroyed {
|
||||
err = w.cache.DeleteThread(id)
|
||||
if err != nil {
|
||||
w.w.Warnf("DeleteThread: %s", err)
|
||||
}
|
||||
}
|
||||
err = w.cache.PutThreadState(r.NewState)
|
||||
if err != nil {
|
||||
w.w.Warnf("PutThreadState: %s", err)
|
||||
}
|
||||
|
||||
case *thread.GetResponse:
|
||||
for _, thread := range r.List {
|
||||
err = w.cache.PutThread(thread.ID, thread.EmailIDs)
|
||||
if err != nil {
|
||||
w.w.Warnf("PutThread: %s", err)
|
||||
}
|
||||
// We keep the list of all emails and check in a
|
||||
// subsequent request which ones we need to
|
||||
// fetch
|
||||
threadEmails = append(threadEmails, thread.EmailIDs...)
|
||||
}
|
||||
err = w.cache.PutThreadState(r.State)
|
||||
if err != nil {
|
||||
w.w.Warnf("PutThreadState: %s", err)
|
||||
}
|
||||
|
||||
case *email.GetResponse:
|
||||
switch inv.CallID {
|
||||
case emailUpdated:
|
||||
for _, m := range r.List {
|
||||
err = w.cache.PutEmail(m.ID, m)
|
||||
if err != nil {
|
||||
w.w.Warnf("PutEmail: %s", err)
|
||||
}
|
||||
// Send an updated message info if this
|
||||
// is part of our selected mailbox
|
||||
if m.MailboxIDs[w.selectedMbox] {
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
var updatedMboxes []jmap.ID
|
||||
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)
|
||||
|
||||
updatedMboxes = append(updatedMboxes, id)
|
||||
} 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 w.refreshQueriesAndThreads(updatedMboxes, threadEmails)
|
||||
}
|
||||
|
||||
// refreshQueriesAndThreads updates the cached query for any mailbox which was updated
|
||||
func (w *JMAPWorker) refreshQueriesAndThreads(
|
||||
updatedMboxes []jmap.ID,
|
||||
threadEmails []jmap.ID,
|
||||
) error {
|
||||
if len(updatedMboxes) == 0 && len(threadEmails) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var req jmap.Request
|
||||
queryChangesCalls := make(map[string]jmap.ID)
|
||||
folderContents := make(map[jmap.ID]*cache.FolderContents)
|
||||
|
||||
for _, id := range updatedMboxes {
|
||||
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
|
||||
}
|
||||
|
||||
emailsToFetch := []jmap.ID{}
|
||||
for _, id := range threadEmails {
|
||||
if w.cache.HasEmail(id) {
|
||||
continue
|
||||
}
|
||||
emailsToFetch = append(emailsToFetch, id)
|
||||
}
|
||||
|
||||
req.Invoke(&email.Get{
|
||||
Account: w.AccountId(),
|
||||
Properties: headersProperties,
|
||||
IDs: emailsToFetch,
|
||||
})
|
||||
|
||||
resp, err := w.Do(&req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, inv := range resp.Responses {
|
||||
switch r := inv.Args.(type) {
|
||||
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:
|
||||
for _, m := range r.List {
|
||||
err = w.cache.PutEmail(m.ID, m)
|
||||
if err != nil {
|
||||
w.w.Warnf("PutEmail: %s", err)
|
||||
}
|
||||
// Send an updated message info if this
|
||||
// is part of our selected mailbox
|
||||
if m.MailboxIDs[w.selectedMbox] {
|
||||
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 *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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package jmap
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"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) translateSearch(
|
||||
mbox jmap.ID, criteria *types.SearchCriteria,
|
||||
) email.Filter {
|
||||
cond := new(email.FilterCondition)
|
||||
|
||||
if mbox == "" {
|
||||
// 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)
|
||||
}
|
||||
cond.InMailboxOtherThan = mboxes
|
||||
} else {
|
||||
cond.InMailbox = mbox
|
||||
}
|
||||
if criteria == nil {
|
||||
return cond
|
||||
}
|
||||
|
||||
// dates
|
||||
if !criteria.StartDate.IsZero() {
|
||||
cond.After = &criteria.StartDate
|
||||
}
|
||||
if !criteria.EndDate.IsZero() {
|
||||
cond.Before = &criteria.EndDate
|
||||
}
|
||||
|
||||
// general search terms
|
||||
terms := strings.Join(criteria.Terms, " ")
|
||||
switch {
|
||||
case criteria.SearchAll:
|
||||
cond.Text = terms
|
||||
case criteria.SearchBody:
|
||||
cond.Body = terms
|
||||
default:
|
||||
cond.Subject = terms
|
||||
}
|
||||
|
||||
filter := &email.FilterOperator{Operator: jmap.OperatorAND}
|
||||
filter.Conditions = append(filter.Conditions, cond)
|
||||
|
||||
// keywords/flags
|
||||
for kw := range flagsToKeywords(criteria.WithFlags) {
|
||||
filter.Conditions = append(filter.Conditions,
|
||||
&email.FilterCondition{HasKeyword: kw})
|
||||
}
|
||||
for kw := range flagsToKeywords(criteria.WithoutFlags) {
|
||||
filter.Conditions = append(filter.Conditions,
|
||||
&email.FilterCondition{NotKeyword: kw})
|
||||
}
|
||||
|
||||
// recipients
|
||||
addrs := &email.FilterOperator{
|
||||
Operator: jmap.OperatorOR,
|
||||
}
|
||||
for _, from := range criteria.From {
|
||||
addrs.Conditions = append(addrs.Conditions,
|
||||
&email.FilterCondition{From: from})
|
||||
}
|
||||
for _, to := range criteria.To {
|
||||
addrs.Conditions = append(addrs.Conditions,
|
||||
&email.FilterCondition{To: to})
|
||||
}
|
||||
for _, cc := range criteria.Cc {
|
||||
addrs.Conditions = append(addrs.Conditions,
|
||||
&email.FilterCondition{Cc: cc})
|
||||
}
|
||||
if len(addrs.Conditions) > 0 {
|
||||
filter.Conditions = append(filter.Conditions, addrs)
|
||||
}
|
||||
|
||||
// specific headers
|
||||
headers := &email.FilterOperator{
|
||||
Operator: jmap.OperatorAND,
|
||||
}
|
||||
for h, values := range criteria.Headers {
|
||||
for _, v := range values {
|
||||
headers.Conditions = append(headers.Conditions,
|
||||
&email.FilterCondition{Header: []string{h, v}})
|
||||
}
|
||||
}
|
||||
if len(headers.Conditions) > 0 {
|
||||
filter.Conditions = append(filter.Conditions, headers)
|
||||
}
|
||||
|
||||
return filter
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
package jmap
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"git.sr.ht/~rjarry/aerc/lib/log"
|
||||
"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/emailsubmission"
|
||||
"git.sr.ht/~rockorager/go-jmap/mail/mailbox"
|
||||
"github.com/emersion/go-message/mail"
|
||||
)
|
||||
|
||||
func (w *JMAPWorker) handleStartSend(msg *types.StartSendingMessage) error {
|
||||
reader, writer := io.Pipe()
|
||||
send := &jmapSendWriter{writer: writer, done: make(chan error)}
|
||||
|
||||
w.w.PostMessage(&types.MessageWriter{
|
||||
Message: types.RespondTo(msg),
|
||||
Writer: send,
|
||||
}, nil)
|
||||
|
||||
go func() {
|
||||
defer log.PanicHandler()
|
||||
defer close(send.done)
|
||||
|
||||
identity, err := w.getSenderIdentity(msg.From)
|
||||
if err != nil {
|
||||
send.done <- err
|
||||
return
|
||||
}
|
||||
|
||||
blob, err := w.Upload(reader)
|
||||
if err != nil {
|
||||
send.done <- err
|
||||
return
|
||||
}
|
||||
|
||||
var req jmap.Request
|
||||
|
||||
// Import the blob into drafts
|
||||
req.Invoke(&email.Import{
|
||||
Account: w.AccountId(),
|
||||
Emails: map[string]*email.EmailImport{
|
||||
"aerc": {
|
||||
BlobID: blob.ID,
|
||||
MailboxIDs: map[jmap.ID]bool{
|
||||
w.roles[mailbox.RoleDrafts]: true,
|
||||
},
|
||||
Keywords: map[string]bool{
|
||||
"$draft": true,
|
||||
"$seen": true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
from := &emailsubmission.Address{Email: msg.From.Address}
|
||||
var rcpts []*emailsubmission.Address
|
||||
for _, address := range msg.Rcpts {
|
||||
rcpts = append(rcpts, &emailsubmission.Address{
|
||||
Email: address.Address,
|
||||
})
|
||||
}
|
||||
envelope := &emailsubmission.Envelope{MailFrom: from, RcptTo: rcpts}
|
||||
onSuccess := jmap.Patch{
|
||||
"keywords/$draft": nil,
|
||||
w.rolePatch(mailbox.RoleSent): true,
|
||||
w.rolePatch(mailbox.RoleDrafts): nil,
|
||||
}
|
||||
for _, dir := range msg.CopyTo {
|
||||
mbox, ok := w.dir2mbox[dir]
|
||||
if ok && mbox != w.roles[mailbox.RoleSent] {
|
||||
onSuccess[w.mboxPatch(mbox)] = true
|
||||
}
|
||||
}
|
||||
// Create the submission
|
||||
req.Invoke(&emailsubmission.Set{
|
||||
Account: w.AccountId(),
|
||||
Create: map[jmap.ID]*emailsubmission.EmailSubmission{
|
||||
"sub": {
|
||||
IdentityID: identity,
|
||||
EmailID: "#aerc",
|
||||
Envelope: envelope,
|
||||
},
|
||||
},
|
||||
OnSuccessUpdateEmail: map[jmap.ID]jmap.Patch{
|
||||
"#sub": onSuccess,
|
||||
},
|
||||
})
|
||||
|
||||
resp, err := w.Do(&req)
|
||||
if err != nil {
|
||||
send.done <- err
|
||||
return
|
||||
}
|
||||
|
||||
for _, inv := range resp.Responses {
|
||||
switch r := inv.Args.(type) {
|
||||
case *email.ImportResponse:
|
||||
if err, ok := r.NotCreated["aerc"]; ok {
|
||||
send.done <- wrapSetError(err)
|
||||
return
|
||||
}
|
||||
case *emailsubmission.SetResponse:
|
||||
if err, ok := r.NotCreated["sub"]; ok {
|
||||
send.done <- wrapSetError(err)
|
||||
return
|
||||
}
|
||||
case *jmap.MethodError:
|
||||
send.done <- wrapMethodError(r)
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type jmapSendWriter struct {
|
||||
writer *io.PipeWriter
|
||||
done chan error
|
||||
}
|
||||
|
||||
func (w *jmapSendWriter) Write(data []byte) (int, error) {
|
||||
return w.writer.Write(data)
|
||||
}
|
||||
|
||||
func (w *jmapSendWriter) Close() error {
|
||||
writeErr := w.writer.Close()
|
||||
sendErr := <-w.done
|
||||
if writeErr != nil {
|
||||
return writeErr
|
||||
}
|
||||
return sendErr
|
||||
}
|
||||
|
||||
func (w *JMAPWorker) getSenderIdentity(from *mail.Address) (jmap.ID, error) {
|
||||
if len(w.identities) == 0 {
|
||||
if err := w.GetIdentities(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
name, domain, _ := strings.Cut(from.Address, "@")
|
||||
for _, ident := range w.identities {
|
||||
n, d, _ := strings.Cut(ident.Email, "@")
|
||||
switch {
|
||||
case n == name && d == domain:
|
||||
fallthrough
|
||||
case n == "*" && d == domain:
|
||||
return ident.ID, nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("no identity found for address: %s@%s", name, domain)
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
package jmap
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.sr.ht/~rjarry/aerc/models"
|
||||
"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) updateFlags(uids []models.UID, flags models.Flags, enable bool) error {
|
||||
var req jmap.Request
|
||||
patches := make(map[jmap.ID]jmap.Patch)
|
||||
|
||||
for _, uid := range uids {
|
||||
patch := jmap.Patch{}
|
||||
for kw := range flagsToKeywords(flags) {
|
||||
path := fmt.Sprintf("keywords/%s", kw)
|
||||
if enable {
|
||||
patch[path] = true
|
||||
} else {
|
||||
patch[path] = nil
|
||||
}
|
||||
}
|
||||
patches[jmap.ID(uid)] = patch
|
||||
}
|
||||
|
||||
req.Invoke(&email.Set{
|
||||
Account: w.AccountId(),
|
||||
Update: patches,
|
||||
})
|
||||
|
||||
resp, err := w.Do(&req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = checkNotUpdated(resp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// If we didn't get an update error, all methods succeeded. We can
|
||||
// update the cache and UI now. We don't update the email state so that
|
||||
// we still grab an updated set from the update channel
|
||||
for _, uid := range uids {
|
||||
jid := jmap.ID(uid)
|
||||
m, err := w.cache.GetEmail(jid)
|
||||
if err != nil {
|
||||
// We'll get this from the update channel
|
||||
continue
|
||||
}
|
||||
if enable {
|
||||
for kw := range flagsToKeywords(flags) {
|
||||
m.Keywords[kw] = true
|
||||
}
|
||||
} else {
|
||||
for kw := range flagsToKeywords(flags) {
|
||||
delete(m.Keywords, kw)
|
||||
}
|
||||
}
|
||||
err = w.cache.PutEmail(jid, m)
|
||||
if err != nil {
|
||||
w.w.Warnf("PutEmail: %s", err)
|
||||
}
|
||||
// Get the UI updated immediately
|
||||
w.w.PostMessage(&types.MessageInfo{
|
||||
Info: w.translateMsgInfo(m),
|
||||
}, nil)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *JMAPWorker) moveCopy(uids []models.UID, destDir string, deleteSrc bool) error {
|
||||
var req jmap.Request
|
||||
var destMbox jmap.ID
|
||||
var destroy []jmap.ID
|
||||
var ok bool
|
||||
|
||||
patches := make(map[jmap.ID]jmap.Patch)
|
||||
|
||||
destMbox, ok = w.dir2mbox[destDir]
|
||||
if !ok && destDir != "" {
|
||||
return fmt.Errorf("unknown destination mailbox")
|
||||
}
|
||||
if destMbox != "" && destMbox == w.selectedMbox {
|
||||
return fmt.Errorf("cannot move to current mailbox")
|
||||
}
|
||||
|
||||
for _, uid := range uids {
|
||||
dest := destMbox
|
||||
mail, err := w.cache.GetEmail(jmap.ID(uid))
|
||||
if err != nil {
|
||||
return fmt.Errorf("bug: unknown message id %s: %w", uid, err)
|
||||
}
|
||||
|
||||
patch := w.moveCopyPatch(mail, dest, deleteSrc)
|
||||
if len(patch) == 0 {
|
||||
destroy = append(destroy, mail.ID)
|
||||
w.w.Debugf("destroying <%s>", mail.MessageID[0])
|
||||
} else {
|
||||
patches[jmap.ID(uid)] = patch
|
||||
}
|
||||
}
|
||||
|
||||
req.Invoke(&email.Set{
|
||||
Account: w.AccountId(),
|
||||
Update: patches,
|
||||
Destroy: destroy,
|
||||
})
|
||||
|
||||
resp, err := w.Do(&req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return checkNotUpdated(resp)
|
||||
}
|
||||
|
||||
func (w *JMAPWorker) moveCopyPatch(
|
||||
mail *email.Email, dest jmap.ID, deleteSrc bool,
|
||||
) jmap.Patch {
|
||||
patch := jmap.Patch{}
|
||||
|
||||
if dest == "" && deleteSrc && len(mail.MailboxIDs) == 1 {
|
||||
dest = w.roles[mailbox.RoleTrash]
|
||||
}
|
||||
if dest != "" && dest != w.selectedMbox {
|
||||
d := w.mbox2dir[dest]
|
||||
if deleteSrc {
|
||||
w.w.Debugf("moving <%s> to %q", mail.MessageID[0], d)
|
||||
} else {
|
||||
w.w.Debugf("copying <%s> to %q", mail.MessageID[0], d)
|
||||
}
|
||||
patch[w.mboxPatch(dest)] = true
|
||||
}
|
||||
if deleteSrc && len(patch) > 0 {
|
||||
switch {
|
||||
case w.selectedMbox != "":
|
||||
patch[w.mboxPatch(w.selectedMbox)] = nil
|
||||
case len(mail.MailboxIDs) == 1:
|
||||
// In "all mail" virtual mailbox and email is in
|
||||
// a single mailbox, "Move" it to the specified
|
||||
// destination
|
||||
patch = jmap.Patch{"mailboxIds": []jmap.ID{dest}}
|
||||
default:
|
||||
// In "all mail" virtual mailbox and email is in
|
||||
// multiple mailboxes. Since we cannot know what mailbox
|
||||
// to remove, try at least to remove role=inbox.
|
||||
patch[w.rolePatch(mailbox.RoleInbox)] = nil
|
||||
}
|
||||
}
|
||||
|
||||
return patch
|
||||
}
|
||||
|
||||
func (w *JMAPWorker) mboxPatch(mbox jmap.ID) string {
|
||||
return fmt.Sprintf("mailboxIds/%s", mbox)
|
||||
}
|
||||
|
||||
func (w *JMAPWorker) rolePatch(role mailbox.Role) string {
|
||||
return fmt.Sprintf("mailboxIds/%s", w.roles[role])
|
||||
}
|
||||
|
||||
func (w *JMAPWorker) handleModifyLabels(msg *types.ModifyLabels) error {
|
||||
var req jmap.Request
|
||||
patch := jmap.Patch{}
|
||||
|
||||
for _, a := range msg.Add {
|
||||
mboxId, ok := w.dir2mbox[a]
|
||||
if !ok {
|
||||
return fmt.Errorf("unknown label: %q", a)
|
||||
}
|
||||
patch[w.mboxPatch(mboxId)] = true
|
||||
}
|
||||
for _, r := range msg.Remove {
|
||||
mboxId, ok := w.dir2mbox[r]
|
||||
if !ok {
|
||||
return fmt.Errorf("unknown label: %q", r)
|
||||
}
|
||||
patch[w.mboxPatch(mboxId)] = nil
|
||||
}
|
||||
|
||||
patches := make(map[jmap.ID]jmap.Patch)
|
||||
|
||||
for _, uid := range msg.Uids {
|
||||
patches[jmap.ID(uid)] = patch
|
||||
}
|
||||
|
||||
req.Invoke(&email.Set{
|
||||
Account: w.AccountId(),
|
||||
Update: patches,
|
||||
})
|
||||
|
||||
resp, err := w.Do(&req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return checkNotUpdated(resp)
|
||||
}
|
||||
|
||||
func checkNotUpdated(resp *jmap.Response) error {
|
||||
for _, inv := range resp.Responses {
|
||||
switch r := inv.Args.(type) {
|
||||
case *email.SetResponse:
|
||||
for _, err := range r.NotUpdated {
|
||||
return wrapSetError(err)
|
||||
}
|
||||
case *jmap.MethodError:
|
||||
return wrapMethodError(r)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *JMAPWorker) handleAppendMessage(msg *types.AppendMessage) error {
|
||||
dest, ok := w.dir2mbox[msg.Destination]
|
||||
if !ok {
|
||||
return fmt.Errorf("unknown destination mailbox")
|
||||
}
|
||||
|
||||
// Upload the message
|
||||
blob, err := w.Upload(msg.Reader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var req jmap.Request
|
||||
|
||||
// Import the blob into specified directory
|
||||
req.Invoke(&email.Import{
|
||||
Account: w.AccountId(),
|
||||
Emails: map[string]*email.EmailImport{
|
||||
"aerc": {
|
||||
BlobID: blob.ID,
|
||||
MailboxIDs: map[jmap.ID]bool{dest: true},
|
||||
Keywords: flagsToKeywords(msg.Flags),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
resp, err := w.Do(&req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, inv := range resp.Responses {
|
||||
switch r := inv.Args.(type) {
|
||||
case *email.ImportResponse:
|
||||
if err, ok := r.NotCreated["aerc"]; ok {
|
||||
return wrapSetError(err)
|
||||
}
|
||||
case *jmap.MethodError:
|
||||
return wrapMethodError(r)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package jmap
|
||||
|
||||
import (
|
||||
"git.sr.ht/~rockorager/go-jmap"
|
||||
"git.sr.ht/~rockorager/go-jmap/mail/email"
|
||||
"git.sr.ht/~rockorager/go-jmap/mail/thread"
|
||||
)
|
||||
|
||||
func (w *JMAPWorker) fetchEntireThreads(threads []jmap.ID) ([]*email.Email, error) {
|
||||
var req jmap.Request
|
||||
|
||||
if len(threads) == 0 {
|
||||
return []*email.Email{}, nil
|
||||
}
|
||||
|
||||
threadGetId := req.Invoke(&thread.Get{
|
||||
Account: w.AccountId(),
|
||||
IDs: threads,
|
||||
})
|
||||
|
||||
// Opportunistically fetch all emails in this thread. We could wait for
|
||||
// the result, check which ones we don't have, then fetch only those.
|
||||
// However we can do this all in a single request which ends up being
|
||||
// faster than two requests for most contexts
|
||||
req.Invoke(&email.Get{
|
||||
Account: w.AccountId(),
|
||||
ReferenceIDs: &jmap.ResultReference{
|
||||
ResultOf: threadGetId,
|
||||
Name: "Thread/get",
|
||||
Path: "/list/*/emailIds",
|
||||
},
|
||||
Properties: headersProperties,
|
||||
})
|
||||
|
||||
resp, err := w.Do(&req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
emailsToReturn := make([]*email.Email, 0)
|
||||
for _, inv := range resp.Responses {
|
||||
switch r := inv.Args.(type) {
|
||||
case *thread.GetResponse:
|
||||
if err = w.cache.PutThreadState(r.State); err != nil {
|
||||
w.w.Warnf("PutThreadState: %s", err)
|
||||
}
|
||||
for _, thread := range r.List {
|
||||
if err = w.cache.PutThread(thread.ID, thread.EmailIDs); err != nil {
|
||||
w.w.Warnf("PutThread: %s", err)
|
||||
}
|
||||
}
|
||||
case *email.GetResponse:
|
||||
emailsToReturn = append(emailsToReturn, r.List...)
|
||||
if err = w.cache.PutEmailState(r.State); err != nil {
|
||||
w.w.Warnf("PutEmailState: %s", err)
|
||||
}
|
||||
case *jmap.MethodError:
|
||||
return nil, wrapMethodError(r)
|
||||
}
|
||||
}
|
||||
|
||||
return emailsToReturn, nil
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
package jmap
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"git.sr.ht/~rjarry/aerc/config"
|
||||
"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
|
||||
|
||||
selectedMbox jmap.ID
|
||||
dir2mbox map[string]jmap.ID
|
||||
mbox2dir map[jmap.ID]string
|
||||
roles map[mailbox.Role]jmap.ID
|
||||
identities map[string]*identity.Identity
|
||||
|
||||
changes chan jmap.TypeState
|
||||
stop chan struct{}
|
||||
}
|
||||
|
||||
func NewJMAPWorker(worker *types.Worker) (types.Backend, error) {
|
||||
return &JMAPWorker{
|
||||
w: worker,
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user