@remit/mailbox-service 0.0.112 → 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 (270) 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} +16 -72
  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} +38 -64
  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} +13 -68
  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 -97
  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 -88
  150. package/src/account-credentials.ts +0 -210
  151. package/src/attribute-mapper.test.ts +0 -104
  152. package/src/attribute-mapper.ts +0 -148
  153. package/src/body-parse.test.ts +0 -55
  154. package/src/body-part-mapper.property.test.ts +0 -354
  155. package/src/body-part-mapper.ts +0 -410
  156. package/src/body-sync-queue.test.ts +0 -64
  157. package/src/body-sync-queue.ts +0 -131
  158. package/src/body-sync-terminal.test.ts +0 -148
  159. package/src/body-sync-terminal.ts +0 -174
  160. package/src/body-sync.test.ts +0 -925
  161. package/src/body-sync.ts +0 -1891
  162. package/src/calendar-parts.ts +0 -41
  163. package/src/classification-backfill.test.ts +0 -285
  164. package/src/classification-backfill.ts +0 -363
  165. package/src/classify-message.test.ts +0 -142
  166. package/src/classify-message.ts +0 -116
  167. package/src/connection-factory.ts +0 -102
  168. package/src/connection-test.ts +0 -208
  169. package/src/credentials.test.ts +0 -181
  170. package/src/external-move.test.ts +0 -262
  171. package/src/external-move.ts +0 -285
  172. package/src/filter-operations.test.ts +0 -319
  173. package/src/filter-operations.ts +0 -377
  174. package/src/filter-update.test.ts +0 -313
  175. package/src/filters/filter-config.test.ts +0 -87
  176. package/src/filters/filter-config.ts +0 -34
  177. package/src/filters/list-id.test.ts +0 -50
  178. package/src/filters/match.test.ts +0 -392
  179. package/src/filters/match.ts +0 -225
  180. package/src/filters/pipeline.test.ts +0 -199
  181. package/src/filters/pipeline.ts +0 -170
  182. package/src/flag-push.test.ts +0 -112
  183. package/src/flag-push.ts +0 -198
  184. package/src/flag-queue.test.ts +0 -197
  185. package/src/flag-queue.ts +0 -509
  186. package/src/folder-operations.test.ts +0 -273
  187. package/src/folder-operations.ts +0 -217
  188. package/src/folder-placement.ts +0 -56
  189. package/src/forwarded-message.test.ts +0 -122
  190. package/src/heuristics/authenticationResults.test.ts +0 -189
  191. package/src/heuristics/authenticationResults.ts +0 -212
  192. package/src/heuristics/classifyByHeaders.test.ts +0 -889
  193. package/src/heuristics/classifyByHeaders.ts +0 -467
  194. package/src/heuristics/classifyPlacement.test.ts +0 -366
  195. package/src/heuristics/classifyPlacement.ts +0 -211
  196. package/src/heuristics/classifyRealMail.test.ts +0 -242
  197. package/src/heuristics/machineSenders.test.ts +0 -101
  198. package/src/heuristics/senderMismatch.test.ts +0 -346
  199. package/src/heuristics/senderMismatch.ts +0 -259
  200. package/src/imapflow-connect-retry.test.ts +0 -97
  201. package/src/imapflow-connection.test.ts +0 -503
  202. package/src/imapflow-connection.ts +0 -1476
  203. package/src/index.ts +0 -397
  204. package/src/list-id-backfill.test.ts +0 -207
  205. package/src/list-id-backfill.ts +0 -274
  206. package/src/mailbox-cursor-rebuild.test.ts +0 -171
  207. package/src/mailbox-cursor-rebuild.ts +0 -146
  208. package/src/mailbox-cursor.test.ts +0 -141
  209. package/src/mailbox-delete-intent.test.ts +0 -119
  210. package/src/mailbox-management.test.ts +0 -243
  211. package/src/mailbox-management.ts +0 -838
  212. package/src/mailbox-presence.test.ts +0 -74
  213. package/src/mailbox-queue.ts +0 -397
  214. package/src/mailbox-rename-intent.test.ts +0 -220
  215. package/src/mailbox-rename-subtree.test.ts +0 -287
  216. package/src/mailbox-sync.test.ts +0 -286
  217. package/src/mailbox-sync.ts +0 -724
  218. package/src/message-move.test.ts +0 -726
  219. package/src/message-move.ts +0 -1518
  220. package/src/message-parser.ts +0 -26
  221. package/src/message-presence.test.ts +0 -50
  222. package/src/message-read.ts +0 -87
  223. package/src/message-save-retry.test.ts +0 -90
  224. package/src/message-sync-flags.test.ts +0 -167
  225. package/src/message-sync.test.ts +0 -762
  226. package/src/message-sync.ts +0 -2133
  227. package/src/mime-walker.test.ts +0 -311
  228. package/src/mime-walker.ts +0 -306
  229. package/src/organize-job.ts +0 -93
  230. package/src/outbox-attachment-content.test.ts +0 -153
  231. package/src/outbox-attachment-content.ts +0 -104
  232. package/src/outbox-attachment-filename.test.ts +0 -132
  233. package/src/outbox-attachment-filename.ts +0 -76
  234. package/src/outbox-attachment.test.ts +0 -626
  235. package/src/outbox-attachment.ts +0 -536
  236. package/src/outbox-failed-recovery.test.ts +0 -299
  237. package/src/outbox-queue.test.ts +0 -315
  238. package/src/outbox-queue.ts +0 -525
  239. package/src/ownership.ts +0 -140
  240. package/src/placement-move-terminal.ts +0 -145
  241. package/src/placement-move.ts +0 -363
  242. package/src/placement-settled.test.ts +0 -147
  243. package/src/placement.test.ts +0 -404
  244. package/src/quarantine.test.ts +0 -133
  245. package/src/quarantine.ts +0 -229
  246. package/src/refusal.ts +0 -29
  247. package/src/snippet.test.ts +0 -213
  248. package/src/snippet.ts +0 -152
  249. package/src/spam-report.test.ts +0 -329
  250. package/src/spam-report.ts +0 -357
  251. package/src/sync-warning.test.ts +0 -213
  252. package/src/sync-warning.ts +0 -56
  253. package/src/sync-watermarks.test.ts +0 -409
  254. package/src/sync-watermarks.ts +0 -319
  255. package/src/system-flag-wire-format.test.ts +0 -77
  256. package/src/test-helpers/fake-imap.ts +0 -690
  257. package/src/test-helpers/folder-roles.ts +0 -66
  258. package/src/test-helpers/mime-tree-generator.ts +0 -461
  259. package/src/test-helpers/test-queue.ts +0 -56
  260. package/src/test-helpers/test-store.ts +0 -299
  261. package/src/text/normalizer.test.ts +0 -223
  262. package/src/text/normalizer.ts +0 -140
  263. package/src/thread-message-rows.ts +0 -77
  264. package/src/thread-root.test.ts +0 -29
  265. package/src/thread-root.ts +0 -51
  266. package/src/types/mailparser-augment.d.ts +0 -23
  267. package/src/types/natural-porter-stemmer.d.ts +0 -50
  268. package/src/types/stopword.d.ts +0 -69
  269. package/src/types.ts +0 -371
  270. package/tsconfig.json +0 -8
@@ -1,6 +1,4 @@
1
1
  import type { MessageItem } from "@remit/data-ports";
2
- import { MessageStatus } from "@remit/domain-enums";
3
-
4
2
  /**
5
3
  * Whether the message row's placement is still an unconfirmed local write
6
4
  * (issue #496). While a move is in flight the row carries the destination in
@@ -21,12 +19,7 @@ import { MessageStatus } from "@remit/domain-enums";
21
19
  * same mismatched pair. Reading `moving` alone let a move enqueued behind a
22
20
  * delete bind to the row mid-delete and be dropped when the delete settled.
23
21
  */
24
- export const isPlacementUnsettled = (
25
- message: Pick<MessageItem, "status">,
26
- ): boolean =>
27
- message.status === MessageStatus.moving ||
28
- message.status === MessageStatus.deleting;
29
-
22
+ export declare const isPlacementUnsettled: (message: Pick<MessageItem, "status">) => boolean;
30
23
  /**
31
24
  * Whether the row's `uid` belongs to a folder other than the one `mailboxId`
32
25
  * names — the state that makes the pair a lie rather than merely incomplete.
@@ -46,44 +39,17 @@ export const isPlacementUnsettled = (
46
39
  * untouched, so anything that resolves a uid off a row it did not settle
47
40
  * itself asks this rather than the binding below (issue #1217).
48
41
  */
49
- export const carriesForeignUid = <
50
- T extends Pick<
51
- MessageItem,
52
- "mailboxId" | "uid" | "originalMailboxId" | "originalUid"
53
- >,
54
- >(
55
- message: T,
56
- ): message is T & { originalMailboxId: string; originalUid: number } =>
57
- message.originalUid !== undefined &&
58
- message.originalUid === message.uid &&
59
- message.originalMailboxId !== undefined &&
60
- message.originalMailboxId !== message.mailboxId;
61
-
42
+ export declare const carriesForeignUid: <T extends Pick<MessageItem, "mailboxId" | "uid" | "originalMailboxId" | "originalUid">>(message: T) => message is T & {
43
+ originalMailboxId: string;
44
+ originalUid: number;
45
+ };
62
46
  /**
63
47
  * The folder the row's `uid` is a uid in: the source while a move has not
64
48
  * confirmed, otherwise the folder the row is in. A fetch by uid is only safe
65
49
  * from this folder; from any other the same number names a different message.
66
50
  */
67
- export const uidMailboxIdOf = (
68
- message: Pick<
69
- MessageItem,
70
- "mailboxId" | "uid" | "originalMailboxId" | "originalUid"
71
- >,
72
- ): string =>
73
- carriesForeignUid(message) ? message.originalMailboxId : message.mailboxId;
74
-
75
- export const bindsForeignUid = (
76
- message: Pick<
77
- MessageItem,
78
- "status" | "mailboxId" | "uid" | "originalMailboxId" | "originalUid"
79
- >,
80
- ): boolean => isPlacementUnsettled(message) && carriesForeignUid(message);
81
-
82
- /** A copy at `uid: 0`, which names nothing on the server until COPYUID lands (#1387). */
83
- const awaitsCopiedUid = (
84
- message: Pick<MessageItem, "status" | "uid">,
85
- ): boolean => message.status === MessageStatus.moving && message.uid === 0;
86
-
51
+ export declare const uidMailboxIdOf: (message: Pick<MessageItem, "mailboxId" | "uid" | "originalMailboxId" | "originalUid">) => string;
52
+ export declare const bindsForeignUid: (message: Pick<MessageItem, "status" | "mailboxId" | "uid" | "originalMailboxId" | "originalUid">) => boolean;
87
53
  /**
88
54
  * `in_flight` — a mutation is still working on the row, so waiting resolves it.
89
55
  * That includes a row mid-retry: `message-move`, `message-delete` and
@@ -100,22 +66,11 @@ const awaitsCopiedUid = (
100
66
  * IMAP rather than leaving a mismatched pair behind (#1005).
101
67
  */
102
68
  export type PlacementBinding = "consistent" | "in_flight";
103
-
104
- export const placementBindingOf = (
105
- message: Pick<
106
- MessageItem,
107
- "status" | "mailboxId" | "uid" | "originalMailboxId" | "originalUid"
108
- >,
109
- ): PlacementBinding =>
110
- bindsForeignUid(message) || awaitsCopiedUid(message)
111
- ? "in_flight"
112
- : "consistent";
113
-
69
+ export declare const placementBindingOf: (message: Pick<MessageItem, "status" | "mailboxId" | "uid" | "originalMailboxId" | "originalUid">) => PlacementBinding;
114
70
  export interface PlacementSettleOptions {
115
- timeoutMs: number;
116
- pollMs: number;
71
+ timeoutMs: number;
72
+ pollMs: number;
117
73
  }
118
-
119
74
  /**
120
75
  * Block a dependent mutation until the row's placement settles, then hand back
121
76
  * the confirmed row — the wait half of docs/architecture/imap-mutations.md R2.
@@ -124,16 +79,6 @@ export interface PlacementSettleOptions {
124
79
  * when the deadline passes; the caller decides what a dependency that never
125
80
  * settled means for it.
126
81
  */
127
- export const waitForPlacementToSettle = async (
128
- messageService: { get(messageId: string): Promise<MessageItem> },
129
- messageId: string,
130
- { timeoutMs, pollMs }: PlacementSettleOptions,
131
- ): Promise<MessageItem> => {
132
- const deadline = Date.now() + timeoutMs;
133
- let message = await messageService.get(messageId);
134
- while (isPlacementUnsettled(message) && Date.now() < deadline) {
135
- await new Promise((resolve) => setTimeout(resolve, pollMs));
136
- message = await messageService.get(messageId);
137
- }
138
- return message;
139
- };
82
+ export declare const waitForPlacementToSettle: (messageService: {
83
+ get(messageId: string): Promise<MessageItem>;
84
+ }, messageId: string, { timeoutMs, pollMs }: PlacementSettleOptions) => Promise<MessageItem>;
@@ -0,0 +1,75 @@
1
+ import { MessageStatus } from "@remit/domain-enums";
2
+ /**
3
+ * Whether the message row's placement is still an unconfirmed local write
4
+ * (issue #496). While a move is in flight the row carries the destination in
5
+ * `mailboxId` but the SOURCE folder's `uid` — the pair is only made consistent
6
+ * when the IMAP move confirms and `updateUid` writes the destination's COPYUID.
7
+ * Any dependent mutation that resolves a folder and a uid from such a row
8
+ * therefore addresses the destination's OWN message at that uid: a different
9
+ * message, on a mailbox Remit is never the only client of.
10
+ *
11
+ * `syncStatus` is not the signal. An ordinary freshly-synced inbound row is
12
+ * `pending` forever (nothing on the inbound path promotes it), so keying off it
13
+ * defers every outbound mutation in the product. `status` is what an actual
14
+ * mutation writes and what settling clears.
15
+ *
16
+ * Both in-flight states count, per the R3 state table: a delete points the row
17
+ * at Trash with the source's uid exactly as a move points it at the
18
+ * destination, so a dependent mutation that reads a `deleting` row resolves the
19
+ * same mismatched pair. Reading `moving` alone let a move enqueued behind a
20
+ * delete bind to the row mid-delete and be dropped when the delete settled.
21
+ */
22
+ export const isPlacementUnsettled = (message) => message.status === MessageStatus.moving ||
23
+ message.status === MessageStatus.deleting;
24
+ /**
25
+ * Whether the row's `uid` belongs to a folder other than the one `mailboxId`
26
+ * names — the state that makes the pair a lie rather than merely incomplete.
27
+ *
28
+ * The optimistic half of a move is the only writer of this shape: it points the row at the
29
+ * destination and records the pre-move pair, leaving `uid` untouched until the
30
+ * server confirms and `updateUid` replaces it. A row with no `originalUid` was
31
+ * never moved, so its uid names nothing but itself — a freshly copied row is
32
+ * `moving` with `uid: 0` until COPYUID lands, which is a uid that is not ready,
33
+ * not a uid that names somebody else's message.
34
+ *
35
+ * Answered without reference to `status`, because `status` is not what clears
36
+ * it: `updateUid` does, dropping `originalUid` in the same statement that
37
+ * writes the destination's own, and a restore does by pointing `mailboxId`
38
+ * back at `originalMailboxId`. An operation that rewrites `status` on its way
39
+ * past — Empty Trash marking a whole folder `deleting` — leaves the pair
40
+ * untouched, so anything that resolves a uid off a row it did not settle
41
+ * itself asks this rather than the binding below (issue #1217).
42
+ */
43
+ export const carriesForeignUid = (message) => message.originalUid !== undefined &&
44
+ message.originalUid === message.uid &&
45
+ message.originalMailboxId !== undefined &&
46
+ message.originalMailboxId !== message.mailboxId;
47
+ /**
48
+ * The folder the row's `uid` is a uid in: the source while a move has not
49
+ * confirmed, otherwise the folder the row is in. A fetch by uid is only safe
50
+ * from this folder; from any other the same number names a different message.
51
+ */
52
+ export const uidMailboxIdOf = (message) => carriesForeignUid(message) ? message.originalMailboxId : message.mailboxId;
53
+ export const bindsForeignUid = (message) => isPlacementUnsettled(message) && carriesForeignUid(message);
54
+ /** A copy at `uid: 0`, which names nothing on the server until COPYUID lands (#1387). */
55
+ const awaitsCopiedUid = (message) => message.status === MessageStatus.moving && message.uid === 0;
56
+ export const placementBindingOf = (message) => bindsForeignUid(message) || awaitsCopiedUid(message)
57
+ ? "in_flight"
58
+ : "consistent";
59
+ /**
60
+ * Block a dependent mutation until the row's placement settles, then hand back
61
+ * the confirmed row — the wait half of docs/architecture/imap-mutations.md R2.
62
+ * A move ordinarily settles in well under a second, so blocking is the cheap
63
+ * option the doc's default guidance names. The row comes back still unsettled
64
+ * when the deadline passes; the caller decides what a dependency that never
65
+ * settled means for it.
66
+ */
67
+ export const waitForPlacementToSettle = async (messageService, messageId, { timeoutMs, pollMs }) => {
68
+ const deadline = Date.now() + timeoutMs;
69
+ let message = await messageService.get(messageId);
70
+ while (isPlacementUnsettled(message) && Date.now() < deadline) {
71
+ await new Promise((resolve) => setTimeout(resolve, pollMs));
72
+ message = await messageService.get(messageId);
73
+ }
74
+ return message;
75
+ };
@@ -0,0 +1,95 @@
1
+ import type { IMailboxSpecialUseRepository, IQuarantineRepository, MessageData, MessageItem, QuarantineItem, QuarantineMimeNodeItem } from "@remit/data-ports";
2
+ type CanonicalRole = NonNullable<QuarantineItem["mailboxRole"]>;
3
+ type FailureStage = QuarantineItem["failureStage"];
4
+ type FailureCode = QuarantineItem["failureCode"];
5
+ /**
6
+ * The canonical role of the folder a quarantined message arrived in, read from
7
+ * the server's own RFC 6154 SPECIAL-USE declaration — the same source the
8
+ * appointment flow seeds from. A folder the server declares nothing about has
9
+ * no role, which is the normal state for a plain folder and is why the field
10
+ * is optional. `Important` is a SPECIAL-USE with no canonical role and drops
11
+ * out here rather than being invented into one.
12
+ */
13
+ export declare const resolveMailboxRole: (mailboxPath: string, specialUse: readonly string[]) => CanonicalRole | undefined;
14
+ /** Everything about the quarantine that is the same for every uid in a round. */
15
+ export interface QuarantineContext {
16
+ accountId: string;
17
+ accountConfigId: string;
18
+ mailboxId: string;
19
+ mailboxPath: string;
20
+ uidValidity: number;
21
+ /** Rounds tried before the message was set aside. */
22
+ attempts: number;
23
+ }
24
+ /**
25
+ * The repro fingerprint: what the message declared itself to be. Every field
26
+ * is optional-by-absence because a message can fail before its shape was read,
27
+ * and a required column would force the writer to invent a value or drop the
28
+ * record.
29
+ */
30
+ export interface QuarantineMessageShape {
31
+ contentType?: string;
32
+ transferEncoding?: string;
33
+ charset?: string;
34
+ sizeBytes?: number;
35
+ structure: QuarantineMimeNodeItem[];
36
+ messageIdHash?: string;
37
+ }
38
+ export interface QuarantineFailure {
39
+ stage: FailureStage;
40
+ code: FailureCode;
41
+ /** Parser error text. Stored, shown on screen, never published. */
42
+ message: string;
43
+ partPath?: string;
44
+ }
45
+ /**
46
+ * Message shape off the rows metadata sync already wrote. The body path has no
47
+ * FETCH result to read — it streams raw bytes — but the MIME tree it needs was
48
+ * walked into BodyPart rows when the message's metadata was synced, so the
49
+ * fingerprint comes from there rather than from re-reading headers the parser
50
+ * has just refused.
51
+ */
52
+ export declare const shapeFromMessageData: (message: Pick<MessageItem, "rfc822Size">, data: MessageData, messageIdHeader: string | undefined) => QuarantineMessageShape;
53
+ /**
54
+ * The set of messages already set aside, held for the duration of one sync
55
+ * round. A round loads it once and filters against it in memory: the list is
56
+ * small by design — a growing one is a bug being reported, not a page to
57
+ * paginate — and a lookup per message would put a query on the hot path for a
58
+ * state that is almost always empty.
59
+ */
60
+ export declare class QuarantinedUids {
61
+ private readonly keys;
62
+ constructor(entries: readonly QuarantineItem[]);
63
+ get size(): number;
64
+ has(mailboxId: string, uidValidity: number, uid: number): boolean;
65
+ }
66
+ export interface QuarantineLogger {
67
+ info(obj: Record<string, unknown>, msg: string): void;
68
+ warn(obj: Record<string, unknown>, msg: string): void;
69
+ }
70
+ /**
71
+ * Writes the record a message becomes when the sync path cannot apply it
72
+ * (issue #72), and loads the set a round filters against.
73
+ *
74
+ * Only a message defect reaches this class. Deciding that is the caller's job
75
+ * and it is made at exactly one kind of catch site — one narrow enough that
76
+ * the error can only have come from the message itself. An S3, queue or
77
+ * database failure propagates; writing one here would advance a cursor past
78
+ * mail that is fine and tell the user it was unreadable.
79
+ */
80
+ export declare class QuarantineService {
81
+ private readonly repository;
82
+ private readonly mailboxSpecialUseService;
83
+ private readonly workerVersion;
84
+ private readonly log;
85
+ constructor(repository: IQuarantineRepository, mailboxSpecialUseService: IMailboxSpecialUseRepository, workerVersion: string, log: QuarantineLogger);
86
+ load(accountConfigId: string): Promise<QuarantinedUids>;
87
+ /**
88
+ * Set a message aside. Resolves only once the row is durable, because the
89
+ * caller's next act is to let a cursor move past this uid — and a cursor
90
+ * that moves past work no record survives is the silent loss this whole
91
+ * feature exists to end.
92
+ */
93
+ record(context: QuarantineContext, uid: number, failure: QuarantineFailure, shape?: QuarantineMessageShape): Promise<void>;
94
+ }
95
+ export {};
@@ -0,0 +1,131 @@
1
+ import { quarantineMessageIdHash, ROOT_PART_PATH } from "@remit/data-ports/id";
2
+ import { CanonicalMailboxRole } from "@remit/domain-enums";
3
+ const CANONICAL_ROLES = new Set(Object.values(CanonicalMailboxRole));
4
+ /**
5
+ * The canonical role of the folder a quarantined message arrived in, read from
6
+ * the server's own RFC 6154 SPECIAL-USE declaration — the same source the
7
+ * appointment flow seeds from. A folder the server declares nothing about has
8
+ * no role, which is the normal state for a plain folder and is why the field
9
+ * is optional. `Important` is a SPECIAL-USE with no canonical role and drops
10
+ * out here rather than being invented into one.
11
+ */
12
+ export const resolveMailboxRole = (mailboxPath, specialUse) => {
13
+ if (mailboxPath.toUpperCase() === "INBOX")
14
+ return CanonicalMailboxRole.Inbox;
15
+ const matched = specialUse.find((use) => CANONICAL_ROLES.has(use));
16
+ return matched;
17
+ };
18
+ const EMPTY_SHAPE = { structure: [] };
19
+ /**
20
+ * Message shape off the rows metadata sync already wrote. The body path has no
21
+ * FETCH result to read — it streams raw bytes — but the MIME tree it needs was
22
+ * walked into BodyPart rows when the message's metadata was synced, so the
23
+ * fingerprint comes from there rather than from re-reading headers the parser
24
+ * has just refused.
25
+ */
26
+ export const shapeFromMessageData = (message, data, messageIdHeader) => {
27
+ const parts = [...data.bodyPart].sort((a, b) => a.partPath.localeCompare(b.partPath, "en", { numeric: true }));
28
+ const root = parts.find((part) => part.partPath === ROOT_PART_PATH);
29
+ const rootCharset = root
30
+ ? data.bodyPartParameter.find((param) => param.bodyPartId === root.bodyPartId &&
31
+ param.parameterName.toLowerCase() === "charset")?.parameterValue
32
+ : undefined;
33
+ return {
34
+ ...(root ? { contentType: `${root.mediaType}/${root.mediaSubtype}` } : {}),
35
+ ...(root ? { transferEncoding: root.transferEncoding } : {}),
36
+ ...(rootCharset ? { charset: rootCharset } : {}),
37
+ ...(message.rfc822Size ? { sizeBytes: message.rfc822Size } : {}),
38
+ structure: parts.map((part) => ({
39
+ depth: partPathDepth(part.partPath),
40
+ contentType: `${part.mediaType}/${part.mediaSubtype}`,
41
+ })),
42
+ ...hashOf(messageIdHeader),
43
+ };
44
+ };
45
+ const partPathDepth = (partPath) => partPath === ROOT_PART_PATH ? 0 : partPath.split(".").length;
46
+ const hashOf = (messageIdHeader) => {
47
+ const messageIdHash = quarantineMessageIdHash(messageIdHeader);
48
+ return messageIdHash ? { messageIdHash } : {};
49
+ };
50
+ /**
51
+ * The set of messages already set aside, held for the duration of one sync
52
+ * round. A round loads it once and filters against it in memory: the list is
53
+ * small by design — a growing one is a bug being reported, not a page to
54
+ * paginate — and a lookup per message would put a query on the hot path for a
55
+ * state that is almost always empty.
56
+ */
57
+ export class QuarantinedUids {
58
+ keys;
59
+ constructor(entries) {
60
+ this.keys = new Set(entries.map((entry) => uidKey(entry.mailboxId, entry.uidValidity, entry.uid)));
61
+ }
62
+ get size() {
63
+ return this.keys.size;
64
+ }
65
+ has(mailboxId, uidValidity, uid) {
66
+ return this.keys.has(uidKey(mailboxId, uidValidity, uid));
67
+ }
68
+ }
69
+ const uidKey = (mailboxId, uidValidity, uid) => `${mailboxId}:${uidValidity}:${uid}`;
70
+ /**
71
+ * Writes the record a message becomes when the sync path cannot apply it
72
+ * (issue #72), and loads the set a round filters against.
73
+ *
74
+ * Only a message defect reaches this class. Deciding that is the caller's job
75
+ * and it is made at exactly one kind of catch site — one narrow enough that
76
+ * the error can only have come from the message itself. An S3, queue or
77
+ * database failure propagates; writing one here would advance a cursor past
78
+ * mail that is fine and tell the user it was unreadable.
79
+ */
80
+ export class QuarantineService {
81
+ repository;
82
+ mailboxSpecialUseService;
83
+ workerVersion;
84
+ log;
85
+ constructor(repository, mailboxSpecialUseService, workerVersion, log) {
86
+ this.repository = repository;
87
+ this.mailboxSpecialUseService = mailboxSpecialUseService;
88
+ this.workerVersion = workerVersion;
89
+ this.log = log;
90
+ }
91
+ async load(accountConfigId) {
92
+ return new QuarantinedUids(await this.repository.listByAccountConfigId(accountConfigId));
93
+ }
94
+ /**
95
+ * Set a message aside. Resolves only once the row is durable, because the
96
+ * caller's next act is to let a cursor move past this uid — and a cursor
97
+ * that moves past work no record survives is the silent loss this whole
98
+ * feature exists to end.
99
+ */
100
+ async record(context, uid, failure, shape = EMPTY_SHAPE) {
101
+ // Resolved here rather than per round: a round almost never writes a row,
102
+ // so the lookup belongs on the write and not on the hot path.
103
+ const specialUse = await this.mailboxSpecialUseService.listByMailboxId(context.mailboxId);
104
+ const mailboxRole = resolveMailboxRole(context.mailboxPath, specialUse.map((entry) => entry.specialUse));
105
+ await this.repository.upsert({
106
+ accountConfigId: context.accountConfigId,
107
+ accountId: context.accountId,
108
+ mailboxId: context.mailboxId,
109
+ uidValidity: context.uidValidity,
110
+ uid,
111
+ ...(mailboxRole ? { mailboxRole } : {}),
112
+ mailboxPath: context.mailboxPath,
113
+ quarantinedAt: Date.now(),
114
+ attempts: context.attempts,
115
+ failureStage: failure.stage,
116
+ failureCode: failure.code,
117
+ failureMessage: failure.message,
118
+ ...(failure.partPath ? { failurePartPath: failure.partPath } : {}),
119
+ workerVersion: this.workerVersion,
120
+ ...shape,
121
+ });
122
+ this.log.warn({
123
+ mailboxId: context.mailboxId,
124
+ mailboxPath: context.mailboxPath,
125
+ uid,
126
+ uidValidity: context.uidValidity,
127
+ failureStage: failure.stage,
128
+ failureCode: failure.code,
129
+ }, "Message quarantined; cursor may advance past it");
130
+ }
131
+ }
@@ -0,0 +1,31 @@
1
+ import type { MailboxItem } from "@remit/data-ports";
2
+ export type AccessMode = "read" | "act";
3
+ export type Refusal = {
4
+ kind: "Invalid";
5
+ message: string;
6
+ } | {
7
+ kind: "Forbidden";
8
+ message: string;
9
+ } | {
10
+ kind: "NotFound";
11
+ message: string;
12
+ } | {
13
+ kind: "Conflict";
14
+ message: string;
15
+ } | {
16
+ kind: "NotSettled";
17
+ message: string;
18
+ mailboxId: string;
19
+ syncStatus: MailboxItem["syncStatus"];
20
+ };
21
+ export interface Refused {
22
+ ok: false;
23
+ refusal: Refusal;
24
+ }
25
+ export type Refusable<T> = {
26
+ ok: true;
27
+ value: T;
28
+ } | Refused;
29
+ export declare const accept: <T>(value: T) => Refusable<T>;
30
+ export declare const refuse: (refusal: Refusal) => Refused;
31
+ export declare const invalid: (message: string) => Refused;
@@ -0,0 +1,3 @@
1
+ export const accept = (value) => ({ ok: true, value });
2
+ export const refuse = (refusal) => ({ ok: false, refusal });
3
+ export const invalid = (message) => refuse({ kind: "Invalid", message });
@@ -1,52 +1,37 @@
1
- import type {
2
- IMessageRepository,
3
- IThreadMessageRepository,
4
- MessageItem,
5
- ThreadMessageItem,
6
- } from "@remit/data-ports";
7
- import { isNotFoundError } from "@remit/data-ports/errors";
8
- import { MessageStatus } from "@remit/domain-enums";
9
- import { buildThreadMessageMoveRevert } from "./thread-message-rows.js";
10
-
1
+ import type { IMessageRepository, IThreadMessageRepository, MessageItem, ThreadMessageItem } from "@remit/data-ports";
11
2
  export interface RestoreSourcePlacementDeps {
12
- messageService: Pick<IMessageRepository, "transitionPlacement">;
13
- threadMessageService: Pick<
14
- IThreadMessageRepository,
15
- "findAllByMessageId" | "update"
16
- >;
3
+ messageService: Pick<IMessageRepository, "transitionPlacement">;
4
+ threadMessageService: Pick<IThreadMessageRepository, "findAllByMessageId" | "update">;
17
5
  }
18
-
19
6
  /** Whether the row was still owed a restore when this ran. */
20
7
  export type RestoreSourcePlacementOutcome = "restored" | "superseded";
21
-
22
8
  export interface RestoreSourcePlacementInput {
23
- accountConfigId: string;
24
- messageId: string;
25
- sourceMailboxId: string;
26
- uid: number;
27
- /**
28
- * What the row records about the mutation that will now never happen.
29
- * `synced` where the row is a faithful projection of the source again and
30
- * nothing was refused. `abandoned` where a mutation the user asked for gave
31
- * up, which is a failure the row has to carry (imap-mutations R3).
32
- */
33
- syncStatus: MessageItem["syncStatus"];
34
- /**
35
- * Which mutation gave up, for the row to carry alongside `abandoned`. The
36
- * hand-back is about to set `status` back to `active`, and `status` was the
37
- * only field naming it — without this the client can see that something was
38
- * abandoned and not what, which is how a move that handed back was reported
39
- * as a failed delete (issue #1229). `none` accompanies every other
40
- * `syncStatus`, so the field never outlives its gate.
41
- */
42
- abandonedMutation: MessageItem["abandonedMutation"];
43
- /**
44
- * The listing rows, where the caller has already read them to pick this
45
- * outcome. Omitted, they are read here.
46
- */
47
- threadMessages?: ThreadMessageItem[];
9
+ accountConfigId: string;
10
+ messageId: string;
11
+ sourceMailboxId: string;
12
+ uid: number;
13
+ /**
14
+ * What the row records about the mutation that will now never happen.
15
+ * `synced` where the row is a faithful projection of the source again and
16
+ * nothing was refused. `abandoned` where a mutation the user asked for gave
17
+ * up, which is a failure the row has to carry (imap-mutations R3).
18
+ */
19
+ syncStatus: MessageItem["syncStatus"];
20
+ /**
21
+ * Which mutation gave up, for the row to carry alongside `abandoned`. The
22
+ * hand-back is about to set `status` back to `active`, and `status` was the
23
+ * only field naming it — without this the client can see that something was
24
+ * abandoned and not what, which is how a move that handed back was reported
25
+ * as a failed delete (issue #1229). `none` accompanies every other
26
+ * `syncStatus`, so the field never outlives its gate.
27
+ */
28
+ abandonedMutation: MessageItem["abandonedMutation"];
29
+ /**
30
+ * The listing rows, where the caller has already read them to pick this
31
+ * outcome. Omitted, they are read here.
32
+ */
33
+ threadMessages?: ThreadMessageItem[];
48
34
  }
49
-
50
35
  /**
51
36
  * Put a row back on the pre-mutation source pair and hand its listing rows back
52
37
  * with it.
@@ -97,57 +82,4 @@ export interface RestoreSourcePlacementInput {
97
82
  * `.catch()` and cost the record its ack and its resync, which is the contract
98
83
  * the terminal resolvers state: never re-thrown.
99
84
  */
100
- export const restoreSourcePlacement = async (
101
- deps: RestoreSourcePlacementDeps,
102
- input: RestoreSourcePlacementInput,
103
- ): Promise<RestoreSourcePlacementOutcome> => {
104
- const {
105
- accountConfigId,
106
- messageId,
107
- sourceMailboxId,
108
- uid,
109
- syncStatus,
110
- abandonedMutation,
111
- } = input;
112
-
113
- const skipNotFound = (error: unknown): void => {
114
- if (isNotFoundError(error)) return;
115
- throw error;
116
- };
117
-
118
- const restored = await deps.messageService.transitionPlacement(
119
- messageId,
120
- { status: [MessageStatus.moving, MessageStatus.deleting] },
121
- {
122
- mailboxId: sourceMailboxId,
123
- uid,
124
- status: MessageStatus.active,
125
- syncStatus,
126
- abandonedMutation,
127
- },
128
- );
129
- if (!restored) return "superseded";
130
-
131
- const threadMessages =
132
- input.threadMessages ??
133
- (await deps.threadMessageService.findAllByMessageId(
134
- accountConfigId,
135
- messageId,
136
- ));
137
- for (const threadMessage of threadMessages) {
138
- const args = buildThreadMessageMoveRevert(
139
- threadMessage,
140
- uid,
141
- sourceMailboxId,
142
- );
143
- await deps.threadMessageService
144
- .update(
145
- threadMessage.accountConfigId,
146
- threadMessage.threadMessageId,
147
- args.set,
148
- { composites: args.composites },
149
- )
150
- .catch(skipNotFound);
151
- }
152
- return "restored";
153
- };
85
+ export declare const restoreSourcePlacement: (deps: RestoreSourcePlacementDeps, input: RestoreSourcePlacementInput) => Promise<RestoreSourcePlacementOutcome>;