mirror of
https://github.com/mjl-/mox.git
synced 2025-06-29 01:38:14 +03:00

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).
151 lines
4.7 KiB
Go
151 lines
4.7 KiB
Go
package imapserver
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/mjl-/mox/imapclient"
|
|
)
|
|
|
|
func TestReplace(t *testing.T) {
|
|
defer mockUIDValidity()()
|
|
|
|
tc := start(t)
|
|
defer tc.close()
|
|
|
|
tc2 := startNoSwitchboard(t)
|
|
defer tc2.close()
|
|
|
|
tc.client.Login("mjl@mox.example", password0)
|
|
tc.client.Select("inbox")
|
|
|
|
// Append 3 messages, remove first. Leaves msgseq 1,2 with uid 2,3.
|
|
tc.client.Append("inbox", makeAppend(exampleMsg), makeAppend(exampleMsg), makeAppend(exampleMsg))
|
|
tc.client.StoreFlagsSet("1", true, `\deleted`)
|
|
tc.client.Expunge()
|
|
|
|
tc2.client.Login("mjl@mox.example", password0)
|
|
tc2.client.Select("inbox")
|
|
|
|
// Replace last message (msgseq 2, uid 3) in same mailbox.
|
|
tc.lastUntagged, tc.lastResult, tc.lastErr = tc.client.Replace("2", "INBOX", makeAppend(searchMsg))
|
|
tcheck(tc.t, tc.lastErr, "read imap response")
|
|
tc.xuntagged(
|
|
imapclient.UntaggedResult{Status: "OK", RespText: imapclient.RespText{Code: "APPENDUID", CodeArg: imapclient.CodeAppendUID{UIDValidity: 1, UIDs: xparseUIDRange("4")}, More: ""}},
|
|
imapclient.UntaggedExists(3),
|
|
imapclient.UntaggedExpunge(2),
|
|
)
|
|
tc.xcodeArg(imapclient.CodeHighestModSeq(6))
|
|
|
|
// Check that other client sees Exists and Expunge.
|
|
tc2.transactf("ok", "noop")
|
|
tc2.xuntagged(
|
|
imapclient.UntaggedExpunge(2),
|
|
imapclient.UntaggedExists(2),
|
|
imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(4), imapclient.FetchFlags(nil)}},
|
|
)
|
|
|
|
// Enable qresync, replace uid 2 (msgseq 1) to different mailbox, see that we get vanished instead of expunged.
|
|
tc.transactf("ok", "enable qresync")
|
|
tc.lastUntagged, tc.lastResult, tc.lastErr = tc.client.UIDReplace("2", "INBOX", makeAppend(searchMsg))
|
|
tcheck(tc.t, tc.lastErr, "read imap response")
|
|
tc.xuntagged(
|
|
imapclient.UntaggedResult{Status: "OK", RespText: imapclient.RespText{Code: "APPENDUID", CodeArg: imapclient.CodeAppendUID{UIDValidity: 1, UIDs: xparseUIDRange("5")}, More: ""}},
|
|
imapclient.UntaggedExists(3),
|
|
imapclient.UntaggedVanished{UIDs: xparseNumSet("2")},
|
|
)
|
|
tc.xcodeArg(imapclient.CodeHighestModSeq(7))
|
|
|
|
// Leftover data.
|
|
tc.transactf("bad", "replace 1 inbox () {6+}\r\ntest\r\n ")
|
|
}
|
|
|
|
func TestReplaceBigNonsyncLit(t *testing.T) {
|
|
tc := start(t)
|
|
defer tc.close()
|
|
|
|
tc.client.Login("mjl@mox.example", password0)
|
|
tc.client.Select("inbox")
|
|
|
|
// Adding a message >1mb with non-sync literal to non-existent mailbox should abort entire connection.
|
|
tc.transactf("bad", "replace 12345 inbox {2000000+}")
|
|
tc.xuntagged(
|
|
imapclient.UntaggedBye{Code: "ALERT", More: "error condition and non-synchronizing literal too big"},
|
|
)
|
|
tc.xcode("TOOBIG")
|
|
}
|
|
|
|
func TestReplaceQuota(t *testing.T) {
|
|
// with quota limit
|
|
tc := startArgs(t, true, false, true, true, "limit")
|
|
defer tc.close()
|
|
|
|
tc.client.Login("limit@mox.example", password0)
|
|
tc.client.Select("inbox")
|
|
tc.client.Append("inbox", makeAppend("x"))
|
|
|
|
// Synchronizing literal, we get failure immediately.
|
|
tc.transactf("no", "replace 1 inbox {6}\r\n")
|
|
tc.xcode("OVERQUOTA")
|
|
|
|
// Synchronizing literal to non-existent mailbox, we get failure immediately.
|
|
tc.transactf("no", "replace 1 badbox {6}\r\n")
|
|
tc.xcode("TRYCREATE")
|
|
|
|
buf := make([]byte, 4000, 4002)
|
|
for i := range buf {
|
|
buf[i] = 'x'
|
|
}
|
|
buf = append(buf, "\r\n"...)
|
|
|
|
// Non-synchronizing literal. We get to write our data.
|
|
tc.client.Commandf("", "replace 1 inbox ~{4000+}")
|
|
_, err := tc.client.Write(buf)
|
|
tc.check(err, "write replace message")
|
|
tc.response("no")
|
|
tc.xcode("OVERQUOTA")
|
|
|
|
// Non-synchronizing literal to bad mailbox.
|
|
tc.client.Commandf("", "replace 1 badbox {4000+}")
|
|
_, err = tc.client.Write(buf)
|
|
tc.check(err, "write replace message")
|
|
tc.response("no")
|
|
tc.xcode("TRYCREATE")
|
|
}
|
|
|
|
func TestReplaceExpunged(t *testing.T) {
|
|
tc := start(t)
|
|
defer tc.close()
|
|
|
|
tc.client.Login("mjl@mox.example", password0)
|
|
tc.client.Select("inbox")
|
|
tc.client.Append("inbox", makeAppend(exampleMsg))
|
|
|
|
// We start the command, but don't write data yet.
|
|
tc.client.Commandf("", "replace 1 inbox {4000}")
|
|
|
|
// Get in with second client and remove the message we are replacing.
|
|
tc2 := startNoSwitchboard(t)
|
|
defer tc2.close()
|
|
tc2.client.Login("mjl@mox.example", password0)
|
|
tc2.client.Select("inbox")
|
|
tc2.client.StoreFlagsSet("1", true, `\Deleted`)
|
|
tc2.client.Expunge()
|
|
tc2.client.Unselect()
|
|
tc2.client.Close()
|
|
|
|
// Now continue trying to replace the message. We should get an error and an expunge.
|
|
tc.readprefixline("+ ")
|
|
buf := make([]byte, 4000, 4002)
|
|
for i := range buf {
|
|
buf[i] = 'x'
|
|
}
|
|
buf = append(buf, "\r\n"...)
|
|
_, err := tc.client.Write(buf)
|
|
tc.check(err, "write replace message")
|
|
tc.response("no")
|
|
tc.xuntagged(
|
|
imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{imapclient.FetchUID(1), imapclient.FetchFlags{`\Deleted`}}},
|
|
imapclient.UntaggedExpunge(1),
|
|
)
|
|
}
|