@remit/backend 0.0.124 → 0.0.126

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remit/backend",
3
- "version": "0.0.124",
3
+ "version": "0.0.126",
4
4
  "description": "Remit Mail Inspector API backend",
5
5
  "license": "MIT",
6
6
  "author": "",
@@ -69,6 +69,8 @@ const run = async (): Promise<void> => {
69
69
  const result = await backfillClassifications(
70
70
  {
71
71
  accountConfigService: client.accountConfig,
72
+ accountService: client.account,
73
+ mailboxSpecialUseService: client.mailboxSpecialUse,
72
74
  addressService: client.address,
73
75
  threadMessageService: client.threadMessage,
74
76
  messageService: client.message,
@@ -1,6 +1,7 @@
1
1
  import assert from "node:assert/strict";
2
2
  import { afterEach, describe, it } from "node:test";
3
3
  import type { MailboxItem } from "@remit/data-ports";
4
+ import { BadRequestError } from "@remit/data-ports/errors";
4
5
  import { MailboxSyncStatus } from "@remit/domain-enums";
5
6
  import { UnconfirmedTrashMailboxError } from "@remit/mailbox-service";
6
7
  import type { APIGatewayProxyEvent } from "aws-lambda";
@@ -11,7 +12,11 @@ import {
11
12
  type RemitClient,
12
13
  setClient,
13
14
  } from "../service/data-client.js";
14
- import { MailboxDetailOperations, TrashOperations } from "./mailbox.js";
15
+ import {
16
+ MailboxDetailOperations,
17
+ MailboxOperations,
18
+ TrashOperations,
19
+ } from "./mailbox.js";
15
20
 
16
21
  const SUB = "cognito-sub-887";
17
22
  const ACCOUNT_ID = "acc-887";
@@ -230,3 +235,46 @@ describe("MailboxDetailOperations_deleteMailbox — what it refuses (D4, D16)",
230
235
  assert.deepEqual(deleted, ["mbx-1"]);
231
236
  });
232
237
  });
238
+
239
+ const createMailbox =
240
+ MailboxOperations.MailboxOperations_createMailbox as unknown as (
241
+ context: Context,
242
+ event: APIGatewayProxyEvent,
243
+ ) => Promise<unknown>;
244
+
245
+ describe("MailboxOperations_createMailbox", () => {
246
+ afterEach(() => {
247
+ _resetForTest();
248
+ });
249
+
250
+ it("refuses a path the account already has a folder at", async () => {
251
+ setClient({
252
+ account: {
253
+ get: async () => ({
254
+ accountId: ACCOUNT_ID,
255
+ accountConfigId: deriveAccountConfigId(SUB),
256
+ }),
257
+ },
258
+ mailboxQueue: {
259
+ createMailbox: async () => ({ outcome: "PathTaken" }),
260
+ },
261
+ } as unknown as RemitClient);
262
+
263
+ await assert.rejects(
264
+ createMailbox(
265
+ {
266
+ request: {
267
+ params: { accountId: ACCOUNT_ID },
268
+ requestBody: { namespaceType: "personal", fullPath: "Receipts" },
269
+ },
270
+ } as unknown as Context,
271
+ {
272
+ requestContext: { authorizer: { claims: { sub: SUB } } },
273
+ } as unknown as APIGatewayProxyEvent,
274
+ ),
275
+ (error: unknown) =>
276
+ error instanceof BadRequestError &&
277
+ error.message === "A folder named “Receipts” is already there.",
278
+ );
279
+ });
280
+ });
@@ -402,7 +402,7 @@ export const MailboxOperations: Record<
402
402
  const account = await client.account.get(accountId);
403
403
  assertAccountOwnership(account, accountConfigId, "act");
404
404
 
405
- const mailbox = await client.mailboxQueue.createMailbox(
405
+ const created = await client.mailboxQueue.createMailbox(
406
406
  {
407
407
  accountId,
408
408
  namespaceType: namespaceType as "personal" | "other_users" | "shared",
@@ -423,8 +423,13 @@ export const MailboxOperations: Record<
423
423
  accountId,
424
424
  true,
425
425
  );
426
+ if (created.outcome === "PathTaken") {
427
+ throw new BadRequestError(
428
+ `A folder named “${fullPath}” is already there.`,
429
+ );
430
+ }
426
431
 
427
- return toMailboxResponse(mailbox);
432
+ return toMailboxResponse(created.mailbox);
428
433
  },
429
434
  };
430
435
 
@@ -0,0 +1,103 @@
1
+ import assert from "node:assert/strict";
2
+ import { afterEach, describe, it } from "node:test";
3
+ import type { APIGatewayProxyEvent } from "aws-lambda";
4
+ import type { Context } from "openapi-backend";
5
+ import {
6
+ _resetForTest,
7
+ type RemitClient,
8
+ setClient,
9
+ } from "../service/data-client.js";
10
+ import { MessageOperations } from "./message.js";
11
+
12
+ type Handler = (
13
+ context: Context,
14
+ event: APIGatewayProxyEvent,
15
+ ) => Promise<unknown>;
16
+
17
+ const describeMessage =
18
+ MessageOperations.MessageOperations_describeMessage as unknown as Handler;
19
+ const getRawMessage =
20
+ MessageOperations.MessageOperations_getRawMessage as unknown as Handler;
21
+
22
+ const MESSAGE_ID = "msg-1";
23
+ const MAILBOX_ID = "mb-1";
24
+ const ACCOUNT_ID = "acc-1";
25
+
26
+ const event = {
27
+ requestContext: { authorizer: { claims: { sub: "sub-610" } } },
28
+ } as unknown as APIGatewayProxyEvent;
29
+
30
+ const context = {
31
+ request: { params: { messageId: MESSAGE_ID } },
32
+ } as unknown as Context;
33
+
34
+ const unstoredMessage = {
35
+ messageId: MESSAGE_ID,
36
+ mailboxId: MAILBOX_ID,
37
+ uid: 7,
38
+ internalDate: 1,
39
+ status: "active",
40
+ syncStatus: "synced",
41
+ };
42
+
43
+ const mailboxRow = {
44
+ mailboxId: MAILBOX_ID,
45
+ accountId: ACCOUNT_ID,
46
+ fullPath: "Junk",
47
+ cursorState: "normal",
48
+ };
49
+
50
+ const clientWithFailingFolderLookup = (opened: string[]): RemitClient =>
51
+ ({
52
+ message: {
53
+ get: async () => unstoredMessage,
54
+ describe: async () => ({
55
+ message: [unstoredMessage],
56
+ envelope: [],
57
+ envelopeAddress: [],
58
+ messageFlag: [],
59
+ bodyPart: [],
60
+ messageReference: [],
61
+ }),
62
+ },
63
+ messageLabel: { listByMessageId: async () => [] },
64
+ account: {
65
+ listAllByAccountConfig: async () => [{ accountId: ACCOUNT_ID }],
66
+ },
67
+ mailbox: {
68
+ get: async (_accountId: string, ids: string | string[]) =>
69
+ Array.isArray(ids) ? [mailboxRow] : mailboxRow,
70
+ },
71
+ storage: { bodyPartExists: async () => false },
72
+ mailboxSpecialUse: {
73
+ findJunkMailbox: async () => {
74
+ throw new Error("special-use lookup timed out");
75
+ },
76
+ findInboxMailbox: async () => null,
77
+ },
78
+ createConnectionScope: async (accountId: string) => {
79
+ opened.push(accountId);
80
+ return {
81
+ getConnection: async () => ({}),
82
+ disconnect: async () => {},
83
+ };
84
+ },
85
+ }) as unknown as RemitClient;
86
+
87
+ describe("reading an unstored message when the folder lookup fails", () => {
88
+ afterEach(() => _resetForTest());
89
+
90
+ for (const [name, handler] of [
91
+ ["describeMessage", describeMessage],
92
+ ["getRawMessage", getRawMessage],
93
+ ] as const) {
94
+ it(`${name} opens no IMAP connection`, async () => {
95
+ const opened: string[] = [];
96
+ setClient(clientWithFailingFolderLookup(opened));
97
+
98
+ await assert.rejects(handler(context, event), /special-use lookup/);
99
+
100
+ assert.deepEqual(opened, []);
101
+ });
102
+ }
103
+ });
@@ -26,6 +26,7 @@ import {
26
26
  MailboxCursorPausedError,
27
27
  MoveNotSettledError,
28
28
  NoJunkMailboxError,
29
+ resolveFolderPlacement,
29
30
  } from "@remit/mailbox-service";
30
31
  import {
31
32
  isStorageNotFoundError as isStorageNotFoundErrorFromService,
@@ -605,6 +606,14 @@ export const MessageOperations: Record<
605
606
  "Mailbox cursor not normal; skipping IMAP body backfill for this request",
606
607
  );
607
608
  } else {
609
+ const folder = {
610
+ fullPath: mailbox.fullPath,
611
+ placement: await resolveFolderPlacement(
612
+ client.mailboxSpecialUse,
613
+ accountId,
614
+ mailbox.mailboxId,
615
+ ),
616
+ };
608
617
  const scope = await client.createConnectionScope(accountId);
609
618
  // Guard at the one choke point every UID-based IMAP op already
610
619
  // passes through: openBox. A mismatch trips the mailbox to
@@ -619,12 +628,8 @@ export const MessageOperations: Record<
619
628
  );
620
629
 
621
630
  await client.bodySync
622
- .fetchAndGetBody(
623
- messageId,
624
- accountId,
625
- accountConfigId,
626
- mailbox.fullPath,
627
- () => Promise.resolve(connection),
631
+ .fetchAndGetBody(messageId, accountId, accountConfigId, folder, () =>
632
+ Promise.resolve(connection),
628
633
  )
629
634
  .catch((error: unknown) => {
630
635
  if (error instanceof MailboxCursorPausedError) {
@@ -742,6 +747,14 @@ export const MessageOperations: Record<
742
747
  "Mailbox cursor not normal; skipping IMAP body backfill for this request",
743
748
  );
744
749
  } else {
750
+ const folder = {
751
+ fullPath: mailbox.fullPath,
752
+ placement: await resolveFolderPlacement(
753
+ client.mailboxSpecialUse,
754
+ accountId,
755
+ mailbox.mailboxId,
756
+ ),
757
+ };
745
758
  const scope = await client.createConnectionScope(accountId);
746
759
  // Guard at the openBox choke point — a fresh mismatch trips the
747
760
  // mailbox and throws once the SELECT reveals it; caught below.
@@ -752,12 +765,8 @@ export const MessageOperations: Record<
752
765
  mailbox,
753
766
  );
754
767
  await client.bodySync
755
- .fetchAndGetBody(
756
- messageId,
757
- accountId,
758
- accountConfigId,
759
- mailbox.fullPath,
760
- () => Promise.resolve(connection),
768
+ .fetchAndGetBody(messageId, accountId, accountConfigId, folder, () =>
769
+ Promise.resolve(connection),
761
770
  )
762
771
  .catch((error: unknown) => {
763
772
  if (error instanceof MailboxCursorPausedError) {
@@ -165,7 +165,7 @@ const readBodyThroughClient = async (
165
165
  "m-1",
166
166
  "acc-1",
167
167
  "cfg-1",
168
- "INBOX",
168
+ { fullPath: "INBOX", placement: "inbox" },
169
169
  async () => connection,
170
170
  );
171
171
  };