@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
package/src/handlers/mailbox.ts
DELETED
|
@@ -1,581 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
MailboxResponse,
|
|
3
|
-
RenameMailboxInput,
|
|
4
|
-
} from "@remit/api-openapi-types";
|
|
5
|
-
import type { IAccountSettingRepository, MailboxItem } from "@remit/data-ports";
|
|
6
|
-
import {
|
|
7
|
-
BadRequestError,
|
|
8
|
-
ForbiddenError,
|
|
9
|
-
MailboxNotSettledError,
|
|
10
|
-
NotFoundError,
|
|
11
|
-
} from "@remit/data-ports/errors";
|
|
12
|
-
import { isReservedFolderName } from "@remit/data-ports/folder-role";
|
|
13
|
-
import { mailboxLeafName } from "@remit/data-ports/mailbox-name";
|
|
14
|
-
import { MailboxSyncStatus, MessageSystemFlag } from "@remit/domain-enums";
|
|
15
|
-
import type { APIGatewayProxyEvent } from "aws-lambda";
|
|
16
|
-
import { getAccountConfigIdFromEvent } from "../auth.js";
|
|
17
|
-
import {
|
|
18
|
-
applyPendingMoveCountPrediction,
|
|
19
|
-
type PendingUnseenFlagPush,
|
|
20
|
-
} from "../derive/pendingMoveCounts.js";
|
|
21
|
-
import { getClient } from "../service/data-client.js";
|
|
22
|
-
import type {
|
|
23
|
-
MailboxDetailOperationIds,
|
|
24
|
-
MailboxOperationIds,
|
|
25
|
-
OperationHandler,
|
|
26
|
-
TrashOperationIds,
|
|
27
|
-
} from "../types.js";
|
|
28
|
-
import {
|
|
29
|
-
applyMailboxOverrideChanges,
|
|
30
|
-
loadMailboxOverrides,
|
|
31
|
-
loadMailboxOverridesForConfig,
|
|
32
|
-
type MailboxOverrides,
|
|
33
|
-
} from "./account-overrides.js";
|
|
34
|
-
import { assertAccountOwnership } from "./account-ownership.js";
|
|
35
|
-
import { assertNoBindings, loadMailboxBindings } from "./mailbox-bindings.js";
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* The mute flag and the display-name override are user preferences that live
|
|
39
|
-
* in per-mailbox AccountSetting rows (RFC 032), not on the Mailbox entity.
|
|
40
|
-
* Pick only those keys from a PATCH body: each follows the same `null` → remove,
|
|
41
|
-
* value → set, absent/undefined → no-op semantics as UpdateAddressFlagsInput.
|
|
42
|
-
* The canonical role a folder fills is appointed separately — see
|
|
43
|
-
* FolderRoleOperations.appointFolderRole (RFC 032 exclusive-folder-appointment).
|
|
44
|
-
*/
|
|
45
|
-
export const pickMailboxOverrideChanges = (
|
|
46
|
-
body: RenameMailboxInput,
|
|
47
|
-
): {
|
|
48
|
-
displayNameOverride?: string | null;
|
|
49
|
-
muted?: RenameMailboxInput["muted"];
|
|
50
|
-
} => {
|
|
51
|
-
const changes: {
|
|
52
|
-
displayNameOverride?: string | null;
|
|
53
|
-
muted?: RenameMailboxInput["muted"];
|
|
54
|
-
} = {};
|
|
55
|
-
if (Object.hasOwn(body, "displayNameOverride")) {
|
|
56
|
-
changes.displayNameOverride = body.displayNameOverride;
|
|
57
|
-
}
|
|
58
|
-
if (Object.hasOwn(body, "muted")) {
|
|
59
|
-
changes.muted = body.muted;
|
|
60
|
-
}
|
|
61
|
-
return changes;
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Minimal client surface needed to apply a mailbox PATCH. Structurally
|
|
66
|
-
* satisfied by RemitClient; narrowed so tests can stub it.
|
|
67
|
-
*/
|
|
68
|
-
export interface MailboxPatchClient {
|
|
69
|
-
mailbox: {
|
|
70
|
-
get(accountId: string, mailboxId: string): Promise<MailboxItem>;
|
|
71
|
-
listAllByAccount(accountId: string): Promise<MailboxItem[]>;
|
|
72
|
-
};
|
|
73
|
-
mailboxQueue: {
|
|
74
|
-
renameMailbox(
|
|
75
|
-
mailboxId: string,
|
|
76
|
-
newPath: string,
|
|
77
|
-
accountId: string,
|
|
78
|
-
): Promise<MailboxItem>;
|
|
79
|
-
dismissMailboxIntent(
|
|
80
|
-
mailboxId: string,
|
|
81
|
-
accountId: string,
|
|
82
|
-
): Promise<MailboxItem>;
|
|
83
|
-
};
|
|
84
|
-
accountSetting: Pick<IAccountSettingRepository, "get" | "upsert" | "delete">;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Refuse a rename that no retry of the same request could ever satisfy (D4).
|
|
89
|
-
*
|
|
90
|
-
* `INBOX` is refused at both ends: renaming it moves its mail to the new name
|
|
91
|
-
* and leaves an empty INBOX behind, and renaming anything *to* it collides with
|
|
92
|
-
* the one name RFC 3501 reserves (D5).
|
|
93
|
-
*
|
|
94
|
-
* A reserved leaf name is refused for a different reason. The mailbox sweep
|
|
95
|
-
* reads a folder whose leaf name is a role's conventional name but which lacks
|
|
96
|
-
* the server's flag as a duplicate, and deletes its row — under D8, with the
|
|
97
|
-
* folder's mail — as soon as another folder holds the flag. A rename to such a
|
|
98
|
-
* name settles `synced` and is then reaped. Refusing at the API is the same
|
|
99
|
-
* class of check as the rest of D4 and does not require teaching that heuristic
|
|
100
|
-
* about renames.
|
|
101
|
-
*/
|
|
102
|
-
const assertRenameTargetAllowed = async (
|
|
103
|
-
client: Pick<MailboxPatchClient, "mailbox">,
|
|
104
|
-
accountId: string,
|
|
105
|
-
before: MailboxItem,
|
|
106
|
-
target: string,
|
|
107
|
-
): Promise<void> => {
|
|
108
|
-
if (before.fullPath.toUpperCase() === "INBOX") {
|
|
109
|
-
throw new BadRequestError("The inbox can't be renamed.");
|
|
110
|
-
}
|
|
111
|
-
if (target.trim().length === 0) {
|
|
112
|
-
throw new BadRequestError("A folder needs a name.");
|
|
113
|
-
}
|
|
114
|
-
if (target.toUpperCase() === "INBOX") {
|
|
115
|
-
throw new BadRequestError("“INBOX” is reserved for the inbox.");
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
const leaf = mailboxLeafName({
|
|
119
|
-
fullPath: target,
|
|
120
|
-
hierarchyDelimiter: before.hierarchyDelimiter,
|
|
121
|
-
});
|
|
122
|
-
if (isReservedFolderName(leaf)) {
|
|
123
|
-
throw new BadRequestError(
|
|
124
|
-
`“${leaf}” is reserved for a system folder. Pick another name.`,
|
|
125
|
-
);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// A path a rename is on its way to is as taken as one a folder already sits
|
|
129
|
-
// at (D2). Reading `fullPath` alone accepts a second rename onto a target
|
|
130
|
-
// another is already recorded for: both settle, both write the same path,
|
|
131
|
-
// and the sweep then reaps one row — with its mail — and inserts a duplicate
|
|
132
|
-
// for the survivor's path. The account's folders are few and the sweep
|
|
133
|
-
// already reads them all, so one pass answers both questions.
|
|
134
|
-
const folders = await client.mailbox.listAllByAccount(accountId);
|
|
135
|
-
for (const folder of folders) {
|
|
136
|
-
if (folder.mailboxId === before.mailboxId) continue;
|
|
137
|
-
if (folder.fullPath === target) {
|
|
138
|
-
throw new BadRequestError(`A folder named “${target}” is already there.`);
|
|
139
|
-
}
|
|
140
|
-
// `pending` only. A failed rename keeps its target so the client can name
|
|
141
|
-
// what it was aiming at and offer a retry (T6), and nothing clears it
|
|
142
|
-
// until the user retries or dismisses — so honouring a `failed` row's
|
|
143
|
-
// claim would tell everyone else the path is taken by a rename that is
|
|
144
|
-
// not happening, with no way to find that out and nothing to wait for.
|
|
145
|
-
if (
|
|
146
|
-
folder.syncStatus === MailboxSyncStatus.pending &&
|
|
147
|
-
folder.pendingPath === target
|
|
148
|
-
) {
|
|
149
|
-
throw new BadRequestError(
|
|
150
|
-
`“${folder.fullPath}” is already being renamed to “${target}”.`,
|
|
151
|
-
);
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
};
|
|
155
|
-
|
|
156
|
-
/**
|
|
157
|
-
* Apply a mailbox PATCH body: override changes first (mute flag + display-name/
|
|
158
|
-
* role overrides — written to per-mailbox AccountSetting rows, no IMAP
|
|
159
|
-
* machinery), then rename — which triggers the IMAP rename machinery
|
|
160
|
-
* (the recorded subtree intent + MAILBOX_RENAME event) — only when `fullPath`
|
|
161
|
-
* is present. An override-only PATCH therefore never calls
|
|
162
|
-
* `mailboxQueue.renameMailbox`.
|
|
163
|
-
*
|
|
164
|
-
* A `fullPath` equal to the row's confirmed one is not a rename: it is the
|
|
165
|
-
* dismissal of a failed intent (T10, T11), which is how a user keeps the name
|
|
166
|
-
* the folder already has without new API surface. It is checked first, because
|
|
167
|
-
* from `failed` it is the one route out that enqueues nothing.
|
|
168
|
-
*/
|
|
169
|
-
export const applyMailboxPatch = async (
|
|
170
|
-
client: MailboxPatchClient,
|
|
171
|
-
accountConfigId: string,
|
|
172
|
-
mailboxId: string,
|
|
173
|
-
accountId: string,
|
|
174
|
-
body: RenameMailboxInput,
|
|
175
|
-
): Promise<MailboxItem> => {
|
|
176
|
-
const { fullPath } = body;
|
|
177
|
-
|
|
178
|
-
// --- Override settings (mute flag + display-name/role overrides) ---
|
|
179
|
-
// Written to per-mailbox AccountSetting rows (RFC 032) with the same
|
|
180
|
-
// null→remove semantics as UpdateAddressFlagsInput. Applied before (and
|
|
181
|
-
// independent of) any rename so an override-only PATCH never touches the
|
|
182
|
-
// row's mutation state.
|
|
183
|
-
const changes = pickMailboxOverrideChanges(body);
|
|
184
|
-
if (Object.keys(changes).length > 0) {
|
|
185
|
-
await applyMailboxOverrideChanges(
|
|
186
|
-
client.accountSetting,
|
|
187
|
-
accountConfigId,
|
|
188
|
-
mailboxId,
|
|
189
|
-
changes,
|
|
190
|
-
);
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// --- Rename (IMAP machinery) ---
|
|
194
|
-
// Only triggered when fullPath is present.
|
|
195
|
-
if (!fullPath) {
|
|
196
|
-
return client.mailbox.get(accountId, mailboxId);
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
const before = await client.mailbox.get(accountId, mailboxId);
|
|
200
|
-
if (fullPath === before.fullPath) {
|
|
201
|
-
return client.mailboxQueue.dismissMailboxIntent(mailboxId, accountId);
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
await assertRenameTargetAllowed(client, accountId, before, fullPath);
|
|
205
|
-
|
|
206
|
-
// The row keeps the path the server holds until the rename settles (D2), so
|
|
207
|
-
// nothing that names a folder by path moves here — the appointment labels
|
|
208
|
-
// recorded beside each role (#887) included. They move with the settle.
|
|
209
|
-
return client.mailboxQueue.renameMailbox(mailboxId, fullPath, accountId);
|
|
210
|
-
};
|
|
211
|
-
|
|
212
|
-
/**
|
|
213
|
-
* Cross-account guard for a mailbox reached by id under an `accountId` path.
|
|
214
|
-
* `assertAccountOwnership` only proves the caller owns the account in the path;
|
|
215
|
-
* the `mailboxId` in that same path can belong to a different account, so without
|
|
216
|
-
* this check a caller could read or mutate another account's mailbox. Mirrors the
|
|
217
|
-
* read/act split of `assertAccountOwnership`: 404 on a read (no existence leak),
|
|
218
|
-
* 403 on an action.
|
|
219
|
-
*/
|
|
220
|
-
export const assertMailboxInAccount = (
|
|
221
|
-
mailbox: Pick<MailboxItem, "mailboxId" | "accountId">,
|
|
222
|
-
accountId: string,
|
|
223
|
-
mode: "read" | "act",
|
|
224
|
-
): void => {
|
|
225
|
-
if (mailbox.accountId === accountId) return;
|
|
226
|
-
if (mode === "read") {
|
|
227
|
-
throw new NotFoundError(`Mailbox not found: ${mailbox.mailboxId}`);
|
|
228
|
-
}
|
|
229
|
-
throw new ForbiddenError(`Mailbox ${mailbox.mailboxId} not in account`);
|
|
230
|
-
};
|
|
231
|
-
|
|
232
|
-
/**
|
|
233
|
-
* Why a folder cannot be bound to yet, per state — one wording in one place,
|
|
234
|
-
* because the remedy differs and the client has only the sentence to show.
|
|
235
|
-
*/
|
|
236
|
-
const UNSETTLED_REASON: Record<string, (path: string) => string> = {
|
|
237
|
-
[MailboxSyncStatus.pending]: (path) =>
|
|
238
|
-
`“${path}” isn’t ready yet — the mail server hasn’t confirmed it.`,
|
|
239
|
-
[MailboxSyncStatus.deleting]: (path) => `“${path}” is being deleted.`,
|
|
240
|
-
[MailboxSyncStatus.failed]: (path) =>
|
|
241
|
-
`The last change to “${path}” failed. Retry or dismiss it first.`,
|
|
242
|
-
};
|
|
243
|
-
|
|
244
|
-
/**
|
|
245
|
-
* Every API write that binds a durable reference to a mailbox requires that
|
|
246
|
-
* mailbox to be `synced` (D12, first row; imap-mutations R2: wait). A dangling
|
|
247
|
-
* reference is permanent; blocking costs seconds.
|
|
248
|
-
*
|
|
249
|
-
* This is the server-side floor under the client-side wait, and it is not
|
|
250
|
-
* redundant with it: there is more than one client, and a script hitting the
|
|
251
|
-
* API directly gets no wait at all.
|
|
252
|
-
*
|
|
253
|
-
* It is the other half of D16. A binding is only ever created against a settled
|
|
254
|
-
* folder (here) and only ever removed by the user (the delete refusal below),
|
|
255
|
-
* so a `mailboxId` that names nothing never exists — which is what lets every
|
|
256
|
-
* reader of a filter's `actionMailboxId` or a role appointment treat the
|
|
257
|
-
* reference as resolvable, with no missing-target branch.
|
|
258
|
-
*
|
|
259
|
-
* A `\Noselect` container is refused by construction — mailbox-sync keeps no
|
|
260
|
-
* row for one — so there is nothing to bind to. Clearing a reference is not a
|
|
261
|
-
* bind and is never gated, and neither is a move *out of* a folder: a
|
|
262
|
-
* `deleting` folder still holds its mail until the DELETE lands, which is the
|
|
263
|
-
* delete-with-move flow (D13).
|
|
264
|
-
*/
|
|
265
|
-
export const assertMailboxSettled = (
|
|
266
|
-
target: Pick<MailboxItem, "mailboxId" | "fullPath" | "syncStatus">,
|
|
267
|
-
): void => {
|
|
268
|
-
const reason = UNSETTLED_REASON[target.syncStatus];
|
|
269
|
-
if (!reason) return;
|
|
270
|
-
throw new MailboxNotSettledError(
|
|
271
|
-
reason(target.fullPath),
|
|
272
|
-
target.mailboxId,
|
|
273
|
-
target.syncStatus,
|
|
274
|
-
);
|
|
275
|
-
};
|
|
276
|
-
|
|
277
|
-
/**
|
|
278
|
-
* Every pending placement move (issue #1271) for an account, on whichever
|
|
279
|
-
* backend is active (`RemitClient.placementMove` is present on both — see
|
|
280
|
-
* `create-remit-client.ts`). Read-only; feeds
|
|
281
|
-
* `applyPendingMoveCountPrediction`'s read-time adjustment only, never mutates
|
|
282
|
-
* stored counts (epic #1281 invariant 4). Markers are written by the
|
|
283
|
-
* imap-worker bulk sync path through `RemitClient.placementMove` on every
|
|
284
|
-
* backend, so this is a real signal on the self-host stack too.
|
|
285
|
-
*/
|
|
286
|
-
const loadPendingMoves = (
|
|
287
|
-
client: Awaited<ReturnType<typeof getClient>>,
|
|
288
|
-
accountId: string,
|
|
289
|
-
) => client.placementMove.listByAccountId(accountId);
|
|
290
|
-
|
|
291
|
-
/**
|
|
292
|
-
* Every pending `\Seen` flag-push marker (issue #1273) for an account —
|
|
293
|
-
* `\Flagged` (star) markers are excluded, since only read/unread state feeds
|
|
294
|
-
* `unseenCount`'s prediction. `RemitClient.flagPush` is present and WRITTEN
|
|
295
|
-
* on both backends, so this is a real signal on the self-host stack too.
|
|
296
|
-
*/
|
|
297
|
-
const loadPendingUnseenFlagPushes = async (
|
|
298
|
-
client: Awaited<ReturnType<typeof getClient>>,
|
|
299
|
-
accountId: string,
|
|
300
|
-
): Promise<PendingUnseenFlagPush[]> => {
|
|
301
|
-
const pushes = await client.flagPush.listByAccountId(accountId);
|
|
302
|
-
return pushes
|
|
303
|
-
.filter((push) => push.flagName === MessageSystemFlag.Seen)
|
|
304
|
-
.map((push) => ({ mailboxId: push.mailboxId, operation: push.operation }));
|
|
305
|
-
};
|
|
306
|
-
|
|
307
|
-
const toMailboxResponse = (
|
|
308
|
-
mailbox: MailboxItem,
|
|
309
|
-
overrides: MailboxOverrides = {},
|
|
310
|
-
): MailboxResponse => ({
|
|
311
|
-
mailboxId: mailbox.mailboxId,
|
|
312
|
-
accountId: mailbox.accountId,
|
|
313
|
-
namespaceType: mailbox.namespaceType,
|
|
314
|
-
namespacePrefix: mailbox.namespacePrefix,
|
|
315
|
-
hierarchyDelimiter: mailbox.hierarchyDelimiter,
|
|
316
|
-
fullPath: mailbox.fullPath,
|
|
317
|
-
messageCount: mailbox.messageCount,
|
|
318
|
-
unseenCount: mailbox.unseenCount,
|
|
319
|
-
deletedCount: mailbox.deletedCount,
|
|
320
|
-
specialUse: mailbox.specialUse ? Array.from(mailbox.specialUse) : undefined,
|
|
321
|
-
lastSyncUid: mailbox.lastSyncUid,
|
|
322
|
-
highWaterMarkUid: mailbox.highWaterMarkUid,
|
|
323
|
-
lastMessageSyncAt: mailbox.lastMessageSyncAt,
|
|
324
|
-
syncStatus: mailbox.syncStatus,
|
|
325
|
-
// The row is the only thing that knows what a rename is aiming at, and
|
|
326
|
-
// `fullPath` stays the confirmed one throughout (D2), so without this a
|
|
327
|
-
// client cannot tell a create in flight from a rename in flight, name the
|
|
328
|
-
// target of a failed rename, or offer the right retry.
|
|
329
|
-
pendingPath: mailbox.pendingPath,
|
|
330
|
-
syncFailureReason: mailbox.syncFailureReason,
|
|
331
|
-
muted: overrides.muted,
|
|
332
|
-
displayNameOverride: overrides.displayNameOverride,
|
|
333
|
-
createdAt: mailbox.createdAt,
|
|
334
|
-
updatedAt: mailbox.updatedAt,
|
|
335
|
-
});
|
|
336
|
-
|
|
337
|
-
export const MailboxOperations: Record<
|
|
338
|
-
MailboxOperationIds,
|
|
339
|
-
OperationHandler<MailboxOperationIds>
|
|
340
|
-
> = {
|
|
341
|
-
MailboxOperations_listMailboxes: async (context, ...args: unknown[]) => {
|
|
342
|
-
const event = args[0] as APIGatewayProxyEvent;
|
|
343
|
-
const accountConfigId = getAccountConfigIdFromEvent(event);
|
|
344
|
-
const { accountId } = context.request.params as { accountId: string };
|
|
345
|
-
const { continuationToken } = context.request.query as {
|
|
346
|
-
continuationToken?: string;
|
|
347
|
-
};
|
|
348
|
-
|
|
349
|
-
const client = await getClient();
|
|
350
|
-
const account = await client.account.get(accountId);
|
|
351
|
-
assertAccountOwnership(account, accountConfigId, "read");
|
|
352
|
-
|
|
353
|
-
const result = await client.mailbox.listByAccount(accountId, {
|
|
354
|
-
continuationToken,
|
|
355
|
-
});
|
|
356
|
-
|
|
357
|
-
// Overrides (mute / display-name / role) live in per-mailbox AccountSetting
|
|
358
|
-
// rows (RFC 032). Load the whole config's set in one query and key it by
|
|
359
|
-
// mailboxId so each mailbox surfaces its overrides without an N+1.
|
|
360
|
-
const overridesByMailbox = await loadMailboxOverridesForConfig(
|
|
361
|
-
client.accountSetting,
|
|
362
|
-
accountConfigId,
|
|
363
|
-
);
|
|
364
|
-
|
|
365
|
-
// Read-time prediction for any pending placement move (issue #1271) and
|
|
366
|
-
// pending \Seen flag push (issue #1273) — adjusts messageCount /
|
|
367
|
-
// unseenCount only, never the stored row (epic #1281 invariant 4).
|
|
368
|
-
const pendingMoves = await loadPendingMoves(client, accountId);
|
|
369
|
-
const pendingUnseenFlagPushes = await loadPendingUnseenFlagPushes(
|
|
370
|
-
client,
|
|
371
|
-
accountId,
|
|
372
|
-
);
|
|
373
|
-
// A folder mid-mutation is listed, labelled (D11). Hiding a `deleting` one
|
|
374
|
-
// made a delete that does not settle look like a folder that silently
|
|
375
|
-
// vanished, while the server still held it.
|
|
376
|
-
const items = applyPendingMoveCountPrediction(
|
|
377
|
-
result.items,
|
|
378
|
-
pendingMoves,
|
|
379
|
-
pendingUnseenFlagPushes,
|
|
380
|
-
);
|
|
381
|
-
|
|
382
|
-
return {
|
|
383
|
-
items: items.map((mailbox) =>
|
|
384
|
-
toMailboxResponse(
|
|
385
|
-
mailbox,
|
|
386
|
-
overridesByMailbox.get(mailbox.mailboxId) ?? {},
|
|
387
|
-
),
|
|
388
|
-
),
|
|
389
|
-
continuationToken: result.continuationToken,
|
|
390
|
-
};
|
|
391
|
-
},
|
|
392
|
-
|
|
393
|
-
MailboxOperations_createMailbox: async (context, ...args: unknown[]) => {
|
|
394
|
-
const event = args[0] as APIGatewayProxyEvent;
|
|
395
|
-
const accountConfigId = getAccountConfigIdFromEvent(event);
|
|
396
|
-
const { accountId } = context.request.params as { accountId: string };
|
|
397
|
-
const { namespaceType, fullPath } = context.request.requestBody as {
|
|
398
|
-
namespaceType: string;
|
|
399
|
-
fullPath: string;
|
|
400
|
-
};
|
|
401
|
-
|
|
402
|
-
const client = await getClient();
|
|
403
|
-
const account = await client.account.get(accountId);
|
|
404
|
-
assertAccountOwnership(account, accountConfigId, "act");
|
|
405
|
-
|
|
406
|
-
const created = await client.mailboxQueue.createMailbox(
|
|
407
|
-
{
|
|
408
|
-
accountId,
|
|
409
|
-
namespaceType: namespaceType as "personal" | "other_users" | "shared",
|
|
410
|
-
namespacePrefix: "",
|
|
411
|
-
hierarchyDelimiter: "/",
|
|
412
|
-
fullPath,
|
|
413
|
-
uidValidity: 0,
|
|
414
|
-
uidNext: 1,
|
|
415
|
-
highestModseq: "0",
|
|
416
|
-
messageCount: 0,
|
|
417
|
-
unseenCount: 0,
|
|
418
|
-
deletedCount: 0,
|
|
419
|
-
totalSize: 0,
|
|
420
|
-
lastSyncUid: 0,
|
|
421
|
-
highWaterMarkUid: 0,
|
|
422
|
-
lastMessageSyncAt: 0,
|
|
423
|
-
},
|
|
424
|
-
accountId,
|
|
425
|
-
true,
|
|
426
|
-
);
|
|
427
|
-
if (created.outcome === "PathTaken") {
|
|
428
|
-
throw new BadRequestError(
|
|
429
|
-
`A folder named “${fullPath}” is already there.`,
|
|
430
|
-
);
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
return toMailboxResponse(created.mailbox);
|
|
434
|
-
},
|
|
435
|
-
};
|
|
436
|
-
|
|
437
|
-
export const MailboxDetailOperations: Record<
|
|
438
|
-
MailboxDetailOperationIds,
|
|
439
|
-
OperationHandler<MailboxDetailOperationIds>
|
|
440
|
-
> = {
|
|
441
|
-
MailboxDetailOperations_getMailbox: async (context, ...args: unknown[]) => {
|
|
442
|
-
const event = args[0] as APIGatewayProxyEvent;
|
|
443
|
-
const accountConfigId = getAccountConfigIdFromEvent(event);
|
|
444
|
-
const { accountId, mailboxId } = context.request.params as {
|
|
445
|
-
accountId: string;
|
|
446
|
-
mailboxId: string;
|
|
447
|
-
};
|
|
448
|
-
|
|
449
|
-
const client = await getClient();
|
|
450
|
-
const account = await client.account.get(accountId);
|
|
451
|
-
assertAccountOwnership(account, accountConfigId, "read");
|
|
452
|
-
|
|
453
|
-
const mailbox = await client.mailbox.get(accountId, mailboxId);
|
|
454
|
-
assertMailboxInAccount(mailbox, accountId, "read");
|
|
455
|
-
const overrides = await loadMailboxOverrides(
|
|
456
|
-
client.accountSetting,
|
|
457
|
-
accountConfigId,
|
|
458
|
-
mailboxId,
|
|
459
|
-
);
|
|
460
|
-
|
|
461
|
-
// Read-time prediction for any pending placement move (issue #1271) and
|
|
462
|
-
// pending \Seen flag push (issue #1273) — adjusts messageCount /
|
|
463
|
-
// unseenCount only, never the stored row (epic #1281 invariant 4).
|
|
464
|
-
const pendingMoves = await loadPendingMoves(client, accountId);
|
|
465
|
-
const pendingUnseenFlagPushes = await loadPendingUnseenFlagPushes(
|
|
466
|
-
client,
|
|
467
|
-
accountId,
|
|
468
|
-
);
|
|
469
|
-
const adjusted =
|
|
470
|
-
applyPendingMoveCountPrediction(
|
|
471
|
-
[mailbox],
|
|
472
|
-
pendingMoves,
|
|
473
|
-
pendingUnseenFlagPushes,
|
|
474
|
-
)[0] ?? mailbox;
|
|
475
|
-
|
|
476
|
-
return toMailboxResponse(adjusted, overrides);
|
|
477
|
-
},
|
|
478
|
-
|
|
479
|
-
MailboxDetailOperations_renameMailbox: async (
|
|
480
|
-
context,
|
|
481
|
-
...args: unknown[]
|
|
482
|
-
) => {
|
|
483
|
-
const event = args[0] as APIGatewayProxyEvent;
|
|
484
|
-
const accountConfigId = getAccountConfigIdFromEvent(event);
|
|
485
|
-
const { accountId, mailboxId } = context.request.params as {
|
|
486
|
-
accountId: string;
|
|
487
|
-
mailboxId: string;
|
|
488
|
-
};
|
|
489
|
-
const body = context.request.requestBody as RenameMailboxInput;
|
|
490
|
-
|
|
491
|
-
const client = await getClient();
|
|
492
|
-
const account = await client.account.get(accountId);
|
|
493
|
-
assertAccountOwnership(account, accountConfigId, "act");
|
|
494
|
-
|
|
495
|
-
const existing = await client.mailbox.get(accountId, mailboxId);
|
|
496
|
-
assertMailboxInAccount(existing, accountId, "act");
|
|
497
|
-
|
|
498
|
-
const mailbox = await applyMailboxPatch(
|
|
499
|
-
client,
|
|
500
|
-
accountConfigId,
|
|
501
|
-
mailboxId,
|
|
502
|
-
accountId,
|
|
503
|
-
body,
|
|
504
|
-
);
|
|
505
|
-
const overrides = await loadMailboxOverrides(
|
|
506
|
-
client.accountSetting,
|
|
507
|
-
accountConfigId,
|
|
508
|
-
mailboxId,
|
|
509
|
-
);
|
|
510
|
-
return toMailboxResponse(mailbox, overrides);
|
|
511
|
-
},
|
|
512
|
-
|
|
513
|
-
MailboxDetailOperations_deleteMailbox: async (
|
|
514
|
-
context,
|
|
515
|
-
...args: unknown[]
|
|
516
|
-
) => {
|
|
517
|
-
const event = args[0] as APIGatewayProxyEvent;
|
|
518
|
-
const accountConfigId = getAccountConfigIdFromEvent(event);
|
|
519
|
-
const { accountId, mailboxId } = context.request.params as {
|
|
520
|
-
accountId: string;
|
|
521
|
-
mailboxId: string;
|
|
522
|
-
};
|
|
523
|
-
|
|
524
|
-
const client = await getClient();
|
|
525
|
-
const account = await client.account.get(accountId);
|
|
526
|
-
assertAccountOwnership(account, accountConfigId, "act");
|
|
527
|
-
|
|
528
|
-
const mailbox = await client.mailbox.get(accountId, mailboxId);
|
|
529
|
-
assertMailboxInAccount(mailbox, accountId, "act");
|
|
530
|
-
|
|
531
|
-
// Cheapest first (D4, D13). None of the three becomes possible by
|
|
532
|
-
// retrying the same request, so all three are 400 and all three run
|
|
533
|
-
// before anything is recorded.
|
|
534
|
-
if (mailbox.fullPath.toUpperCase() === "INBOX") {
|
|
535
|
-
throw new BadRequestError("The inbox can't be deleted.");
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
const children = await client.mailbox.findByPathPrefix(
|
|
539
|
-
accountId,
|
|
540
|
-
mailbox.fullPath,
|
|
541
|
-
mailbox.hierarchyDelimiter,
|
|
542
|
-
);
|
|
543
|
-
if (children.length > 0) {
|
|
544
|
-
// The server would keep the parent's name as a placeholder and leave the
|
|
545
|
-
// children untouched — an outcome no local row describes (D9).
|
|
546
|
-
throw new BadRequestError(
|
|
547
|
-
`“${mailbox.fullPath}” has folders inside it. Delete those first.`,
|
|
548
|
-
);
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
assertNoBindings(
|
|
552
|
-
mailbox,
|
|
553
|
-
await loadMailboxBindings(client, accountConfigId, accountId, mailboxId),
|
|
554
|
-
);
|
|
555
|
-
|
|
556
|
-
await client.mailboxQueue.deleteMailbox(mailboxId, accountId);
|
|
557
|
-
return { statusCode: 204 };
|
|
558
|
-
},
|
|
559
|
-
};
|
|
560
|
-
|
|
561
|
-
export const TrashOperations: Record<
|
|
562
|
-
TrashOperationIds,
|
|
563
|
-
OperationHandler<TrashOperationIds>
|
|
564
|
-
> = {
|
|
565
|
-
TrashOperations_emptyTrash: async (context, ...args: unknown[]) => {
|
|
566
|
-
const event = args[0] as APIGatewayProxyEvent;
|
|
567
|
-
const accountConfigId = getAccountConfigIdFromEvent(event);
|
|
568
|
-
const { accountId } = context.request.params as { accountId: string };
|
|
569
|
-
|
|
570
|
-
const client = await getClient();
|
|
571
|
-
|
|
572
|
-
const account = await client.account.get(accountId);
|
|
573
|
-
assertAccountOwnership(account, accountConfigId, "act");
|
|
574
|
-
|
|
575
|
-
// The service resolves Trash, marks the rows and counts them, all off one
|
|
576
|
-
// read, and refuses with a coded 409 when the role is unresolved. Counting
|
|
577
|
-
// here as well would be a second answer free to disagree with the one that
|
|
578
|
-
// acts, and "0 deleted" reads as success to a user whose Trash is untouched.
|
|
579
|
-
return client.messageMove.emptyTrash(accountConfigId, accountId);
|
|
580
|
-
},
|
|
581
|
-
};
|
package/src/handlers/me.test.ts
DELETED
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { afterEach, describe, it } from "node:test";
|
|
3
|
-
import type { QuarantineItem } from "@remit/data-ports";
|
|
4
|
-
import type { APIGatewayProxyEvent } from "aws-lambda";
|
|
5
|
-
import type { Context } from "openapi-backend";
|
|
6
|
-
import { deriveAccountConfigId } from "../auth.js";
|
|
7
|
-
import {
|
|
8
|
-
_resetForTest,
|
|
9
|
-
type RemitClient,
|
|
10
|
-
setClient,
|
|
11
|
-
} from "../service/data-client.js";
|
|
12
|
-
import { MeOperations } from "./me.js";
|
|
13
|
-
|
|
14
|
-
const listQuarantine = MeOperations.MeOperations_listQuarantine as unknown as (
|
|
15
|
-
context: Context,
|
|
16
|
-
event: APIGatewayProxyEvent,
|
|
17
|
-
) => Promise<{ entries: QuarantineItem[] }>;
|
|
18
|
-
|
|
19
|
-
const SUB = "cognito-sub-1";
|
|
20
|
-
|
|
21
|
-
const eventFor = (sub: string): APIGatewayProxyEvent =>
|
|
22
|
-
({
|
|
23
|
-
requestContext: { authorizer: { claims: { sub } } },
|
|
24
|
-
}) as unknown as APIGatewayProxyEvent;
|
|
25
|
-
|
|
26
|
-
const entry = (over: Partial<QuarantineItem> = {}): QuarantineItem =>
|
|
27
|
-
({
|
|
28
|
-
quarantineId: "q-1",
|
|
29
|
-
accountConfigId: deriveAccountConfigId(SUB),
|
|
30
|
-
accountId: "acct-1",
|
|
31
|
-
mailboxId: "mbx-1",
|
|
32
|
-
uidValidity: 1_712_000_000,
|
|
33
|
-
uid: 40217,
|
|
34
|
-
mailboxPath: "INBOX",
|
|
35
|
-
quarantinedAt: 1_000,
|
|
36
|
-
attempts: 3,
|
|
37
|
-
failureStage: "BodyParse",
|
|
38
|
-
failureCode: "UnreadableBody",
|
|
39
|
-
failureMessage: "multipart boundary was never closed",
|
|
40
|
-
workerVersion: "worker 1.0.0",
|
|
41
|
-
structure: [{ depth: 0, contentType: "multipart/mixed" }],
|
|
42
|
-
createdAt: 1_000,
|
|
43
|
-
updatedAt: 1_000,
|
|
44
|
-
...over,
|
|
45
|
-
}) as QuarantineItem;
|
|
46
|
-
|
|
47
|
-
const clientListing = (items: QuarantineItem[], seen: string[]): RemitClient =>
|
|
48
|
-
({
|
|
49
|
-
quarantine: {
|
|
50
|
-
listByAccountConfigId: async (
|
|
51
|
-
accountConfigId: string,
|
|
52
|
-
): Promise<QuarantineItem[]> => {
|
|
53
|
-
seen.push(accountConfigId);
|
|
54
|
-
return items;
|
|
55
|
-
},
|
|
56
|
-
},
|
|
57
|
-
}) as unknown as RemitClient;
|
|
58
|
-
|
|
59
|
-
afterEach(() => {
|
|
60
|
-
_resetForTest();
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
describe("MeOperations_listQuarantine", () => {
|
|
64
|
-
it("lists only what belongs to the caller", async () => {
|
|
65
|
-
const seen: string[] = [];
|
|
66
|
-
setClient(clientListing([entry()], seen));
|
|
67
|
-
|
|
68
|
-
const response = await listQuarantine({} as Context, eventFor(SUB));
|
|
69
|
-
|
|
70
|
-
assert.deepEqual(seen, [deriveAccountConfigId(SUB)]);
|
|
71
|
-
assert.deepEqual(response, { entries: [entry()] });
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it("returns an empty list rather than failing when nothing is quarantined", async () => {
|
|
75
|
-
setClient(clientListing([], []));
|
|
76
|
-
|
|
77
|
-
const response = await listQuarantine({} as Context, eventFor(SUB));
|
|
78
|
-
|
|
79
|
-
assert.deepEqual(response, { entries: [] });
|
|
80
|
-
});
|
|
81
|
-
});
|