@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
package/src/mailbox-sync.test.ts
DELETED
|
@@ -1,848 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { describe, it } from "node:test";
|
|
3
|
-
import type {
|
|
4
|
-
IMailboxRepository,
|
|
5
|
-
IMailboxSpecialUseRepository,
|
|
6
|
-
} from "@remit/data-ports";
|
|
7
|
-
import { NotFoundError } from "@remit/data-ports/errors";
|
|
8
|
-
import {
|
|
9
|
-
MailboxCursorState,
|
|
10
|
-
MailboxSpecialUse,
|
|
11
|
-
MailboxSyncStatus,
|
|
12
|
-
} from "@remit/domain-enums";
|
|
13
|
-
import { parseImapAttributes } from "./attribute-mapper.js";
|
|
14
|
-
import { type MailboxSyncLogger, MailboxSyncService } from "./mailbox-sync.js";
|
|
15
|
-
|
|
16
|
-
const silentLogger: MailboxSyncLogger = {
|
|
17
|
-
info: () => {},
|
|
18
|
-
debug: () => {},
|
|
19
|
-
error: () => {},
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
import type { IImapConnection, ImapNamespaces } from "./types.js";
|
|
23
|
-
|
|
24
|
-
describe("parseImapAttributes – locale invariance (#194)", () => {
|
|
25
|
-
it("recognizes \\Sent regardless of folder name", () => {
|
|
26
|
-
const dutch = parseImapAttributes(["\\HasNoChildren", "\\Sent"]);
|
|
27
|
-
assert.deepEqual(dutch.specialUse, [MailboxSpecialUse.Sent]);
|
|
28
|
-
|
|
29
|
-
const english = parseImapAttributes(["\\Sent"]);
|
|
30
|
-
assert.deepEqual(english.specialUse, [MailboxSpecialUse.Sent]);
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
it("recognizes \\Drafts on a localized 'Concepten' folder", () => {
|
|
34
|
-
// IMAP server tells us the flag — the folder name is irrelevant.
|
|
35
|
-
const parsed = parseImapAttributes(["\\Drafts"]);
|
|
36
|
-
assert.deepEqual(parsed.specialUse, [MailboxSpecialUse.Drafts]);
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it("returns an empty list when no flag is present", () => {
|
|
40
|
-
// A real user folder like Outlook NL's "Nieuwsbrieven" carries no
|
|
41
|
-
// SPECIAL-USE attributes — leave it untouched downstream.
|
|
42
|
-
const parsed = parseImapAttributes(["\\HasNoChildren"]);
|
|
43
|
-
assert.deepEqual(parsed.specialUse, []);
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it("normalizes case: \\sent is treated the same as \\Sent", () => {
|
|
47
|
-
const parsed = parseImapAttributes(["\\sent"]);
|
|
48
|
-
assert.deepEqual(parsed.specialUse, [MailboxSpecialUse.Sent]);
|
|
49
|
-
});
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
describe("MailboxSyncService.syncMailboxes — UIDVALIDITY cursor detection (#1272)", () => {
|
|
53
|
-
const namespaces: ImapNamespaces = {
|
|
54
|
-
personal: [{ prefix: "", delimiter: "/" }],
|
|
55
|
-
other: [],
|
|
56
|
-
shared: [],
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
const buildConnection = (
|
|
60
|
-
uidValidity: number,
|
|
61
|
-
highestModseq = "0",
|
|
62
|
-
): IImapConnection =>
|
|
63
|
-
({
|
|
64
|
-
getNamespaces: async () => namespaces,
|
|
65
|
-
listMailboxes: async () => [
|
|
66
|
-
{
|
|
67
|
-
fullPath: "INBOX",
|
|
68
|
-
name: "INBOX",
|
|
69
|
-
delimiter: "/",
|
|
70
|
-
attributes: [],
|
|
71
|
-
parentPath: null,
|
|
72
|
-
},
|
|
73
|
-
],
|
|
74
|
-
getMailboxStatus: async () => ({
|
|
75
|
-
messages: 5,
|
|
76
|
-
recent: 0,
|
|
77
|
-
unseen: 1,
|
|
78
|
-
uidNext: 100,
|
|
79
|
-
uidValidity,
|
|
80
|
-
highestModseq,
|
|
81
|
-
deletedCount: 0,
|
|
82
|
-
}),
|
|
83
|
-
}) as unknown as IImapConnection;
|
|
84
|
-
|
|
85
|
-
const buildServices = (
|
|
86
|
-
existingUidValidity: number,
|
|
87
|
-
existingCursorState?: string,
|
|
88
|
-
) => {
|
|
89
|
-
const updateCalls: Array<Record<string, unknown>> = [];
|
|
90
|
-
const mailboxService = {
|
|
91
|
-
listByAccount: async () => ({
|
|
92
|
-
items: [
|
|
93
|
-
{
|
|
94
|
-
mailboxId: "mbx-1",
|
|
95
|
-
fullPath: "INBOX",
|
|
96
|
-
uidNext: 100,
|
|
97
|
-
uidValidity: existingUidValidity,
|
|
98
|
-
messageCount: 5,
|
|
99
|
-
unseenCount: 1,
|
|
100
|
-
deletedCount: 0,
|
|
101
|
-
highestModseq: "0",
|
|
102
|
-
specialUse: undefined,
|
|
103
|
-
cursorState: existingCursorState,
|
|
104
|
-
},
|
|
105
|
-
],
|
|
106
|
-
continuationToken: undefined,
|
|
107
|
-
}),
|
|
108
|
-
update: async (
|
|
109
|
-
_accountId: string,
|
|
110
|
-
_mailboxId: string,
|
|
111
|
-
patch: Record<string, unknown>,
|
|
112
|
-
) => {
|
|
113
|
-
updateCalls.push(patch);
|
|
114
|
-
return {};
|
|
115
|
-
},
|
|
116
|
-
delete: async () => undefined,
|
|
117
|
-
create: async () => ({
|
|
118
|
-
outcome: "Created",
|
|
119
|
-
mailbox: { mailboxId: "new" },
|
|
120
|
-
}),
|
|
121
|
-
} as unknown as IMailboxRepository;
|
|
122
|
-
|
|
123
|
-
const specialUseService = {
|
|
124
|
-
listByMailboxId: async () => [],
|
|
125
|
-
deleteByMailboxId: async () => undefined,
|
|
126
|
-
createMany: async () => undefined,
|
|
127
|
-
} as unknown as IMailboxSpecialUseRepository;
|
|
128
|
-
|
|
129
|
-
return { mailboxService, specialUseService, updateCalls };
|
|
130
|
-
};
|
|
131
|
-
|
|
132
|
-
it("trips cursor_invalid when the STATUS sweep observes a changed UIDVALIDITY", async () => {
|
|
133
|
-
const { mailboxService, specialUseService, updateCalls } = buildServices(1);
|
|
134
|
-
const service = new MailboxSyncService(
|
|
135
|
-
mailboxService,
|
|
136
|
-
specialUseService,
|
|
137
|
-
silentLogger,
|
|
138
|
-
);
|
|
139
|
-
const connection = buildConnection(2);
|
|
140
|
-
|
|
141
|
-
await service.syncMailboxes({ accountId: "acc-1" }, connection);
|
|
142
|
-
|
|
143
|
-
const uidValidityUpdate = updateCalls.find((c) => "uidValidity" in c);
|
|
144
|
-
assert.ok(uidValidityUpdate, "expected the sweep to write the mailbox");
|
|
145
|
-
assert.equal(
|
|
146
|
-
uidValidityUpdate?.cursorState,
|
|
147
|
-
MailboxCursorState.cursor_invalid,
|
|
148
|
-
);
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
it("does not write anything when UIDVALIDITY (and everything else) is unchanged", async () => {
|
|
152
|
-
const { mailboxService, specialUseService, updateCalls } = buildServices(1);
|
|
153
|
-
const service = new MailboxSyncService(
|
|
154
|
-
mailboxService,
|
|
155
|
-
specialUseService,
|
|
156
|
-
silentLogger,
|
|
157
|
-
);
|
|
158
|
-
const connection = buildConnection(1);
|
|
159
|
-
|
|
160
|
-
await service.syncMailboxes({ accountId: "acc-1" }, connection);
|
|
161
|
-
|
|
162
|
-
assert.equal(updateCalls.length, 0);
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
it("never writes the message-sync cursor, whatever the server reports", async () => {
|
|
166
|
-
// `highestModseq` on the mailbox row is message sync's own cursor over
|
|
167
|
-
// applied changes, not a status projection. The sweep overwriting it with
|
|
168
|
-
// the server's current value would step it over every change message sync
|
|
169
|
-
// had not yet applied.
|
|
170
|
-
const { mailboxService, specialUseService, updateCalls } = buildServices(1);
|
|
171
|
-
const service = new MailboxSyncService(
|
|
172
|
-
mailboxService,
|
|
173
|
-
specialUseService,
|
|
174
|
-
silentLogger,
|
|
175
|
-
);
|
|
176
|
-
const connection = buildConnection(2, "99999");
|
|
177
|
-
|
|
178
|
-
await service.syncMailboxes({ accountId: "acc-1" }, connection);
|
|
179
|
-
|
|
180
|
-
for (const call of updateCalls) {
|
|
181
|
-
assert.equal("highestModseq" in call, false);
|
|
182
|
-
}
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
it("does not sweep-write a mailbox whose only difference is the server mod-sequence", async () => {
|
|
186
|
-
const { mailboxService, specialUseService, updateCalls } = buildServices(1);
|
|
187
|
-
const service = new MailboxSyncService(
|
|
188
|
-
mailboxService,
|
|
189
|
-
specialUseService,
|
|
190
|
-
silentLogger,
|
|
191
|
-
);
|
|
192
|
-
const connection = buildConnection(1, "4242");
|
|
193
|
-
|
|
194
|
-
await service.syncMailboxes({ accountId: "acc-1" }, connection);
|
|
195
|
-
|
|
196
|
-
assert.equal(updateCalls.length, 0);
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
it("does not re-trip (no cursorState write) when the mailbox is already cursor_invalid", async () => {
|
|
200
|
-
const { mailboxService, specialUseService, updateCalls } = buildServices(
|
|
201
|
-
1,
|
|
202
|
-
MailboxCursorState.cursor_invalid,
|
|
203
|
-
);
|
|
204
|
-
const service = new MailboxSyncService(
|
|
205
|
-
mailboxService,
|
|
206
|
-
specialUseService,
|
|
207
|
-
silentLogger,
|
|
208
|
-
);
|
|
209
|
-
const connection = buildConnection(2);
|
|
210
|
-
|
|
211
|
-
await service.syncMailboxes({ accountId: "acc-1" }, connection);
|
|
212
|
-
|
|
213
|
-
const uidValidityUpdate = updateCalls.find((c) => "uidValidity" in c);
|
|
214
|
-
assert.ok(uidValidityUpdate);
|
|
215
|
-
assert.equal("cursorState" in (uidValidityUpdate ?? {}), false);
|
|
216
|
-
});
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
describe("MailboxSyncService.syncMailboxes — reconcile does not delete pending folders (#290)", () => {
|
|
220
|
-
const namespaces: ImapNamespaces = {
|
|
221
|
-
personal: [{ prefix: "", delimiter: "/" }],
|
|
222
|
-
other: [],
|
|
223
|
-
shared: [],
|
|
224
|
-
};
|
|
225
|
-
|
|
226
|
-
// The server lists only INBOX unless a test says otherwise: neither user
|
|
227
|
-
// folder is on it yet.
|
|
228
|
-
const serverConnection = (
|
|
229
|
-
folders: Array<{ fullPath: string; attributes?: string[] }> = [
|
|
230
|
-
{ fullPath: "INBOX" },
|
|
231
|
-
],
|
|
232
|
-
): IImapConnection =>
|
|
233
|
-
({
|
|
234
|
-
getNamespaces: async () => namespaces,
|
|
235
|
-
listMailboxes: async () =>
|
|
236
|
-
folders.map((folder) => ({
|
|
237
|
-
fullPath: folder.fullPath,
|
|
238
|
-
name: folder.fullPath.split("/").pop() ?? folder.fullPath,
|
|
239
|
-
delimiter: "/",
|
|
240
|
-
attributes: folder.attributes ?? [],
|
|
241
|
-
parentPath: null,
|
|
242
|
-
})),
|
|
243
|
-
getMailboxStatus: async () => ({
|
|
244
|
-
messages: 0,
|
|
245
|
-
recent: 0,
|
|
246
|
-
unseen: 0,
|
|
247
|
-
uidNext: 1,
|
|
248
|
-
uidValidity: 1,
|
|
249
|
-
highestModseq: "0",
|
|
250
|
-
deletedCount: 0,
|
|
251
|
-
}),
|
|
252
|
-
openBox: async () => ({
|
|
253
|
-
uidvalidity: 1,
|
|
254
|
-
uidnext: 1,
|
|
255
|
-
messages: { total: 0 },
|
|
256
|
-
}),
|
|
257
|
-
closeBox: async () => undefined,
|
|
258
|
-
}) as unknown as IImapConnection;
|
|
259
|
-
|
|
260
|
-
const buildServices = (
|
|
261
|
-
existing: Array<{
|
|
262
|
-
mailboxId: string;
|
|
263
|
-
fullPath: string;
|
|
264
|
-
syncStatus?: string;
|
|
265
|
-
pendingPath?: string;
|
|
266
|
-
}>,
|
|
267
|
-
createOutcome: "Created" | "PathTaken" = "Created",
|
|
268
|
-
) => {
|
|
269
|
-
const deleted: string[] = [];
|
|
270
|
-
const created: Array<Record<string, unknown>> = [];
|
|
271
|
-
const mailboxService = {
|
|
272
|
-
listByAccount: async () => ({
|
|
273
|
-
items: existing.map((m) => ({
|
|
274
|
-
mailboxId: m.mailboxId,
|
|
275
|
-
fullPath: m.fullPath,
|
|
276
|
-
uidNext: 1,
|
|
277
|
-
uidValidity: 1,
|
|
278
|
-
messageCount: 0,
|
|
279
|
-
unseenCount: 0,
|
|
280
|
-
deletedCount: 0,
|
|
281
|
-
highestModseq: "0",
|
|
282
|
-
specialUse: undefined,
|
|
283
|
-
syncStatus: m.syncStatus,
|
|
284
|
-
pendingPath: m.pendingPath,
|
|
285
|
-
})),
|
|
286
|
-
continuationToken: undefined,
|
|
287
|
-
}),
|
|
288
|
-
update: async () => ({}),
|
|
289
|
-
delete: async (_accountId: string, mailboxId: string) => {
|
|
290
|
-
deleted.push(mailboxId);
|
|
291
|
-
},
|
|
292
|
-
create: async (input: Record<string, unknown>) => {
|
|
293
|
-
created.push(input);
|
|
294
|
-
if (createOutcome === "PathTaken") return { outcome: "PathTaken" };
|
|
295
|
-
return {
|
|
296
|
-
outcome: "Created",
|
|
297
|
-
mailbox: { ...input, mailboxId: `new-${created.length}` },
|
|
298
|
-
};
|
|
299
|
-
},
|
|
300
|
-
} as unknown as IMailboxRepository;
|
|
301
|
-
|
|
302
|
-
const specialUseService = {
|
|
303
|
-
listByMailboxId: async () => [],
|
|
304
|
-
deleteByMailboxId: async () => undefined,
|
|
305
|
-
createMany: async () => undefined,
|
|
306
|
-
} as unknown as IMailboxSpecialUseRepository;
|
|
307
|
-
|
|
308
|
-
return { mailboxService, specialUseService, deleted, created };
|
|
309
|
-
};
|
|
310
|
-
|
|
311
|
-
for (const syncStatus of [
|
|
312
|
-
MailboxSyncStatus.pending,
|
|
313
|
-
MailboxSyncStatus.deleting,
|
|
314
|
-
]) {
|
|
315
|
-
it(`keeps a \`${syncStatus}\` folder the server has not listed`, async () => {
|
|
316
|
-
// `pending`: the folder was just created locally and MAILBOX_CREATE has
|
|
317
|
-
// not reached the server, so the LIST omits it — deleting the row races
|
|
318
|
-
// the create and wedges the account's FIFO group for a visibility
|
|
319
|
-
// window (#290).
|
|
320
|
-
//
|
|
321
|
-
// `deleting`: the delete has landed on the server and the worker has not
|
|
322
|
-
// written back. Hard-deleting the row removes it under the client
|
|
323
|
-
// holding its id — the "Mailbox not found" of #333 — and removes it
|
|
324
|
-
// without the folder's mail, which the worker owns (D8).
|
|
325
|
-
const { mailboxService, specialUseService, deleted } = buildServices([
|
|
326
|
-
{
|
|
327
|
-
mailboxId: "inbox",
|
|
328
|
-
fullPath: "INBOX",
|
|
329
|
-
syncStatus: MailboxSyncStatus.synced,
|
|
330
|
-
},
|
|
331
|
-
{
|
|
332
|
-
mailboxId: "in-flight",
|
|
333
|
-
fullPath: "New Folder",
|
|
334
|
-
syncStatus,
|
|
335
|
-
},
|
|
336
|
-
]);
|
|
337
|
-
const service = new MailboxSyncService(
|
|
338
|
-
mailboxService,
|
|
339
|
-
specialUseService,
|
|
340
|
-
silentLogger,
|
|
341
|
-
);
|
|
342
|
-
|
|
343
|
-
const result = await service.syncMailboxes(
|
|
344
|
-
{ accountId: "acc-1" },
|
|
345
|
-
serverConnection(),
|
|
346
|
-
);
|
|
347
|
-
|
|
348
|
-
assert.deepEqual(deleted, []);
|
|
349
|
-
assert.equal(result.deleted, 0);
|
|
350
|
-
});
|
|
351
|
-
|
|
352
|
-
it(`keeps a \`${syncStatus}\` folder the server lists as \\Noselect`, async () => {
|
|
353
|
-
// A folder mid-create can transiently LIST as `\Noselect`, and the
|
|
354
|
-
// non-selectable branch deleted the row before looking at its state.
|
|
355
|
-
const { mailboxService, specialUseService, deleted } = buildServices([
|
|
356
|
-
{
|
|
357
|
-
mailboxId: "in-flight",
|
|
358
|
-
fullPath: "New Folder",
|
|
359
|
-
syncStatus,
|
|
360
|
-
},
|
|
361
|
-
]);
|
|
362
|
-
const service = new MailboxSyncService(
|
|
363
|
-
mailboxService,
|
|
364
|
-
specialUseService,
|
|
365
|
-
silentLogger,
|
|
366
|
-
);
|
|
367
|
-
|
|
368
|
-
await service.syncMailboxes(
|
|
369
|
-
{ accountId: "acc-1" },
|
|
370
|
-
serverConnection([
|
|
371
|
-
{ fullPath: "INBOX" },
|
|
372
|
-
{ fullPath: "New Folder", attributes: ["\\Noselect"] },
|
|
373
|
-
]),
|
|
374
|
-
);
|
|
375
|
-
|
|
376
|
-
assert.deepEqual(deleted, []);
|
|
377
|
-
});
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
it("does not insert a second row for a path a recorded rename claims", async () => {
|
|
381
|
-
// D14. `fullPath` stays the path the server held when the intent was
|
|
382
|
-
// recorded, so once the server executes RENAME the LIST returns a path no
|
|
383
|
-
// row is keyed to. Without the pendingPath index the insert branch makes a
|
|
384
|
-
// second row under a fresh mailboxId and initial-syncs the folder's mail
|
|
385
|
-
// into it, leaving the original as a permanent phantom.
|
|
386
|
-
const { mailboxService, specialUseService, deleted, created } =
|
|
387
|
-
buildServices([
|
|
388
|
-
{
|
|
389
|
-
mailboxId: "inbox",
|
|
390
|
-
fullPath: "INBOX",
|
|
391
|
-
syncStatus: MailboxSyncStatus.synced,
|
|
392
|
-
},
|
|
393
|
-
{
|
|
394
|
-
mailboxId: "renaming",
|
|
395
|
-
fullPath: "Archive/2025",
|
|
396
|
-
syncStatus: MailboxSyncStatus.pending,
|
|
397
|
-
pendingPath: "Records/2025",
|
|
398
|
-
},
|
|
399
|
-
]);
|
|
400
|
-
const service = new MailboxSyncService(
|
|
401
|
-
mailboxService,
|
|
402
|
-
specialUseService,
|
|
403
|
-
silentLogger,
|
|
404
|
-
);
|
|
405
|
-
|
|
406
|
-
const result = await service.syncMailboxes(
|
|
407
|
-
{ accountId: "acc-1" },
|
|
408
|
-
serverConnection([{ fullPath: "INBOX" }, { fullPath: "Records/2025" }]),
|
|
409
|
-
);
|
|
410
|
-
|
|
411
|
-
assert.deepEqual(created, []);
|
|
412
|
-
assert.equal(result.created, 0);
|
|
413
|
-
assert.deepEqual(deleted, []);
|
|
414
|
-
});
|
|
415
|
-
|
|
416
|
-
it("counts no insert and keeps the account sweep going when the path is already taken", async () => {
|
|
417
|
-
const { mailboxService, specialUseService, created } = buildServices(
|
|
418
|
-
[
|
|
419
|
-
{
|
|
420
|
-
mailboxId: "inbox",
|
|
421
|
-
fullPath: "INBOX",
|
|
422
|
-
syncStatus: MailboxSyncStatus.synced,
|
|
423
|
-
},
|
|
424
|
-
],
|
|
425
|
-
"PathTaken",
|
|
426
|
-
);
|
|
427
|
-
const errors: Array<Record<string, unknown>> = [];
|
|
428
|
-
const service = new MailboxSyncService(mailboxService, specialUseService, {
|
|
429
|
-
...silentLogger,
|
|
430
|
-
error: (obj) => {
|
|
431
|
-
errors.push(obj);
|
|
432
|
-
},
|
|
433
|
-
});
|
|
434
|
-
|
|
435
|
-
const result = await service.syncMailboxes(
|
|
436
|
-
{ accountId: "acc-1" },
|
|
437
|
-
serverConnection([{ fullPath: "INBOX" }, { fullPath: "Projects" }]),
|
|
438
|
-
);
|
|
439
|
-
|
|
440
|
-
assert.equal(created.length, 1);
|
|
441
|
-
assert.equal(result.created, 0);
|
|
442
|
-
assert.deepEqual(errors, [{ accountId: "acc-1", fullPath: "Projects" }]);
|
|
443
|
-
});
|
|
444
|
-
|
|
445
|
-
it("lets a failed rename's target be discovered as a folder of its own", async () => {
|
|
446
|
-
// A failed rename keeps its target so the UI can name what it was aiming
|
|
447
|
-
// at (T6), and nothing clears it until the user retries or dismisses. If
|
|
448
|
-
// the guard honoured that claim, one stuck folder would block the sweep
|
|
449
|
-
// from ever seeing a real folder another client later created at that
|
|
450
|
-
// path — permanently, and silently.
|
|
451
|
-
const { mailboxService, specialUseService, created } = buildServices([
|
|
452
|
-
{
|
|
453
|
-
mailboxId: "stuck",
|
|
454
|
-
fullPath: "Archive/2025",
|
|
455
|
-
syncStatus: MailboxSyncStatus.failed,
|
|
456
|
-
pendingPath: "Records/2025",
|
|
457
|
-
},
|
|
458
|
-
]);
|
|
459
|
-
const service = new MailboxSyncService(
|
|
460
|
-
mailboxService,
|
|
461
|
-
specialUseService,
|
|
462
|
-
silentLogger,
|
|
463
|
-
);
|
|
464
|
-
|
|
465
|
-
await service.syncMailboxes(
|
|
466
|
-
{ accountId: "acc-1" },
|
|
467
|
-
serverConnection([
|
|
468
|
-
{ fullPath: "INBOX" },
|
|
469
|
-
{ fullPath: "Archive/2025" },
|
|
470
|
-
{ fullPath: "Records/2025" },
|
|
471
|
-
]),
|
|
472
|
-
);
|
|
473
|
-
|
|
474
|
-
assert.deepEqual(
|
|
475
|
-
created.map((input) => input.fullPath),
|
|
476
|
-
["INBOX", "Records/2025"],
|
|
477
|
-
);
|
|
478
|
-
});
|
|
479
|
-
|
|
480
|
-
it("writes syncStatus synced on the row it discovers", async () => {
|
|
481
|
-
const { mailboxService, specialUseService, created } = buildServices([]);
|
|
482
|
-
const service = new MailboxSyncService(
|
|
483
|
-
mailboxService,
|
|
484
|
-
specialUseService,
|
|
485
|
-
silentLogger,
|
|
486
|
-
);
|
|
487
|
-
|
|
488
|
-
await service.syncMailboxes(
|
|
489
|
-
{ accountId: "acc-1" },
|
|
490
|
-
serverConnection([{ fullPath: "INBOX" }]),
|
|
491
|
-
);
|
|
492
|
-
|
|
493
|
-
assert.equal(created.length, 1);
|
|
494
|
-
assert.equal(created[0].syncStatus, MailboxSyncStatus.synced);
|
|
495
|
-
});
|
|
496
|
-
|
|
497
|
-
it("still deletes a synced folder that has left the server", async () => {
|
|
498
|
-
// A folder that was confirmed on the server and is now gone is a genuine
|
|
499
|
-
// server-side deletion; the reconcile must still reap it.
|
|
500
|
-
const { mailboxService, specialUseService, deleted } = buildServices([
|
|
501
|
-
{
|
|
502
|
-
mailboxId: "inbox",
|
|
503
|
-
fullPath: "INBOX",
|
|
504
|
-
syncStatus: MailboxSyncStatus.synced,
|
|
505
|
-
},
|
|
506
|
-
{
|
|
507
|
-
mailboxId: "synced-gone",
|
|
508
|
-
fullPath: "Old Folder",
|
|
509
|
-
syncStatus: MailboxSyncStatus.synced,
|
|
510
|
-
},
|
|
511
|
-
]);
|
|
512
|
-
const service = new MailboxSyncService(
|
|
513
|
-
mailboxService,
|
|
514
|
-
specialUseService,
|
|
515
|
-
silentLogger,
|
|
516
|
-
);
|
|
517
|
-
|
|
518
|
-
const result = await service.syncMailboxes(
|
|
519
|
-
{ accountId: "acc-1" },
|
|
520
|
-
serverConnection(),
|
|
521
|
-
);
|
|
522
|
-
|
|
523
|
-
assert.deepEqual(deleted, ["synced-gone"]);
|
|
524
|
-
assert.equal(result.deleted, 1);
|
|
525
|
-
});
|
|
526
|
-
});
|
|
527
|
-
|
|
528
|
-
describe("MailboxSyncService.syncMailboxes — a lookalike is not a folder the user keeps mail in (#837)", () => {
|
|
529
|
-
const namespaces: ImapNamespaces = {
|
|
530
|
-
personal: [{ prefix: "", delimiter: "/" }],
|
|
531
|
-
other: [],
|
|
532
|
-
shared: [],
|
|
533
|
-
};
|
|
534
|
-
|
|
535
|
-
const serverConnection = (
|
|
536
|
-
folders: Array<{ fullPath: string; attributes: string[] }>,
|
|
537
|
-
): IImapConnection =>
|
|
538
|
-
({
|
|
539
|
-
getNamespaces: async () => namespaces,
|
|
540
|
-
listMailboxes: async () =>
|
|
541
|
-
folders.map((folder) => ({
|
|
542
|
-
fullPath: folder.fullPath,
|
|
543
|
-
name: folder.fullPath.split("/").pop() ?? folder.fullPath,
|
|
544
|
-
delimiter: "/",
|
|
545
|
-
attributes: folder.attributes,
|
|
546
|
-
parentPath: null,
|
|
547
|
-
})),
|
|
548
|
-
getMailboxStatus: async () => ({
|
|
549
|
-
messages: 0,
|
|
550
|
-
recent: 0,
|
|
551
|
-
unseen: 0,
|
|
552
|
-
uidNext: 1,
|
|
553
|
-
uidValidity: 1,
|
|
554
|
-
highestModseq: "0",
|
|
555
|
-
deletedCount: 0,
|
|
556
|
-
}),
|
|
557
|
-
}) as unknown as IImapConnection;
|
|
558
|
-
|
|
559
|
-
const buildServices = (
|
|
560
|
-
existing: Array<{ mailboxId: string; fullPath: string }>,
|
|
561
|
-
) => {
|
|
562
|
-
const deleted: string[] = [];
|
|
563
|
-
const mailboxService = {
|
|
564
|
-
listByAccount: async () => ({
|
|
565
|
-
items: existing.map((m) => ({
|
|
566
|
-
mailboxId: m.mailboxId,
|
|
567
|
-
fullPath: m.fullPath,
|
|
568
|
-
uidNext: 1,
|
|
569
|
-
uidValidity: 1,
|
|
570
|
-
messageCount: 0,
|
|
571
|
-
unseenCount: 0,
|
|
572
|
-
deletedCount: 0,
|
|
573
|
-
highestModseq: "0",
|
|
574
|
-
specialUse: undefined,
|
|
575
|
-
syncStatus: MailboxSyncStatus.synced,
|
|
576
|
-
})),
|
|
577
|
-
continuationToken: undefined,
|
|
578
|
-
}),
|
|
579
|
-
update: async () => ({}),
|
|
580
|
-
delete: async (_accountId: string, mailboxId: string) => {
|
|
581
|
-
deleted.push(mailboxId);
|
|
582
|
-
},
|
|
583
|
-
create: async () => ({
|
|
584
|
-
outcome: "Created",
|
|
585
|
-
mailbox: { mailboxId: "new" },
|
|
586
|
-
}),
|
|
587
|
-
} as unknown as IMailboxRepository;
|
|
588
|
-
|
|
589
|
-
const specialUseService = {
|
|
590
|
-
listByMailboxId: async () => [],
|
|
591
|
-
deleteByMailboxId: async () => undefined,
|
|
592
|
-
createMany: async () => undefined,
|
|
593
|
-
} as unknown as IMailboxSpecialUseRepository;
|
|
594
|
-
|
|
595
|
-
return { mailboxService, specialUseService, deleted };
|
|
596
|
-
};
|
|
597
|
-
|
|
598
|
-
const syncWith = async (
|
|
599
|
-
folders: Array<{ fullPath: string; attributes: string[] }>,
|
|
600
|
-
existing: Array<{ mailboxId: string; fullPath: string }>,
|
|
601
|
-
): Promise<string[]> => {
|
|
602
|
-
const { mailboxService, specialUseService, deleted } =
|
|
603
|
-
buildServices(existing);
|
|
604
|
-
const service = new MailboxSyncService(
|
|
605
|
-
mailboxService,
|
|
606
|
-
specialUseService,
|
|
607
|
-
silentLogger,
|
|
608
|
-
);
|
|
609
|
-
await service.syncMailboxes(
|
|
610
|
-
{ accountId: "acc-1" },
|
|
611
|
-
serverConnection(folders),
|
|
612
|
-
);
|
|
613
|
-
return deleted;
|
|
614
|
-
};
|
|
615
|
-
|
|
616
|
-
it("keeps a folder called Deleted beside the folder the server flagged", async () => {
|
|
617
|
-
// `deleted` and `bin` were Trash names in this file's own copy of the hint
|
|
618
|
-
// table long after #843 dropped them from the shared one. Every sync of an
|
|
619
|
-
// account holding a flagged Trash and a user folder called `Deleted` took
|
|
620
|
-
// the user's folder, and its mail, out of the client.
|
|
621
|
-
const deleted = await syncWith(
|
|
622
|
-
[
|
|
623
|
-
{ fullPath: "INBOX", attributes: [] },
|
|
624
|
-
{ fullPath: "Trash", attributes: ["\\Trash"] },
|
|
625
|
-
{ fullPath: "Deleted", attributes: [] },
|
|
626
|
-
{ fullPath: "Bin", attributes: [] },
|
|
627
|
-
],
|
|
628
|
-
[
|
|
629
|
-
{ mailboxId: "inbox", fullPath: "INBOX" },
|
|
630
|
-
{ mailboxId: "trash", fullPath: "Trash" },
|
|
631
|
-
{ mailboxId: "keepsakes", fullPath: "Deleted" },
|
|
632
|
-
{ mailboxId: "bin", fullPath: "Bin" },
|
|
633
|
-
],
|
|
634
|
-
);
|
|
635
|
-
|
|
636
|
-
assert.deepEqual(deleted, []);
|
|
637
|
-
});
|
|
638
|
-
|
|
639
|
-
it("still hides the unflagged twin of a folder the server flagged", async () => {
|
|
640
|
-
const deleted = await syncWith(
|
|
641
|
-
[
|
|
642
|
-
{ fullPath: "INBOX", attributes: [] },
|
|
643
|
-
{ fullPath: "[Gmail]/Trash", attributes: ["\\Trash"] },
|
|
644
|
-
{ fullPath: "Trash", attributes: [] },
|
|
645
|
-
],
|
|
646
|
-
[
|
|
647
|
-
{ mailboxId: "inbox", fullPath: "INBOX" },
|
|
648
|
-
{ mailboxId: "gmail-trash", fullPath: "[Gmail]/Trash" },
|
|
649
|
-
{ mailboxId: "lookalike", fullPath: "Trash" },
|
|
650
|
-
],
|
|
651
|
-
);
|
|
652
|
-
|
|
653
|
-
assert.deepEqual(deleted, ["lookalike"]);
|
|
654
|
-
});
|
|
655
|
-
|
|
656
|
-
it("keeps a folder that holds a role of its own", async () => {
|
|
657
|
-
// `all mail` is a conventional name for Archive as well as for All, and the
|
|
658
|
-
// account already has a flagged Archive. The folder holds the All role, so
|
|
659
|
-
// it is a folder in its own right rather than a lookalike of Archive.
|
|
660
|
-
const deleted = await syncWith(
|
|
661
|
-
[
|
|
662
|
-
{ fullPath: "INBOX", attributes: [] },
|
|
663
|
-
{ fullPath: "Archive", attributes: ["\\Archive"] },
|
|
664
|
-
{ fullPath: "INBOX/All Mail", attributes: [] },
|
|
665
|
-
],
|
|
666
|
-
[
|
|
667
|
-
{ mailboxId: "inbox", fullPath: "INBOX" },
|
|
668
|
-
{ mailboxId: "archive", fullPath: "Archive" },
|
|
669
|
-
{ mailboxId: "all-mail", fullPath: "INBOX/All Mail" },
|
|
670
|
-
],
|
|
671
|
-
);
|
|
672
|
-
|
|
673
|
-
assert.deepEqual(deleted, []);
|
|
674
|
-
});
|
|
675
|
-
});
|
|
676
|
-
|
|
677
|
-
/**
|
|
678
|
-
* The folder set can also change under a running sweep: a delete asked for while
|
|
679
|
-
* the account is enumerating lands between the LIST and one folder's STATUS.
|
|
680
|
-
* Failing the whole account's enumeration over that one folder stalls every
|
|
681
|
-
* later sync for the account, on a per-account FIFO queue, for the queue's whole
|
|
682
|
-
* visibility window (issue #339).
|
|
683
|
-
*/
|
|
684
|
-
describe("MailboxSyncService.syncMailboxes — a folder leaving mid-sweep (#339)", () => {
|
|
685
|
-
const namespaces: ImapNamespaces = {
|
|
686
|
-
personal: [{ prefix: "", delimiter: "/" }],
|
|
687
|
-
other: [],
|
|
688
|
-
shared: [],
|
|
689
|
-
};
|
|
690
|
-
|
|
691
|
-
const status = () => ({
|
|
692
|
-
messages: 3,
|
|
693
|
-
recent: 0,
|
|
694
|
-
unseen: 0,
|
|
695
|
-
uidNext: 100,
|
|
696
|
-
uidValidity: 1,
|
|
697
|
-
highestModseq: "0",
|
|
698
|
-
deletedCount: 0,
|
|
699
|
-
});
|
|
700
|
-
|
|
701
|
-
const buildConnection = (
|
|
702
|
-
statusFor: (path: string) => Promise<ReturnType<typeof status>>,
|
|
703
|
-
): { connection: IImapConnection; statusPaths: string[] } => {
|
|
704
|
-
const statusPaths: string[] = [];
|
|
705
|
-
const connection = {
|
|
706
|
-
getNamespaces: async () => namespaces,
|
|
707
|
-
listMailboxes: async () =>
|
|
708
|
-
["INBOX", "Doomed"].map((fullPath) => ({
|
|
709
|
-
fullPath,
|
|
710
|
-
name: fullPath,
|
|
711
|
-
delimiter: "/",
|
|
712
|
-
attributes: [],
|
|
713
|
-
parentPath: null,
|
|
714
|
-
})),
|
|
715
|
-
getMailboxStatus: async (path: string) => {
|
|
716
|
-
statusPaths.push(path);
|
|
717
|
-
return statusFor(path);
|
|
718
|
-
},
|
|
719
|
-
} as unknown as IImapConnection;
|
|
720
|
-
return { connection, statusPaths };
|
|
721
|
-
};
|
|
722
|
-
|
|
723
|
-
const buildServices = (options: {
|
|
724
|
-
doomedSyncStatus?: string;
|
|
725
|
-
getDoomed?: () => Promise<unknown>;
|
|
726
|
-
}) => {
|
|
727
|
-
const updatedIds: string[] = [];
|
|
728
|
-
const row = (mailboxId: string, fullPath: string, syncStatus?: string) => ({
|
|
729
|
-
mailboxId,
|
|
730
|
-
fullPath,
|
|
731
|
-
uidNext: 1,
|
|
732
|
-
uidValidity: 1,
|
|
733
|
-
messageCount: 0,
|
|
734
|
-
unseenCount: 0,
|
|
735
|
-
deletedCount: 0,
|
|
736
|
-
highestModseq: "0",
|
|
737
|
-
specialUse: undefined,
|
|
738
|
-
syncStatus,
|
|
739
|
-
});
|
|
740
|
-
|
|
741
|
-
const mailboxService = {
|
|
742
|
-
listByAccount: async () => ({
|
|
743
|
-
items: [
|
|
744
|
-
row("mbx-inbox", "INBOX", MailboxSyncStatus.synced),
|
|
745
|
-
row("mbx-doomed", "Doomed", options.doomedSyncStatus),
|
|
746
|
-
],
|
|
747
|
-
continuationToken: undefined,
|
|
748
|
-
}),
|
|
749
|
-
get: async (_accountId: string, mailboxId: string) => {
|
|
750
|
-
if (mailboxId === "mbx-doomed" && options.getDoomed) {
|
|
751
|
-
return options.getDoomed();
|
|
752
|
-
}
|
|
753
|
-
return row(mailboxId, mailboxId, MailboxSyncStatus.synced);
|
|
754
|
-
},
|
|
755
|
-
update: async (
|
|
756
|
-
_accountId: string,
|
|
757
|
-
mailboxId: string,
|
|
758
|
-
_patch: Record<string, unknown>,
|
|
759
|
-
) => {
|
|
760
|
-
updatedIds.push(mailboxId);
|
|
761
|
-
return {};
|
|
762
|
-
},
|
|
763
|
-
delete: async () => undefined,
|
|
764
|
-
create: async () => ({
|
|
765
|
-
outcome: "Created",
|
|
766
|
-
mailbox: { mailboxId: "new" },
|
|
767
|
-
}),
|
|
768
|
-
} as unknown as IMailboxRepository;
|
|
769
|
-
|
|
770
|
-
const specialUseService = {
|
|
771
|
-
listByMailboxId: async () => [],
|
|
772
|
-
deleteByMailboxId: async () => undefined,
|
|
773
|
-
createMany: async () => undefined,
|
|
774
|
-
} as unknown as IMailboxSpecialUseRepository;
|
|
775
|
-
|
|
776
|
-
return { mailboxService, specialUseService, updatedIds };
|
|
777
|
-
};
|
|
778
|
-
|
|
779
|
-
for (const syncStatus of [
|
|
780
|
-
MailboxSyncStatus.pending,
|
|
781
|
-
MailboxSyncStatus.deleting,
|
|
782
|
-
]) {
|
|
783
|
-
it(`leaves a \`${syncStatus}\` folder untouched — no STATUS, no write`, async () => {
|
|
784
|
-
const { mailboxService, specialUseService, updatedIds } = buildServices({
|
|
785
|
-
doomedSyncStatus: syncStatus,
|
|
786
|
-
});
|
|
787
|
-
const { connection, statusPaths } = buildConnection(async () => status());
|
|
788
|
-
const service = new MailboxSyncService(
|
|
789
|
-
mailboxService,
|
|
790
|
-
specialUseService,
|
|
791
|
-
silentLogger,
|
|
792
|
-
);
|
|
793
|
-
|
|
794
|
-
await service.syncMailboxes({ accountId: "acc-1" }, connection);
|
|
795
|
-
|
|
796
|
-
assert.deepEqual(statusPaths, ["INBOX"]);
|
|
797
|
-
assert.deepEqual(updatedIds, ["mbx-inbox"]);
|
|
798
|
-
});
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
it("finishes the sweep when a folder's STATUS fails and that folder has since been deleted", async () => {
|
|
802
|
-
const { mailboxService, specialUseService, updatedIds } = buildServices({
|
|
803
|
-
doomedSyncStatus: MailboxSyncStatus.synced,
|
|
804
|
-
getDoomed: async () => {
|
|
805
|
-
throw new NotFoundError("Mailbox not found: mbx-doomed");
|
|
806
|
-
},
|
|
807
|
-
});
|
|
808
|
-
const { connection } = buildConnection(async (path) => {
|
|
809
|
-
if (path === "Doomed") throw new Error("Mailbox doesn't exist: Doomed");
|
|
810
|
-
return status();
|
|
811
|
-
});
|
|
812
|
-
const service = new MailboxSyncService(
|
|
813
|
-
mailboxService,
|
|
814
|
-
specialUseService,
|
|
815
|
-
silentLogger,
|
|
816
|
-
);
|
|
817
|
-
|
|
818
|
-
await assert.doesNotReject(
|
|
819
|
-
service.syncMailboxes({ accountId: "acc-1" }, connection),
|
|
820
|
-
);
|
|
821
|
-
assert.deepEqual(updatedIds, ["mbx-inbox"]);
|
|
822
|
-
});
|
|
823
|
-
|
|
824
|
-
it("fails the sweep when a folder's STATUS fails and the folder is still live", async () => {
|
|
825
|
-
const { mailboxService, specialUseService } = buildServices({
|
|
826
|
-
doomedSyncStatus: MailboxSyncStatus.synced,
|
|
827
|
-
getDoomed: async () => ({
|
|
828
|
-
mailboxId: "mbx-doomed",
|
|
829
|
-
fullPath: "Doomed",
|
|
830
|
-
syncStatus: MailboxSyncStatus.synced,
|
|
831
|
-
}),
|
|
832
|
-
});
|
|
833
|
-
const { connection } = buildConnection(async (path) => {
|
|
834
|
-
if (path === "Doomed") throw new Error("connection reset by peer");
|
|
835
|
-
return status();
|
|
836
|
-
});
|
|
837
|
-
const service = new MailboxSyncService(
|
|
838
|
-
mailboxService,
|
|
839
|
-
specialUseService,
|
|
840
|
-
silentLogger,
|
|
841
|
-
);
|
|
842
|
-
|
|
843
|
-
await assert.rejects(
|
|
844
|
-
service.syncMailboxes({ accountId: "acc-1" }, connection),
|
|
845
|
-
/connection reset by peer/,
|
|
846
|
-
);
|
|
847
|
-
});
|
|
848
|
-
});
|