@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,274 +0,0 @@
1
- import type {
2
- IAccountConfigRepository,
3
- IMessageRepository,
4
- IThreadMessageRepository,
5
- ThreadMessageItem,
6
- } from "@remit/data-ports";
7
- import type { StorageService } from "@remit/storage-service";
8
- import { parseMessageBody } from "./body-parse.js";
9
- import { extractListId } from "./filters/list-id.js";
10
-
11
- const DEFAULT_BATCH_SIZE = 200;
12
-
13
- /**
14
- * Where the full-corpus pass left off: the identity of the account it was
15
- * working through, and the page cursor within that account. Resuming looks the
16
- * account up by id and re-opens it at its saved cursor, rather than re-scanning
17
- * the whole corpus from the top after an interruption. A checkpoint naming no
18
- * configured account restarts the pass.
19
- */
20
- export interface ListIdBackfillCheckpoint {
21
- accountConfigId: string;
22
- continuationToken?: string;
23
- }
24
-
25
- /**
26
- * Persistence for {@link ListIdBackfillCheckpoint}, injected so the pass stays
27
- * testable with an in-memory fake; the real entrypoint backs it with a file.
28
- * `clear()` runs once the whole corpus has been scanned, so a later run starts
29
- * fresh rather than reading a stale finished-run checkpoint.
30
- */
31
- export interface ListIdBackfillCheckpointStore {
32
- load(): Promise<ListIdBackfillCheckpoint | undefined>;
33
- save(checkpoint: ListIdBackfillCheckpoint): Promise<void>;
34
- clear(): Promise<void>;
35
- }
36
-
37
- export interface ListIdBackfillDeps {
38
- accountConfigService: Pick<IAccountConfigRepository, "listAll">;
39
- threadMessageService: Pick<
40
- IThreadMessageRepository,
41
- "listByAccount" | "update"
42
- >;
43
- messageService: Pick<IMessageRepository, "get">;
44
- storageService: Pick<StorageService, "retrieve">;
45
- }
46
-
47
- export interface ListIdBackfillLogger {
48
- info(obj: Record<string, unknown>, msg: string): void;
49
- error?(obj: Record<string, unknown>, msg: string): void;
50
- }
51
-
52
- export interface ListIdBackfillTotals {
53
- scanned: number;
54
- alreadySet: number;
55
- skippedNoBody: number;
56
- backfilled: number;
57
- noListId: number;
58
- failed: number;
59
- }
60
-
61
- export interface ListIdBackfillResult extends ListIdBackfillTotals {
62
- failedThreadMessageIds: string[];
63
- }
64
-
65
- export interface ListIdBackfillProgress extends ListIdBackfillTotals {
66
- accountConfigId: string;
67
- }
68
-
69
- export interface ListIdBackfillOptions {
70
- /** Rows fetched per `listByAccount` page. */
71
- batchSize?: number;
72
- checkpointStore?: ListIdBackfillCheckpointStore;
73
- logger?: ListIdBackfillLogger;
74
- /** Called once per page, after that page's rows are settled. */
75
- onProgress?: (progress: ListIdBackfillProgress) => void;
76
- }
77
-
78
- const emptyTotals = (): ListIdBackfillTotals => ({
79
- scanned: 0,
80
- alreadySet: 0,
81
- skippedNoBody: 0,
82
- backfilled: 0,
83
- noListId: 0,
84
- failed: 0,
85
- });
86
-
87
- /**
88
- * Read the stored raw source, derive `List-Id`, and — only when one is
89
- * present — write it. Kept as its own function (rather than inline in a
90
- * try/catch) so the caller can contain a failure with `.then(fulfilled,
91
- * rejected)` instead of a block catch, which keeps the `try` around the one
92
- * call whose failure this contains.
93
- */
94
- const deriveAndApplyListId = async (
95
- deps: ListIdBackfillDeps,
96
- accountConfigId: string,
97
- row: ThreadMessageItem,
98
- bodyStorageKey: string,
99
- ): Promise<string> => {
100
- const body = await deps.storageService.retrieve(bodyStorageKey);
101
- const parsed = await parseMessageBody(body);
102
- const listId = extractListId(parsed);
103
-
104
- if (listId) {
105
- await deps.threadMessageService.update(
106
- accountConfigId,
107
- row.threadMessageId,
108
- { listId },
109
- {
110
- composites: {
111
- sentDate: row.sentDate,
112
- mailboxId: row.mailboxId,
113
- isRead: row.isRead,
114
- isDeleted: row.isDeleted,
115
- hasStars: row.hasStars,
116
- hasAttachment: row.hasAttachment,
117
- },
118
- },
119
- );
120
- }
121
-
122
- return listId;
123
- };
124
-
125
- /**
126
- * One-time, resumable pass that derives `ThreadMessage.listId` for rows synced
127
- * before header extraction shipped (issue #263). Read-only against the stored
128
- * raw source: it never opens IMAP and never touches anything but the single
129
- * `listId` field.
130
- *
131
- * A row is a candidate when `listId` is `undefined` — written before the
132
- * column existed, or written since by a path that found no `List-Id` header
133
- * (body-sync only sets the field when the header is present, so "no header"
134
- * and "not yet backfilled" look the same on the row; re-checking an
135
- * already-correct empty row is wasted work, never a wrong answer). A
136
- * candidate whose Message has no `bodyStorageKey` yet is left alone — its
137
- * body was never synced, so there is nothing local to read, and the ordinary
138
- * sync path will populate `listId` for it once the body lands.
139
- *
140
- * Chunked by `listByAccount`'s existing keyset pagination, one account at a
141
- * time in account-id order, so an interrupted run and its resume walk the same
142
- * sequence. A failure reading or parsing one message's
143
- * stored body is contained to that message — logged, counted, and the pass
144
- * continues: one unreadable object must not strand the rest of the corpus.
145
- *
146
- * The shape a later pass over `Message.classificationState` should take
147
- * (issue #331): a one-time, resumable, storage-only sweep that selects its
148
- * cohort by an explicit field, so it can be run once, deliberately, after a
149
- * classifier release — rather than a re-derivation folded into every sync pass.
150
- */
151
- export const backfillListIds = async (
152
- deps: ListIdBackfillDeps,
153
- options: ListIdBackfillOptions = {},
154
- ): Promise<ListIdBackfillResult> => {
155
- const batchSize = options.batchSize ?? DEFAULT_BATCH_SIZE;
156
- const { logger, checkpointStore } = options;
157
-
158
- const accounts = [...(await deps.accountConfigService.listAll())].sort(
159
- (left, right) => left.accountConfigId.localeCompare(right.accountConfigId),
160
- );
161
- const startingCheckpoint = await checkpointStore?.load();
162
- const checkpointedIndex = startingCheckpoint
163
- ? accounts.findIndex(
164
- (account) =>
165
- account.accountConfigId === startingCheckpoint.accountConfigId,
166
- )
167
- : -1;
168
-
169
- if (startingCheckpoint && checkpointedIndex === -1) {
170
- logger?.info(
171
- { accountConfigId: startingCheckpoint.accountConfigId },
172
- "ListId backfill checkpoint names no configured account; restarting the pass",
173
- );
174
- }
175
-
176
- const startIndex = checkpointedIndex === -1 ? 0 : checkpointedIndex;
177
- const startContinuationToken =
178
- checkpointedIndex === -1
179
- ? undefined
180
- : startingCheckpoint?.continuationToken;
181
-
182
- const totals = emptyTotals();
183
- const failedThreadMessageIds: string[] = [];
184
-
185
- for (
186
- let accountIndex = startIndex;
187
- accountIndex < accounts.length;
188
- accountIndex++
189
- ) {
190
- const account = accounts[accountIndex];
191
- let continuationToken: string | undefined =
192
- accountIndex === startIndex ? startContinuationToken : undefined;
193
-
194
- do {
195
- const page = await deps.threadMessageService.listByAccount(
196
- account.accountConfigId,
197
- { limit: batchSize, continuationToken },
198
- );
199
-
200
- totals.scanned += page.items.length;
201
- const candidates = page.items.filter((row) => row.listId === undefined);
202
- totals.alreadySet += page.items.length - candidates.length;
203
-
204
- if (candidates.length > 0) {
205
- const messages = await deps.messageService.get(
206
- candidates.map((row) => row.messageId),
207
- );
208
- const messageByMessageId = new Map(
209
- messages.map((message) => [message.messageId, message]),
210
- );
211
-
212
- for (const row of candidates) {
213
- const message = messageByMessageId.get(row.messageId);
214
- if (!message?.bodyStorageKey) {
215
- totals.skippedNoBody++;
216
- continue;
217
- }
218
-
219
- const outcome = await deriveAndApplyListId(
220
- deps,
221
- account.accountConfigId,
222
- row,
223
- message.bodyStorageKey,
224
- ).then(
225
- (listId) => ({ error: null, listId }) as const,
226
- (error: unknown) => ({ error, listId: null }) as const,
227
- );
228
-
229
- if (outcome.error !== null) {
230
- totals.failed++;
231
- failedThreadMessageIds.push(row.threadMessageId);
232
- logger?.error?.(
233
- {
234
- threadMessageId: row.threadMessageId,
235
- messageId: row.messageId,
236
- error:
237
- outcome.error instanceof Error
238
- ? outcome.error.message
239
- : String(outcome.error),
240
- },
241
- "ListId backfill failed for a message; leaving it for a later pass",
242
- );
243
- continue;
244
- }
245
-
246
- if (outcome.listId) {
247
- totals.backfilled++;
248
- } else {
249
- totals.noListId++;
250
- }
251
- }
252
- }
253
-
254
- continuationToken = page.continuationToken;
255
- await checkpointStore?.save({
256
- accountConfigId: account.accountConfigId,
257
- continuationToken,
258
- });
259
-
260
- logger?.info(
261
- { accountConfigId: account.accountConfigId, ...totals },
262
- "ListId backfill progress",
263
- );
264
- options.onProgress?.({
265
- accountConfigId: account.accountConfigId,
266
- ...totals,
267
- });
268
- } while (continuationToken);
269
- }
270
-
271
- await checkpointStore?.clear();
272
-
273
- return { ...totals, failedThreadMessageIds };
274
- };
@@ -1,171 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { describe, it } from "node:test";
3
- import {
4
- type CursorRebuildRow,
5
- type CursorRebuildSnapshot,
6
- matchCursorRebuild,
7
- } from "./mailbox-cursor-rebuild.js";
8
-
9
- const row = (over: Partial<CursorRebuildRow>): CursorRebuildRow => ({
10
- messageId: "msg-1",
11
- messageIdHeader: "<a@example.com>",
12
- internalDate: 1_700_000_000_000,
13
- uid: 10,
14
- ...over,
15
- });
16
-
17
- const snapshot = (
18
- over: Partial<CursorRebuildSnapshot>,
19
- ): CursorRebuildSnapshot => ({
20
- uid: 10,
21
- messageId: "<a@example.com>",
22
- internalDate: 1_700_000_000_000,
23
- ...over,
24
- });
25
-
26
- describe("matchCursorRebuild", () => {
27
- it("matches by Message-ID and rewrites the UID mapping when the UID changed", () => {
28
- const existingRows = [row({ messageId: "msg-1", uid: 10 })];
29
- const serverSnapshots = [snapshot({ uid: 55 })];
30
-
31
- const result = matchCursorRebuild(serverSnapshots, existingRows);
32
-
33
- assert.deepEqual(result.matched, [
34
- { messageId: "msg-1", oldUid: 10, newUid: 55 },
35
- ]);
36
- assert.deepEqual(result.newUids, []);
37
- assert.deepEqual(result.staleMessageIds, []);
38
- });
39
-
40
- it("does not emit a rewrite when the matched UID is unchanged (frugal — no-op write)", () => {
41
- const existingRows = [row({ messageId: "msg-1", uid: 10 })];
42
- const serverSnapshots = [snapshot({ uid: 10 })];
43
-
44
- const result = matchCursorRebuild(serverSnapshots, existingRows);
45
-
46
- assert.deepEqual(result.matched, []);
47
- assert.deepEqual(result.staleMessageIds, []);
48
- });
49
-
50
- it("classifies a server message with no matching row as new — normal new-message sync", () => {
51
- const existingRows: CursorRebuildRow[] = [];
52
- const serverSnapshots = [
53
- snapshot({ uid: 99, messageId: "<never-seen@example.com>" }),
54
- ];
55
-
56
- const result = matchCursorRebuild(serverSnapshots, existingRows);
57
-
58
- assert.deepEqual(result.matched, []);
59
- assert.deepEqual(result.newUids, [99]);
60
- assert.deepEqual(result.staleMessageIds, []);
61
- });
62
-
63
- it("classifies a row with no matching server message as stale — expunged, reconcile", () => {
64
- const existingRows = [
65
- row({ messageId: "msg-gone", messageIdHeader: "<gone@example.com>" }),
66
- ];
67
- const serverSnapshots: CursorRebuildSnapshot[] = [];
68
-
69
- const result = matchCursorRebuild(serverSnapshots, existingRows);
70
-
71
- assert.deepEqual(result.matched, []);
72
- assert.deepEqual(result.newUids, []);
73
- assert.deepEqual(result.staleMessageIds, ["msg-gone"]);
74
- });
75
-
76
- it("handles a mixed batch: one match, one new, one stale", () => {
77
- const existingRows = [
78
- row({
79
- messageId: "msg-keep",
80
- messageIdHeader: "<keep@example.com>",
81
- uid: 5,
82
- }),
83
- row({
84
- messageId: "msg-gone",
85
- messageIdHeader: "<gone@example.com>",
86
- uid: 6,
87
- }),
88
- ];
89
- const serverSnapshots = [
90
- snapshot({ uid: 105, messageId: "<keep@example.com>" }),
91
- snapshot({ uid: 200, messageId: "<new@example.com>" }),
92
- ];
93
-
94
- const result = matchCursorRebuild(serverSnapshots, existingRows);
95
-
96
- assert.deepEqual(result.matched, [
97
- { messageId: "msg-keep", oldUid: 5, newUid: 105 },
98
- ]);
99
- assert.deepEqual(result.newUids, [200]);
100
- assert.deepEqual(result.staleMessageIds, ["msg-gone"]);
101
- });
102
-
103
- it("falls back to an exact INTERNALDATE match for headerless messages on both sides", () => {
104
- const existingRows = [
105
- row({
106
- messageId: "msg-headerless",
107
- messageIdHeader: "",
108
- internalDate: 42,
109
- uid: 3,
110
- }),
111
- ];
112
- const serverSnapshots = [
113
- snapshot({ uid: 77, messageId: "", internalDate: 42 }),
114
- ];
115
-
116
- const result = matchCursorRebuild(serverSnapshots, existingRows);
117
-
118
- assert.deepEqual(result.matched, [
119
- { messageId: "msg-headerless", oldUid: 3, newUid: 77 },
120
- ]);
121
- assert.deepEqual(result.staleMessageIds, []);
122
- });
123
-
124
- it("picks the closest INTERNALDATE among multiple rows sharing one Message-ID (legitimate resends)", () => {
125
- const existingRows = [
126
- row({ messageId: "msg-early", internalDate: 1000, uid: 1 }),
127
- row({ messageId: "msg-late", internalDate: 5000, uid: 2 }),
128
- ];
129
- const serverSnapshots = [snapshot({ uid: 50, internalDate: 4900 })];
130
-
131
- const result = matchCursorRebuild(serverSnapshots, existingRows);
132
-
133
- assert.deepEqual(result.matched, [
134
- { messageId: "msg-late", oldUid: 2, newUid: 50 },
135
- ]);
136
- assert.deepEqual(result.staleMessageIds, ["msg-early"]);
137
- });
138
-
139
- it("carries the row's threadMessage ref through to the match, so the caller can rewrite ThreadMessage.uid alongside Message.uid", () => {
140
- const threadMessage = {
141
- accountConfigId: "acfg-1",
142
- threadMessageId: "tm-1",
143
- sentDate: 1_700_000_000_000,
144
- mailboxId: "mbx-1",
145
- isRead: true,
146
- isDeleted: false,
147
- hasStars: false,
148
- hasAttachment: false,
149
- };
150
- const existingRows = [row({ messageId: "msg-1", uid: 10, threadMessage })];
151
- const serverSnapshots = [snapshot({ uid: 99 })];
152
-
153
- const result = matchCursorRebuild(serverSnapshots, existingRows);
154
-
155
- assert.deepEqual(result.matched, [
156
- { messageId: "msg-1", oldUid: 10, newUid: 99, threadMessage },
157
- ]);
158
- });
159
-
160
- it("omits threadMessage from the match when the row carries none (test fixtures that don't need it)", () => {
161
- const existingRows = [row({ messageId: "msg-1", uid: 10 })];
162
- const serverSnapshots = [snapshot({ uid: 99 })];
163
-
164
- const result = matchCursorRebuild(serverSnapshots, existingRows);
165
-
166
- assert.deepEqual(result.matched, [
167
- { messageId: "msg-1", oldUid: 10, newUid: 99 },
168
- ]);
169
- assert.equal("threadMessage" in result.matched[0], false);
170
- });
171
- });
@@ -1,146 +0,0 @@
1
- /**
2
- * Pure matching logic for the UIDVALIDITY cursor rebuild (issue #1272).
3
- *
4
- * Message identity is the Message-ID header; UIDs are cursor positions on
5
- * the server's axis. When UIDVALIDITY changes, the axis is gone but the
6
- * data isn't — this reconciles a fresh envelope-level snapshot (UID +
7
- * Message-ID + INTERNALDATE, no body fetch) against the rows already stored
8
- * for the mailbox, by identity rather than by position.
9
- */
10
-
11
- /** One server message from the cheap envelope-only rebuild FETCH. */
12
- export interface CursorRebuildSnapshot {
13
- uid: number;
14
- /** Raw RFC 822 Message-ID header; empty string when absent/unparseable. */
15
- messageId: string;
16
- /** INTERNALDATE in epoch milliseconds. */
17
- internalDate: number;
18
- }
19
-
20
- /**
21
- * ThreadMessage identity + every composite-key attribute ElectroDB needs to
22
- * run a conditional update against the row (see `buildThreadMessageMoveUpdate`
23
- * in `message-move.ts` for the same pattern on a normal move). Carried
24
- * alongside a {@link CursorRebuildRow} purely so a match can rewrite
25
- * ThreadMessage.uid together with Message.uid — matching itself never reads
26
- * these fields.
27
- */
28
- export interface CursorRebuildThreadMessageRef {
29
- accountConfigId: string;
30
- threadMessageId: string;
31
- sentDate: number;
32
- mailboxId: string;
33
- isRead: boolean;
34
- isDeleted: boolean;
35
- hasStars: boolean;
36
- hasAttachment: boolean;
37
- }
38
-
39
- /** One existing row for the mailbox, as needed to match it against a snapshot. */
40
- export interface CursorRebuildRow {
41
- /** Our internal messageId (the Message row's primary key). */
42
- messageId: string;
43
- /** Raw RFC 822 Message-ID header the row was created with. */
44
- messageIdHeader: string;
45
- /** INTERNALDATE in epoch milliseconds. */
46
- internalDate: number;
47
- /** The row's currently-stored UID (on the old, invalid axis). */
48
- uid: number;
49
- /** Present when the caller wants a match to also rewrite ThreadMessage.uid. */
50
- threadMessage?: CursorRebuildThreadMessageRef;
51
- }
52
-
53
- export interface CursorRebuildMatch {
54
- messageId: string;
55
- oldUid: number;
56
- newUid: number;
57
- /** Carried through from the matched row so the caller can rewrite
58
- * ThreadMessage.uid alongside Message.uid without a second lookup. */
59
- threadMessage?: CursorRebuildThreadMessageRef;
60
- }
61
-
62
- export interface CursorRebuildMatchResult {
63
- /** Rows whose Message-ID matched a server message under a different UID — rewrite in place. */
64
- matched: CursorRebuildMatch[];
65
- /** Server UIDs with no matching row — run through normal new-message sync. */
66
- newUids: number[];
67
- /** Rows with no matching server message — expunged; reconcile (#1283). */
68
- staleMessageIds: string[];
69
- }
70
-
71
- const pickClosestByInternalDate = (
72
- snapshot: CursorRebuildSnapshot,
73
- candidates: CursorRebuildRow[],
74
- consumed: Set<string>,
75
- ): CursorRebuildRow | undefined => {
76
- const available = candidates.filter((row) => !consumed.has(row.messageId));
77
- if (available.length === 0) return undefined;
78
- if (available.length === 1) return available[0];
79
- return available.reduce((best, row) =>
80
- Math.abs(row.internalDate - snapshot.internalDate) <
81
- Math.abs(best.internalDate - snapshot.internalDate)
82
- ? row
83
- : best,
84
- );
85
- };
86
-
87
- /**
88
- * Match a fresh server snapshot against the mailbox's existing rows by
89
- * Message-ID, falling back to an exact INTERNALDATE match for the (rare)
90
- * messages with no usable Message-ID header on either side — mirrors the
91
- * same headerless fallback `message-sync.ts` already uses for threading.
92
- *
93
- * Pure and side-effect free: the caller is responsible for acting on the
94
- * three outcomes (rewrite / new-message sync / reconcile).
95
- */
96
- export const matchCursorRebuild = (
97
- serverSnapshots: CursorRebuildSnapshot[],
98
- existingRows: CursorRebuildRow[],
99
- ): CursorRebuildMatchResult => {
100
- const rowsByHeader = new Map<string, CursorRebuildRow[]>();
101
- const rowsByInternalDateOnly = new Map<number, CursorRebuildRow[]>();
102
-
103
- for (const row of existingRows) {
104
- if (row.messageIdHeader) {
105
- const list = rowsByHeader.get(row.messageIdHeader) ?? [];
106
- list.push(row);
107
- rowsByHeader.set(row.messageIdHeader, list);
108
- } else {
109
- const list = rowsByInternalDateOnly.get(row.internalDate) ?? [];
110
- list.push(row);
111
- rowsByInternalDateOnly.set(row.internalDate, list);
112
- }
113
- }
114
-
115
- const consumed = new Set<string>();
116
- const matched: CursorRebuildMatch[] = [];
117
- const newUids: number[] = [];
118
-
119
- for (const snapshot of serverSnapshots) {
120
- const candidates = snapshot.messageId
121
- ? (rowsByHeader.get(snapshot.messageId) ?? [])
122
- : (rowsByInternalDateOnly.get(snapshot.internalDate) ?? []);
123
- const row = pickClosestByInternalDate(snapshot, candidates, consumed);
124
-
125
- if (!row) {
126
- newUids.push(snapshot.uid);
127
- continue;
128
- }
129
-
130
- consumed.add(row.messageId);
131
- if (row.uid !== snapshot.uid) {
132
- matched.push({
133
- messageId: row.messageId,
134
- oldUid: row.uid,
135
- newUid: snapshot.uid,
136
- ...(row.threadMessage ? { threadMessage: row.threadMessage } : {}),
137
- });
138
- }
139
- }
140
-
141
- const staleMessageIds = existingRows
142
- .filter((row) => !consumed.has(row.messageId))
143
- .map((row) => row.messageId);
144
-
145
- return { matched, newUids, staleMessageIds };
146
- };