@remit/data-ports 0.0.44 → 0.0.46

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/data-ports",
3
- "version": "0.0.44",
3
+ "version": "0.0.46",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
@@ -2,6 +2,7 @@ import assert from "node:assert/strict";
2
2
  import { describe, it } from "node:test";
3
3
  import {
4
4
  CanonicalMailboxRole,
5
+ FolderAppointmentSource,
5
6
  MailboxSpecialUse,
6
7
  MailboxSyncStatus,
7
8
  } from "@remit/domain-enums";
@@ -15,6 +16,7 @@ import {
15
16
  resolveConfirmedMailboxForRole,
16
17
  resolveMailboxForRole,
17
18
  resolveRoleForAccount,
19
+ trashSourceMeetsAssurance,
18
20
  } from "./folder-role.js";
19
21
 
20
22
  const mailbox = (
@@ -328,6 +330,31 @@ describe("the adapters over resolveRoleForAccount", () => {
328
330
  });
329
331
  });
330
332
 
333
+ describe("trashSourceMeetsAssurance", () => {
334
+ it("confirms the user's appointment and the server's flag, and nothing else", () => {
335
+ // The set a client may word an expunge from. `Reserved` reads as
336
+ // designated and is not: it is the INBOX name rule, which nobody chose.
337
+ const confirmed = Object.values(FolderAppointmentSource).filter((source) =>
338
+ trashSourceMeetsAssurance(source, "confirmed"),
339
+ );
340
+ assert.deepEqual(confirmed, [
341
+ FolderAppointmentSource.Appointed,
342
+ FolderAppointmentSource.Flagged,
343
+ ]);
344
+ });
345
+
346
+ it("adds the name guess at `resolved`, where a wrong guess only misfiles", () => {
347
+ const resolved = Object.values(FolderAppointmentSource).filter((source) =>
348
+ trashSourceMeetsAssurance(source, "resolved"),
349
+ );
350
+ assert.deepEqual(resolved, [
351
+ FolderAppointmentSource.Appointed,
352
+ FolderAppointmentSource.Flagged,
353
+ FolderAppointmentSource.Proposed,
354
+ ]);
355
+ });
356
+ });
357
+
331
358
  describe("the appointment name and its label sibling", () => {
332
359
  it("never lets the label row parse as an appointment", () => {
333
360
  // The label is display only. If the appointment parser matched it, a path
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  AccountSettingName,
3
3
  CanonicalMailboxRole,
4
+ FolderAppointmentSource,
4
5
  MailboxSpecialUse,
5
6
  } from "@remit/domain-enums";
6
7
  import {
@@ -160,9 +161,12 @@ export const STALE_TRASH_FOLDER_REASON = `The folder appointed as this account's
160
161
 
161
162
  /**
162
163
  * A folder matches the Trash name hint and nothing else — enough to file a
163
- * delete somewhere retrievable, never enough to expunge a whole folder.
164
+ * delete somewhere retrievable, never enough to erase what is already in it.
165
+ * Emptying that folder and deleting a message already inside it both land here
166
+ * (#876), so the sentence names neither verb: "nothing was deleted" is true of
167
+ * both, the way its two siblings above are.
164
168
  */
165
- export const UNCONFIRMED_TRASH_FOLDER_REASON = `Nobody has confirmed which folder is this account's Trash, so it was not emptied. Appoint one under ${FOLDER_ROLES_SETTINGS_PATH}, then try again.`;
169
+ export const UNCONFIRMED_TRASH_FOLDER_REASON = `Nobody has confirmed which folder is this account's Trash, so nothing was deleted. Appoint one under ${FOLDER_ROLES_SETTINGS_PATH}, then try again.`;
166
170
 
167
171
  /** The minimal mailbox shape role resolution reads. */
168
172
  export interface RoleMailboxCandidate extends MailboxNameCandidate {
@@ -307,6 +311,40 @@ export const meetsTrashAssurance = <T>(
307
311
  level: TrashAssuranceLevel,
308
312
  ): boolean => trashMailboxAt(resolution, level).allowed;
309
313
 
314
+ export type FolderAppointmentSourceValue =
315
+ (typeof FolderAppointmentSource)[keyof typeof FolderAppointmentSource];
316
+
317
+ /** The resolution each reported source came from (`toFolderAppointment`). */
318
+ const RESOLUTION_FOR_SOURCE: Record<
319
+ FolderAppointmentSourceValue,
320
+ RoleResolution<null>
321
+ > = {
322
+ [FolderAppointmentSource.Appointed]: { kind: "appointed", mailbox: null },
323
+ [FolderAppointmentSource.Flagged]: { kind: "flagged", mailbox: null },
324
+ [FolderAppointmentSource.Reserved]: { kind: "reserved", mailbox: null },
325
+ [FolderAppointmentSource.Proposed]: { kind: "proposed", mailbox: null },
326
+ [FolderAppointmentSource.Stale]: {
327
+ kind: "appointment_stale",
328
+ appointedMailboxId: "",
329
+ fallback: { kind: "none" },
330
+ },
331
+ [FolderAppointmentSource.None]: { kind: "none" },
332
+ };
333
+
334
+ /**
335
+ * The same gate as {@link trashMailboxAt}, asked of the source `/config`
336
+ * reports rather than of the resolution the server computed — so a client can
337
+ * word a delete from the answer the service will give it.
338
+ *
339
+ * It runs that switch rather than restating it. A hand-written mirror gets
340
+ * `Reserved` wrong: it reads as designated, and the gate accepts only the user's
341
+ * appointment and the server's flag.
342
+ */
343
+ export const trashSourceMeetsAssurance = (
344
+ source: FolderAppointmentSourceValue,
345
+ level: TrashAssuranceLevel,
346
+ ): boolean => meetsTrashAssurance(RESOLUTION_FOR_SOURCE[source], level);
347
+
310
348
  /**
311
349
  * The mailbox a role is CONFIRMED to hold: the one the user appointed, or the
312
350
  * one the server flagged (RFC 6154). No name guessing — `null` here means
package/src/id.test.ts CHANGED
@@ -1,10 +1,19 @@
1
1
  import assert from "node:assert/strict";
2
2
  import { describe, it } from "node:test";
3
3
  import {
4
+ base36uuidv5,
5
+ deriveAddressId,
6
+ deriveBodyPartId,
4
7
  deriveCopyMessageId,
8
+ deriveEnvelopeId,
5
9
  deriveMessageId,
10
+ deriveMessageIdFromSource,
6
11
  deriveQuarantineId,
12
+ deriveThreadId,
13
+ normalizeMessageIdHeader,
7
14
  quarantineMessageIdHash,
15
+ REMIT_NAMESPACE,
16
+ ROOT_PART_PATH,
8
17
  } from "./id.js";
9
18
 
10
19
  const ACCOUNT = "acct-1";
@@ -90,3 +99,97 @@ describe("quarantineMessageIdHash", () => {
90
99
  }
91
100
  });
92
101
  });
102
+
103
+ describe("derived ids are stored primary keys: a changed value orphans every row already written and needs a migration, never a new expectation here", () => {
104
+ const ACCOUNT = "account-golden";
105
+ const MESSAGE = "message-golden";
106
+ const MAILBOX = "mailbox-golden";
107
+ const ABSENT_HEADER_SOURCE = {
108
+ messageId: undefined,
109
+ mailboxId: MAILBOX,
110
+ uid: 40217,
111
+ date: "2026-08-23T00:00:00Z",
112
+ subject: "Golden",
113
+ fromMailbox: "golden",
114
+ fromHost: "example.com",
115
+ };
116
+
117
+ it("REMIT_NAMESPACE is the seed of every derived id and can never change", () => {
118
+ assert.equal(REMIT_NAMESPACE, "9e89694d-214b-4d9b-99f5-214b4d9b99f5");
119
+ });
120
+
121
+ it("base36uuidv5 pins the encoding every derivation below is built on", () => {
122
+ assert.equal(
123
+ base36uuidv5("golden-seed", REMIT_NAMESPACE),
124
+ "67hxxs95ofdk5frbprqyrgwmo",
125
+ );
126
+ });
127
+
128
+ it("deriveAddressId pins the address primary key, lowercased email included", () => {
129
+ assert.equal(
130
+ deriveAddressId(ACCOUNT, "Golden.User@Example.COM"),
131
+ "dzu01kz6aj1ro8fr8l375tfbr",
132
+ );
133
+ });
134
+
135
+ it("deriveMessageId pins the message primary key", () => {
136
+ assert.equal(
137
+ deriveMessageId(ACCOUNT, "<golden@example.com>"),
138
+ "bfthbm5n4279aacb5a1q1c94h",
139
+ );
140
+ });
141
+
142
+ it("normalizeMessageIdHeader pins the synthetic header that seeds a Message-ID-less message", () => {
143
+ assert.equal(
144
+ normalizeMessageIdHeader(ABSENT_HEADER_SOURCE),
145
+ "generated:mailbox-golden:40217:2026-08-23T00:00:00Z:Golden:golden:example.com",
146
+ );
147
+ });
148
+
149
+ it("deriveMessageIdFromSource pins the key of a message that declared no Message-ID", () => {
150
+ assert.equal(
151
+ deriveMessageIdFromSource(ACCOUNT, ABSENT_HEADER_SOURCE),
152
+ "0rae70onzy4ufhlnndqe7lwml",
153
+ );
154
+ });
155
+
156
+ it("deriveCopyMessageId pins the key of a message copied into another folder", () => {
157
+ assert.equal(
158
+ deriveCopyMessageId(MESSAGE, MAILBOX),
159
+ "43cwqvpfh4o1fcbmaciabmqo3",
160
+ );
161
+ });
162
+
163
+ it("deriveThreadId pins the thread primary key, lowercased root header included", () => {
164
+ assert.equal(
165
+ deriveThreadId(ACCOUNT, "<Golden.Root@Example.COM>"),
166
+ "bcy76e2uat5yq3xd1p7j7z4pr",
167
+ );
168
+ });
169
+
170
+ it("deriveEnvelopeId pins the envelope primary key", () => {
171
+ assert.equal(deriveEnvelopeId(MESSAGE), "f1bngysevoxuqt625rd5hvtzt");
172
+ });
173
+
174
+ it("ROOT_PART_PATH is baked into stored body part keys and S3 keys", () => {
175
+ assert.equal(ROOT_PART_PATH, "0");
176
+ });
177
+
178
+ it("deriveBodyPartId pins the body_part primary key", () => {
179
+ assert.equal(deriveBodyPartId(MESSAGE, "1.2"), "363nl1jzgh77ki2ojk1hqvxxe");
180
+ });
181
+
182
+ it("deriveQuarantineId pins the quarantine primary key", () => {
183
+ assert.equal(
184
+ deriveQuarantineId(ACCOUNT, MAILBOX, 1_700_000_000, 40217),
185
+ "4xg2y7x2j7nmvy0wtwgdq8b8p",
186
+ );
187
+ });
188
+
189
+ it("quarantineMessageIdHash pins the stored Message-ID hash that correlates reports", () => {
190
+ assert.equal(
191
+ quarantineMessageIdHash("<golden@example.com>"),
192
+ "sha256:5e888fd63760269e96a61e1a09b83ddec28556d0e3cda7e480897e58c8e6a2f5",
193
+ );
194
+ });
195
+ });