@remit/mailbox-service 0.0.1

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.
Files changed (124) hide show
  1. package/README.md +147 -0
  2. package/package.json +65 -0
  3. package/src/account-credentials.ts +128 -0
  4. package/src/adversarial-mime.e2e.test.ts +311 -0
  5. package/src/attribute-mapper.test.ts +78 -0
  6. package/src/attribute-mapper.ts +147 -0
  7. package/src/body-part-mapper.property.test.ts +354 -0
  8. package/src/body-part-mapper.ts +360 -0
  9. package/src/body-sync-queue.test.ts +124 -0
  10. package/src/body-sync-queue.ts +131 -0
  11. package/src/body-sync-terminal.test.ts +265 -0
  12. package/src/body-sync-terminal.ts +172 -0
  13. package/src/body-sync.ts +1202 -0
  14. package/src/connection-factory.ts +102 -0
  15. package/src/connection-test.ts +208 -0
  16. package/src/credentials.test.ts +181 -0
  17. package/src/filters/match.test.ts +181 -0
  18. package/src/filters/match.ts +143 -0
  19. package/src/filters/pipeline.ts +243 -0
  20. package/src/flag-push-terminal.test.ts +156 -0
  21. package/src/flag-push-terminal.ts +106 -0
  22. package/src/flag-push.test.ts +408 -0
  23. package/src/flag-push.ts +198 -0
  24. package/src/flag-queue.ts +500 -0
  25. package/src/heuristics/classifyByHeaders.test.ts +578 -0
  26. package/src/heuristics/classifyByHeaders.ts +369 -0
  27. package/src/heuristics/classifyPlacement.ts +96 -0
  28. package/src/heuristics/socialDomains.ts +23 -0
  29. package/src/heuristics/transactionalDomains.ts +26 -0
  30. package/src/imapflow-connect-retry.test.ts +108 -0
  31. package/src/imapflow-connection.e2e.test.ts +355 -0
  32. package/src/imapflow-connection.integ.test.ts +327 -0
  33. package/src/imapflow-connection.test.ts +161 -0
  34. package/src/imapflow-connection.ts +1374 -0
  35. package/src/index.ts +214 -0
  36. package/src/mailbox-cursor-rebuild.test.ts +171 -0
  37. package/src/mailbox-cursor-rebuild.ts +146 -0
  38. package/src/mailbox-cursor.test.ts +249 -0
  39. package/src/mailbox-cursor.ts +177 -0
  40. package/src/mailbox-management.test.ts +103 -0
  41. package/src/mailbox-management.ts +235 -0
  42. package/src/mailbox-queue.ts +258 -0
  43. package/src/mailbox-sync.test.ts +153 -0
  44. package/src/mailbox-sync.ts +590 -0
  45. package/src/message-move.ts +825 -0
  46. package/src/message-parser.ts +26 -0
  47. package/src/message-sync.ts +1021 -0
  48. package/src/mime-walker.test.ts +311 -0
  49. package/src/mime-walker.ts +305 -0
  50. package/src/outbox-queue.ts +294 -0
  51. package/src/pass-through-unit-of-work.ts +17 -0
  52. package/src/placement-move-terminal.test.ts +179 -0
  53. package/src/placement-move-terminal.ts +104 -0
  54. package/src/placement-move.ts +224 -0
  55. package/src/snippet.test.ts +213 -0
  56. package/src/snippet.ts +152 -0
  57. package/src/stale-message-reconcile.test.ts +82 -0
  58. package/src/stale-message-reconcile.ts +55 -0
  59. package/src/test-helpers/isolated-mailbox.ts +44 -0
  60. package/src/test-helpers/mailfuzz-connection.ts +27 -0
  61. package/src/test-helpers/mime-tree-generator.ts +461 -0
  62. package/src/text/normalizer.test.ts +223 -0
  63. package/src/text/normalizer.ts +140 -0
  64. package/src/types/mailparser-augment.d.ts +15 -0
  65. package/src/types/natural-porter-stemmer.d.ts +50 -0
  66. package/src/types/stopword.d.ts +69 -0
  67. package/src/types.ts +349 -0
  68. package/test/fixtures/mime/01-flat-text-plain.bodyparts.json +8 -0
  69. package/test/fixtures/mime/01-flat-text-plain.eml +10 -0
  70. package/test/fixtures/mime/01-flat-text-plain.expected.json +10 -0
  71. package/test/fixtures/mime/02-flat-text-html.bodyparts.json +8 -0
  72. package/test/fixtures/mime/02-flat-text-html.eml +10 -0
  73. package/test/fixtures/mime/02-flat-text-html.expected.json +10 -0
  74. package/test/fixtures/mime/03-alternative-text-html.bodyparts.json +20 -0
  75. package/test/fixtures/mime/03-alternative-text-html.eml +19 -0
  76. package/test/fixtures/mime/03-alternative-text-html.expected.json +16 -0
  77. package/test/fixtures/mime/04-mixed-html-pdf-attachment.bodyparts.json +22 -0
  78. package/test/fixtures/mime/04-mixed-html-pdf-attachment.eml +20 -0
  79. package/test/fixtures/mime/04-mixed-html-pdf-attachment.expected.json +16 -0
  80. package/test/fixtures/mime/05-mixed-html-octet-stream-pdf.bodyparts.json +22 -0
  81. package/test/fixtures/mime/05-mixed-html-octet-stream-pdf.eml +21 -0
  82. package/test/fixtures/mime/05-mixed-html-octet-stream-pdf.expected.json +16 -0
  83. package/test/fixtures/mime/06-related-html-cid-image.bodyparts.json +23 -0
  84. package/test/fixtures/mime/06-related-html-cid-image.eml +21 -0
  85. package/test/fixtures/mime/06-related-html-cid-image.expected.json +16 -0
  86. package/test/fixtures/mime/07-mixed-alternative-related-attachment.bodyparts.json +55 -0
  87. package/test/fixtures/mime/07-mixed-alternative-related-attachment.eml +47 -0
  88. package/test/fixtures/mime/07-mixed-alternative-related-attachment.expected.json +34 -0
  89. package/test/fixtures/mime/08-two-pdfs-same-filename.bodyparts.json +30 -0
  90. package/test/fixtures/mime/08-two-pdfs-same-filename.eml +26 -0
  91. package/test/fixtures/mime/08-two-pdfs-same-filename.expected.json +22 -0
  92. package/test/fixtures/mime/09-attachment-no-filename.bodyparts.json +21 -0
  93. package/test/fixtures/mime/09-attachment-no-filename.eml +20 -0
  94. package/test/fixtures/mime/09-attachment-no-filename.expected.json +16 -0
  95. package/test/fixtures/mime/10-calendar-invite.bodyparts.json +22 -0
  96. package/test/fixtures/mime/10-calendar-invite.eml +33 -0
  97. package/test/fixtures/mime/10-calendar-invite.expected.json +16 -0
  98. package/test/fixtures/mime/11-quoted-printable-text.bodyparts.json +8 -0
  99. package/test/fixtures/mime/11-quoted-printable-text.eml +10 -0
  100. package/test/fixtures/mime/11-quoted-printable-text.expected.json +10 -0
  101. package/test/fixtures/mime/12-base64-binary.bodyparts.json +22 -0
  102. package/test/fixtures/mime/12-base64-binary.eml +20 -0
  103. package/test/fixtures/mime/12-base64-binary.expected.json +16 -0
  104. package/test/fixtures/mime/13-message-rfc822-forward.bodyparts.json +22 -0
  105. package/test/fixtures/mime/13-message-rfc822-forward.eml +28 -0
  106. package/test/fixtures/mime/13-message-rfc822-forward.expected.json +16 -0
  107. package/test/fixtures/mime/14-deeply-nested-5-levels.bodyparts.json +38 -0
  108. package/test/fixtures/mime/14-deeply-nested-5-levels.eml +34 -0
  109. package/test/fixtures/mime/14-deeply-nested-5-levels.expected.json +10 -0
  110. package/test/fixtures/mime/15-empty-text-part.bodyparts.json +21 -0
  111. package/test/fixtures/mime/15-empty-text-part.eml +18 -0
  112. package/test/fixtures/mime/15-empty-text-part.expected.json +16 -0
  113. package/test/fixtures/mime/16-empty-non-text-part.bodyparts.json +30 -0
  114. package/test/fixtures/mime/16-empty-non-text-part.eml +24 -0
  115. package/test/fixtures/mime/16-empty-non-text-part.expected.json +22 -0
  116. package/test/fixtures/mime-adversarial-e2e/01-octet-stream-pdf.eml +21 -0
  117. package/test/fixtures/mime-adversarial-e2e/02-nested-related-cid-image.eml +31 -0
  118. package/test/fixtures/mime-adversarial-e2e/03-calendar-invite.eml +33 -0
  119. package/test/fixtures/mime-adversarial-e2e/04-forwarded-rfc822-with-attachments.eml +42 -0
  120. package/test/fixtures/mime-adversarial-e2e/05-tnef-winmail.eml +25 -0
  121. package/test/fixtures/mime-adversarial-e2e/06-8bit-non-ascii.eml +15 -0
  122. package/test/fixtures/mime-adversarial-e2e/07-quoted-printable-soft-breaks.eml +17 -0
  123. package/test/fixtures/mime-adversarial-e2e/08-alternative-empty-text.eml +18 -0
  124. package/tsconfig.json +8 -0
@@ -0,0 +1,161 @@
1
+ import assert from "node:assert";
2
+ import { describe, it } from "node:test";
3
+ import {
4
+ ImapFlowConnection,
5
+ toInternalDate,
6
+ toIsoDateString,
7
+ } from "./imapflow-connection.js";
8
+
9
+ describe("toIsoDateString", () => {
10
+ it("converts a Date to an ISO string", () => {
11
+ const date = new Date("2026-06-21T10:20:30.000Z");
12
+ assert.strictEqual(toIsoDateString(date), "2026-06-21T10:20:30.000Z");
13
+ });
14
+
15
+ it("converts a date string to an ISO string", () => {
16
+ assert.strictEqual(
17
+ toIsoDateString("2026-06-21T10:20:30.000Z"),
18
+ "2026-06-21T10:20:30.000Z",
19
+ );
20
+ });
21
+
22
+ it("converts an epoch number to an ISO string", () => {
23
+ assert.strictEqual(toIsoDateString(0), new Date(0).toISOString());
24
+ });
25
+
26
+ it('returns "" for an invalid date string', () => {
27
+ assert.strictEqual(toIsoDateString("not a date"), "");
28
+ });
29
+
30
+ it('returns "" for an invalid Date', () => {
31
+ assert.strictEqual(toIsoDateString(new Date("not a date")), "");
32
+ });
33
+
34
+ it('returns "" for undefined', () => {
35
+ assert.strictEqual(toIsoDateString(undefined), "");
36
+ });
37
+
38
+ it('returns "" for null', () => {
39
+ assert.strictEqual(toIsoDateString(null), "");
40
+ });
41
+
42
+ it('returns "" for a non-Date value without toISOString instead of throwing', () => {
43
+ assert.strictEqual(toIsoDateString({ foo: "bar" }), "");
44
+ });
45
+ });
46
+
47
+ describe("toInternalDate", () => {
48
+ it("passes through a valid Date", () => {
49
+ const date = new Date("2026-06-21T10:20:30.000Z");
50
+ assert.strictEqual(toInternalDate(date), date);
51
+ });
52
+
53
+ it("parses a valid date string", () => {
54
+ const result = toInternalDate("2026-06-21T10:20:30.000Z");
55
+ assert.ok(result instanceof Date);
56
+ assert.strictEqual(result?.toISOString(), "2026-06-21T10:20:30.000Z");
57
+ });
58
+
59
+ it("parses a numeric epoch", () => {
60
+ const result = toInternalDate(0);
61
+ assert.strictEqual(result?.getTime(), 0);
62
+ });
63
+
64
+ it("returns null for an absent value (transient imapflow row, #408)", () => {
65
+ assert.strictEqual(toInternalDate(null), null);
66
+ assert.strictEqual(toInternalDate(undefined), null);
67
+ });
68
+
69
+ it("falls back to a valid now for an unparseable string, never NaN", () => {
70
+ const result = toInternalDate("not a date");
71
+ assert.ok(result instanceof Date);
72
+ assert.ok(Number.isFinite(result?.getTime()));
73
+ });
74
+
75
+ it("falls back to a valid now for an Invalid Date object", () => {
76
+ const result = toInternalDate(new Date("nonsense"));
77
+ assert.ok(result instanceof Date);
78
+ assert.ok(Number.isFinite(result?.getTime()));
79
+ });
80
+
81
+ it("falls back to a valid now for an unexpected type instead of throwing", () => {
82
+ const result = toInternalDate({ foo: "bar" });
83
+ assert.ok(result instanceof Date);
84
+ assert.ok(Number.isFinite(result?.getTime()));
85
+ });
86
+ });
87
+
88
+ const buildConnectionWithClient = (client: unknown): ImapFlowConnection => {
89
+ const connection = new ImapFlowConnection({
90
+ host: "localhost",
91
+ port: 143,
92
+ user: "alice@example.com",
93
+ credentials: { kind: "password", password: "x" },
94
+ tls: false,
95
+ });
96
+ Object.assign(connection as unknown as Record<string, unknown>, {
97
+ client,
98
+ _state: "authenticated",
99
+ });
100
+ return connection;
101
+ };
102
+
103
+ const fakeMailbox = (path: string, exists: number) => ({
104
+ path,
105
+ delimiter: "/",
106
+ flags: new Set<string>(),
107
+ permanentFlags: new Set<string>(),
108
+ uidValidity: 1n,
109
+ uidNext: exists + 1,
110
+ exists,
111
+ readOnly: true,
112
+ });
113
+
114
+ describe("ImapFlowConnection.getMailboxStatus — deletedCount (#1042)", () => {
115
+ it("returns the SEARCH \\Deleted count alongside the STATUS counts", async () => {
116
+ const searchQueries: Array<Record<string, unknown>> = [];
117
+ const connection = buildConnectionWithClient({
118
+ status: async () => ({
119
+ path: "INBOX",
120
+ messages: 10,
121
+ recent: 0,
122
+ unseen: 3,
123
+ uidNext: 11,
124
+ uidValidity: 1n,
125
+ highestModseq: 0n,
126
+ }),
127
+ mailboxOpen: async (path: string) => fakeMailbox(path, 10),
128
+ search: async (query: Record<string, unknown>) => {
129
+ searchQueries.push(query);
130
+ return query.deleted ? [1, 2] : [];
131
+ },
132
+ });
133
+
134
+ const status = await connection.getMailboxStatus("INBOX");
135
+
136
+ assert.strictEqual(status.messages, 10);
137
+ assert.strictEqual(status.unseen, 3);
138
+ assert.strictEqual(status.deletedCount, 2);
139
+ assert.deepStrictEqual(searchQueries, [{ deleted: true }]);
140
+ });
141
+
142
+ it("reports zero deleted when SEARCH \\Deleted has no matches", async () => {
143
+ const connection = buildConnectionWithClient({
144
+ status: async () => ({
145
+ path: "INBOX",
146
+ messages: 4,
147
+ recent: 0,
148
+ unseen: 0,
149
+ uidNext: 5,
150
+ uidValidity: 1n,
151
+ highestModseq: 0n,
152
+ }),
153
+ mailboxOpen: async (path: string) => fakeMailbox(path, 4),
154
+ search: async () => false,
155
+ });
156
+
157
+ const status = await connection.getMailboxStatus("INBOX");
158
+
159
+ assert.strictEqual(status.deletedCount, 0);
160
+ });
161
+ });