@remit/mailbox-service 0.0.8 → 0.0.10

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/mailbox-service",
3
- "version": "0.0.8",
3
+ "version": "0.0.10",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
@@ -1,6 +1,6 @@
1
1
  import assert from "node:assert";
2
2
  import { describe, it } from "node:test";
3
- import { MailboxSpecialUse } from "@remit/domain-enums";
3
+ import { MailboxAttribute, MailboxSpecialUse } from "@remit/domain-enums";
4
4
  import {
5
5
  hasChildren,
6
6
  isNoSelect,
@@ -50,7 +50,33 @@ describe("parseImapAttributes special-use", () => {
50
50
  it("separates standard attributes from special-use", () => {
51
51
  const parsed = parseImapAttributes(["\\HasChildren", "\\Sent"]);
52
52
  assert.deepStrictEqual(parsed.specialUse, [MailboxSpecialUse.Sent]);
53
- assert.deepStrictEqual(parsed.attributes, ["HasChildren"]);
53
+ assert.deepStrictEqual(parsed.attributes, ["\\HasChildren"]);
54
+ });
55
+
56
+ it("keeps standard attributes in the RFC 9051 backslash-prefixed form", () => {
57
+ const parsed = parseImapAttributes([
58
+ "\\NonExistent",
59
+ "\\Noinferiors",
60
+ "\\Noselect",
61
+ "\\HasChildren",
62
+ "\\HasNoChildren",
63
+ "\\Marked",
64
+ "\\Unmarked",
65
+ "\\Subscribed",
66
+ "\\Remote",
67
+ ]);
68
+ assert.deepStrictEqual(parsed.attributes, [
69
+ "\\NonExistent",
70
+ "\\Noinferiors",
71
+ "\\Noselect",
72
+ "\\HasChildren",
73
+ "\\HasNoChildren",
74
+ "\\Marked",
75
+ "\\Unmarked",
76
+ "\\Subscribed",
77
+ "\\Remote",
78
+ ]);
79
+ assert.deepStrictEqual(parsed.attributes, Object.values(MailboxAttribute));
54
80
  });
55
81
 
56
82
  it("collects unknown attributes without dropping them", () => {
@@ -11,8 +11,9 @@ type MailboxSpecialUseValue =
11
11
  (typeof MailboxSpecialUse)[keyof typeof MailboxSpecialUse];
12
12
 
13
13
  /**
14
- * Map from IMAP attribute strings to MailboxAttribute enum values
15
- * IMAP attributes have backslash prefix, our enum values don't
14
+ * Map from IMAP attribute strings to MailboxAttribute enum values.
15
+ * MailboxAttribute keeps the RFC 9051 backslash prefix; MailboxSpecialUse
16
+ * below is a canonical bare designation, so only that map strips it.
16
17
  */
17
18
  const ATTRIBUTE_MAP: Record<string, MailboxAttributeValue> = {
18
19
  "\\NonExistent": MailboxAttribute.NonExistent,
@@ -695,10 +695,14 @@ export class MessageMoveService {
695
695
  /**
696
696
  * Delete every ThreadMessage row that points at this messageId.
697
697
  *
698
- * A single Message can have multiple ThreadMessage rows one per mailbox
699
- * the message exists in (e.g. INBOX + a label/folder copy). Deleting them
700
- * up-front in the permanent-delete optimistic step prevents stale rows from
701
- * leaking into mailbox listings while IMAP catches up. See issue #212.
698
+ * A row is keyed by (threadId, messageId), so this is one row in practice —
699
+ * a message filed in several folders keeps a single row, because its
700
+ * messageId is derived from the Message-ID header and not from the mailbox.
701
+ * The query stays a list because the key permits more than one thread per
702
+ * message and nothing enforces otherwise.
703
+ *
704
+ * Deleting up-front in the permanent-delete optimistic step prevents stale
705
+ * rows from leaking into mailbox listings while IMAP catches up (#212).
702
706
  */
703
707
  private deleteThreadMessagesForMessage = async (
704
708
  accountConfigId: string,
@@ -0,0 +1,77 @@
1
+ import assert from "node:assert/strict";
2
+ import { describe, it } from "node:test";
3
+ import {
4
+ MailboxAttribute,
5
+ MessageKeywordFlag,
6
+ MessageSystemFlag,
7
+ } from "@remit/domain-enums";
8
+
9
+ /**
10
+ * RFC 9051 §2.3.2 system flags and §7.3.1 mailbox attributes are
11
+ * backslash-prefixed on the wire, and these enum values reach an IMAP server
12
+ * verbatim: `handleFlagPush` passes a marker's `flagName` straight to
13
+ * `ImapFlowConnection.addFlags`/`removeFlags`. A value that has lost its
14
+ * backslash is still a legal STORE argument — the server takes it as a custom
15
+ * keyword — so the push silently sets the wrong thing rather than failing.
16
+ *
17
+ * These enums are generated, and the emitter writes their values into
18
+ * JavaScript source where an unescaped `\S` collapses to `S` (issue #64).
19
+ * Pin the exact bytes so a regression fails here instead of on a real
20
+ * mailbox.
21
+ */
22
+ const SYSTEM_FLAG_WIRE_FORMAT = {
23
+ Seen: "\\Seen",
24
+ Answered: "\\Answered",
25
+ Flagged: "\\Flagged",
26
+ Deleted: "\\Deleted",
27
+ Draft: "\\Draft",
28
+ } as const;
29
+
30
+ const MAILBOX_ATTRIBUTE_WIRE_FORMAT = {
31
+ NonExistent: "\\NonExistent",
32
+ NoInferiors: "\\Noinferiors",
33
+ NoSelect: "\\Noselect",
34
+ HasChildren: "\\HasChildren",
35
+ HasNoChildren: "\\HasNoChildren",
36
+ Marked: "\\Marked",
37
+ Unmarked: "\\Unmarked",
38
+ Subscribed: "\\Subscribed",
39
+ Remote: "\\Remote",
40
+ } as const;
41
+
42
+ describe("IMAP system flag wire format (issue #64)", () => {
43
+ it("carries the RFC 9051 backslash prefix on every member", () => {
44
+ assert.deepEqual({ ...MessageSystemFlag }, { ...SYSTEM_FLAG_WIRE_FORMAT });
45
+ });
46
+
47
+ for (const [name, wireValue] of Object.entries(SYSTEM_FLAG_WIRE_FORMAT)) {
48
+ it(`${name} is one backslash followed by the flag name`, () => {
49
+ assert.equal(wireValue.charCodeAt(0), 0x5c);
50
+ assert.equal(wireValue.slice(1), name);
51
+ assert.equal(wireValue.length, name.length + 1);
52
+ });
53
+ }
54
+
55
+ it("leaves keyword flags unprefixed", () => {
56
+ for (const wireValue of Object.values(MessageKeywordFlag)) {
57
+ assert.equal(wireValue.startsWith("\\"), false);
58
+ assert.equal(wireValue.startsWith("$"), true);
59
+ }
60
+ });
61
+ });
62
+
63
+ describe("IMAP mailbox attribute wire format (issue #64)", () => {
64
+ it("carries the RFC 9051 backslash prefix on every member", () => {
65
+ assert.deepEqual(
66
+ { ...MailboxAttribute },
67
+ { ...MAILBOX_ATTRIBUTE_WIRE_FORMAT },
68
+ );
69
+ });
70
+
71
+ it("prefixes every value with exactly one backslash", () => {
72
+ for (const wireValue of Object.values(MailboxAttribute)) {
73
+ assert.equal(wireValue.charCodeAt(0), 0x5c);
74
+ assert.equal(wireValue.charCodeAt(1) === 0x5c, false);
75
+ }
76
+ });
77
+ });