1package imapserver
2
3import (
4 "testing"
5
6 "github.com/mjl-/mox/imapclient"
7)
8
9func TestReplace(t *testing.T) {
10 testReplace(t, false)
11}
12
13func TestReplaceUIDOnly(t *testing.T) {
14 testReplace(t, true)
15}
16
17func testReplace(t *testing.T, uidonly bool) {
18 defer mockUIDValidity()()
19
20 tc := start(t, uidonly)
21 defer tc.close()
22
23 tc2 := startNoSwitchboard(t, uidonly)
24 defer tc2.closeNoWait()
25
26 tc.login("mjl@mox.example", password0)
27 tc.client.Select("inbox")
28
29 // Star not allowed on empty mailbox.
30 tc.transactf("bad", "uid replace * inbox {1}")
31 if !uidonly {
32 tc.transactf("bad", "replace * inbox {1}")
33 }
34
35 // Append 3 messages, remove first. Leaves msgseq 1,2 with uid 2,3.
36 tc.client.MultiAppend("inbox", makeAppend(exampleMsg), makeAppend(exampleMsg), makeAppend(exampleMsg))
37 tc.client.UIDStoreFlagsSet("1", true, `\deleted`)
38 tc.client.Expunge()
39
40 tc.transactf("no", "uid replace 1 expungebox {1}") // Mailbox no longer exists.
41 tc.xcodeWord("TRYCREATE")
42
43 tc2.login("mjl@mox.example", password0)
44 tc2.client.Select("inbox")
45
46 // Replace last message (msgseq 2, uid 3) in same mailbox.
47 if uidonly {
48 tc.lastResponse, tc.lastErr = tc.client.UIDReplace("3", "INBOX", makeAppend(searchMsg))
49 } else {
50 tc.lastResponse, tc.lastErr = tc.client.MSNReplace("2", "INBOX", makeAppend(searchMsg))
51 }
52 tcheck(tc.t, tc.lastErr, "read imap response")
53 if uidonly {
54 tc.xuntagged(
55 imapclient.UntaggedResult{Status: "OK", Code: imapclient.CodeAppendUID{UIDValidity: 1, UIDs: xparseUIDRange("4")}, Text: ""},
56 imapclient.UntaggedExists(3),
57 imapclient.UntaggedVanished{UIDs: xparseNumSet("3")},
58 )
59 } else {
60 tc.xuntagged(
61 imapclient.UntaggedResult{Status: "OK", Code: imapclient.CodeAppendUID{UIDValidity: 1, UIDs: xparseUIDRange("4")}, Text: ""},
62 imapclient.UntaggedExists(3),
63 imapclient.UntaggedExpunge(2),
64 )
65 }
66 tc.xcode(imapclient.CodeHighestModSeq(8))
67
68 // Check that other client sees Exists and Expunge.
69 tc2.transactf("ok", "noop")
70 if uidonly {
71 tc2.xuntagged(
72 imapclient.UntaggedVanished{UIDs: xparseNumSet("3")},
73 imapclient.UntaggedExists(2),
74 tc.untaggedFetch(2, 4, imapclient.FetchFlags(nil)),
75 )
76 } else {
77 tc2.xuntagged(
78 imapclient.UntaggedExpunge(2),
79 imapclient.UntaggedExists(2),
80 tc.untaggedFetch(2, 4, imapclient.FetchFlags(nil)),
81 )
82 }
83
84 // Enable qresync, replace uid 2 (msgseq 1) to different mailbox, see that we get vanished instead of expunged.
85 tc.transactf("ok", "enable qresync")
86 tc.lastResponse, tc.lastErr = tc.client.UIDReplace("2", "INBOX", makeAppend(searchMsg))
87 tcheck(tc.t, tc.lastErr, "read imap response")
88 tc.xuntagged(
89 imapclient.UntaggedResult{Status: "OK", Code: imapclient.CodeAppendUID{UIDValidity: 1, UIDs: xparseUIDRange("5")}, Text: ""},
90 imapclient.UntaggedExists(3),
91 imapclient.UntaggedVanished{UIDs: xparseNumSet("2")},
92 )
93 tc.xcode(imapclient.CodeHighestModSeq(9))
94
95 // Use "*" for replacing.
96 tc.transactf("ok", "uid replace * inbox {1+}\r\nx")
97 tc.xuntagged(
98 imapclient.UntaggedResult{Status: "OK", Code: imapclient.CodeAppendUID{UIDValidity: 1, UIDs: xparseUIDRange("6")}, Text: ""},
99 imapclient.UntaggedExists(3),
100 imapclient.UntaggedVanished{UIDs: xparseNumSet("5")},
101 )
102 if !uidonly {
103 tc.transactf("ok", "replace * inbox {1+}\r\ny")
104 tc.xuntagged(
105 imapclient.UntaggedResult{Status: "OK", Code: imapclient.CodeAppendUID{UIDValidity: 1, UIDs: xparseUIDRange("7")}, Text: ""},
106 imapclient.UntaggedExists(3),
107 imapclient.UntaggedVanished{UIDs: xparseNumSet("6")},
108 )
109 }
110
111 // Non-existent mailbox with non-synchronizing literal should consume the literal.
112 if uidonly {
113 tc.transactf("no", "uid replace 1 bogusbox {1+}\r\nx")
114 } else {
115 tc.transactf("no", "replace 1 bogusbox {1+}\r\nx")
116 }
117
118 // Leftover data.
119 tc.transactf("bad", "replace 1 inbox () {6+}\r\ntest\r\n ")
120}
121
122func TestReplaceBigNonsyncLit(t *testing.T) {
123 tc := start(t, false)
124 defer tc.close()
125
126 tc.login("mjl@mox.example", password0)
127 tc.client.Select("inbox")
128
129 // Adding a message >1mb with non-sync literal to non-existent mailbox should abort entire connection.
130 tc.transactf("bad", "replace 12345 inbox {2000000+}")
131 tc.xuntagged(
132 imapclient.UntaggedBye{Code: imapclient.CodeWord("ALERT"), Text: "error condition and non-synchronizing literal too big"},
133 )
134 tc.xcodeWord("TOOBIG")
135}
136
137func TestReplaceQuota(t *testing.T) {
138 testReplaceQuota(t, false)
139}
140
141func TestReplaceQuotaUIDOnly(t *testing.T) {
142 testReplaceQuota(t, true)
143}
144
145func testReplaceQuota(t *testing.T, uidonly bool) {
146 // with quota limit
147 tc := startArgs(t, uidonly, true, false, true, true, "limit")
148 defer tc.close()
149
150 tc.login("limit@mox.example", password0)
151 tc.client.Select("inbox")
152 tc.client.Append("inbox", makeAppend("x"))
153
154 // Synchronizing literal, we get failure immediately.
155 tc.transactf("no", "uid replace 1 inbox {6}\r\n")
156 tc.xcodeWord("OVERQUOTA")
157
158 // Synchronizing literal to non-existent mailbox, we get failure immediately.
159 tc.transactf("no", "uid replace 1 badbox {6}\r\n")
160 tc.xcodeWord("TRYCREATE")
161
162 buf := make([]byte, 4000, 4002)
163 for i := range buf {
164 buf[i] = 'x'
165 }
166 buf = append(buf, "\r\n"...)
167
168 // Non-synchronizing literal. We get to write our data.
169 tc.client.WriteCommandf("", "uid replace 1 inbox ~{4000+}")
170 _, err := tc.client.Write(buf)
171 tc.check(err, "write replace message")
172 tc.response("no")
173 tc.xcodeWord("OVERQUOTA")
174
175 // Non-synchronizing literal to bad mailbox.
176 tc.client.WriteCommandf("", "uid replace 1 badbox {4000+}")
177 _, err = tc.client.Write(buf)
178 tc.check(err, "write replace message")
179 tc.response("no")
180 tc.xcodeWord("TRYCREATE")
181}
182
183func TestReplaceExpunged(t *testing.T) {
184 testReplaceExpunged(t, false)
185}
186
187func TestReplaceExpungedUIDOnly(t *testing.T) {
188 testReplaceExpunged(t, true)
189}
190
191func testReplaceExpunged(t *testing.T, uidonly bool) {
192 tc := start(t, uidonly)
193 defer tc.close()
194
195 tc.login("mjl@mox.example", password0)
196 tc.client.Select("inbox")
197 tc.client.Append("inbox", makeAppend(exampleMsg))
198
199 // We start the command, but don't write data yet.
200 tc.client.WriteCommandf("", "uid replace 1 inbox {4000}")
201
202 // Get in with second client and remove the message we are replacing.
203 tc2 := startNoSwitchboard(t, uidonly)
204 defer tc2.closeNoWait()
205 tc2.login("mjl@mox.example", password0)
206 tc2.client.Select("inbox")
207 tc2.client.UIDStoreFlagsSet("1", true, `\Deleted`)
208 tc2.client.Expunge()
209 tc2.client.Unselect()
210 tc2.client.Close()
211
212 // Now continue trying to replace the message. We should get an error and an expunge.
213 tc.readprefixline("+ ")
214 buf := make([]byte, 4000, 4002)
215 for i := range buf {
216 buf[i] = 'x'
217 }
218 buf = append(buf, "\r\n"...)
219 _, err := tc.client.Write(buf)
220 tc.check(err, "write replace message")
221 tc.response("no")
222 if uidonly {
223 tc.xuntagged(
224 tc.untaggedFetch(1, 1, imapclient.FetchFlags{`\Deleted`}),
225 imapclient.UntaggedVanished{UIDs: xparseNumSet("1")},
226 )
227 } else {
228 tc.xuntagged(
229 tc.untaggedFetch(1, 1, imapclient.FetchFlags{`\Deleted`}),
230 imapclient.UntaggedExpunge(1),
231 )
232 }
233}
234