@remit/imap-worker 0.0.89 → 0.0.91
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/dist/account-check.d.ts +27 -0
- package/dist/account-check.js +70 -0
- package/dist/body-sync-gate.d.ts +12 -0
- package/dist/body-sync-gate.js +53 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +193 -0
- package/dist/config-env.d.ts +5 -0
- package/dist/config-env.js +37 -0
- package/dist/connection-pool.d.ts +22 -0
- package/dist/connection-pool.js +97 -0
- package/dist/connection-scope.d.ts +8 -0
- package/dist/connection-scope.js +10 -0
- package/dist/context.d.ts +46 -0
- package/dist/context.js +15 -0
- package/dist/e2e-processor-shim.d.ts +2 -0
- package/dist/e2e-processor-shim.js +181 -0
- package/dist/emit.d.ts +15 -0
- package/dist/emit.js +76 -0
- package/dist/events.d.ts +203 -0
- package/dist/events.js +1 -0
- package/dist/failure-kind.d.ts +10 -0
- package/{src/failure-kind.ts → dist/failure-kind.js} +6 -6
- package/dist/handlers/append-sent-message.d.ts +3 -0
- package/dist/handlers/append-sent-message.js +152 -0
- package/dist/handlers/delete-account-objects.d.ts +3 -0
- package/dist/handlers/delete-account-objects.js +35 -0
- package/dist/handlers/empty-trash.d.ts +14 -0
- package/dist/handlers/empty-trash.js +217 -0
- package/dist/handlers/flag-push.d.ts +38 -0
- package/dist/handlers/flag-push.js +195 -0
- package/dist/handlers/imported-folder.d.ts +4 -0
- package/dist/handlers/imported-folder.js +23 -0
- package/dist/handlers/mailbox-management.d.ts +6 -0
- package/dist/handlers/mailbox-management.js +294 -0
- package/dist/handlers/mailbox-sync-order.d.ts +21 -0
- package/dist/handlers/mailbox-sync-order.js +50 -0
- package/dist/handlers/message-copy.d.ts +40 -0
- package/dist/handlers/message-copy.js +371 -0
- package/dist/handlers/message-delete-terminal.d.ts +60 -0
- package/dist/handlers/message-delete-terminal.js +77 -0
- package/dist/handlers/message-delete.d.ts +57 -0
- package/dist/handlers/message-delete.js +520 -0
- package/{src/handlers/message-move-terminal.ts → dist/handlers/message-move-terminal.d.ts} +18 -105
- package/dist/handlers/message-move-terminal.js +94 -0
- package/dist/handlers/message-move.d.ts +146 -0
- package/dist/handlers/message-move.js +490 -0
- package/dist/handlers/placement-move-push.d.ts +62 -0
- package/dist/handlers/placement-move-push.js +307 -0
- package/dist/handlers/probe-message-id.d.ts +2 -0
- package/dist/handlers/probe-message-id.js +6 -0
- package/dist/handlers/sync-mailboxes.d.ts +45 -0
- package/dist/handlers/sync-mailboxes.js +233 -0
- package/dist/handlers/sync-message-body.d.ts +23 -0
- package/dist/handlers/sync-message-body.js +182 -0
- package/dist/handlers/sync-messages.d.ts +29 -0
- package/dist/handlers/sync-messages.js +285 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +67 -0
- package/dist/lambda-context.d.ts +8 -0
- package/dist/lambda-context.js +33 -0
- package/dist/mailbox-row.d.ts +9 -0
- package/dist/mailbox-row.js +6 -0
- package/dist/poller.d.ts +1 -0
- package/dist/poller.js +51 -0
- package/dist/process.d.ts +6 -0
- package/dist/process.js +3 -0
- package/dist/processor.d.ts +3 -0
- package/dist/processor.js +44 -0
- package/dist/queues.d.ts +10 -0
- package/dist/queues.js +1 -0
- package/dist/s3-account-objects.d.ts +3 -0
- package/dist/s3-account-objects.js +26 -0
- package/dist/scheduler/attachment-sweep.d.ts +11 -0
- package/dist/scheduler/attachment-sweep.js +23 -0
- package/dist/scheduler/calendar-subscriptions.d.ts +26 -0
- package/dist/scheduler/calendar-subscriptions.js +35 -0
- package/dist/scheduler/config.d.ts +27 -0
- package/{src/scheduler/config.ts → dist/scheduler/config.js} +7 -26
- package/{src/scheduler/decide-due.ts → dist/scheduler/decide-due.d.ts} +3 -12
- package/dist/scheduler/decide-due.js +17 -0
- package/dist/scheduler/handler.d.ts +21 -0
- package/{src/scheduler/handler.ts → dist/scheduler/handler.js} +24 -36
- package/{src/scheduler/loop.ts → dist/scheduler/loop.d.ts} +7 -23
- package/dist/scheduler/loop.js +8 -0
- package/dist/scheduler/run-tick.d.ts +62 -0
- package/dist/scheduler/run-tick.js +164 -0
- package/dist/scheduler/runner.d.ts +2 -0
- package/{src/scheduler/runner.ts → dist/scheduler/runner.js} +58 -84
- package/dist/scheduler/stranded-placement-redrive.d.ts +16 -0
- package/dist/scheduler/stranded-placement-redrive.js +172 -0
- package/dist/sqs-queues.d.ts +4 -0
- package/dist/sqs-queues.js +22 -0
- package/dist/with-oauth-lifecycle-deps.d.ts +5 -0
- package/dist/with-oauth-lifecycle-deps.js +17 -0
- package/dist/with-oauth-lifecycle.d.ts +56 -0
- package/dist/with-oauth-lifecycle.js +92 -0
- package/{src/worker-version.ts → dist/worker-version.d.ts} +1 -2
- package/dist/worker-version.js +12 -0
- package/package.json +75 -69
- package/src/account-check.test.ts +0 -149
- package/src/account-check.ts +0 -101
- package/src/body-sync-gate.test.ts +0 -185
- package/src/body-sync-gate.ts +0 -86
- package/src/cli.ts +0 -224
- package/src/connection-scope.test.ts +0 -266
- package/src/connection-scope.ts +0 -314
- package/src/credential-less-sync-fence.test.ts +0 -152
- package/src/e2e-processor-shim.ts +0 -232
- package/src/emit.test.ts +0 -127
- package/src/emit.ts +0 -117
- package/src/events.ts +0 -252
- package/src/failure-kind.test.ts +0 -42
- package/src/handlers/append-sent-message.sqlite.test.ts +0 -176
- package/src/handlers/append-sent-message.ts +0 -264
- package/src/handlers/delete-account-objects.test.ts +0 -128
- package/src/handlers/delete-account-objects.ts +0 -94
- package/src/handlers/empty-trash.sqlite.test.ts +0 -209
- package/src/handlers/empty-trash.ts +0 -352
- package/src/handlers/flag-push.ts +0 -285
- package/src/handlers/imported-folder.test.ts +0 -63
- package/src/handlers/imported-folder.ts +0 -38
- package/src/handlers/mailbox-management.sqlite.test.ts +0 -265
- package/src/handlers/mailbox-management.ts +0 -486
- package/src/handlers/mailbox-sync-order.test.ts +0 -93
- package/src/handlers/mailbox-sync-order.ts +0 -65
- package/src/handlers/message-copy.sqlite.test.ts +0 -298
- package/src/handlers/message-copy.ts +0 -566
- package/src/handlers/message-delete-terminal.ts +0 -147
- package/src/handlers/message-delete.sqlite.test.ts +0 -291
- package/src/handlers/message-delete.ts +0 -885
- package/src/handlers/message-move.sqlite.test.ts +0 -271
- package/src/handlers/message-move.test.ts +0 -216
- package/src/handlers/message-move.ts +0 -752
- package/src/handlers/placement-move-push.test.ts +0 -137
- package/src/handlers/placement-move-push.ts +0 -475
- package/src/handlers/placement-push.sqlite.test.ts +0 -298
- package/src/handlers/probe-message-id.sqlite.test.ts +0 -192
- package/src/handlers/probe-message-id.ts +0 -15
- package/src/handlers/sync-mailboxes.test.ts +0 -183
- package/src/handlers/sync-mailboxes.ts +0 -421
- package/src/handlers/sync-message-body.sqlite.test.ts +0 -314
- package/src/handlers/sync-message-body.test.ts +0 -150
- package/src/handlers/sync-message-body.ts +0 -365
- package/src/handlers/sync-messages.sqlite.test.ts +0 -183
- package/src/handlers/sync-messages.ts +0 -525
- package/src/index.test.ts +0 -22
- package/src/index.ts +0 -86
- package/src/mail-sync-selection.sqlite.test.ts +0 -234
- package/src/mailbox-row.ts +0 -20
- package/src/pending-sync.sqlite.test.ts +0 -166
- package/src/poller.ts +0 -55
- package/src/processor.ts +0 -60
- package/src/queues.ts +0 -49
- package/src/scheduler/attachment-sweep.sqlite.test.ts +0 -75
- package/src/scheduler/attachment-sweep.ts +0 -46
- package/src/scheduler/calendar-subscriptions.sqlite.test.ts +0 -147
- package/src/scheduler/calendar-subscriptions.ts +0 -103
- package/src/scheduler/config.test.ts +0 -40
- package/src/scheduler/decide-due.test.ts +0 -44
- package/src/scheduler/half-deleted-accounts.sqlite.test.ts +0 -196
- package/src/scheduler/loop.test.ts +0 -143
- package/src/scheduler/run-tick.sqlite.test.ts +0 -191
- package/src/scheduler/run-tick.ts +0 -281
- package/src/scheduler/stranded-placement-redrive.ts +0 -265
- package/src/stranded-placement.sqlite.test.ts +0 -794
- package/src/test-helpers/test-world.ts +0 -427
- package/src/thread-join.sqlite.test.ts +0 -106
- package/src/with-oauth-lifecycle-deps.sqlite.test.ts +0 -269
- package/src/with-oauth-lifecycle-deps.ts +0 -71
- package/src/with-oauth-lifecycle.ts +0 -158
- package/tsconfig.json +0 -8
|
@@ -1,271 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { after, before, describe, it } from "node:test";
|
|
3
|
-
import {
|
|
4
|
-
MailboxCursorState,
|
|
5
|
-
MailboxSpecialUse,
|
|
6
|
-
MessageMutation,
|
|
7
|
-
MessageStatus,
|
|
8
|
-
MessageSyncStatus,
|
|
9
|
-
} from "@remit/domain-enums";
|
|
10
|
-
import type { MessageMoveEvent } from "../events.js";
|
|
11
|
-
import { mail, openStore, type World } from "../test-helpers/test-world.js";
|
|
12
|
-
import { handleMessageMove } from "./message-move.js";
|
|
13
|
-
|
|
14
|
-
let store: Awaited<ReturnType<typeof openStore>>;
|
|
15
|
-
|
|
16
|
-
before(async () => {
|
|
17
|
-
store = await openStore();
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
after(() => store.close());
|
|
21
|
-
|
|
22
|
-
interface Arranged {
|
|
23
|
-
w: World;
|
|
24
|
-
messageId: string;
|
|
25
|
-
uid: number;
|
|
26
|
-
messageIdHeader: string;
|
|
27
|
-
inboxId: string;
|
|
28
|
-
archiveId: string;
|
|
29
|
-
event: MessageMoveEvent;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/** A message in INBOX, moved to Archive through the service the API calls. */
|
|
33
|
-
const arrange = async (
|
|
34
|
-
options: { olderCopyInArchive?: boolean } = {},
|
|
35
|
-
): Promise<Arranged> => {
|
|
36
|
-
const w = await store.world();
|
|
37
|
-
const inbox = await w.folder("INBOX");
|
|
38
|
-
const archive = await w.folder("Archive", {
|
|
39
|
-
specialUse: MailboxSpecialUse.Archive,
|
|
40
|
-
});
|
|
41
|
-
const seeded = await w.message(inbox);
|
|
42
|
-
if (options.olderCopyInArchive) {
|
|
43
|
-
w.imap.deliver("Archive", mail(seeded.messageIdHeader));
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
await w.client.messageMove.moveMessage(
|
|
47
|
-
w.account.accountConfigId,
|
|
48
|
-
seeded.messageId,
|
|
49
|
-
archive.mailboxId,
|
|
50
|
-
w.account.accountId,
|
|
51
|
-
);
|
|
52
|
-
const event = w
|
|
53
|
-
.queued()
|
|
54
|
-
.find(
|
|
55
|
-
(body) => body.type === "MESSAGE_MOVE",
|
|
56
|
-
) as unknown as MessageMoveEvent;
|
|
57
|
-
assert.ok(event, "the move enqueued a MESSAGE_MOVE");
|
|
58
|
-
|
|
59
|
-
return {
|
|
60
|
-
w,
|
|
61
|
-
messageId: seeded.messageId,
|
|
62
|
-
uid: seeded.uid,
|
|
63
|
-
messageIdHeader: seeded.messageIdHeader,
|
|
64
|
-
inboxId: inbox.mailboxId,
|
|
65
|
-
archiveId: archive.mailboxId,
|
|
66
|
-
event,
|
|
67
|
-
};
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
const run = (a: Arranged, receiveCount = 1) =>
|
|
71
|
-
handleMessageMove(a.event, a.w.log, receiveCount, a.w.scope);
|
|
72
|
-
|
|
73
|
-
const resyncs = (w: World): unknown[] =>
|
|
74
|
-
w
|
|
75
|
-
.queued()
|
|
76
|
-
.filter((body) => body.type === "SYNC_MESSAGES")
|
|
77
|
-
.map((body) => body.mailboxId);
|
|
78
|
-
|
|
79
|
-
const logged = (w: World, key: string, value: string): number =>
|
|
80
|
-
w.logs.filter((line) => line.fields[key] === value).length;
|
|
81
|
-
|
|
82
|
-
describe("a message move", () => {
|
|
83
|
-
it("lands at the destination and settles the row on the uid the server gave it", async () => {
|
|
84
|
-
const a = await arrange();
|
|
85
|
-
|
|
86
|
-
await run(a);
|
|
87
|
-
|
|
88
|
-
assert.deepEqual(a.w.imap.uids("INBOX"), []);
|
|
89
|
-
const [archiveUid] = a.w.imap.uids("Archive", a.messageIdHeader);
|
|
90
|
-
const row = await a.w.row(a.messageId);
|
|
91
|
-
assert.equal(row?.mailboxId, a.archiveId);
|
|
92
|
-
assert.equal(row?.uid, archiveUid);
|
|
93
|
-
assert.equal(row?.status, MessageStatus.active);
|
|
94
|
-
assert.deepEqual(await a.w.threadRows(a.messageId), [
|
|
95
|
-
{ mailboxId: a.archiveId, uid: archiveUid, isDeleted: false },
|
|
96
|
-
]);
|
|
97
|
-
assert.deepEqual(resyncs(a.w), [a.inboxId, a.archiveId]);
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
// Issue #1122: without COPYUID the destination is asked by Message-ID, and
|
|
101
|
-
// an older copy already there must not take the row.
|
|
102
|
-
it("binds to the copy this move delivered when the server sends no COPYUID", async () => {
|
|
103
|
-
const a = await arrange({ olderCopyInArchive: true });
|
|
104
|
-
a.w.imap.uidplus = false;
|
|
105
|
-
|
|
106
|
-
await run(a);
|
|
107
|
-
|
|
108
|
-
const copies = a.w.imap.uids("Archive", a.messageIdHeader);
|
|
109
|
-
assert.equal(copies.length, 2);
|
|
110
|
-
assert.equal((await a.w.row(a.messageId))?.uid, Math.max(...copies));
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
it("issues nothing when a redelivery finds the move already settled", async () => {
|
|
114
|
-
const a = await arrange();
|
|
115
|
-
await run(a);
|
|
116
|
-
const commands = a.w.imap.log.length;
|
|
117
|
-
|
|
118
|
-
await run(a);
|
|
119
|
-
|
|
120
|
-
assert.equal(a.w.imap.log.length, commands);
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
it("marks the row failed and redelivers when the connection drops mid-move", async () => {
|
|
124
|
-
const a = await arrange();
|
|
125
|
-
a.w.imap.failNext.MOVE = new Error("Connection reset");
|
|
126
|
-
|
|
127
|
-
await assert.rejects(run(a), /Connection reset/);
|
|
128
|
-
|
|
129
|
-
const row = await a.w.row(a.messageId);
|
|
130
|
-
assert.equal(row?.status, MessageStatus.moving);
|
|
131
|
-
assert.equal(row?.syncStatus, MessageSyncStatus.failed);
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
// Issue #655, BROKEN: the message never left, so the row goes back on the
|
|
135
|
-
// pair the server confirmed and says a move gave up.
|
|
136
|
-
it("puts the row back on its source at the ceiling when the message never left", async () => {
|
|
137
|
-
const a = await arrange();
|
|
138
|
-
a.w.imap.failNext.MOVE = new Error("Connection reset");
|
|
139
|
-
|
|
140
|
-
await run(a, 3);
|
|
141
|
-
|
|
142
|
-
const row = await a.w.row(a.messageId);
|
|
143
|
-
assert.equal(row?.mailboxId, a.inboxId);
|
|
144
|
-
assert.equal(row?.uid, a.uid);
|
|
145
|
-
assert.notEqual(row?.status, MessageStatus.moving);
|
|
146
|
-
assert.equal(row?.abandonedMutation, MessageMutation.move);
|
|
147
|
-
assert.deepEqual(resyncs(a.w), [a.inboxId, a.archiveId]);
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
// Issue #655, RECONCILED: the message is gone from the source, so the stale
|
|
151
|
-
// row goes and the resync re-projects it wherever it actually is.
|
|
152
|
-
it("reconciles the row away at the ceiling when the source no longer holds it", async () => {
|
|
153
|
-
const a = await arrange();
|
|
154
|
-
a.w.imap.empty("INBOX");
|
|
155
|
-
a.w.imap.failNext.MOVE = new Error("Connection reset");
|
|
156
|
-
|
|
157
|
-
await run(a, 3);
|
|
158
|
-
|
|
159
|
-
assert.equal(await a.w.row(a.messageId), undefined);
|
|
160
|
-
assert.deepEqual(resyncs(a.w), [a.inboxId, a.archiveId]);
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
it("creates a destination the server does not have and redelivers", async () => {
|
|
164
|
-
const a = await arrange();
|
|
165
|
-
a.w.imap.removeFolder("Archive");
|
|
166
|
-
|
|
167
|
-
await assert.rejects(run(a), /TRYCREATE/);
|
|
168
|
-
|
|
169
|
-
assert.deepEqual(a.w.imap.commands("CREATE"), ["CREATE Archive"]);
|
|
170
|
-
assert.deepEqual(a.w.imap.uids("INBOX"), [a.uid]);
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
it("hands the row back without connecting when the destination mailbox was deleted", async () => {
|
|
174
|
-
const a = await arrange();
|
|
175
|
-
await a.w.client.mailbox.delete(a.w.account.accountId, a.archiveId);
|
|
176
|
-
|
|
177
|
-
await run(a);
|
|
178
|
-
|
|
179
|
-
assert.equal(a.w.imap.connections, 0);
|
|
180
|
-
const row = await a.w.row(a.messageId);
|
|
181
|
-
assert.equal(row?.mailboxId, a.inboxId);
|
|
182
|
-
assert.equal(row?.uid, a.uid);
|
|
183
|
-
assert.equal(row?.status, MessageStatus.active);
|
|
184
|
-
assert.equal(logged(a.w, "metric", "message_move_destination_gone"), 1);
|
|
185
|
-
assert.deepEqual(resyncs(a.w), [a.inboxId]);
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
it("acks without connecting when the source mailbox was deleted", async () => {
|
|
189
|
-
const a = await arrange();
|
|
190
|
-
await a.w.client.mailbox.delete(a.w.account.accountId, a.inboxId);
|
|
191
|
-
|
|
192
|
-
await run(a);
|
|
193
|
-
|
|
194
|
-
assert.equal(a.w.imap.connections, 0);
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
// Issue #1203: a paused cursor that acked left the row naming the
|
|
198
|
-
// destination with the source's uid, unmovable and undeletable for good.
|
|
199
|
-
it("hands the row back to its source without connecting while the cursor is rebuilding", async () => {
|
|
200
|
-
const a = await arrange();
|
|
201
|
-
await a.w.client.mailbox.update(a.w.account.accountId, a.inboxId, {
|
|
202
|
-
cursorState: MailboxCursorState.rebuilding,
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
await run(a);
|
|
206
|
-
|
|
207
|
-
assert.equal(a.w.imap.connections, 0);
|
|
208
|
-
const row = await a.w.row(a.messageId);
|
|
209
|
-
assert.equal(row?.mailboxId, a.inboxId);
|
|
210
|
-
assert.equal(row?.uid, a.uid);
|
|
211
|
-
assert.equal(row?.status, MessageStatus.active);
|
|
212
|
-
assert.equal(logged(a.w, "metric", "message_move_dropped_on_pause"), 1);
|
|
213
|
-
assert.deepEqual(resyncs(a.w), [a.inboxId, a.archiveId]);
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
// The redelivery half of #1203: the source answers "no" because it was
|
|
217
|
-
// recreated, and Archive holds an older copy. Settling on it would bind the
|
|
218
|
-
// row to mail this move never touched.
|
|
219
|
-
it("never settles a redelivered paused move off a sighting at the destination", async () => {
|
|
220
|
-
const a = await arrange({ olderCopyInArchive: true });
|
|
221
|
-
a.w.imap.empty("INBOX");
|
|
222
|
-
await a.w.client.mailbox.update(a.w.account.accountId, a.inboxId, {
|
|
223
|
-
cursorState: MailboxCursorState.rebuilding,
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
await run(a, 2);
|
|
227
|
-
|
|
228
|
-
assert.deepEqual(a.w.imap.commands("MOVE"), []);
|
|
229
|
-
const row = await a.w.row(a.messageId);
|
|
230
|
-
assert.equal(row?.mailboxId, a.inboxId);
|
|
231
|
-
assert.equal(row?.uid, a.uid);
|
|
232
|
-
assert.equal(row?.syncStatus, MessageSyncStatus.abandoned);
|
|
233
|
-
assert.equal(row?.abandonedMutation, MessageMutation.move);
|
|
234
|
-
assert.equal(
|
|
235
|
-
logged(a.w, "alert", "message_move_paused_placement_unproven"),
|
|
236
|
-
1,
|
|
237
|
-
);
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
it("moves nothing when the source's UIDVALIDITY changed under it", async () => {
|
|
241
|
-
const a = await arrange();
|
|
242
|
-
a.w.imap.setUidValidity("INBOX", 2);
|
|
243
|
-
|
|
244
|
-
await run(a);
|
|
245
|
-
|
|
246
|
-
assert.deepEqual(a.w.imap.commands("MOVE"), []);
|
|
247
|
-
assert.equal((await a.w.row(a.messageId))?.mailboxId, a.inboxId);
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
it("moves under the destination's current path when it was renamed after enqueue", async () => {
|
|
251
|
-
const a = await arrange();
|
|
252
|
-
a.w.imap.renameFolder("Archive", "Records");
|
|
253
|
-
await a.w.client.mailbox.update(a.w.account.accountId, a.archiveId, {
|
|
254
|
-
fullPath: "Records",
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
await run(a);
|
|
258
|
-
|
|
259
|
-
assert.equal(a.w.imap.uids("Records", a.messageIdHeader).length, 1);
|
|
260
|
-
assert.equal((await a.w.row(a.messageId))?.mailboxId, a.archiveId);
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
it("acks without connecting when the message row is already gone", async () => {
|
|
264
|
-
const a = await arrange();
|
|
265
|
-
await a.w.client.message.delete(a.messageId);
|
|
266
|
-
|
|
267
|
-
await run(a);
|
|
268
|
-
|
|
269
|
-
assert.equal(a.w.imap.connections, 0);
|
|
270
|
-
});
|
|
271
|
-
});
|
|
@@ -1,216 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert";
|
|
2
|
-
import { describe, it } from "node:test";
|
|
3
|
-
import type { ThreadMessageItem } from "@remit/data-ports";
|
|
4
|
-
import {
|
|
5
|
-
eml,
|
|
6
|
-
FakeImapServer,
|
|
7
|
-
} from "@remit/mailbox-service/test-helpers/fake-imap";
|
|
8
|
-
import {
|
|
9
|
-
buildThreadMessageMoveUpdate,
|
|
10
|
-
probePausedPlacement,
|
|
11
|
-
searchMailboxForHighestMessageIdUid,
|
|
12
|
-
} from "./message-move.js";
|
|
13
|
-
|
|
14
|
-
const sourceMailboxId = "source-mailbox-id-aaaaaaaaa";
|
|
15
|
-
const destinationMailboxId = "destination-mailbox-aaaaa";
|
|
16
|
-
|
|
17
|
-
const baseThreadMessage = {
|
|
18
|
-
sentDate: 1700000000000,
|
|
19
|
-
mailboxId: sourceMailboxId,
|
|
20
|
-
isRead: true,
|
|
21
|
-
isDeleted: false,
|
|
22
|
-
hasStars: true,
|
|
23
|
-
hasAttachment: false,
|
|
24
|
-
} satisfies Pick<
|
|
25
|
-
ThreadMessageItem,
|
|
26
|
-
| "sentDate"
|
|
27
|
-
| "mailboxId"
|
|
28
|
-
| "isRead"
|
|
29
|
-
| "isDeleted"
|
|
30
|
-
| "hasStars"
|
|
31
|
-
| "hasAttachment"
|
|
32
|
-
>;
|
|
33
|
-
|
|
34
|
-
describe("buildThreadMessageMoveUpdate", () => {
|
|
35
|
-
// Regression for the same composites-direction landmine PR #186 fixed in
|
|
36
|
-
// `flag-queue.ts`. The CURRENT row state must go in `composites`; the NEW
|
|
37
|
-
// values must go in `set`. Flipping any of these silently drops the
|
|
38
|
-
// ThreadMessage local update on every IMAP MOVE — the row stays in the
|
|
39
|
-
// source mailbox while the IMAP server thinks it moved.
|
|
40
|
-
|
|
41
|
-
it("set carries the NEW uid, mailboxId, and isDeleted=false", () => {
|
|
42
|
-
const args = buildThreadMessageMoveUpdate(
|
|
43
|
-
baseThreadMessage,
|
|
44
|
-
42,
|
|
45
|
-
destinationMailboxId,
|
|
46
|
-
);
|
|
47
|
-
|
|
48
|
-
assert.strictEqual(args.set.uid, 42, "set.uid must be the NEW uid");
|
|
49
|
-
assert.strictEqual(
|
|
50
|
-
args.set.mailboxId,
|
|
51
|
-
destinationMailboxId,
|
|
52
|
-
"set.mailboxId must be the NEW destination mailbox",
|
|
53
|
-
);
|
|
54
|
-
assert.strictEqual(
|
|
55
|
-
args.set.isDeleted,
|
|
56
|
-
false,
|
|
57
|
-
"set.isDeleted must be false (move is not a delete)",
|
|
58
|
-
);
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it("composites mirrors every CURRENT indexed attribute on the threadMessage", () => {
|
|
62
|
-
const tm = {
|
|
63
|
-
sentDate: 1700000000123,
|
|
64
|
-
mailboxId: sourceMailboxId,
|
|
65
|
-
isRead: false,
|
|
66
|
-
isDeleted: true,
|
|
67
|
-
hasStars: true,
|
|
68
|
-
hasAttachment: true,
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
const args = buildThreadMessageMoveUpdate(tm, 99, destinationMailboxId);
|
|
72
|
-
|
|
73
|
-
assert.deepStrictEqual(args.composites, {
|
|
74
|
-
sentDate: tm.sentDate,
|
|
75
|
-
mailboxId: tm.mailboxId,
|
|
76
|
-
isRead: tm.isRead,
|
|
77
|
-
isDeleted: tm.isDeleted,
|
|
78
|
-
hasStars: tm.hasStars,
|
|
79
|
-
hasAttachment: tm.hasAttachment,
|
|
80
|
-
});
|
|
81
|
-
});
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
const _accountId = "alice-account-aaaaaaaaaa";
|
|
85
|
-
|
|
86
|
-
describe("probePausedPlacement — which folder a paused mutation left the message in (#1203)", () => {
|
|
87
|
-
const HEADER = "<paused@example.com>";
|
|
88
|
-
|
|
89
|
-
const serverHolding = (holdings: Record<string, number[]>) => {
|
|
90
|
-
const server = new FakeImapServer();
|
|
91
|
-
server.addFolder("INBOX");
|
|
92
|
-
server.addFolder("Archive");
|
|
93
|
-
for (const [path, uids] of Object.entries(holdings)) {
|
|
94
|
-
for (const uid of uids)
|
|
95
|
-
server.deliver(path, eml({ "Message-ID": HEADER }), { uid: uid });
|
|
96
|
-
}
|
|
97
|
-
return server;
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
const opened = (server: FakeImapServer): string[] =>
|
|
101
|
-
server.log
|
|
102
|
-
.filter((line) => /^(SELECT|EXAMINE) /.test(line))
|
|
103
|
-
.map((line) => line.split(" ")[1] ?? "");
|
|
104
|
-
|
|
105
|
-
const probe = (
|
|
106
|
-
server: FakeImapServer,
|
|
107
|
-
input: Partial<Parameters<typeof probePausedPlacement>[1]> = {},
|
|
108
|
-
) =>
|
|
109
|
-
probePausedPlacement(server.connection(), {
|
|
110
|
-
messageIdHeader: HEADER,
|
|
111
|
-
sourceMailboxPath: "INBOX",
|
|
112
|
-
destinationMailboxPath: "Archive",
|
|
113
|
-
...input,
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
it("stops at the source and never asks the destination when the source still holds it", async () => {
|
|
117
|
-
const server = serverHolding({ INBOX: [4], Archive: [12] });
|
|
118
|
-
|
|
119
|
-
assert.deepStrictEqual(await probe(server), { kind: "at-source" });
|
|
120
|
-
assert.deepStrictEqual(
|
|
121
|
-
opened(server),
|
|
122
|
-
["INBOX"],
|
|
123
|
-
"a source hit ends it: with the mutation unrun, every destination hit is an older copy (#1122)",
|
|
124
|
-
);
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
it("answers the destination's highest matching uid once the source has let go", async () => {
|
|
128
|
-
const server = serverHolding({ Archive: [12, 77] });
|
|
129
|
-
|
|
130
|
-
assert.deepStrictEqual(await probe(server), {
|
|
131
|
-
kind: "at-destination",
|
|
132
|
-
uid: 77,
|
|
133
|
-
});
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
it("answers gone when neither folder holds it", async () => {
|
|
137
|
-
assert.deepStrictEqual(await probe(serverHolding({})), { kind: "gone" });
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
it("answers gone, not at-source, when there is no destination to ask", async () => {
|
|
141
|
-
const server = serverHolding({});
|
|
142
|
-
|
|
143
|
-
assert.deepStrictEqual(
|
|
144
|
-
await probe(server, { destinationMailboxPath: undefined }),
|
|
145
|
-
{ kind: "gone" },
|
|
146
|
-
);
|
|
147
|
-
assert.deepStrictEqual(opened(server), ["INBOX"]);
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
// A row with no Message-ID header cannot be asked about, and the server's
|
|
151
|
-
// silence is not an answer. `unprobeable` keeps that distinct from `gone`.
|
|
152
|
-
it("asks nothing at all when the row carries no Message-ID header", async () => {
|
|
153
|
-
const server = serverHolding({ INBOX: [4] });
|
|
154
|
-
|
|
155
|
-
assert.deepStrictEqual(
|
|
156
|
-
await probe(server, { messageIdHeader: undefined }),
|
|
157
|
-
{
|
|
158
|
-
kind: "unprobeable",
|
|
159
|
-
},
|
|
160
|
-
);
|
|
161
|
-
assert.deepStrictEqual(server.log, []);
|
|
162
|
-
});
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
describe("searchMailboxForHighestMessageIdUid — the probe that binds a move to a UID (#912)", () => {
|
|
166
|
-
const archiveHolding = (messages: Array<[uid: number, header: string]>) => {
|
|
167
|
-
const server = new FakeImapServer();
|
|
168
|
-
server.addFolder("Archive");
|
|
169
|
-
for (const [uid, header] of messages)
|
|
170
|
-
server.deliver("Archive", eml({ "Message-ID": header }), { uid: uid });
|
|
171
|
-
return server;
|
|
172
|
-
};
|
|
173
|
-
|
|
174
|
-
const search = (server: FakeImapServer, header: string) =>
|
|
175
|
-
searchMailboxForHighestMessageIdUid(server.connection(), "Archive", header);
|
|
176
|
-
|
|
177
|
-
it("asks the folder by Message-ID rather than by an interpolated string", async () => {
|
|
178
|
-
const server = archiveHolding([]);
|
|
179
|
-
const hostile = '<a"b@example.com>\r\nUID 1';
|
|
180
|
-
|
|
181
|
-
await search(server, hostile);
|
|
182
|
-
|
|
183
|
-
assert.deepStrictEqual(server.commands("SEARCH"), [
|
|
184
|
-
`SEARCH Archive ${JSON.stringify([["HEADER", "Message-ID", hostile]])}`,
|
|
185
|
-
]);
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
it("answers null, not another message's UID, when the folder holds no such message", async () => {
|
|
189
|
-
const server = archiveHolding([
|
|
190
|
-
[11, "<stranger-a@example.com>"],
|
|
191
|
-
[12, "<stranger-b@example.com>"],
|
|
192
|
-
]);
|
|
193
|
-
|
|
194
|
-
assert.strictEqual(await search(server, "<moved@example.com>"), null);
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
it("answers the UID of the message carrying that Message-ID", async () => {
|
|
198
|
-
const server = archiveHolding([
|
|
199
|
-
[11, "<stranger-a@example.com>"],
|
|
200
|
-
[12, "<moved@example.com>"],
|
|
201
|
-
]);
|
|
202
|
-
|
|
203
|
-
assert.strictEqual(await search(server, "<moved@example.com>"), 12);
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
// RFC 3501 does not order a SEARCH response, so the fresh copy is the
|
|
207
|
-
// highest uid returned rather than the last one returned.
|
|
208
|
-
it("takes the highest match whatever order the server lists them in", async () => {
|
|
209
|
-
const server = archiveHolding([
|
|
210
|
-
[40, "<moved@example.com>"],
|
|
211
|
-
[12, "<moved@example.com>"],
|
|
212
|
-
]);
|
|
213
|
-
|
|
214
|
-
assert.strictEqual(await search(server, "<moved@example.com>"), 40);
|
|
215
|
-
});
|
|
216
|
-
});
|