@remit/imap-worker 0.0.90 → 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} +25 -42
- 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} +57 -78
- 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 -218
- 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 -145
- package/src/emit.ts +0 -115
- 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 -126
- package/src/handlers/delete-account-objects.ts +0 -96
- 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 -569
- 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 -888
- 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 -755
- package/src/handlers/placement-move-push.test.ts +0 -137
- package/src/handlers/placement-move-push.ts +0 -478
- package/src/handlers/placement-push.sqlite.test.ts +0 -299
- 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 -425
- 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 -366
- package/src/handlers/sync-messages.sqlite.test.ts +0 -195
- package/src/handlers/sync-messages.ts +0 -537
- package/src/index.test.ts +0 -22
- package/src/index.ts +0 -82
- package/src/mail-sync-selection.sqlite.test.ts +0 -227
- package/src/mailbox-row.ts +0 -20
- package/src/pending-sync.sqlite.test.ts +0 -165
- package/src/poller.ts +0 -55
- package/src/processor.ts +0 -62
- package/src/queues.ts +0 -40
- package/src/scheduler/attachment-sweep.sqlite.test.ts +0 -74
- 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 -799
- package/src/test-helpers/test-world.ts +0 -425
- package/src/test-helpers/worker-queues.ts +0 -11
- 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,299 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The two pushes body sync leaves behind: a placement move (Junk, a filter's
|
|
3
|
-
* folder) and a flag (auto-mark-read). They ride different queues, so either
|
|
4
|
-
* can reach the server first, and the flag must never land on the uid of a
|
|
5
|
-
* move that has not settled (#496).
|
|
6
|
-
*/
|
|
7
|
-
import assert from "node:assert/strict";
|
|
8
|
-
import { after, before, describe, it, mock } from "node:test";
|
|
9
|
-
import type { MailboxItem } from "@remit/data-ports";
|
|
10
|
-
import {
|
|
11
|
-
MailboxCursorState,
|
|
12
|
-
MailboxSpecialUse,
|
|
13
|
-
MessageStatus,
|
|
14
|
-
} from "@remit/domain-enums";
|
|
15
|
-
import { PlacementMoveService } from "@remit/mailbox-service";
|
|
16
|
-
import type { FlagPushEvent, PlacementMovePushEvent } from "../events.js";
|
|
17
|
-
import {
|
|
18
|
-
openStore,
|
|
19
|
-
type SeededMessage,
|
|
20
|
-
type World,
|
|
21
|
-
} from "../test-helpers/test-world.js";
|
|
22
|
-
import { FLAG_PUSH_DEFER_MAX_MS, handleFlagPush } from "./flag-push.js";
|
|
23
|
-
import { handlePlacementMovePush } from "./placement-move-push.js";
|
|
24
|
-
|
|
25
|
-
const SEEN = "\\Seen";
|
|
26
|
-
|
|
27
|
-
let store: Awaited<ReturnType<typeof openStore>>;
|
|
28
|
-
|
|
29
|
-
before(async () => {
|
|
30
|
-
store = await openStore();
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
after(() => store.close());
|
|
34
|
-
|
|
35
|
-
interface Arranged {
|
|
36
|
-
w: World;
|
|
37
|
-
inbox: MailboxItem;
|
|
38
|
-
junk: MailboxItem;
|
|
39
|
-
message: SeededMessage;
|
|
40
|
-
placement: PlacementMoveService;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const arrange = async (): Promise<Arranged> => {
|
|
44
|
-
const w = await store.world();
|
|
45
|
-
const inbox = await w.folder("INBOX");
|
|
46
|
-
const junk = await w.folder("Junk", { specialUse: MailboxSpecialUse.Junk });
|
|
47
|
-
const message = await w.message(inbox);
|
|
48
|
-
const placement = new PlacementMoveService({
|
|
49
|
-
messageService: w.client.message,
|
|
50
|
-
threadMessageService: w.client.threadMessage,
|
|
51
|
-
markerService: w.client.placementMove,
|
|
52
|
-
addressService: w.client.address,
|
|
53
|
-
mailboxSpecialUseService: w.client.mailboxSpecialUse,
|
|
54
|
-
mailboxService: w.client.mailbox,
|
|
55
|
-
queue: w.client.queues.messageManagement,
|
|
56
|
-
});
|
|
57
|
-
return { w, inbox, junk, message, placement };
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
const queuedOf = <T>(w: World, type: string): T => {
|
|
61
|
-
const event = w
|
|
62
|
-
.queued()
|
|
63
|
-
.filter((body) => body.type === type)
|
|
64
|
-
.at(-1) as T | undefined;
|
|
65
|
-
assert.ok(event, `a ${type} was enqueued`);
|
|
66
|
-
return event;
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
const markAsRead = async (a: Arranged): Promise<FlagPushEvent> => {
|
|
70
|
-
await a.w.client.flagQueue.markAsRead(
|
|
71
|
-
a.w.account.accountConfigId,
|
|
72
|
-
a.message.messageId,
|
|
73
|
-
a.w.account.accountId,
|
|
74
|
-
);
|
|
75
|
-
return queuedOf<FlagPushEvent>(a.w, "FLAG_PUSH");
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
const moveToJunk = async (a: Arranged): Promise<PlacementMovePushEvent> => {
|
|
79
|
-
await a.placement.moveMessage(
|
|
80
|
-
a.w.account.accountConfigId,
|
|
81
|
-
a.message.messageId,
|
|
82
|
-
a.junk.mailboxId,
|
|
83
|
-
a.w.account.accountId,
|
|
84
|
-
);
|
|
85
|
-
return queuedOf<PlacementMovePushEvent>(a.w, "PLACEMENT_MOVE_PUSH");
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
const pushFlag = (a: Arranged, event: FlagPushEvent, receiveCount = 1) =>
|
|
89
|
-
handleFlagPush(event, a.w.log, receiveCount, a.w.scope);
|
|
90
|
-
|
|
91
|
-
const pushMove = (
|
|
92
|
-
a: Arranged,
|
|
93
|
-
event: PlacementMovePushEvent,
|
|
94
|
-
receiveCount = 1,
|
|
95
|
-
) =>
|
|
96
|
-
handlePlacementMovePush(event, a.w.log, a.w.queues, receiveCount, a.w.scope);
|
|
97
|
-
|
|
98
|
-
const seenOnServer = (a: Arranged, path: string): boolean[] =>
|
|
99
|
-
a.w.imap
|
|
100
|
-
.messages(path)
|
|
101
|
-
.filter((m) => m.messageIdHeader === a.message.messageIdHeader)
|
|
102
|
-
.map((m) => m.flags.includes(SEEN));
|
|
103
|
-
|
|
104
|
-
describe("a flag push", () => {
|
|
105
|
-
it("stores the flag on the server and clears its marker", async () => {
|
|
106
|
-
const a = await arrange();
|
|
107
|
-
const event = await markAsRead(a);
|
|
108
|
-
|
|
109
|
-
await pushFlag(a, event);
|
|
110
|
-
|
|
111
|
-
assert.deepEqual(seenOnServer(a, "INBOX"), [true]);
|
|
112
|
-
assert.equal(
|
|
113
|
-
await a.w.client.flagPush.find(a.message.messageId, SEEN),
|
|
114
|
-
null,
|
|
115
|
-
);
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
it("waits, without connecting, while the message's placement move has not settled", async () => {
|
|
119
|
-
const a = await arrange();
|
|
120
|
-
await moveToJunk(a);
|
|
121
|
-
const event = await markAsRead(a);
|
|
122
|
-
|
|
123
|
-
await pushFlag(a, event);
|
|
124
|
-
|
|
125
|
-
assert.equal(a.w.imap.connections, 0);
|
|
126
|
-
assert.equal(
|
|
127
|
-
(await a.w.client.flagPush.find(a.message.messageId, SEEN))?.state,
|
|
128
|
-
"pending",
|
|
129
|
-
);
|
|
130
|
-
assert.deepEqual(seenOnServer(a, "INBOX"), [false]);
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
it("lands on the moved message once the placement move has settled", async () => {
|
|
134
|
-
const a = await arrange();
|
|
135
|
-
const move = await moveToJunk(a);
|
|
136
|
-
const flag = await markAsRead(a);
|
|
137
|
-
await pushFlag(a, flag);
|
|
138
|
-
|
|
139
|
-
await pushMove(a, move);
|
|
140
|
-
await pushFlag(a, flag);
|
|
141
|
-
|
|
142
|
-
assert.deepEqual(a.w.imap.uids("INBOX"), []);
|
|
143
|
-
assert.deepEqual(seenOnServer(a, "Junk"), [true]);
|
|
144
|
-
assert.equal(
|
|
145
|
-
await a.w.client.flagPush.find(a.message.messageId, SEEN),
|
|
146
|
-
null,
|
|
147
|
-
);
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
// A move that never settles would otherwise park the flag forever, one
|
|
151
|
-
// queue round trip per sync tick.
|
|
152
|
-
it("drops the marker without connecting once the move has been stuck past the window", async (t) => {
|
|
153
|
-
const a = await arrange();
|
|
154
|
-
await moveToJunk(a);
|
|
155
|
-
const event = await markAsRead(a);
|
|
156
|
-
mock.timers.enable({
|
|
157
|
-
apis: ["Date"],
|
|
158
|
-
now: Date.now() + FLAG_PUSH_DEFER_MAX_MS + 1,
|
|
159
|
-
});
|
|
160
|
-
t.after(() => mock.timers.reset());
|
|
161
|
-
|
|
162
|
-
await pushFlag(a, event);
|
|
163
|
-
|
|
164
|
-
assert.equal(a.w.imap.connections, 0);
|
|
165
|
-
assert.equal(
|
|
166
|
-
await a.w.client.flagPush.find(a.message.messageId, SEEN),
|
|
167
|
-
null,
|
|
168
|
-
);
|
|
169
|
-
assert.deepEqual(seenOnServer(a, "INBOX"), [false]);
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
it("drops the marker without connecting when the mailbox was deleted", async () => {
|
|
173
|
-
const a = await arrange();
|
|
174
|
-
const event = await markAsRead(a);
|
|
175
|
-
await a.w.client.mailbox.delete(a.w.account.accountId, a.inbox.mailboxId);
|
|
176
|
-
|
|
177
|
-
await pushFlag(a, event);
|
|
178
|
-
|
|
179
|
-
assert.equal(a.w.imap.connections, 0);
|
|
180
|
-
assert.equal(
|
|
181
|
-
await a.w.client.flagPush.find(a.message.messageId, SEEN),
|
|
182
|
-
null,
|
|
183
|
-
);
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
it("drops the marker without connecting when the message row is gone", async () => {
|
|
187
|
-
const a = await arrange();
|
|
188
|
-
const event = await markAsRead(a);
|
|
189
|
-
await a.w.client.message.delete(a.message.messageId);
|
|
190
|
-
|
|
191
|
-
await pushFlag(a, event);
|
|
192
|
-
|
|
193
|
-
assert.equal(a.w.imap.connections, 0);
|
|
194
|
-
assert.equal(
|
|
195
|
-
await a.w.client.flagPush.find(a.message.messageId, SEEN),
|
|
196
|
-
null,
|
|
197
|
-
);
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
it("keeps the marker and stores nothing while the cursor is rebuilding", async () => {
|
|
201
|
-
const a = await arrange();
|
|
202
|
-
const event = await markAsRead(a);
|
|
203
|
-
await a.w.client.mailbox.update(a.w.account.accountId, a.inbox.mailboxId, {
|
|
204
|
-
cursorState: MailboxCursorState.rebuilding,
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
await pushFlag(a, event);
|
|
208
|
-
|
|
209
|
-
assert.equal(a.w.imap.connections, 0);
|
|
210
|
-
assert.ok(await a.w.client.flagPush.find(a.message.messageId, SEEN));
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
it("stores nothing when the mailbox's UIDVALIDITY changed under it", async () => {
|
|
214
|
-
const a = await arrange();
|
|
215
|
-
const event = await markAsRead(a);
|
|
216
|
-
a.w.imap.setUidValidity("INBOX", 2);
|
|
217
|
-
|
|
218
|
-
await pushFlag(a, event);
|
|
219
|
-
|
|
220
|
-
assert.deepEqual(a.w.imap.commands("STORE"), []);
|
|
221
|
-
assert.ok(await a.w.client.flagPush.find(a.message.messageId, SEEN));
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
it("keeps the marker and redelivers when the connection drops", async () => {
|
|
225
|
-
const a = await arrange();
|
|
226
|
-
const event = await markAsRead(a);
|
|
227
|
-
a.w.imap.failNext.STORE = new Error("Connection reset");
|
|
228
|
-
|
|
229
|
-
await assert.rejects(pushFlag(a, event), /Connection reset/);
|
|
230
|
-
|
|
231
|
-
assert.ok(await a.w.client.flagPush.find(a.message.messageId, SEEN));
|
|
232
|
-
});
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
describe("a placement move push", () => {
|
|
236
|
-
it("moves the message on the server, settles the row and clears the marker", async () => {
|
|
237
|
-
const a = await arrange();
|
|
238
|
-
const event = await moveToJunk(a);
|
|
239
|
-
|
|
240
|
-
await pushMove(a, event);
|
|
241
|
-
|
|
242
|
-
const [junkUid] = a.w.imap.uids("Junk", a.message.messageIdHeader);
|
|
243
|
-
const row = await a.w.row(a.message.messageId);
|
|
244
|
-
assert.equal(row?.mailboxId, a.junk.mailboxId);
|
|
245
|
-
assert.equal(row?.uid, junkUid);
|
|
246
|
-
assert.equal(row?.status, MessageStatus.active);
|
|
247
|
-
assert.equal(
|
|
248
|
-
await a.w.client.placementMove.find(a.message.messageId),
|
|
249
|
-
null,
|
|
250
|
-
);
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
it("drops the marker without connecting when a newer move took the message elsewhere", async () => {
|
|
254
|
-
const a = await arrange();
|
|
255
|
-
const event = await moveToJunk(a);
|
|
256
|
-
const archive = await a.w.folder("Archive");
|
|
257
|
-
await a.w.client.message.transitionPlacement(
|
|
258
|
-
a.message.messageId,
|
|
259
|
-
{ mailboxId: a.junk.mailboxId },
|
|
260
|
-
{ mailboxId: archive.mailboxId },
|
|
261
|
-
);
|
|
262
|
-
|
|
263
|
-
await pushMove(a, event);
|
|
264
|
-
|
|
265
|
-
assert.equal(a.w.imap.connections, 0);
|
|
266
|
-
assert.equal(
|
|
267
|
-
await a.w.client.placementMove.find(a.message.messageId),
|
|
268
|
-
null,
|
|
269
|
-
);
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
it("drops the marker without connecting when the destination mailbox was deleted", async () => {
|
|
273
|
-
const a = await arrange();
|
|
274
|
-
const event = await moveToJunk(a);
|
|
275
|
-
await a.w.client.mailbox.delete(a.w.account.accountId, a.junk.mailboxId);
|
|
276
|
-
|
|
277
|
-
await pushMove(a, event);
|
|
278
|
-
|
|
279
|
-
assert.equal(a.w.imap.connections, 0);
|
|
280
|
-
assert.equal(
|
|
281
|
-
await a.w.client.placementMove.find(a.message.messageId),
|
|
282
|
-
null,
|
|
283
|
-
);
|
|
284
|
-
});
|
|
285
|
-
|
|
286
|
-
it("keeps the marker and moves nothing while the source cursor is rebuilding", async () => {
|
|
287
|
-
const a = await arrange();
|
|
288
|
-
const event = await moveToJunk(a);
|
|
289
|
-
await a.w.client.mailbox.update(a.w.account.accountId, a.inbox.mailboxId, {
|
|
290
|
-
cursorState: MailboxCursorState.rebuilding,
|
|
291
|
-
});
|
|
292
|
-
|
|
293
|
-
await pushMove(a, event);
|
|
294
|
-
|
|
295
|
-
assert.equal(a.w.imap.connections, 0);
|
|
296
|
-
assert.ok(await a.w.client.placementMove.find(a.message.messageId));
|
|
297
|
-
assert.deepEqual(a.w.imap.uids("INBOX"), [a.message.uid]);
|
|
298
|
-
});
|
|
299
|
-
});
|
|
@@ -1,192 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { afterEach, beforeEach, describe, it, mock } from "node:test";
|
|
3
|
-
import type { MailboxItem } from "@remit/data-ports";
|
|
4
|
-
import { recordingQueue } from "@remit/data-ports/recording-queue";
|
|
5
|
-
import {
|
|
6
|
-
AccountAuthType,
|
|
7
|
-
ConnectionState,
|
|
8
|
-
MailboxCursorState,
|
|
9
|
-
} from "@remit/domain-enums";
|
|
10
|
-
import {
|
|
11
|
-
createSqliteRepositories,
|
|
12
|
-
type DrizzleMessageRepository,
|
|
13
|
-
type DrizzleThreadMessageRepository,
|
|
14
|
-
type MailboxRepo,
|
|
15
|
-
type SqliteRepositories,
|
|
16
|
-
} from "@remit/drizzle-service";
|
|
17
|
-
import { createShippedSqliteDb } from "@remit/drizzle-service/test-sqlite";
|
|
18
|
-
import {
|
|
19
|
-
type IImapConnection,
|
|
20
|
-
type ManagedConnectionFactory,
|
|
21
|
-
MessageMoveService,
|
|
22
|
-
MessageSyncService,
|
|
23
|
-
SyncWarningService,
|
|
24
|
-
} from "@remit/mailbox-service";
|
|
25
|
-
import type { ImapMessage } from "@remit/mailbox-service/types";
|
|
26
|
-
import { probeableMessageIdHeader } from "./probe-message-id.js";
|
|
27
|
-
|
|
28
|
-
const ACCOUNT_CONFIG_ID = "cfg-msgid";
|
|
29
|
-
const UID_VALIDITY = 7;
|
|
30
|
-
|
|
31
|
-
const inboxHolding = (messageIdHeader: string): ImapMessage => ({
|
|
32
|
-
uid: 3,
|
|
33
|
-
seq: 1,
|
|
34
|
-
flags: [],
|
|
35
|
-
internalDate: new Date(0),
|
|
36
|
-
size: 100,
|
|
37
|
-
envelope: {
|
|
38
|
-
date: new Date(0).toISOString(),
|
|
39
|
-
messageId: messageIdHeader,
|
|
40
|
-
subject: "Quarterly report",
|
|
41
|
-
from: [{ name: "Sender", mailbox: "sender", host: "example.com" }],
|
|
42
|
-
sender: [],
|
|
43
|
-
replyTo: [],
|
|
44
|
-
to: [],
|
|
45
|
-
cc: [],
|
|
46
|
-
bcc: [],
|
|
47
|
-
inReplyTo: "",
|
|
48
|
-
},
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
const serverWithInbox = (message: ImapMessage): IImapConnection =>
|
|
52
|
-
({
|
|
53
|
-
openBox: async (path: string) => ({
|
|
54
|
-
name: path,
|
|
55
|
-
readOnly: false,
|
|
56
|
-
uidvalidity: UID_VALIDITY,
|
|
57
|
-
uidnext: message.uid + 1,
|
|
58
|
-
flags: [],
|
|
59
|
-
permFlags: [],
|
|
60
|
-
persistentUIDs: true,
|
|
61
|
-
messages: { total: 1, new: 0 },
|
|
62
|
-
newKeywords: false,
|
|
63
|
-
}),
|
|
64
|
-
getMailboxStatus: async () => ({
|
|
65
|
-
messages: 1,
|
|
66
|
-
recent: 0,
|
|
67
|
-
unseen: 0,
|
|
68
|
-
uidNext: message.uid + 1,
|
|
69
|
-
uidValidity: UID_VALIDITY,
|
|
70
|
-
highestModseq: "0",
|
|
71
|
-
deletedCount: 0,
|
|
72
|
-
}),
|
|
73
|
-
supportsCondstore: () => false,
|
|
74
|
-
search: async () => [message.uid],
|
|
75
|
-
fetchMessages: async () => [message],
|
|
76
|
-
}) as unknown as IImapConnection;
|
|
77
|
-
|
|
78
|
-
describe("the Message-ID the move, delete and copy probes ask with", () => {
|
|
79
|
-
let close: () => void;
|
|
80
|
-
let repos: SqliteRepositories;
|
|
81
|
-
let accountId: string;
|
|
82
|
-
let mailboxes: MailboxRepo;
|
|
83
|
-
let messages: DrizzleMessageRepository;
|
|
84
|
-
let threadMessages: DrizzleThreadMessageRepository;
|
|
85
|
-
|
|
86
|
-
const createMailbox = async (fullPath: string): Promise<MailboxItem> => {
|
|
87
|
-
const created = await mailboxes.create({
|
|
88
|
-
accountId,
|
|
89
|
-
namespacePrefix: "",
|
|
90
|
-
hierarchyDelimiter: "/",
|
|
91
|
-
fullPath,
|
|
92
|
-
uidValidity: UID_VALIDITY,
|
|
93
|
-
uidNext: 1,
|
|
94
|
-
highestModseq: "0",
|
|
95
|
-
messageCount: 0,
|
|
96
|
-
unseenCount: 0,
|
|
97
|
-
deletedCount: 0,
|
|
98
|
-
totalSize: 0,
|
|
99
|
-
lastSyncUid: 0,
|
|
100
|
-
highWaterMarkUid: 0,
|
|
101
|
-
lastMessageSyncAt: 0,
|
|
102
|
-
cursorState: MailboxCursorState.normal,
|
|
103
|
-
} as Parameters<MailboxRepo["create"]>[0]);
|
|
104
|
-
if (created.outcome !== "Created") throw new Error(`${fullPath} taken`);
|
|
105
|
-
return created.mailbox;
|
|
106
|
-
};
|
|
107
|
-
|
|
108
|
-
const syncInbox = async (messageIdHeader: string): Promise<string> => {
|
|
109
|
-
const inbox = await createMailbox("INBOX");
|
|
110
|
-
const connection = serverWithInbox(inboxHolding(messageIdHeader));
|
|
111
|
-
const factory: ManagedConnectionFactory = {
|
|
112
|
-
getConnection: () => connection,
|
|
113
|
-
close: async () => {},
|
|
114
|
-
};
|
|
115
|
-
await new MessageSyncService(
|
|
116
|
-
factory,
|
|
117
|
-
mailboxes,
|
|
118
|
-
repos.mailboxSpecialUse,
|
|
119
|
-
messages,
|
|
120
|
-
repos.envelope,
|
|
121
|
-
repos.address,
|
|
122
|
-
threadMessages,
|
|
123
|
-
new SyncWarningService(repos.syncWarning),
|
|
124
|
-
).syncMessages(inbox.mailboxId, accountId, ACCOUNT_CONFIG_ID);
|
|
125
|
-
const [synced] = await messages.listAllByMailbox(inbox.mailboxId);
|
|
126
|
-
if (!synced) throw new Error("sync stored no message");
|
|
127
|
-
return synced.messageId;
|
|
128
|
-
};
|
|
129
|
-
|
|
130
|
-
const probeable = (messageId: string): Promise<string | undefined> =>
|
|
131
|
-
probeableMessageIdHeader(threadMessages, ACCOUNT_CONFIG_ID, messageId);
|
|
132
|
-
|
|
133
|
-
beforeEach(async () => {
|
|
134
|
-
const store = createShippedSqliteDb();
|
|
135
|
-
close = store.close;
|
|
136
|
-
repos = createSqliteRepositories({
|
|
137
|
-
app: store.db,
|
|
138
|
-
mail: store.db,
|
|
139
|
-
search: store.db,
|
|
140
|
-
});
|
|
141
|
-
mailboxes = repos.mailbox;
|
|
142
|
-
messages = repos.message;
|
|
143
|
-
threadMessages = repos.threadMessage;
|
|
144
|
-
const account = await repos.account.create({
|
|
145
|
-
accountConfigId: ACCOUNT_CONFIG_ID,
|
|
146
|
-
username: "person@example.com",
|
|
147
|
-
email: "person@example.com",
|
|
148
|
-
authType: AccountAuthType.Password,
|
|
149
|
-
passwordHash: '{"ciphertext":"x"}',
|
|
150
|
-
imapHost: "imap.example.com",
|
|
151
|
-
imapPort: 993,
|
|
152
|
-
imapTls: true,
|
|
153
|
-
imapStartTls: false,
|
|
154
|
-
isActive: true,
|
|
155
|
-
connectionState: ConnectionState.Authenticated,
|
|
156
|
-
});
|
|
157
|
-
accountId = account.accountId;
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
afterEach(() => {
|
|
161
|
-
mock.restoreAll();
|
|
162
|
-
close();
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
it("reads the Message-ID a real sync stored", async () => {
|
|
166
|
-
const messageId = await syncInbox("<report@example.com>");
|
|
167
|
-
|
|
168
|
-
assert.equal(await probeable(messageId), "<report@example.com>");
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
it("reads the source's Message-ID for a copy", async () => {
|
|
172
|
-
const sourceId = await syncInbox("<report@example.com>");
|
|
173
|
-
const archive = await createMailbox("Archive");
|
|
174
|
-
const copyId = await new MessageMoveService({
|
|
175
|
-
messageService: messages,
|
|
176
|
-
mailboxService: mailboxes,
|
|
177
|
-
mailboxSpecialUseService: repos.mailboxSpecialUse,
|
|
178
|
-
threadMessageService: threadMessages,
|
|
179
|
-
addressService: repos.address,
|
|
180
|
-
queue: recordingQueue().fifo,
|
|
181
|
-
}).copyMessage(ACCOUNT_CONFIG_ID, sourceId, archive.mailboxId, accountId);
|
|
182
|
-
|
|
183
|
-
assert.notEqual(copyId, sourceId);
|
|
184
|
-
assert.equal(await probeable(copyId), "<report@example.com>");
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
it("answers unprobeable for a bounce whose Message-ID is <>", async () => {
|
|
188
|
-
const messageId = await syncInbox("<>");
|
|
189
|
-
|
|
190
|
-
assert.equal(await probeable(messageId), undefined);
|
|
191
|
-
});
|
|
192
|
-
});
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import type { IThreadMessageRepository } from "@remit/data-ports";
|
|
2
|
-
import { isValidMessageId } from "@remit/data-ports/id";
|
|
3
|
-
|
|
4
|
-
export const probeableMessageIdHeader = async (
|
|
5
|
-
threadMessageService: Pick<IThreadMessageRepository, "findByMessageId">,
|
|
6
|
-
accountConfigId: string,
|
|
7
|
-
messageId: string,
|
|
8
|
-
): Promise<string | undefined> => {
|
|
9
|
-
const row = await threadMessageService.findByMessageId(
|
|
10
|
-
accountConfigId,
|
|
11
|
-
messageId,
|
|
12
|
-
);
|
|
13
|
-
const header = row?.messageIdHeader;
|
|
14
|
-
return isValidMessageId(header) ? header : undefined;
|
|
15
|
-
};
|
|
@@ -1,183 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { describe, it } from "node:test";
|
|
3
|
-
import {
|
|
4
|
-
DEFAULT_MAILBOX_FRESHNESS_MS,
|
|
5
|
-
emitSyncMessagesEvents,
|
|
6
|
-
MAILBOX_FRESHNESS_MS,
|
|
7
|
-
mailboxNeedsSync,
|
|
8
|
-
resolveMailboxFreshnessMs,
|
|
9
|
-
splitInboxFirst,
|
|
10
|
-
} from "./sync-mailboxes.js";
|
|
11
|
-
|
|
12
|
-
const NOW = 1_700_000_000_000;
|
|
13
|
-
|
|
14
|
-
const askedForByName = { explicitRequest: true } as const;
|
|
15
|
-
const sideEffect = {} as const;
|
|
16
|
-
|
|
17
|
-
describe("mailboxNeedsSync", () => {
|
|
18
|
-
// Issue #37: the gate this replaces applied to every trigger, so a refresh
|
|
19
|
-
// that landed just after a side-effect sync did nothing at all. A sync
|
|
20
|
-
// asked for by name (POST /sync) is never gated, however recently one ran.
|
|
21
|
-
it("syncs a mailbox synced a moment ago when the sync was asked for by name", () => {
|
|
22
|
-
const mailbox = { lastMessageSyncAt: NOW - 1_000 };
|
|
23
|
-
|
|
24
|
-
assert.equal(mailboxNeedsSync(mailbox, askedForByName, NOW), true);
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
// Without this, `GET /config` — which fires a trigger per account on every
|
|
28
|
-
// call — re-enumerates every folder an account owns on every page load.
|
|
29
|
-
it("skips a freshly-synced mailbox for a side-effect trigger", () => {
|
|
30
|
-
const mailbox = { lastMessageSyncAt: NOW - 1_000 };
|
|
31
|
-
|
|
32
|
-
assert.equal(mailboxNeedsSync(mailbox, sideEffect, NOW), false);
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
it("syncs a stale mailbox for a side-effect trigger", () => {
|
|
36
|
-
const mailbox = { lastMessageSyncAt: NOW - MAILBOX_FRESHNESS_MS - 1 };
|
|
37
|
-
|
|
38
|
-
assert.equal(mailboxNeedsSync(mailbox, sideEffect, NOW), true);
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it("syncs exactly at the freshness threshold", () => {
|
|
42
|
-
const mailbox = { lastMessageSyncAt: NOW - MAILBOX_FRESHNESS_MS };
|
|
43
|
-
|
|
44
|
-
assert.equal(mailboxNeedsSync(mailbox, sideEffect, NOW), true);
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it("always syncs a mailbox that has never synced", () => {
|
|
48
|
-
assert.equal(mailboxNeedsSync({}, sideEffect, NOW), true);
|
|
49
|
-
assert.equal(
|
|
50
|
-
mailboxNeedsSync({ lastMessageSyncAt: 0 }, sideEffect, NOW),
|
|
51
|
-
true,
|
|
52
|
-
);
|
|
53
|
-
});
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
describe("resolveMailboxFreshnessMs", () => {
|
|
57
|
-
it("defaults to the production window when unset", () => {
|
|
58
|
-
assert.equal(resolveMailboxFreshnessMs({}), DEFAULT_MAILBOX_FRESHNESS_MS);
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it("honors a valid override", () => {
|
|
62
|
-
assert.equal(resolveMailboxFreshnessMs({ MAILBOX_FRESHNESS_MS: "0" }), 0);
|
|
63
|
-
assert.equal(
|
|
64
|
-
resolveMailboxFreshnessMs({ MAILBOX_FRESHNESS_MS: "1000" }),
|
|
65
|
-
1000,
|
|
66
|
-
);
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it("falls back to the default for a non-numeric or negative value", () => {
|
|
70
|
-
assert.equal(
|
|
71
|
-
resolveMailboxFreshnessMs({ MAILBOX_FRESHNESS_MS: "" }),
|
|
72
|
-
DEFAULT_MAILBOX_FRESHNESS_MS,
|
|
73
|
-
);
|
|
74
|
-
assert.equal(
|
|
75
|
-
resolveMailboxFreshnessMs({ MAILBOX_FRESHNESS_MS: "abc" }),
|
|
76
|
-
DEFAULT_MAILBOX_FRESHNESS_MS,
|
|
77
|
-
);
|
|
78
|
-
assert.equal(
|
|
79
|
-
resolveMailboxFreshnessMs({ MAILBOX_FRESHNESS_MS: "-5" }),
|
|
80
|
-
DEFAULT_MAILBOX_FRESHNESS_MS,
|
|
81
|
-
);
|
|
82
|
-
assert.equal(
|
|
83
|
-
resolveMailboxFreshnessMs({ MAILBOX_FRESHNESS_MS: "1.5" }),
|
|
84
|
-
DEFAULT_MAILBOX_FRESHNESS_MS,
|
|
85
|
-
);
|
|
86
|
-
});
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
describe("splitInboxFirst", () => {
|
|
90
|
-
it("pulls INBOX out of the list whatever case it is spelled in", () => {
|
|
91
|
-
const { inbox, rest } = splitInboxFirst([
|
|
92
|
-
{ mailboxId: "mb-sent", fullPath: "Sent" },
|
|
93
|
-
{ mailboxId: "mb-inbox", fullPath: "Inbox" },
|
|
94
|
-
{ mailboxId: "mb-junk", fullPath: "Junk" },
|
|
95
|
-
]);
|
|
96
|
-
|
|
97
|
-
assert.equal(inbox?.mailboxId, "mb-inbox");
|
|
98
|
-
assert.deepEqual(
|
|
99
|
-
rest.map((mailbox) => mailbox.mailboxId),
|
|
100
|
-
["mb-sent", "mb-junk"],
|
|
101
|
-
);
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
it("leaves an account without an INBOX intact", () => {
|
|
105
|
-
const { inbox, rest } = splitInboxFirst([
|
|
106
|
-
{ mailboxId: "mb-sent", fullPath: "Sent" },
|
|
107
|
-
]);
|
|
108
|
-
|
|
109
|
-
assert.equal(inbox, undefined);
|
|
110
|
-
assert.deepEqual(
|
|
111
|
-
rest.map((mailbox) => mailbox.mailboxId),
|
|
112
|
-
["mb-sent"],
|
|
113
|
-
);
|
|
114
|
-
});
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
describe("emitSyncMessagesEvents", () => {
|
|
118
|
-
// Every event of an account shares one FIFO group, so arrival order is
|
|
119
|
-
// service order. pMap only bounds concurrency: it used to race all the
|
|
120
|
-
// emits, letting INBOX queue behind up to nineteen other folders while a
|
|
121
|
-
// person waited for their new mail.
|
|
122
|
-
it("emits INBOX before any other folder", async () => {
|
|
123
|
-
const emitted: string[] = [];
|
|
124
|
-
const mailboxes = Array.from({ length: 25 }, (_, index) => ({
|
|
125
|
-
mailboxId: `mb-${index}`,
|
|
126
|
-
fullPath: `Folder ${index}`,
|
|
127
|
-
}));
|
|
128
|
-
mailboxes.push({ mailboxId: "mb-inbox", fullPath: "INBOX" });
|
|
129
|
-
|
|
130
|
-
await emitSyncMessagesEvents("acc-1", mailboxes, async (event) => {
|
|
131
|
-
emitted.push(event.mailboxId);
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
assert.equal(emitted[0], "mb-inbox");
|
|
135
|
-
assert.equal(emitted.length, 26);
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
// Emitting INBOX and awaiting it is the whole fix: a fire-and-forget emit
|
|
139
|
-
// would leave it racing the fan-out again.
|
|
140
|
-
it("waits for the INBOX emit to resolve before emitting the rest", async () => {
|
|
141
|
-
const emitted: string[] = [];
|
|
142
|
-
let releaseInbox: () => void = () => {};
|
|
143
|
-
const inboxEmitted = new Promise<void>((resolve) => {
|
|
144
|
-
releaseInbox = resolve;
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
const done = emitSyncMessagesEvents(
|
|
148
|
-
"acc-1",
|
|
149
|
-
[
|
|
150
|
-
{ mailboxId: "mb-inbox", fullPath: "INBOX" },
|
|
151
|
-
{ mailboxId: "mb-sent", fullPath: "Sent" },
|
|
152
|
-
],
|
|
153
|
-
async (event) => {
|
|
154
|
-
emitted.push(event.mailboxId);
|
|
155
|
-
if (event.mailboxId === "mb-inbox") await inboxEmitted;
|
|
156
|
-
},
|
|
157
|
-
);
|
|
158
|
-
|
|
159
|
-
await new Promise((resolve) => setImmediate(resolve));
|
|
160
|
-
assert.deepEqual(emitted, ["mb-inbox"]);
|
|
161
|
-
|
|
162
|
-
releaseInbox();
|
|
163
|
-
await done;
|
|
164
|
-
assert.deepEqual(emitted, ["mb-inbox", "mb-sent"]);
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
it("emits every folder for an account with no INBOX", async () => {
|
|
168
|
-
const emitted: string[] = [];
|
|
169
|
-
|
|
170
|
-
await emitSyncMessagesEvents(
|
|
171
|
-
"acc-1",
|
|
172
|
-
[
|
|
173
|
-
{ mailboxId: "mb-sent", fullPath: "Sent" },
|
|
174
|
-
{ mailboxId: "mb-junk", fullPath: "Junk" },
|
|
175
|
-
],
|
|
176
|
-
async (event) => {
|
|
177
|
-
emitted.push(event.mailboxId);
|
|
178
|
-
},
|
|
179
|
-
);
|
|
180
|
-
|
|
181
|
-
assert.deepEqual(emitted.sort(), ["mb-junk", "mb-sent"]);
|
|
182
|
-
});
|
|
183
|
-
});
|