@remit/backend 0.0.143 → 0.0.144
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/dev-server/content-handler.d.ts +38 -0
- package/dist/dev-server/content-handler.js +46 -0
- package/dist/src/api.d.ts +7 -0
- package/dist/src/api.js +144 -0
- package/dist/src/auth.d.ts +30 -0
- package/dist/src/auth.js +73 -0
- package/dist/src/config/msoauth.d.ts +30 -0
- package/dist/src/config/msoauth.js +30 -0
- package/{src/data-backend.ts → dist/src/data-backend.d.ts} +2 -4
- package/dist/src/data-backend.js +12 -0
- package/{src/derive/autoMoved.ts → dist/src/derive/autoMoved.d.ts} +1 -29
- package/dist/src/derive/autoMoved.js +49 -0
- package/dist/src/derive/contentSignature.d.ts +41 -0
- package/{src/derive/contentSignature.ts → dist/src/derive/contentSignature.js} +27 -72
- package/dist/src/derive/contentUrl.d.ts +46 -0
- package/dist/src/derive/contentUrl.js +46 -0
- package/{src/derive/deriveMuted.ts → dist/src/derive/deriveMuted.d.ts} +1 -3
- package/dist/src/derive/deriveMuted.js +7 -0
- package/dist/src/derive/enrichThreadRows.d.ts +118 -0
- package/dist/src/derive/enrichThreadRows.js +151 -0
- package/dist/src/derive/filterThreadCriteria.d.ts +23 -0
- package/dist/src/derive/filterThreadCriteria.js +24 -0
- package/{src/derive/pendingMoveCounts.ts → dist/src/derive/pendingMoveCounts.d.ts} +5 -53
- package/dist/src/derive/pendingMoveCounts.js +53 -0
- package/{src/derive/senderTrust.ts → dist/src/derive/senderTrust.d.ts} +1 -9
- package/dist/src/derive/senderTrust.js +19 -0
- package/dist/src/error.d.ts +21 -0
- package/dist/src/error.js +85 -0
- package/dist/src/handlers/account-guards.d.ts +36 -0
- package/dist/src/handlers/account-guards.js +107 -0
- package/dist/src/handlers/account-oauth.d.ts +14 -0
- package/dist/src/handlers/account-oauth.js +387 -0
- package/dist/src/handlers/account-overrides.d.ts +53 -0
- package/dist/src/handlers/account-overrides.js +233 -0
- package/dist/src/handlers/account-signature.d.ts +28 -0
- package/dist/src/handlers/account-signature.js +85 -0
- package/dist/src/handlers/account.d.ts +6 -0
- package/dist/src/handlers/account.js +350 -0
- package/dist/src/handlers/address.d.ts +35 -0
- package/dist/src/handlers/address.js +141 -0
- package/dist/src/handlers/calendar-event.d.ts +70 -0
- package/dist/src/handlers/calendar-event.js +425 -0
- package/dist/src/handlers/calendar-feed.d.ts +47 -0
- package/dist/src/handlers/calendar-feed.js +192 -0
- package/dist/src/handlers/calendar-suggestion.d.ts +47 -0
- package/dist/src/handlers/calendar-suggestion.js +212 -0
- package/dist/src/handlers/calendar.d.ts +116 -0
- package/dist/src/handlers/calendar.js +330 -0
- package/dist/src/handlers/config.d.ts +43 -0
- package/dist/src/handlers/config.js +176 -0
- package/dist/src/handlers/ensure-account-config.d.ts +9 -0
- package/dist/src/handlers/ensure-account-config.js +27 -0
- package/dist/src/handlers/filter.d.ts +3 -0
- package/dist/src/handlers/filter.js +103 -0
- package/dist/src/handlers/folder-role-appointments.d.ts +68 -0
- package/dist/src/handlers/folder-role-appointments.js +168 -0
- package/dist/src/handlers/folder-role-labels.d.ts +26 -0
- package/dist/src/handlers/folder-role-labels.js +40 -0
- package/dist/src/handlers/folder-role.d.ts +9 -0
- package/dist/src/handlers/folder-role.js +40 -0
- package/dist/src/handlers/index.d.ts +2 -0
- package/dist/src/handlers/index.js +60 -0
- package/dist/src/handlers/label.d.ts +43 -0
- package/dist/src/handlers/label.js +114 -0
- package/dist/src/handlers/mailbox-bindings.d.ts +41 -0
- package/dist/src/handlers/mailbox-bindings.js +61 -0
- package/dist/src/handlers/mailbox.d.ts +23 -0
- package/dist/src/handlers/mailbox.js +219 -0
- package/dist/src/handlers/me.d.ts +2 -0
- package/dist/src/handlers/me.js +138 -0
- package/dist/src/handlers/message.d.ts +84 -0
- package/dist/src/handlers/message.js +607 -0
- package/dist/src/handlers/organize.d.ts +10 -0
- package/dist/src/handlers/organize.js +112 -0
- package/dist/src/handlers/outbox-attachment.d.ts +23 -0
- package/dist/src/handlers/outbox-attachment.js +81 -0
- package/dist/src/handlers/outbox.d.ts +4 -0
- package/dist/src/handlers/outbox.js +144 -0
- package/dist/src/handlers/sync-progress.d.ts +46 -0
- package/dist/src/handlers/sync-progress.js +63 -0
- package/dist/src/handlers/sync.d.ts +28 -0
- package/dist/src/handlers/sync.js +82 -0
- package/dist/src/handlers/system-update.d.ts +2 -0
- package/dist/src/handlers/system-update.js +172 -0
- package/dist/src/handlers/thread.d.ts +148 -0
- package/dist/src/handlers/thread.js +233 -0
- package/dist/src/handlers/unified-threads.d.ts +247 -0
- package/dist/src/handlers/unified-threads.js +409 -0
- package/{src/handlers/vip-suggestions.ts → dist/src/handlers/vip-suggestions.d.ts} +1 -13
- package/dist/src/handlers/vip-suggestions.js +16 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +37 -0
- package/{src/json.ts → dist/src/json.d.ts} +1 -4
- package/dist/src/json.js +10 -0
- package/dist/src/jwt-auth.d.ts +18 -0
- package/dist/src/jwt-auth.js +97 -0
- package/dist/src/request-context.d.ts +9 -0
- package/dist/src/request-context.js +22 -0
- package/dist/src/request.d.ts +3 -0
- package/dist/src/request.js +7 -0
- package/dist/src/response.d.ts +27 -0
- package/dist/src/response.js +87 -0
- package/dist/src/service/account-deletion.d.ts +22 -0
- package/dist/src/service/account-deletion.js +39 -0
- package/dist/src/service/compose-sqlite.d.ts +2 -0
- package/dist/src/service/compose-sqlite.js +11 -0
- package/dist/src/service/create-remit-client.d.ts +95 -0
- package/dist/src/service/create-remit-client.js +207 -0
- package/dist/src/service/data-client.d.ts +12 -0
- package/dist/src/service/data-client.js +44 -0
- package/dist/src/service/export-identity.d.ts +3 -0
- package/dist/src/service/export-identity.js +35 -0
- package/dist/src/service/fire-and-forget.d.ts +54 -0
- package/dist/src/service/fire-and-forget.js +38 -0
- package/dist/src/service/organize.d.ts +184 -0
- package/dist/src/service/organize.js +384 -0
- package/dist/src/service/refusal.d.ts +4 -0
- package/dist/src/service/refusal.js +20 -0
- package/dist/src/service/sender-category.d.ts +51 -0
- package/{src/service/sender-category.ts → dist/src/service/sender-category.js} +40 -81
- package/dist/src/service/sqs.d.ts +1 -0
- package/{src/service/sqs.ts → dist/src/service/sqs.js} +0 -1
- package/dist/src/service/trigger-sync.d.ts +51 -0
- package/dist/src/service/trigger-sync.js +39 -0
- package/dist/src/types.d.ts +49 -0
- package/dist/src/types.js +1 -0
- package/package.json +113 -98
- package/.env.test +0 -4
- package/dev-server/content-auth.test.ts +0 -113
- package/dev-server/content-auth.ts +0 -54
- package/dev-server/content-handler.test.ts +0 -105
- package/dev-server/content-handler.ts +0 -79
- package/dev-server/content-path.test.ts +0 -37
- package/dev-server/content-path.ts +0 -27
- package/dev-server/cors.test.ts +0 -48
- package/dev-server/cors.ts +0 -25
- package/dev-server/keep-alive.test.ts +0 -24
- package/dev-server/lambda-helpers.ts +0 -69
- package/dev-server/log-output.test.ts +0 -173
- package/dev-server/oauth-callback-route.test.ts +0 -66
- package/dev-server/relational-health.test.ts +0 -83
- package/dev-server/relational-health.ts +0 -39
- package/dev-server/server.ts +0 -384
- package/dev-server/sync-age.test.ts +0 -91
- package/dev-server/sync-age.ts +0 -55
- package/dev-server/upload-handler.ts +0 -132
- package/dev-server/upload-route.test.ts +0 -196
- package/scripts/backfill-classification.ts +0 -91
- package/scripts/backfill-list-id.ts +0 -83
- package/scripts/config-save.ts +0 -128
- package/src/auth.test.ts +0 -12
- package/src/auth.ts +0 -92
- package/src/config/msoauth.test.ts +0 -48
- package/src/config/msoauth.ts +0 -48
- package/src/data-backend.test.ts +0 -23
- package/src/derive/autoMoved.test.ts +0 -104
- package/src/derive/contentSignature.test.ts +0 -161
- package/src/derive/contentUrl.test.ts +0 -172
- package/src/derive/contentUrl.ts +0 -86
- package/src/derive/deriveMuted.test.ts +0 -34
- package/src/derive/enrichThreadRows.test.ts +0 -360
- package/src/derive/enrichThreadRows.ts +0 -234
- package/src/derive/filterThreadCriteria.test.ts +0 -126
- package/src/derive/filterThreadCriteria.ts +0 -51
- package/src/derive/senderTrust.test.ts +0 -67
- package/src/error.test.ts +0 -201
- package/src/error.ts +0 -123
- package/src/handlers/account-guards.test.ts +0 -67
- package/src/handlers/account-guards.ts +0 -169
- package/src/handlers/account-oauth-callback.test.ts +0 -155
- package/src/handlers/account-oauth-consent.test.ts +0 -336
- package/src/handlers/account-oauth-start.test.ts +0 -191
- package/src/handlers/account-oauth.test.ts +0 -439
- package/src/handlers/account-oauth.ts +0 -635
- package/src/handlers/account-overrides.test.ts +0 -88
- package/src/handlers/account-overrides.ts +0 -323
- package/src/handlers/account-ownership.ts +0 -25
- package/src/handlers/account-signature.ts +0 -117
- package/src/handlers/account-synced-services.test.ts +0 -245
- package/src/handlers/account.ts +0 -552
- package/src/handlers/address.test.ts +0 -381
- package/src/handlers/address.ts +0 -200
- package/src/handlers/calendar-event.test.ts +0 -685
- package/src/handlers/calendar-event.ts +0 -738
- package/src/handlers/calendar-feed-validators.test.ts +0 -228
- package/src/handlers/calendar-feed.test.ts +0 -439
- package/src/handlers/calendar-feed.ts +0 -291
- package/src/handlers/calendar-sqlite-fixture.ts +0 -36
- package/src/handlers/calendar-suggestion.test.ts +0 -1269
- package/src/handlers/calendar-suggestion.ts +0 -425
- package/src/handlers/calendar.test.ts +0 -1080
- package/src/handlers/calendar.ts +0 -517
- package/src/handlers/config.test.ts +0 -245
- package/src/handlers/config.ts +0 -332
- package/src/handlers/ensure-account-config.ts +0 -30
- package/src/handlers/filter.test.ts +0 -644
- package/src/handlers/filter.ts +0 -518
- package/src/handlers/folder-role-appointments.test.ts +0 -438
- package/src/handlers/folder-role-appointments.ts +0 -249
- package/src/handlers/folder-role-labels.ts +0 -63
- package/src/handlers/folder-role.test.ts +0 -209
- package/src/handlers/folder-role.ts +0 -80
- package/src/handlers/index.ts +0 -83
- package/src/handlers/label.test.ts +0 -112
- package/src/handlers/label.ts +0 -210
- package/src/handlers/mailbox-bindings.ts +0 -111
- package/src/handlers/mailbox-rename-refusal.test.ts +0 -247
- package/src/handlers/mailbox-settled-target.test.ts +0 -251
- package/src/handlers/mailbox.test.ts +0 -280
- package/src/handlers/mailbox.ts +0 -581
- package/src/handlers/me.test.ts +0 -81
- package/src/handlers/me.ts +0 -203
- package/src/handlers/message-read-backfill.test.ts +0 -103
- package/src/handlers/message.test.ts +0 -131
- package/src/handlers/message.ts +0 -1131
- package/src/handlers/organize.test.ts +0 -277
- package/src/handlers/organize.ts +0 -197
- package/src/handlers/outbox-attachment.test.ts +0 -765
- package/src/handlers/outbox-attachment.ts +0 -126
- package/src/handlers/outbox-list.test.ts +0 -179
- package/src/handlers/outbox.test.ts +0 -772
- package/src/handlers/outbox.ts +0 -258
- package/src/handlers/search.test.ts +0 -161
- package/src/handlers/search.ts +0 -105
- package/src/handlers/sync-progress.test.ts +0 -205
- package/src/handlers/sync-progress.ts +0 -99
- package/src/handlers/sync.test.ts +0 -168
- package/src/handlers/sync.ts +0 -118
- package/src/handlers/system-update.test.ts +0 -497
- package/src/handlers/system-update.ts +0 -230
- package/src/handlers/thread.test.ts +0 -396
- package/src/handlers/thread.ts +0 -427
- package/src/handlers/unified-threads-mail-sources.sqlite.test.ts +0 -197
- package/src/handlers/unified-threads.test.ts +0 -785
- package/src/handlers/unified-threads.ts +0 -669
- package/src/index.test.ts +0 -83
- package/src/index.ts +0 -228
- package/src/jwt-auth.test.ts +0 -152
- package/src/jwt-auth.ts +0 -132
- package/src/log-redaction.test.ts +0 -144
- package/src/openapi-spec.test.ts +0 -118
- package/src/request-context.test.ts +0 -61
- package/src/request-context.ts +0 -33
- package/src/request-pipeline.test.ts +0 -88
- package/src/request.ts +0 -10
- package/src/response.test.ts +0 -130
- package/src/response.ts +0 -144
- package/src/service/compose-sqlite.ts +0 -98
- package/src/service/config-import.ts +0 -39
- package/src/service/create-remit-client.ts +0 -522
- package/src/service/data-client.test.ts +0 -97
- package/src/service/data-client.ts +0 -58
- package/src/service/export-identity.ts +0 -39
- package/src/service/filter.ts +0 -40
- package/src/service/fire-and-forget.test.ts +0 -126
- package/src/service/fire-and-forget.ts +0 -79
- package/src/service/localhost-env-config.test.ts +0 -36
- package/src/service/organize-queue.ts +0 -98
- package/src/service/organize.test.ts +0 -1451
- package/src/service/organize.ts +0 -1025
- package/src/service/read-path-filter-config.test.ts +0 -243
- package/src/service/semantic-capability.test.ts +0 -78
- package/src/service/semantic-capability.ts +0 -95
- package/src/service/sender-category.test.ts +0 -235
- package/src/service/trigger-sync.test.ts +0 -205
- package/src/service/trigger-sync.ts +0 -100
- package/src/stdout-capture-fixture.ts +0 -40
- package/src/types.ts +0 -271
- package/tsconfig.json +0 -7
|
@@ -1,438 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import { describe, it } from "node:test";
|
|
5
|
-
import { fileURLToPath } from "node:url";
|
|
6
|
-
import type { AccountSettingItem } from "@remit/data-ports";
|
|
7
|
-
import type { RoleMailboxCandidate } from "@remit/data-ports/folder-role";
|
|
8
|
-
import {
|
|
9
|
-
CanonicalMailboxRole,
|
|
10
|
-
FolderAppointmentSource,
|
|
11
|
-
MailboxSpecialUse,
|
|
12
|
-
} from "@remit/domain-enums";
|
|
13
|
-
import {
|
|
14
|
-
CANONICAL_ROLES,
|
|
15
|
-
groupFolderAppointmentsByAccount,
|
|
16
|
-
loadFolderAppointmentsForAccount,
|
|
17
|
-
type PersistedFolderAppointment,
|
|
18
|
-
resolveFolderAppointments,
|
|
19
|
-
writeFolderRoleAppointment,
|
|
20
|
-
} from "./folder-role-appointments.js";
|
|
21
|
-
|
|
22
|
-
const setting = (name: string, value: string): AccountSettingItem =>
|
|
23
|
-
({
|
|
24
|
-
accountSettingId: `s-${name}`,
|
|
25
|
-
accountConfigId: "cfg-1",
|
|
26
|
-
name,
|
|
27
|
-
value: { kind: "String", value },
|
|
28
|
-
createdAt: 0,
|
|
29
|
-
updatedAt: 0,
|
|
30
|
-
}) as AccountSettingItem;
|
|
31
|
-
|
|
32
|
-
const appointed = (
|
|
33
|
-
mailboxId: string,
|
|
34
|
-
lastKnownPath?: string,
|
|
35
|
-
): Map<string, PersistedFolderAppointment> =>
|
|
36
|
-
new Map([[CanonicalMailboxRole.Trash, { mailboxId, lastKnownPath }]]);
|
|
37
|
-
|
|
38
|
-
describe("CANONICAL_ROLES", () => {
|
|
39
|
-
it("carries every RFC 032 anchor role, Custom excluded", () => {
|
|
40
|
-
assert.deepEqual(
|
|
41
|
-
[...CANONICAL_ROLES].sort(),
|
|
42
|
-
[
|
|
43
|
-
CanonicalMailboxRole.Inbox,
|
|
44
|
-
CanonicalMailboxRole.Drafts,
|
|
45
|
-
CanonicalMailboxRole.Sent,
|
|
46
|
-
CanonicalMailboxRole.Archive,
|
|
47
|
-
CanonicalMailboxRole.Junk,
|
|
48
|
-
CanonicalMailboxRole.Trash,
|
|
49
|
-
CanonicalMailboxRole.All,
|
|
50
|
-
CanonicalMailboxRole.Flagged,
|
|
51
|
-
].sort(),
|
|
52
|
-
);
|
|
53
|
-
});
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
describe("resolveFolderAppointments", () => {
|
|
57
|
-
const folders: RoleMailboxCandidate[] = [
|
|
58
|
-
{ mailboxId: "mb-inbox", fullPath: "INBOX", hierarchyDelimiter: "/" },
|
|
59
|
-
{
|
|
60
|
-
mailboxId: "mb-concepten",
|
|
61
|
-
fullPath: "INBOX/Concepten",
|
|
62
|
-
hierarchyDelimiter: "/",
|
|
63
|
-
},
|
|
64
|
-
{
|
|
65
|
-
mailboxId: "mb-spam",
|
|
66
|
-
fullPath: "INBOX/Spam",
|
|
67
|
-
hierarchyDelimiter: "/",
|
|
68
|
-
specialUse: [MailboxSpecialUse.Junk],
|
|
69
|
-
},
|
|
70
|
-
];
|
|
71
|
-
|
|
72
|
-
const roleIn = (
|
|
73
|
-
result: ReturnType<typeof resolveFolderAppointments>,
|
|
74
|
-
role: string,
|
|
75
|
-
) => result.find((entry) => entry.role === role);
|
|
76
|
-
|
|
77
|
-
it("carries one entry per canonical role, even when unfilled", () => {
|
|
78
|
-
const result = resolveFolderAppointments(new Map(), folders);
|
|
79
|
-
assert.deepEqual(
|
|
80
|
-
result.map((r) => r.role).sort(),
|
|
81
|
-
[...CANONICAL_ROLES].sort(),
|
|
82
|
-
);
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
it("gives every role a source, so no surface has to infer one", () => {
|
|
86
|
-
const result = resolveFolderAppointments(new Map(), folders);
|
|
87
|
-
assert.equal(
|
|
88
|
-
result.every((entry) => entry.source !== undefined),
|
|
89
|
-
true,
|
|
90
|
-
);
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
it("tells a folder the user chose from one reader guessed by name", () => {
|
|
94
|
-
const persisted = new Map<string, PersistedFolderAppointment>([
|
|
95
|
-
[CanonicalMailboxRole.Drafts, { mailboxId: "mb-concepten" }],
|
|
96
|
-
]);
|
|
97
|
-
const result = resolveFolderAppointments(persisted, folders);
|
|
98
|
-
const drafts = roleIn(result, CanonicalMailboxRole.Drafts);
|
|
99
|
-
assert.equal(drafts?.mailboxId, "mb-concepten");
|
|
100
|
-
assert.equal(drafts?.source, FolderAppointmentSource.Appointed);
|
|
101
|
-
|
|
102
|
-
const proposed = resolveFolderAppointments(new Map(), folders);
|
|
103
|
-
const guess = roleIn(proposed, CanonicalMailboxRole.Drafts);
|
|
104
|
-
assert.equal(guess?.mailboxId, "mb-concepten");
|
|
105
|
-
assert.equal(guess?.source, FolderAppointmentSource.Proposed);
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
it("marks the server's own flag and the reserved INBOX name as such", () => {
|
|
109
|
-
const result = resolveFolderAppointments(new Map(), folders);
|
|
110
|
-
assert.equal(
|
|
111
|
-
roleIn(result, CanonicalMailboxRole.Junk)?.source,
|
|
112
|
-
FolderAppointmentSource.Flagged,
|
|
113
|
-
);
|
|
114
|
-
assert.equal(
|
|
115
|
-
roleIn(result, CanonicalMailboxRole.Inbox)?.source,
|
|
116
|
-
FolderAppointmentSource.Reserved,
|
|
117
|
-
);
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
it("surfaces a choice whose folder is gone as broken, not as absent", () => {
|
|
121
|
-
const persisted = new Map<string, PersistedFolderAppointment>([
|
|
122
|
-
[
|
|
123
|
-
CanonicalMailboxRole.Junk,
|
|
124
|
-
{ mailboxId: "mb-deleted-long-ago", lastKnownPath: "INBOX/Rommel" },
|
|
125
|
-
],
|
|
126
|
-
]);
|
|
127
|
-
const junk = roleIn(
|
|
128
|
-
resolveFolderAppointments(persisted, folders),
|
|
129
|
-
CanonicalMailboxRole.Junk,
|
|
130
|
-
);
|
|
131
|
-
assert.equal(junk?.source, FolderAppointmentSource.Stale);
|
|
132
|
-
assert.equal(junk?.staleAppointmentMailboxId, "mb-deleted-long-ago");
|
|
133
|
-
assert.equal(junk?.staleAppointmentPath, "INBOX/Rommel");
|
|
134
|
-
assert.equal(junk?.mailboxId, "mb-spam");
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
it("leaves a role unfilled when nothing persisted or proposed matches", () => {
|
|
138
|
-
const archive = roleIn(
|
|
139
|
-
resolveFolderAppointments(new Map(), folders),
|
|
140
|
-
CanonicalMailboxRole.Archive,
|
|
141
|
-
);
|
|
142
|
-
assert.equal(archive?.mailboxId, undefined);
|
|
143
|
-
assert.equal(archive?.source, FolderAppointmentSource.None);
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
it("never reads the recorded path as evidence for the role", () => {
|
|
147
|
-
const trash = roleIn(
|
|
148
|
-
resolveFolderAppointments(appointed("mb-inbox", "Prullenbak"), folders),
|
|
149
|
-
CanonicalMailboxRole.Trash,
|
|
150
|
-
);
|
|
151
|
-
assert.equal(trash?.mailboxId, "mb-inbox");
|
|
152
|
-
assert.equal(trash?.staleAppointmentPath, undefined);
|
|
153
|
-
});
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
describe("groupFolderAppointmentsByAccount", () => {
|
|
157
|
-
it("groups by accountId then role from the composite setting name", () => {
|
|
158
|
-
const settings = [
|
|
159
|
-
setting(
|
|
160
|
-
`FolderRoleAppointment#acc-1#${CanonicalMailboxRole.Drafts}`,
|
|
161
|
-
"mb-1",
|
|
162
|
-
),
|
|
163
|
-
setting(
|
|
164
|
-
`FolderRoleAppointment#acc-1#${CanonicalMailboxRole.Sent}`,
|
|
165
|
-
"mb-2",
|
|
166
|
-
),
|
|
167
|
-
setting(
|
|
168
|
-
`FolderRoleAppointment#acc-2#${CanonicalMailboxRole.Inbox}`,
|
|
169
|
-
"mb-3",
|
|
170
|
-
),
|
|
171
|
-
setting("AccountDisplayName#acc-1", "Alice"),
|
|
172
|
-
];
|
|
173
|
-
const grouped = groupFolderAppointmentsByAccount(settings);
|
|
174
|
-
assert.deepEqual(Object.fromEntries(grouped.get("acc-1") ?? []), {
|
|
175
|
-
[CanonicalMailboxRole.Drafts]: { mailboxId: "mb-1" },
|
|
176
|
-
[CanonicalMailboxRole.Sent]: { mailboxId: "mb-2" },
|
|
177
|
-
});
|
|
178
|
-
assert.deepEqual(Object.fromEntries(grouped.get("acc-2") ?? []), {
|
|
179
|
-
[CanonicalMailboxRole.Inbox]: { mailboxId: "mb-3" },
|
|
180
|
-
});
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
it("attaches the recorded path to the appointment it belongs to", () => {
|
|
184
|
-
const settings = [
|
|
185
|
-
setting(
|
|
186
|
-
`FolderRoleAppointment#acc-1#${CanonicalMailboxRole.Trash}`,
|
|
187
|
-
"mb-1",
|
|
188
|
-
),
|
|
189
|
-
setting(
|
|
190
|
-
`FolderRoleAppointmentLabel#acc-1#${CanonicalMailboxRole.Trash}`,
|
|
191
|
-
"INBOX/Bak",
|
|
192
|
-
),
|
|
193
|
-
setting(
|
|
194
|
-
`FolderRoleAppointmentLabel#acc-1#${CanonicalMailboxRole.Sent}`,
|
|
195
|
-
"INBOX/Verzonden",
|
|
196
|
-
),
|
|
197
|
-
];
|
|
198
|
-
const grouped = groupFolderAppointmentsByAccount(settings);
|
|
199
|
-
assert.deepEqual(Object.fromEntries(grouped.get("acc-1") ?? []), {
|
|
200
|
-
[CanonicalMailboxRole.Trash]: {
|
|
201
|
-
mailboxId: "mb-1",
|
|
202
|
-
lastKnownPath: "INBOX/Bak",
|
|
203
|
-
},
|
|
204
|
-
});
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
it("ignores a leftover MailboxRole#<mailboxId> row instead of throwing", () => {
|
|
208
|
-
// The #963/#964 backfill wrote `MailboxRole#<mailboxId>` rows that are no
|
|
209
|
-
// longer written but still persist in production. `FolderRoleAppointment`
|
|
210
|
-
// superseded `MailboxRole` (RFC 032 exclusive-folder-appointment, #976);
|
|
211
|
-
// a leftover row alongside real appointments must not make GET /config
|
|
212
|
-
// throw — it is simply not a folder-role appointment and gets skipped.
|
|
213
|
-
const settings = [
|
|
214
|
-
setting(
|
|
215
|
-
`FolderRoleAppointment#acc-1#${CanonicalMailboxRole.Drafts}`,
|
|
216
|
-
"mb-1",
|
|
217
|
-
),
|
|
218
|
-
setting("MailboxRole#mb-legacy", "custom"),
|
|
219
|
-
];
|
|
220
|
-
const grouped = groupFolderAppointmentsByAccount(settings);
|
|
221
|
-
assert.deepEqual(Object.fromEntries(grouped.get("acc-1") ?? []), {
|
|
222
|
-
[CanonicalMailboxRole.Drafts]: { mailboxId: "mb-1" },
|
|
223
|
-
});
|
|
224
|
-
});
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
describe("loadFolderAppointmentsForAccount", () => {
|
|
228
|
-
const readerOver = (stored: Map<string, string>) => ({
|
|
229
|
-
get: async (_accountConfigId: string, name: string) => {
|
|
230
|
-
const value = stored.get(name);
|
|
231
|
-
return value ? setting(name, value) : null;
|
|
232
|
-
},
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
it("reads each role's row and collects only the ones that exist", async () => {
|
|
236
|
-
const roles = await loadFolderAppointmentsForAccount(
|
|
237
|
-
readerOver(
|
|
238
|
-
new Map([
|
|
239
|
-
[
|
|
240
|
-
`FolderRoleAppointment#acc-1#${CanonicalMailboxRole.Sent}`,
|
|
241
|
-
"mb-sent",
|
|
242
|
-
],
|
|
243
|
-
]),
|
|
244
|
-
),
|
|
245
|
-
"cfg-1",
|
|
246
|
-
"acc-1",
|
|
247
|
-
);
|
|
248
|
-
assert.deepEqual(Object.fromEntries(roles), {
|
|
249
|
-
[CanonicalMailboxRole.Sent]: { mailboxId: "mb-sent" },
|
|
250
|
-
});
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
it("picks up the recorded path beside the appointment", async () => {
|
|
254
|
-
const roles = await loadFolderAppointmentsForAccount(
|
|
255
|
-
readerOver(
|
|
256
|
-
new Map([
|
|
257
|
-
[
|
|
258
|
-
`FolderRoleAppointment#acc-1#${CanonicalMailboxRole.Trash}`,
|
|
259
|
-
"mb-bak",
|
|
260
|
-
],
|
|
261
|
-
[
|
|
262
|
-
`FolderRoleAppointmentLabel#acc-1#${CanonicalMailboxRole.Trash}`,
|
|
263
|
-
"INBOX/Bak",
|
|
264
|
-
],
|
|
265
|
-
]),
|
|
266
|
-
),
|
|
267
|
-
"cfg-1",
|
|
268
|
-
"acc-1",
|
|
269
|
-
);
|
|
270
|
-
assert.deepEqual(roles.get(CanonicalMailboxRole.Trash), {
|
|
271
|
-
mailboxId: "mb-bak",
|
|
272
|
-
lastKnownPath: "INBOX/Bak",
|
|
273
|
-
});
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
it("drops a label left behind by an appointment that was cleared", async () => {
|
|
277
|
-
const roles = await loadFolderAppointmentsForAccount(
|
|
278
|
-
readerOver(
|
|
279
|
-
new Map([
|
|
280
|
-
[
|
|
281
|
-
`FolderRoleAppointmentLabel#acc-1#${CanonicalMailboxRole.Trash}`,
|
|
282
|
-
"INBOX/Bak",
|
|
283
|
-
],
|
|
284
|
-
]),
|
|
285
|
-
),
|
|
286
|
-
"cfg-1",
|
|
287
|
-
"acc-1",
|
|
288
|
-
);
|
|
289
|
-
assert.equal(roles.get(CanonicalMailboxRole.Trash), undefined);
|
|
290
|
-
});
|
|
291
|
-
});
|
|
292
|
-
|
|
293
|
-
describe("writeFolderRoleAppointment", () => {
|
|
294
|
-
const recorder = () => {
|
|
295
|
-
const upserts: unknown[] = [];
|
|
296
|
-
const deletes: unknown[][] = [];
|
|
297
|
-
return {
|
|
298
|
-
upserts,
|
|
299
|
-
deletes,
|
|
300
|
-
accountSetting: {
|
|
301
|
-
upsert: async (input: unknown) => {
|
|
302
|
-
upserts.push(input);
|
|
303
|
-
return input as never;
|
|
304
|
-
},
|
|
305
|
-
delete: async (accountConfigId: string, name: string) => {
|
|
306
|
-
deletes.push([accountConfigId, name]);
|
|
307
|
-
},
|
|
308
|
-
},
|
|
309
|
-
};
|
|
310
|
-
};
|
|
311
|
-
|
|
312
|
-
it("records the path alongside the appointment", async () => {
|
|
313
|
-
const { upserts, deletes, accountSetting } = recorder();
|
|
314
|
-
await writeFolderRoleAppointment(
|
|
315
|
-
accountSetting,
|
|
316
|
-
"cfg-1",
|
|
317
|
-
"acc-1",
|
|
318
|
-
CanonicalMailboxRole.Archive,
|
|
319
|
-
"mb-archive",
|
|
320
|
-
"INBOX/Archief",
|
|
321
|
-
);
|
|
322
|
-
assert.deepEqual(upserts, [
|
|
323
|
-
{
|
|
324
|
-
accountConfigId: "cfg-1",
|
|
325
|
-
name: `FolderRoleAppointment#acc-1#${CanonicalMailboxRole.Archive}`,
|
|
326
|
-
value: { kind: "String", value: "mb-archive" },
|
|
327
|
-
},
|
|
328
|
-
{
|
|
329
|
-
accountConfigId: "cfg-1",
|
|
330
|
-
name: `FolderRoleAppointmentLabel#acc-1#${CanonicalMailboxRole.Archive}`,
|
|
331
|
-
value: { kind: "String", value: "INBOX/Archief" },
|
|
332
|
-
},
|
|
333
|
-
]);
|
|
334
|
-
assert.deepEqual(deletes, []);
|
|
335
|
-
});
|
|
336
|
-
|
|
337
|
-
it("clears a path left over from the previous choice", async () => {
|
|
338
|
-
const { upserts, deletes, accountSetting } = recorder();
|
|
339
|
-
await writeFolderRoleAppointment(
|
|
340
|
-
accountSetting,
|
|
341
|
-
"cfg-1",
|
|
342
|
-
"acc-1",
|
|
343
|
-
CanonicalMailboxRole.Archive,
|
|
344
|
-
"mb-archive",
|
|
345
|
-
);
|
|
346
|
-
assert.equal(upserts.length, 1);
|
|
347
|
-
assert.deepEqual(deletes, [
|
|
348
|
-
[
|
|
349
|
-
"cfg-1",
|
|
350
|
-
`FolderRoleAppointmentLabel#acc-1#${CanonicalMailboxRole.Archive}`,
|
|
351
|
-
],
|
|
352
|
-
]);
|
|
353
|
-
});
|
|
354
|
-
|
|
355
|
-
it("deletes both rows when the appointment is cleared", async () => {
|
|
356
|
-
const { upserts, deletes, accountSetting } = recorder();
|
|
357
|
-
await writeFolderRoleAppointment(
|
|
358
|
-
accountSetting,
|
|
359
|
-
"cfg-1",
|
|
360
|
-
"acc-1",
|
|
361
|
-
CanonicalMailboxRole.Archive,
|
|
362
|
-
null,
|
|
363
|
-
);
|
|
364
|
-
assert.deepEqual(upserts, []);
|
|
365
|
-
assert.deepEqual(deletes, [
|
|
366
|
-
["cfg-1", `FolderRoleAppointment#acc-1#${CanonicalMailboxRole.Archive}`],
|
|
367
|
-
[
|
|
368
|
-
"cfg-1",
|
|
369
|
-
`FolderRoleAppointmentLabel#acc-1#${CanonicalMailboxRole.Archive}`,
|
|
370
|
-
],
|
|
371
|
-
]);
|
|
372
|
-
});
|
|
373
|
-
});
|
|
374
|
-
|
|
375
|
-
/**
|
|
376
|
-
* D1 of the #887 design: a folder-role appointment row means one thing — a
|
|
377
|
-
* person decided. That is what makes an appointment outrank the server's own
|
|
378
|
-
* \Trash flag, and what makes an Empty Trash on one defensible. It holds only
|
|
379
|
-
* while the row has a single author, so the author is pinned here rather than
|
|
380
|
-
* left to review.
|
|
381
|
-
*/
|
|
382
|
-
describe("who may write a folder-role appointment", () => {
|
|
383
|
-
const PACKAGES = fileURLToPath(new URL("../../../", import.meta.url));
|
|
384
|
-
|
|
385
|
-
const sourceFiles = (): string[] => {
|
|
386
|
-
const files: string[] = [];
|
|
387
|
-
for (const pkg of readdirSync(PACKAGES, { withFileTypes: true })) {
|
|
388
|
-
if (!pkg.isDirectory()) continue;
|
|
389
|
-
const src = join(PACKAGES, pkg.name, "src");
|
|
390
|
-
if (!existsSync(src)) continue;
|
|
391
|
-
for (const entry of readdirSync(src, { recursive: true })) {
|
|
392
|
-
const name = String(entry);
|
|
393
|
-
if (!name.endsWith(".ts") && !name.endsWith(".tsx")) continue;
|
|
394
|
-
if (name.endsWith(".test.ts") || name.endsWith(".test.tsx")) continue;
|
|
395
|
-
files.push(`${pkg.name}/src/${name}`);
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
return files;
|
|
399
|
-
};
|
|
400
|
-
|
|
401
|
-
it("is composed in three places, and nowhere else in the repo", () => {
|
|
402
|
-
const referrers = sourceFiles().filter((file) =>
|
|
403
|
-
readFileSync(join(PACKAGES, file), "utf8").includes(
|
|
404
|
-
"composeFolderRoleAppointmentName",
|
|
405
|
-
),
|
|
406
|
-
);
|
|
407
|
-
assert.deepEqual(referrers.sort(), [
|
|
408
|
-
"backend/src/handlers/folder-role-appointments.ts",
|
|
409
|
-
"data-ports/src/folder-role.ts",
|
|
410
|
-
"drizzle-service/src/repos/i4-mailbox-special-use.ts",
|
|
411
|
-
]);
|
|
412
|
-
});
|
|
413
|
-
|
|
414
|
-
it("is written by writeFolderRoleAppointment alone", () => {
|
|
415
|
-
const source = readFileSync(
|
|
416
|
-
fileURLToPath(new URL("./folder-role-appointments.ts", import.meta.url)),
|
|
417
|
-
"utf8",
|
|
418
|
-
);
|
|
419
|
-
const writer = source.slice(
|
|
420
|
-
source.indexOf("export const writeFolderRoleAppointment"),
|
|
421
|
-
);
|
|
422
|
-
const body = writer.slice(0, writer.indexOf("\n};"));
|
|
423
|
-
|
|
424
|
-
const count = (text: string, needle: string) =>
|
|
425
|
-
text.split(needle).length - 1;
|
|
426
|
-
// Without these, renaming the receiver satisfies the guard with 0 === 0.
|
|
427
|
-
assert.ok(count(body, "accountSetting.upsert(") > 0);
|
|
428
|
-
assert.ok(count(body, "accountSetting.delete(") > 0);
|
|
429
|
-
assert.equal(
|
|
430
|
-
count(source, "accountSetting.upsert("),
|
|
431
|
-
count(body, "accountSetting.upsert("),
|
|
432
|
-
);
|
|
433
|
-
assert.equal(
|
|
434
|
-
count(source, "accountSetting.delete("),
|
|
435
|
-
count(body, "accountSetting.delete("),
|
|
436
|
-
);
|
|
437
|
-
});
|
|
438
|
-
});
|
|
@@ -1,249 +0,0 @@
|
|
|
1
|
-
import type { FolderAppointment } from "@remit/api-openapi-types";
|
|
2
|
-
import type {
|
|
3
|
-
AccountSettingItem,
|
|
4
|
-
IAccountSettingRepository,
|
|
5
|
-
IMailboxRepository,
|
|
6
|
-
} from "@remit/data-ports";
|
|
7
|
-
import {
|
|
8
|
-
CANONICAL_ROLES,
|
|
9
|
-
type CanonicalMailboxRoleValue,
|
|
10
|
-
composeFolderRoleAppointmentLabelName,
|
|
11
|
-
composeFolderRoleAppointmentName,
|
|
12
|
-
parseFolderRoleAppointmentLabelName,
|
|
13
|
-
parseFolderRoleAppointmentName,
|
|
14
|
-
type RoleMailboxCandidate,
|
|
15
|
-
type RoleResolution,
|
|
16
|
-
resolveRoleForAccount,
|
|
17
|
-
} from "@remit/data-ports/folder-role";
|
|
18
|
-
import { FolderAppointmentSource } from "@remit/domain-enums";
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* RFC 032 exclusive-folder-appointment (#976): a per-account role→mailbox map.
|
|
22
|
-
* Each row is a `FolderRoleAppointment#<accountId>#<role>` AccountSetting (RFC
|
|
23
|
-
* 032 settings tiers), so a role can never be persisted twice for one account —
|
|
24
|
-
* writing it replaces whichever mailbox previously held it. This module owns
|
|
25
|
-
* that persistence; the read side is `resolveRoleForAccount`
|
|
26
|
-
* (`@remit/data-ports/folder-role`), the same rule every special-folder lookup
|
|
27
|
-
* in the backend and the workers goes through.
|
|
28
|
-
*
|
|
29
|
-
* A second row, `FolderRoleAppointmentLabel#<accountId>#<role>`, records the
|
|
30
|
-
* path the appointed mailbox had at the time (#887), so a folder another client
|
|
31
|
-
* later deletes can still be named back to the user. It is display only: it
|
|
32
|
-
* lives beside the appointment rather than inside it precisely so the
|
|
33
|
-
* repository that resolves roles composes one name and cannot reach the other.
|
|
34
|
-
*/
|
|
35
|
-
|
|
36
|
-
export { CANONICAL_ROLES };
|
|
37
|
-
|
|
38
|
-
/** One role's persisted appointment: the choice, plus the path it was made on. */
|
|
39
|
-
export interface PersistedFolderAppointment {
|
|
40
|
-
mailboxId: string;
|
|
41
|
-
lastKnownPath?: string;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const stringValueOf = (item: AccountSettingItem): string | undefined => {
|
|
45
|
-
const { value } = item;
|
|
46
|
-
return value.kind === "String" ? value.value : undefined;
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Group every persisted folder-role appointment in one config-wide settings
|
|
51
|
-
* read (e.g. GET /config's already-loaded `listByAccountConfig` result) by
|
|
52
|
-
* accountId, then by role. Mirrors `groupAccountOverrides` / `groupMailboxOverrides`.
|
|
53
|
-
*/
|
|
54
|
-
export const groupFolderAppointmentsByAccount = (
|
|
55
|
-
settings: AccountSettingItem[],
|
|
56
|
-
): Map<string, Map<string, PersistedFolderAppointment>> => {
|
|
57
|
-
const byAccount = new Map<string, Map<string, PersistedFolderAppointment>>();
|
|
58
|
-
|
|
59
|
-
for (const setting of settings) {
|
|
60
|
-
const target = parseFolderRoleAppointmentName(setting.name);
|
|
61
|
-
if (!target) continue;
|
|
62
|
-
const mailboxId = stringValueOf(setting);
|
|
63
|
-
if (mailboxId === undefined) continue;
|
|
64
|
-
const roles =
|
|
65
|
-
byAccount.get(target.accountId) ??
|
|
66
|
-
new Map<string, PersistedFolderAppointment>();
|
|
67
|
-
roles.set(target.role, { mailboxId });
|
|
68
|
-
byAccount.set(target.accountId, roles);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
for (const setting of settings) {
|
|
72
|
-
const target = parseFolderRoleAppointmentLabelName(setting.name);
|
|
73
|
-
if (!target) continue;
|
|
74
|
-
const lastKnownPath = stringValueOf(setting);
|
|
75
|
-
if (lastKnownPath === undefined) continue;
|
|
76
|
-
const appointment = byAccount.get(target.accountId)?.get(target.role);
|
|
77
|
-
if (!appointment) continue;
|
|
78
|
-
appointment.lastKnownPath = lastKnownPath;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
return byAccount;
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Load one account's persisted appointments by reading each role's row
|
|
86
|
-
* directly (mirrors `loadMailboxOverrides`'s three-get pattern). Used by the
|
|
87
|
-
* create/update account handlers, which only ever need a single account.
|
|
88
|
-
*/
|
|
89
|
-
export const loadFolderAppointmentsForAccount = async (
|
|
90
|
-
accountSetting: Pick<IAccountSettingRepository, "get">,
|
|
91
|
-
accountConfigId: string,
|
|
92
|
-
accountId: string,
|
|
93
|
-
): Promise<Map<string, PersistedFolderAppointment>> => {
|
|
94
|
-
const entries = await Promise.all(
|
|
95
|
-
CANONICAL_ROLES.map(async (role) => {
|
|
96
|
-
const appointment = await accountSetting.get(
|
|
97
|
-
accountConfigId,
|
|
98
|
-
composeFolderRoleAppointmentName(accountId, role),
|
|
99
|
-
);
|
|
100
|
-
const mailboxId = appointment ? stringValueOf(appointment) : undefined;
|
|
101
|
-
if (mailboxId === undefined) return [role, undefined] as const;
|
|
102
|
-
|
|
103
|
-
const label = await accountSetting.get(
|
|
104
|
-
accountConfigId,
|
|
105
|
-
composeFolderRoleAppointmentLabelName(accountId, role),
|
|
106
|
-
);
|
|
107
|
-
const lastKnownPath = label ? stringValueOf(label) : undefined;
|
|
108
|
-
return [
|
|
109
|
-
role,
|
|
110
|
-
lastKnownPath === undefined
|
|
111
|
-
? { mailboxId }
|
|
112
|
-
: { mailboxId, lastKnownPath },
|
|
113
|
-
] as const;
|
|
114
|
-
}),
|
|
115
|
-
);
|
|
116
|
-
const roles = new Map<string, PersistedFolderAppointment>();
|
|
117
|
-
for (const [role, appointment] of entries) {
|
|
118
|
-
if (appointment) roles.set(role, appointment);
|
|
119
|
-
}
|
|
120
|
-
return roles;
|
|
121
|
-
};
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* Persist (or clear) one role's appointment. `mailboxId: null` deletes the row
|
|
125
|
-
* — the role goes back to unfilled (RFC 032 settings tiers: absence is unset).
|
|
126
|
-
* A value upserts it, replacing whatever mailbox the role pointed at before;
|
|
127
|
-
* there is no second row a duplicate could live in.
|
|
128
|
-
*
|
|
129
|
-
* The label row moves with it, so a path recorded for a previous choice can
|
|
130
|
-
* never be shown against the current one.
|
|
131
|
-
*/
|
|
132
|
-
export const writeFolderRoleAppointment = async (
|
|
133
|
-
accountSetting: Pick<IAccountSettingRepository, "upsert" | "delete">,
|
|
134
|
-
accountConfigId: string,
|
|
135
|
-
accountId: string,
|
|
136
|
-
role: CanonicalMailboxRoleValue,
|
|
137
|
-
mailboxId: string | null,
|
|
138
|
-
lastKnownPath?: string,
|
|
139
|
-
): Promise<void> => {
|
|
140
|
-
const name = composeFolderRoleAppointmentName(accountId, role);
|
|
141
|
-
const labelName = composeFolderRoleAppointmentLabelName(accountId, role);
|
|
142
|
-
|
|
143
|
-
if (mailboxId === null) {
|
|
144
|
-
await accountSetting.delete(accountConfigId, name);
|
|
145
|
-
await accountSetting.delete(accountConfigId, labelName);
|
|
146
|
-
return;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
await accountSetting.upsert({
|
|
150
|
-
accountConfigId,
|
|
151
|
-
name,
|
|
152
|
-
value: { kind: "String", value: mailboxId },
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
if (lastKnownPath === undefined) {
|
|
156
|
-
await accountSetting.delete(accountConfigId, labelName);
|
|
157
|
-
return;
|
|
158
|
-
}
|
|
159
|
-
await accountSetting.upsert({
|
|
160
|
-
accountConfigId,
|
|
161
|
-
name: labelName,
|
|
162
|
-
value: { kind: "String", value: lastKnownPath },
|
|
163
|
-
});
|
|
164
|
-
};
|
|
165
|
-
|
|
166
|
-
const SOURCE_BY_KIND: Record<
|
|
167
|
-
"appointed" | "flagged" | "reserved" | "proposed",
|
|
168
|
-
FolderAppointment["source"]
|
|
169
|
-
> = {
|
|
170
|
-
appointed: FolderAppointmentSource.Appointed,
|
|
171
|
-
flagged: FolderAppointmentSource.Flagged,
|
|
172
|
-
reserved: FolderAppointmentSource.Reserved,
|
|
173
|
-
proposed: FolderAppointmentSource.Proposed,
|
|
174
|
-
};
|
|
175
|
-
|
|
176
|
-
const toFolderAppointment = (
|
|
177
|
-
role: CanonicalMailboxRoleValue,
|
|
178
|
-
resolution: RoleResolution<RoleMailboxCandidate>,
|
|
179
|
-
lastKnownPath: string | undefined,
|
|
180
|
-
): FolderAppointment => {
|
|
181
|
-
if (resolution.kind === "appointment_stale") {
|
|
182
|
-
const { fallback } = resolution;
|
|
183
|
-
return {
|
|
184
|
-
role,
|
|
185
|
-
source: FolderAppointmentSource.Stale,
|
|
186
|
-
mailboxId:
|
|
187
|
-
fallback.kind === "none" ? undefined : fallback.mailbox.mailboxId,
|
|
188
|
-
staleAppointmentMailboxId: resolution.appointedMailboxId,
|
|
189
|
-
staleAppointmentPath: lastKnownPath,
|
|
190
|
-
};
|
|
191
|
-
}
|
|
192
|
-
if (resolution.kind === "none") {
|
|
193
|
-
return { role, source: FolderAppointmentSource.None };
|
|
194
|
-
}
|
|
195
|
-
return {
|
|
196
|
-
role,
|
|
197
|
-
source: SOURCE_BY_KIND[resolution.kind],
|
|
198
|
-
mailboxId: resolution.mailbox.mailboxId,
|
|
199
|
-
};
|
|
200
|
-
};
|
|
201
|
-
|
|
202
|
-
/**
|
|
203
|
-
* Resolve the full appointment set for one account, each entry saying where it
|
|
204
|
-
* came from: the user's persisted choice when set, else the server's
|
|
205
|
-
* SPECIAL-USE flag, else the reserved INBOX name, else a name proposal. A
|
|
206
|
-
* choice naming a mailbox the account no longer holds resolves to whatever the
|
|
207
|
-
* account would have had without it, and says so — the user's decision is
|
|
208
|
-
* surfaced as broken rather than quietly discarded. Always one entry per
|
|
209
|
-
* `CANONICAL_ROLES` member (RFC 032 settings tiers: total, never a sparse
|
|
210
|
-
* array).
|
|
211
|
-
*/
|
|
212
|
-
export const resolveFolderAppointments = (
|
|
213
|
-
persisted: ReadonlyMap<string, PersistedFolderAppointment>,
|
|
214
|
-
mailboxes: readonly RoleMailboxCandidate[],
|
|
215
|
-
): FolderAppointment[] =>
|
|
216
|
-
CANONICAL_ROLES.map((role) => {
|
|
217
|
-
const appointment = persisted.get(role);
|
|
218
|
-
return toFolderAppointment(
|
|
219
|
-
role,
|
|
220
|
-
resolveRoleForAccount(role, mailboxes, appointment?.mailboxId),
|
|
221
|
-
appointment?.lastKnownPath,
|
|
222
|
-
);
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
/**
|
|
226
|
-
* End-to-end resolution for one account: load its persisted appointments and
|
|
227
|
-
* its mailboxes, then resolve. The single entry point every handler that
|
|
228
|
-
* builds an AccountResponse calls (create/update account, GET /config's
|
|
229
|
-
* per-account fan-out mirrors this with a pre-loaded settings batch instead —
|
|
230
|
-
* see `groupFolderAppointmentsByAccount` — to avoid an N+1 settings read).
|
|
231
|
-
*/
|
|
232
|
-
export const resolveAccountFolderAppointments = async (
|
|
233
|
-
client: {
|
|
234
|
-
mailbox: Pick<IMailboxRepository, "listAllByAccount">;
|
|
235
|
-
accountSetting: Pick<IAccountSettingRepository, "get">;
|
|
236
|
-
},
|
|
237
|
-
accountConfigId: string,
|
|
238
|
-
accountId: string,
|
|
239
|
-
): Promise<FolderAppointment[]> => {
|
|
240
|
-
const [persisted, mailboxes] = await Promise.all([
|
|
241
|
-
loadFolderAppointmentsForAccount(
|
|
242
|
-
client.accountSetting,
|
|
243
|
-
accountConfigId,
|
|
244
|
-
accountId,
|
|
245
|
-
),
|
|
246
|
-
client.mailbox.listAllByAccount(accountId),
|
|
247
|
-
]);
|
|
248
|
-
return resolveFolderAppointments(persisted, mailboxes);
|
|
249
|
-
};
|