mirror of
https://github.com/mjl-/mox.git
synced 2025-07-12 12:24:38 +03:00
implement storing non-system/well-known flags (keywords) for messages and mailboxes, with imap
the mailbox select/examine responses now return all flags used in a mailbox in the FLAGS response. and indicate in the PERMANENTFLAGS response that clients can set new keywords. we store these values on the new Message.Keywords field. system/well-known flags are still in Message.Flags, so we're recognizing those and handling them separately. the imap store command handles the new flags. as does the append command, and the search command. we store keywords in a mailbox when a message in that mailbox gets the keyword. we don't automatically remove the keywords from a mailbox. there is currently no way at all to remove a keyword from a mailbox. the import commands now handle non-system/well-known keywords too, when importing from mbox/maildir. jmap requires keyword support, so best to get it out of the way now.
This commit is contained in:
@ -1233,7 +1233,7 @@ func (c *conn) applyChanges(changes []store.Change, initial bool) {
|
||||
c.bwritelinef("* %d EXISTS", len(c.uids))
|
||||
for _, add := range adds {
|
||||
seq := c.xsequence(add.UID)
|
||||
c.bwritelinef("* %d FETCH (UID %d FLAGS %s)", seq, add.UID, flaglist(add.Flags).pack(c))
|
||||
c.bwritelinef("* %d FETCH (UID %d FLAGS %s)", seq, add.UID, flaglist(add.Flags, add.Keywords).pack(c))
|
||||
}
|
||||
continue
|
||||
}
|
||||
@ -1265,7 +1265,7 @@ func (c *conn) applyChanges(changes []store.Change, initial bool) {
|
||||
continue
|
||||
}
|
||||
if !initial {
|
||||
c.bwritelinef("* %d FETCH (UID %d FLAGS %s)", seq, ch.UID, flaglist(ch.Flags).pack(c))
|
||||
c.bwritelinef("* %d FETCH (UID %d FLAGS %s)", seq, ch.UID, flaglist(ch.Flags, ch.Keywords).pack(c))
|
||||
}
|
||||
case store.ChangeRemoveMailbox:
|
||||
// Only announce \NonExistent to modern clients, otherwise they may ignore the
|
||||
@ -1862,8 +1862,12 @@ func (c *conn) cmdSelectExamine(isselect bool, tag, cmd string, p *parser) {
|
||||
})
|
||||
c.applyChanges(c.comm.Get(), true)
|
||||
|
||||
c.bwritelinef(`* FLAGS (\Seen \Answered \Flagged \Deleted \Draft $Forwarded $Junk $NotJunk $Phishing $MDNSent)`)
|
||||
c.bwritelinef(`* OK [PERMANENTFLAGS (\Seen \Answered \Flagged \Deleted \Draft $Forwarded $Junk $NotJunk $Phishing $MDNSent)] x`)
|
||||
var flags string
|
||||
if len(mb.Keywords) > 0 {
|
||||
flags = " " + strings.Join(mb.Keywords, " ")
|
||||
}
|
||||
c.bwritelinef(`* FLAGS (\Seen \Answered \Flagged \Deleted \Draft $Forwarded $Junk $NotJunk $Phishing $MDNSent%s)`, flags)
|
||||
c.bwritelinef(`* OK [PERMANENTFLAGS (\Seen \Answered \Flagged \Deleted \Draft $Forwarded $Junk $NotJunk $Phishing $MDNSent \*)] x`)
|
||||
if !c.enabled[capIMAP4rev2] {
|
||||
c.bwritelinef(`* 0 RECENT`)
|
||||
}
|
||||
@ -2436,7 +2440,7 @@ func (c *conn) xstatusLine(tx *bstore.Tx, mb store.Mailbox, attrs []string) stri
|
||||
return fmt.Sprintf("* STATUS %s (%s)", astring(mb.Name).pack(c), strings.Join(status, " "))
|
||||
}
|
||||
|
||||
func xparseStoreFlags(l []string, syntax bool) (flags store.Flags) {
|
||||
func xparseStoreFlags(l []string, syntax bool) (flags store.Flags, keywords []string) {
|
||||
fields := map[string]*bool{
|
||||
`\answered`: &flags.Answered,
|
||||
`\flagged`: &flags.Flagged,
|
||||
@ -2449,20 +2453,24 @@ func xparseStoreFlags(l []string, syntax bool) (flags store.Flags) {
|
||||
`$phishing`: &flags.Phishing,
|
||||
`$mdnsent`: &flags.MDNSent,
|
||||
}
|
||||
seen := map[string]bool{}
|
||||
for _, f := range l {
|
||||
if field, ok := fields[strings.ToLower(f)]; !ok {
|
||||
if syntax {
|
||||
xsyntaxErrorf("unknown flag %q", f)
|
||||
}
|
||||
xuserErrorf("unknown flag %q", f)
|
||||
} else {
|
||||
f = strings.ToLower(f)
|
||||
if field, ok := fields[f]; ok {
|
||||
*field = true
|
||||
} else if seen[f] {
|
||||
if moxvar.Pedantic {
|
||||
xuserErrorf("duplicate keyword %s", f)
|
||||
}
|
||||
} else {
|
||||
keywords = append(keywords, f)
|
||||
seen[f] = true
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func flaglist(fl store.Flags) listspace {
|
||||
func flaglist(fl store.Flags, keywords []string) listspace {
|
||||
l := listspace{}
|
||||
flag := func(v bool, s string) {
|
||||
if v {
|
||||
@ -2479,6 +2487,9 @@ func flaglist(fl store.Flags) listspace {
|
||||
flag(fl.Notjunk, `$NotJunk`)
|
||||
flag(fl.Phishing, `$Phishing`)
|
||||
flag(fl.MDNSent, `$MDNSent`)
|
||||
for _, k := range keywords {
|
||||
l = append(l, bare(k))
|
||||
}
|
||||
return l
|
||||
}
|
||||
|
||||
@ -2494,9 +2505,10 @@ func (c *conn) cmdAppend(tag, cmd string, p *parser) {
|
||||
name := p.xmailbox()
|
||||
p.xspace()
|
||||
var storeFlags store.Flags
|
||||
var keywords []string
|
||||
if p.hasPrefix("(") {
|
||||
// Error must be a syntax error, to properly abort the connection due to literal.
|
||||
storeFlags = xparseStoreFlags(p.xflagList(), true)
|
||||
storeFlags, keywords = xparseStoreFlags(p.xflagList(), true)
|
||||
p.xspace()
|
||||
}
|
||||
var tm time.Time
|
||||
@ -2570,11 +2582,21 @@ func (c *conn) cmdAppend(tag, cmd string, p *parser) {
|
||||
c.account.WithWLock(func() {
|
||||
c.xdbwrite(func(tx *bstore.Tx) {
|
||||
mb = c.xmailbox(tx, name, "TRYCREATE")
|
||||
|
||||
// Ensure keywords are stored in mailbox.
|
||||
var changed bool
|
||||
mb.Keywords, changed = store.MergeKeywords(mb.Keywords, keywords)
|
||||
if changed {
|
||||
err := tx.Update(&mb)
|
||||
xcheckf(err, "updating keywords in mailbox")
|
||||
}
|
||||
|
||||
msg = store.Message{
|
||||
MailboxID: mb.ID,
|
||||
MailboxOrigID: mb.ID,
|
||||
Received: tm,
|
||||
Flags: storeFlags,
|
||||
Keywords: keywords,
|
||||
Size: size,
|
||||
MsgPrefix: msgPrefix,
|
||||
}
|
||||
@ -2589,7 +2611,7 @@ func (c *conn) cmdAppend(tag, cmd string, p *parser) {
|
||||
}
|
||||
|
||||
// Broadcast the change to other connections.
|
||||
c.broadcast([]store.Change{store.ChangeAddUID{MailboxID: mb.ID, UID: msg.UID, Flags: msg.Flags}})
|
||||
c.broadcast([]store.Change{store.ChangeAddUID{MailboxID: mb.ID, UID: msg.UID, Flags: msg.Flags, Keywords: msg.Keywords}})
|
||||
})
|
||||
|
||||
err = msgFile.Close()
|
||||
@ -2952,6 +2974,7 @@ func (c *conn) cmdxCopy(isUID bool, tag, cmd string, p *parser) {
|
||||
var mbDst store.Mailbox
|
||||
var origUIDs, newUIDs []store.UID
|
||||
var flags []store.Flags
|
||||
var keywords [][]string
|
||||
|
||||
c.account.WithWLock(func() {
|
||||
c.xdbwrite(func(tx *bstore.Tx) {
|
||||
@ -3017,6 +3040,7 @@ func (c *conn) cmdxCopy(isUID bool, tag, cmd string, p *parser) {
|
||||
newUIDs = append(newUIDs, m.UID)
|
||||
newMsgIDs = append(newMsgIDs, m.ID)
|
||||
flags = append(flags, m.Flags)
|
||||
keywords = append(keywords, m.Keywords)
|
||||
|
||||
qmr := bstore.QueryTx[store.Recipient](tx)
|
||||
qmr.FilterNonzero(store.Recipient{MessageID: origID})
|
||||
@ -3048,7 +3072,7 @@ func (c *conn) cmdxCopy(isUID bool, tag, cmd string, p *parser) {
|
||||
if len(newUIDs) > 0 {
|
||||
changes := make([]store.Change, len(newUIDs))
|
||||
for i, uid := range newUIDs {
|
||||
changes[i] = store.ChangeAddUID{MailboxID: mbDst.ID, UID: uid, Flags: flags[i]}
|
||||
changes[i] = store.ChangeAddUID{MailboxID: mbDst.ID, UID: uid, Flags: flags[i], Keywords: keywords[i]}
|
||||
}
|
||||
c.broadcast(changes)
|
||||
}
|
||||
@ -3187,7 +3211,7 @@ func (c *conn) cmdxMove(isUID bool, tag, cmd string, p *parser) {
|
||||
changes = append(changes, store.ChangeRemoveUIDs{MailboxID: c.mailboxID, UIDs: uids})
|
||||
for _, m := range msgs {
|
||||
newUIDs = append(newUIDs, m.UID)
|
||||
changes = append(changes, store.ChangeAddUID{MailboxID: mbDst.ID, UID: m.UID, Flags: m.Flags})
|
||||
changes = append(changes, store.ChangeAddUID{MailboxID: mbDst.ID, UID: m.UID, Flags: m.Flags, Keywords: m.Keywords})
|
||||
}
|
||||
})
|
||||
|
||||
@ -3240,25 +3264,21 @@ func (c *conn) cmdxStore(isUID bool, tag, cmd string, p *parser) {
|
||||
xuserErrorf("mailbox open in read-only mode")
|
||||
}
|
||||
|
||||
var mask, flags store.Flags
|
||||
flags, keywords := xparseStoreFlags(flagstrs, false)
|
||||
var mask store.Flags
|
||||
if plus {
|
||||
mask = xparseStoreFlags(flagstrs, false)
|
||||
flags = store.FlagsAll
|
||||
mask, flags = flags, store.FlagsAll
|
||||
} else if minus {
|
||||
mask = xparseStoreFlags(flagstrs, false)
|
||||
flags = store.Flags{}
|
||||
mask, flags = flags, store.Flags{}
|
||||
} else {
|
||||
mask = store.FlagsAll
|
||||
flags = xparseStoreFlags(flagstrs, false)
|
||||
}
|
||||
|
||||
updates := store.FlagsQuerySet(mask, flags)
|
||||
|
||||
var updated []store.Message
|
||||
|
||||
c.account.WithWLock(func() {
|
||||
c.xdbwrite(func(tx *bstore.Tx) {
|
||||
c.xmailboxID(tx, c.mailboxID) // Validate.
|
||||
mb := c.xmailboxID(tx, c.mailboxID) // Validate.
|
||||
|
||||
uidargs := c.xnumSetCondition(isUID, nums)
|
||||
|
||||
@ -3266,27 +3286,41 @@ func (c *conn) cmdxStore(isUID bool, tag, cmd string, p *parser) {
|
||||
return
|
||||
}
|
||||
|
||||
// Ensure keywords are in mailbox.
|
||||
if !minus {
|
||||
var changed bool
|
||||
mb.Keywords, changed = store.MergeKeywords(mb.Keywords, keywords)
|
||||
if changed {
|
||||
err := tx.Update(&mb)
|
||||
xcheckf(err, "updating mailbox with keywords")
|
||||
}
|
||||
}
|
||||
|
||||
q := bstore.QueryTx[store.Message](tx)
|
||||
q.FilterNonzero(store.Message{MailboxID: c.mailboxID})
|
||||
q.FilterEqual("UID", uidargs...)
|
||||
if len(updates) == 0 {
|
||||
var err error
|
||||
updated, err = q.List()
|
||||
xcheckf(err, "listing for flags")
|
||||
} else {
|
||||
q.Gather(&updated)
|
||||
_, err := q.UpdateFields(updates)
|
||||
xcheckf(err, "updating flags")
|
||||
}
|
||||
err := q.ForEach(func(m store.Message) error {
|
||||
m.Flags = m.Flags.Set(mask, flags)
|
||||
if minus {
|
||||
m.Keywords = store.RemoveKeywords(m.Keywords, keywords)
|
||||
} else if plus {
|
||||
m.Keywords, _ = store.MergeKeywords(m.Keywords, keywords)
|
||||
} else {
|
||||
m.Keywords = keywords
|
||||
}
|
||||
updated = append(updated, m)
|
||||
return tx.Update(&m)
|
||||
})
|
||||
xcheckf(err, "storing flags in messages")
|
||||
|
||||
err := c.account.RetrainMessages(context.TODO(), c.log, tx, updated, false)
|
||||
err = c.account.RetrainMessages(context.TODO(), c.log, tx, updated, false)
|
||||
xcheckf(err, "training messages")
|
||||
})
|
||||
|
||||
// Broadcast changes to other connections.
|
||||
changes := make([]store.Change, len(updated))
|
||||
for i, m := range updated {
|
||||
changes[i] = store.ChangeFlags{MailboxID: m.MailboxID, UID: m.UID, Mask: mask, Flags: m.Flags}
|
||||
changes[i] = store.ChangeFlags{MailboxID: m.MailboxID, UID: m.UID, Mask: mask, Flags: m.Flags, Keywords: m.Keywords}
|
||||
}
|
||||
c.broadcast(changes)
|
||||
})
|
||||
@ -3294,7 +3328,7 @@ func (c *conn) cmdxStore(isUID bool, tag, cmd string, p *parser) {
|
||||
for _, m := range updated {
|
||||
if !silent {
|
||||
// ../rfc/9051:6749 ../rfc/3501:4869
|
||||
c.bwritelinef("* %d FETCH (UID %d FLAGS %s)", c.xsequence(m.UID), m.UID, flaglist(m.Flags).pack(c))
|
||||
c.bwritelinef("* %d FETCH (UID %d FLAGS %s)", c.xsequence(m.UID), m.UID, flaglist(m.Flags, m.Keywords).pack(c))
|
||||
}
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user