Implement IMAP REPLACE extension, RFC 8508.

REPLACE can be used to update draft messages as you are editing. Instead of
requiring an APPEND and STORE of \Deleted and EXPUNGE. REPLACE works
atomically.

It has a syntax similar to APPEND, just allows you to specify the message to
replace that's in the currently selected mailbox. The regular REPLACE-command
works on a message sequence number, the UID REPLACE commands on a uid. The
destination mailbox, of the updated message, can be different. For example to
move a draft message from the Drafts folder to the Sent folder.

We have to do quite some bookkeeping, e.g. for updating (message) counts for
the mailbox, checking quota, un/retraining the junk filter. During a
synchronizing literal, we check the parameters early and reject if the replace
would fail (eg over quota, bad destination mailbox).
This commit is contained in:
Mechiel Lukkien
2025-02-25 23:27:19 +01:00
parent 1066eb4c9f
commit 92a87acfcb
7 changed files with 602 additions and 4 deletions

View File

@ -394,3 +394,44 @@ func (c *Conn) UIDMove(uidSet NumSet, dstMailbox string) (untagged []Untagged, r
defer c.recover(&rerr)
return c.Transactf("uid move %s %s", uidSet.String(), astring(dstMailbox))
}
// Replace replaces a message from the currently selected mailbox with a
// new/different version of the message in the named mailbox, which may be the
// same or different than the currently selected mailbox.
//
// Num is a message sequence number. "*" references the last message.
//
// Servers must have announced the REPLACE capability.
func (c *Conn) Replace(msgseq string, mailbox string, msg Append) (untagged []Untagged, result Result, rerr error) {
// todo: parse msgseq, must be nznumber, with a known msgseq. or "*" with at least one message.
return c.replace("replace", msgseq, mailbox, msg)
}
// UIDReplace is like Replace, but operates on a UID instead of message
// sequence number.
func (c *Conn) UIDReplace(uid string, mailbox string, msg Append) (untagged []Untagged, result Result, rerr error) {
// todo: parse uid, must be nznumber, with a known uid. or "*" with at least one message.
return c.replace("uid replace", uid, mailbox, msg)
}
func (c *Conn) replace(cmd string, num string, mailbox string, msg Append) (untagged []Untagged, result Result, rerr error) {
defer c.recover(&rerr)
// todo: use synchronizing literal for larger messages.
var date string
if msg.Received != nil {
date = ` "` + msg.Received.Format("_2-Jan-2006 15:04:05 -0700") + `"`
}
// todo: only use literal8 if needed, possibly with "UTF8()"
// todo: encode mailbox
c.Commandf("", "%s %s %s (%s)%s ~{%d+}", cmd, num, astring(mailbox), strings.Join(msg.Flags, " "), date, msg.Size)
_, err := io.Copy(c, msg.Data)
c.xcheckf(err, "write message data")
fmt.Fprintf(c.bw, "\r\n")
c.xflush()
return c.Response()
}

View File

@ -39,6 +39,7 @@ const (
CapCompressDeflate Capability = "COMPRESS=DEFLATE" // ../rfc/4978:65
CapListMetadata Capability = "LIST-METADTA" // ../rfc/9590:73
CapMultiAppend Capability = "MULTIAPPEND" // ../rfc/3502:33
CapReplace Capability = "REPLACE" // ../rfc/8508:155
)
// Status is the tagged final result of a command.