@remit/mailbox-service 0.0.4 → 0.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remit/mailbox-service",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.6",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"types": "src/index.ts",
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
},
|
|
60
60
|
"repository": {
|
|
61
61
|
"type": "git",
|
|
62
|
-
"url": "git+https://github.com/remit-mail/
|
|
62
|
+
"url": "git+https://github.com/remit-mail/reader.git",
|
|
63
63
|
"directory": "packages/mailbox-service"
|
|
64
64
|
}
|
|
65
65
|
}
|
|
@@ -151,161 +151,157 @@ const bodyText = (parsed: ParsedMail): string => {
|
|
|
151
151
|
return `${text}\n${html}`;
|
|
152
152
|
};
|
|
153
153
|
|
|
154
|
-
describe(
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
const inbox = uniqueMailboxName("E2E_adversarial");
|
|
154
|
+
describe("Adversarial MIME corpus (Dovecot) — #402", {
|
|
155
|
+
skip: !process.env.RUN_E2E_TESTS,
|
|
156
|
+
}, () => {
|
|
157
|
+
// Own mailbox instead of the shared INBOX so appending fixtures never
|
|
158
|
+
// collides with another e2e file's count/flag assertions (#508).
|
|
159
|
+
const inbox = uniqueMailboxName("E2E_adversarial");
|
|
161
160
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
});
|
|
161
|
+
before(async () => {
|
|
162
|
+
await withMailfuzzConnection(async (connection) => {
|
|
163
|
+
await connection.createMailbox(inbox);
|
|
166
164
|
});
|
|
165
|
+
});
|
|
167
166
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
167
|
+
after(async () => {
|
|
168
|
+
const connection = createMailfuzzConnection();
|
|
169
|
+
await connection.connect();
|
|
170
|
+
await connection.deleteMailbox(inbox).catch(() => {});
|
|
171
|
+
if (connection.isConnected) {
|
|
172
|
+
await connection.disconnect();
|
|
173
|
+
}
|
|
174
|
+
});
|
|
176
175
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
176
|
+
// Sanity: every .eml on disk has an expectation entry, and every
|
|
177
|
+
// expectation entry points at an existing file. Catches a fixture being
|
|
178
|
+
// added without an assertion (or vice versa).
|
|
179
|
+
test("every fixture has an expectation entry", () => {
|
|
180
|
+
const onDisk = new Set(listEmlFixtures());
|
|
181
|
+
const expected = new Set(Object.keys(EXPECTATIONS));
|
|
182
|
+
for (const name of onDisk) {
|
|
183
|
+
assert.ok(
|
|
184
|
+
expected.has(name),
|
|
185
|
+
`Fixture ${name} has no expectation entry in EXPECTATIONS`,
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
for (const name of expected) {
|
|
189
|
+
assert.ok(
|
|
190
|
+
onDisk.has(name),
|
|
191
|
+
`Expectation ${name} has no .eml file on disk`,
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
// Pin the count: #402 calls for 8 adversarial shapes.
|
|
195
|
+
assert.equal(
|
|
196
|
+
onDisk.size,
|
|
197
|
+
8,
|
|
198
|
+
"Expected exactly 8 adversarial MIME fixtures",
|
|
199
|
+
);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
for (const name of Object.keys(EXPECTATIONS).sort()) {
|
|
203
|
+
const expectation = EXPECTATIONS[name];
|
|
204
|
+
if (!expectation) continue;
|
|
205
|
+
|
|
206
|
+
test(`fixture ${name} round-trips through IMAP and parses`, async () => {
|
|
207
|
+
const raw = loadFixtureRaw(name);
|
|
208
|
+
|
|
209
|
+
await withMailfuzzConnection(async (connection) => {
|
|
210
|
+
// APPEND the fixture into this file's own mailbox. Mailfuzz uses
|
|
211
|
+
// Dovecot which returns a proper UID on APPEND (mokapi does not —
|
|
212
|
+
// see outbox-roundtrip.e2e.test.ts and the RFC 025 notes).
|
|
213
|
+
const appendResult = await connection.append(inbox, raw);
|
|
190
214
|
assert.ok(
|
|
191
|
-
|
|
192
|
-
`
|
|
215
|
+
appendResult.uid > 0,
|
|
216
|
+
`APPEND should return a UID for ${name}`,
|
|
193
217
|
);
|
|
194
|
-
}
|
|
195
|
-
// Pin the count: #402 calls for 8 adversarial shapes.
|
|
196
|
-
assert.equal(
|
|
197
|
-
onDisk.size,
|
|
198
|
-
8,
|
|
199
|
-
"Expected exactly 8 adversarial MIME fixtures",
|
|
200
|
-
);
|
|
201
|
-
});
|
|
202
218
|
|
|
203
|
-
|
|
204
|
-
const expectation = EXPECTATIONS[name];
|
|
205
|
-
if (!expectation) continue;
|
|
219
|
+
await connection.openBox(inbox, true);
|
|
206
220
|
|
|
207
|
-
|
|
208
|
-
|
|
221
|
+
// Fetch the message we just appended back. fetchMessageBody
|
|
222
|
+
// returns the raw RFC822 source; mailparser handles the rest
|
|
223
|
+
// (multipart walking, base64/qp decoding, charset folding).
|
|
224
|
+
const rfc822 = await connection.fetchMessageBody(appendResult.uid);
|
|
225
|
+
assert.ok(
|
|
226
|
+
rfc822.length > 0,
|
|
227
|
+
`fetchMessageBody should return bytes for ${name}`,
|
|
228
|
+
);
|
|
209
229
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
230
|
+
// BODYSTRUCTURE re-fetch: exercises the IMAP mapper path at the
|
|
231
|
+
// integration boundary (the exact regression class from #394).
|
|
232
|
+
// Skipped for fixtures with synthetic shapes that don't produce
|
|
233
|
+
// meaningful BODYSTRUCTURE (see skipBodyStructure flag + #408).
|
|
234
|
+
// The guard in fetchMessages skips rows with undefined uid/internalDate
|
|
235
|
+
// so back-to-back FETCH is safe on Dovecot (see imapflow-connection.ts).
|
|
236
|
+
if (!expectation.skipBodyStructure) {
|
|
237
|
+
const refetched = await connection.fetchMessages([appendResult.uid]);
|
|
215
238
|
assert.ok(
|
|
216
|
-
|
|
217
|
-
`
|
|
239
|
+
refetched.length > 0,
|
|
240
|
+
`BODYSTRUCTURE re-fetch should return a message for ${name}`,
|
|
218
241
|
);
|
|
219
|
-
|
|
220
|
-
await connection.openBox(inbox, true);
|
|
221
|
-
|
|
222
|
-
// Fetch the message we just appended back. fetchMessageBody
|
|
223
|
-
// returns the raw RFC822 source; mailparser handles the rest
|
|
224
|
-
// (multipart walking, base64/qp decoding, charset folding).
|
|
225
|
-
const rfc822 = await connection.fetchMessageBody(appendResult.uid);
|
|
226
242
|
assert.ok(
|
|
227
|
-
|
|
228
|
-
`
|
|
243
|
+
refetched[0]?.bodyStructure != null,
|
|
244
|
+
`BODYSTRUCTURE should be present for ${name}`,
|
|
229
245
|
);
|
|
246
|
+
}
|
|
230
247
|
|
|
231
|
-
|
|
232
|
-
// integration boundary (the exact regression class from #394).
|
|
233
|
-
// Skipped for fixtures with synthetic shapes that don't produce
|
|
234
|
-
// meaningful BODYSTRUCTURE (see skipBodyStructure flag + #408).
|
|
235
|
-
// The guard in fetchMessages skips rows with undefined uid/internalDate
|
|
236
|
-
// so back-to-back FETCH is safe on Dovecot (see imapflow-connection.ts).
|
|
237
|
-
if (!expectation.skipBodyStructure) {
|
|
238
|
-
const refetched = await connection.fetchMessages([
|
|
239
|
-
appendResult.uid,
|
|
240
|
-
]);
|
|
241
|
-
assert.ok(
|
|
242
|
-
refetched.length > 0,
|
|
243
|
-
`BODYSTRUCTURE re-fetch should return a message for ${name}`,
|
|
244
|
-
);
|
|
245
|
-
assert.ok(
|
|
246
|
-
refetched[0]?.bodyStructure != null,
|
|
247
|
-
`BODYSTRUCTURE should be present for ${name}`,
|
|
248
|
-
);
|
|
249
|
-
}
|
|
248
|
+
const parsed = await simpleParser(rfc822);
|
|
250
249
|
|
|
251
|
-
|
|
250
|
+
// Subject round-trips (proves we fetched the right message and
|
|
251
|
+
// any encoded-word headers decoded correctly).
|
|
252
|
+
assert.ok(
|
|
253
|
+
parsed.subject,
|
|
254
|
+
`Parsed subject should be present for ${name}`,
|
|
255
|
+
);
|
|
256
|
+
assert.ok(
|
|
257
|
+
parsed.subject?.includes(expectation.subjectIncludes),
|
|
258
|
+
`Subject "${parsed.subject}" should include "${expectation.subjectIncludes}" for ${name}`,
|
|
259
|
+
);
|
|
252
260
|
|
|
253
|
-
|
|
254
|
-
|
|
261
|
+
// Body renders — at least one of text/html is non-empty. This
|
|
262
|
+
// is the headline "body renders" assertion from #402.
|
|
263
|
+
const text = parsed.text ?? "";
|
|
264
|
+
const html = typeof parsed.html === "string" ? parsed.html : "";
|
|
265
|
+
assert.ok(
|
|
266
|
+
text.trim().length > 0 || html.trim().length > 0,
|
|
267
|
+
`Body should render (text or html non-empty) for ${name}`,
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
if (expectation.bodyContains) {
|
|
271
|
+
const haystack = bodyText(parsed).toLowerCase();
|
|
272
|
+
const needle = expectation.bodyContains.toLowerCase();
|
|
255
273
|
assert.ok(
|
|
256
|
-
|
|
257
|
-
`
|
|
274
|
+
haystack.includes(needle),
|
|
275
|
+
`Body should contain "${expectation.bodyContains}" for ${name}`,
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Attachments are listable — every expected attachment appears
|
|
280
|
+
// in parsed.attachments with a matching filename and non-zero
|
|
281
|
+
// content length. This is the "attachments listable" half of
|
|
282
|
+
// the #402 contract.
|
|
283
|
+
const attachments = parsed.attachments ?? [];
|
|
284
|
+
for (const expectedAtt of expectation.attachments) {
|
|
285
|
+
const found = attachments.find(
|
|
286
|
+
(a) => a.filename === expectedAtt.filename,
|
|
258
287
|
);
|
|
259
288
|
assert.ok(
|
|
260
|
-
|
|
261
|
-
`
|
|
289
|
+
found,
|
|
290
|
+
`Attachment "${expectedAtt.filename}" should be listable for ${name}; got [${attachments.map((a) => a.filename).join(", ")}]`,
|
|
262
291
|
);
|
|
263
|
-
|
|
264
|
-
// Body renders — at least one of text/html is non-empty. This
|
|
265
|
-
// is the headline "body renders" assertion from #402.
|
|
266
|
-
const text = parsed.text ?? "";
|
|
267
|
-
const html = typeof parsed.html === "string" ? parsed.html : "";
|
|
268
292
|
assert.ok(
|
|
269
|
-
|
|
270
|
-
`
|
|
293
|
+
(found.size ?? 0) > 0,
|
|
294
|
+
`Attachment "${expectedAtt.filename}" should have non-zero size for ${name}`,
|
|
271
295
|
);
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
haystack.includes(needle),
|
|
278
|
-
`Body should contain "${expectation.bodyContains}" for ${name}`,
|
|
279
|
-
);
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
// Attachments are listable — every expected attachment appears
|
|
283
|
-
// in parsed.attachments with a matching filename and non-zero
|
|
284
|
-
// content length. This is the "attachments listable" half of
|
|
285
|
-
// the #402 contract.
|
|
286
|
-
const attachments = parsed.attachments ?? [];
|
|
287
|
-
for (const expectedAtt of expectation.attachments) {
|
|
288
|
-
const found = attachments.find(
|
|
289
|
-
(a) => a.filename === expectedAtt.filename,
|
|
290
|
-
);
|
|
291
|
-
assert.ok(
|
|
292
|
-
found,
|
|
293
|
-
`Attachment "${expectedAtt.filename}" should be listable for ${name}; got [${attachments.map((a) => a.filename).join(", ")}]`,
|
|
296
|
+
if (expectedAtt.contentType) {
|
|
297
|
+
assert.equal(
|
|
298
|
+
found.contentType,
|
|
299
|
+
expectedAtt.contentType,
|
|
300
|
+
`Attachment "${expectedAtt.filename}" should have contentType "${expectedAtt.contentType}" for ${name}`,
|
|
294
301
|
);
|
|
295
|
-
assert.ok(
|
|
296
|
-
(found.size ?? 0) > 0,
|
|
297
|
-
`Attachment "${expectedAtt.filename}" should have non-zero size for ${name}`,
|
|
298
|
-
);
|
|
299
|
-
if (expectedAtt.contentType) {
|
|
300
|
-
assert.equal(
|
|
301
|
-
found.contentType,
|
|
302
|
-
expectedAtt.contentType,
|
|
303
|
-
`Attachment "${expectedAtt.filename}" should have contentType "${expectedAtt.contentType}" for ${name}`,
|
|
304
|
-
);
|
|
305
|
-
}
|
|
306
302
|
}
|
|
307
|
-
}
|
|
303
|
+
}
|
|
308
304
|
});
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
);
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
});
|
|
@@ -9,304 +9,284 @@ import {
|
|
|
9
9
|
withMailfuzzConnection,
|
|
10
10
|
} from "./test-helpers/mailfuzz-connection.js";
|
|
11
11
|
|
|
12
|
-
describe(
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const inbox = mailboxes.find((m) => m.fullPath === "INBOX");
|
|
25
|
-
assert.ok(inbox, "INBOX should exist in mailbox list");
|
|
26
|
-
assert.equal(inbox.name, "INBOX");
|
|
27
|
-
});
|
|
12
|
+
describe("Mailbox listing (Dovecot)", {
|
|
13
|
+
skip: !process.env.RUN_E2E_TESTS,
|
|
14
|
+
}, () => {
|
|
15
|
+
test("lists mailboxes and INBOX exists", async () => {
|
|
16
|
+
await withMailfuzzConnection(async (connection) => {
|
|
17
|
+
const mailboxes = await connection.listMailboxes();
|
|
18
|
+
|
|
19
|
+
assert.ok(mailboxes.length > 0, "Should have at least one mailbox");
|
|
20
|
+
|
|
21
|
+
const inbox = mailboxes.find((m) => m.fullPath === "INBOX");
|
|
22
|
+
assert.ok(inbox, "INBOX should exist in mailbox list");
|
|
23
|
+
assert.equal(inbox.name, "INBOX");
|
|
28
24
|
});
|
|
25
|
+
});
|
|
29
26
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
27
|
+
test("returns mailbox status with uidValidity and uidNext", async () => {
|
|
28
|
+
await withMailfuzzConnection(async (connection) => {
|
|
29
|
+
const status = await connection.getMailboxStatus("INBOX");
|
|
33
30
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
});
|
|
31
|
+
assert.ok(status.uidValidity > 0, "uidValidity should be positive");
|
|
32
|
+
assert.ok(status.uidNext > 0, "uidNext should be positive");
|
|
33
|
+
assert.ok(status.messages > 0, "INBOX should have messages");
|
|
38
34
|
});
|
|
39
|
-
}
|
|
40
|
-
);
|
|
41
|
-
|
|
42
|
-
describe(
|
|
43
|
-
"Message fetch (Dovecot)",
|
|
44
|
-
{
|
|
45
|
-
skip: !process.env.RUN_E2E_TESTS,
|
|
46
|
-
},
|
|
47
|
-
() => {
|
|
48
|
-
test("fetches messages with ENVELOPE data", async () => {
|
|
49
|
-
await withMailfuzzConnection(async (connection) => {
|
|
50
|
-
const boxStatus = await connection.openBox("INBOX", true);
|
|
51
|
-
assert.ok(boxStatus.messages.total > 0, "INBOX should have messages");
|
|
35
|
+
});
|
|
36
|
+
});
|
|
52
37
|
|
|
53
|
-
|
|
54
|
-
|
|
38
|
+
describe("Message fetch (Dovecot)", {
|
|
39
|
+
skip: !process.env.RUN_E2E_TESTS,
|
|
40
|
+
}, () => {
|
|
41
|
+
test("fetches messages with ENVELOPE data", async () => {
|
|
42
|
+
await withMailfuzzConnection(async (connection) => {
|
|
43
|
+
const boxStatus = await connection.openBox("INBOX", true);
|
|
44
|
+
assert.ok(boxStatus.messages.total > 0, "INBOX should have messages");
|
|
55
45
|
|
|
56
|
-
|
|
57
|
-
|
|
46
|
+
const uids = await connection.search(["ALL"]);
|
|
47
|
+
assert.ok(uids.length > 0, "Should find messages");
|
|
58
48
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
49
|
+
const fetchUids = uids.slice(0, 5);
|
|
50
|
+
const messages = await connection.fetchMessages(fetchUids);
|
|
51
|
+
|
|
52
|
+
assert.equal(
|
|
53
|
+
messages.length,
|
|
54
|
+
fetchUids.length,
|
|
55
|
+
"Should fetch all requested messages",
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
for (const msg of messages) {
|
|
59
|
+
assert.ok(msg.uid > 0, "Message should have a UID");
|
|
60
|
+
assert.ok(msg.envelope, "Message should have envelope data");
|
|
61
|
+
assert.ok(msg.envelope.subject, "Envelope should have a subject");
|
|
62
|
+
assert.ok(msg.envelope.messageId, "Envelope should have a messageId");
|
|
63
|
+
assert.ok(msg.envelope.date, "Envelope should have a date");
|
|
64
|
+
assert.ok(
|
|
65
|
+
msg.envelope.from.length > 0,
|
|
66
|
+
"Envelope should have from addresses",
|
|
67
|
+
);
|
|
68
|
+
assert.ok(
|
|
69
|
+
msg.envelope.to.length > 0,
|
|
70
|
+
"Envelope should have to addresses",
|
|
63
71
|
);
|
|
64
72
|
|
|
65
|
-
for (const
|
|
66
|
-
assert.ok(
|
|
67
|
-
assert.ok(
|
|
68
|
-
assert.ok(msg.envelope.subject, "Envelope should have a subject");
|
|
69
|
-
assert.ok(msg.envelope.messageId, "Envelope should have a messageId");
|
|
70
|
-
assert.ok(msg.envelope.date, "Envelope should have a date");
|
|
71
|
-
assert.ok(
|
|
72
|
-
msg.envelope.from.length > 0,
|
|
73
|
-
"Envelope should have from addresses",
|
|
74
|
-
);
|
|
75
|
-
assert.ok(
|
|
76
|
-
msg.envelope.to.length > 0,
|
|
77
|
-
"Envelope should have to addresses",
|
|
78
|
-
);
|
|
79
|
-
|
|
80
|
-
for (const addr of msg.envelope.from) {
|
|
81
|
-
assert.ok(addr.mailbox, "From address should have mailbox");
|
|
82
|
-
assert.ok(addr.host, "From address should have host");
|
|
83
|
-
}
|
|
73
|
+
for (const addr of msg.envelope.from) {
|
|
74
|
+
assert.ok(addr.mailbox, "From address should have mailbox");
|
|
75
|
+
assert.ok(addr.host, "From address should have host");
|
|
84
76
|
}
|
|
85
|
-
}
|
|
77
|
+
}
|
|
86
78
|
});
|
|
79
|
+
});
|
|
87
80
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
const uids = await connection.search(["ALL"]);
|
|
99
|
-
|
|
100
|
-
assert.equal(
|
|
101
|
-
uids.length,
|
|
102
|
-
boxStatus.messages.total,
|
|
103
|
-
"Search ALL count should match SELECT messages count",
|
|
104
|
-
);
|
|
105
|
-
} finally {
|
|
106
|
-
await connection.closeBox().catch(() => {});
|
|
107
|
-
await connection.deleteMailbox(mailbox).catch(() => {});
|
|
108
|
-
}
|
|
109
|
-
});
|
|
110
|
-
});
|
|
111
|
-
},
|
|
112
|
-
);
|
|
113
|
-
|
|
114
|
-
describe(
|
|
115
|
-
"Message bodies (Dovecot)",
|
|
116
|
-
{
|
|
117
|
-
skip: !process.env.RUN_E2E_TESTS,
|
|
118
|
-
},
|
|
119
|
-
() => {
|
|
120
|
-
test("fetches message body with text content", async () => {
|
|
121
|
-
await withMailfuzzConnection(async (connection) => {
|
|
122
|
-
await connection.openBox("INBOX", true);
|
|
81
|
+
test("fetched message count matches status", async () => {
|
|
82
|
+
// Seed a mailbox this test owns so the SELECT-vs-SEARCH invariant holds
|
|
83
|
+
// regardless of what other e2e files do in parallel — the TOCTOU race
|
|
84
|
+
// that #501 papered over with --test-concurrency=1 is removed by
|
|
85
|
+
// isolation, not serialisation (#508).
|
|
86
|
+
const mailbox = uniqueMailboxName("E2E_count");
|
|
87
|
+
await withMailfuzzConnection(async (connection) => {
|
|
88
|
+
await seedMailbox(connection, mailbox, 3);
|
|
89
|
+
try {
|
|
90
|
+
const boxStatus = await connection.openBox(mailbox, true);
|
|
123
91
|
const uids = await connection.search(["ALL"]);
|
|
124
|
-
assert.ok(uids.length > 0, "Should have messages to fetch body for");
|
|
125
|
-
|
|
126
|
-
const body = await connection.fetchMessageBody(uids[0]);
|
|
127
|
-
assert.ok(body.length > 0, "Body should not be empty");
|
|
128
92
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
93
|
+
assert.equal(
|
|
94
|
+
uids.length,
|
|
95
|
+
boxStatus.messages.total,
|
|
96
|
+
"Search ALL count should match SELECT messages count",
|
|
97
|
+
);
|
|
98
|
+
} finally {
|
|
99
|
+
await connection.closeBox().catch(() => {});
|
|
100
|
+
await connection.deleteMailbox(mailbox).catch(() => {});
|
|
101
|
+
}
|
|
132
102
|
});
|
|
133
|
-
}
|
|
134
|
-
);
|
|
135
|
-
|
|
136
|
-
describe(
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
before(async () => {
|
|
152
|
-
await withMailfuzzConnection(async (connection) => {
|
|
153
|
-
await seedMailbox(connection, mailbox, 3);
|
|
154
|
-
});
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
describe("Message bodies (Dovecot)", {
|
|
107
|
+
skip: !process.env.RUN_E2E_TESTS,
|
|
108
|
+
}, () => {
|
|
109
|
+
test("fetches message body with text content", async () => {
|
|
110
|
+
await withMailfuzzConnection(async (connection) => {
|
|
111
|
+
await connection.openBox("INBOX", true);
|
|
112
|
+
const uids = await connection.search(["ALL"]);
|
|
113
|
+
assert.ok(uids.length > 0, "Should have messages to fetch body for");
|
|
114
|
+
|
|
115
|
+
const body = await connection.fetchMessageBody(uids[0]);
|
|
116
|
+
assert.ok(body.length > 0, "Body should not be empty");
|
|
117
|
+
|
|
118
|
+
const bodyText = body.toString("utf-8");
|
|
119
|
+
assert.ok(bodyText.length > 0, "Body text should not be empty");
|
|
155
120
|
});
|
|
121
|
+
});
|
|
122
|
+
});
|
|
156
123
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
124
|
+
describe("Flag operations (Dovecot)", {
|
|
125
|
+
skip: !process.env.RUN_E2E_TESTS,
|
|
126
|
+
// These three subtests all mutate uids[0] of the same seeded mailbox, so
|
|
127
|
+
// they must run sequentially relative to each other. node:test already
|
|
128
|
+
// serialises subtests within a describe; concurrency: 1 pins that intent.
|
|
129
|
+
// Cross-file isolation (a dedicated seeded mailbox, not the shared INBOX)
|
|
130
|
+
// is what makes the whole suite safe to run in parallel — see #508.
|
|
131
|
+
concurrency: 1,
|
|
132
|
+
}, () => {
|
|
133
|
+
let targetUid: number;
|
|
134
|
+
const mailbox = uniqueMailboxName("E2E_flags");
|
|
135
|
+
|
|
136
|
+
before(async () => {
|
|
137
|
+
await withMailfuzzConnection(async (connection) => {
|
|
138
|
+
await seedMailbox(connection, mailbox, 3);
|
|
164
139
|
});
|
|
140
|
+
});
|
|
165
141
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
142
|
+
after(async () => {
|
|
143
|
+
const connection = createMailfuzzConnection();
|
|
144
|
+
await connection.connect();
|
|
145
|
+
await connection.deleteMailbox(mailbox).catch(() => {});
|
|
146
|
+
if (connection.isConnected) {
|
|
147
|
+
await connection.disconnect();
|
|
148
|
+
}
|
|
149
|
+
});
|
|
172
150
|
|
|
173
|
-
|
|
174
|
-
|
|
151
|
+
test("sets \\Seen flag and verifies it persists", async () => {
|
|
152
|
+
await withMailfuzzConnection(async (connection) => {
|
|
153
|
+
await connection.openBox(mailbox, false);
|
|
154
|
+
const uids = await connection.search(["ALL"]);
|
|
155
|
+
assert.ok(uids.length > 0, "Should have messages");
|
|
156
|
+
targetUid = uids[0];
|
|
175
157
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
158
|
+
await connection.removeFlags([targetUid], ["\\Seen"]);
|
|
159
|
+
await connection.addFlags([targetUid], ["\\Seen"]);
|
|
160
|
+
|
|
161
|
+
const messages = await connection.fetchMessages([targetUid]);
|
|
162
|
+
assert.equal(messages.length, 1);
|
|
163
|
+
assert.ok(
|
|
164
|
+
messages[0].flags.includes("\\Seen"),
|
|
165
|
+
"Message should have \\Seen flag",
|
|
166
|
+
);
|
|
183
167
|
});
|
|
168
|
+
});
|
|
184
169
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
170
|
+
test("clears \\Seen flag and verifies it is cleared", async () => {
|
|
171
|
+
await withMailfuzzConnection(async (connection) => {
|
|
172
|
+
await connection.openBox(mailbox, false);
|
|
173
|
+
const uids = await connection.search(["ALL"]);
|
|
174
|
+
targetUid = uids[0];
|
|
190
175
|
|
|
191
|
-
|
|
192
|
-
|
|
176
|
+
await connection.addFlags([targetUid], ["\\Seen"]);
|
|
177
|
+
await connection.removeFlags([targetUid], ["\\Seen"]);
|
|
193
178
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
});
|
|
179
|
+
const messages = await connection.fetchMessages([targetUid]);
|
|
180
|
+
assert.equal(messages.length, 1);
|
|
181
|
+
assert.ok(
|
|
182
|
+
!messages[0].flags.includes("\\Seen"),
|
|
183
|
+
"Message should not have \\Seen flag after removal",
|
|
184
|
+
);
|
|
201
185
|
});
|
|
186
|
+
});
|
|
202
187
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
188
|
+
test("sets custom keyword flag", async () => {
|
|
189
|
+
await withMailfuzzConnection(async (connection) => {
|
|
190
|
+
await connection.openBox(mailbox, false);
|
|
191
|
+
const uids = await connection.search(["ALL"]);
|
|
192
|
+
targetUid = uids[0];
|
|
208
193
|
|
|
209
|
-
|
|
194
|
+
await connection.addFlags([targetUid], ["$label1"]);
|
|
210
195
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
196
|
+
const messages = await connection.fetchMessages([targetUid]);
|
|
197
|
+
assert.equal(messages.length, 1);
|
|
198
|
+
assert.ok(
|
|
199
|
+
messages[0].flags.includes("$label1"),
|
|
200
|
+
"Message should have custom keyword $label1",
|
|
201
|
+
);
|
|
217
202
|
|
|
218
|
-
|
|
219
|
-
});
|
|
203
|
+
await connection.removeFlags([targetUid], ["$label1"]);
|
|
220
204
|
});
|
|
221
|
-
}
|
|
222
|
-
);
|
|
223
|
-
|
|
224
|
-
describe(
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
()
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
before(async () => {
|
|
236
|
-
await withMailfuzzConnection(async (connection) => {
|
|
237
|
-
await seedMailbox(connection, sourceFolder, 3);
|
|
238
|
-
});
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
describe("Message operations (Dovecot)", {
|
|
209
|
+
skip: !process.env.RUN_E2E_TESTS,
|
|
210
|
+
}, () => {
|
|
211
|
+
const testFolder = uniqueMailboxName("E2E_TestCopy");
|
|
212
|
+
// Copy/move source their messages from a mailbox this file owns rather than
|
|
213
|
+
// the shared INBOX, so nothing here appends to or mutates INBOX (#508).
|
|
214
|
+
const sourceFolder = uniqueMailboxName("E2E_TestSource");
|
|
215
|
+
|
|
216
|
+
before(async () => {
|
|
217
|
+
await withMailfuzzConnection(async (connection) => {
|
|
218
|
+
await seedMailbox(connection, sourceFolder, 3);
|
|
239
219
|
});
|
|
220
|
+
});
|
|
240
221
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
222
|
+
test("copy returns COPYUID response with uidMap entries", async () => {
|
|
223
|
+
await withMailfuzzConnection(async (connection) => {
|
|
224
|
+
await connection.createMailbox(testFolder);
|
|
244
225
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
226
|
+
await connection.openBox(sourceFolder, false);
|
|
227
|
+
const uids = await connection.search(["ALL"]);
|
|
228
|
+
assert.ok(uids.length > 0, "Should have messages to copy");
|
|
248
229
|
|
|
249
|
-
|
|
230
|
+
const result = await connection.copyMessages([uids[0]], testFolder);
|
|
250
231
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
});
|
|
232
|
+
assert.equal(result.destination, testFolder);
|
|
233
|
+
assert.ok(
|
|
234
|
+
result.uidMap.size > 0,
|
|
235
|
+
"COPYUID response should have uidMap entries",
|
|
236
|
+
);
|
|
237
|
+
assert.ok(
|
|
238
|
+
result.uidMap.has(uids[0]),
|
|
239
|
+
"uidMap should contain the source UID",
|
|
240
|
+
);
|
|
261
241
|
});
|
|
242
|
+
});
|
|
262
243
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
await withMailfuzzConnection(async (connection) => {
|
|
267
|
-
await connection.createMailbox(moveFolder);
|
|
268
|
-
|
|
269
|
-
const appendResult = await connection.append(
|
|
270
|
-
sourceFolder,
|
|
271
|
-
[
|
|
272
|
-
"From: test@example.com",
|
|
273
|
-
"To: vmail@localhost",
|
|
274
|
-
"Subject: Move test message",
|
|
275
|
-
`Date: ${new Date().toUTCString()}`,
|
|
276
|
-
`Message-ID: <move-test-${Date.now()}@test.example.com>`,
|
|
277
|
-
"",
|
|
278
|
-
"Body for move test",
|
|
279
|
-
].join("\r\n"),
|
|
280
|
-
);
|
|
244
|
+
test("move returns COPYUID response with uidMap entries", async () => {
|
|
245
|
+
const moveFolder = uniqueMailboxName("E2E_TestMove");
|
|
281
246
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
247
|
+
await withMailfuzzConnection(async (connection) => {
|
|
248
|
+
await connection.createMailbox(moveFolder);
|
|
249
|
+
|
|
250
|
+
const appendResult = await connection.append(
|
|
251
|
+
sourceFolder,
|
|
252
|
+
[
|
|
253
|
+
"From: test@example.com",
|
|
254
|
+
"To: vmail@localhost",
|
|
255
|
+
"Subject: Move test message",
|
|
256
|
+
`Date: ${new Date().toUTCString()}`,
|
|
257
|
+
`Message-ID: <move-test-${Date.now()}@test.example.com>`,
|
|
258
|
+
"",
|
|
259
|
+
"Body for move test",
|
|
260
|
+
].join("\r\n"),
|
|
261
|
+
);
|
|
287
262
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
263
|
+
await connection.openBox(sourceFolder, false);
|
|
264
|
+
const result = await connection.moveMessages(
|
|
265
|
+
[appendResult.uid],
|
|
266
|
+
moveFolder,
|
|
267
|
+
);
|
|
293
268
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
269
|
+
assert.equal(result.destination, moveFolder);
|
|
270
|
+
assert.ok(
|
|
271
|
+
result.uidMap.size > 0,
|
|
272
|
+
"COPYUID response should have uidMap entries",
|
|
273
|
+
);
|
|
298
274
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
await connection.connect();
|
|
302
|
-
await connection.deleteMailbox(testFolder).catch(() => {});
|
|
303
|
-
await connection.deleteMailbox(sourceFolder).catch(() => {});
|
|
304
|
-
if (connection.isConnected) {
|
|
305
|
-
await connection.disconnect();
|
|
306
|
-
}
|
|
275
|
+
await connection.closeBox();
|
|
276
|
+
await connection.deleteMailbox(moveFolder);
|
|
307
277
|
});
|
|
308
|
-
}
|
|
309
|
-
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
after(async () => {
|
|
281
|
+
const connection = createMailfuzzConnection();
|
|
282
|
+
await connection.connect();
|
|
283
|
+
await connection.deleteMailbox(testFolder).catch(() => {});
|
|
284
|
+
await connection.deleteMailbox(sourceFolder).catch(() => {});
|
|
285
|
+
if (connection.isConnected) {
|
|
286
|
+
await connection.disconnect();
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
});
|
|
310
290
|
|
|
311
291
|
describe("Mailbox CRUD (Dovecot)", { skip: !process.env.RUN_E2E_TESTS }, () => {
|
|
312
292
|
const crudFolder = `CrudTest_${Date.now()}`;
|
|
@@ -134,194 +134,183 @@ const getMailboxUids = async (mailbox: string): Promise<number[]> => {
|
|
|
134
134
|
return uids;
|
|
135
135
|
};
|
|
136
136
|
|
|
137
|
-
describe(
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
// mokapi doesn't return uidMap, but should return destination
|
|
160
|
-
assert.equal(
|
|
161
|
-
result.destination,
|
|
162
|
-
"Work",
|
|
163
|
-
"Destination should be Work",
|
|
164
|
-
);
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
// Verify message moved using fresh connections (mokapi caching workaround)
|
|
168
|
-
const inboxUids = await getMailboxUids("INBOX");
|
|
169
|
-
assert.ok(
|
|
170
|
-
!inboxUids.includes(uid),
|
|
171
|
-
"Original UID should not be in INBOX",
|
|
172
|
-
);
|
|
173
|
-
|
|
174
|
-
const workCountAfter = await countMessagesInMailbox("Work");
|
|
175
|
-
assert.ok(
|
|
176
|
-
workCountAfter > workCountBefore,
|
|
177
|
-
`Work folder should have more messages (before: ${workCountBefore}, after: ${workCountAfter})`,
|
|
178
|
-
);
|
|
137
|
+
describe("ImapFlowConnection integration tests", {
|
|
138
|
+
skip: !process.env.RUN_INTEG_TESTS,
|
|
139
|
+
}, () => {
|
|
140
|
+
describe("moveMessages", () => {
|
|
141
|
+
test("moves a message to another mailbox", async () => {
|
|
142
|
+
// Seed the test message first
|
|
143
|
+
const subject = `Move test ${Date.now()}`;
|
|
144
|
+
const uid = await seedTestMessage("INBOX", subject);
|
|
145
|
+
|
|
146
|
+
// Count messages in Work before
|
|
147
|
+
const workCountBefore = await countMessagesInMailbox("Work");
|
|
148
|
+
|
|
149
|
+
await withConnection(async (connection) => {
|
|
150
|
+
// Open INBOX
|
|
151
|
+
await connection.openBox("INBOX", false);
|
|
152
|
+
|
|
153
|
+
// Move to Work folder
|
|
154
|
+
const result = await connection.moveMessages([uid], "Work");
|
|
155
|
+
|
|
156
|
+
// mokapi doesn't return uidMap, but should return destination
|
|
157
|
+
assert.equal(result.destination, "Work", "Destination should be Work");
|
|
179
158
|
});
|
|
180
159
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
160
|
+
// Verify message moved using fresh connections (mokapi caching workaround)
|
|
161
|
+
const inboxUids = await getMailboxUids("INBOX");
|
|
162
|
+
assert.ok(
|
|
163
|
+
!inboxUids.includes(uid),
|
|
164
|
+
"Original UID should not be in INBOX",
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
const workCountAfter = await countMessagesInMailbox("Work");
|
|
168
|
+
assert.ok(
|
|
169
|
+
workCountAfter > workCountBefore,
|
|
170
|
+
`Work folder should have more messages (before: ${workCountBefore}, after: ${workCountAfter})`,
|
|
171
|
+
);
|
|
172
|
+
});
|
|
184
173
|
|
|
185
|
-
|
|
174
|
+
test("returns empty uidMap when no UIDs provided", async () => {
|
|
175
|
+
await withConnection(async (connection) => {
|
|
176
|
+
await connection.openBox("INBOX", false);
|
|
186
177
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
178
|
+
const result = await connection.moveMessages([], "Work");
|
|
179
|
+
|
|
180
|
+
assert.equal(result.destination, "Work");
|
|
181
|
+
assert.equal(result.uidMap.size, 0);
|
|
190
182
|
});
|
|
191
183
|
});
|
|
184
|
+
});
|
|
192
185
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
// Count messages in Archive before
|
|
200
|
-
const archiveCountBefore = await countMessagesInMailbox("Archive");
|
|
201
|
-
|
|
202
|
-
await withConnection(async (connection) => {
|
|
203
|
-
// Open INBOX
|
|
204
|
-
await connection.openBox("INBOX", false);
|
|
205
|
-
|
|
206
|
-
// Copy to Archive folder
|
|
207
|
-
const result = await connection.copyMessages([uid], "Archive");
|
|
208
|
-
|
|
209
|
-
assert.equal(
|
|
210
|
-
result.destination,
|
|
211
|
-
"Archive",
|
|
212
|
-
"Destination should be Archive",
|
|
213
|
-
);
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
// Verify original message is still in INBOX
|
|
217
|
-
const inboxUids = await getMailboxUids("INBOX");
|
|
218
|
-
assert.ok(
|
|
219
|
-
inboxUids.includes(uid),
|
|
220
|
-
"Original UID should still be in INBOX",
|
|
221
|
-
);
|
|
186
|
+
describe("copyMessages", () => {
|
|
187
|
+
test("copies a message to another mailbox", async () => {
|
|
188
|
+
// Seed the test message first
|
|
189
|
+
const subject = `Copy test ${Date.now()}`;
|
|
190
|
+
const uid = await seedTestMessage("INBOX", subject);
|
|
222
191
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
192
|
+
// Count messages in Archive before
|
|
193
|
+
const archiveCountBefore = await countMessagesInMailbox("Archive");
|
|
194
|
+
|
|
195
|
+
await withConnection(async (connection) => {
|
|
196
|
+
// Open INBOX
|
|
197
|
+
await connection.openBox("INBOX", false);
|
|
198
|
+
|
|
199
|
+
// Copy to Archive folder
|
|
200
|
+
const result = await connection.copyMessages([uid], "Archive");
|
|
201
|
+
|
|
202
|
+
assert.equal(
|
|
203
|
+
result.destination,
|
|
204
|
+
"Archive",
|
|
205
|
+
"Destination should be Archive",
|
|
228
206
|
);
|
|
207
|
+
});
|
|
229
208
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
209
|
+
// Verify original message is still in INBOX
|
|
210
|
+
const inboxUids = await getMailboxUids("INBOX");
|
|
211
|
+
assert.ok(
|
|
212
|
+
inboxUids.includes(uid),
|
|
213
|
+
"Original UID should still be in INBOX",
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
// Verify copy is in Archive (count increased)
|
|
217
|
+
const archiveCountAfter = await countMessagesInMailbox("Archive");
|
|
218
|
+
assert.ok(
|
|
219
|
+
archiveCountAfter > archiveCountBefore,
|
|
220
|
+
`Archive folder should have more messages (before: ${archiveCountBefore}, after: ${archiveCountAfter})`,
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
// Cleanup: delete the original from INBOX
|
|
224
|
+
await withConnection(async (connection) => {
|
|
225
|
+
await connection.openBox("INBOX", false);
|
|
226
|
+
await connection.deleteMessages([uid]);
|
|
235
227
|
});
|
|
228
|
+
});
|
|
236
229
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
230
|
+
test("returns empty uidMap when no UIDs provided", async () => {
|
|
231
|
+
await withConnection(async (connection) => {
|
|
232
|
+
await connection.openBox("INBOX", false);
|
|
240
233
|
|
|
241
|
-
|
|
234
|
+
const result = await connection.copyMessages([], "Archive");
|
|
242
235
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
});
|
|
236
|
+
assert.equal(result.destination, "Archive");
|
|
237
|
+
assert.equal(result.uidMap.size, 0);
|
|
246
238
|
});
|
|
247
239
|
});
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
describe("deleteMessages", () => {
|
|
243
|
+
test("permanently deletes a message", async () => {
|
|
244
|
+
// Seed the test message first
|
|
245
|
+
const subject = `Delete test ${Date.now()}`;
|
|
246
|
+
const uid = await seedTestMessage("INBOX", subject);
|
|
248
247
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
await connection.closeBox();
|
|
270
|
-
|
|
271
|
-
// Verify within same connection
|
|
272
|
-
await connection.openBox("INBOX", true);
|
|
273
|
-
uids = await connection.search(["ALL"]);
|
|
274
|
-
assert.ok(
|
|
275
|
-
!uids.includes(uid),
|
|
276
|
-
"Message should not exist after delete",
|
|
277
|
-
);
|
|
278
|
-
});
|
|
248
|
+
// Verify message exists
|
|
249
|
+
let uids = await getMailboxUids("INBOX");
|
|
250
|
+
assert.ok(uids.includes(uid), "Message should exist before delete");
|
|
251
|
+
|
|
252
|
+
await withConnection(async (connection) => {
|
|
253
|
+
// Open INBOX
|
|
254
|
+
await connection.openBox("INBOX", false);
|
|
255
|
+
|
|
256
|
+
// Delete the message
|
|
257
|
+
const deleted = await connection.deleteMessages([uid]);
|
|
258
|
+
// mokapi may return true instead of count
|
|
259
|
+
assert.ok(deleted, "Should report message deleted");
|
|
260
|
+
|
|
261
|
+
// mokapi requires mailbox close for delete to persist
|
|
262
|
+
await connection.closeBox();
|
|
263
|
+
|
|
264
|
+
// Verify within same connection
|
|
265
|
+
await connection.openBox("INBOX", true);
|
|
266
|
+
uids = await connection.search(["ALL"]);
|
|
267
|
+
assert.ok(!uids.includes(uid), "Message should not exist after delete");
|
|
279
268
|
});
|
|
269
|
+
});
|
|
280
270
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
271
|
+
test("returns 0 when no UIDs provided", async () => {
|
|
272
|
+
await withConnection(async (connection) => {
|
|
273
|
+
await connection.openBox("INBOX", false);
|
|
284
274
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
});
|
|
275
|
+
const deleted = await connection.deleteMessages([]);
|
|
276
|
+
assert.equal(deleted, 0);
|
|
288
277
|
});
|
|
289
278
|
});
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
describe("move to Trash workflow", () => {
|
|
282
|
+
test("moves message to Trash folder", async () => {
|
|
283
|
+
// Seed the test message first
|
|
284
|
+
const subject = `Trash test ${Date.now()}`;
|
|
285
|
+
const uid = await seedTestMessage("INBOX", subject);
|
|
290
286
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
const result = await connection.moveMessages([uid], "Trash");
|
|
306
|
-
|
|
307
|
-
assert.equal(
|
|
308
|
-
result.destination,
|
|
309
|
-
"Trash",
|
|
310
|
-
"Destination should be Trash",
|
|
311
|
-
);
|
|
312
|
-
});
|
|
313
|
-
|
|
314
|
-
// Verify message is no longer in INBOX
|
|
315
|
-
const inboxUids = await getMailboxUids("INBOX");
|
|
316
|
-
assert.ok(!inboxUids.includes(uid), "Message should not be in INBOX");
|
|
317
|
-
|
|
318
|
-
// Verify message is in Trash (count increased)
|
|
319
|
-
const trashCountAfter = await countMessagesInMailbox("Trash");
|
|
320
|
-
assert.ok(
|
|
321
|
-
trashCountAfter > trashCountBefore,
|
|
322
|
-
`Trash folder should have more messages (before: ${trashCountBefore}, after: ${trashCountAfter})`,
|
|
287
|
+
// Count messages in Trash before
|
|
288
|
+
const trashCountBefore = await countMessagesInMailbox("Trash");
|
|
289
|
+
|
|
290
|
+
await withConnection(async (connection) => {
|
|
291
|
+
// Open INBOX
|
|
292
|
+
await connection.openBox("INBOX", false);
|
|
293
|
+
|
|
294
|
+
// Move to Trash
|
|
295
|
+
const result = await connection.moveMessages([uid], "Trash");
|
|
296
|
+
|
|
297
|
+
assert.equal(
|
|
298
|
+
result.destination,
|
|
299
|
+
"Trash",
|
|
300
|
+
"Destination should be Trash",
|
|
323
301
|
);
|
|
324
302
|
});
|
|
303
|
+
|
|
304
|
+
// Verify message is no longer in INBOX
|
|
305
|
+
const inboxUids = await getMailboxUids("INBOX");
|
|
306
|
+
assert.ok(!inboxUids.includes(uid), "Message should not be in INBOX");
|
|
307
|
+
|
|
308
|
+
// Verify message is in Trash (count increased)
|
|
309
|
+
const trashCountAfter = await countMessagesInMailbox("Trash");
|
|
310
|
+
assert.ok(
|
|
311
|
+
trashCountAfter > trashCountBefore,
|
|
312
|
+
`Trash folder should have more messages (before: ${trashCountBefore}, after: ${trashCountAfter})`,
|
|
313
|
+
);
|
|
325
314
|
});
|
|
326
|
-
}
|
|
327
|
-
);
|
|
315
|
+
});
|
|
316
|
+
});
|