@remit/mailbox-service 0.0.72 → 0.0.73
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
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
MessageMoveService,
|
|
14
14
|
NoTrashMailboxError,
|
|
15
15
|
StaleTrashAppointmentError,
|
|
16
|
+
UnconfirmedTrashMailboxError,
|
|
16
17
|
} from "./message-move.js";
|
|
17
18
|
|
|
18
19
|
const ACCOUNT = "acc-1";
|
|
@@ -200,14 +201,34 @@ describe("delete only expunges when it was asked to", () => {
|
|
|
200
201
|
);
|
|
201
202
|
});
|
|
202
203
|
|
|
203
|
-
it("
|
|
204
|
-
//
|
|
205
|
-
//
|
|
206
|
-
|
|
204
|
+
it("refuses to expunge a message already inside a Trash that only resolves by name", async () => {
|
|
205
|
+
// #876: a message already sitting in the guessed folder used to fall
|
|
206
|
+
// straight through to an expunge, on the same name guess Empty Trash
|
|
207
|
+
// refuses to act on. Deleting it is exactly as unrecoverable, so it
|
|
208
|
+
// demands the same confirmed evidence — nobody, neither the user nor the
|
|
209
|
+
// server, ever said this folder is Trash.
|
|
210
|
+
const { service, patches, events, message } = buildWorld(
|
|
207
211
|
{ kind: "proposed", mailbox: { mailboxId: TRASH, fullPath: "Trash" } },
|
|
208
212
|
TRASH,
|
|
209
213
|
);
|
|
210
214
|
|
|
215
|
+
await assert.rejects(
|
|
216
|
+
() => service.deleteMessages(ACCOUNT_CONFIG, [MESSAGE_ID], ACCOUNT),
|
|
217
|
+
(error: unknown) =>
|
|
218
|
+
error instanceof UnconfirmedTrashMailboxError &&
|
|
219
|
+
error.statusCode === 409 &&
|
|
220
|
+
error.publicApiError?.details?.reason === "unconfirmed" &&
|
|
221
|
+
error.publicApiError?.details?.accountId === ACCOUNT,
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
assert.deepEqual(events, []);
|
|
225
|
+
assert.deepEqual(patches, []);
|
|
226
|
+
assert.equal(message.mailboxId, TRASH);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it("expunges a message already inside a confirmed Trash", async () => {
|
|
230
|
+
const { service, events } = buildWorld(flaggedTrash, TRASH);
|
|
231
|
+
|
|
211
232
|
await service.deleteMessages(ACCOUNT_CONFIG, [MESSAGE_ID], ACCOUNT);
|
|
212
233
|
|
|
213
234
|
assert.deepEqual(
|
package/src/message-move.ts
CHANGED
|
@@ -162,7 +162,8 @@ export class StaleTrashAppointmentError extends FolderRoleUnresolvedError {
|
|
|
162
162
|
|
|
163
163
|
/**
|
|
164
164
|
* A folder resolves as Trash by its name alone. Enough to move mail into,
|
|
165
|
-
* never enough to expunge —
|
|
165
|
+
* never enough to expunge — raised by Empty Trash and by a delete that would
|
|
166
|
+
* expunge a message already sitting in that folder.
|
|
166
167
|
*/
|
|
167
168
|
export class UnconfirmedTrashMailboxError extends FolderRoleUnresolvedError {
|
|
168
169
|
name = "UnconfirmedTrashMailboxError";
|
|
@@ -307,12 +308,13 @@ export class MessageMoveService {
|
|
|
307
308
|
const wantsPermanentDelete =
|
|
308
309
|
options.permanent === true || options.toTrash === false;
|
|
309
310
|
|
|
310
|
-
const
|
|
311
|
+
const trashResolution = wantsPermanentDelete
|
|
311
312
|
? null
|
|
312
|
-
:
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
313
|
+
: await this.mailboxSpecialUseService.resolveTrashRole(accountId);
|
|
314
|
+
|
|
315
|
+
const trashGate = trashResolution
|
|
316
|
+
? trashMailboxAt(trashResolution, "resolved")
|
|
317
|
+
: null;
|
|
316
318
|
|
|
317
319
|
if (trashGate && !trashGate.allowed) {
|
|
318
320
|
this.log.error(
|
|
@@ -324,6 +326,34 @@ export class MessageMoveService {
|
|
|
324
326
|
|
|
325
327
|
const trashMailbox = trashGate ? trashGate.mailbox : null;
|
|
326
328
|
|
|
329
|
+
// A message already sitting in the folder resolved as Trash deletes by
|
|
330
|
+
// expunging it, not by moving it — the same unrecoverable act Empty
|
|
331
|
+
// Trash performs, so it demands the same confirmed evidence. The name
|
|
332
|
+
// guess ("resolved") is enough to file mail into; it is never enough to
|
|
333
|
+
// destroy mail that is already there. Checked once, before any local
|
|
334
|
+
// write, so a batch that touches such a message is refused whole rather
|
|
335
|
+
// than partially expunged.
|
|
336
|
+
if (!wantsPermanentDelete && trashResolution && trashMailbox) {
|
|
337
|
+
const deletesInPlace = messages.some(
|
|
338
|
+
(message) => message.mailboxId === trashMailbox.mailboxId,
|
|
339
|
+
);
|
|
340
|
+
|
|
341
|
+
if (deletesInPlace) {
|
|
342
|
+
const confirmedTrashGate = trashMailboxAt(trashResolution, "confirmed");
|
|
343
|
+
if (!confirmedTrashGate.allowed) {
|
|
344
|
+
this.log.error(
|
|
345
|
+
{
|
|
346
|
+
accountId,
|
|
347
|
+
messageCount: messages.length,
|
|
348
|
+
reason: confirmedTrashGate.reason,
|
|
349
|
+
},
|
|
350
|
+
"Refused to delete: this account's Trash is not confirmed",
|
|
351
|
+
);
|
|
352
|
+
throw trashRefusal(confirmedTrashGate.reason, accountId);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
327
357
|
// Group messages by operation type
|
|
328
358
|
const moveToTrashMessages: Array<{
|
|
329
359
|
messageId: string;
|