@remit/mailbox-service 0.0.111 → 0.0.112
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 +21 -6
- package/src/body-part-mapper.ts +51 -1
- package/src/body-sync-queue.test.ts +39 -99
- package/src/body-sync-terminal.test.ts +115 -277
- package/src/body-sync-terminal.ts +10 -12
- package/src/body-sync.test.ts +925 -0
- package/src/body-sync.ts +156 -235
- package/src/classification-backfill.test.ts +197 -527
- package/src/classify-message.test.ts +11 -74
- package/src/external-move.test.ts +102 -209
- package/src/external-move.ts +13 -0
- package/src/filter-operations.test.ts +319 -0
- package/src/filter-operations.ts +377 -0
- package/src/filter-update.test.ts +313 -0
- package/src/filters/filter-config.test.ts +55 -197
- package/src/filters/filter-config.ts +4 -39
- package/src/filters/match.test.ts +1 -38
- package/src/filters/match.ts +3 -52
- package/src/filters/pipeline.test.ts +158 -289
- package/src/filters/pipeline.ts +56 -170
- package/src/flag-push-terminal.ts +9 -11
- package/src/flag-push.test.ts +76 -372
- package/src/flag-queue.test.ts +197 -0
- package/src/folder-operations.test.ts +273 -0
- package/src/folder-operations.ts +217 -0
- package/src/forwarded-message.test.ts +122 -0
- package/src/imapflow-connect-retry.test.ts +76 -113
- package/src/imapflow-connection.test.ts +34 -0
- package/src/imapflow-connection.ts +5 -4
- package/src/index.ts +59 -12
- package/src/list-id-backfill.test.ts +133 -499
- package/src/mailbox-cursor.test.ts +81 -189
- package/src/mailbox-delete-intent.test.ts +78 -205
- package/src/mailbox-intent.ts +5 -3
- package/src/mailbox-management.test.ts +107 -220
- package/src/mailbox-management.ts +1 -2
- package/src/mailbox-presence.test.ts +36 -34
- package/src/mailbox-queue.ts +2 -20
- package/src/mailbox-rename-intent.test.ts +156 -325
- package/src/mailbox-rename-subtree.test.ts +205 -582
- package/src/mailbox-sync.test.ts +182 -744
- package/src/message-move.test.ts +726 -0
- package/src/message-move.ts +227 -57
- package/src/message-read.ts +87 -0
- package/src/message-save-retry.test.ts +90 -0
- package/src/message-sync-flags.test.ts +10 -1
- package/src/message-sync.test.ts +762 -0
- package/src/message-sync.ts +409 -239
- package/src/mime-walker.ts +16 -15
- package/src/organize-job.ts +93 -0
- package/src/outbox-attachment.test.ts +97 -209
- package/src/outbox-failed-recovery.test.ts +198 -269
- package/src/outbox-queue.test.ts +24 -0
- package/src/outbox-queue.ts +25 -8
- package/src/ownership.ts +140 -0
- package/src/placement-move-terminal.ts +9 -12
- package/src/placement-move.ts +33 -6
- package/src/placement-settled.ts +28 -4
- package/src/placement.test.ts +404 -0
- package/src/quarantine.test.ts +90 -148
- package/src/quarantine.ts +3 -2
- package/src/refusal.ts +29 -0
- package/src/restore-source-placement.ts +0 -4
- package/src/spam-report.test.ts +238 -710
- package/src/sync-warning.test.ts +213 -0
- package/src/sync-warning.ts +56 -0
- package/src/test-helpers/fake-imap.ts +690 -0
- package/src/test-helpers/test-queue.ts +56 -0
- package/src/test-helpers/test-store.ts +299 -0
- package/src/thread-root.test.ts +29 -0
- package/src/thread-root.ts +51 -0
- package/src/types/mailparser-augment.d.ts +8 -0
- package/src/adversarial-mime.e2e.test.ts +0 -307
- package/src/body-sync-auto-read-writeonce.test.ts +0 -219
- package/src/body-sync-calendar-suggestion.test.ts +0 -433
- package/src/body-sync-category-denormalize.test.ts +0 -352
- package/src/body-sync-category-override.test.ts +0 -501
- package/src/body-sync-category-writeonce.test.ts +0 -234
- package/src/body-sync-filter.test.ts +0 -237
- package/src/body-sync-message-gone.test.ts +0 -139
- package/src/body-sync-placement-flags.test.ts +0 -347
- package/src/body-sync-placement-writeonce.test.ts +0 -337
- package/src/body-sync-quarantine.test.ts +0 -276
- package/src/body-sync-resync.test.ts +0 -605
- package/src/body-sync-unclassifiable-once.test.ts +0 -260
- package/src/body-sync-unsubscribed-auto-read.test.ts +0 -176
- package/src/filters/anchor-drift.ts +0 -55
- package/src/filters/message-embedder.test.ts +0 -39
- package/src/filters/message-embedder.ts +0 -33
- package/src/flag-push-terminal.test.ts +0 -209
- package/src/flag-queue-stars.test.ts +0 -171
- package/src/imapflow-connection.e2e.test.ts +0 -335
- package/src/imapflow-connection.integ.test.ts +0 -316
- package/src/message-delete-junk-reconcile.test.ts +0 -117
- package/src/message-delete-no-trash.test.ts +0 -245
- package/src/message-delete-unsettled-placement.test.ts +0 -396
- package/src/message-move-copy.test.ts +0 -562
- package/src/message-move-empty-trash.test.ts +0 -326
- package/src/message-move-same-mailbox.test.ts +0 -139
- package/src/message-move-unsettled-placement.test.ts +0 -378
- package/src/message-sync-address-spoof.test.ts +0 -293
- package/src/message-sync-category.test.ts +0 -142
- package/src/message-sync-changedsince.test.ts +0 -840
- package/src/message-sync-junk-harvest.test.ts +0 -244
- package/src/message-sync-quarantine.test.ts +0 -416
- package/src/message-sync-repoint.test.ts +0 -419
- package/src/outbox-queue-settled-row.test.ts +0 -140
- package/src/pass-through-unit-of-work.ts +0 -17
- package/src/placement-move-terminal.test.ts +0 -300
- package/src/placement-move-unsettled.test.ts +0 -275
- package/src/placement-transition-race.test.ts +0 -482
- package/src/stale-message-reconcile.test.ts +0 -82
- package/src/stale-message-reconcile.ts +0 -55
- package/src/test-helpers/isolated-mailbox.ts +0 -44
- package/src/test-helpers/mailfuzz-connection.ts +0 -27
- package/test/fixtures/mime-adversarial-e2e/01-octet-stream-pdf.eml +0 -21
- package/test/fixtures/mime-adversarial-e2e/02-nested-related-cid-image.eml +0 -31
- package/test/fixtures/mime-adversarial-e2e/03-calendar-invite.eml +0 -33
- package/test/fixtures/mime-adversarial-e2e/04-forwarded-rfc822-with-attachments.eml +0 -42
- package/test/fixtures/mime-adversarial-e2e/05-tnef-winmail.eml +0 -25
- package/test/fixtures/mime-adversarial-e2e/06-8bit-non-ascii.eml +0 -15
- package/test/fixtures/mime-adversarial-e2e/07-quoted-printable-soft-breaks.eml +0 -17
- package/test/fixtures/mime-adversarial-e2e/08-alternative-empty-text.eml +0 -18
|
@@ -1,60 +1,31 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ListId is only written at body-sync time, going forward (issue #263). A row
|
|
3
|
-
* synced before header extraction shipped keeps `listId` unset forever, so a
|
|
4
|
-
* `ListId` filter clause silently under-matches the back catalogue.
|
|
5
|
-
*
|
|
6
|
-
* These tests pin the one-time, resumable pass that closes that gap: it reads
|
|
7
|
-
* each candidate's already-stored raw source (never IMAP), extracts
|
|
8
|
-
* `List-Id`, and writes only that field.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
1
|
import assert from "node:assert/strict";
|
|
12
|
-
import { describe, it } from "node:test";
|
|
13
|
-
import type {
|
|
14
|
-
AccountConfigItem,
|
|
15
|
-
IAccountConfigRepository,
|
|
16
|
-
IMessageRepository,
|
|
17
|
-
IThreadMessageRepository,
|
|
18
|
-
MessageItem,
|
|
19
|
-
ResultList,
|
|
20
|
-
ThreadMessageItem,
|
|
21
|
-
UpdateThreadMessageInput,
|
|
22
|
-
} from "@remit/data-ports";
|
|
2
|
+
import { afterEach, beforeEach, describe, it } from "node:test";
|
|
3
|
+
import type { AccountItem, MailboxItem } from "@remit/data-ports";
|
|
23
4
|
import type { StorageService } from "@remit/storage-service";
|
|
24
5
|
import {
|
|
25
6
|
backfillListIds,
|
|
26
7
|
type ListIdBackfillCheckpoint,
|
|
27
|
-
type ListIdBackfillCheckpointStore,
|
|
28
|
-
type ListIdBackfillProgress,
|
|
29
8
|
} from "./list-id-backfill.js";
|
|
9
|
+
import { eml, FakeImapServer } from "./test-helpers/fake-imap.js";
|
|
10
|
+
import {
|
|
11
|
+
arriveAt,
|
|
12
|
+
openStore,
|
|
13
|
+
type Store,
|
|
14
|
+
seedAccount,
|
|
15
|
+
seedMailbox,
|
|
16
|
+
storeBody,
|
|
17
|
+
} from "./test-helpers/test-store.js";
|
|
18
|
+
|
|
19
|
+
const LIST = eml({
|
|
20
|
+
From: "Weekly News <news@example.com>",
|
|
21
|
+
"List-Id": "Weekly News <weekly.news.example.com>",
|
|
22
|
+
});
|
|
30
23
|
|
|
31
|
-
const
|
|
32
|
-
[
|
|
33
|
-
"From: Weekly News <news@example.com>",
|
|
34
|
-
"To: me@example.com",
|
|
35
|
-
"Subject: This week",
|
|
36
|
-
"List-Id: Weekly News <weekly.news.example.com>",
|
|
37
|
-
"Content-Type: text/plain",
|
|
38
|
-
"",
|
|
39
|
-
"news",
|
|
40
|
-
].join("\r\n"),
|
|
41
|
-
);
|
|
42
|
-
|
|
43
|
-
const PLAIN_EML = Buffer.from(
|
|
44
|
-
[
|
|
45
|
-
"From: Alice <alice@example.com>",
|
|
46
|
-
"To: me@example.com",
|
|
47
|
-
"Subject: Hi",
|
|
48
|
-
"Content-Type: text/plain",
|
|
49
|
-
"",
|
|
50
|
-
"hi",
|
|
51
|
-
].join("\r\n"),
|
|
52
|
-
);
|
|
24
|
+
const PLAIN = eml({ From: "Alice <alice@example.com>" });
|
|
53
25
|
|
|
54
|
-
const
|
|
26
|
+
const MALFORMED = Buffer.from(
|
|
55
27
|
[
|
|
56
28
|
"From: Weird <weird@example.com>",
|
|
57
|
-
"To: me@example.com",
|
|
58
29
|
"Subject: Odd header",
|
|
59
30
|
"List-Id:",
|
|
60
31
|
"Content-Type: text/plain",
|
|
@@ -63,511 +34,174 @@ const MALFORMED_LIST_ID_EML = Buffer.from(
|
|
|
63
34
|
].join("\r\n"),
|
|
64
35
|
);
|
|
65
36
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
hasStars: false,
|
|
81
|
-
isDeleted: false,
|
|
82
|
-
category: "uncategorized",
|
|
83
|
-
createdAt: 1,
|
|
84
|
-
updatedAt: 1,
|
|
85
|
-
...overrides,
|
|
86
|
-
}) as unknown as ThreadMessageItem;
|
|
87
|
-
|
|
88
|
-
const message = (overrides: Partial<MessageItem>): MessageItem =>
|
|
89
|
-
({
|
|
90
|
-
messageId: "m-1",
|
|
91
|
-
mailboxId: "mb-1",
|
|
92
|
-
uid: 1,
|
|
93
|
-
status: "active",
|
|
94
|
-
syncStatus: "synced",
|
|
95
|
-
category: "uncategorized",
|
|
96
|
-
hasListUnsubscribe: false,
|
|
97
|
-
movedByRemit: false,
|
|
98
|
-
createdAt: 1,
|
|
99
|
-
updatedAt: 1,
|
|
100
|
-
...overrides,
|
|
101
|
-
}) as unknown as MessageItem;
|
|
37
|
+
let store: Store;
|
|
38
|
+
let storage: StorageService;
|
|
39
|
+
let imap: FakeImapServer;
|
|
40
|
+
let account: AccountItem;
|
|
41
|
+
let inbox: MailboxItem;
|
|
42
|
+
|
|
43
|
+
beforeEach(async () => {
|
|
44
|
+
store = await openStore();
|
|
45
|
+
storage = store.storage();
|
|
46
|
+
imap = new FakeImapServer();
|
|
47
|
+
imap.addFolder("INBOX");
|
|
48
|
+
account = await seedAccount(store);
|
|
49
|
+
inbox = await seedMailbox(store, account, "INBOX");
|
|
50
|
+
});
|
|
102
51
|
|
|
103
|
-
|
|
104
|
-
({ accountConfigId }) as unknown as AccountConfigItem;
|
|
52
|
+
afterEach(() => store.close());
|
|
105
53
|
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
checkpoint = undefined;
|
|
115
|
-
},
|
|
116
|
-
};
|
|
54
|
+
const stored = async (
|
|
55
|
+
raw: Buffer,
|
|
56
|
+
owner: AccountItem = account,
|
|
57
|
+
mailbox: MailboxItem = inbox,
|
|
58
|
+
): Promise<string> => {
|
|
59
|
+
const messageId = await arriveAt(store, imap, owner, mailbox, raw);
|
|
60
|
+
await storeBody(store, storage, owner, messageId, raw);
|
|
61
|
+
return messageId;
|
|
117
62
|
};
|
|
118
63
|
|
|
119
|
-
|
|
120
|
-
accountConfigService:
|
|
121
|
-
threadMessageService:
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
messageService: Pick<IMessageRepository, "get">;
|
|
126
|
-
storageService: Pick<StorageService, "retrieve">;
|
|
127
|
-
updates: Array<{ threadMessageId: string; input: UpdateThreadMessageInput }>;
|
|
128
|
-
retrieved: string[];
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
const buildHarness = (options: {
|
|
132
|
-
accounts?: AccountConfigItem[];
|
|
133
|
-
rows: ThreadMessageItem[];
|
|
134
|
-
messages: MessageItem[];
|
|
135
|
-
retrieve?: (key: string) => Promise<Buffer>;
|
|
136
|
-
pageSize?: number;
|
|
137
|
-
}): Harness => {
|
|
138
|
-
const accounts = options.accounts ?? [
|
|
139
|
-
{ accountConfigId: "acc-1" } as unknown as AccountConfigItem,
|
|
140
|
-
];
|
|
141
|
-
const messagesById = new Map(options.messages.map((m) => [m.messageId, m]));
|
|
142
|
-
const updates: Array<{
|
|
143
|
-
threadMessageId: string;
|
|
144
|
-
input: UpdateThreadMessageInput;
|
|
145
|
-
}> = [];
|
|
146
|
-
const retrieved: string[] = [];
|
|
147
|
-
const pageSize = options.pageSize ?? 200;
|
|
64
|
+
const deps = () => ({
|
|
65
|
+
accountConfigService: store.accountConfig,
|
|
66
|
+
threadMessageService: store.threadMessage,
|
|
67
|
+
messageService: store.message,
|
|
68
|
+
storageService: storage,
|
|
69
|
+
});
|
|
148
70
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
71
|
+
const listIdOf = async (messageId: string, owner: AccountItem = account) =>
|
|
72
|
+
(
|
|
73
|
+
await store.threadMessage.findAllByMessageId(
|
|
74
|
+
owner.accountConfigId,
|
|
75
|
+
messageId,
|
|
76
|
+
)
|
|
77
|
+
)[0].listId;
|
|
152
78
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
opts?: { limit?: number; continuationToken?: string },
|
|
160
|
-
): Promise<ResultList<ThreadMessageItem>> => {
|
|
161
|
-
const scoped = options.rows.filter(
|
|
162
|
-
(r) => r.accountConfigId === accountConfigId,
|
|
163
|
-
);
|
|
164
|
-
const start = opts?.continuationToken
|
|
165
|
-
? Number(opts.continuationToken)
|
|
166
|
-
: 0;
|
|
167
|
-
const limit = opts?.limit ?? pageSize;
|
|
168
|
-
const page = scoped.slice(start, start + limit);
|
|
169
|
-
const nextStart = start + page.length;
|
|
170
|
-
return {
|
|
171
|
-
items: page,
|
|
172
|
-
continuationToken:
|
|
173
|
-
nextStart < scoped.length ? String(nextStart) : undefined,
|
|
174
|
-
};
|
|
175
|
-
},
|
|
176
|
-
update: async (
|
|
177
|
-
_accountConfigId: string,
|
|
178
|
-
threadMessageId: string,
|
|
179
|
-
input: UpdateThreadMessageInput,
|
|
180
|
-
) => {
|
|
181
|
-
updates.push({ threadMessageId, input });
|
|
182
|
-
return row({ threadMessageId, ...input });
|
|
79
|
+
const memoryCheckpoint = () => {
|
|
80
|
+
let saved: ListIdBackfillCheckpoint | undefined;
|
|
81
|
+
return {
|
|
82
|
+
load: async () => saved,
|
|
83
|
+
save: async (next: ListIdBackfillCheckpoint) => {
|
|
84
|
+
saved = next;
|
|
183
85
|
},
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
const messageService: Pick<IMessageRepository, "get"> = {
|
|
187
|
-
get: (async (messageIds: string | string[]) => {
|
|
188
|
-
if (Array.isArray(messageIds)) {
|
|
189
|
-
return messageIds
|
|
190
|
-
.map((id) => messagesById.get(id))
|
|
191
|
-
.filter((m): m is MessageItem => m !== undefined);
|
|
192
|
-
}
|
|
193
|
-
const found = messagesById.get(messageIds);
|
|
194
|
-
if (!found) throw new Error(`no fixture for ${messageIds}`);
|
|
195
|
-
return found;
|
|
196
|
-
}) as IMessageRepository["get"],
|
|
197
|
-
};
|
|
198
|
-
|
|
199
|
-
const storageService: Pick<StorageService, "retrieve"> = {
|
|
200
|
-
retrieve: async (key: string) => {
|
|
201
|
-
retrieved.push(key);
|
|
202
|
-
return options.retrieve ? options.retrieve(key) : LIST_EML;
|
|
86
|
+
clear: async () => {
|
|
87
|
+
saved = undefined;
|
|
203
88
|
},
|
|
204
89
|
};
|
|
205
|
-
|
|
206
|
-
return {
|
|
207
|
-
accountConfigService,
|
|
208
|
-
threadMessageService,
|
|
209
|
-
messageService,
|
|
210
|
-
storageService,
|
|
211
|
-
updates,
|
|
212
|
-
retrieved,
|
|
213
|
-
};
|
|
214
90
|
};
|
|
215
91
|
|
|
216
92
|
describe("backfillListIds", () => {
|
|
217
|
-
it("writes the
|
|
218
|
-
const
|
|
219
|
-
rows: [row({ threadMessageId: "tm-1", messageId: "m-1" })],
|
|
220
|
-
messages: [message({ messageId: "m-1", bodyStorageKey: "s3://m-1" })],
|
|
221
|
-
});
|
|
93
|
+
it("writes the List-Id a stored body carries", async () => {
|
|
94
|
+
const messageId = await stored(LIST);
|
|
222
95
|
|
|
223
|
-
const result = await backfillListIds(
|
|
96
|
+
const result = await backfillListIds(deps());
|
|
224
97
|
|
|
225
98
|
assert.equal(result.backfilled, 1);
|
|
226
|
-
assert.equal(
|
|
227
|
-
assert.equal(harness.updates.length, 1);
|
|
228
|
-
assert.equal(harness.updates[0].input.listId, "weekly.news.example.com");
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
it("leaves listId empty, without error, when the message has no List-Id header", async () => {
|
|
232
|
-
const harness = buildHarness({
|
|
233
|
-
rows: [row({ threadMessageId: "tm-1", messageId: "m-1" })],
|
|
234
|
-
messages: [message({ messageId: "m-1", bodyStorageKey: "s3://m-1" })],
|
|
235
|
-
retrieve: async () => PLAIN_EML,
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
const result = await backfillListIds(harness);
|
|
239
|
-
|
|
240
|
-
assert.equal(result.noListId, 1);
|
|
241
|
-
assert.equal(result.failed, 0);
|
|
242
|
-
assert.deepEqual(harness.updates, []);
|
|
99
|
+
assert.equal(await listIdOf(messageId), "weekly.news.example.com");
|
|
243
100
|
});
|
|
244
101
|
|
|
245
|
-
it("
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
messages: [message({ messageId: "m-1", bodyStorageKey: "s3://m-1" })],
|
|
249
|
-
retrieve: async () => MALFORMED_LIST_ID_EML,
|
|
250
|
-
});
|
|
102
|
+
it("leaves the field empty for mail with no or a malformed List-Id", async () => {
|
|
103
|
+
const plain = await stored(PLAIN);
|
|
104
|
+
const malformed = await stored(MALFORMED);
|
|
251
105
|
|
|
252
|
-
const result = await backfillListIds(
|
|
106
|
+
const result = await backfillListIds(deps());
|
|
253
107
|
|
|
108
|
+
assert.equal(result.noListId, 2);
|
|
254
109
|
assert.equal(result.failed, 0);
|
|
255
|
-
assert.equal(
|
|
256
|
-
assert.
|
|
110
|
+
assert.equal(await listIdOf(plain), undefined);
|
|
111
|
+
assert.equal(await listIdOf(malformed), undefined);
|
|
257
112
|
});
|
|
258
113
|
|
|
259
|
-
it("skips
|
|
260
|
-
const
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
114
|
+
it("skips rows already carrying a List-Id and rows with no stored body", async () => {
|
|
115
|
+
const set = await stored(LIST);
|
|
116
|
+
const [row] = await store.threadMessage.findAllByMessageId(
|
|
117
|
+
account.accountConfigId,
|
|
118
|
+
set,
|
|
119
|
+
);
|
|
120
|
+
await store.threadMessage.update(
|
|
121
|
+
account.accountConfigId,
|
|
122
|
+
row.threadMessageId,
|
|
123
|
+
{ listId: "already.set" },
|
|
124
|
+
);
|
|
125
|
+
await arriveAt(store, imap, account, inbox, LIST);
|
|
270
126
|
|
|
271
|
-
const result = await backfillListIds(
|
|
127
|
+
const result = await backfillListIds(deps());
|
|
272
128
|
|
|
273
129
|
assert.equal(result.alreadySet, 1);
|
|
274
|
-
assert.equal(result.backfilled, 0);
|
|
275
|
-
assert.deepEqual(harness.retrieved, []);
|
|
276
|
-
assert.deepEqual(harness.updates, []);
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
it("skips a candidate whose body was never synced, without reading storage", async () => {
|
|
280
|
-
const harness = buildHarness({
|
|
281
|
-
rows: [row({ threadMessageId: "tm-1", messageId: "m-1" })],
|
|
282
|
-
messages: [message({ messageId: "m-1", bodyStorageKey: undefined })],
|
|
283
|
-
});
|
|
284
|
-
|
|
285
|
-
const result = await backfillListIds(harness);
|
|
286
|
-
|
|
287
130
|
assert.equal(result.skippedNoBody, 1);
|
|
288
|
-
assert.
|
|
289
|
-
assert.deepEqual(harness.updates, []);
|
|
131
|
+
assert.equal(await listIdOf(set), "already.set");
|
|
290
132
|
});
|
|
291
133
|
|
|
292
134
|
it("contains a storage failure to the one message and keeps going", async () => {
|
|
293
|
-
const
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
row({ threadMessageId: "tm-good", messageId: "m-good" }),
|
|
297
|
-
],
|
|
298
|
-
messages: [
|
|
299
|
-
message({ messageId: "m-bad", bodyStorageKey: "s3://m-bad" }),
|
|
300
|
-
message({ messageId: "m-good", bodyStorageKey: "s3://m-good" }),
|
|
301
|
-
],
|
|
302
|
-
retrieve: async (key) => {
|
|
303
|
-
if (key === "s3://m-bad") throw new Error("AccessDenied");
|
|
304
|
-
return LIST_EML;
|
|
305
|
-
},
|
|
135
|
+
const lost = await arriveAt(store, imap, account, inbox, LIST);
|
|
136
|
+
await store.message.update(lost, {
|
|
137
|
+
bodyStorageKey: "file:///nonexistent/remit/body.eml",
|
|
306
138
|
});
|
|
139
|
+
const fine = await stored(LIST);
|
|
307
140
|
|
|
308
|
-
const result = await backfillListIds(
|
|
141
|
+
const result = await backfillListIds(deps());
|
|
309
142
|
|
|
310
143
|
assert.equal(result.failed, 1);
|
|
311
|
-
assert.
|
|
312
|
-
assert.equal(result.backfilled, 1);
|
|
313
|
-
assert.equal(harness.updates.length, 1);
|
|
314
|
-
assert.equal(harness.updates[0].threadMessageId, "tm-good");
|
|
315
|
-
});
|
|
316
|
-
|
|
317
|
-
it("reports progress as pages are processed", async () => {
|
|
318
|
-
const rows = Array.from({ length: 3 }, (_, i) =>
|
|
319
|
-
row({ threadMessageId: `tm-${i}`, messageId: `m-${i}` }),
|
|
320
|
-
);
|
|
321
|
-
const messages = rows.map((r) =>
|
|
322
|
-
message({
|
|
323
|
-
messageId: r.messageId,
|
|
324
|
-
bodyStorageKey: `s3://${r.messageId}`,
|
|
325
|
-
}),
|
|
326
|
-
);
|
|
327
|
-
const harness = buildHarness({ rows, messages, pageSize: 2 });
|
|
328
|
-
const progress: ListIdBackfillProgress[] = [];
|
|
329
|
-
|
|
330
|
-
const result = await backfillListIds(harness, {
|
|
331
|
-
batchSize: 2,
|
|
332
|
-
onProgress: (p) => progress.push({ ...p }),
|
|
333
|
-
});
|
|
334
|
-
|
|
335
|
-
assert.equal(result.backfilled, 3);
|
|
336
|
-
assert.equal(progress.length, 2);
|
|
337
|
-
assert.equal(progress[0].scanned, 2);
|
|
338
|
-
assert.equal(progress[1].scanned, 3);
|
|
144
|
+
assert.equal(await listIdOf(fine), "weekly.news.example.com");
|
|
339
145
|
});
|
|
340
146
|
|
|
341
|
-
it("
|
|
342
|
-
const
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
],
|
|
347
|
-
rows: [
|
|
348
|
-
row({
|
|
349
|
-
threadMessageId: "tm-1",
|
|
350
|
-
messageId: "m-1",
|
|
351
|
-
accountConfigId: "acc-1",
|
|
352
|
-
}),
|
|
353
|
-
row({
|
|
354
|
-
threadMessageId: "tm-2",
|
|
355
|
-
messageId: "m-2",
|
|
356
|
-
accountConfigId: "acc-2",
|
|
357
|
-
}),
|
|
358
|
-
],
|
|
359
|
-
messages: [
|
|
360
|
-
message({ messageId: "m-1", bodyStorageKey: "s3://m-1" }),
|
|
361
|
-
message({ messageId: "m-2", bodyStorageKey: "s3://m-2" }),
|
|
362
|
-
],
|
|
363
|
-
});
|
|
364
|
-
|
|
365
|
-
const result = await backfillListIds(harness);
|
|
366
|
-
|
|
367
|
-
assert.equal(result.backfilled, 2);
|
|
368
|
-
assert.equal(harness.updates.length, 2);
|
|
369
|
-
});
|
|
370
|
-
|
|
371
|
-
it("scans accounts in account-id order, whatever order listAll returns", async () => {
|
|
372
|
-
const accountIds = ["acc-1", "acc-2", "acc-3"];
|
|
373
|
-
const rows = accountIds.map((accountConfigId) =>
|
|
374
|
-
row({
|
|
375
|
-
threadMessageId: `tm-${accountConfigId}`,
|
|
376
|
-
messageId: `m-${accountConfigId}`,
|
|
377
|
-
accountConfigId,
|
|
378
|
-
}),
|
|
379
|
-
);
|
|
380
|
-
const harness = buildHarness({
|
|
381
|
-
accounts: ["acc-3", "acc-1", "acc-2"].map(asAccount),
|
|
382
|
-
rows,
|
|
383
|
-
messages: rows.map((r) =>
|
|
384
|
-
message({
|
|
385
|
-
messageId: r.messageId,
|
|
386
|
-
bodyStorageKey: `s3://${r.messageId}`,
|
|
387
|
-
}),
|
|
388
|
-
),
|
|
389
|
-
});
|
|
390
|
-
const progress: ListIdBackfillProgress[] = [];
|
|
391
|
-
|
|
392
|
-
await backfillListIds(harness, {
|
|
393
|
-
onProgress: (p) => progress.push({ ...p }),
|
|
394
|
-
});
|
|
395
|
-
|
|
396
|
-
assert.deepEqual(
|
|
397
|
-
progress.map((p) => p.accountConfigId),
|
|
398
|
-
accountIds,
|
|
399
|
-
);
|
|
400
|
-
});
|
|
401
|
-
|
|
402
|
-
it("checkpoints after each page and clears it on completion", async () => {
|
|
403
|
-
const rows = Array.from({ length: 3 }, (_, i) =>
|
|
404
|
-
row({ threadMessageId: `tm-${i}`, messageId: `m-${i}` }),
|
|
405
|
-
);
|
|
406
|
-
const messages = rows.map((r) =>
|
|
407
|
-
message({
|
|
408
|
-
messageId: r.messageId,
|
|
409
|
-
bodyStorageKey: `s3://${r.messageId}`,
|
|
410
|
-
}),
|
|
411
|
-
);
|
|
412
|
-
const harness = buildHarness({ rows, messages, pageSize: 2 });
|
|
413
|
-
|
|
414
|
-
const saved: ListIdBackfillCheckpoint[] = [];
|
|
415
|
-
let cleared = false;
|
|
416
|
-
|
|
417
|
-
await backfillListIds(harness, {
|
|
418
|
-
batchSize: 2,
|
|
419
|
-
checkpointStore: {
|
|
420
|
-
load: async () => undefined,
|
|
421
|
-
save: async (checkpoint) => {
|
|
422
|
-
saved.push(checkpoint);
|
|
423
|
-
},
|
|
424
|
-
clear: async () => {
|
|
425
|
-
cleared = true;
|
|
426
|
-
},
|
|
427
|
-
},
|
|
428
|
-
});
|
|
429
|
-
|
|
430
|
-
assert.equal(saved.length, 2);
|
|
431
|
-
assert.equal(saved[0].accountConfigId, "acc-1");
|
|
432
|
-
assert.equal(saved[0].continuationToken, "2");
|
|
433
|
-
assert.equal(saved[1].continuationToken, undefined);
|
|
434
|
-
assert.equal(cleared, true);
|
|
435
|
-
});
|
|
436
|
-
|
|
437
|
-
it("resumes from a saved checkpoint instead of rescanning from the start", async () => {
|
|
438
|
-
const rows = Array.from({ length: 3 }, (_, i) =>
|
|
439
|
-
row({ threadMessageId: `tm-${i}`, messageId: `m-${i}` }),
|
|
440
|
-
);
|
|
441
|
-
const messages = rows.map((r) =>
|
|
442
|
-
message({
|
|
443
|
-
messageId: r.messageId,
|
|
444
|
-
bodyStorageKey: `s3://${r.messageId}`,
|
|
445
|
-
}),
|
|
446
|
-
);
|
|
447
|
-
const harness = buildHarness({ rows, messages, pageSize: 2 });
|
|
448
|
-
|
|
449
|
-
const result = await backfillListIds(harness, {
|
|
450
|
-
batchSize: 2,
|
|
451
|
-
checkpointStore: {
|
|
452
|
-
load: async () => ({
|
|
453
|
-
accountConfigId: "acc-1",
|
|
454
|
-
continuationToken: "2",
|
|
455
|
-
}),
|
|
456
|
-
save: async () => {},
|
|
457
|
-
clear: async () => {},
|
|
458
|
-
},
|
|
459
|
-
});
|
|
460
|
-
|
|
461
|
-
assert.equal(result.scanned, 1);
|
|
462
|
-
assert.equal(result.backfilled, 1);
|
|
463
|
-
assert.deepEqual(
|
|
464
|
-
harness.updates.map((u) => u.threadMessageId),
|
|
465
|
-
["tm-2"],
|
|
466
|
-
);
|
|
467
|
-
});
|
|
468
|
-
|
|
469
|
-
it("resumes the account it was working through after an earlier account is removed", async () => {
|
|
470
|
-
const accountIds = ["acc-1", "acc-2", "acc-3"];
|
|
471
|
-
const rows = [
|
|
472
|
-
row({
|
|
473
|
-
threadMessageId: "tm-1",
|
|
474
|
-
messageId: "m-1",
|
|
475
|
-
accountConfigId: "acc-1",
|
|
476
|
-
}),
|
|
477
|
-
row({
|
|
478
|
-
threadMessageId: "tm-2a",
|
|
479
|
-
messageId: "m-2a",
|
|
480
|
-
accountConfigId: "acc-2",
|
|
481
|
-
}),
|
|
482
|
-
row({
|
|
483
|
-
threadMessageId: "tm-2b",
|
|
484
|
-
messageId: "m-2b",
|
|
485
|
-
accountConfigId: "acc-2",
|
|
486
|
-
}),
|
|
487
|
-
row({
|
|
488
|
-
threadMessageId: "tm-3",
|
|
489
|
-
messageId: "m-3",
|
|
490
|
-
accountConfigId: "acc-3",
|
|
491
|
-
}),
|
|
147
|
+
it("resumes the account it was in after an earlier account is removed", async () => {
|
|
148
|
+
const owners = [
|
|
149
|
+
await seedAccount(store, { accountConfigId: "cfg-1" }),
|
|
150
|
+
await seedAccount(store, { accountConfigId: "cfg-2" }),
|
|
151
|
+
await seedAccount(store, { accountConfigId: "cfg-3" }),
|
|
492
152
|
];
|
|
493
|
-
const
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
const
|
|
509
|
-
|
|
510
|
-
accountConfigId,
|
|
511
|
-
opts,
|
|
512
|
-
) => {
|
|
513
|
-
if (accountConfigId === "acc-2" && opts?.continuationToken) {
|
|
514
|
-
throw new Error("interrupted");
|
|
515
|
-
}
|
|
516
|
-
return listByAccount(accountConfigId, opts);
|
|
517
|
-
};
|
|
153
|
+
const ids = new Map<string, string[]>();
|
|
154
|
+
for (const owner of owners) {
|
|
155
|
+
imap.addFolder(`INBOX-${owner.accountConfigId}`);
|
|
156
|
+
const mailbox = await seedMailbox(
|
|
157
|
+
store,
|
|
158
|
+
owner,
|
|
159
|
+
`INBOX-${owner.accountConfigId}`,
|
|
160
|
+
);
|
|
161
|
+
const count = owner.accountConfigId === "cfg-2" ? 2 : 1;
|
|
162
|
+
const mine: string[] = [];
|
|
163
|
+
for (let i = 0; i < count; i++)
|
|
164
|
+
mine.push(await stored(LIST, owner, mailbox));
|
|
165
|
+
ids.set(owner.accountConfigId, mine);
|
|
166
|
+
}
|
|
167
|
+
await store.accountConfig.delete(account.accountConfigId);
|
|
168
|
+
const checkpointStore = memoryCheckpoint();
|
|
169
|
+
let pages = 0;
|
|
518
170
|
|
|
519
171
|
await assert.rejects(
|
|
520
|
-
backfillListIds(
|
|
521
|
-
|
|
172
|
+
backfillListIds(deps(), {
|
|
173
|
+
batchSize: 1,
|
|
174
|
+
checkpointStore,
|
|
175
|
+
onProgress: (progress) => {
|
|
176
|
+
if (progress.accountConfigId === "cfg-2" && ++pages === 1) {
|
|
177
|
+
throw new Error("killed");
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
}),
|
|
522
181
|
);
|
|
523
|
-
|
|
524
|
-
const resumed =
|
|
525
|
-
accounts: ["acc-2", "acc-3"].map(asAccount),
|
|
526
|
-
rows,
|
|
527
|
-
messages,
|
|
528
|
-
pageSize: 1,
|
|
529
|
-
});
|
|
530
|
-
|
|
531
|
-
const result = await backfillListIds(resumed, {
|
|
182
|
+
await store.accountConfig.delete("cfg-1");
|
|
183
|
+
const resumed = await backfillListIds(deps(), {
|
|
532
184
|
batchSize: 1,
|
|
533
|
-
checkpointStore
|
|
185
|
+
checkpointStore,
|
|
534
186
|
});
|
|
535
187
|
|
|
536
|
-
assert.
|
|
537
|
-
|
|
538
|
-
["tm-2b", "tm-3"],
|
|
539
|
-
);
|
|
540
|
-
assert.equal(result.backfilled, 2);
|
|
188
|
+
assert.equal(resumed.backfilled, 2);
|
|
189
|
+
assert.equal(resumed.scanned, 2);
|
|
541
190
|
});
|
|
542
191
|
|
|
543
192
|
it("restarts the pass when the checkpointed account no longer exists", async () => {
|
|
544
|
-
|
|
545
|
-
row({ threadMessageId: `tm-${i}`, messageId: `m-${i}` }),
|
|
546
|
-
);
|
|
547
|
-
const messages = rows.map((r) =>
|
|
548
|
-
message({
|
|
549
|
-
messageId: r.messageId,
|
|
550
|
-
bodyStorageKey: `s3://${r.messageId}`,
|
|
551
|
-
}),
|
|
552
|
-
);
|
|
553
|
-
const harness = buildHarness({ rows, messages, pageSize: 2 });
|
|
193
|
+
for (let i = 0; i < 3; i++) await stored(LIST);
|
|
554
194
|
|
|
555
|
-
const result = await backfillListIds(
|
|
195
|
+
const result = await backfillListIds(deps(), {
|
|
556
196
|
batchSize: 2,
|
|
557
197
|
checkpointStore: {
|
|
558
|
-
load: async () => ({
|
|
559
|
-
accountConfigId: "acc-removed",
|
|
560
|
-
continuationToken: "2",
|
|
561
|
-
}),
|
|
198
|
+
load: async () => ({ accountConfigId: "cfg-removed" }),
|
|
562
199
|
save: async () => {},
|
|
563
200
|
clear: async () => {},
|
|
564
201
|
},
|
|
565
202
|
});
|
|
566
203
|
|
|
567
204
|
assert.equal(result.scanned, 3);
|
|
568
|
-
assert.
|
|
569
|
-
harness.updates.map((u) => u.threadMessageId),
|
|
570
|
-
["tm-0", "tm-1", "tm-2"],
|
|
571
|
-
);
|
|
205
|
+
assert.equal(result.backfilled, 3);
|
|
572
206
|
});
|
|
573
207
|
});
|