@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,300 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { describe, it } from "node:test";
3
- import type {
4
- MessageItem,
5
- PlacementPredicate,
6
- PlacementTransitionInput,
7
- } from "@remit/data-ports";
8
- import {
9
- MessageMutation,
10
- MessageStatus,
11
- MessageSyncStatus,
12
- } from "@remit/domain-enums";
13
- import type { PlacementMoveLogger } from "./placement-move.js";
14
- import {
15
- type ResolveExhaustedPlacementMoveDeps,
16
- resolveExhaustedPlacementMoveFailure,
17
- } from "./placement-move-terminal.js";
18
- import type { IImapConnection } from "./types.js";
19
-
20
- interface LogEntry {
21
- obj: Record<string, unknown>;
22
- msg: string;
23
- }
24
-
25
- interface Transition {
26
- messageId: string;
27
- expected: PlacementPredicate;
28
- next: PlacementTransitionInput;
29
- }
30
-
31
- interface Recorder {
32
- deps: ResolveExhaustedPlacementMoveDeps;
33
- infos: LogEntry[];
34
- errors: LogEntry[];
35
- markerDeletes: string[];
36
- deletedMessages: string[];
37
- deletedThreadMessages: Array<{
38
- accountConfigId: string;
39
- threadMessageId: string;
40
- }>;
41
- transitions: Transition[];
42
- threadUpdates: string[];
43
- }
44
-
45
- const buildRecorder = (
46
- threadMessages: Array<{ accountConfigId: string; threadMessageId: string }>,
47
- transitionWins = true,
48
- ): Recorder => {
49
- const infos: LogEntry[] = [];
50
- const errors: LogEntry[] = [];
51
- const markerDeletes: string[] = [];
52
- const deletedMessages: string[] = [];
53
- const deletedThreadMessages: Array<{
54
- accountConfigId: string;
55
- threadMessageId: string;
56
- }> = [];
57
- const transitions: Transition[] = [];
58
- const threadUpdates: string[] = [];
59
-
60
- const log: PlacementMoveLogger = {
61
- info: (obj, msg) => infos.push({ obj, msg }),
62
- error: (obj, msg) => errors.push({ obj, msg }),
63
- };
64
-
65
- const deps: ResolveExhaustedPlacementMoveDeps = {
66
- markerService: {
67
- delete: async (id: string) => {
68
- markerDeletes.push(id);
69
- },
70
- },
71
- messageService: {
72
- delete: async (id: string) => {
73
- deletedMessages.push(id);
74
- },
75
- transitionPlacement: async (
76
- messageId: string,
77
- expected: PlacementPredicate,
78
- next: PlacementTransitionInput,
79
- ) => {
80
- transitions.push({ messageId, expected, next });
81
- return transitionWins ? ({ messageId } as MessageItem) : undefined;
82
- },
83
- },
84
- threadMessageService: {
85
- findAllByMessageId: async () => threadMessages as never,
86
- deleteMany: async (
87
- keys: Array<{ accountConfigId: string; threadMessageId: string }>,
88
- ) => {
89
- deletedThreadMessages.push(...keys);
90
- },
91
- update: async (_accountConfigId: string, threadMessageId: string) => {
92
- threadUpdates.push(threadMessageId);
93
- return undefined as never;
94
- },
95
- },
96
- log,
97
- };
98
-
99
- return {
100
- deps,
101
- infos,
102
- errors,
103
- markerDeletes,
104
- deletedMessages,
105
- deletedThreadMessages,
106
- transitions,
107
- threadUpdates,
108
- };
109
- };
110
-
111
- const buildConnection = (
112
- present: Set<number>,
113
- fetchDrops: Set<number> = new Set(),
114
- ): IImapConnection =>
115
- ({
116
- openBox: async () => ({}) as never,
117
- fetchMessages: async (uids: number[]) =>
118
- uids
119
- .filter((uid) => present.has(uid) && !fetchDrops.has(uid))
120
- .map((uid) => ({ uid }) as unknown as never),
121
- search: async (criteria: unknown[]) => {
122
- const [, value] = (criteria as Array<[string, string]>)[0];
123
- const uid = Number(value);
124
- return present.has(uid) ? [uid] : [];
125
- },
126
- }) as unknown as IImapConnection;
127
-
128
- describe("resolveExhaustedPlacementMoveFailure — the two terminal outcomes (mirrors #1270 for placement moves)", () => {
129
- it("RECONCILED (expected): the message is gone from its pending-move source — marker dropped, stale row reconciled, no alarm", async () => {
130
- const rec = buildRecorder([
131
- { accountConfigId: "cfg-1", threadMessageId: "tm-msg-gone" },
132
- ]);
133
-
134
- const result = await resolveExhaustedPlacementMoveFailure(rec.deps, {
135
- accountId: "acc-1",
136
- accountConfigId: "cfg-1",
137
- messageId: "msg-gone",
138
- uid: 101,
139
- sourceMailboxId: "mbx-junk",
140
- sourceMailboxPath: "Junk",
141
- getConnection: async () => buildConnection(new Set()),
142
- });
143
-
144
- assert.equal(result.outcome, "reconciled");
145
- assert.deepEqual(rec.markerDeletes, ["msg-gone"]);
146
- assert.deepEqual(rec.deletedMessages, ["msg-gone"]);
147
- assert.deepEqual(rec.deletedThreadMessages, [
148
- { accountConfigId: "cfg-1", threadMessageId: "tm-msg-gone" },
149
- ]);
150
- assert.equal(
151
- rec.errors.length,
152
- 0,
153
- "no alarm for the expected/routine outcome",
154
- );
155
- assert.ok(
156
- rec.infos.some(
157
- (e) => e.obj.metric === "placement_move_stale_row_reconciled",
158
- ),
159
- );
160
- });
161
-
162
- it("BROKEN: the message is still at the source — the row goes back to the source pair rather than being left `moving` forever", async () => {
163
- const rec = buildRecorder([
164
- { accountConfigId: "cfg-1", threadMessageId: "tm-msg-broken" },
165
- ]);
166
-
167
- const result = await resolveExhaustedPlacementMoveFailure(rec.deps, {
168
- accountId: "acc-1",
169
- accountConfigId: "cfg-1",
170
- messageId: "msg-broken",
171
- uid: 202,
172
- sourceMailboxId: "mbx-inbox",
173
- sourceMailboxPath: "INBOX",
174
- getConnection: async () => buildConnection(new Set([202])),
175
- });
176
-
177
- assert.equal(result.outcome, "broken");
178
- assert.deepEqual(rec.transitions, [
179
- {
180
- messageId: "msg-broken",
181
- expected: {
182
- status: [MessageStatus.moving, MessageStatus.deleting],
183
- },
184
- next: {
185
- mailboxId: "mbx-inbox",
186
- uid: 202,
187
- status: MessageStatus.active,
188
- syncStatus: MessageSyncStatus.synced,
189
- // Remit's own classification filing, not something the user asked
190
- // for, so no per-message give-up marker — the alert is the surface.
191
- abandonedMutation: MessageMutation.none,
192
- },
193
- },
194
- ]);
195
- assert.deepEqual(rec.threadUpdates, ["tm-msg-broken"]);
196
- // The marker predicted a move that has now been given up on; leaving it
197
- // would keep a folder-count prediction nothing will ever settle.
198
- assert.deepEqual(rec.markerDeletes, ["msg-broken"]);
199
- assert.deepEqual(rec.deletedMessages, []);
200
- assert.ok(
201
- rec.errors.some((e) => e.obj.alert === "placement_move_failed"),
202
- "the operator signal is the alert, not a stuck row",
203
- );
204
- });
205
-
206
- // The marker is the operator's evidence that this move still owed IMAP a
207
- // push. Dropping it first meant a restore that threw left the row stuck
208
- // `moving` with nothing left to say why.
209
- it("BROKEN: a restore that throws leaves the marker standing", async () => {
210
- const rec = buildRecorder([
211
- { accountConfigId: "cfg-1", threadMessageId: "tm-msg-broken" },
212
- ]);
213
- rec.deps.messageService.transitionPlacement = async () => {
214
- throw new Error("ProvisionedThroughputExceeded");
215
- };
216
-
217
- await assert.rejects(
218
- () =>
219
- resolveExhaustedPlacementMoveFailure(rec.deps, {
220
- accountId: "acc-1",
221
- accountConfigId: "cfg-1",
222
- messageId: "msg-broken",
223
- uid: 202,
224
- sourceMailboxId: "mbx-inbox",
225
- sourceMailboxPath: "INBOX",
226
- getConnection: async () => buildConnection(new Set([202])),
227
- }),
228
- /ProvisionedThroughputExceeded/,
229
- );
230
-
231
- assert.deepEqual(
232
- rec.markerDeletes,
233
- [],
234
- "the record of the owed push outlives a failed restore",
235
- );
236
- });
237
-
238
- it("BROKEN with another lane already settled: the restore loses its predicate and writes nothing", async () => {
239
- const rec = buildRecorder(
240
- [{ accountConfigId: "cfg-1", threadMessageId: "tm-msg-raced" }],
241
- false,
242
- );
243
-
244
- const result = await resolveExhaustedPlacementMoveFailure(rec.deps, {
245
- accountId: "acc-1",
246
- accountConfigId: "cfg-1",
247
- messageId: "msg-raced",
248
- uid: 404,
249
- sourceMailboxId: "mbx-inbox",
250
- sourceMailboxPath: "INBOX",
251
- getConnection: async () => buildConnection(new Set([404])),
252
- });
253
-
254
- assert.equal(result.outcome, "broken");
255
- assert.equal(rec.transitions.length, 1);
256
- assert.deepEqual(
257
- rec.threadUpdates,
258
- [],
259
- "a lost predicate leaves the listing rows to whoever won",
260
- );
261
- });
262
-
263
- it("a dropped FETCH row is not absence: the message is still at the source — local rows survive", async () => {
264
- const rec = buildRecorder([
265
- { accountConfigId: "cfg-1", threadMessageId: "tm-msg-live" },
266
- ]);
267
-
268
- const result = await resolveExhaustedPlacementMoveFailure(rec.deps, {
269
- accountId: "acc-1",
270
- accountConfigId: "cfg-1",
271
- messageId: "msg-live",
272
- uid: 303,
273
- sourceMailboxId: "mbx-inbox",
274
- sourceMailboxPath: "INBOX",
275
- getConnection: async () =>
276
- buildConnection(new Set([303]), new Set([303])),
277
- });
278
-
279
- assert.equal(result.outcome, "broken");
280
- assert.deepEqual(rec.deletedMessages, []);
281
- assert.deepEqual(rec.deletedThreadMessages, []);
282
- assert.ok(rec.errors.some((e) => e.obj.alert === "placement_move_failed"));
283
- });
284
-
285
- it("never throws — both outcomes are terminal, the caller always acks", async () => {
286
- const rec = buildRecorder([]);
287
-
288
- await assert.doesNotReject(() =>
289
- resolveExhaustedPlacementMoveFailure(rec.deps, {
290
- accountId: "acc-1",
291
- accountConfigId: "cfg-1",
292
- messageId: "msg-x",
293
- uid: 1,
294
- sourceMailboxId: "mbx-inbox",
295
- sourceMailboxPath: "INBOX",
296
- getConnection: async () => buildConnection(new Set([1])),
297
- }),
298
- );
299
- });
300
- });
@@ -1,148 +0,0 @@
1
- import { MessageMutation, MessageSyncStatus } from "@remit/domain-enums";
2
- import { isMessageGoneFromOpenMailbox } from "./message-presence.js";
3
- import type { PlacementMoveLogger } from "./placement-move.js";
4
- import {
5
- type RestoreSourcePlacementDeps,
6
- restoreSourcePlacement,
7
- } from "./restore-source-placement.js";
8
- import {
9
- reconcileStaleMessage,
10
- type StaleMessageReconcileDeps,
11
- } from "./stale-message-reconcile.js";
12
- import type { IImapConnection } from "./types.js";
13
-
14
- export interface ResolveExhaustedPlacementMoveDeps {
15
- messageService: StaleMessageReconcileDeps["messageService"] &
16
- RestoreSourcePlacementDeps["messageService"];
17
- threadMessageService: StaleMessageReconcileDeps["threadMessageService"] &
18
- RestoreSourcePlacementDeps["threadMessageService"];
19
- markerService: { delete(messageId: string): Promise<void> };
20
- log: PlacementMoveLogger;
21
- }
22
-
23
- export interface ResolveExhaustedPlacementMoveInput {
24
- accountId: string;
25
- accountConfigId: string;
26
- messageId: string;
27
- uid: number;
28
- sourceMailboxId: string;
29
- sourceMailboxPath: string;
30
- getConnection: () => Promise<IImapConnection>;
31
- }
32
-
33
- export type PlacementMoveTerminalOutcome = "reconciled" | "broken";
34
-
35
- export interface ResolveExhaustedPlacementMoveResult {
36
- outcome: PlacementMoveTerminalOutcome;
37
- }
38
-
39
- /**
40
- * Resolve a PLACEMENT_MOVE_PUSH failure that has exhausted the placement-move
41
- * queue's redelivery budget into exactly one of two terminal outcomes,
42
- * mirroring `resolveExhaustedBodySyncFailures` (#1270) for the same failure
43
- * taxonomy (epic #1281 invariant 3) — no third, softer outcome.
44
- *
45
- * 1. RECONCILED (expected) — the message no longer exists at its pending-move
46
- * source on IMAP, confirmed by {@link isMessageGoneFromOpenMailbox} rather
47
- * than by a FETCH coming back empty. Per invariant 2, an external delete
48
- * supersedes the marker entirely: the marker is dropped and the stale
49
- * Message/ThreadMessage rows are deleted via
50
- * {@link reconcileStaleMessage}. This is also the
51
- * outcome for the (rarer, functionally indistinguishable from here) case
52
- * where a foreign client moved the message elsewhere — either way, our
53
- * prediction no longer holds, and the marker cannot be honoured. Metric
54
- * only, no alarm — routine.
55
- * 2. BROKEN — the message still exists at the source, but the move keeps
56
- * failing. This indicates broken code or a broken account (issue #1271),
57
- * not a transient blip. The row is put back on the source pair the server
58
- * has just confirmed, exactly as the move and delete resolvers do, and the
59
- * marker goes with it: an auto-file whose push gave up must not leave
60
- * `moving` plus the destination folder plus the source's uid standing
61
- * forever, because that pair refuses every later delete and move with a
62
- * 409, blocks the sighting repair, and nothing else clears it. The failure
63
- * is an operator's to read, so it lives in the `alert`-shaped log entry and
64
- * the alarm on it — not in a row the user's own mail is stuck behind.
65
- * Never re-thrown (terminal — the caller acks either way, since retrying a
66
- * stale or permanently-broken move can never succeed).
67
- *
68
- * An operator reading `placement_move_failed` should know one case where the
69
- * message is not actually at the source: a message another client expunged
70
- * mid-session can answer an empty FETCH while the server still lists its UID
71
- * in SEARCH, until it is allowed to send the untagged EXPUNGE. That message
72
- * lands in BROKEN, and BROKEN is terminal — the alert stands until someone
73
- * clears it, and the row goes back to a source the message may already have
74
- * left. The reverse mistake deletes live mail, so the cost is paid
75
- * deliberately: a stale alert over a row the next sync re-points is
76
- * recoverable, a deleted message is not.
77
- */
78
- export const resolveExhaustedPlacementMoveFailure = async (
79
- deps: ResolveExhaustedPlacementMoveDeps,
80
- input: ResolveExhaustedPlacementMoveInput,
81
- ): Promise<ResolveExhaustedPlacementMoveResult> => {
82
- const {
83
- accountId,
84
- accountConfigId,
85
- messageId,
86
- uid,
87
- sourceMailboxId,
88
- sourceMailboxPath,
89
- getConnection,
90
- } = input;
91
-
92
- const connection = await getConnection();
93
- await connection.openBox(sourceMailboxPath);
94
-
95
- if (await isMessageGoneFromOpenMailbox(connection, uid)) {
96
- await deps.markerService.delete(messageId);
97
- const { threadMessagesDeleted } = await reconcileStaleMessage(
98
- deps,
99
- accountConfigId,
100
- messageId,
101
- );
102
- deps.log.info(
103
- {
104
- metric: "placement_move_stale_row_reconciled",
105
- accountId,
106
- accountConfigId,
107
- messageId,
108
- uid,
109
- sourceMailboxPath,
110
- threadMessagesDeleted,
111
- },
112
- "Message no longer at its pending-move source after retry exhaustion (external delete or move); marker dropped, stale row reconciled",
113
- );
114
- return { outcome: "reconciled" };
115
- }
116
-
117
- // The row first, the marker second. The marker is the operator's evidence
118
- // that this move still owed IMAP a push; dropping it before the restore
119
- // means a restore that throws leaves the row stuck `moving` with nothing
120
- // left to say why.
121
- await restoreSourcePlacement(deps, {
122
- accountConfigId,
123
- messageId,
124
- sourceMailboxId,
125
- uid,
126
- // No give-up marker: this move is Remit's own classification filing, not
127
- // something the user asked for, so a per-message "your move failed" would
128
- // report a failure against an intent nobody formed. The alert below is the
129
- // whole surface, and it is an operator's.
130
- syncStatus: MessageSyncStatus.synced,
131
- abandonedMutation: MessageMutation.none,
132
- });
133
- await deps.markerService.delete(messageId);
134
-
135
- deps.log.error(
136
- {
137
- alert: "placement_move_failed",
138
- accountId,
139
- accountConfigId,
140
- messageId,
141
- uid,
142
- sourceMailboxId,
143
- sourceMailboxPath,
144
- },
145
- "Placement move could not be pushed to IMAP after retry exhaustion; message still exists at its source — row restored to the source pair, marker dropped, alert left for operator investigation",
146
- );
147
- return { outcome: "broken" };
148
- };