imapserver: implement UIDONLY extension, RFC 9586

Once clients enable this extension, commands can no longer refer to "message
sequence numbers" (MSNs), but can only refer to messages with UIDs. This means
both sides no longer have to carefully keep their sequence numbers in sync
(error-prone), and don't have to keep track of a mapping of sequence numbers to
UIDs (saves resources).

With UIDONLY enabled, all FETCH responses are replaced with UIDFETCH response.
This commit is contained in:
Mechiel Lukkien
2025-04-11 11:45:49 +02:00
parent 8bab38eac4
commit 507ca73b96
41 changed files with 2405 additions and 1545 deletions

View File

@ -7,22 +7,30 @@ import (
)
func TestAppend(t *testing.T) {
testAppend(t, false)
}
func TestAppendUIDOnly(t *testing.T) {
testAppend(t, true)
}
func testAppend(t *testing.T, uidonly bool) {
defer mockUIDValidity()()
tc := start(t) // note: with switchboard because this connection stays alive unlike tc2.
tc := start(t, uidonly) // note: with switchboard because this connection stays alive unlike tc2.
defer tc.close()
tc2 := startNoSwitchboard(t) // note: without switchboard because this connection will break during tests.
tc2 := startNoSwitchboard(t, uidonly) // note: without switchboard because this connection will break during tests.
defer tc2.closeNoWait()
tc3 := startNoSwitchboard(t)
tc3 := startNoSwitchboard(t, uidonly)
defer tc3.closeNoWait()
tc2.client.Login("mjl@mox.example", password0)
tc2.login("mjl@mox.example", password0)
tc2.client.Select("inbox")
tc.client.Login("mjl@mox.example", password0)
tc.login("mjl@mox.example", password0)
tc.client.Select("inbox")
tc3.client.Login("mjl@mox.example", password0)
tc3.login("mjl@mox.example", password0)
tc2.transactf("bad", "append") // Missing params.
tc2.transactf("bad", `append inbox`) // Missing message.
@ -30,15 +38,15 @@ func TestAppend(t *testing.T) {
// Syntax error for line ending in literal causes connection abort.
tc2.transactf("bad", "append inbox (\\Badflag) {1+}\r\nx") // Unknown flag.
tc2 = startNoSwitchboard(t)
tc2 = startNoSwitchboard(t, uidonly)
defer tc2.closeNoWait()
tc2.client.Login("mjl@mox.example", password0)
tc2.login("mjl@mox.example", password0)
tc2.client.Select("inbox")
tc2.transactf("bad", "append inbox () \"bad time\" {1+}\r\nx") // Bad time.
tc2 = startNoSwitchboard(t)
tc2 = startNoSwitchboard(t, uidonly)
defer tc2.closeNoWait()
tc2.client.Login("mjl@mox.example", password0)
tc2.login("mjl@mox.example", password0)
tc2.client.Select("inbox")
tc2.transactf("no", "append nobox (\\Seen) \" 1-Jan-2022 10:10:00 +0100\" {1}")
@ -52,9 +60,8 @@ func TestAppend(t *testing.T) {
tc2.xcodeArg(imapclient.CodeAppendUID{UIDValidity: 1, UIDs: xparseUIDRange("1")})
tc.transactf("ok", "noop")
uid1 := imapclient.FetchUID(1)
flags := imapclient.FetchFlags{`\Seen`, "$label2", "label1"}
tc.xuntagged(imapclient.UntaggedExists(1), imapclient.UntaggedFetch{Seq: 1, Attrs: []imapclient.FetchAttr{uid1, flags}})
tc.xuntagged(imapclient.UntaggedExists(1), tc.untaggedFetch(1, 1, flags))
tc3.transactf("ok", "noop")
tc3.xuntagged() // Inbox is not selected, nothing to report.
@ -69,7 +76,6 @@ func TestAppend(t *testing.T) {
// Messages that we cannot parse are marked as application/octet-stream. Perhaps
// the imap client knows how to deal with them.
tc2.transactf("ok", "uid fetch 2 body")
uid2 := imapclient.FetchUID(2)
xbs := imapclient.FetchBodystructure{
RespAttr: "BODY",
Body: imapclient.BodyTypeBasic{
@ -80,7 +86,7 @@ func TestAppend(t *testing.T) {
},
},
}
tc2.xuntagged(imapclient.UntaggedFetch{Seq: 2, Attrs: []imapclient.FetchAttr{uid2, xbs}})
tc2.xuntagged(tc.untaggedFetch(2, 2, xbs))
// Multiappend with two messages.
tc.transactf("ok", "noop") // Flush pending untagged responses.
@ -91,9 +97,9 @@ func TestAppend(t *testing.T) {
// Cancelled with zero-length message.
tc.transactf("no", "append inbox {6+}\r\ntest\r\n {0+}\r\n")
tclimit := startArgs(t, false, false, true, true, "limit")
tclimit := startArgs(t, uidonly, false, false, true, true, "limit")
defer tclimit.close()
tclimit.client.Login("limit@mox.example", password0)
tclimit.login("limit@mox.example", password0)
tclimit.client.Select("inbox")
// First message of 1 byte is within limits.
tclimit.transactf("ok", "append inbox (\\Seen Label1 $label2) \" 1-Jan-2022 10:10:00 +0100\" {1+}\r\nx")
@ -103,7 +109,11 @@ func TestAppend(t *testing.T) {
tclimit.xcode("OVERQUOTA")
// Empty mailbox.
tclimit.transactf("ok", `store 1 flags (\deleted)`)
if uidonly {
tclimit.transactf("ok", `uid store 1 flags (\deleted)`)
} else {
tclimit.transactf("ok", `store 1 flags (\deleted)`)
}
tclimit.transactf("ok", "expunge")
// Multiappend with first message within quota, and second message with sync