mirror of
https://git.sr.ht/~rjarry/aerc
synced 2026-03-02 18:23:33 +01:00
Add Directory and Filter fields to DirectoryContents, DirectoryThreaded, SearchResults, MessageInfo, and FullMessage response types. Workers now populate these fields so the UI knows which directory and filter context each response belongs to. This metadata allows the message store to correctly associate incoming messages with their source context, which is essential for the offline worker to cache messages by directory and for proper handling of concurrent operations across multiple folders. Signed-off-by: Robin Jarry <robin@jarry.cc> Reviewed-by: Simon Martin <simon@nasilyan.com>
182 lines
4.2 KiB
Go
182 lines
4.2 KiB
Go
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, dir string) *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,
|
|
Directory: dir,
|
|
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, ok := w.mboxes[mbox.ParentID]
|
|
if !ok {
|
|
w.w.Warnf("MailboxPath: parent=%s unknown", mbox.ParentID)
|
|
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,
|
|
},
|
|
ContentID: part.CID,
|
|
}
|
|
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)
|
|
}
|