@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
@@ -0,0 +1,51 @@
1
+ import type { IThreadMessageRepository, ThreadMessageItem } from "@remit/data-ports";
2
+ export type MessageCategoryValue = ThreadMessageItem["category"];
3
+ /**
4
+ * RFC 034 Decision 3.1: `Message.category` is written once and never mutated
5
+ * after — RFC 030's message-list GSI sort key depends on it never churning.
6
+ * "Already decided" is any real category, so a message that carries one is
7
+ * never re-categorized by a re-entrant pass.
8
+ *
9
+ * `uncategorized` fails this test whether or not the classifier has run, which
10
+ * is deliberate: this asks what the row holds, not what was done to it. Whether
11
+ * the classifier has run is {@link Message.classificationState}, and it is the
12
+ * skip guard in `BodySyncService.syncBodies` — not this one — that keeps a
13
+ * declined message from being examined twice.
14
+ */
15
+ export declare const hasDecidedCategory: (category: MessageCategoryValue | undefined) => boolean;
16
+ export interface MessageCategoryDenormalizeDeps {
17
+ threadMessageService: Pick<IThreadMessageRepository, "findAllByMessageId" | "update">;
18
+ }
19
+ export interface MessageCategoryDenormalizeInput {
20
+ category: MessageCategoryValue;
21
+ snippet?: string;
22
+ listId?: string;
23
+ }
24
+ /**
25
+ * Write a message's category (and, when the caller has them, its snippet and
26
+ * `List-Id`) onto every ThreadMessage row that indexes it. The thread row is
27
+ * what the message list renders and what the category filter matches, so it
28
+ * must never disagree with the Message row it mirrors.
29
+ *
30
+ * More than one row per messageId is schema-legal but not normally produced,
31
+ * and this iterates for the same reason `message-move.ts` does (see the model
32
+ * stated at its `deleteThreadMessagesForMessage`): the key permits it and
33
+ * nothing enforces otherwise. It is NOT the second mailbox a message appears
34
+ * in — `deriveMessageId` and `deriveThreadMessageId` are both
35
+ * mailbox-independent, so INBOX and Archive resolve to one row, and a copy
36
+ * gets its own messageId. The reachable case is thread-root drift: the same
37
+ * message re-saved under different `References`, which mints a second
38
+ * threadId and so a second row. Iterating is therefore hardening against a
39
+ * legal state, not a repair for one the sync path manufactures, which is why
40
+ * the tree's other single-row `messageId` lookups are correct as they stand.
41
+ * `flag-queue.ts` iterates the same list.
42
+ *
43
+ * Rows are looked up by messageId, so this does not depend on the RFC822
44
+ * Message-ID header — a headerless message still gets denormalized, matching
45
+ * the unconditional Message.category write. The composite set is built per
46
+ * row, never reused: `mailboxId` and `isRead` can differ between two rows for
47
+ * one message, and it is passed at all so that a future key-attribute
48
+ * addition touching the lsi3/lsi4/lsi5/gsi2 sort keys keeps the index rows
49
+ * consistent.
50
+ */
51
+ export declare const denormalizeMessageCategory: (deps: MessageCategoryDenormalizeDeps, accountConfigId: string, messageId: string, update: MessageCategoryDenormalizeInput) => Promise<void>;
@@ -1,12 +1,5 @@
1
- import type {
2
- IThreadMessageRepository,
3
- ThreadMessageItem,
4
- } from "@remit/data-ports";
5
1
  import { NotFoundError } from "@remit/data-ports/errors";
6
2
  import { MessageCategory } from "@remit/domain-enums";
7
-
8
- export type MessageCategoryValue = ThreadMessageItem["category"];
9
-
10
3
  /**
11
4
  * RFC 034 Decision 3.1: `Message.category` is written once and never mutated
12
5
  * after — RFC 030's message-list GSI sort key depends on it never churning.
@@ -19,37 +12,15 @@ export type MessageCategoryValue = ThreadMessageItem["category"];
19
12
  * skip guard in `BodySyncService.syncBodies` — not this one — that keeps a
20
13
  * declined message from being examined twice.
21
14
  */
22
- export const hasDecidedCategory = (
23
- category: MessageCategoryValue | undefined,
24
- ): boolean =>
25
- category !== undefined && category !== MessageCategory.uncategorized;
26
-
27
- export interface MessageCategoryDenormalizeDeps {
28
- threadMessageService: Pick<
29
- IThreadMessageRepository,
30
- "findAllByMessageId" | "update"
31
- >;
32
- }
33
-
34
- export interface MessageCategoryDenormalizeInput {
35
- category: MessageCategoryValue;
36
- snippet?: string;
37
- listId?: string;
38
- }
39
-
15
+ export const hasDecidedCategory = (category) => category !== undefined && category !== MessageCategory.uncategorized;
40
16
  /**
41
17
  * A row is skipped only when every field the denormalization would write
42
18
  * already matches. `snippet` and `listId` are absent from the update when the
43
19
  * message has neither, and an absent field is not a mismatch.
44
20
  */
45
- const alreadyDenormalized = (
46
- row: ThreadMessageItem,
47
- update: MessageCategoryDenormalizeInput,
48
- ): boolean =>
49
- row.category === update.category &&
50
- (update.snippet === undefined || row.snippet === update.snippet) &&
51
- (update.listId === undefined || row.listId === update.listId);
52
-
21
+ const alreadyDenormalized = (row, update) => row.category === update.category &&
22
+ (update.snippet === undefined || row.snippet === update.snippet) &&
23
+ (update.listId === undefined || row.listId === update.listId);
53
24
  /**
54
25
  * Write a message's category (and, when the caller has them, its snippet and
55
26
  * `List-Id`) onto every ThreadMessage row that indexes it. The thread row is
@@ -77,42 +48,28 @@ const alreadyDenormalized = (
77
48
  * addition touching the lsi3/lsi4/lsi5/gsi2 sort keys keeps the index rows
78
49
  * consistent.
79
50
  */
80
- export const denormalizeMessageCategory = async (
81
- deps: MessageCategoryDenormalizeDeps,
82
- accountConfigId: string,
83
- messageId: string,
84
- update: MessageCategoryDenormalizeInput,
85
- ): Promise<void> => {
86
- const rows = await deps.threadMessageService.findAllByMessageId(
87
- accountConfigId,
88
- messageId,
89
- );
90
- if (rows.length === 0) {
91
- throw new NotFoundError(`ThreadMessage not found for message ${messageId}`);
92
- }
93
-
94
- const written = {
95
- category: update.category,
96
- ...(update.snippet ? { snippet: update.snippet } : {}),
97
- ...(update.listId ? { listId: update.listId } : {}),
98
- };
99
-
100
- for (const row of rows) {
101
- if (alreadyDenormalized(row, written)) continue;
102
- await deps.threadMessageService.update(
103
- accountConfigId,
104
- row.threadMessageId,
105
- written,
106
- {
107
- composites: {
108
- sentDate: row.sentDate,
109
- mailboxId: row.mailboxId,
110
- isRead: row.isRead,
111
- isDeleted: row.isDeleted,
112
- hasStars: row.hasStars,
113
- hasAttachment: row.hasAttachment,
114
- },
115
- },
116
- );
117
- }
51
+ export const denormalizeMessageCategory = async (deps, accountConfigId, messageId, update) => {
52
+ const rows = await deps.threadMessageService.findAllByMessageId(accountConfigId, messageId);
53
+ if (rows.length === 0) {
54
+ throw new NotFoundError(`ThreadMessage not found for message ${messageId}`);
55
+ }
56
+ const written = {
57
+ category: update.category,
58
+ ...(update.snippet ? { snippet: update.snippet } : {}),
59
+ ...(update.listId ? { listId: update.listId } : {}),
60
+ };
61
+ for (const row of rows) {
62
+ if (alreadyDenormalized(row, written))
63
+ continue;
64
+ await deps.threadMessageService.update(accountConfigId, row.threadMessageId, written, {
65
+ composites: {
66
+ sentDate: row.sentDate,
67
+ mailboxId: row.mailboxId,
68
+ isRead: row.isRead,
69
+ isDeleted: row.isDeleted,
70
+ hasStars: row.hasStars,
71
+ hasAttachment: row.hasAttachment,
72
+ },
73
+ });
74
+ }
118
75
  };
@@ -0,0 +1,285 @@
1
+ import { type FifoQueueProducer, type IAddressRepository, type IMailboxRepository, type IMailboxSpecialUseRepository, type IMessageRepository, type IThreadMessageRepository } from "@remit/data-ports";
2
+ import { FolderRoleUnresolvedError } from "@remit/data-ports/errors";
3
+ import { type RoleResolution, type TrashAssuranceLevel } from "@remit/data-ports/folder-role";
4
+ import { MUTATION_EVENT_SCHEMA_VERSION } from "@remit/data-ports/mutation-events";
5
+ /**
6
+ * What a delete batch did not claim. A row whose placement changed between the
7
+ * read that decided this delete and the mark that recorded it belongs to
8
+ * whoever changed it (docs/architecture/imap-mutations.md R3), so this delete
9
+ * leaves it alone — and says which, because the client removed it optimistically
10
+ * and a row that reappears unexplained is a failure the user cannot act on.
11
+ */
12
+ export interface DeleteMessagesOutcome {
13
+ refusedMessageIds: string[];
14
+ }
15
+ /**
16
+ * Event types for message move/delete operations.
17
+ * These match the worker event types in remit-imap-worker/events.ts.
18
+ */
19
+ export interface MessageDeleteEvent {
20
+ type: "MESSAGE_DELETE";
21
+ schemaVersion: typeof MUTATION_EVENT_SCHEMA_VERSION;
22
+ eventId: string;
23
+ timestamp: number;
24
+ accountId: string;
25
+ messageId: string;
26
+ mailboxId: string;
27
+ mailboxPath: string;
28
+ uid: number;
29
+ operation: "move_to_trash" | "permanent_delete";
30
+ destinationMailboxId?: string;
31
+ destinationMailboxPath?: string;
32
+ }
33
+ export interface MessageMoveEvent {
34
+ type: "MESSAGE_MOVE";
35
+ eventId: string;
36
+ timestamp: number;
37
+ accountId: string;
38
+ messageId: string;
39
+ sourceMailboxId: string;
40
+ sourceMailboxPath: string;
41
+ destinationMailboxId: string;
42
+ destinationMailboxPath: string;
43
+ uid: number;
44
+ }
45
+ export interface EmptyTrashEvent {
46
+ type: "EMPTY_TRASH";
47
+ schemaVersion: typeof MUTATION_EVENT_SCHEMA_VERSION;
48
+ eventId: string;
49
+ timestamp: number;
50
+ accountId: string;
51
+ trashMailboxId: string;
52
+ trashMailboxPath: string;
53
+ /** The folder's UIDVALIDITY at consent time; the worker re-checks it. */
54
+ trashUidValidity: number;
55
+ }
56
+ export interface MessageCopyEvent {
57
+ type: "MESSAGE_COPY";
58
+ eventId: string;
59
+ timestamp: number;
60
+ accountId: string;
61
+ sourceMessageId: string;
62
+ newMessageId: string;
63
+ sourceMailboxId: string;
64
+ sourceMailboxPath: string;
65
+ destinationMailboxId: string;
66
+ destinationMailboxPath: string;
67
+ uid: number;
68
+ }
69
+ export type MessageMoveQueueEvent = MessageDeleteEvent | MessageMoveEvent | EmptyTrashEvent | MessageCopyEvent;
70
+ /**
71
+ * Logger interface
72
+ */
73
+ export interface MessageMoveLogger {
74
+ info(obj: Record<string, unknown>, msg: string): void;
75
+ error(obj: Record<string, unknown>, msg: string): void;
76
+ warn?(obj: Record<string, unknown>, msg: string): void;
77
+ }
78
+ /**
79
+ * Configuration for MessageMoveService
80
+ */
81
+ export interface MessageMoveConfig {
82
+ messageService: IMessageRepository;
83
+ mailboxService: IMailboxRepository;
84
+ mailboxSpecialUseService: IMailboxSpecialUseRepository;
85
+ threadMessageService: IThreadMessageRepository;
86
+ addressService: IAddressRepository;
87
+ queue: FifoQueueProducer<MessageMoveQueueEvent>;
88
+ logger?: MessageMoveLogger;
89
+ moveSettleTimeoutMs?: number;
90
+ moveSettlePollMs?: number;
91
+ }
92
+ /**
93
+ * The account resolves no Trash folder, so a move-to-Trash delete has nowhere
94
+ * to put the mail and an Empty Trash has nothing it may empty. A designed,
95
+ * user-facing outcome carrying its own text and a 409 the backend returns
96
+ * verbatim: the absence of a Trash folder is a blocker the user resolves by
97
+ * appointing one, never a licence to expunge.
98
+ */
99
+ export declare class NoTrashMailboxError extends FolderRoleUnresolvedError {
100
+ name: string;
101
+ constructor(accountId: string);
102
+ }
103
+ /**
104
+ * The folder the user appointed as Trash is gone from the server. Distinct from
105
+ * having no Trash at all: there is a decision on file, and the repair is to
106
+ * point it at a folder that exists — not to let reader pick one instead.
107
+ */
108
+ export declare class StaleTrashAppointmentError extends FolderRoleUnresolvedError {
109
+ name: string;
110
+ constructor(accountId: string);
111
+ }
112
+ /**
113
+ * A folder resolves as Trash by its name alone. Enough to move mail into,
114
+ * never enough to expunge — raised by Empty Trash and by a delete that would
115
+ * expunge a message already sitting in that folder.
116
+ */
117
+ export declare class UnconfirmedTrashMailboxError extends FolderRoleUnresolvedError {
118
+ name: string;
119
+ constructor(accountId: string);
120
+ }
121
+ /**
122
+ * The Trash folder a verb may act on at the assurance it demands, or the coded
123
+ * refusal naming which of the three reasons stopped it. Every Trash gate goes
124
+ * through here — the delete and the expunge — so no two of them can tell a
125
+ * user different things about the same account.
126
+ */
127
+ export declare const requireTrashMailbox: <T>(resolution: RoleResolution<T>, level: TrashAssuranceLevel, accountId: string) => T;
128
+ /**
129
+ * Options for delete operations
130
+ */
131
+ export interface DeleteOptions {
132
+ /** Move to Trash instead of permanent delete. Default: true */
133
+ toTrash?: boolean;
134
+ /** Permanently delete even if in Trash. Default: false */
135
+ permanent?: boolean;
136
+ }
137
+ /**
138
+ * Service for moving, copying, and deleting messages.
139
+ *
140
+ * Implements optimistic local-first pattern:
141
+ * 1. Updates local state immediately (Message + ThreadMessage)
142
+ * 2. Enqueues event to SQS for worker to sync to IMAP
143
+ *
144
+ * Following RFC 016 for message deletion and moving.
145
+ */
146
+ export declare class MessageMoveService {
147
+ private messageService;
148
+ private mailboxService;
149
+ private mailboxSpecialUseService;
150
+ private threadMessageService;
151
+ private addressService;
152
+ private queue;
153
+ private log;
154
+ private moveSettleTimeoutMs;
155
+ private moveSettlePollMs;
156
+ constructor(config: MessageMoveConfig);
157
+ /**
158
+ * Delete a message. By default moves to Trash.
159
+ *
160
+ * @param messageId - Message to delete
161
+ * @param accountId - Account ID
162
+ * @param options - Delete options (toTrash, permanent)
163
+ */
164
+ deleteMessage: (accountConfigId: string, messageId: string, accountId: string, options?: DeleteOptions) => Promise<DeleteMessagesOutcome>;
165
+ /**
166
+ * Delete multiple messages using batch operations.
167
+ * By default moves to Trash, unless permanent is specified or already in Trash.
168
+ *
169
+ * @param messageIds - Messages to delete
170
+ * @param accountId - Account ID
171
+ * @param options - Delete options (toTrash, permanent)
172
+ */
173
+ deleteMessages: (accountConfigId: string, messageIds: string[], accountId: string, options?: DeleteOptions) => Promise<DeleteMessagesOutcome>;
174
+ /**
175
+ * Move a message to another mailbox.
176
+ *
177
+ * Waits, never reconciles (imap-mutations R2): the folder and uid go to the
178
+ * server as one pair, and once the worker has moved the destination's own
179
+ * message at that uid there is no reference left to repair. The person is
180
+ * looking at the result, so blocking for the sub-second a move takes is the
181
+ * cheap half; on timeout the move refuses ahead of any local write.
182
+ *
183
+ * @param messageId - Message to move
184
+ * @param destinationMailboxId - Destination mailbox ID
185
+ * @param accountId - Account ID
186
+ */
187
+ moveMessage: (accountConfigId: string, messageId: string, destinationMailboxId: string, accountId: string) => Promise<void>;
188
+ private moveSettledMessage;
189
+ /**
190
+ * Move multiple messages to another mailbox.
191
+ *
192
+ * @param messageIds - Messages to move
193
+ * @param destinationMailboxId - Destination mailbox ID
194
+ * @param accountId - Account ID
195
+ */
196
+ moveMessages: (accountConfigId: string, messageIds: string[], destinationMailboxId: string, accountId: string) => Promise<void>;
197
+ /**
198
+ * Copy a message to another mailbox.
199
+ * Creates a new message record locally and enqueues IMAP COPY.
200
+ *
201
+ * @param messageId - Message to copy
202
+ * @param destinationMailboxId - Destination mailbox ID
203
+ * @param accountId - Account ID
204
+ * @returns The new message ID for the copy
205
+ */
206
+ copyMessage: (accountConfigId: string, messageId: string, destinationMailboxId: string, accountId: string) => Promise<string>;
207
+ private copySettledMessage;
208
+ /**
209
+ * Copy multiple messages to another mailbox.
210
+ *
211
+ * @param messageIds - Messages to copy
212
+ * @param destinationMailboxId - Destination mailbox ID
213
+ * @param accountId - Account ID
214
+ * @returns Array of new message IDs for the copies
215
+ */
216
+ copyMessages: (accountConfigId: string, messageIds: string[], destinationMailboxId: string, accountId: string) => Promise<string[]>;
217
+ /**
218
+ * Restore a message from Trash to its original mailbox.
219
+ *
220
+ * @param messageId - Message to restore
221
+ * @param accountId - Account ID
222
+ */
223
+ restoreMessage: (accountConfigId: string, messageId: string, accountId: string) => Promise<void>;
224
+ /**
225
+ * Empty the Trash mailbox: an EXPUNGE of everything in it, with no undo.
226
+ *
227
+ * Demands confirmed evidence: the folder the user appointed or the one the
228
+ * server flagged \Trash — never one that merely reads like a trash folder,
229
+ * and never the fallback behind an appointment that went missing. A user
230
+ * with an ordinary folder called `Deleted` would otherwise lose its contents
231
+ * permanently. Each of the three ways that evidence can be absent refuses
232
+ * under its own reason, so the surface can name the remedy instead of
233
+ * offering a silent no-op.
234
+ *
235
+ * Reports the rows it marked, read once here, so the number the caller shows
236
+ * a user is the number this operation acted on. Pressed twice before the
237
+ * worker runs it re-marks the same rows and reports the same number: the
238
+ * folder really does still hold them, and the second expunge finds nothing
239
+ * to do.
240
+ */
241
+ emptyTrash: (accountConfigId: string, accountId: string) => Promise<{
242
+ deletedCount: number;
243
+ }>;
244
+ private refusePlacement;
245
+ private settledPlacement;
246
+ /**
247
+ * Settle a whole batch before any of it is acted on, so one unsettled row
248
+ * refuses the batch whole rather than leaving part of it expunged, and the
249
+ * batch's total wait is one row's ceiling rather than the sum across it.
250
+ */
251
+ private settledPlacements;
252
+ /**
253
+ * Update ThreadMessage for move operations.
254
+ * Updates mailboxId and optionally isDeleted.
255
+ */
256
+ private updateThreadMessageForMove;
257
+ /**
258
+ * Delete every ThreadMessage row that points at this messageId.
259
+ *
260
+ * A row is keyed by (threadId, messageId), so this is one row in practice —
261
+ * a message filed in several folders keeps a single row, because its
262
+ * messageId is derived from the Message-ID header and not from the mailbox.
263
+ * The query stays a list because the key permits more than one thread per
264
+ * message and nothing enforces otherwise.
265
+ *
266
+ * Deleting up-front in the permanent-delete optimistic step prevents stale
267
+ * rows from leaking into mailbox listings while IMAP catches up (#212).
268
+ */
269
+ private deleteThreadMessagesForMessage;
270
+ /**
271
+ * Update ThreadMessage.isDeleted flag only.
272
+ */
273
+ private updateThreadMessageDeleted;
274
+ /**
275
+ * Enqueue a message move/delete event, grouped by accountId so events for
276
+ * different accounts can be processed in parallel.
277
+ */
278
+ private enqueueEvent;
279
+ /**
280
+ * Enqueue multiple events in batches of at most {@link QUEUE_BATCH_LIMIT}.
281
+ */
282
+ private enqueueEventsBatch;
283
+ private sendEventsBatch;
284
+ private handBackUnsentMove;
285
+ }