@remit/imap-worker 0.0.86 → 0.0.87
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/README.md +3 -1
- package/package.json +10 -2
- package/src/account-check.ts +1 -1
- package/src/body-sync-gate.test.ts +1 -1
- package/src/body-sync-gate.ts +1 -1
- package/src/e2e-processor-shim.ts +6 -21
- package/src/emit.ts +1 -1
- package/src/failure-kind.ts +1 -1
- package/src/handlers/append-sent-message.sqlite.test.ts +176 -0
- package/src/handlers/append-sent-message.ts +3 -24
- package/src/handlers/delete-account-objects.test.ts +1 -1
- package/src/handlers/delete-account-objects.ts +1 -1
- package/src/handlers/empty-trash.sqlite.test.ts +209 -0
- package/src/handlers/empty-trash.ts +3 -24
- package/src/handlers/flag-push.ts +11 -18
- package/src/handlers/mailbox-management.sqlite.test.ts +265 -0
- package/src/handlers/mailbox-management.ts +12 -48
- package/src/handlers/message-copy.sqlite.test.ts +298 -0
- package/src/handlers/message-copy.ts +31 -49
- package/src/handlers/message-delete-terminal.ts +12 -10
- package/src/handlers/message-delete.sqlite.test.ts +291 -0
- package/src/handlers/message-delete.ts +26 -32
- package/src/handlers/message-move-terminal.ts +12 -10
- package/src/handlers/message-move.sqlite.test.ts +271 -0
- package/src/handlers/message-move.test.ts +76 -863
- package/src/handlers/message-move.ts +49 -39
- package/src/handlers/placement-move-push.test.ts +96 -329
- package/src/handlers/placement-move-push.ts +75 -29
- package/src/handlers/placement-push.sqlite.test.ts +298 -0
- package/src/handlers/probe-message-id.sqlite.test.ts +197 -0
- package/src/handlers/probe-message-id.ts +15 -0
- package/src/handlers/sync-mailboxes.test.ts +0 -1
- package/src/handlers/sync-mailboxes.ts +1 -1
- package/src/handlers/sync-message-body.sqlite.test.ts +314 -0
- package/src/handlers/sync-message-body.test.ts +3 -299
- package/src/handlers/sync-message-body.ts +21 -11
- package/src/handlers/sync-messages.sqlite.test.ts +183 -0
- package/src/handlers/sync-messages.ts +27 -33
- package/src/index.ts +5 -3
- package/src/mail-sync-selection.sqlite.test.ts +16 -18
- package/src/poller.ts +5 -4
- package/src/processor.test.ts +1 -1
- package/src/processor.ts +1 -1
- package/src/scheduler/attachment-sweep.sqlite.test.ts +74 -0
- package/src/scheduler/attachment-sweep.ts +1 -1
- package/src/scheduler/{calendar-subscriptions.test.ts → calendar-subscriptions.sqlite.test.ts} +43 -55
- package/src/scheduler/calendar-subscriptions.ts +6 -6
- package/src/scheduler/half-deleted-accounts.sqlite.test.ts +203 -0
- package/src/scheduler/handler.ts +12 -1
- package/src/scheduler/loop.test.ts +2 -0
- package/src/scheduler/run-tick.sqlite.test.ts +192 -0
- package/src/scheduler/run-tick.ts +39 -1
- package/src/scheduler/runner.ts +21 -1
- package/src/scheduler/stranded-placement-redrive.ts +265 -0
- package/src/stranded-placement.sqlite.test.ts +795 -0
- package/src/test-helpers/test-world.ts +398 -0
- package/src/thread-join.sqlite.test.ts +115 -0
- package/src/with-oauth-lifecycle-deps.sqlite.test.ts +138 -14
- package/src/with-oauth-lifecycle.ts +1 -1
- package/src/handlers/append-sent-message.test.ts +0 -529
- package/src/handlers/empty-trash.test.ts +0 -728
- package/src/handlers/flag-push-placement-race.test.ts +0 -385
- package/src/handlers/flag-push.test.ts +0 -325
- package/src/handlers/mailbox-management.test.ts +0 -850
- package/src/handlers/message-copy.test.ts +0 -654
- package/src/handlers/message-delete.test.ts +0 -1869
- package/src/handlers/message-move-terminal.test.ts +0 -412
- package/src/handlers/sync-messages.test.ts +0 -569
- package/src/scheduler/attachment-sweep.test.ts +0 -169
- package/src/scheduler/run-tick.test.ts +0 -478
- package/src/with-oauth-lifecycle.test.ts +0 -318
|
@@ -5,8 +5,8 @@ import {
|
|
|
5
5
|
MessageStatus,
|
|
6
6
|
MessageSyncStatus,
|
|
7
7
|
} from "@remit/domain-enums";
|
|
8
|
-
import type { Logger } from "@remit/logger
|
|
9
|
-
import { recordImapFailure } from "@remit/logger-lambda";
|
|
8
|
+
import type { Logger } from "@remit/logger";
|
|
9
|
+
import { recordImapFailure } from "@remit/logger-lambda/metrics";
|
|
10
10
|
import {
|
|
11
11
|
guardConnectionCursor,
|
|
12
12
|
type IImapConnection,
|
|
@@ -23,6 +23,7 @@ import { findMailboxRow } from "../mailbox-row.js";
|
|
|
23
23
|
import { withOAuthLifecycle } from "../with-oauth-lifecycle.js";
|
|
24
24
|
import { buildLifecycleDeps } from "../with-oauth-lifecycle-deps.js";
|
|
25
25
|
import { resolveExhaustedMessageMoveFailure } from "./message-move-terminal.js";
|
|
26
|
+
import { probeableMessageIdHeader } from "./probe-message-id.js";
|
|
26
27
|
|
|
27
28
|
export const getMessageMoveMaxAttempts = (
|
|
28
29
|
processEnv: NodeJS.ProcessEnv = process.env,
|
|
@@ -229,22 +230,6 @@ export const buildThreadMessageMoveUpdate = (
|
|
|
229
230
|
},
|
|
230
231
|
});
|
|
231
232
|
|
|
232
|
-
export interface MessageMoveDeps {
|
|
233
|
-
getClient: typeof getClient;
|
|
234
|
-
buildLifecycleDeps: typeof buildLifecycleDeps;
|
|
235
|
-
withOAuthLifecycle: typeof withOAuthLifecycle;
|
|
236
|
-
createConnectionScope: typeof createConnectionScopeWithCredentials;
|
|
237
|
-
emitEvent: typeof emitEvent;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
const defaultDeps: MessageMoveDeps = {
|
|
241
|
-
getClient,
|
|
242
|
-
buildLifecycleDeps,
|
|
243
|
-
withOAuthLifecycle,
|
|
244
|
-
createConnectionScope: createConnectionScopeWithCredentials,
|
|
245
|
-
emitEvent,
|
|
246
|
-
};
|
|
247
|
-
|
|
248
233
|
/**
|
|
249
234
|
* Handle MESSAGE_MOVE events.
|
|
250
235
|
* Executes IMAP MOVE command and updates local state with new UID.
|
|
@@ -260,16 +245,8 @@ export const handleMessageMove = async (
|
|
|
260
245
|
event: MessageMoveEvent,
|
|
261
246
|
log: Logger,
|
|
262
247
|
receiveCount = 1,
|
|
263
|
-
|
|
248
|
+
createConnectionScope = createConnectionScopeWithCredentials,
|
|
264
249
|
): Promise<void> => {
|
|
265
|
-
const {
|
|
266
|
-
getClient,
|
|
267
|
-
buildLifecycleDeps,
|
|
268
|
-
withOAuthLifecycle,
|
|
269
|
-
createConnectionScope: createConnectionScopeWithCredentials,
|
|
270
|
-
emitEvent,
|
|
271
|
-
} = deps;
|
|
272
|
-
|
|
273
250
|
const {
|
|
274
251
|
account: accountService,
|
|
275
252
|
message: messageService,
|
|
@@ -341,17 +318,35 @@ export const handleMessageMove = async (
|
|
|
341
318
|
// a queued event pointing at a gone row. The lookup then throws
|
|
342
319
|
// NotFoundError forever, and on the account's per-group FIFO that head
|
|
343
320
|
// message stalls the whole pipeline (issues #287, #289, #290). A deleted
|
|
344
|
-
// source mailbox makes the move moot:
|
|
321
|
+
// source mailbox makes the move moot, but not the row: left `moving`, it
|
|
322
|
+
// names the destination with a uid from a folder that no longer exists,
|
|
323
|
+
// and nothing else settles it. It is reconciled away, and the destination
|
|
324
|
+
// resync re-projects the message if the MOVE did land there.
|
|
345
325
|
const source = await findMailboxRow(
|
|
346
326
|
mailboxService,
|
|
347
327
|
accountId,
|
|
348
328
|
sourceMailboxId,
|
|
349
329
|
);
|
|
350
330
|
if (source.kind === "gone") {
|
|
331
|
+
const reconciled = await messageService.deleteIfPlacement(messageId, {
|
|
332
|
+
status: message.status,
|
|
333
|
+
mailboxId: message.mailboxId,
|
|
334
|
+
uid: message.uid,
|
|
335
|
+
});
|
|
351
336
|
log.warn(
|
|
352
|
-
{
|
|
353
|
-
|
|
337
|
+
{
|
|
338
|
+
metric: "message_move_source_gone",
|
|
339
|
+
accountId,
|
|
340
|
+
messageId,
|
|
341
|
+
mailboxId: sourceMailboxId,
|
|
342
|
+
reconciled,
|
|
343
|
+
},
|
|
344
|
+
"MESSAGE_MOVE not pushed: source mailbox no longer exists (deleted); stale row reconciled",
|
|
354
345
|
);
|
|
346
|
+
await emitMailboxResync(emitEvent, {
|
|
347
|
+
accountId,
|
|
348
|
+
mailboxId: destinationMailboxId,
|
|
349
|
+
});
|
|
355
350
|
return;
|
|
356
351
|
}
|
|
357
352
|
const mailbox = source.mailbox;
|
|
@@ -462,7 +457,7 @@ export const handleMessageMove = async (
|
|
|
462
457
|
}
|
|
463
458
|
const destinationMailboxPath = destination.mailbox.fullPath;
|
|
464
459
|
|
|
465
|
-
const scope =
|
|
460
|
+
const scope = createConnectionScope(account, credentials);
|
|
466
461
|
|
|
467
462
|
// The terminal resolver opens the source itself, and its answer now
|
|
468
463
|
// WRITES a placement, so it has to reach IMAP through the same
|
|
@@ -504,7 +499,11 @@ export const handleMessageMove = async (
|
|
|
504
499
|
|
|
505
500
|
const placement: PausedPlacement = mayHaveIssued
|
|
506
501
|
? await probePausedPlacement(await scope.getConnection(), {
|
|
507
|
-
messageIdHeader:
|
|
502
|
+
messageIdHeader: await probeableMessageIdHeader(
|
|
503
|
+
threadMessageService,
|
|
504
|
+
account.accountConfigId,
|
|
505
|
+
messageId,
|
|
506
|
+
),
|
|
508
507
|
sourceMailboxPath,
|
|
509
508
|
destinationMailboxPath,
|
|
510
509
|
})
|
|
@@ -602,13 +601,19 @@ export const handleMessageMove = async (
|
|
|
602
601
|
// because a handler that returns never redelivers.
|
|
603
602
|
const newUid =
|
|
604
603
|
result.uidMap.get(uid) ??
|
|
605
|
-
(
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
604
|
+
(await probeableMessageIdHeader(
|
|
605
|
+
threadMessageService,
|
|
606
|
+
account.accountConfigId,
|
|
607
|
+
messageId,
|
|
608
|
+
).then((messageIdHeader) =>
|
|
609
|
+
messageIdHeader
|
|
610
|
+
? searchMailboxForHighestMessageIdUid(
|
|
611
|
+
rawConnection,
|
|
612
|
+
destinationMailboxPath,
|
|
613
|
+
messageIdHeader,
|
|
614
|
+
)
|
|
615
|
+
: null,
|
|
616
|
+
));
|
|
612
617
|
|
|
613
618
|
if (!newUid) {
|
|
614
619
|
throw new Error(
|
|
@@ -679,6 +684,11 @@ export const handleMessageMove = async (
|
|
|
679
684
|
const settled = await resolveExhaustedMessageMoveFailure(
|
|
680
685
|
{ messageService, threadMessageService, log },
|
|
681
686
|
{
|
|
687
|
+
placement: {
|
|
688
|
+
status: message.status,
|
|
689
|
+
mailboxId: message.mailboxId,
|
|
690
|
+
uid: message.uid,
|
|
691
|
+
},
|
|
682
692
|
accountId,
|
|
683
693
|
accountConfigId: account.accountConfigId,
|
|
684
694
|
messageId,
|
|
@@ -1,370 +1,137 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
|
-
import {
|
|
3
|
-
import { getClient, type RemitClient, setClient } from "@remit/backend/client";
|
|
4
|
-
import type { Logger } from "@remit/logger-lambda";
|
|
5
|
-
import type { IImapConnection } from "@remit/mailbox-service";
|
|
6
|
-
import type { PlacementMovePushEvent } from "../events.js";
|
|
2
|
+
import { describe, it } from "node:test";
|
|
7
3
|
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
return
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
/** Whether the message is still in the source mailbox. */
|
|
34
|
-
stillAtSource?: boolean;
|
|
35
|
-
/** The source FETCH drops the row for a message that is still there. */
|
|
36
|
-
sourceFetchDrops?: boolean;
|
|
37
|
-
}): IImapConnection =>
|
|
38
|
-
({
|
|
39
|
-
openBox: async () => ({}) as never,
|
|
40
|
-
moveMessages: async (_uids: number[], destination: string) => {
|
|
41
|
-
if (opts.moveError) throw opts.moveError;
|
|
42
|
-
return {
|
|
43
|
-
destination,
|
|
44
|
-
uidValidity: 1,
|
|
45
|
-
uidMap: opts.uidMap ?? new Map(),
|
|
46
|
-
};
|
|
47
|
-
},
|
|
48
|
-
search: async (criteria: unknown[]) => {
|
|
49
|
-
const [first] = criteria;
|
|
50
|
-
if (Array.isArray(first) && first[0] === "UID") {
|
|
51
|
-
return opts.stillAtSource ? [Number(first[1])] : [];
|
|
52
|
-
}
|
|
53
|
-
return opts.destinationSearchUids ?? [];
|
|
54
|
-
},
|
|
55
|
-
fetchMessages: async (uids: number[]) =>
|
|
56
|
-
opts.stillAtSource && !opts.sourceFetchDrops
|
|
57
|
-
? uids.map((uid) => ({ uid }) as unknown as never)
|
|
58
|
-
: [],
|
|
59
|
-
}) as unknown as IImapConnection;
|
|
60
|
-
|
|
61
|
-
const MESSAGE_ID_HEADER = "<abc@example.com>";
|
|
4
|
+
eml,
|
|
5
|
+
FakeImapServer,
|
|
6
|
+
} from "@remit/mailbox-service/test-helpers/fake-imap";
|
|
7
|
+
import { attemptMove } from "./placement-move-push.js";
|
|
8
|
+
|
|
9
|
+
const HEADER = "<abc@example.com>";
|
|
10
|
+
|
|
11
|
+
/** Junk holds the message at uid 42; INBOX is where the placement sends it. */
|
|
12
|
+
const arrange = (
|
|
13
|
+
options: { inJunk?: boolean; inInbox?: boolean; inboxExists?: boolean } = {},
|
|
14
|
+
) => {
|
|
15
|
+
const server = new FakeImapServer();
|
|
16
|
+
server.addFolder("Junk");
|
|
17
|
+
if (options.inboxExists !== false) server.addFolder("INBOX");
|
|
18
|
+
if (options.inJunk !== false)
|
|
19
|
+
server.deliver("Junk", eml({ "Message-ID": HEADER }), { uid: 42 });
|
|
20
|
+
if (options.inInbox) server.deliver("INBOX", eml({ "Message-ID": HEADER }));
|
|
21
|
+
return server;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const move = (
|
|
25
|
+
server: FakeImapServer,
|
|
26
|
+
header: string | undefined = HEADER,
|
|
27
|
+
source = server.connection(),
|
|
28
|
+
) => attemptMove(source, server.connection(), "Junk", "INBOX", 42, header);
|
|
62
29
|
|
|
63
30
|
describe("attemptMove — the IMAP push (issue #1271)", () => {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
// guarding per mailbox is #1272's `guardConnectionCursor`, tested in
|
|
67
|
-
// remit-mailbox-service; not re-tested here).
|
|
68
|
-
it("moved: returns the new uid from the COPYUID map", async () => {
|
|
69
|
-
const connection = buildConnection({ uidMap: new Map([[42, 99]]) });
|
|
31
|
+
it("moves the message and returns the uid from the COPYUID map", async () => {
|
|
32
|
+
const server = arrange();
|
|
70
33
|
|
|
71
|
-
const outcome = await
|
|
72
|
-
connection,
|
|
73
|
-
connection,
|
|
74
|
-
"Junk",
|
|
75
|
-
"INBOX",
|
|
76
|
-
42,
|
|
77
|
-
MESSAGE_ID_HEADER,
|
|
78
|
-
);
|
|
79
|
-
|
|
80
|
-
assert.equal(outcome.kind, "moved");
|
|
81
|
-
assert.equal(outcome.newUid, 99);
|
|
82
|
-
});
|
|
34
|
+
const outcome = await move(server);
|
|
83
35
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
36
|
+
assert.deepEqual(outcome, {
|
|
37
|
+
kind: "moved",
|
|
38
|
+
newUid: server.uids("INBOX", HEADER)[0],
|
|
87
39
|
});
|
|
88
|
-
|
|
89
|
-
const outcome = await attemptMove(
|
|
90
|
-
connection,
|
|
91
|
-
connection,
|
|
92
|
-
"Junk",
|
|
93
|
-
"NewLabel",
|
|
94
|
-
42,
|
|
95
|
-
MESSAGE_ID_HEADER,
|
|
96
|
-
);
|
|
97
|
-
|
|
98
|
-
assert.equal(outcome.kind, "trycreate");
|
|
40
|
+
assert.deepEqual(server.uids("Junk"), []);
|
|
99
41
|
});
|
|
100
42
|
|
|
101
|
-
it("
|
|
102
|
-
const
|
|
103
|
-
moveError: new Error("ECONNRESET"),
|
|
104
|
-
});
|
|
43
|
+
it("answers trycreate when the destination does not exist yet", async () => {
|
|
44
|
+
const server = arrange({ inboxExists: false });
|
|
105
45
|
|
|
106
|
-
await
|
|
107
|
-
() =>
|
|
108
|
-
attemptMove(
|
|
109
|
-
connection,
|
|
110
|
-
connection,
|
|
111
|
-
"Junk",
|
|
112
|
-
"INBOX",
|
|
113
|
-
42,
|
|
114
|
-
MESSAGE_ID_HEADER,
|
|
115
|
-
),
|
|
116
|
-
/ECONNRESET/,
|
|
117
|
-
);
|
|
46
|
+
assert.equal((await move(server)).kind, "trycreate");
|
|
118
47
|
});
|
|
119
48
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
uidMap: new Map(),
|
|
124
|
-
destinationSearchUids: [77],
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
const outcome = await attemptMove(
|
|
128
|
-
connection,
|
|
129
|
-
connection,
|
|
130
|
-
"Junk",
|
|
131
|
-
"INBOX",
|
|
132
|
-
42,
|
|
133
|
-
MESSAGE_ID_HEADER,
|
|
134
|
-
);
|
|
135
|
-
|
|
136
|
-
assert.equal(outcome.kind, "moved");
|
|
137
|
-
assert.equal(outcome.newUid, 77);
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
it("moved: explicit NONEXISTENT error, but a Message-ID search finds it at the destination", async () => {
|
|
141
|
-
const connection = buildConnection({
|
|
142
|
-
moveError: new Error("Command failed: NONEXISTENT no such message"),
|
|
143
|
-
destinationSearchUids: [77],
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
const outcome = await attemptMove(
|
|
147
|
-
connection,
|
|
148
|
-
connection,
|
|
149
|
-
"Junk",
|
|
150
|
-
"INBOX",
|
|
151
|
-
42,
|
|
152
|
-
MESSAGE_ID_HEADER,
|
|
153
|
-
);
|
|
154
|
-
|
|
155
|
-
assert.equal(outcome.kind, "moved");
|
|
156
|
-
assert.equal(outcome.newUid, 77);
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
// Issue #1122. The destination already held an older copy of this
|
|
160
|
-
// Message-ID; the marker must bind to the copy this MOVE delivered, or
|
|
161
|
-
// the placement row names mail this push never touched.
|
|
162
|
-
it("moved: binds to the newest copy when the destination already held an older one", async () => {
|
|
163
|
-
const connection = buildConnection({
|
|
164
|
-
uidMap: new Map(),
|
|
165
|
-
destinationSearchUids: [15, 77],
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
const outcome = await attemptMove(
|
|
169
|
-
connection,
|
|
170
|
-
connection,
|
|
171
|
-
"Junk",
|
|
172
|
-
"INBOX",
|
|
173
|
-
42,
|
|
174
|
-
MESSAGE_ID_HEADER,
|
|
175
|
-
);
|
|
176
|
-
|
|
177
|
-
assert.equal(outcome.kind, "moved");
|
|
178
|
-
assert.equal(outcome.newUid, 77);
|
|
179
|
-
});
|
|
49
|
+
it("propagates any other failure untouched for the caller to retry", async () => {
|
|
50
|
+
const server = arrange();
|
|
51
|
+
server.failNext.MOVE = new Error("ECONNRESET");
|
|
180
52
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
uidMap: new Map(),
|
|
184
|
-
destinationSearchUids: [],
|
|
185
|
-
stillAtSource: true,
|
|
186
|
-
});
|
|
53
|
+
await assert.rejects(move(server), /ECONNRESET/);
|
|
54
|
+
});
|
|
187
55
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
"Junk",
|
|
194
|
-
"INBOX",
|
|
195
|
-
42,
|
|
196
|
-
MESSAGE_ID_HEADER,
|
|
197
|
-
),
|
|
198
|
-
/unresolved/,
|
|
199
|
-
);
|
|
200
|
-
});
|
|
56
|
+
// PR #1289 review finding 2: neither a missing COPYUID entry nor an explicit
|
|
57
|
+
// not-found is trusted without asking the server.
|
|
58
|
+
it("confirms a move by Message-ID when the server sends no COPYUID", async () => {
|
|
59
|
+
const server = arrange();
|
|
60
|
+
server.uidplus = false;
|
|
201
61
|
|
|
202
|
-
|
|
203
|
-
const connection = buildConnection({
|
|
204
|
-
uidMap: new Map(),
|
|
205
|
-
destinationSearchUids: [],
|
|
206
|
-
stillAtSource: true,
|
|
207
|
-
sourceFetchDrops: true,
|
|
208
|
-
});
|
|
62
|
+
const outcome = await move(server);
|
|
209
63
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
connection,
|
|
214
|
-
connection,
|
|
215
|
-
"Junk",
|
|
216
|
-
"INBOX",
|
|
217
|
-
42,
|
|
218
|
-
MESSAGE_ID_HEADER,
|
|
219
|
-
),
|
|
220
|
-
/unresolved/,
|
|
221
|
-
);
|
|
64
|
+
assert.deepEqual(outcome, {
|
|
65
|
+
kind: "moved",
|
|
66
|
+
newUid: server.uids("INBOX", HEADER)[0],
|
|
222
67
|
});
|
|
68
|
+
});
|
|
223
69
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
const outcome = await attemptMove(
|
|
232
|
-
connection,
|
|
233
|
-
connection,
|
|
234
|
-
"Junk",
|
|
235
|
-
"INBOX",
|
|
236
|
-
42,
|
|
237
|
-
MESSAGE_ID_HEADER,
|
|
238
|
-
);
|
|
239
|
-
|
|
240
|
-
assert.equal(outcome.kind, "not-found");
|
|
241
|
-
});
|
|
70
|
+
it("confirms a move by Message-ID when the server answers NONEXISTENT for one that landed", async () => {
|
|
71
|
+
const server = arrange({ inJunk: false, inInbox: true });
|
|
72
|
+
server.failNext.MOVE = new Error(
|
|
73
|
+
"Command failed: NONEXISTENT no such message",
|
|
74
|
+
);
|
|
242
75
|
|
|
243
|
-
|
|
244
|
-
const connection = buildConnection({
|
|
245
|
-
uidMap: new Map(),
|
|
246
|
-
stillAtSource: true,
|
|
247
|
-
});
|
|
76
|
+
const outcome = await move(server);
|
|
248
77
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
)
|
|
78
|
+
assert.deepEqual(outcome, {
|
|
79
|
+
kind: "moved",
|
|
80
|
+
newUid: server.uids("INBOX", HEADER)[0],
|
|
252
81
|
});
|
|
82
|
+
});
|
|
253
83
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
84
|
+
// Issue #1122: the destination already held an older copy, and the row must
|
|
85
|
+
// bind to the one this MOVE delivered.
|
|
86
|
+
it("binds to the newest copy when the destination already held an older one", async () => {
|
|
87
|
+
const server = arrange({ inInbox: true });
|
|
88
|
+
server.uidplus = false;
|
|
259
89
|
|
|
260
|
-
|
|
261
|
-
connection,
|
|
262
|
-
connection,
|
|
263
|
-
"Junk",
|
|
264
|
-
"INBOX",
|
|
265
|
-
42,
|
|
266
|
-
undefined,
|
|
267
|
-
);
|
|
90
|
+
const outcome = await move(server);
|
|
268
91
|
|
|
269
|
-
|
|
92
|
+
assert.deepEqual(outcome, {
|
|
93
|
+
kind: "moved",
|
|
94
|
+
newUid: Math.max(...server.uids("INBOX", HEADER)),
|
|
270
95
|
});
|
|
271
96
|
});
|
|
272
|
-
});
|
|
273
97
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
3,
|
|
279
|
-
);
|
|
280
|
-
assert.equal(
|
|
281
|
-
getPlacementMoveMaxAttempts({ PLACEMENT_MOVE_MAX_ATTEMPTS: "5" }),
|
|
282
|
-
5,
|
|
283
|
-
);
|
|
284
|
-
});
|
|
98
|
+
it("never settles a move the destination does not confirm while the source still holds it", async () => {
|
|
99
|
+
const server = arrange();
|
|
100
|
+
server.uidplus = false;
|
|
101
|
+
server.ignoreNext.add("MOVE");
|
|
285
102
|
|
|
286
|
-
|
|
287
|
-
assert.equal(getPlacementMoveMaxAttempts({}), 3);
|
|
103
|
+
await assert.rejects(move(server), /unresolved/);
|
|
288
104
|
});
|
|
289
105
|
|
|
290
|
-
it("
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
);
|
|
295
|
-
|
|
296
|
-
getPlacementMoveMaxAttempts({ PLACEMENT_MOVE_MAX_ATTEMPTS: "0" }),
|
|
297
|
-
3,
|
|
298
|
-
);
|
|
299
|
-
});
|
|
106
|
+
it("never settles when the source FETCH drops the row for a message the server still lists", async () => {
|
|
107
|
+
const server = arrange();
|
|
108
|
+
server.uidplus = false;
|
|
109
|
+
server.ignoreNext.add("MOVE");
|
|
110
|
+
const source = server.connection();
|
|
111
|
+
source.fetchMessages = async () => [];
|
|
300
112
|
|
|
301
|
-
|
|
302
|
-
assert.ok(PLACEMENT_MOVE_MAX_ATTEMPTS > 0);
|
|
113
|
+
await assert.rejects(move(server, HEADER, source), /unresolved/);
|
|
303
114
|
});
|
|
304
|
-
});
|
|
305
|
-
|
|
306
|
-
describe("handlePlacementMovePush — deleted mailbox is terminal (#287/#289)", () => {
|
|
307
|
-
const accountId = "pm-acc-zzz";
|
|
308
|
-
const messageId = "pm-msg-zzz";
|
|
309
|
-
const destinationMailboxId = "pm-dest-zzz";
|
|
310
115
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
accountConfigId: "pm-cfg-zzz",
|
|
315
|
-
messageId,
|
|
316
|
-
eventId: "pm-evt-zzz",
|
|
317
|
-
timestamp: 1700000000000,
|
|
318
|
-
};
|
|
116
|
+
it("answers not-found once neither folder holds the message", async () => {
|
|
117
|
+
const server = arrange({ inJunk: false });
|
|
118
|
+
server.uidplus = false;
|
|
319
119
|
|
|
320
|
-
|
|
321
|
-
// the repositories they then mock rather than reaching a composition.
|
|
322
|
-
before(() => {
|
|
323
|
-
setClient({
|
|
324
|
-
account: { get: async () => undefined },
|
|
325
|
-
message: { get: async () => undefined },
|
|
326
|
-
mailbox: { get: async () => undefined },
|
|
327
|
-
placementMove: {
|
|
328
|
-
find: async () => undefined,
|
|
329
|
-
updateState: async () => undefined,
|
|
330
|
-
delete: async () => undefined,
|
|
331
|
-
},
|
|
332
|
-
} as unknown as RemitClient);
|
|
120
|
+
assert.equal((await move(server)).kind, "not-found");
|
|
333
121
|
});
|
|
334
122
|
|
|
335
|
-
|
|
123
|
+
it("falls back to the source alone without a Message-ID, and still never settles while it holds the message", async () => {
|
|
124
|
+
const server = arrange();
|
|
125
|
+
server.uidplus = false;
|
|
126
|
+
server.ignoreNext.add("MOVE");
|
|
336
127
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
mock.method(client.account, "get", async () => ({
|
|
340
|
-
accountId,
|
|
341
|
-
accountConfigId: "pm-cfg-zzz",
|
|
342
|
-
}));
|
|
343
|
-
mock.method(client.message, "get", async () => ({
|
|
344
|
-
messageId,
|
|
345
|
-
mailboxId: destinationMailboxId,
|
|
346
|
-
}));
|
|
347
|
-
mock.method(client.mailbox, "get", async () => {
|
|
348
|
-
throw Object.assign(new Error("Mailbox not found: pm-src-zzz"), {
|
|
349
|
-
name: "NotFoundError",
|
|
350
|
-
});
|
|
351
|
-
});
|
|
352
|
-
mock.method(client.placementMove, "find", async () => ({
|
|
353
|
-
sourceMailboxId: "pm-src-zzz",
|
|
354
|
-
destinationMailboxId,
|
|
355
|
-
}));
|
|
356
|
-
mock.method(client.placementMove, "updateState", async () => {});
|
|
357
|
-
const deleteMarker = mock.method(
|
|
358
|
-
client.placementMove,
|
|
359
|
-
"delete",
|
|
360
|
-
async () => {},
|
|
361
|
-
);
|
|
128
|
+
await assert.rejects(move(server, undefined));
|
|
129
|
+
});
|
|
362
130
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
131
|
+
it("answers not-found without a Message-ID once the source no longer holds the message", async () => {
|
|
132
|
+
const server = arrange({ inJunk: false });
|
|
133
|
+
server.uidplus = false;
|
|
366
134
|
|
|
367
|
-
assert.equal(
|
|
368
|
-
assert.deepEqual(deleteMarker.mock.calls[0].arguments, [messageId]);
|
|
135
|
+
assert.equal((await move(server, undefined)).kind, "not-found");
|
|
369
136
|
});
|
|
370
137
|
});
|