@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,352 +0,0 @@
1
- /**
2
- * `thread_message.category` is the copy the list read path is about to filter
3
- * on, and its write-path defects only appear under conditions a happy-path test
4
- * never creates (issue #320).
5
- *
6
- * 1. The classifying pass wrote the Message before the ThreadMessage while the
7
- * skip guard keyed off the Message. A failure between the two left the guard
8
- * satisfied and the denormalized row stranded at `uncategorized` forever,
9
- * because the requeued retry returned early.
10
- * 2. `denormalizeCategory` resolved one arbitrary row per message through an
11
- * unordered `.limit(1)`. More than one row per messageId is schema-legal —
12
- * reachable through thread-root drift, not through a second mailbox, whose ids
13
- * collapse to one row — and every row a message has must end up correct.
14
- * 3. It also wrote only `category` on one of its paths, so the snippet and the
15
- * `listId` derived from the same bytes were dropped on the floor.
16
- *
17
- * None of these show up on a single row in a single successful pass.
18
- */
19
-
20
- import assert from "node:assert/strict";
21
- import { Readable } from "node:stream";
22
- import { describe, it } from "node:test";
23
- import type {
24
- IAddressRepository,
25
- IEnvelopeRepository,
26
- IMessageRepository,
27
- IThreadMessageRepository,
28
- MessageItem,
29
- ThreadMessageItem,
30
- UpdateMessageInput,
31
- UpdateThreadMessageInput,
32
- } from "@remit/data-ports";
33
- import { NotFoundError } from "@remit/data-ports/errors";
34
- import { MessageCategory } from "@remit/domain-enums";
35
- import type { StorageService } from "@remit/storage-service";
36
- import { BodySyncService } from "./body-sync.js";
37
- import type { IImapConnection } from "./types.js";
38
-
39
- const LINKEDIN_EML = Buffer.from(
40
- [
41
- "From: LinkedIn <messages-noreply@linkedin.com>",
42
- "To: me@example.com",
43
- "Subject: You have a new invitation",
44
- "List-Id: LinkedIn Invitations <invitations.linkedin.com>",
45
- "List-Unsubscribe: <https://www.linkedin.com/e/unsub>",
46
- "Content-Type: text/plain",
47
- "",
48
- "invitation body text",
49
- ].join("\r\n"),
50
- );
51
-
52
- type Row = Pick<
53
- ThreadMessageItem,
54
- | "threadMessageId"
55
- | "messageId"
56
- | "accountConfigId"
57
- | "mailboxId"
58
- | "sentDate"
59
- | "isRead"
60
- | "isDeleted"
61
- | "hasStars"
62
- | "hasAttachment"
63
- | "category"
64
- > &
65
- Partial<Pick<ThreadMessageItem, "snippet" | "listId">>;
66
-
67
- interface ThreadUpdate {
68
- threadMessageId: string;
69
- input: UpdateThreadMessageInput;
70
- composites: Record<string, unknown> | undefined;
71
- }
72
-
73
- interface Harness {
74
- service: BodySyncService;
75
- message: MessageItem;
76
- rows: Row[];
77
- messageUpdates: Array<{ messageId: string; input: UpdateMessageInput }>;
78
- threadUpdates: ThreadUpdate[];
79
- writeOrder: string[];
80
- }
81
-
82
- const buildRow = (
83
- overrides: Partial<Row> & Pick<Row, "threadMessageId">,
84
- ): Row =>
85
- ({
86
- messageId: "m-1",
87
- accountConfigId: "cfg-1",
88
- mailboxId: "mb-inbox",
89
- sentDate: 1,
90
- isRead: false,
91
- isDeleted: false,
92
- hasStars: false,
93
- hasAttachment: false,
94
- category: MessageCategory.uncategorized,
95
- ...overrides,
96
- }) as Row;
97
-
98
- /**
99
- * The Message and ThreadMessage fixtures are mutated by their writes, so the
100
- * skip guard sees the state a requeued delivery would. A harness that only
101
- * records the write cannot show a stranded row: the strand is the guard reading
102
- * a value that outlived a failed pass.
103
- */
104
- const buildHarness = (
105
- rows: Row[],
106
- options: {
107
- failThreadUpdates?: number;
108
- } = {},
109
- ): Harness => {
110
- const messageUpdates: Array<{
111
- messageId: string;
112
- input: UpdateMessageInput;
113
- }> = [];
114
- const threadUpdates: ThreadUpdate[] = [];
115
- const writeOrder: string[] = [];
116
- let threadUpdateAttempts = 0;
117
-
118
- const message = {
119
- messageId: "m-1",
120
- mailboxId: "mb-inbox",
121
- uid: 1,
122
- category: MessageCategory.uncategorized,
123
- } as unknown as MessageItem;
124
-
125
- const messageService = {
126
- get: async () => message,
127
- update: async (messageId: string, input: UpdateMessageInput) => {
128
- writeOrder.push("message");
129
- messageUpdates.push({ messageId, input });
130
- Object.assign(message, input);
131
- },
132
- } as unknown as IMessageRepository;
133
-
134
- const threadMessageService = {
135
- findAllByMessageId: async () => rows,
136
- // Load-bearing, not a leftover: the current code never calls it, but
137
- // reverting body-sync.ts to reproduce these failures does. Deleting it
138
- // makes the fail-before run throw instead of demonstrating the defect.
139
- getByMessageId: async () => rows[0],
140
- update: async (
141
- _accountConfigId: string,
142
- threadMessageId: string,
143
- input: UpdateThreadMessageInput,
144
- updateOptions?: { composites?: Record<string, unknown> },
145
- ) => {
146
- threadUpdateAttempts++;
147
- if (threadUpdateAttempts <= (options.failThreadUpdates ?? 0)) {
148
- throw new Error("thread_message write failed");
149
- }
150
- writeOrder.push("thread");
151
- threadUpdates.push({
152
- threadMessageId,
153
- input,
154
- composites: updateOptions?.composites,
155
- });
156
- const row = rows.find((r) => r.threadMessageId === threadMessageId);
157
- if (row) Object.assign(row, input);
158
- },
159
- } as unknown as IThreadMessageRepository;
160
-
161
- const storageService = {
162
- retrieve: async () => LINKEDIN_EML,
163
- storeMessageBody: async () => ({ uri: "s3://bodies/m-1" }),
164
- storeMessageBodyStream: async () => ({ uri: "s3://bodies/m-1" }),
165
- storeParsedBody: async () => {},
166
- listBodyParts: async () => [],
167
- } as unknown as StorageService;
168
-
169
- const service = new BodySyncService(
170
- messageService,
171
- storageService,
172
- threadMessageService,
173
- {
174
- getAddress: async () => {
175
- throw new NotFoundError("Address not found");
176
- },
177
- incrementInboundCount: async () => {},
178
- } as unknown as IAddressRepository,
179
- { listBodyParts: async () => [] } as unknown as IEnvelopeRepository,
180
- { info: () => {}, error: () => {} },
181
- );
182
-
183
- return {
184
- service,
185
- message,
186
- rows,
187
- messageUpdates,
188
- threadUpdates,
189
- writeOrder,
190
- };
191
- };
192
-
193
- const syncBatch = (harness: Harness) =>
194
- harness.service.syncBodies(
195
- ["m-1"],
196
- "acc-1",
197
- "cfg-1",
198
- { fullPath: "INBOX", placement: "inbox" },
199
- async () =>
200
- ({
201
- openBox: async () => {},
202
- async *fetchMessageBodies(uids: number[]) {
203
- for (const uid of uids) {
204
- yield { uid, source: Readable.from([LINKEDIN_EML]) };
205
- }
206
- },
207
- }) as unknown as IImapConnection,
208
- );
209
-
210
- const syncFromImap = (harness: Harness) =>
211
- harness.service.fetchAndGetBody(
212
- "m-1",
213
- "acc-1",
214
- "cfg-1",
215
- { fullPath: "INBOX", placement: "inbox" },
216
- async () =>
217
- ({
218
- openBox: async () => {},
219
- fetchMessageBody: async () => LINKEDIN_EML,
220
- }) as unknown as IImapConnection,
221
- );
222
-
223
- describe("the classifying pass survives an interrupted write", () => {
224
- it("leaves the Message unclassified when the ThreadMessage write fails, so the retry runs again", async () => {
225
- const harness = buildHarness([buildRow({ threadMessageId: "tm-1" })], {
226
- failThreadUpdates: 1,
227
- });
228
-
229
- const failed = await syncBatch(harness);
230
-
231
- assert.deepEqual(failed.failedMessageIds, ["m-1"]);
232
- assert.equal(harness.message.category, MessageCategory.uncategorized);
233
- assert.deepEqual(harness.messageUpdates, []);
234
-
235
- const retried = await syncBatch(harness);
236
-
237
- assert.deepEqual(retried.failedMessageIds, []);
238
- assert.equal(harness.message.category, MessageCategory.social);
239
- assert.equal(harness.rows[0].category, MessageCategory.social);
240
- });
241
-
242
- it("writes the Message only after the denormalized row is durable", async () => {
243
- const harness = buildHarness([buildRow({ threadMessageId: "tm-1" })]);
244
-
245
- await syncBatch(harness);
246
-
247
- assert.deepEqual(harness.writeOrder, ["thread", "message"]);
248
- });
249
- });
250
-
251
- describe("denormalization reaches every row a message has", () => {
252
- it("carries the classified category onto all of a message's rows", async () => {
253
- const harness = buildHarness([
254
- buildRow({ threadMessageId: "tm-inbox", mailboxId: "mb-inbox" }),
255
- buildRow({
256
- threadMessageId: "tm-archive",
257
- mailboxId: "mb-archive",
258
- isRead: true,
259
- }),
260
- ]);
261
-
262
- await syncBatch(harness);
263
-
264
- assert.deepEqual(
265
- harness.rows.map((r) => r.category),
266
- [MessageCategory.social, MessageCategory.social],
267
- );
268
- });
269
-
270
- it("builds the composite block from each row rather than reusing the first", async () => {
271
- const harness = buildHarness([
272
- buildRow({ threadMessageId: "tm-inbox", mailboxId: "mb-inbox" }),
273
- buildRow({
274
- threadMessageId: "tm-archive",
275
- mailboxId: "mb-archive",
276
- isRead: true,
277
- }),
278
- ]);
279
-
280
- await syncBatch(harness);
281
-
282
- assert.deepEqual(
283
- harness.threadUpdates.map((u) => ({
284
- threadMessageId: u.threadMessageId,
285
- mailboxId: u.composites?.mailboxId,
286
- isRead: u.composites?.isRead,
287
- })),
288
- [
289
- { threadMessageId: "tm-inbox", mailboxId: "mb-inbox", isRead: false },
290
- {
291
- threadMessageId: "tm-archive",
292
- mailboxId: "mb-archive",
293
- isRead: true,
294
- },
295
- ],
296
- );
297
- });
298
-
299
- it("fans the snippet and the list id out to every row on the read path", async () => {
300
- // A snippet and a List-Id belong to the message, not to the mailbox a row
301
- // happens to name, so every row carries them.
302
- const harness = buildHarness([
303
- buildRow({ threadMessageId: "tm-inbox", mailboxId: "mb-inbox" }),
304
- buildRow({ threadMessageId: "tm-archive", mailboxId: "mb-archive" }),
305
- ]);
306
-
307
- await syncFromImap(harness);
308
-
309
- for (const row of harness.rows) {
310
- assert.equal(row.category, MessageCategory.social);
311
- assert.equal(row.listId, "invitations.linkedin.com");
312
- assert.equal(row.snippet, "invitation body text");
313
- }
314
- });
315
-
316
- it("writes nothing for a row that already carries every value", async () => {
317
- const harness = buildHarness([
318
- buildRow({
319
- threadMessageId: "tm-inbox",
320
- mailboxId: "mb-inbox",
321
- category: MessageCategory.social,
322
- snippet: "invitation body text",
323
- listId: "invitations.linkedin.com",
324
- }),
325
- buildRow({ threadMessageId: "tm-archive", mailboxId: "mb-archive" }),
326
- ]);
327
-
328
- await syncBatch(harness);
329
-
330
- assert.deepEqual(
331
- harness.threadUpdates.map((u) => u.threadMessageId),
332
- ["tm-archive"],
333
- );
334
- });
335
- });
336
-
337
- describe("the classifying pass writes the whole denormalized set", () => {
338
- // Nothing but this pass ever derives `listId` from a message's bytes, so a
339
- // write that carried only `category` left the row's `list_id` NULL for good.
340
- it("carries the list id and the snippet, not just the category", async () => {
341
- const harness = buildHarness([buildRow({ threadMessageId: "tm-1" })]);
342
-
343
- await syncBatch(harness);
344
-
345
- assert.deepEqual(harness.threadUpdates.length, 1);
346
- assert.deepEqual(harness.threadUpdates[0].input, {
347
- category: MessageCategory.social,
348
- snippet: "invitation body text",
349
- listId: "invitations.linkedin.com",
350
- });
351
- });
352
- });