@remit/imap-worker 0.0.72 → 0.0.74
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 +1 -1
- package/src/handlers/append-sent-message.ts +1 -1
- package/src/handlers/empty-trash.test.ts +118 -3
- package/src/handlers/empty-trash.ts +90 -31
- package/src/handlers/flag-push-placement-race.test.ts +18 -4
- package/src/handlers/flag-push.ts +1 -1
- package/src/handlers/mailbox-management.test.ts +36 -27
- package/src/handlers/mailbox-management.ts +67 -19
- package/src/handlers/message-copy.test.ts +56 -25
- package/src/handlers/message-copy.ts +43 -20
- package/src/handlers/message-delete-terminal.ts +11 -7
- package/src/handlers/message-delete.test.ts +225 -88
- package/src/handlers/message-delete.ts +92 -12
- package/src/handlers/message-move-terminal.test.ts +65 -20
- package/src/handlers/message-move-terminal.ts +10 -6
- package/src/handlers/message-move.test.ts +26 -17
- package/src/handlers/message-move.ts +27 -10
- package/src/handlers/placement-move-push.ts +2 -1
- package/src/handlers/sync-message-body.ts +4 -4
- package/src/handlers/sync-messages.test.ts +2 -4
- package/src/handlers/sync-messages.ts +20 -14
- package/src/handlers/restore-source-placement.ts +0 -135
- package/src/handlers/thread-message-rows.ts +0 -77
- package/src/is-not-found.ts +0 -17
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { getClient } from "@remit/backend/client";
|
|
2
|
+
import type { IMailboxRepository } from "@remit/data-ports";
|
|
3
|
+
import { isNotFoundError, NotFoundError } from "@remit/data-ports/errors";
|
|
2
4
|
import { MailboxSyncStatus } from "@remit/domain-enums";
|
|
3
5
|
import type { Logger } from "@remit/logger-lambda";
|
|
4
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
EVERY_MAILBOX_STATE,
|
|
8
|
+
MailboxManagementService,
|
|
9
|
+
} from "@remit/mailbox-service";
|
|
5
10
|
import { isAccountDeleted } from "../account-check.js";
|
|
6
11
|
import { createConnectionScopeWithCredentials } from "../connection-scope.js";
|
|
7
12
|
import type {
|
|
@@ -10,7 +15,6 @@ import type {
|
|
|
10
15
|
MailboxManagementEvent,
|
|
11
16
|
MailboxRenameEvent,
|
|
12
17
|
} from "../events.js";
|
|
13
|
-
import { isNotFoundError } from "../is-not-found.js";
|
|
14
18
|
import { withOAuthLifecycle } from "../with-oauth-lifecycle.js";
|
|
15
19
|
import { buildLifecycleDeps } from "../with-oauth-lifecycle-deps.js";
|
|
16
20
|
|
|
@@ -59,6 +63,36 @@ const isMailboxAbsentUpstream = (error: unknown): boolean => {
|
|
|
59
63
|
return /not found|does ?n.?t exist/i.test(saidByServer(error));
|
|
60
64
|
};
|
|
61
65
|
|
|
66
|
+
/**
|
|
67
|
+
* Write the outcome of a folder operation back, as the conditional write that
|
|
68
|
+
* is the only way to write a folder's state
|
|
69
|
+
* (docs/architecture/folder-rename-and-delete.md D3).
|
|
70
|
+
*
|
|
71
|
+
* The from-set is every state because that is what these write-backs decide
|
|
72
|
+
* against today — they are the unconditional writes they replace, moved onto
|
|
73
|
+
* the door. #363 narrows each to the state its intent recorded, and gives a
|
|
74
|
+
* lost predicate its `superseded` outcome. A null means the row is gone, which
|
|
75
|
+
* is the NotFoundError the whole-chain guards below classify as the user having
|
|
76
|
+
* deleted the folder mid-sync.
|
|
77
|
+
*/
|
|
78
|
+
const recordOutcome = async (
|
|
79
|
+
mailboxService: Pick<IMailboxRepository, "transition">,
|
|
80
|
+
accountId: string,
|
|
81
|
+
mailboxId: string,
|
|
82
|
+
to: (typeof MailboxSyncStatus)[keyof typeof MailboxSyncStatus],
|
|
83
|
+
confirmedPath?: string,
|
|
84
|
+
): Promise<void> => {
|
|
85
|
+
const written = await mailboxService.transition(accountId, mailboxId, {
|
|
86
|
+
from: EVERY_MAILBOX_STATE,
|
|
87
|
+
to,
|
|
88
|
+
// `failed` keeps whatever rename target the row carries, so the UI can
|
|
89
|
+
// name what the rename was aiming at (T6); every other outcome drops it,
|
|
90
|
+
// which the transition does on its own.
|
|
91
|
+
set: confirmedPath !== undefined ? { fullPath: confirmedPath } : {},
|
|
92
|
+
});
|
|
93
|
+
if (!written) throw new NotFoundError(`Mailbox not found: ${mailboxId}`);
|
|
94
|
+
};
|
|
95
|
+
|
|
62
96
|
/** The folder is already on the server: a create has nothing left to do. */
|
|
63
97
|
const isMailboxPresentUpstream = (error: unknown): boolean => {
|
|
64
98
|
if (!(error instanceof Error)) return false;
|
|
@@ -158,13 +192,19 @@ const handleCreate = async (
|
|
|
158
192
|
{ accountId, mailboxId, path },
|
|
159
193
|
"Mailbox already exists, marking as synced",
|
|
160
194
|
);
|
|
161
|
-
await
|
|
162
|
-
|
|
163
|
-
|
|
195
|
+
await recordOutcome(
|
|
196
|
+
mailboxService,
|
|
197
|
+
accountId,
|
|
198
|
+
mailboxId,
|
|
199
|
+
MailboxSyncStatus.synced,
|
|
200
|
+
);
|
|
164
201
|
} else {
|
|
165
|
-
await
|
|
166
|
-
|
|
167
|
-
|
|
202
|
+
await recordOutcome(
|
|
203
|
+
mailboxService,
|
|
204
|
+
accountId,
|
|
205
|
+
mailboxId,
|
|
206
|
+
MailboxSyncStatus.failed,
|
|
207
|
+
);
|
|
168
208
|
throw error;
|
|
169
209
|
}
|
|
170
210
|
})
|
|
@@ -269,11 +309,13 @@ const handleRename = async (
|
|
|
269
309
|
await mailboxService.delete(accountId, mailboxId);
|
|
270
310
|
} else {
|
|
271
311
|
// Rollback local rename by restoring old path
|
|
272
|
-
await
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
312
|
+
await recordOutcome(
|
|
313
|
+
mailboxService,
|
|
314
|
+
accountId,
|
|
315
|
+
mailboxId,
|
|
316
|
+
MailboxSyncStatus.failed,
|
|
317
|
+
oldPath,
|
|
318
|
+
);
|
|
277
319
|
throw error;
|
|
278
320
|
}
|
|
279
321
|
})
|
|
@@ -370,9 +412,12 @@ const handleDelete = async (
|
|
|
370
412
|
error.message.includes("Cannot delete INBOX")
|
|
371
413
|
) {
|
|
372
414
|
// Restore the mailbox
|
|
373
|
-
await
|
|
374
|
-
|
|
375
|
-
|
|
415
|
+
await recordOutcome(
|
|
416
|
+
mailboxService,
|
|
417
|
+
accountId,
|
|
418
|
+
mailboxId,
|
|
419
|
+
MailboxSyncStatus.synced,
|
|
420
|
+
);
|
|
376
421
|
log.error(
|
|
377
422
|
{ accountId, mailboxId, path },
|
|
378
423
|
"Cannot delete INBOX, restoring mailbox",
|
|
@@ -380,9 +425,12 @@ const handleDelete = async (
|
|
|
380
425
|
// Don't rethrow - this is an expected error
|
|
381
426
|
} else {
|
|
382
427
|
// Restore the mailbox on other errors
|
|
383
|
-
await
|
|
384
|
-
|
|
385
|
-
|
|
428
|
+
await recordOutcome(
|
|
429
|
+
mailboxService,
|
|
430
|
+
accountId,
|
|
431
|
+
mailboxId,
|
|
432
|
+
MailboxSyncStatus.failed,
|
|
433
|
+
);
|
|
386
434
|
throw error;
|
|
387
435
|
}
|
|
388
436
|
})
|
|
@@ -112,6 +112,10 @@ const deps = (): MessageCopyDeps =>
|
|
|
112
112
|
},
|
|
113
113
|
updateUid: record("message.updateUid"),
|
|
114
114
|
update: record("message.update"),
|
|
115
|
+
transitionPlacement: async (...args: unknown[]) => {
|
|
116
|
+
h.calls.push({ method: "message.transitionPlacement", args });
|
|
117
|
+
return { messageId: "new-msg" };
|
|
118
|
+
},
|
|
115
119
|
delete: record("message.delete"),
|
|
116
120
|
},
|
|
117
121
|
threadMessage: {
|
|
@@ -191,11 +195,9 @@ describe("handleMessageCopy", () => {
|
|
|
191
195
|
20,
|
|
192
196
|
"dst-mbx",
|
|
193
197
|
]);
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
"synced",
|
|
198
|
-
);
|
|
198
|
+
// `updateUid` writes the confirmed uid, `active` and `synced` in one
|
|
199
|
+
// statement, so there is no second, unpredicated write behind it.
|
|
200
|
+
assert.equal(called("message.update").length, 0);
|
|
199
201
|
assert.equal(called("threadMessage.update").length, 1);
|
|
200
202
|
assert.equal(h.disconnectCount, 1, "the scope is always disconnected");
|
|
201
203
|
});
|
|
@@ -212,11 +214,7 @@ describe("handleMessageCopy", () => {
|
|
|
212
214
|
42,
|
|
213
215
|
"dst-mbx",
|
|
214
216
|
]);
|
|
215
|
-
assert.equal(
|
|
216
|
-
(called("message.update")[0]?.args[1] as { syncStatus?: string })
|
|
217
|
-
?.syncStatus,
|
|
218
|
-
"synced",
|
|
219
|
-
);
|
|
217
|
+
assert.equal(called("message.update").length, 0);
|
|
220
218
|
assert.equal(called("threadMessage.update").length, 1);
|
|
221
219
|
});
|
|
222
220
|
|
|
@@ -263,11 +261,15 @@ describe("handleMessageCopy", () => {
|
|
|
263
261
|
|
|
264
262
|
assert.equal(copies, 1, "the COPY is issued once");
|
|
265
263
|
assert.equal(called("search").length, 0, "there is nothing to ask with");
|
|
266
|
-
const
|
|
267
|
-
assert.
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
264
|
+
const [givenUp] = called("message.transitionPlacement");
|
|
265
|
+
assert.deepEqual(
|
|
266
|
+
givenUp?.args[2],
|
|
267
|
+
{
|
|
268
|
+
status: "deleted",
|
|
269
|
+
syncStatus: "abandoned",
|
|
270
|
+
abandonedMutation: "copy",
|
|
271
|
+
},
|
|
272
|
+
"a copy that gave up carries the give-up value and names itself",
|
|
271
273
|
);
|
|
272
274
|
assert.equal(called("message.delete").length, 0, "no row is thrown away");
|
|
273
275
|
});
|
|
@@ -302,8 +304,8 @@ describe("handleMessageCopy", () => {
|
|
|
302
304
|
|
|
303
305
|
assert.equal(copies, 1, "the redelivery issues no second COPY");
|
|
304
306
|
assert.equal(called("search").length, 0);
|
|
305
|
-
const settled = called("message.
|
|
306
|
-
assert.equal((settled?.args[
|
|
307
|
+
const settled = called("message.transitionPlacement").at(-1);
|
|
308
|
+
assert.equal((settled?.args[2] as { status?: string })?.status, "deleted");
|
|
307
309
|
});
|
|
308
310
|
|
|
309
311
|
it("acks a copy that already settled without touching IMAP", async () => {
|
|
@@ -327,6 +329,7 @@ describe("handleMessageCopy", () => {
|
|
|
327
329
|
|
|
328
330
|
assert.equal(h.getConnectionCount, 0);
|
|
329
331
|
assert.equal(called("message.update").length, 0);
|
|
332
|
+
assert.equal(called("message.transitionPlacement").length, 0);
|
|
330
333
|
});
|
|
331
334
|
|
|
332
335
|
it("returns early without connecting when the account is soft-deleted", async () => {
|
|
@@ -358,6 +361,7 @@ describe("handleMessageCopy", () => {
|
|
|
358
361
|
assert.equal(h.getConnectionCount, 0);
|
|
359
362
|
assert.equal(called("message.updateUid").length, 0);
|
|
360
363
|
assert.equal(called("message.update").length, 0);
|
|
364
|
+
assert.equal(called("message.transitionPlacement").length, 0);
|
|
361
365
|
});
|
|
362
366
|
|
|
363
367
|
// Issue #1203. Both pauses are reached before `copyMessages`, so the COPY
|
|
@@ -427,7 +431,11 @@ describe("handleMessageCopy", () => {
|
|
|
427
431
|
"silence is never grounds to delete a row that may describe real mail",
|
|
428
432
|
);
|
|
429
433
|
assert.equal(
|
|
430
|
-
(
|
|
434
|
+
(
|
|
435
|
+
called("message.transitionPlacement")[0]?.args[2] as {
|
|
436
|
+
status?: string;
|
|
437
|
+
}
|
|
438
|
+
)?.status,
|
|
431
439
|
"deleted",
|
|
432
440
|
);
|
|
433
441
|
});
|
|
@@ -505,11 +513,33 @@ describe("handleMessageCopy", () => {
|
|
|
505
513
|
|
|
506
514
|
await handleMessageCopy(event, noopLogger, 1, deps());
|
|
507
515
|
|
|
508
|
-
const
|
|
509
|
-
assert.equal((
|
|
516
|
+
const [givenUp] = called("message.transitionPlacement");
|
|
517
|
+
assert.equal((givenUp?.args[2] as { status?: string })?.status, "deleted");
|
|
518
|
+
assert.equal(
|
|
519
|
+
(givenUp?.args[2] as { abandonedMutation?: string })?.abandonedMutation,
|
|
520
|
+
"copy",
|
|
521
|
+
);
|
|
510
522
|
assert.equal(called("createMailbox").length, 0);
|
|
511
523
|
});
|
|
512
524
|
|
|
525
|
+
// The guard asks whether THIS copy is still outstanding, not whether anything
|
|
526
|
+
// is. A row a delete has claimed is `deleting`, and reading that as unsettled
|
|
527
|
+
// let a redelivery COPY the message a second time — COPY has no source-side
|
|
528
|
+
// effect to make that a no-op, so the duplicate is permanent.
|
|
529
|
+
it("skips a redelivered copy whose row a delete has claimed", async () => {
|
|
530
|
+
let copies = 0;
|
|
531
|
+
h.connection.copyMessages = async () => {
|
|
532
|
+
copies += 1;
|
|
533
|
+
return { uidMap: new Map([[10, 20]]) };
|
|
534
|
+
};
|
|
535
|
+
h.copyRow = { ...unsettledCopyRow(), status: "deleting" };
|
|
536
|
+
|
|
537
|
+
await handleMessageCopy(event, noopLogger, 2, deps());
|
|
538
|
+
|
|
539
|
+
assert.equal(copies, 0, "no second copy is issued");
|
|
540
|
+
assert.equal(called("message.updateUid").length, 0);
|
|
541
|
+
});
|
|
542
|
+
|
|
513
543
|
it("marks failed and rethrows on an unclassified IMAP error within the budget", async () => {
|
|
514
544
|
h.connection.copyMessages = async () => {
|
|
515
545
|
throw new Error("server exploded");
|
|
@@ -520,11 +550,12 @@ describe("handleMessageCopy", () => {
|
|
|
520
550
|
/server exploded/,
|
|
521
551
|
);
|
|
522
552
|
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
);
|
|
553
|
+
// The attempt marker is a transition too, predicated on this copy still
|
|
554
|
+
// being outstanding: a row another mutation has claimed keeps its own
|
|
555
|
+
// placement rather than picking up this attempt's failure.
|
|
556
|
+
const [marker] = called("message.transitionPlacement");
|
|
557
|
+
assert.deepEqual(marker?.args[1], { status: "moving" });
|
|
558
|
+
assert.deepEqual(marker?.args[2], { syncStatus: "failed" });
|
|
528
559
|
});
|
|
529
560
|
|
|
530
561
|
it("settles on the destination UID instead of dead-lettering the last attempt", async () => {
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import { getClient } from "@remit/backend/client";
|
|
2
|
-
import {
|
|
2
|
+
import { isNotFoundError } from "@remit/data-ports/errors";
|
|
3
|
+
import {
|
|
4
|
+
MessageMutation,
|
|
5
|
+
MessageStatus,
|
|
6
|
+
MessageSyncStatus,
|
|
7
|
+
} from "@remit/domain-enums";
|
|
3
8
|
import type { Logger } from "@remit/logger-lambda";
|
|
4
9
|
import {
|
|
5
10
|
guardConnectionCursor,
|
|
6
11
|
type IImapConnection,
|
|
7
12
|
isCursorRebuildNeeded,
|
|
8
|
-
isPlacementUnsettled,
|
|
9
13
|
MailboxCursorPausedError,
|
|
10
14
|
reconcileStaleMessage,
|
|
11
15
|
} from "@remit/mailbox-service";
|
|
@@ -14,7 +18,6 @@ import { isAccountDeleted } from "../account-check.js";
|
|
|
14
18
|
import { createConnectionScopeWithCredentials } from "../connection-scope.js";
|
|
15
19
|
import { emitEvent } from "../emit.js";
|
|
16
20
|
import type { MessageCopyEvent } from "../events.js";
|
|
17
|
-
import { isNotFoundError } from "../is-not-found.js";
|
|
18
21
|
import { withOAuthLifecycle } from "../with-oauth-lifecycle.js";
|
|
19
22
|
import { buildLifecycleDeps } from "../with-oauth-lifecycle-deps.js";
|
|
20
23
|
import {
|
|
@@ -147,7 +150,12 @@ export const handleMessageCopy = async (
|
|
|
147
150
|
// This copy already settled — `updateUid` cleared `status: moving` when the
|
|
148
151
|
// server confirmed it. A redelivery reaching here would COPY the message a
|
|
149
152
|
// second time, and COPY has no source-side effect to make that a no-op.
|
|
150
|
-
|
|
153
|
+
//
|
|
154
|
+
// `moving` specifically, not the shared unsettled predicate: the question is
|
|
155
|
+
// whether THIS copy is still outstanding, and a `deleting` row is somebody
|
|
156
|
+
// else's work — reading it as unsettled is what would let the second COPY
|
|
157
|
+
// through.
|
|
158
|
+
if (copyRow.status !== MessageStatus.moving) {
|
|
151
159
|
log.info(
|
|
152
160
|
{ accountId, newMessageId, uid: copyRow.uid, status: copyRow.status },
|
|
153
161
|
"Skipping MESSAGE_COPY: the copy already settled against confirmed IMAP state",
|
|
@@ -206,17 +214,15 @@ export const handleMessageCopy = async (
|
|
|
206
214
|
};
|
|
207
215
|
|
|
208
216
|
const settleCopied = async (newUid: number): Promise<void> => {
|
|
217
|
+
// `updateUid` writes the confirmed uid, `active` and `synced` in one
|
|
218
|
+
// statement, so the second write this used to make said nothing the
|
|
219
|
+
// first had not — and said it without a predicate.
|
|
209
220
|
await messageService.updateUid(
|
|
210
221
|
newMessageId,
|
|
211
222
|
newUid,
|
|
212
223
|
destinationMailboxId,
|
|
213
224
|
);
|
|
214
225
|
|
|
215
|
-
await messageService.update(newMessageId, {
|
|
216
|
-
status: MessageStatus.active,
|
|
217
|
-
syncStatus: MessageSyncStatus.synced,
|
|
218
|
-
});
|
|
219
|
-
|
|
220
226
|
const threadMessage = await threadMessageService.findByMessageId(
|
|
221
227
|
account.accountConfigId,
|
|
222
228
|
newMessageId,
|
|
@@ -293,10 +299,20 @@ export const handleMessageCopy = async (
|
|
|
293
299
|
// that turns out to be there. Silence buys the safer half of one pair
|
|
294
300
|
// and the worse half of the other.
|
|
295
301
|
const settleBroken = async (reason: string): Promise<void> => {
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
302
|
+
// A transition, not a plain update (imap-mutations R3): the copy row
|
|
303
|
+
// is only this handler's to settle while its own placement is still
|
|
304
|
+
// unsettled. The row it leaves is `deleted`, so no listing carries it
|
|
305
|
+
// and the marker is read by nothing — it is written anyway, because a
|
|
306
|
+
// give-up that does not name itself is the gap #1229 came from.
|
|
307
|
+
await messageService.transitionPlacement(
|
|
308
|
+
newMessageId,
|
|
309
|
+
{ status: MessageStatus.moving },
|
|
310
|
+
{
|
|
311
|
+
status: MessageStatus.deleted,
|
|
312
|
+
syncStatus: MessageSyncStatus.abandoned,
|
|
313
|
+
abandonedMutation: MessageMutation.copy,
|
|
314
|
+
},
|
|
315
|
+
);
|
|
300
316
|
log.error(
|
|
301
317
|
{
|
|
302
318
|
alert: "message_copy_unconfirmed",
|
|
@@ -495,10 +511,15 @@ export const handleMessageCopy = async (
|
|
|
495
511
|
{ sourceMessageId, uid },
|
|
496
512
|
"Source message not found on IMAP, marking copy as failed",
|
|
497
513
|
);
|
|
498
|
-
await messageService.
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
514
|
+
await messageService.transitionPlacement(
|
|
515
|
+
newMessageId,
|
|
516
|
+
{ status: MessageStatus.moving },
|
|
517
|
+
{
|
|
518
|
+
status: MessageStatus.deleted,
|
|
519
|
+
syncStatus: MessageSyncStatus.abandoned,
|
|
520
|
+
abandonedMutation: MessageMutation.copy,
|
|
521
|
+
},
|
|
522
|
+
);
|
|
502
523
|
return;
|
|
503
524
|
}
|
|
504
525
|
|
|
@@ -506,9 +527,11 @@ export const handleMessageCopy = async (
|
|
|
506
527
|
// Transient copy failure — expected (connections drop). Queue
|
|
507
528
|
// redelivery retries, and the probe above keeps the retry from
|
|
508
529
|
// copying the message twice.
|
|
509
|
-
await messageService.
|
|
510
|
-
|
|
511
|
-
|
|
530
|
+
await messageService.transitionPlacement(
|
|
531
|
+
newMessageId,
|
|
532
|
+
{ status: MessageStatus.moving },
|
|
533
|
+
{ syncStatus: MessageSyncStatus.failed },
|
|
534
|
+
);
|
|
512
535
|
throw error;
|
|
513
536
|
}
|
|
514
537
|
|
|
@@ -2,14 +2,14 @@ import type {
|
|
|
2
2
|
IMessageRepository,
|
|
3
3
|
IThreadMessageRepository,
|
|
4
4
|
} from "@remit/data-ports";
|
|
5
|
-
import { MessageSyncStatus } from "@remit/domain-enums";
|
|
5
|
+
import { MessageMutation, MessageSyncStatus } from "@remit/domain-enums";
|
|
6
6
|
import {
|
|
7
7
|
type IImapConnection,
|
|
8
8
|
isMessageGoneFromOpenMailbox,
|
|
9
9
|
reconcileStaleMessage,
|
|
10
|
+
restoreSourcePlacement,
|
|
10
11
|
type StaleMessageReconcileDeps,
|
|
11
12
|
} from "@remit/mailbox-service";
|
|
12
|
-
import { restoreSourcePlacement } from "./restore-source-placement.js";
|
|
13
13
|
|
|
14
14
|
export interface MessageDeleteTerminalLogger {
|
|
15
15
|
info(obj: Record<string, unknown>, msg: string): void;
|
|
@@ -18,7 +18,7 @@ export interface MessageDeleteTerminalLogger {
|
|
|
18
18
|
|
|
19
19
|
export interface ResolveExhaustedMessageDeleteDeps
|
|
20
20
|
extends StaleMessageReconcileDeps {
|
|
21
|
-
messageService: Pick<IMessageRepository, "delete" | "
|
|
21
|
+
messageService: Pick<IMessageRepository, "delete" | "transitionPlacement">;
|
|
22
22
|
threadMessageService: Pick<
|
|
23
23
|
IThreadMessageRepository,
|
|
24
24
|
"findAllByMessageId" | "deleteMany" | "update"
|
|
@@ -60,7 +60,7 @@ export interface ResolveExhaustedMessageDeleteResult {
|
|
|
60
60
|
* effect, but it keeps failing: broken code or a broken account, not a
|
|
61
61
|
* transient blip. The row is put back where the server has just said the
|
|
62
62
|
* message is: `mailboxId` and `uid` return to the delete's source pair, and
|
|
63
|
-
* the thread rows lose the deletion mark the optimistic
|
|
63
|
+
* the thread rows lose the deletion mark the optimistic transition
|
|
64
64
|
* wrote (issue #1098).
|
|
65
65
|
*
|
|
66
66
|
* Settling `status` to `active` while leaving `mailboxId` on Trash is what
|
|
@@ -132,9 +132,13 @@ export const resolveExhaustedMessageDeleteFailure = async (
|
|
|
132
132
|
sourceMailboxId,
|
|
133
133
|
uid,
|
|
134
134
|
// The probe above just confirmed the pair, so the row is a faithful
|
|
135
|
-
// projection of the source again
|
|
136
|
-
// not
|
|
137
|
-
|
|
135
|
+
// projection of the source again — and the delete the user asked for did
|
|
136
|
+
// not happen, with nothing left to try it. `abandoned` is the value that
|
|
137
|
+
// says so without claiming a retry is coming (imap-mutations R3); the
|
|
138
|
+
// alert stays for the operator, and the mutation names itself so the
|
|
139
|
+
// reading pane offers the right way out (issue #1229).
|
|
140
|
+
syncStatus: MessageSyncStatus.abandoned,
|
|
141
|
+
abandonedMutation: MessageMutation.delete,
|
|
138
142
|
});
|
|
139
143
|
|
|
140
144
|
return { outcome: "broken" };
|