@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
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { afterEach, beforeEach, describe, it } from "node:test";
|
|
3
|
+
import type {
|
|
4
|
+
AccountItem,
|
|
5
|
+
ISyncWarningRepository,
|
|
6
|
+
MailboxItem,
|
|
7
|
+
} from "@remit/data-ports";
|
|
8
|
+
import { BodySyncService } from "./body-sync.js";
|
|
9
|
+
import { MessageSyncService } from "./message-sync.js";
|
|
10
|
+
import { SyncWarningService } from "./sync-warning.js";
|
|
11
|
+
import {
|
|
12
|
+
eml,
|
|
13
|
+
type FakeImapConnection,
|
|
14
|
+
FakeImapServer,
|
|
15
|
+
} from "./test-helpers/fake-imap.js";
|
|
16
|
+
import {
|
|
17
|
+
messageAtUid,
|
|
18
|
+
openStore,
|
|
19
|
+
type Store,
|
|
20
|
+
seedAccount,
|
|
21
|
+
seedMailbox,
|
|
22
|
+
silentLog,
|
|
23
|
+
syncFolder,
|
|
24
|
+
} from "./test-helpers/test-store.js";
|
|
25
|
+
|
|
26
|
+
let store: Store;
|
|
27
|
+
let imap: FakeImapServer;
|
|
28
|
+
let connection: FakeImapConnection;
|
|
29
|
+
let account: AccountItem;
|
|
30
|
+
let inbox: MailboxItem;
|
|
31
|
+
|
|
32
|
+
beforeEach(async () => {
|
|
33
|
+
store = await openStore();
|
|
34
|
+
imap = new FakeImapServer();
|
|
35
|
+
account = await seedAccount(store);
|
|
36
|
+
inbox = await seedMailbox(store, account, "INBOX");
|
|
37
|
+
imap.addFolder("INBOX");
|
|
38
|
+
connection = imap.connection();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
afterEach(async () => {
|
|
42
|
+
await store.close();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const syncHeaders = (
|
|
46
|
+
syncWarnings: ISyncWarningRepository = store.syncWarning,
|
|
47
|
+
) =>
|
|
48
|
+
new MessageSyncService(
|
|
49
|
+
{ getConnection: () => connection, close: async () => {} },
|
|
50
|
+
store.mailbox,
|
|
51
|
+
store.mailboxSpecialUse,
|
|
52
|
+
store.message,
|
|
53
|
+
store.envelope,
|
|
54
|
+
store.address,
|
|
55
|
+
store.threadMessage,
|
|
56
|
+
new SyncWarningService(syncWarnings),
|
|
57
|
+
silentLog,
|
|
58
|
+
store.flagPush,
|
|
59
|
+
store.messageFlag,
|
|
60
|
+
).syncMessages(inbox.mailboxId, account.accountId, account.accountConfigId);
|
|
61
|
+
|
|
62
|
+
const syncBody = (messageId: string, source: FakeImapConnection) =>
|
|
63
|
+
new BodySyncService(
|
|
64
|
+
store.message,
|
|
65
|
+
store.storage(),
|
|
66
|
+
store.threadMessage,
|
|
67
|
+
store.address,
|
|
68
|
+
store.envelope,
|
|
69
|
+
new SyncWarningService(store.syncWarning),
|
|
70
|
+
silentLog,
|
|
71
|
+
).syncBodies(
|
|
72
|
+
[messageId],
|
|
73
|
+
account.accountId,
|
|
74
|
+
account.accountConfigId,
|
|
75
|
+
{
|
|
76
|
+
mailboxId: inbox.mailboxId,
|
|
77
|
+
fullPath: "INBOX",
|
|
78
|
+
placement: "inbox",
|
|
79
|
+
uidValidity: inbox.uidValidity,
|
|
80
|
+
},
|
|
81
|
+
async () => source,
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
const warnings = async () =>
|
|
85
|
+
(await store.syncWarning.listByMailboxId(inbox.mailboxId)).map(
|
|
86
|
+
({ uid, stage, attempts }) => ({ uid, stage, attempts }),
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
const servedUidValidity = async (): Promise<number> =>
|
|
90
|
+
(await imap.connection().openBox("INBOX")).uidvalidity;
|
|
91
|
+
|
|
92
|
+
const plant = (uid: number, uidValidity: number) =>
|
|
93
|
+
store.syncWarning.recordFailures([
|
|
94
|
+
{
|
|
95
|
+
accountConfigId: account.accountConfigId,
|
|
96
|
+
accountId: account.accountId,
|
|
97
|
+
mailboxId: inbox.mailboxId,
|
|
98
|
+
uidValidity,
|
|
99
|
+
uid,
|
|
100
|
+
stage: "Header",
|
|
101
|
+
},
|
|
102
|
+
]);
|
|
103
|
+
|
|
104
|
+
const dropEnvelopes = () => {
|
|
105
|
+
const fetch = connection.fetchMessages;
|
|
106
|
+
connection.fetchMessages = async (uids) =>
|
|
107
|
+
(await fetch(uids)).map((row) => ({ ...row, envelope: undefined }));
|
|
108
|
+
return () => {
|
|
109
|
+
connection.fetchMessages = fetch;
|
|
110
|
+
};
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
describe("a message whose headers fail to sync", () => {
|
|
114
|
+
it("keeps one warning that counts every failed round", async () => {
|
|
115
|
+
const uid = imap.deliver("INBOX", eml({ "Message-ID": "<a@example.com>" }));
|
|
116
|
+
dropEnvelopes();
|
|
117
|
+
|
|
118
|
+
for (let round = 0; round < 3; round++) await syncHeaders();
|
|
119
|
+
|
|
120
|
+
assert.deepEqual(await warnings(), [{ uid, stage: "Header", attempts: 3 }]);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("drops the warning once the message syncs", async () => {
|
|
124
|
+
imap.deliver("INBOX", eml({ "Message-ID": "<a@example.com>" }));
|
|
125
|
+
const restore = dropEnvelopes();
|
|
126
|
+
await syncHeaders();
|
|
127
|
+
restore();
|
|
128
|
+
|
|
129
|
+
await syncHeaders();
|
|
130
|
+
|
|
131
|
+
assert.deepEqual(await warnings(), []);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("fails the round when the warning cannot be written", async () => {
|
|
135
|
+
imap.deliver("INBOX", eml({ "Message-ID": "<a@example.com>" }));
|
|
136
|
+
dropEnvelopes();
|
|
137
|
+
const failing: ISyncWarningRepository = {
|
|
138
|
+
...store.syncWarning,
|
|
139
|
+
recordFailures: async () => {
|
|
140
|
+
throw new Error("database is locked");
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
await assert.rejects(syncHeaders(failing), /database is locked/);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("drops a warning from an earlier UIDVALIDITY", async () => {
|
|
148
|
+
imap.deliver("INBOX", eml({ "Message-ID": "<a@example.com>" }));
|
|
149
|
+
const uidValidity = await servedUidValidity();
|
|
150
|
+
await plant(1, uidValidity + 1);
|
|
151
|
+
|
|
152
|
+
await syncHeaders();
|
|
153
|
+
|
|
154
|
+
assert.deepEqual(await warnings(), []);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("drops a warning for a uid the server no longer lists", async () => {
|
|
158
|
+
const uidValidity = await servedUidValidity();
|
|
159
|
+
await plant(40217, uidValidity);
|
|
160
|
+
|
|
161
|
+
await syncHeaders();
|
|
162
|
+
|
|
163
|
+
assert.deepEqual(await warnings(), []);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("goes with its mailbox", async () => {
|
|
167
|
+
imap.deliver("INBOX", eml({ "Message-ID": "<a@example.com>" }));
|
|
168
|
+
dropEnvelopes();
|
|
169
|
+
await syncHeaders();
|
|
170
|
+
|
|
171
|
+
await store.mailbox.delete(account.accountId, inbox.mailboxId);
|
|
172
|
+
|
|
173
|
+
assert.deepEqual(await warnings(), []);
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
describe("a message whose body fails to sync", () => {
|
|
178
|
+
it("drops the warning once a header round finds the message gone from the server", async () => {
|
|
179
|
+
const uid = imap.deliver("INBOX", eml({ "Message-ID": "<c@example.com>" }));
|
|
180
|
+
await syncFolder(store, account, inbox, imap);
|
|
181
|
+
const { messageId } = await messageAtUid(store, inbox, uid);
|
|
182
|
+
const silent = imap.connection();
|
|
183
|
+
silent.fetchMessageBodies = async function* () {
|
|
184
|
+
yield* [];
|
|
185
|
+
};
|
|
186
|
+
for (let round = 0; round < 3; round++) await syncBody(messageId, silent);
|
|
187
|
+
assert.deepEqual(await warnings(), [{ uid, stage: "Body", attempts: 3 }]);
|
|
188
|
+
|
|
189
|
+
imap.expunge("INBOX", uid);
|
|
190
|
+
await syncHeaders();
|
|
191
|
+
|
|
192
|
+
assert.deepEqual(await warnings(), []);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it("keeps one warning per failed round and drops it once the body is stored", async () => {
|
|
196
|
+
const uid = imap.deliver("INBOX", eml({ "Message-ID": "<b@example.com>" }));
|
|
197
|
+
await syncFolder(store, account, inbox, imap);
|
|
198
|
+
const { messageId } = await messageAtUid(store, inbox, uid);
|
|
199
|
+
const silent = imap.connection();
|
|
200
|
+
silent.fetchMessageBodies = async function* () {
|
|
201
|
+
yield* [];
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
for (let round = 0; round < 3; round++) await syncBody(messageId, silent);
|
|
205
|
+
|
|
206
|
+
assert.deepEqual(await warnings(), [{ uid, stage: "Body", attempts: 3 }]);
|
|
207
|
+
|
|
208
|
+
const result = await syncBody(messageId, imap.connection());
|
|
209
|
+
|
|
210
|
+
assert.deepEqual(result.failedMessageIds, []);
|
|
211
|
+
assert.deepEqual(await warnings(), []);
|
|
212
|
+
});
|
|
213
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ISyncWarningRepository,
|
|
3
|
+
SyncWarningItem,
|
|
4
|
+
SyncWarningKey,
|
|
5
|
+
} from "@remit/data-ports";
|
|
6
|
+
|
|
7
|
+
export type SyncWarningContext = Omit<SyncWarningKey, "uid" | "stage">;
|
|
8
|
+
|
|
9
|
+
export type ServerUids =
|
|
10
|
+
| { kind: "Listed"; uids: readonly number[] }
|
|
11
|
+
| { kind: "OnRequest"; list: () => Promise<readonly number[]> }
|
|
12
|
+
| { kind: "Unlisted" };
|
|
13
|
+
|
|
14
|
+
export interface SyncWarningRound {
|
|
15
|
+
failedUids: readonly number[];
|
|
16
|
+
syncedUids: readonly number[];
|
|
17
|
+
serverUids: ServerUids;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const listedUids = async (
|
|
21
|
+
serverUids: ServerUids,
|
|
22
|
+
recordedCount: number,
|
|
23
|
+
): Promise<Set<number> | undefined> => {
|
|
24
|
+
if (serverUids.kind === "Listed") return new Set(serverUids.uids);
|
|
25
|
+
if (serverUids.kind === "OnRequest" && recordedCount > 0) {
|
|
26
|
+
return new Set(await serverUids.list());
|
|
27
|
+
}
|
|
28
|
+
return undefined;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export class SyncWarningService {
|
|
32
|
+
constructor(private readonly repository: ISyncWarningRepository) {}
|
|
33
|
+
|
|
34
|
+
async settle(
|
|
35
|
+
context: SyncWarningContext,
|
|
36
|
+
stage: SyncWarningItem["stage"],
|
|
37
|
+
round: SyncWarningRound,
|
|
38
|
+
): Promise<void> {
|
|
39
|
+
await this.repository.recordFailures(
|
|
40
|
+
[...new Set(round.failedUids)].map((uid) => ({ ...context, uid, stage })),
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
const recorded = await this.repository.listByMailboxId(context.mailboxId);
|
|
44
|
+
const synced = new Set(round.syncedUids);
|
|
45
|
+
const listed = await listedUids(round.serverUids, recorded.length);
|
|
46
|
+
const cleared = recorded
|
|
47
|
+
.filter(
|
|
48
|
+
(row) =>
|
|
49
|
+
(listed !== undefined && !listed.has(row.uid)) ||
|
|
50
|
+
(row.stage === stage &&
|
|
51
|
+
(row.uidValidity !== context.uidValidity || synced.has(row.uid))),
|
|
52
|
+
)
|
|
53
|
+
.map((row) => row.syncWarningId);
|
|
54
|
+
await this.repository.deleteMany(cleared);
|
|
55
|
+
}
|
|
56
|
+
}
|