@remit/mailbox-service 0.0.111 → 0.0.113

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 (298) hide show
  1. package/dist/account-credentials.d.ts +83 -0
  2. package/dist/account-credentials.js +104 -0
  3. package/dist/attribute-mapper.d.ts +36 -0
  4. package/dist/attribute-mapper.js +119 -0
  5. package/dist/body-parse.d.ts +28 -0
  6. package/{src/body-parse.ts → dist/body-parse.js} +18 -26
  7. package/dist/body-part-mapper.d.ts +92 -0
  8. package/dist/body-part-mapper.js +301 -0
  9. package/dist/body-sync-queue.d.ts +59 -0
  10. package/dist/body-sync-queue.js +56 -0
  11. package/dist/body-sync-terminal.d.ts +58 -0
  12. package/dist/body-sync-terminal.js +95 -0
  13. package/dist/body-sync.d.ts +351 -0
  14. package/dist/body-sync.js +1204 -0
  15. package/dist/calendar-parts.d.ts +23 -0
  16. package/dist/calendar-parts.js +18 -0
  17. package/dist/classification-backfill.d.ts +91 -0
  18. package/dist/classification-backfill.js +180 -0
  19. package/dist/classify-message.d.ts +31 -0
  20. package/dist/classify-message.js +89 -0
  21. package/dist/connection-factory.d.ts +54 -0
  22. package/dist/connection-factory.js +54 -0
  23. package/dist/connection-test.d.ts +41 -0
  24. package/dist/connection-test.js +152 -0
  25. package/dist/external-move.d.ts +94 -0
  26. package/dist/external-move.js +173 -0
  27. package/dist/filter-operations.d.ts +42 -0
  28. package/dist/filter-operations.js +211 -0
  29. package/dist/filters/filter-config.d.ts +14 -0
  30. package/dist/filters/filter-config.js +10 -0
  31. package/{src/filters/list-id.ts → dist/filters/list-id.d.ts} +2 -16
  32. package/dist/filters/list-id.js +26 -0
  33. package/dist/filters/match.d.ts +69 -0
  34. package/dist/filters/match.js +165 -0
  35. package/dist/filters/pipeline.d.ts +64 -0
  36. package/dist/filters/pipeline.js +100 -0
  37. package/{src/flag-push-terminal.ts → dist/flag-push-terminal.d.ts} +19 -77
  38. package/dist/flag-push-terminal.js +63 -0
  39. package/dist/flag-push.d.ts +69 -0
  40. package/dist/flag-push.js +111 -0
  41. package/dist/flag-queue.d.ts +175 -0
  42. package/dist/flag-queue.js +291 -0
  43. package/dist/folder-operations.d.ts +39 -0
  44. package/dist/folder-operations.js +112 -0
  45. package/dist/folder-placement.d.ts +13 -0
  46. package/dist/folder-placement.js +23 -0
  47. package/dist/heuristics/authenticationResults.d.ts +10 -0
  48. package/dist/heuristics/authenticationResults.js +187 -0
  49. package/dist/heuristics/classifyByHeaders.d.ts +82 -0
  50. package/dist/heuristics/classifyByHeaders.js +365 -0
  51. package/dist/heuristics/classifyPlacement.d.ts +67 -0
  52. package/dist/heuristics/classifyPlacement.js +153 -0
  53. package/dist/heuristics/machineSenders.d.ts +21 -0
  54. package/{src/heuristics/machineSenders.ts → dist/heuristics/machineSenders.js} +34 -40
  55. package/dist/heuristics/senderMismatch.d.ts +81 -0
  56. package/dist/heuristics/senderMismatch.js +192 -0
  57. package/dist/heuristics/socialDomains.d.ts +14 -0
  58. package/{src/heuristics/socialDomains.ts → dist/heuristics/socialDomains.js} +9 -9
  59. package/dist/heuristics/transactionalDomains.d.ts +18 -0
  60. package/{src/heuristics/transactionalDomains.ts → dist/heuristics/transactionalDomains.js} +8 -8
  61. package/dist/imapflow-connection.d.ts +377 -0
  62. package/dist/imapflow-connection.js +1147 -0
  63. package/dist/index.d.ts +63 -0
  64. package/dist/index.js +65 -0
  65. package/dist/list-id-backfill.d.ts +83 -0
  66. package/dist/list-id-backfill.js +132 -0
  67. package/dist/mailbox-cursor-rebuild.d.ts +74 -0
  68. package/dist/mailbox-cursor-rebuild.js +71 -0
  69. package/{src/mailbox-cursor.ts → dist/mailbox-cursor.d.ts} +20 -98
  70. package/dist/mailbox-cursor.js +102 -0
  71. package/dist/mailbox-intent.d.ts +41 -0
  72. package/{src/mailbox-intent.ts → dist/mailbox-intent.js} +39 -63
  73. package/dist/mailbox-management.d.ts +240 -0
  74. package/dist/mailbox-management.js +576 -0
  75. package/dist/mailbox-presence.d.ts +52 -0
  76. package/{src/mailbox-presence.ts → dist/mailbox-presence.js} +19 -30
  77. package/dist/mailbox-queue.d.ts +126 -0
  78. package/dist/mailbox-queue.js +214 -0
  79. package/dist/mailbox-sync.d.ts +93 -0
  80. package/dist/mailbox-sync.js +490 -0
  81. package/dist/mailbox-upstream.d.ts +38 -0
  82. package/{src/mailbox-upstream.ts → dist/mailbox-upstream.js} +37 -47
  83. package/dist/message-category.d.ts +51 -0
  84. package/{src/message-category.ts → dist/message-category.js} +28 -71
  85. package/dist/message-move.d.ts +285 -0
  86. package/dist/message-move.js +858 -0
  87. package/dist/message-parser.d.ts +14 -0
  88. package/dist/message-parser.js +17 -0
  89. package/{src/message-presence.ts → dist/message-presence.d.ts} +1 -13
  90. package/dist/message-presence.js +29 -0
  91. package/dist/message-read.d.ts +51 -0
  92. package/dist/message-read.js +31 -0
  93. package/dist/message-sync.d.ts +345 -0
  94. package/dist/message-sync.js +1358 -0
  95. package/dist/mime-walker.d.ts +102 -0
  96. package/dist/mime-walker.js +198 -0
  97. package/dist/organize-job.d.ts +20 -0
  98. package/dist/organize-job.js +37 -0
  99. package/dist/outbox-attachment-content.d.ts +37 -0
  100. package/dist/outbox-attachment-content.js +50 -0
  101. package/dist/outbox-attachment-filename.d.ts +19 -0
  102. package/dist/outbox-attachment-filename.js +64 -0
  103. package/dist/outbox-attachment.d.ts +148 -0
  104. package/dist/outbox-attachment.js +273 -0
  105. package/dist/outbox-queue.d.ts +73 -0
  106. package/dist/outbox-queue.js +284 -0
  107. package/{src/outbox-status.ts → dist/outbox-status.d.ts} +1 -6
  108. package/dist/outbox-status.js +18 -0
  109. package/dist/ownership.d.ts +24 -0
  110. package/dist/ownership.js +83 -0
  111. package/dist/placement-move-terminal.d.ts +67 -0
  112. package/dist/placement-move-terminal.js +88 -0
  113. package/dist/placement-move.d.ts +83 -0
  114. package/dist/placement-move.js +216 -0
  115. package/{src/placement-settled.ts → dist/placement-settled.d.ts} +19 -50
  116. package/dist/placement-settled.js +75 -0
  117. package/dist/quarantine.d.ts +95 -0
  118. package/dist/quarantine.js +131 -0
  119. package/dist/refusal.d.ts +31 -0
  120. package/dist/refusal.js +3 -0
  121. package/{src/restore-source-placement.ts → dist/restore-source-placement.d.ts} +29 -101
  122. package/dist/restore-source-placement.js +79 -0
  123. package/dist/search-text-backfill.d.ts +15 -0
  124. package/dist/search-text-backfill.js +33 -0
  125. package/dist/snippet.d.ts +61 -0
  126. package/dist/snippet.js +129 -0
  127. package/dist/spam-report.d.ts +86 -0
  128. package/dist/spam-report.js +241 -0
  129. package/dist/sync-warning.d.ts +21 -0
  130. package/dist/sync-warning.js +26 -0
  131. package/dist/sync-watermarks.d.ts +138 -0
  132. package/dist/sync-watermarks.js +205 -0
  133. package/dist/test-helpers/fake-imap.d.ts +151 -0
  134. package/dist/test-helpers/fake-imap.js +508 -0
  135. package/dist/test-helpers/folder-roles.d.ts +34 -0
  136. package/dist/test-helpers/folder-roles.js +47 -0
  137. package/dist/test-helpers/mime-tree-generator.d.ts +82 -0
  138. package/dist/test-helpers/mime-tree-generator.js +329 -0
  139. package/dist/test-helpers/test-store.d.ts +69 -0
  140. package/dist/test-helpers/test-store.js +193 -0
  141. package/dist/text/normalizer.d.ts +20 -0
  142. package/dist/text/normalizer.js +102 -0
  143. package/dist/thread-message-rows.d.ts +69 -0
  144. package/dist/thread-message-rows.js +40 -0
  145. package/dist/thread-root.d.ts +7 -0
  146. package/dist/thread-root.js +25 -0
  147. package/dist/types.d.ts +343 -0
  148. package/dist/types.js +19 -0
  149. package/package.json +86 -73
  150. package/src/account-credentials.ts +0 -210
  151. package/src/adversarial-mime.e2e.test.ts +0 -307
  152. package/src/attribute-mapper.test.ts +0 -104
  153. package/src/attribute-mapper.ts +0 -148
  154. package/src/body-parse.test.ts +0 -55
  155. package/src/body-part-mapper.property.test.ts +0 -354
  156. package/src/body-part-mapper.ts +0 -360
  157. package/src/body-sync-auto-read-writeonce.test.ts +0 -219
  158. package/src/body-sync-calendar-suggestion.test.ts +0 -433
  159. package/src/body-sync-category-denormalize.test.ts +0 -352
  160. package/src/body-sync-category-override.test.ts +0 -501
  161. package/src/body-sync-category-writeonce.test.ts +0 -234
  162. package/src/body-sync-filter.test.ts +0 -237
  163. package/src/body-sync-message-gone.test.ts +0 -139
  164. package/src/body-sync-placement-flags.test.ts +0 -347
  165. package/src/body-sync-placement-writeonce.test.ts +0 -337
  166. package/src/body-sync-quarantine.test.ts +0 -276
  167. package/src/body-sync-queue.test.ts +0 -124
  168. package/src/body-sync-queue.ts +0 -131
  169. package/src/body-sync-resync.test.ts +0 -605
  170. package/src/body-sync-terminal.test.ts +0 -310
  171. package/src/body-sync-terminal.ts +0 -176
  172. package/src/body-sync-unclassifiable-once.test.ts +0 -260
  173. package/src/body-sync-unsubscribed-auto-read.test.ts +0 -176
  174. package/src/body-sync.ts +0 -1970
  175. package/src/calendar-parts.ts +0 -41
  176. package/src/classification-backfill.test.ts +0 -615
  177. package/src/classification-backfill.ts +0 -363
  178. package/src/classify-message.test.ts +0 -205
  179. package/src/classify-message.ts +0 -116
  180. package/src/connection-factory.ts +0 -102
  181. package/src/connection-test.ts +0 -208
  182. package/src/credentials.test.ts +0 -181
  183. package/src/external-move.test.ts +0 -369
  184. package/src/external-move.ts +0 -272
  185. package/src/filters/anchor-drift.ts +0 -55
  186. package/src/filters/filter-config.test.ts +0 -229
  187. package/src/filters/filter-config.ts +0 -69
  188. package/src/filters/list-id.test.ts +0 -50
  189. package/src/filters/match.test.ts +0 -429
  190. package/src/filters/match.ts +0 -274
  191. package/src/filters/message-embedder.test.ts +0 -39
  192. package/src/filters/message-embedder.ts +0 -33
  193. package/src/filters/pipeline.test.ts +0 -330
  194. package/src/filters/pipeline.ts +0 -284
  195. package/src/flag-push-terminal.test.ts +0 -209
  196. package/src/flag-push.test.ts +0 -408
  197. package/src/flag-push.ts +0 -198
  198. package/src/flag-queue-stars.test.ts +0 -171
  199. package/src/flag-queue.ts +0 -509
  200. package/src/folder-placement.ts +0 -56
  201. package/src/heuristics/authenticationResults.test.ts +0 -189
  202. package/src/heuristics/authenticationResults.ts +0 -212
  203. package/src/heuristics/classifyByHeaders.test.ts +0 -889
  204. package/src/heuristics/classifyByHeaders.ts +0 -467
  205. package/src/heuristics/classifyPlacement.test.ts +0 -366
  206. package/src/heuristics/classifyPlacement.ts +0 -211
  207. package/src/heuristics/classifyRealMail.test.ts +0 -242
  208. package/src/heuristics/machineSenders.test.ts +0 -101
  209. package/src/heuristics/senderMismatch.test.ts +0 -346
  210. package/src/heuristics/senderMismatch.ts +0 -259
  211. package/src/imapflow-connect-retry.test.ts +0 -134
  212. package/src/imapflow-connection.e2e.test.ts +0 -335
  213. package/src/imapflow-connection.integ.test.ts +0 -316
  214. package/src/imapflow-connection.test.ts +0 -469
  215. package/src/imapflow-connection.ts +0 -1475
  216. package/src/index.ts +0 -350
  217. package/src/list-id-backfill.test.ts +0 -573
  218. package/src/list-id-backfill.ts +0 -274
  219. package/src/mailbox-cursor-rebuild.test.ts +0 -171
  220. package/src/mailbox-cursor-rebuild.ts +0 -146
  221. package/src/mailbox-cursor.test.ts +0 -249
  222. package/src/mailbox-delete-intent.test.ts +0 -246
  223. package/src/mailbox-management.test.ts +0 -356
  224. package/src/mailbox-management.ts +0 -839
  225. package/src/mailbox-presence.test.ts +0 -72
  226. package/src/mailbox-queue.ts +0 -415
  227. package/src/mailbox-rename-intent.test.ts +0 -389
  228. package/src/mailbox-rename-subtree.test.ts +0 -664
  229. package/src/mailbox-sync.test.ts +0 -848
  230. package/src/mailbox-sync.ts +0 -724
  231. package/src/message-delete-junk-reconcile.test.ts +0 -117
  232. package/src/message-delete-no-trash.test.ts +0 -245
  233. package/src/message-delete-unsettled-placement.test.ts +0 -396
  234. package/src/message-move-copy.test.ts +0 -562
  235. package/src/message-move-empty-trash.test.ts +0 -326
  236. package/src/message-move-same-mailbox.test.ts +0 -139
  237. package/src/message-move-unsettled-placement.test.ts +0 -378
  238. package/src/message-move.ts +0 -1348
  239. package/src/message-parser.ts +0 -26
  240. package/src/message-presence.test.ts +0 -50
  241. package/src/message-sync-address-spoof.test.ts +0 -293
  242. package/src/message-sync-category.test.ts +0 -142
  243. package/src/message-sync-changedsince.test.ts +0 -840
  244. package/src/message-sync-flags.test.ts +0 -158
  245. package/src/message-sync-junk-harvest.test.ts +0 -244
  246. package/src/message-sync-quarantine.test.ts +0 -416
  247. package/src/message-sync-repoint.test.ts +0 -419
  248. package/src/message-sync.ts +0 -1963
  249. package/src/mime-walker.test.ts +0 -311
  250. package/src/mime-walker.ts +0 -305
  251. package/src/outbox-attachment-content.test.ts +0 -153
  252. package/src/outbox-attachment-content.ts +0 -104
  253. package/src/outbox-attachment-filename.test.ts +0 -132
  254. package/src/outbox-attachment-filename.ts +0 -76
  255. package/src/outbox-attachment.test.ts +0 -738
  256. package/src/outbox-attachment.ts +0 -536
  257. package/src/outbox-failed-recovery.test.ts +0 -370
  258. package/src/outbox-queue-settled-row.test.ts +0 -140
  259. package/src/outbox-queue.test.ts +0 -291
  260. package/src/outbox-queue.ts +0 -508
  261. package/src/pass-through-unit-of-work.ts +0 -17
  262. package/src/placement-move-terminal.test.ts +0 -300
  263. package/src/placement-move-terminal.ts +0 -148
  264. package/src/placement-move-unsettled.test.ts +0 -275
  265. package/src/placement-move.ts +0 -336
  266. package/src/placement-settled.test.ts +0 -147
  267. package/src/placement-transition-race.test.ts +0 -482
  268. package/src/quarantine.test.ts +0 -191
  269. package/src/quarantine.ts +0 -228
  270. package/src/snippet.test.ts +0 -213
  271. package/src/snippet.ts +0 -152
  272. package/src/spam-report.test.ts +0 -801
  273. package/src/spam-report.ts +0 -357
  274. package/src/stale-message-reconcile.test.ts +0 -82
  275. package/src/stale-message-reconcile.ts +0 -55
  276. package/src/sync-watermarks.test.ts +0 -409
  277. package/src/sync-watermarks.ts +0 -319
  278. package/src/system-flag-wire-format.test.ts +0 -77
  279. package/src/test-helpers/folder-roles.ts +0 -66
  280. package/src/test-helpers/isolated-mailbox.ts +0 -44
  281. package/src/test-helpers/mailfuzz-connection.ts +0 -27
  282. package/src/test-helpers/mime-tree-generator.ts +0 -461
  283. package/src/text/normalizer.test.ts +0 -223
  284. package/src/text/normalizer.ts +0 -140
  285. package/src/thread-message-rows.ts +0 -77
  286. package/src/types/mailparser-augment.d.ts +0 -15
  287. package/src/types/natural-porter-stemmer.d.ts +0 -50
  288. package/src/types/stopword.d.ts +0 -69
  289. package/src/types.ts +0 -371
  290. package/test/fixtures/mime-adversarial-e2e/01-octet-stream-pdf.eml +0 -21
  291. package/test/fixtures/mime-adversarial-e2e/02-nested-related-cid-image.eml +0 -31
  292. package/test/fixtures/mime-adversarial-e2e/03-calendar-invite.eml +0 -33
  293. package/test/fixtures/mime-adversarial-e2e/04-forwarded-rfc822-with-attachments.eml +0 -42
  294. package/test/fixtures/mime-adversarial-e2e/05-tnef-winmail.eml +0 -25
  295. package/test/fixtures/mime-adversarial-e2e/06-8bit-non-ascii.eml +0 -15
  296. package/test/fixtures/mime-adversarial-e2e/07-quoted-printable-soft-breaks.eml +0 -17
  297. package/test/fixtures/mime-adversarial-e2e/08-alternative-empty-text.eml +0 -18
  298. package/tsconfig.json +0 -8
@@ -1,335 +0,0 @@
1
- import assert from "node:assert";
2
- import { after, before, describe, test } from "node:test";
3
- import {
4
- seedMailbox,
5
- uniqueMailboxName,
6
- } from "./test-helpers/isolated-mailbox.js";
7
- import {
8
- createMailfuzzConnection,
9
- withMailfuzzConnection,
10
- } from "./test-helpers/mailfuzz-connection.js";
11
-
12
- describe("Mailbox listing (Dovecot)", {
13
- skip: !process.env.RUN_E2E_TESTS,
14
- }, () => {
15
- test("lists mailboxes and INBOX exists", async () => {
16
- await withMailfuzzConnection(async (connection) => {
17
- const mailboxes = await connection.listMailboxes();
18
-
19
- assert.ok(mailboxes.length > 0, "Should have at least one mailbox");
20
-
21
- const inbox = mailboxes.find((m) => m.fullPath === "INBOX");
22
- assert.ok(inbox, "INBOX should exist in mailbox list");
23
- assert.equal(inbox.name, "INBOX");
24
- });
25
- });
26
-
27
- test("returns mailbox status with uidValidity and uidNext", async () => {
28
- await withMailfuzzConnection(async (connection) => {
29
- const status = await connection.getMailboxStatus("INBOX");
30
-
31
- assert.ok(status.uidValidity > 0, "uidValidity should be positive");
32
- assert.ok(status.uidNext > 0, "uidNext should be positive");
33
- assert.ok(status.messages > 0, "INBOX should have messages");
34
- });
35
- });
36
- });
37
-
38
- describe("Message fetch (Dovecot)", {
39
- skip: !process.env.RUN_E2E_TESTS,
40
- }, () => {
41
- test("fetches messages with ENVELOPE data", async () => {
42
- await withMailfuzzConnection(async (connection) => {
43
- const boxStatus = await connection.openBox("INBOX", true);
44
- assert.ok(boxStatus.messages.total > 0, "INBOX should have messages");
45
-
46
- const uids = await connection.search(["ALL"]);
47
- assert.ok(uids.length > 0, "Should find messages");
48
-
49
- const fetchUids = uids.slice(0, 5);
50
- const messages = await connection.fetchMessages(fetchUids);
51
-
52
- assert.equal(
53
- messages.length,
54
- fetchUids.length,
55
- "Should fetch all requested messages",
56
- );
57
-
58
- for (const msg of messages) {
59
- assert.ok(msg.uid > 0, "Message should have a UID");
60
- assert.ok(msg.envelope, "Message should have envelope data");
61
- assert.ok(msg.envelope.subject, "Envelope should have a subject");
62
- assert.ok(msg.envelope.messageId, "Envelope should have a messageId");
63
- assert.ok(msg.envelope.date, "Envelope should have a date");
64
- assert.ok(
65
- msg.envelope.from.length > 0,
66
- "Envelope should have from addresses",
67
- );
68
- assert.ok(
69
- msg.envelope.to.length > 0,
70
- "Envelope should have to addresses",
71
- );
72
-
73
- for (const addr of msg.envelope.from) {
74
- assert.ok(addr.mailbox, "From address should have mailbox");
75
- assert.ok(addr.host, "From address should have host");
76
- }
77
- }
78
- });
79
- });
80
-
81
- test("fetched message count matches status", async () => {
82
- // Seed a mailbox this test owns so the SELECT-vs-SEARCH invariant holds
83
- // regardless of what other e2e files do in parallel — the TOCTOU race
84
- // that #501 papered over with --test-concurrency=1 is removed by
85
- // isolation, not serialisation (#508).
86
- const mailbox = uniqueMailboxName("E2E_count");
87
- await withMailfuzzConnection(async (connection) => {
88
- await seedMailbox(connection, mailbox, 3);
89
- try {
90
- const boxStatus = await connection.openBox(mailbox, true);
91
- const uids = await connection.search(["ALL"]);
92
-
93
- assert.equal(
94
- uids.length,
95
- boxStatus.messages.total,
96
- "Search ALL count should match SELECT messages count",
97
- );
98
- } finally {
99
- await connection.closeBox().catch(() => {});
100
- await connection.deleteMailbox(mailbox).catch(() => {});
101
- }
102
- });
103
- });
104
- });
105
-
106
- describe("Message bodies (Dovecot)", {
107
- skip: !process.env.RUN_E2E_TESTS,
108
- }, () => {
109
- test("fetches message body with text content", async () => {
110
- await withMailfuzzConnection(async (connection) => {
111
- await connection.openBox("INBOX", true);
112
- const uids = await connection.search(["ALL"]);
113
- assert.ok(uids.length > 0, "Should have messages to fetch body for");
114
-
115
- const body = await connection.fetchMessageBody(uids[0]);
116
- assert.ok(body && body.length > 0, "Body should not be empty");
117
-
118
- const bodyText = body.toString("utf-8");
119
- assert.ok(bodyText.length > 0, "Body text should not be empty");
120
- });
121
- });
122
- });
123
-
124
- describe("Flag operations (Dovecot)", {
125
- skip: !process.env.RUN_E2E_TESTS,
126
- // These three subtests all mutate uids[0] of the same seeded mailbox, so
127
- // they must run sequentially relative to each other. node:test already
128
- // serialises subtests within a describe; concurrency: 1 pins that intent.
129
- // Cross-file isolation (a dedicated seeded mailbox, not the shared INBOX)
130
- // is what makes the whole suite safe to run in parallel — see #508.
131
- concurrency: 1,
132
- }, () => {
133
- let targetUid: number;
134
- const mailbox = uniqueMailboxName("E2E_flags");
135
-
136
- before(async () => {
137
- await withMailfuzzConnection(async (connection) => {
138
- await seedMailbox(connection, mailbox, 3);
139
- });
140
- });
141
-
142
- after(async () => {
143
- const connection = createMailfuzzConnection();
144
- await connection.connect();
145
- await connection.deleteMailbox(mailbox).catch(() => {});
146
- if (connection.isConnected) {
147
- await connection.disconnect();
148
- }
149
- });
150
-
151
- test("sets \\Seen flag and verifies it persists", async () => {
152
- await withMailfuzzConnection(async (connection) => {
153
- await connection.openBox(mailbox, false);
154
- const uids = await connection.search(["ALL"]);
155
- assert.ok(uids.length > 0, "Should have messages");
156
- targetUid = uids[0];
157
-
158
- await connection.removeFlags([targetUid], ["\\Seen"]);
159
- await connection.addFlags([targetUid], ["\\Seen"]);
160
-
161
- const messages = await connection.fetchMessages([targetUid]);
162
- assert.equal(messages.length, 1);
163
- assert.ok(
164
- messages[0].flags.includes("\\Seen"),
165
- "Message should have \\Seen flag",
166
- );
167
- });
168
- });
169
-
170
- test("clears \\Seen flag and verifies it is cleared", async () => {
171
- await withMailfuzzConnection(async (connection) => {
172
- await connection.openBox(mailbox, false);
173
- const uids = await connection.search(["ALL"]);
174
- targetUid = uids[0];
175
-
176
- await connection.addFlags([targetUid], ["\\Seen"]);
177
- await connection.removeFlags([targetUid], ["\\Seen"]);
178
-
179
- const messages = await connection.fetchMessages([targetUid]);
180
- assert.equal(messages.length, 1);
181
- assert.ok(
182
- !messages[0].flags.includes("\\Seen"),
183
- "Message should not have \\Seen flag after removal",
184
- );
185
- });
186
- });
187
-
188
- test("sets custom keyword flag", async () => {
189
- await withMailfuzzConnection(async (connection) => {
190
- await connection.openBox(mailbox, false);
191
- const uids = await connection.search(["ALL"]);
192
- targetUid = uids[0];
193
-
194
- await connection.addFlags([targetUid], ["$label1"]);
195
-
196
- const messages = await connection.fetchMessages([targetUid]);
197
- assert.equal(messages.length, 1);
198
- assert.ok(
199
- messages[0].flags.includes("$label1"),
200
- "Message should have custom keyword $label1",
201
- );
202
-
203
- await connection.removeFlags([targetUid], ["$label1"]);
204
- });
205
- });
206
- });
207
-
208
- describe("Message operations (Dovecot)", {
209
- skip: !process.env.RUN_E2E_TESTS,
210
- }, () => {
211
- const testFolder = uniqueMailboxName("E2E_TestCopy");
212
- // Copy/move source their messages from a mailbox this file owns rather than
213
- // the shared INBOX, so nothing here appends to or mutates INBOX (#508).
214
- const sourceFolder = uniqueMailboxName("E2E_TestSource");
215
-
216
- before(async () => {
217
- await withMailfuzzConnection(async (connection) => {
218
- await seedMailbox(connection, sourceFolder, 3);
219
- });
220
- });
221
-
222
- test("copy returns COPYUID response with uidMap entries", async () => {
223
- await withMailfuzzConnection(async (connection) => {
224
- await connection.createMailbox(testFolder);
225
-
226
- await connection.openBox(sourceFolder, false);
227
- const uids = await connection.search(["ALL"]);
228
- assert.ok(uids.length > 0, "Should have messages to copy");
229
-
230
- const result = await connection.copyMessages([uids[0]], testFolder);
231
-
232
- assert.equal(result.destination, testFolder);
233
- assert.ok(
234
- result.uidMap.size > 0,
235
- "COPYUID response should have uidMap entries",
236
- );
237
- assert.ok(
238
- result.uidMap.has(uids[0]),
239
- "uidMap should contain the source UID",
240
- );
241
- });
242
- });
243
-
244
- test("move returns COPYUID response with uidMap entries", async () => {
245
- const moveFolder = uniqueMailboxName("E2E_TestMove");
246
-
247
- await withMailfuzzConnection(async (connection) => {
248
- await connection.createMailbox(moveFolder);
249
-
250
- const appendResult = await connection.append(
251
- sourceFolder,
252
- [
253
- "From: test@example.com",
254
- "To: vmail@localhost",
255
- "Subject: Move test message",
256
- `Date: ${new Date().toUTCString()}`,
257
- `Message-ID: <move-test-${Date.now()}@test.example.com>`,
258
- "",
259
- "Body for move test",
260
- ].join("\r\n"),
261
- );
262
-
263
- await connection.openBox(sourceFolder, false);
264
- const result = await connection.moveMessages(
265
- [appendResult.uid],
266
- moveFolder,
267
- );
268
-
269
- assert.equal(result.destination, moveFolder);
270
- assert.ok(
271
- result.uidMap.size > 0,
272
- "COPYUID response should have uidMap entries",
273
- );
274
-
275
- await connection.closeBox();
276
- await connection.deleteMailbox(moveFolder);
277
- });
278
- });
279
-
280
- after(async () => {
281
- const connection = createMailfuzzConnection();
282
- await connection.connect();
283
- await connection.deleteMailbox(testFolder).catch(() => {});
284
- await connection.deleteMailbox(sourceFolder).catch(() => {});
285
- if (connection.isConnected) {
286
- await connection.disconnect();
287
- }
288
- });
289
- });
290
-
291
- describe("Mailbox CRUD (Dovecot)", { skip: !process.env.RUN_E2E_TESTS }, () => {
292
- const crudFolder = `CrudTest_${Date.now()}`;
293
- const renamedFolder = `CrudRenamed_${Date.now()}`;
294
-
295
- test("creates a new mailbox", async () => {
296
- await withMailfuzzConnection(async (connection) => {
297
- const result = await connection.createMailbox(crudFolder);
298
- assert.ok(result.created, "Mailbox should be created");
299
-
300
- const mailboxes = await connection.listMailboxes();
301
- const found = mailboxes.find((m) => m.fullPath === crudFolder);
302
- assert.ok(found, `Mailbox ${crudFolder} should exist after creation`);
303
- });
304
- });
305
-
306
- test("renames a mailbox", async () => {
307
- await withMailfuzzConnection(async (connection) => {
308
- const result = await connection.renameMailbox(crudFolder, renamedFolder);
309
- assert.ok(result.newPath, "Should return new path");
310
-
311
- const mailboxes = await connection.listMailboxes();
312
- const oldFound = mailboxes.find((m) => m.fullPath === crudFolder);
313
- assert.ok(
314
- !oldFound,
315
- `Old mailbox ${crudFolder} should not exist after rename`,
316
- );
317
-
318
- const newFound = mailboxes.find((m) => m.fullPath === renamedFolder);
319
- assert.ok(newFound, `Renamed mailbox ${renamedFolder} should exist`);
320
- });
321
- });
322
-
323
- test("deletes a mailbox", async () => {
324
- await withMailfuzzConnection(async (connection) => {
325
- await connection.deleteMailbox(renamedFolder);
326
-
327
- const mailboxes = await connection.listMailboxes();
328
- const found = mailboxes.find((m) => m.fullPath === renamedFolder);
329
- assert.ok(
330
- !found,
331
- `Mailbox ${renamedFolder} should not exist after deletion`,
332
- );
333
- });
334
- });
335
- });
@@ -1,316 +0,0 @@
1
- /**
2
- * Integration tests for ImapFlowConnection using mokapi.
3
- *
4
- * These tests require mokapi to be running:
5
- * npm run start:mokapi
6
- *
7
- * Run with:
8
- * npm run test:integ -w packages/mailbox-service
9
- *
10
- * Note: mokapi has some limitations:
11
- * - Doesn't return COPYUID responses (uidMap is empty)
12
- * - Doesn't return ENVELOPE data
13
- * - Closes connection after APPEND
14
- * - Requires mailbox close/reopen to see changes
15
- */
16
-
17
- import assert from "node:assert";
18
- import { describe, test } from "node:test";
19
- import { ImapFlowConnection } from "./imapflow-connection.js";
20
-
21
- const MOKAPI_CONFIG = {
22
- host: "localhost",
23
- port: 143,
24
- user: "alice@mokapi.io",
25
- credentials: { kind: "password" as const, password: "alice123" },
26
- tls: false,
27
- };
28
-
29
- /**
30
- * Helper to create a connection.
31
- */
32
- const createConnection = (): ImapFlowConnection => {
33
- return new ImapFlowConnection(MOKAPI_CONFIG);
34
- };
35
-
36
- /**
37
- * Helper to run a test with a managed connection.
38
- * Creates a connection, runs the test, and ensures cleanup.
39
- */
40
- const withConnection = async (
41
- fn: (connection: ImapFlowConnection) => Promise<void>,
42
- ): Promise<void> => {
43
- const connection = createConnection();
44
- await connection.connect();
45
- await fn(connection).finally(() => {
46
- if (connection.isConnected) {
47
- return connection.disconnect();
48
- }
49
- });
50
- };
51
-
52
- /**
53
- * Helper to append a test message to a mailbox and find its UID.
54
- * Works around mokapi closing connection after APPEND by using
55
- * a separate connection for seeding.
56
- *
57
- * @returns The UID of the appended message
58
- */
59
- const seedTestMessage = async (
60
- mailbox: string,
61
- subject: string,
62
- ): Promise<number> => {
63
- const seedConn = createConnection();
64
- await seedConn.connect();
65
-
66
- const message = [
67
- "From: test@example.com",
68
- "To: alice@mokapi.io",
69
- `Subject: ${subject}`,
70
- `Date: ${new Date().toUTCString()}`,
71
- `Message-ID: <${Date.now()}-${Math.random()}@test.example.com>`,
72
- "",
73
- `Test message body for ${subject}`,
74
- ].join("\r\n");
75
-
76
- // Append the message (connection may close after this in mokapi)
77
- await seedConn.append(mailbox, message);
78
-
79
- // mokapi closes connection after APPEND, so reconnect to find the UID
80
- const findConn = createConnection();
81
- await findConn.connect();
82
- await findConn.openBox(mailbox, true);
83
- const uids = await findConn.search(["ALL"]);
84
-
85
- // Find the message by fetching headers (mokapi doesn't support ENVELOPE)
86
- // biome-ignore lint/suspicious/noExplicitAny: Accessing private client for test purposes
87
- const client = (findConn as any).client;
88
- let foundUid: number | undefined;
89
-
90
- for await (const msg of client.fetch(
91
- uids.slice(-10).join(","),
92
- { uid: true, headers: ["subject"] },
93
- { uid: true },
94
- )) {
95
- const headerText = msg.headers?.toString() || "";
96
- const subjectMatch = headerText.match(/^subject:\s*(.+)$/im);
97
- if (subjectMatch && subjectMatch[1].trim() === subject) {
98
- foundUid = msg.uid;
99
- break;
100
- }
101
- }
102
-
103
- await findConn.disconnect();
104
-
105
- if (!foundUid) {
106
- throw new Error(`Could not find seeded message with subject: ${subject}`);
107
- }
108
-
109
- return foundUid;
110
- };
111
-
112
- /**
113
- * Helper to count messages in a mailbox using a fresh connection.
114
- * This works around mokapi's caching by using a new connection.
115
- */
116
- const countMessagesInMailbox = async (mailbox: string): Promise<number> => {
117
- const conn = createConnection();
118
- await conn.connect();
119
- await conn.openBox(mailbox, true);
120
- const uids = await conn.search(["ALL"]);
121
- await conn.disconnect();
122
- return uids.length;
123
- };
124
-
125
- /**
126
- * Helper to get all UIDs in a mailbox using a fresh connection.
127
- */
128
- const getMailboxUids = async (mailbox: string): Promise<number[]> => {
129
- const conn = createConnection();
130
- await conn.connect();
131
- await conn.openBox(mailbox, true);
132
- const uids = await conn.search(["ALL"]);
133
- await conn.disconnect();
134
- return uids;
135
- };
136
-
137
- describe("ImapFlowConnection integration tests", {
138
- skip: !process.env.RUN_INTEG_TESTS,
139
- }, () => {
140
- describe("moveMessages", () => {
141
- test("moves a message to another mailbox", async () => {
142
- // Seed the test message first
143
- const subject = `Move test ${Date.now()}`;
144
- const uid = await seedTestMessage("INBOX", subject);
145
-
146
- // Count messages in Work before
147
- const workCountBefore = await countMessagesInMailbox("Work");
148
-
149
- await withConnection(async (connection) => {
150
- // Open INBOX
151
- await connection.openBox("INBOX", false);
152
-
153
- // Move to Work folder
154
- const result = await connection.moveMessages([uid], "Work");
155
-
156
- // mokapi doesn't return uidMap, but should return destination
157
- assert.equal(result.destination, "Work", "Destination should be Work");
158
- });
159
-
160
- // Verify message moved using fresh connections (mokapi caching workaround)
161
- const inboxUids = await getMailboxUids("INBOX");
162
- assert.ok(
163
- !inboxUids.includes(uid),
164
- "Original UID should not be in INBOX",
165
- );
166
-
167
- const workCountAfter = await countMessagesInMailbox("Work");
168
- assert.ok(
169
- workCountAfter > workCountBefore,
170
- `Work folder should have more messages (before: ${workCountBefore}, after: ${workCountAfter})`,
171
- );
172
- });
173
-
174
- test("returns empty uidMap when no UIDs provided", async () => {
175
- await withConnection(async (connection) => {
176
- await connection.openBox("INBOX", false);
177
-
178
- const result = await connection.moveMessages([], "Work");
179
-
180
- assert.equal(result.destination, "Work");
181
- assert.equal(result.uidMap.size, 0);
182
- });
183
- });
184
- });
185
-
186
- describe("copyMessages", () => {
187
- test("copies a message to another mailbox", async () => {
188
- // Seed the test message first
189
- const subject = `Copy test ${Date.now()}`;
190
- const uid = await seedTestMessage("INBOX", subject);
191
-
192
- // Count messages in Archive before
193
- const archiveCountBefore = await countMessagesInMailbox("Archive");
194
-
195
- await withConnection(async (connection) => {
196
- // Open INBOX
197
- await connection.openBox("INBOX", false);
198
-
199
- // Copy to Archive folder
200
- const result = await connection.copyMessages([uid], "Archive");
201
-
202
- assert.equal(
203
- result.destination,
204
- "Archive",
205
- "Destination should be Archive",
206
- );
207
- });
208
-
209
- // Verify original message is still in INBOX
210
- const inboxUids = await getMailboxUids("INBOX");
211
- assert.ok(
212
- inboxUids.includes(uid),
213
- "Original UID should still be in INBOX",
214
- );
215
-
216
- // Verify copy is in Archive (count increased)
217
- const archiveCountAfter = await countMessagesInMailbox("Archive");
218
- assert.ok(
219
- archiveCountAfter > archiveCountBefore,
220
- `Archive folder should have more messages (before: ${archiveCountBefore}, after: ${archiveCountAfter})`,
221
- );
222
-
223
- // Cleanup: delete the original from INBOX
224
- await withConnection(async (connection) => {
225
- await connection.openBox("INBOX", false);
226
- await connection.deleteMessages([uid]);
227
- });
228
- });
229
-
230
- test("returns empty uidMap when no UIDs provided", async () => {
231
- await withConnection(async (connection) => {
232
- await connection.openBox("INBOX", false);
233
-
234
- const result = await connection.copyMessages([], "Archive");
235
-
236
- assert.equal(result.destination, "Archive");
237
- assert.equal(result.uidMap.size, 0);
238
- });
239
- });
240
- });
241
-
242
- describe("deleteMessages", () => {
243
- test("permanently deletes a message", async () => {
244
- // Seed the test message first
245
- const subject = `Delete test ${Date.now()}`;
246
- const uid = await seedTestMessage("INBOX", subject);
247
-
248
- // Verify message exists
249
- let uids = await getMailboxUids("INBOX");
250
- assert.ok(uids.includes(uid), "Message should exist before delete");
251
-
252
- await withConnection(async (connection) => {
253
- // Open INBOX
254
- await connection.openBox("INBOX", false);
255
-
256
- // Delete the message
257
- const deleted = await connection.deleteMessages([uid]);
258
- // mokapi may return true instead of count
259
- assert.ok(deleted, "Should report message deleted");
260
-
261
- // mokapi requires mailbox close for delete to persist
262
- await connection.closeBox();
263
-
264
- // Verify within same connection
265
- await connection.openBox("INBOX", true);
266
- uids = await connection.search(["ALL"]);
267
- assert.ok(!uids.includes(uid), "Message should not exist after delete");
268
- });
269
- });
270
-
271
- test("returns 0 when no UIDs provided", async () => {
272
- await withConnection(async (connection) => {
273
- await connection.openBox("INBOX", false);
274
-
275
- const deleted = await connection.deleteMessages([]);
276
- assert.equal(deleted, 0);
277
- });
278
- });
279
- });
280
-
281
- describe("move to Trash workflow", () => {
282
- test("moves message to Trash folder", async () => {
283
- // Seed the test message first
284
- const subject = `Trash test ${Date.now()}`;
285
- const uid = await seedTestMessage("INBOX", subject);
286
-
287
- // Count messages in Trash before
288
- const trashCountBefore = await countMessagesInMailbox("Trash");
289
-
290
- await withConnection(async (connection) => {
291
- // Open INBOX
292
- await connection.openBox("INBOX", false);
293
-
294
- // Move to Trash
295
- const result = await connection.moveMessages([uid], "Trash");
296
-
297
- assert.equal(
298
- result.destination,
299
- "Trash",
300
- "Destination should be Trash",
301
- );
302
- });
303
-
304
- // Verify message is no longer in INBOX
305
- const inboxUids = await getMailboxUids("INBOX");
306
- assert.ok(!inboxUids.includes(uid), "Message should not be in INBOX");
307
-
308
- // Verify message is in Trash (count increased)
309
- const trashCountAfter = await countMessagesInMailbox("Trash");
310
- assert.ok(
311
- trashCountAfter > trashCountBefore,
312
- `Trash folder should have more messages (before: ${trashCountBefore}, after: ${trashCountAfter})`,
313
- );
314
- });
315
- });
316
- });