@remit/mailbox-service 0.0.111 → 0.0.113
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-credentials.d.ts +83 -0
- package/dist/account-credentials.js +104 -0
- package/dist/attribute-mapper.d.ts +36 -0
- package/dist/attribute-mapper.js +119 -0
- package/dist/body-parse.d.ts +28 -0
- package/{src/body-parse.ts → dist/body-parse.js} +18 -26
- package/dist/body-part-mapper.d.ts +92 -0
- package/dist/body-part-mapper.js +301 -0
- package/dist/body-sync-queue.d.ts +59 -0
- package/dist/body-sync-queue.js +56 -0
- package/dist/body-sync-terminal.d.ts +58 -0
- package/dist/body-sync-terminal.js +95 -0
- package/dist/body-sync.d.ts +351 -0
- package/dist/body-sync.js +1204 -0
- package/dist/calendar-parts.d.ts +23 -0
- package/dist/calendar-parts.js +18 -0
- package/dist/classification-backfill.d.ts +91 -0
- package/dist/classification-backfill.js +180 -0
- package/dist/classify-message.d.ts +31 -0
- package/dist/classify-message.js +89 -0
- package/dist/connection-factory.d.ts +54 -0
- package/dist/connection-factory.js +54 -0
- package/dist/connection-test.d.ts +41 -0
- package/dist/connection-test.js +152 -0
- package/dist/external-move.d.ts +94 -0
- package/dist/external-move.js +173 -0
- package/dist/filter-operations.d.ts +42 -0
- package/dist/filter-operations.js +211 -0
- package/dist/filters/filter-config.d.ts +14 -0
- package/dist/filters/filter-config.js +10 -0
- package/{src/filters/list-id.ts → dist/filters/list-id.d.ts} +2 -16
- package/dist/filters/list-id.js +26 -0
- package/dist/filters/match.d.ts +69 -0
- package/dist/filters/match.js +165 -0
- package/dist/filters/pipeline.d.ts +64 -0
- package/dist/filters/pipeline.js +100 -0
- package/{src/flag-push-terminal.ts → dist/flag-push-terminal.d.ts} +19 -77
- package/dist/flag-push-terminal.js +63 -0
- package/dist/flag-push.d.ts +69 -0
- package/dist/flag-push.js +111 -0
- package/dist/flag-queue.d.ts +175 -0
- package/dist/flag-queue.js +291 -0
- package/dist/folder-operations.d.ts +39 -0
- package/dist/folder-operations.js +112 -0
- package/dist/folder-placement.d.ts +13 -0
- package/dist/folder-placement.js +23 -0
- package/dist/heuristics/authenticationResults.d.ts +10 -0
- package/dist/heuristics/authenticationResults.js +187 -0
- package/dist/heuristics/classifyByHeaders.d.ts +82 -0
- package/dist/heuristics/classifyByHeaders.js +365 -0
- package/dist/heuristics/classifyPlacement.d.ts +67 -0
- package/dist/heuristics/classifyPlacement.js +153 -0
- package/dist/heuristics/machineSenders.d.ts +21 -0
- package/{src/heuristics/machineSenders.ts → dist/heuristics/machineSenders.js} +34 -40
- package/dist/heuristics/senderMismatch.d.ts +81 -0
- package/dist/heuristics/senderMismatch.js +192 -0
- package/dist/heuristics/socialDomains.d.ts +14 -0
- package/{src/heuristics/socialDomains.ts → dist/heuristics/socialDomains.js} +9 -9
- package/dist/heuristics/transactionalDomains.d.ts +18 -0
- package/{src/heuristics/transactionalDomains.ts → dist/heuristics/transactionalDomains.js} +8 -8
- package/dist/imapflow-connection.d.ts +377 -0
- package/dist/imapflow-connection.js +1147 -0
- package/dist/index.d.ts +63 -0
- package/dist/index.js +65 -0
- package/dist/list-id-backfill.d.ts +83 -0
- package/dist/list-id-backfill.js +132 -0
- package/dist/mailbox-cursor-rebuild.d.ts +74 -0
- package/dist/mailbox-cursor-rebuild.js +71 -0
- package/{src/mailbox-cursor.ts → dist/mailbox-cursor.d.ts} +20 -98
- package/dist/mailbox-cursor.js +102 -0
- package/dist/mailbox-intent.d.ts +41 -0
- package/{src/mailbox-intent.ts → dist/mailbox-intent.js} +39 -63
- package/dist/mailbox-management.d.ts +240 -0
- package/dist/mailbox-management.js +576 -0
- package/dist/mailbox-presence.d.ts +52 -0
- package/{src/mailbox-presence.ts → dist/mailbox-presence.js} +19 -30
- package/dist/mailbox-queue.d.ts +126 -0
- package/dist/mailbox-queue.js +214 -0
- package/dist/mailbox-sync.d.ts +93 -0
- package/dist/mailbox-sync.js +490 -0
- package/dist/mailbox-upstream.d.ts +38 -0
- package/{src/mailbox-upstream.ts → dist/mailbox-upstream.js} +37 -47
- package/dist/message-category.d.ts +51 -0
- package/{src/message-category.ts → dist/message-category.js} +28 -71
- package/dist/message-move.d.ts +285 -0
- package/dist/message-move.js +858 -0
- package/dist/message-parser.d.ts +14 -0
- package/dist/message-parser.js +17 -0
- package/{src/message-presence.ts → dist/message-presence.d.ts} +1 -13
- package/dist/message-presence.js +29 -0
- package/dist/message-read.d.ts +51 -0
- package/dist/message-read.js +31 -0
- package/dist/message-sync.d.ts +345 -0
- package/dist/message-sync.js +1358 -0
- package/dist/mime-walker.d.ts +102 -0
- package/dist/mime-walker.js +198 -0
- package/dist/organize-job.d.ts +20 -0
- package/dist/organize-job.js +37 -0
- package/dist/outbox-attachment-content.d.ts +37 -0
- package/dist/outbox-attachment-content.js +50 -0
- package/dist/outbox-attachment-filename.d.ts +19 -0
- package/dist/outbox-attachment-filename.js +64 -0
- package/dist/outbox-attachment.d.ts +148 -0
- package/dist/outbox-attachment.js +273 -0
- package/dist/outbox-queue.d.ts +73 -0
- package/dist/outbox-queue.js +284 -0
- package/{src/outbox-status.ts → dist/outbox-status.d.ts} +1 -6
- package/dist/outbox-status.js +18 -0
- package/dist/ownership.d.ts +24 -0
- package/dist/ownership.js +83 -0
- package/dist/placement-move-terminal.d.ts +67 -0
- package/dist/placement-move-terminal.js +88 -0
- package/dist/placement-move.d.ts +83 -0
- package/dist/placement-move.js +216 -0
- package/{src/placement-settled.ts → dist/placement-settled.d.ts} +19 -50
- package/dist/placement-settled.js +75 -0
- package/dist/quarantine.d.ts +95 -0
- package/dist/quarantine.js +131 -0
- package/dist/refusal.d.ts +31 -0
- package/dist/refusal.js +3 -0
- package/{src/restore-source-placement.ts → dist/restore-source-placement.d.ts} +29 -101
- package/dist/restore-source-placement.js +79 -0
- package/dist/search-text-backfill.d.ts +15 -0
- package/dist/search-text-backfill.js +33 -0
- package/dist/snippet.d.ts +61 -0
- package/dist/snippet.js +129 -0
- package/dist/spam-report.d.ts +86 -0
- package/dist/spam-report.js +241 -0
- package/dist/sync-warning.d.ts +21 -0
- package/dist/sync-warning.js +26 -0
- package/dist/sync-watermarks.d.ts +138 -0
- package/dist/sync-watermarks.js +205 -0
- package/dist/test-helpers/fake-imap.d.ts +151 -0
- package/dist/test-helpers/fake-imap.js +508 -0
- package/dist/test-helpers/folder-roles.d.ts +34 -0
- package/dist/test-helpers/folder-roles.js +47 -0
- package/dist/test-helpers/mime-tree-generator.d.ts +82 -0
- package/dist/test-helpers/mime-tree-generator.js +329 -0
- package/dist/test-helpers/test-store.d.ts +69 -0
- package/dist/test-helpers/test-store.js +193 -0
- package/dist/text/normalizer.d.ts +20 -0
- package/dist/text/normalizer.js +102 -0
- package/dist/thread-message-rows.d.ts +69 -0
- package/dist/thread-message-rows.js +40 -0
- package/dist/thread-root.d.ts +7 -0
- package/dist/thread-root.js +25 -0
- package/dist/types.d.ts +343 -0
- package/dist/types.js +19 -0
- package/package.json +86 -73
- package/src/account-credentials.ts +0 -210
- package/src/adversarial-mime.e2e.test.ts +0 -307
- package/src/attribute-mapper.test.ts +0 -104
- package/src/attribute-mapper.ts +0 -148
- package/src/body-parse.test.ts +0 -55
- package/src/body-part-mapper.property.test.ts +0 -354
- package/src/body-part-mapper.ts +0 -360
- 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-queue.test.ts +0 -124
- package/src/body-sync-queue.ts +0 -131
- package/src/body-sync-resync.test.ts +0 -605
- package/src/body-sync-terminal.test.ts +0 -310
- package/src/body-sync-terminal.ts +0 -176
- package/src/body-sync-unclassifiable-once.test.ts +0 -260
- package/src/body-sync-unsubscribed-auto-read.test.ts +0 -176
- package/src/body-sync.ts +0 -1970
- package/src/calendar-parts.ts +0 -41
- package/src/classification-backfill.test.ts +0 -615
- package/src/classification-backfill.ts +0 -363
- package/src/classify-message.test.ts +0 -205
- package/src/classify-message.ts +0 -116
- package/src/connection-factory.ts +0 -102
- package/src/connection-test.ts +0 -208
- package/src/credentials.test.ts +0 -181
- package/src/external-move.test.ts +0 -369
- package/src/external-move.ts +0 -272
- package/src/filters/anchor-drift.ts +0 -55
- package/src/filters/filter-config.test.ts +0 -229
- package/src/filters/filter-config.ts +0 -69
- package/src/filters/list-id.test.ts +0 -50
- package/src/filters/match.test.ts +0 -429
- package/src/filters/match.ts +0 -274
- package/src/filters/message-embedder.test.ts +0 -39
- package/src/filters/message-embedder.ts +0 -33
- package/src/filters/pipeline.test.ts +0 -330
- package/src/filters/pipeline.ts +0 -284
- package/src/flag-push-terminal.test.ts +0 -209
- package/src/flag-push.test.ts +0 -408
- package/src/flag-push.ts +0 -198
- package/src/flag-queue-stars.test.ts +0 -171
- package/src/flag-queue.ts +0 -509
- package/src/folder-placement.ts +0 -56
- package/src/heuristics/authenticationResults.test.ts +0 -189
- package/src/heuristics/authenticationResults.ts +0 -212
- package/src/heuristics/classifyByHeaders.test.ts +0 -889
- package/src/heuristics/classifyByHeaders.ts +0 -467
- package/src/heuristics/classifyPlacement.test.ts +0 -366
- package/src/heuristics/classifyPlacement.ts +0 -211
- package/src/heuristics/classifyRealMail.test.ts +0 -242
- package/src/heuristics/machineSenders.test.ts +0 -101
- package/src/heuristics/senderMismatch.test.ts +0 -346
- package/src/heuristics/senderMismatch.ts +0 -259
- package/src/imapflow-connect-retry.test.ts +0 -134
- package/src/imapflow-connection.e2e.test.ts +0 -335
- package/src/imapflow-connection.integ.test.ts +0 -316
- package/src/imapflow-connection.test.ts +0 -469
- package/src/imapflow-connection.ts +0 -1475
- package/src/index.ts +0 -350
- package/src/list-id-backfill.test.ts +0 -573
- package/src/list-id-backfill.ts +0 -274
- package/src/mailbox-cursor-rebuild.test.ts +0 -171
- package/src/mailbox-cursor-rebuild.ts +0 -146
- package/src/mailbox-cursor.test.ts +0 -249
- package/src/mailbox-delete-intent.test.ts +0 -246
- package/src/mailbox-management.test.ts +0 -356
- package/src/mailbox-management.ts +0 -839
- package/src/mailbox-presence.test.ts +0 -72
- package/src/mailbox-queue.ts +0 -415
- package/src/mailbox-rename-intent.test.ts +0 -389
- package/src/mailbox-rename-subtree.test.ts +0 -664
- package/src/mailbox-sync.test.ts +0 -848
- package/src/mailbox-sync.ts +0 -724
- 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-move.ts +0 -1348
- package/src/message-parser.ts +0 -26
- package/src/message-presence.test.ts +0 -50
- 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-flags.test.ts +0 -158
- 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/message-sync.ts +0 -1963
- package/src/mime-walker.test.ts +0 -311
- package/src/mime-walker.ts +0 -305
- package/src/outbox-attachment-content.test.ts +0 -153
- package/src/outbox-attachment-content.ts +0 -104
- package/src/outbox-attachment-filename.test.ts +0 -132
- package/src/outbox-attachment-filename.ts +0 -76
- package/src/outbox-attachment.test.ts +0 -738
- package/src/outbox-attachment.ts +0 -536
- package/src/outbox-failed-recovery.test.ts +0 -370
- package/src/outbox-queue-settled-row.test.ts +0 -140
- package/src/outbox-queue.test.ts +0 -291
- package/src/outbox-queue.ts +0 -508
- package/src/pass-through-unit-of-work.ts +0 -17
- package/src/placement-move-terminal.test.ts +0 -300
- package/src/placement-move-terminal.ts +0 -148
- package/src/placement-move-unsettled.test.ts +0 -275
- package/src/placement-move.ts +0 -336
- package/src/placement-settled.test.ts +0 -147
- package/src/placement-transition-race.test.ts +0 -482
- package/src/quarantine.test.ts +0 -191
- package/src/quarantine.ts +0 -228
- package/src/snippet.test.ts +0 -213
- package/src/snippet.ts +0 -152
- package/src/spam-report.test.ts +0 -801
- package/src/spam-report.ts +0 -357
- package/src/stale-message-reconcile.test.ts +0 -82
- package/src/stale-message-reconcile.ts +0 -55
- package/src/sync-watermarks.test.ts +0 -409
- package/src/sync-watermarks.ts +0 -319
- package/src/system-flag-wire-format.test.ts +0 -77
- package/src/test-helpers/folder-roles.ts +0 -66
- package/src/test-helpers/isolated-mailbox.ts +0 -44
- package/src/test-helpers/mailfuzz-connection.ts +0 -27
- package/src/test-helpers/mime-tree-generator.ts +0 -461
- package/src/text/normalizer.test.ts +0 -223
- package/src/text/normalizer.ts +0 -140
- package/src/thread-message-rows.ts +0 -77
- package/src/types/mailparser-augment.d.ts +0 -15
- package/src/types/natural-porter-stemmer.d.ts +0 -50
- package/src/types/stopword.d.ts +0 -69
- package/src/types.ts +0 -371
- 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
- package/tsconfig.json +0 -8
|
@@ -1,664 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { describe, it } from "node:test";
|
|
3
|
-
import type { IMailboxRepository, MailboxItem } from "@remit/data-ports";
|
|
4
|
-
import { MailboxSyncStatus } from "@remit/domain-enums";
|
|
5
|
-
import { MailboxManagementService } from "./mailbox-management.js";
|
|
6
|
-
import {
|
|
7
|
-
FolderGoneUpstreamError,
|
|
8
|
-
FolderRenameSettleError,
|
|
9
|
-
} from "./mailbox-upstream.js";
|
|
10
|
-
import type { IImapConnection } from "./types.js";
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* A folder mid-rename: the confirmed path the server still holds, and the
|
|
14
|
-
* target the intent recorded (D2). The settle's row set is exactly the rows
|
|
15
|
-
* carrying such a target, which is what makes it independent of whatever the
|
|
16
|
-
* subtree looks like by the time the RENAME lands (D15).
|
|
17
|
-
*/
|
|
18
|
-
const renaming = (
|
|
19
|
-
mailboxId: string,
|
|
20
|
-
fullPath: string,
|
|
21
|
-
pendingPath: string,
|
|
22
|
-
): MailboxItem =>
|
|
23
|
-
({
|
|
24
|
-
mailboxId,
|
|
25
|
-
accountId: "acc-1",
|
|
26
|
-
fullPath,
|
|
27
|
-
hierarchyDelimiter: "/",
|
|
28
|
-
namespacePrefix: "",
|
|
29
|
-
syncStatus: MailboxSyncStatus.pending,
|
|
30
|
-
pendingPath,
|
|
31
|
-
}) as MailboxItem;
|
|
32
|
-
|
|
33
|
-
const settled = (
|
|
34
|
-
mailboxId: string,
|
|
35
|
-
fullPath: string,
|
|
36
|
-
syncStatus: MailboxItem["syncStatus"] = MailboxSyncStatus.synced,
|
|
37
|
-
): MailboxItem =>
|
|
38
|
-
({
|
|
39
|
-
mailboxId,
|
|
40
|
-
accountId: "acc-1",
|
|
41
|
-
fullPath,
|
|
42
|
-
hierarchyDelimiter: "/",
|
|
43
|
-
namespacePrefix: "",
|
|
44
|
-
syncStatus,
|
|
45
|
-
}) as MailboxItem;
|
|
46
|
-
|
|
47
|
-
const store = (rows: MailboxItem[], vanishBeforeSettle: string[] = []) => {
|
|
48
|
-
const byId = new Map(rows.map((r) => [r.mailboxId, { ...r }]));
|
|
49
|
-
|
|
50
|
-
const repo: Pick<
|
|
51
|
-
IMailboxRepository,
|
|
52
|
-
"get" | "update" | "transition" | "findBySyncStatus"
|
|
53
|
-
> = {
|
|
54
|
-
get: (async (_accountId: string, mailboxId: string) => {
|
|
55
|
-
const found = byId.get(mailboxId as string);
|
|
56
|
-
if (!found) {
|
|
57
|
-
throw Object.assign(new Error(`Mailbox not found: ${mailboxId}`), {
|
|
58
|
-
name: "NotFoundError",
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
return found;
|
|
62
|
-
}) as IMailboxRepository["get"],
|
|
63
|
-
update: async (_accountId, mailboxId, patch) => {
|
|
64
|
-
const existing = byId.get(mailboxId);
|
|
65
|
-
if (!existing) {
|
|
66
|
-
throw Object.assign(new Error(`Mailbox not found: ${mailboxId}`), {
|
|
67
|
-
name: "NotFoundError",
|
|
68
|
-
});
|
|
69
|
-
}
|
|
70
|
-
const next = { ...existing, ...patch } as MailboxItem;
|
|
71
|
-
byId.set(mailboxId, next);
|
|
72
|
-
return next;
|
|
73
|
-
},
|
|
74
|
-
transition: async (_accountId, mailboxId, intent) => {
|
|
75
|
-
const existing = byId.get(mailboxId);
|
|
76
|
-
if (!existing) return null;
|
|
77
|
-
if (!intent.from.includes(existing.syncStatus)) return null;
|
|
78
|
-
if (
|
|
79
|
-
intent.wherePendingPath !== undefined &&
|
|
80
|
-
(intent.wherePendingPath ?? undefined) !== existing.pendingPath
|
|
81
|
-
) {
|
|
82
|
-
return null;
|
|
83
|
-
}
|
|
84
|
-
const keepsTarget =
|
|
85
|
-
intent.to === MailboxSyncStatus.pending ||
|
|
86
|
-
intent.to === MailboxSyncStatus.failed;
|
|
87
|
-
const next = {
|
|
88
|
-
...existing,
|
|
89
|
-
...(intent.set?.fullPath !== undefined
|
|
90
|
-
? { fullPath: intent.set.fullPath }
|
|
91
|
-
: {}),
|
|
92
|
-
syncStatus: intent.to,
|
|
93
|
-
} as MailboxItem;
|
|
94
|
-
if (!keepsTarget) delete (next as { pendingPath?: string }).pendingPath;
|
|
95
|
-
byId.set(mailboxId, next);
|
|
96
|
-
return next;
|
|
97
|
-
},
|
|
98
|
-
findBySyncStatus: async (_accountId, syncStatus) => {
|
|
99
|
-
const found = [...byId.values()].filter(
|
|
100
|
-
(r) => r.syncStatus === syncStatus,
|
|
101
|
-
);
|
|
102
|
-
for (const mailboxId of vanishBeforeSettle) byId.delete(mailboxId);
|
|
103
|
-
return found;
|
|
104
|
-
},
|
|
105
|
-
};
|
|
106
|
-
|
|
107
|
-
return {
|
|
108
|
-
repo: repo as IMailboxRepository,
|
|
109
|
-
rowOf: (mailboxId: string) => byId.get(mailboxId),
|
|
110
|
-
};
|
|
111
|
-
};
|
|
112
|
-
|
|
113
|
-
const connection = (newPath?: string): IImapConnection =>
|
|
114
|
-
({
|
|
115
|
-
renameMailbox: async (_old: string, requested: string) => ({
|
|
116
|
-
path: _old,
|
|
117
|
-
newPath: newPath ?? requested,
|
|
118
|
-
}),
|
|
119
|
-
listMailboxes: async () => [],
|
|
120
|
-
}) as unknown as IImapConnection;
|
|
121
|
-
|
|
122
|
-
const refusing = (
|
|
123
|
-
error: Error,
|
|
124
|
-
listed: string[] | "unreadable" = [],
|
|
125
|
-
): IImapConnection =>
|
|
126
|
-
({
|
|
127
|
-
renameMailbox: async () => {
|
|
128
|
-
throw error;
|
|
129
|
-
},
|
|
130
|
-
listMailboxes: async () => {
|
|
131
|
-
if (listed === "unreadable") throw new Error("IMAP connection lost");
|
|
132
|
-
return listed.map((fullPath) => ({ fullPath, delimiter: "/" }));
|
|
133
|
-
},
|
|
134
|
-
}) as unknown as IImapConnection;
|
|
135
|
-
|
|
136
|
-
const nonexistent = (): Error =>
|
|
137
|
-
Object.assign(new Error("Command failed"), {
|
|
138
|
-
serverResponseCode: "NONEXISTENT",
|
|
139
|
-
responseText: "Mailbox doesn't exist: Work",
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
describe("MailboxManagementService.syncRename — settling one intent", () => {
|
|
143
|
-
it("adopts the confirmed path on the folder and every row that carried the intent", async () => {
|
|
144
|
-
const { repo, rowOf } = store([
|
|
145
|
-
renaming("mbx-parent", "Work", "Projects"),
|
|
146
|
-
renaming("mbx-child", "Work/2026", "Projects/2026"),
|
|
147
|
-
renaming("mbx-grandchild", "Work/2026/Q1", "Projects/2026/Q1"),
|
|
148
|
-
]);
|
|
149
|
-
const service = new MailboxManagementService(repo);
|
|
150
|
-
|
|
151
|
-
const result = await service.syncRename(
|
|
152
|
-
"acc-1",
|
|
153
|
-
"mbx-parent",
|
|
154
|
-
"Work",
|
|
155
|
-
"Projects",
|
|
156
|
-
async () => connection(),
|
|
157
|
-
);
|
|
158
|
-
|
|
159
|
-
assert.equal(result.success, true);
|
|
160
|
-
for (const [id, path] of [
|
|
161
|
-
["mbx-parent", "Projects"],
|
|
162
|
-
["mbx-child", "Projects/2026"],
|
|
163
|
-
["mbx-grandchild", "Projects/2026/Q1"],
|
|
164
|
-
] as const) {
|
|
165
|
-
assert.equal(rowOf(id)?.fullPath, path);
|
|
166
|
-
assert.equal(rowOf(id)?.syncStatus, MailboxSyncStatus.synced);
|
|
167
|
-
assert.equal(rowOf(id)?.pendingPath, undefined);
|
|
168
|
-
}
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
it("adopts the path ImapFlow resolved in preference to the requested one", async () => {
|
|
172
|
-
// Not the server echoing a name back — IMAP's RENAME reply carries none.
|
|
173
|
-
// It is ImapFlow's own normalization, with the namespace prefix applied,
|
|
174
|
-
// and adopting it is what stops the row and the server drifting apart.
|
|
175
|
-
const { repo, rowOf } = store([
|
|
176
|
-
renaming("mbx-parent", "Work", "Projects"),
|
|
177
|
-
renaming("mbx-child", "Work/2026", "Projects/2026"),
|
|
178
|
-
]);
|
|
179
|
-
const service = new MailboxManagementService(repo);
|
|
180
|
-
|
|
181
|
-
await service.syncRename(
|
|
182
|
-
"acc-1",
|
|
183
|
-
"mbx-parent",
|
|
184
|
-
"Work",
|
|
185
|
-
"Projects",
|
|
186
|
-
async () => connection("INBOX/Projects"),
|
|
187
|
-
);
|
|
188
|
-
|
|
189
|
-
assert.equal(rowOf("mbx-parent")?.fullPath, "INBOX/Projects");
|
|
190
|
-
assert.equal(rowOf("mbx-child")?.fullPath, "INBOX/Projects/2026");
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
it("leaves a folder the sweep inserted under the branch after the intent alone", async () => {
|
|
194
|
-
// D15: the settle applies to the rows that recorded the intent, and those
|
|
195
|
-
// rows identify themselves. Re-resolving the subtree would either throw —
|
|
196
|
-
// poisoning the account's FIFO group — or leave every row pending forever.
|
|
197
|
-
const { repo, rowOf } = store([
|
|
198
|
-
renaming("mbx-parent", "Work", "Projects"),
|
|
199
|
-
settled("mbx-interloper", "Projects/Fresh"),
|
|
200
|
-
]);
|
|
201
|
-
const service = new MailboxManagementService(repo);
|
|
202
|
-
|
|
203
|
-
const result = await service.syncRename(
|
|
204
|
-
"acc-1",
|
|
205
|
-
"mbx-parent",
|
|
206
|
-
"Work",
|
|
207
|
-
"Projects",
|
|
208
|
-
async () => connection(),
|
|
209
|
-
);
|
|
210
|
-
|
|
211
|
-
assert.equal(result.success, true);
|
|
212
|
-
assert.equal(rowOf("mbx-parent")?.syncStatus, MailboxSyncStatus.synced);
|
|
213
|
-
assert.equal(rowOf("mbx-interloper")?.fullPath, "Projects/Fresh");
|
|
214
|
-
assert.equal(rowOf("mbx-interloper")?.syncStatus, MailboxSyncStatus.synced);
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
it("settles the rest when a descendant is deleted after the intent", async () => {
|
|
218
|
-
const { repo, rowOf } = store(
|
|
219
|
-
[
|
|
220
|
-
renaming("mbx-parent", "Work", "Projects"),
|
|
221
|
-
renaming("mbx-gone", "Work/Gone", "Projects/Gone"),
|
|
222
|
-
renaming("mbx-child", "Work/2026", "Projects/2026"),
|
|
223
|
-
],
|
|
224
|
-
["mbx-gone"],
|
|
225
|
-
);
|
|
226
|
-
const service = new MailboxManagementService(repo);
|
|
227
|
-
|
|
228
|
-
const result = await service.syncRename(
|
|
229
|
-
"acc-1",
|
|
230
|
-
"mbx-parent",
|
|
231
|
-
"Work",
|
|
232
|
-
"Projects",
|
|
233
|
-
async () => connection(),
|
|
234
|
-
);
|
|
235
|
-
|
|
236
|
-
assert.equal(result.success, true);
|
|
237
|
-
assert.equal(rowOf("mbx-parent")?.fullPath, "Projects");
|
|
238
|
-
assert.equal(rowOf("mbx-child")?.fullPath, "Projects/2026");
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
it("does not touch a folder whose own rename is aimed somewhere else", async () => {
|
|
242
|
-
const { repo, rowOf } = store([
|
|
243
|
-
renaming("mbx-parent", "Work", "Projects"),
|
|
244
|
-
renaming("mbx-other", "Admin", "Beheer"),
|
|
245
|
-
]);
|
|
246
|
-
const service = new MailboxManagementService(repo);
|
|
247
|
-
|
|
248
|
-
await service.syncRename(
|
|
249
|
-
"acc-1",
|
|
250
|
-
"mbx-parent",
|
|
251
|
-
"Work",
|
|
252
|
-
"Projects",
|
|
253
|
-
async () => connection(),
|
|
254
|
-
);
|
|
255
|
-
|
|
256
|
-
assert.equal(rowOf("mbx-other")?.fullPath, "Admin");
|
|
257
|
-
assert.equal(rowOf("mbx-other")?.pendingPath, "Beheer");
|
|
258
|
-
assert.equal(rowOf("mbx-other")?.syncStatus, MailboxSyncStatus.pending);
|
|
259
|
-
});
|
|
260
|
-
|
|
261
|
-
it("issues no RENAME when the row has already settled", async () => {
|
|
262
|
-
const { repo } = store([settled("mbx-parent", "Projects")]);
|
|
263
|
-
const service = new MailboxManagementService(repo);
|
|
264
|
-
let issued = 0;
|
|
265
|
-
|
|
266
|
-
const result = await service.syncRename(
|
|
267
|
-
"acc-1",
|
|
268
|
-
"mbx-parent",
|
|
269
|
-
"Work",
|
|
270
|
-
"Projects",
|
|
271
|
-
async () =>
|
|
272
|
-
({
|
|
273
|
-
renameMailbox: async () => {
|
|
274
|
-
issued += 1;
|
|
275
|
-
return { path: "Work", newPath: "Projects" };
|
|
276
|
-
},
|
|
277
|
-
}) as unknown as IImapConnection,
|
|
278
|
-
);
|
|
279
|
-
|
|
280
|
-
assert.equal(result.success, true);
|
|
281
|
-
assert.equal(issued, 0, "a redelivery must not re-issue the rename");
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
it("issues no RENAME when the row is pending for a different target", async () => {
|
|
285
|
-
// A redelivered MAILBOX_RENAME against a folder a second rename has since
|
|
286
|
-
// claimed. `pending` alone passes; the recorded target is what refuses it,
|
|
287
|
-
// and re-issuing from a path that has already moved is what that prevents.
|
|
288
|
-
const { repo, rowOf } = store([renaming("mbx-parent", "Work", "Beheer")]);
|
|
289
|
-
const service = new MailboxManagementService(repo);
|
|
290
|
-
let issued = 0;
|
|
291
|
-
|
|
292
|
-
await service.syncRename(
|
|
293
|
-
"acc-1",
|
|
294
|
-
"mbx-parent",
|
|
295
|
-
"Work",
|
|
296
|
-
"Projects",
|
|
297
|
-
async () =>
|
|
298
|
-
({
|
|
299
|
-
renameMailbox: async () => {
|
|
300
|
-
issued += 1;
|
|
301
|
-
return { path: "Work", newPath: "Projects" };
|
|
302
|
-
},
|
|
303
|
-
}) as unknown as IImapConnection,
|
|
304
|
-
);
|
|
305
|
-
|
|
306
|
-
assert.equal(issued, 0);
|
|
307
|
-
assert.equal(rowOf("mbx-parent")?.pendingPath, "Beheer");
|
|
308
|
-
});
|
|
309
|
-
});
|
|
310
|
-
|
|
311
|
-
describe("MailboxManagementService.syncRename — rows it must not claim", () => {
|
|
312
|
-
it("leaves a row a different rename recorded under the same branch alone", async () => {
|
|
313
|
-
// Two renames aimed under one path. Matching on the recorded target alone
|
|
314
|
-
// stamps `Other` onto a path this rename never asked for, leaves its own
|
|
315
|
-
// rename with nothing left to settle, and hands the sweep two rows
|
|
316
|
-
// claiming one path to reap and re-insert.
|
|
317
|
-
const { repo, rowOf } = store([
|
|
318
|
-
renaming("mbx-parent", "Work", "Projects"),
|
|
319
|
-
renaming("mbx-other", "Other", "Projects/Old"),
|
|
320
|
-
]);
|
|
321
|
-
const service = new MailboxManagementService(repo);
|
|
322
|
-
|
|
323
|
-
await service.syncRename(
|
|
324
|
-
"acc-1",
|
|
325
|
-
"mbx-parent",
|
|
326
|
-
"Work",
|
|
327
|
-
"Projects",
|
|
328
|
-
async () => connection(),
|
|
329
|
-
);
|
|
330
|
-
|
|
331
|
-
assert.equal(rowOf("mbx-parent")?.fullPath, "Projects");
|
|
332
|
-
assert.equal(rowOf("mbx-other")?.fullPath, "Other");
|
|
333
|
-
assert.equal(rowOf("mbx-other")?.pendingPath, "Projects/Old");
|
|
334
|
-
assert.equal(rowOf("mbx-other")?.syncStatus, MailboxSyncStatus.pending);
|
|
335
|
-
});
|
|
336
|
-
|
|
337
|
-
it("leaves a descendant whose recorded target is not this rename's rebase", async () => {
|
|
338
|
-
const { repo, rowOf } = store([
|
|
339
|
-
renaming("mbx-parent", "Work", "Projects"),
|
|
340
|
-
renaming("mbx-child", "Work/2026", "Projects/Archief"),
|
|
341
|
-
]);
|
|
342
|
-
const service = new MailboxManagementService(repo);
|
|
343
|
-
|
|
344
|
-
await service.syncRename(
|
|
345
|
-
"acc-1",
|
|
346
|
-
"mbx-parent",
|
|
347
|
-
"Work",
|
|
348
|
-
"Projects",
|
|
349
|
-
async () => connection(),
|
|
350
|
-
);
|
|
351
|
-
|
|
352
|
-
assert.equal(rowOf("mbx-parent")?.fullPath, "Projects");
|
|
353
|
-
assert.equal(rowOf("mbx-child")?.fullPath, "Work/2026");
|
|
354
|
-
assert.equal(rowOf("mbx-child")?.syncStatus, MailboxSyncStatus.pending);
|
|
355
|
-
});
|
|
356
|
-
|
|
357
|
-
it("fails only the rows this rename recorded", async () => {
|
|
358
|
-
const { repo, rowOf } = store([
|
|
359
|
-
renaming("mbx-parent", "Work", "Projects"),
|
|
360
|
-
renaming("mbx-other", "Other", "Projects/Old"),
|
|
361
|
-
]);
|
|
362
|
-
const service = new MailboxManagementService(repo);
|
|
363
|
-
|
|
364
|
-
await service.failRename(
|
|
365
|
-
"acc-1",
|
|
366
|
-
"mbx-parent",
|
|
367
|
-
"Work",
|
|
368
|
-
"Projects",
|
|
369
|
-
"Permission denied",
|
|
370
|
-
);
|
|
371
|
-
|
|
372
|
-
assert.equal(rowOf("mbx-parent")?.syncStatus, MailboxSyncStatus.failed);
|
|
373
|
-
assert.equal(rowOf("mbx-other")?.syncStatus, MailboxSyncStatus.pending);
|
|
374
|
-
});
|
|
375
|
-
});
|
|
376
|
-
|
|
377
|
-
describe("MailboxManagementService.syncRename — a rename that already landed", () => {
|
|
378
|
-
it("settles rather than deleting when NONEXISTENT means the source has moved", async () => {
|
|
379
|
-
// A redelivery after a lost settle re-issues RENAME from a path that has
|
|
380
|
-
// moved. Reading that NONEXISTENT as an upstream delete drops the folder
|
|
381
|
-
// and its mail while the folder is alive at the target.
|
|
382
|
-
const { repo, rowOf } = store([renaming("mbx-parent", "Work", "Projects")]);
|
|
383
|
-
const service = new MailboxManagementService(repo);
|
|
384
|
-
|
|
385
|
-
const result = await service.syncRename(
|
|
386
|
-
"acc-1",
|
|
387
|
-
"mbx-parent",
|
|
388
|
-
"Work",
|
|
389
|
-
"Projects",
|
|
390
|
-
async () => refusing(nonexistent(), ["Projects"]),
|
|
391
|
-
);
|
|
392
|
-
|
|
393
|
-
assert.equal(result.success, true);
|
|
394
|
-
assert.equal(rowOf("mbx-parent")?.fullPath, "Projects");
|
|
395
|
-
assert.equal(rowOf("mbx-parent")?.syncStatus, MailboxSyncStatus.synced);
|
|
396
|
-
});
|
|
397
|
-
|
|
398
|
-
it("reports the folder as gone, carrying what the server said, when neither path is listed", async () => {
|
|
399
|
-
const { repo } = store([renaming("mbx-parent", "Work", "Projects")]);
|
|
400
|
-
const service = new MailboxManagementService(repo);
|
|
401
|
-
|
|
402
|
-
await assert.rejects(
|
|
403
|
-
service.syncRename("acc-1", "mbx-parent", "Work", "Projects", async () =>
|
|
404
|
-
refusing(nonexistent(), ["Andere"]),
|
|
405
|
-
),
|
|
406
|
-
(error: unknown) =>
|
|
407
|
-
error instanceof FolderGoneUpstreamError &&
|
|
408
|
-
(error.cause as { serverResponseCode?: string })?.serverResponseCode ===
|
|
409
|
-
"NONEXISTENT",
|
|
410
|
-
);
|
|
411
|
-
});
|
|
412
|
-
|
|
413
|
-
it("raises a settle failure as its own kind, never as a refused rename", async () => {
|
|
414
|
-
// The server executed the rename. Marking the rows failed here would offer
|
|
415
|
-
// a retry of work already done, on a `fullPath` the server no longer holds.
|
|
416
|
-
const { repo, rowOf } = store([renaming("mbx-parent", "Work", "Projects")]);
|
|
417
|
-
const broken = {
|
|
418
|
-
...repo,
|
|
419
|
-
transition: async () => {
|
|
420
|
-
throw new Error("database is locked");
|
|
421
|
-
},
|
|
422
|
-
} as unknown as IMailboxRepository;
|
|
423
|
-
const service = new MailboxManagementService(broken);
|
|
424
|
-
|
|
425
|
-
await assert.rejects(
|
|
426
|
-
service.syncRename("acc-1", "mbx-parent", "Work", "Projects", async () =>
|
|
427
|
-
connection(),
|
|
428
|
-
),
|
|
429
|
-
(error: unknown) => error instanceof FolderRenameSettleError,
|
|
430
|
-
);
|
|
431
|
-
|
|
432
|
-
assert.equal(rowOf("mbx-parent")?.syncStatus, MailboxSyncStatus.pending);
|
|
433
|
-
assert.equal(
|
|
434
|
-
rowOf("mbx-parent")?.pendingPath,
|
|
435
|
-
"Projects",
|
|
436
|
-
"the intent stands, so the redelivery finishes the settle",
|
|
437
|
-
);
|
|
438
|
-
});
|
|
439
|
-
});
|
|
440
|
-
|
|
441
|
-
describe("MailboxManagementService.syncRename — a settle that dies part-way", () => {
|
|
442
|
-
/**
|
|
443
|
-
* The named folder's row is the redelivery's only witness: the guard reads
|
|
444
|
-
* it and nothing else. Settling it before its descendants would ack a run
|
|
445
|
-
* that stopped half way, and those rows would stay `pending` forever —
|
|
446
|
-
* skipped by message sync, and with no route out, because an intent may only
|
|
447
|
-
* be recorded from `synced` or `failed`.
|
|
448
|
-
*/
|
|
449
|
-
it("leaves the named folder carrying the intent when a descendant's write fails", async () => {
|
|
450
|
-
const { repo, rowOf } = store([
|
|
451
|
-
renaming("mbx-parent", "Work", "Projects"),
|
|
452
|
-
renaming("mbx-child", "Work/2026", "Projects/2026"),
|
|
453
|
-
]);
|
|
454
|
-
let broken = true;
|
|
455
|
-
const flaky = {
|
|
456
|
-
...repo,
|
|
457
|
-
transition: async (
|
|
458
|
-
accountId: string,
|
|
459
|
-
mailboxId: string,
|
|
460
|
-
intent: Parameters<IMailboxRepository["transition"]>[2],
|
|
461
|
-
) => {
|
|
462
|
-
if (broken && mailboxId === "mbx-child") {
|
|
463
|
-
throw new Error("database is locked");
|
|
464
|
-
}
|
|
465
|
-
return repo.transition(accountId, mailboxId, intent);
|
|
466
|
-
},
|
|
467
|
-
} as unknown as IMailboxRepository;
|
|
468
|
-
const service = new MailboxManagementService(flaky);
|
|
469
|
-
|
|
470
|
-
await assert.rejects(
|
|
471
|
-
service.syncRename("acc-1", "mbx-parent", "Work", "Projects", async () =>
|
|
472
|
-
connection(),
|
|
473
|
-
),
|
|
474
|
-
(error: unknown) => error instanceof FolderRenameSettleError,
|
|
475
|
-
);
|
|
476
|
-
|
|
477
|
-
assert.equal(rowOf("mbx-parent")?.syncStatus, MailboxSyncStatus.pending);
|
|
478
|
-
assert.equal(rowOf("mbx-parent")?.pendingPath, "Projects");
|
|
479
|
-
assert.equal(rowOf("mbx-child")?.syncStatus, MailboxSyncStatus.pending);
|
|
480
|
-
|
|
481
|
-
// The redelivery re-enters against an intent still standing and finishes.
|
|
482
|
-
broken = false;
|
|
483
|
-
const redelivered = new MailboxManagementService(flaky);
|
|
484
|
-
const result = await redelivered.syncRename(
|
|
485
|
-
"acc-1",
|
|
486
|
-
"mbx-parent",
|
|
487
|
-
"Work",
|
|
488
|
-
"Projects",
|
|
489
|
-
async () => connection(),
|
|
490
|
-
);
|
|
491
|
-
|
|
492
|
-
assert.equal(result.success, true);
|
|
493
|
-
assert.equal(rowOf("mbx-parent")?.fullPath, "Projects");
|
|
494
|
-
assert.equal(rowOf("mbx-child")?.fullPath, "Projects/2026");
|
|
495
|
-
assert.equal(rowOf("mbx-child")?.syncStatus, MailboxSyncStatus.synced);
|
|
496
|
-
});
|
|
497
|
-
});
|
|
498
|
-
|
|
499
|
-
describe("MailboxManagementService.syncRename — reading NONEXISTENT", () => {
|
|
500
|
-
const prefixed = (): MailboxItem =>
|
|
501
|
-
({
|
|
502
|
-
mailboxId: "mbx-parent",
|
|
503
|
-
accountId: "acc-1",
|
|
504
|
-
fullPath: "Work",
|
|
505
|
-
hierarchyDelimiter: ".",
|
|
506
|
-
namespacePrefix: "INBOX.",
|
|
507
|
-
syncStatus: MailboxSyncStatus.pending,
|
|
508
|
-
pendingPath: "Projects",
|
|
509
|
-
}) as MailboxItem;
|
|
510
|
-
|
|
511
|
-
it("reads the target through the account's namespace prefix", async () => {
|
|
512
|
-
// The requested string and the path the server keeps are not the same:
|
|
513
|
-
// a Dovecot INBOX namespace stores `Projects` as `INBOX.Projects`.
|
|
514
|
-
// Comparing the raw request against the listing reads a folder that is
|
|
515
|
-
// plainly there as absent — and that answer deletes the user's mail.
|
|
516
|
-
const { repo, rowOf } = store([prefixed()]);
|
|
517
|
-
const service = new MailboxManagementService(repo);
|
|
518
|
-
|
|
519
|
-
const result = await service.syncRename(
|
|
520
|
-
"acc-1",
|
|
521
|
-
"mbx-parent",
|
|
522
|
-
"Work",
|
|
523
|
-
"Projects",
|
|
524
|
-
async () => refusing(nonexistent(), ["INBOX.Projects"]),
|
|
525
|
-
);
|
|
526
|
-
|
|
527
|
-
assert.equal(result.success, true);
|
|
528
|
-
assert.equal(rowOf("mbx-parent")?.syncStatus, MailboxSyncStatus.synced);
|
|
529
|
-
});
|
|
530
|
-
|
|
531
|
-
it("raises the confirmed kind only when the listing holds neither path", async () => {
|
|
532
|
-
const { repo } = store([renaming("mbx-parent", "Work", "Projects")]);
|
|
533
|
-
const service = new MailboxManagementService(repo);
|
|
534
|
-
|
|
535
|
-
await assert.rejects(
|
|
536
|
-
service.syncRename("acc-1", "mbx-parent", "Work", "Projects", async () =>
|
|
537
|
-
refusing(nonexistent(), ["Andere"]),
|
|
538
|
-
),
|
|
539
|
-
(error: unknown) => error instanceof FolderGoneUpstreamError,
|
|
540
|
-
);
|
|
541
|
-
});
|
|
542
|
-
|
|
543
|
-
it("refuses the rename rather than confirming a delete when the source is still listed", async () => {
|
|
544
|
-
const { repo } = store([renaming("mbx-parent", "Work", "Projects")]);
|
|
545
|
-
const service = new MailboxManagementService(repo);
|
|
546
|
-
|
|
547
|
-
await assert.rejects(
|
|
548
|
-
service.syncRename("acc-1", "mbx-parent", "Work", "Projects", async () =>
|
|
549
|
-
refusing(nonexistent(), ["Work"]),
|
|
550
|
-
),
|
|
551
|
-
(error: unknown) =>
|
|
552
|
-
!(error instanceof FolderGoneUpstreamError) &&
|
|
553
|
-
(error as { serverResponseCode?: string }).serverResponseCode ===
|
|
554
|
-
"NONEXISTENT",
|
|
555
|
-
);
|
|
556
|
-
});
|
|
557
|
-
|
|
558
|
-
it("refuses the rename when the listing cannot be read at all", async () => {
|
|
559
|
-
// A probe that answers nothing decides nothing. A refused rename costs a
|
|
560
|
-
// retry; reading an unclear answer as a delete costs the folder's mail.
|
|
561
|
-
const { repo } = store([renaming("mbx-parent", "Work", "Projects")]);
|
|
562
|
-
const service = new MailboxManagementService(repo);
|
|
563
|
-
|
|
564
|
-
await assert.rejects(
|
|
565
|
-
service.syncRename("acc-1", "mbx-parent", "Work", "Projects", async () =>
|
|
566
|
-
refusing(nonexistent(), "unreadable"),
|
|
567
|
-
),
|
|
568
|
-
(error: unknown) => !(error instanceof FolderGoneUpstreamError),
|
|
569
|
-
);
|
|
570
|
-
});
|
|
571
|
-
});
|
|
572
|
-
|
|
573
|
-
describe("MailboxManagementService.failRename", () => {
|
|
574
|
-
it("marks every intent-carrying row failed, keeping its path and its target", async () => {
|
|
575
|
-
const { repo, rowOf } = store([
|
|
576
|
-
renaming("mbx-parent", "Work", "Projects"),
|
|
577
|
-
renaming("mbx-child", "Work/2026", "Projects/2026"),
|
|
578
|
-
]);
|
|
579
|
-
const service = new MailboxManagementService(repo);
|
|
580
|
-
|
|
581
|
-
await service.failRename(
|
|
582
|
-
"acc-1",
|
|
583
|
-
"mbx-parent",
|
|
584
|
-
"Work",
|
|
585
|
-
"Projects",
|
|
586
|
-
"Permission denied",
|
|
587
|
-
);
|
|
588
|
-
|
|
589
|
-
for (const [id, path, target] of [
|
|
590
|
-
["mbx-parent", "Work", "Projects"],
|
|
591
|
-
["mbx-child", "Work/2026", "Projects/2026"],
|
|
592
|
-
] as const) {
|
|
593
|
-
assert.equal(rowOf(id)?.syncStatus, MailboxSyncStatus.failed);
|
|
594
|
-
assert.equal(rowOf(id)?.fullPath, path, "nothing is restored (D2)");
|
|
595
|
-
assert.equal(
|
|
596
|
-
rowOf(id)?.pendingPath,
|
|
597
|
-
target,
|
|
598
|
-
"the target is kept so the client can name it and offer a retry",
|
|
599
|
-
);
|
|
600
|
-
}
|
|
601
|
-
});
|
|
602
|
-
|
|
603
|
-
it("raises a NotFoundError when the folder row itself is gone", async () => {
|
|
604
|
-
const { repo } = store([]);
|
|
605
|
-
const service = new MailboxManagementService(repo);
|
|
606
|
-
|
|
607
|
-
await assert.rejects(
|
|
608
|
-
service.failRename(
|
|
609
|
-
"acc-1",
|
|
610
|
-
"mbx-parent",
|
|
611
|
-
"Work",
|
|
612
|
-
"Projects",
|
|
613
|
-
"Permission denied",
|
|
614
|
-
),
|
|
615
|
-
(error: unknown) => (error as Error).name === "NotFoundError",
|
|
616
|
-
);
|
|
617
|
-
});
|
|
618
|
-
|
|
619
|
-
it("records nothing, quietly, when the row is there but carries no intent", async () => {
|
|
620
|
-
// Never a NotFoundError: the caller reads that as the user having deleted
|
|
621
|
-
// the folder and acks the message, which would swallow the IMAP failure
|
|
622
|
-
// that brought us here and leave the subtree `pending` with no retry.
|
|
623
|
-
const { repo } = store([settled("mbx-parent", "Work")]);
|
|
624
|
-
const service = new MailboxManagementService(repo);
|
|
625
|
-
|
|
626
|
-
await assert.doesNotReject(
|
|
627
|
-
service.failRename(
|
|
628
|
-
"acc-1",
|
|
629
|
-
"mbx-parent",
|
|
630
|
-
"Work",
|
|
631
|
-
"Projects",
|
|
632
|
-
"Permission denied",
|
|
633
|
-
),
|
|
634
|
-
);
|
|
635
|
-
});
|
|
636
|
-
|
|
637
|
-
it("settles a rename ImapFlow never issued as a refusal, not a TypeError", async () => {
|
|
638
|
-
// `mailboxRename` resolves `undefined` when the connection is not
|
|
639
|
-
// AUTHENTICATED or SELECTED. The settle reads the resolved path, so an
|
|
640
|
-
// unguarded dereference would raise a TypeError the rename cannot classify.
|
|
641
|
-
const { repo, rowOf } = store([renaming("mbx-parent", "Work", "Projects")]);
|
|
642
|
-
const service = new MailboxManagementService(repo);
|
|
643
|
-
const notReady = new Error(
|
|
644
|
-
'Rename of "Work" was not issued: the IMAP connection is not ready',
|
|
645
|
-
);
|
|
646
|
-
|
|
647
|
-
await assert.rejects(
|
|
648
|
-
service.syncRename("acc-1", "mbx-parent", "Work", "Projects", async () =>
|
|
649
|
-
refusing(notReady),
|
|
650
|
-
),
|
|
651
|
-
/not ready/,
|
|
652
|
-
);
|
|
653
|
-
|
|
654
|
-
await service.failRename(
|
|
655
|
-
"acc-1",
|
|
656
|
-
"mbx-parent",
|
|
657
|
-
"Work",
|
|
658
|
-
"Projects",
|
|
659
|
-
"Permission denied",
|
|
660
|
-
);
|
|
661
|
-
assert.equal(rowOf("mbx-parent")?.syncStatus, MailboxSyncStatus.failed);
|
|
662
|
-
assert.equal(rowOf("mbx-parent")?.fullPath, "Work");
|
|
663
|
-
});
|
|
664
|
-
});
|