From 2e5376d7ebff114c8d5e88120db219c2e17c4dc3 Mon Sep 17 00:00:00 2001 From: Mechiel Lukkien Date: Mon, 24 Jul 2023 08:49:19 +0200 Subject: [PATCH] when moving/copying messages in imapserve, also ensure the message keywords make it into the destination mailbox keywords list --- imapserver/server.go | 37 +++++++++++++++++++++++++++++++++---- store/account.go | 4 ++-- 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/imapserver/server.go b/imapserver/server.go index cd8284d..595da18 100644 --- a/imapserver/server.go +++ b/imapserver/server.go @@ -60,6 +60,7 @@ import ( "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" + "golang.org/x/exp/slices" "golang.org/x/text/unicode/norm" "github.com/mjl-/bstore" @@ -2083,6 +2084,7 @@ func (c *conn) cmdRename(tag, cmd string, p *parser) { Name: dst, UIDValidity: uidval, UIDNext: 1, + Keywords: srcMB.Keywords, } err = tx.Insert(&dstMB) xcheckf(err, "create new destination mailbox") @@ -3001,8 +3003,6 @@ func (c *conn) cmdxCopy(isUID bool, tag, cmd string, p *parser) { // Reserve the uids in the destination mailbox. uidFirst := mbDst.UIDNext mbDst.UIDNext += store.UID(len(uidargs)) - err := tx.Update(&mbDst) - xcheckf(err, "reserve uid in destination mailbox") // Fetch messages from database. q := bstore.QueryTx[store.Message](tx) @@ -3023,6 +3023,8 @@ func (c *conn) cmdxCopy(isUID bool, tag, cmd string, p *parser) { conf, _ := c.account.Conf() + mbKeywords := map[string]struct{}{} + // Insert new messages into database. var origMsgIDs, newMsgIDs []int64 for i, uid := range uids { @@ -3051,6 +3053,9 @@ func (c *conn) cmdxCopy(isUID bool, tag, cmd string, p *parser) { newMsgIDs = append(newMsgIDs, m.ID) flags = append(flags, m.Flags) keywords = append(keywords, m.Keywords) + for _, kw := range m.Keywords { + mbKeywords[kw] = struct{}{} + } qmr := bstore.QueryTx[store.Recipient](tx) qmr.FilterNonzero(store.Recipient{MessageID: origID}) @@ -3064,6 +3069,16 @@ func (c *conn) cmdxCopy(isUID bool, tag, cmd string, p *parser) { } } + // Ensure destination mailbox has keywords of the moved messages. + for kw := range mbKeywords { + if !slices.Contains(mbDst.Keywords, kw) { + mbDst.Keywords = append(mbDst.Keywords, kw) + } + } + + err = tx.Update(&mbDst) + xcheckf(err, "updating destination mailbox for uids and keywords") + // Copy message files to new message ID's. syncDirs := map[string]struct{}{} for i := range origMsgIDs { @@ -3146,8 +3161,6 @@ func (c *conn) cmdxMove(isUID bool, tag, cmd string, p *parser) { uidFirst := mbDst.UIDNext uidnext := uidFirst mbDst.UIDNext += store.UID(len(uids)) - err := tx.Update(&mbDst) - xcheckf(err, "reserve uids in destination mailbox") // Update UID and MailboxID in database for messages. q := bstore.QueryTx[store.Message](tx) @@ -3161,6 +3174,8 @@ func (c *conn) cmdxMove(isUID bool, tag, cmd string, p *parser) { xserverErrorf("uid and message mismatch") } + keywords := map[string]struct{}{} + conf, _ := c.account.Conf() for i := range msgs { m := &msgs[i] @@ -3178,8 +3193,22 @@ func (c *conn) cmdxMove(isUID bool, tag, cmd string, p *parser) { uidnext++ err := tx.Update(m) xcheckf(err, "updating moved message in database") + + for _, kw := range m.Keywords { + keywords[kw] = struct{}{} + } } + // Ensure destination mailbox has keywords of the moved messages. + for kw := range keywords { + if !slices.Contains(mbDst.Keywords, kw) { + mbDst.Keywords = append(mbDst.Keywords, kw) + } + } + + err = tx.Update(&mbDst) + xcheckf(err, "updating destination mailbox for uids and keywords") + err = c.account.RetrainMessages(context.TODO(), c.log, tx, msgs, false) xcheckf(err, "retraining messages after move") diff --git a/store/account.go b/store/account.go index e839c45..8fb821a 100644 --- a/store/account.go +++ b/store/account.go @@ -292,7 +292,7 @@ type Message struct { MessageHash []byte // Hash of message. For rejects delivery, so optional like MessageID. Flags - Keywords []string `bstore:"index"` // Non-system or well-known $-flags. Only in "atom" syntax, stored in lower case. + Keywords []string `bstore:"index"` // For keywords other than system flags or the basic well-known $-flags. Only in "atom" syntax, stored in lower case. Size int64 TrainedJunk *bool // If nil, no training done yet. Otherwise, true is trained as junk, false trained as nonjunk. MsgPrefix []byte // Typically holds received headers and/or header separator. @@ -1345,7 +1345,7 @@ func RemoveKeywords(l, remove []string) []string { func MergeKeywords(l, add []string) ([]string, bool) { var changed bool for _, k := range add { - if slices.Index(l, k) < 0 { + if !slices.Contains(l, k) { l = append(l, k) changed = true }