@remit/mailbox-service 0.0.111 → 0.0.113

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (298) hide show
  1. package/dist/account-credentials.d.ts +83 -0
  2. package/dist/account-credentials.js +104 -0
  3. package/dist/attribute-mapper.d.ts +36 -0
  4. package/dist/attribute-mapper.js +119 -0
  5. package/dist/body-parse.d.ts +28 -0
  6. package/{src/body-parse.ts → dist/body-parse.js} +18 -26
  7. package/dist/body-part-mapper.d.ts +92 -0
  8. package/dist/body-part-mapper.js +301 -0
  9. package/dist/body-sync-queue.d.ts +59 -0
  10. package/dist/body-sync-queue.js +56 -0
  11. package/dist/body-sync-terminal.d.ts +58 -0
  12. package/dist/body-sync-terminal.js +95 -0
  13. package/dist/body-sync.d.ts +351 -0
  14. package/dist/body-sync.js +1204 -0
  15. package/dist/calendar-parts.d.ts +23 -0
  16. package/dist/calendar-parts.js +18 -0
  17. package/dist/classification-backfill.d.ts +91 -0
  18. package/dist/classification-backfill.js +180 -0
  19. package/dist/classify-message.d.ts +31 -0
  20. package/dist/classify-message.js +89 -0
  21. package/dist/connection-factory.d.ts +54 -0
  22. package/dist/connection-factory.js +54 -0
  23. package/dist/connection-test.d.ts +41 -0
  24. package/dist/connection-test.js +152 -0
  25. package/dist/external-move.d.ts +94 -0
  26. package/dist/external-move.js +173 -0
  27. package/dist/filter-operations.d.ts +42 -0
  28. package/dist/filter-operations.js +211 -0
  29. package/dist/filters/filter-config.d.ts +14 -0
  30. package/dist/filters/filter-config.js +10 -0
  31. package/{src/filters/list-id.ts → dist/filters/list-id.d.ts} +2 -16
  32. package/dist/filters/list-id.js +26 -0
  33. package/dist/filters/match.d.ts +69 -0
  34. package/dist/filters/match.js +165 -0
  35. package/dist/filters/pipeline.d.ts +64 -0
  36. package/dist/filters/pipeline.js +100 -0
  37. package/{src/flag-push-terminal.ts → dist/flag-push-terminal.d.ts} +19 -77
  38. package/dist/flag-push-terminal.js +63 -0
  39. package/dist/flag-push.d.ts +69 -0
  40. package/dist/flag-push.js +111 -0
  41. package/dist/flag-queue.d.ts +175 -0
  42. package/dist/flag-queue.js +291 -0
  43. package/dist/folder-operations.d.ts +39 -0
  44. package/dist/folder-operations.js +112 -0
  45. package/dist/folder-placement.d.ts +13 -0
  46. package/dist/folder-placement.js +23 -0
  47. package/dist/heuristics/authenticationResults.d.ts +10 -0
  48. package/dist/heuristics/authenticationResults.js +187 -0
  49. package/dist/heuristics/classifyByHeaders.d.ts +82 -0
  50. package/dist/heuristics/classifyByHeaders.js +365 -0
  51. package/dist/heuristics/classifyPlacement.d.ts +67 -0
  52. package/dist/heuristics/classifyPlacement.js +153 -0
  53. package/dist/heuristics/machineSenders.d.ts +21 -0
  54. package/{src/heuristics/machineSenders.ts → dist/heuristics/machineSenders.js} +34 -40
  55. package/dist/heuristics/senderMismatch.d.ts +81 -0
  56. package/dist/heuristics/senderMismatch.js +192 -0
  57. package/dist/heuristics/socialDomains.d.ts +14 -0
  58. package/{src/heuristics/socialDomains.ts → dist/heuristics/socialDomains.js} +9 -9
  59. package/dist/heuristics/transactionalDomains.d.ts +18 -0
  60. package/{src/heuristics/transactionalDomains.ts → dist/heuristics/transactionalDomains.js} +8 -8
  61. package/dist/imapflow-connection.d.ts +377 -0
  62. package/dist/imapflow-connection.js +1147 -0
  63. package/dist/index.d.ts +63 -0
  64. package/dist/index.js +65 -0
  65. package/dist/list-id-backfill.d.ts +83 -0
  66. package/dist/list-id-backfill.js +132 -0
  67. package/dist/mailbox-cursor-rebuild.d.ts +74 -0
  68. package/dist/mailbox-cursor-rebuild.js +71 -0
  69. package/{src/mailbox-cursor.ts → dist/mailbox-cursor.d.ts} +20 -98
  70. package/dist/mailbox-cursor.js +102 -0
  71. package/dist/mailbox-intent.d.ts +41 -0
  72. package/{src/mailbox-intent.ts → dist/mailbox-intent.js} +39 -63
  73. package/dist/mailbox-management.d.ts +240 -0
  74. package/dist/mailbox-management.js +576 -0
  75. package/dist/mailbox-presence.d.ts +52 -0
  76. package/{src/mailbox-presence.ts → dist/mailbox-presence.js} +19 -30
  77. package/dist/mailbox-queue.d.ts +126 -0
  78. package/dist/mailbox-queue.js +214 -0
  79. package/dist/mailbox-sync.d.ts +93 -0
  80. package/dist/mailbox-sync.js +490 -0
  81. package/dist/mailbox-upstream.d.ts +38 -0
  82. package/{src/mailbox-upstream.ts → dist/mailbox-upstream.js} +37 -47
  83. package/dist/message-category.d.ts +51 -0
  84. package/{src/message-category.ts → dist/message-category.js} +28 -71
  85. package/dist/message-move.d.ts +285 -0
  86. package/dist/message-move.js +858 -0
  87. package/dist/message-parser.d.ts +14 -0
  88. package/dist/message-parser.js +17 -0
  89. package/{src/message-presence.ts → dist/message-presence.d.ts} +1 -13
  90. package/dist/message-presence.js +29 -0
  91. package/dist/message-read.d.ts +51 -0
  92. package/dist/message-read.js +31 -0
  93. package/dist/message-sync.d.ts +345 -0
  94. package/dist/message-sync.js +1358 -0
  95. package/dist/mime-walker.d.ts +102 -0
  96. package/dist/mime-walker.js +198 -0
  97. package/dist/organize-job.d.ts +20 -0
  98. package/dist/organize-job.js +37 -0
  99. package/dist/outbox-attachment-content.d.ts +37 -0
  100. package/dist/outbox-attachment-content.js +50 -0
  101. package/dist/outbox-attachment-filename.d.ts +19 -0
  102. package/dist/outbox-attachment-filename.js +64 -0
  103. package/dist/outbox-attachment.d.ts +148 -0
  104. package/dist/outbox-attachment.js +273 -0
  105. package/dist/outbox-queue.d.ts +73 -0
  106. package/dist/outbox-queue.js +284 -0
  107. package/{src/outbox-status.ts → dist/outbox-status.d.ts} +1 -6
  108. package/dist/outbox-status.js +18 -0
  109. package/dist/ownership.d.ts +24 -0
  110. package/dist/ownership.js +83 -0
  111. package/dist/placement-move-terminal.d.ts +67 -0
  112. package/dist/placement-move-terminal.js +88 -0
  113. package/dist/placement-move.d.ts +83 -0
  114. package/dist/placement-move.js +216 -0
  115. package/{src/placement-settled.ts → dist/placement-settled.d.ts} +19 -50
  116. package/dist/placement-settled.js +75 -0
  117. package/dist/quarantine.d.ts +95 -0
  118. package/dist/quarantine.js +131 -0
  119. package/dist/refusal.d.ts +31 -0
  120. package/dist/refusal.js +3 -0
  121. package/{src/restore-source-placement.ts → dist/restore-source-placement.d.ts} +29 -101
  122. package/dist/restore-source-placement.js +79 -0
  123. package/dist/search-text-backfill.d.ts +15 -0
  124. package/dist/search-text-backfill.js +33 -0
  125. package/dist/snippet.d.ts +61 -0
  126. package/dist/snippet.js +129 -0
  127. package/dist/spam-report.d.ts +86 -0
  128. package/dist/spam-report.js +241 -0
  129. package/dist/sync-warning.d.ts +21 -0
  130. package/dist/sync-warning.js +26 -0
  131. package/dist/sync-watermarks.d.ts +138 -0
  132. package/dist/sync-watermarks.js +205 -0
  133. package/dist/test-helpers/fake-imap.d.ts +151 -0
  134. package/dist/test-helpers/fake-imap.js +508 -0
  135. package/dist/test-helpers/folder-roles.d.ts +34 -0
  136. package/dist/test-helpers/folder-roles.js +47 -0
  137. package/dist/test-helpers/mime-tree-generator.d.ts +82 -0
  138. package/dist/test-helpers/mime-tree-generator.js +329 -0
  139. package/dist/test-helpers/test-store.d.ts +69 -0
  140. package/dist/test-helpers/test-store.js +193 -0
  141. package/dist/text/normalizer.d.ts +20 -0
  142. package/dist/text/normalizer.js +102 -0
  143. package/dist/thread-message-rows.d.ts +69 -0
  144. package/dist/thread-message-rows.js +40 -0
  145. package/dist/thread-root.d.ts +7 -0
  146. package/dist/thread-root.js +25 -0
  147. package/dist/types.d.ts +343 -0
  148. package/dist/types.js +19 -0
  149. package/package.json +86 -73
  150. package/src/account-credentials.ts +0 -210
  151. package/src/adversarial-mime.e2e.test.ts +0 -307
  152. package/src/attribute-mapper.test.ts +0 -104
  153. package/src/attribute-mapper.ts +0 -148
  154. package/src/body-parse.test.ts +0 -55
  155. package/src/body-part-mapper.property.test.ts +0 -354
  156. package/src/body-part-mapper.ts +0 -360
  157. package/src/body-sync-auto-read-writeonce.test.ts +0 -219
  158. package/src/body-sync-calendar-suggestion.test.ts +0 -433
  159. package/src/body-sync-category-denormalize.test.ts +0 -352
  160. package/src/body-sync-category-override.test.ts +0 -501
  161. package/src/body-sync-category-writeonce.test.ts +0 -234
  162. package/src/body-sync-filter.test.ts +0 -237
  163. package/src/body-sync-message-gone.test.ts +0 -139
  164. package/src/body-sync-placement-flags.test.ts +0 -347
  165. package/src/body-sync-placement-writeonce.test.ts +0 -337
  166. package/src/body-sync-quarantine.test.ts +0 -276
  167. package/src/body-sync-queue.test.ts +0 -124
  168. package/src/body-sync-queue.ts +0 -131
  169. package/src/body-sync-resync.test.ts +0 -605
  170. package/src/body-sync-terminal.test.ts +0 -310
  171. package/src/body-sync-terminal.ts +0 -176
  172. package/src/body-sync-unclassifiable-once.test.ts +0 -260
  173. package/src/body-sync-unsubscribed-auto-read.test.ts +0 -176
  174. package/src/body-sync.ts +0 -1970
  175. package/src/calendar-parts.ts +0 -41
  176. package/src/classification-backfill.test.ts +0 -615
  177. package/src/classification-backfill.ts +0 -363
  178. package/src/classify-message.test.ts +0 -205
  179. package/src/classify-message.ts +0 -116
  180. package/src/connection-factory.ts +0 -102
  181. package/src/connection-test.ts +0 -208
  182. package/src/credentials.test.ts +0 -181
  183. package/src/external-move.test.ts +0 -369
  184. package/src/external-move.ts +0 -272
  185. package/src/filters/anchor-drift.ts +0 -55
  186. package/src/filters/filter-config.test.ts +0 -229
  187. package/src/filters/filter-config.ts +0 -69
  188. package/src/filters/list-id.test.ts +0 -50
  189. package/src/filters/match.test.ts +0 -429
  190. package/src/filters/match.ts +0 -274
  191. package/src/filters/message-embedder.test.ts +0 -39
  192. package/src/filters/message-embedder.ts +0 -33
  193. package/src/filters/pipeline.test.ts +0 -330
  194. package/src/filters/pipeline.ts +0 -284
  195. package/src/flag-push-terminal.test.ts +0 -209
  196. package/src/flag-push.test.ts +0 -408
  197. package/src/flag-push.ts +0 -198
  198. package/src/flag-queue-stars.test.ts +0 -171
  199. package/src/flag-queue.ts +0 -509
  200. package/src/folder-placement.ts +0 -56
  201. package/src/heuristics/authenticationResults.test.ts +0 -189
  202. package/src/heuristics/authenticationResults.ts +0 -212
  203. package/src/heuristics/classifyByHeaders.test.ts +0 -889
  204. package/src/heuristics/classifyByHeaders.ts +0 -467
  205. package/src/heuristics/classifyPlacement.test.ts +0 -366
  206. package/src/heuristics/classifyPlacement.ts +0 -211
  207. package/src/heuristics/classifyRealMail.test.ts +0 -242
  208. package/src/heuristics/machineSenders.test.ts +0 -101
  209. package/src/heuristics/senderMismatch.test.ts +0 -346
  210. package/src/heuristics/senderMismatch.ts +0 -259
  211. package/src/imapflow-connect-retry.test.ts +0 -134
  212. package/src/imapflow-connection.e2e.test.ts +0 -335
  213. package/src/imapflow-connection.integ.test.ts +0 -316
  214. package/src/imapflow-connection.test.ts +0 -469
  215. package/src/imapflow-connection.ts +0 -1475
  216. package/src/index.ts +0 -350
  217. package/src/list-id-backfill.test.ts +0 -573
  218. package/src/list-id-backfill.ts +0 -274
  219. package/src/mailbox-cursor-rebuild.test.ts +0 -171
  220. package/src/mailbox-cursor-rebuild.ts +0 -146
  221. package/src/mailbox-cursor.test.ts +0 -249
  222. package/src/mailbox-delete-intent.test.ts +0 -246
  223. package/src/mailbox-management.test.ts +0 -356
  224. package/src/mailbox-management.ts +0 -839
  225. package/src/mailbox-presence.test.ts +0 -72
  226. package/src/mailbox-queue.ts +0 -415
  227. package/src/mailbox-rename-intent.test.ts +0 -389
  228. package/src/mailbox-rename-subtree.test.ts +0 -664
  229. package/src/mailbox-sync.test.ts +0 -848
  230. package/src/mailbox-sync.ts +0 -724
  231. package/src/message-delete-junk-reconcile.test.ts +0 -117
  232. package/src/message-delete-no-trash.test.ts +0 -245
  233. package/src/message-delete-unsettled-placement.test.ts +0 -396
  234. package/src/message-move-copy.test.ts +0 -562
  235. package/src/message-move-empty-trash.test.ts +0 -326
  236. package/src/message-move-same-mailbox.test.ts +0 -139
  237. package/src/message-move-unsettled-placement.test.ts +0 -378
  238. package/src/message-move.ts +0 -1348
  239. package/src/message-parser.ts +0 -26
  240. package/src/message-presence.test.ts +0 -50
  241. package/src/message-sync-address-spoof.test.ts +0 -293
  242. package/src/message-sync-category.test.ts +0 -142
  243. package/src/message-sync-changedsince.test.ts +0 -840
  244. package/src/message-sync-flags.test.ts +0 -158
  245. package/src/message-sync-junk-harvest.test.ts +0 -244
  246. package/src/message-sync-quarantine.test.ts +0 -416
  247. package/src/message-sync-repoint.test.ts +0 -419
  248. package/src/message-sync.ts +0 -1963
  249. package/src/mime-walker.test.ts +0 -311
  250. package/src/mime-walker.ts +0 -305
  251. package/src/outbox-attachment-content.test.ts +0 -153
  252. package/src/outbox-attachment-content.ts +0 -104
  253. package/src/outbox-attachment-filename.test.ts +0 -132
  254. package/src/outbox-attachment-filename.ts +0 -76
  255. package/src/outbox-attachment.test.ts +0 -738
  256. package/src/outbox-attachment.ts +0 -536
  257. package/src/outbox-failed-recovery.test.ts +0 -370
  258. package/src/outbox-queue-settled-row.test.ts +0 -140
  259. package/src/outbox-queue.test.ts +0 -291
  260. package/src/outbox-queue.ts +0 -508
  261. package/src/pass-through-unit-of-work.ts +0 -17
  262. package/src/placement-move-terminal.test.ts +0 -300
  263. package/src/placement-move-terminal.ts +0 -148
  264. package/src/placement-move-unsettled.test.ts +0 -275
  265. package/src/placement-move.ts +0 -336
  266. package/src/placement-settled.test.ts +0 -147
  267. package/src/placement-transition-race.test.ts +0 -482
  268. package/src/quarantine.test.ts +0 -191
  269. package/src/quarantine.ts +0 -228
  270. package/src/snippet.test.ts +0 -213
  271. package/src/snippet.ts +0 -152
  272. package/src/spam-report.test.ts +0 -801
  273. package/src/spam-report.ts +0 -357
  274. package/src/stale-message-reconcile.test.ts +0 -82
  275. package/src/stale-message-reconcile.ts +0 -55
  276. package/src/sync-watermarks.test.ts +0 -409
  277. package/src/sync-watermarks.ts +0 -319
  278. package/src/system-flag-wire-format.test.ts +0 -77
  279. package/src/test-helpers/folder-roles.ts +0 -66
  280. package/src/test-helpers/isolated-mailbox.ts +0 -44
  281. package/src/test-helpers/mailfuzz-connection.ts +0 -27
  282. package/src/test-helpers/mime-tree-generator.ts +0 -461
  283. package/src/text/normalizer.test.ts +0 -223
  284. package/src/text/normalizer.ts +0 -140
  285. package/src/thread-message-rows.ts +0 -77
  286. package/src/types/mailparser-augment.d.ts +0 -15
  287. package/src/types/natural-porter-stemmer.d.ts +0 -50
  288. package/src/types/stopword.d.ts +0 -69
  289. package/src/types.ts +0 -371
  290. package/test/fixtures/mime-adversarial-e2e/01-octet-stream-pdf.eml +0 -21
  291. package/test/fixtures/mime-adversarial-e2e/02-nested-related-cid-image.eml +0 -31
  292. package/test/fixtures/mime-adversarial-e2e/03-calendar-invite.eml +0 -33
  293. package/test/fixtures/mime-adversarial-e2e/04-forwarded-rfc822-with-attachments.eml +0 -42
  294. package/test/fixtures/mime-adversarial-e2e/05-tnef-winmail.eml +0 -25
  295. package/test/fixtures/mime-adversarial-e2e/06-8bit-non-ascii.eml +0 -15
  296. package/test/fixtures/mime-adversarial-e2e/07-quoted-printable-soft-breaks.eml +0 -17
  297. package/test/fixtures/mime-adversarial-e2e/08-alternative-empty-text.eml +0 -18
  298. package/tsconfig.json +0 -8
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Message parsing utilities for extracting content from raw RFC822 messages.
3
+ */
4
+ export interface ParsedMessageContent {
5
+ text: string | null;
6
+ html: string | null;
7
+ }
8
+ /**
9
+ * Parse a raw RFC822 message buffer and extract text/html content.
10
+ *
11
+ * @param buffer - The raw message content (RFC822 format)
12
+ * @returns Parsed content with text and html fields
13
+ */
14
+ export declare const parseMessageContent: (buffer: Buffer) => Promise<ParsedMessageContent>;
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Message parsing utilities for extracting content from raw RFC822 messages.
3
+ */
4
+ import { simpleParser } from "mailparser";
5
+ /**
6
+ * Parse a raw RFC822 message buffer and extract text/html content.
7
+ *
8
+ * @param buffer - The raw message content (RFC822 format)
9
+ * @returns Parsed content with text and html fields
10
+ */
11
+ export const parseMessageContent = async (buffer) => {
12
+ const parsed = await simpleParser(buffer);
13
+ return {
14
+ text: parsed.text ?? null,
15
+ html: typeof parsed.html === "string" ? parsed.html : null,
16
+ };
17
+ };
@@ -1,5 +1,4 @@
1
1
  import type { IImapConnection } from "./types.js";
2
-
3
2
  /**
4
3
  * Whether a UID is confirmed gone from the currently open mailbox.
5
4
  *
@@ -21,15 +20,4 @@ import type { IImapConnection } from "./types.js";
21
20
  * hands this function a deletion authority it cannot tell apart from a genuine
22
21
  * miss.
23
22
  */
24
- export const isMessageGoneFromOpenMailbox = async (
25
- connection: Pick<IImapConnection, "fetchMessages" | "search">,
26
- uid: number,
27
- ): Promise<boolean> => {
28
- const found = await connection.fetchMessages([uid]);
29
- if (found.length > 0) {
30
- return false;
31
- }
32
-
33
- const matched = await connection.search([["UID", String(uid)]]);
34
- return !matched.includes(uid);
35
- };
23
+ export declare const isMessageGoneFromOpenMailbox: (connection: Pick<IImapConnection, "fetchMessages" | "search">, uid: number) => Promise<boolean>;
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Whether a UID is confirmed gone from the currently open mailbox.
3
+ *
4
+ * A FETCH that yields no row is not proof of absence. imapflow drops rows on
5
+ * back-to-back FETCHes (#408) — the same client glitch #100 had to stop
6
+ * reading as authoritative on the sync path — so a transient blip returns an
7
+ * empty result for a message that is still on the server. Anything that
8
+ * deletes local rows on that reading destroys live mail.
9
+ *
10
+ * Absence is therefore confirmed with a UID SEARCH, which the server answers
11
+ * with a plain UID set rather than a stream of message rows, and which
12
+ * `placement-move-push.ts` already uses as its verification probe. Only a
13
+ * SEARCH that does not list the UID counts as gone; an empty FETCH the SEARCH
14
+ * contradicts leaves the message present, and the caller treats it as such.
15
+ *
16
+ * The absence verdict rests on an empty SEARCH meaning the server matched
17
+ * nothing, so `IImapConnection.search` must throw when a SEARCH fails rather
18
+ * than answer `[]` — an implementation that reports failure as an empty array
19
+ * hands this function a deletion authority it cannot tell apart from a genuine
20
+ * miss.
21
+ */
22
+ export const isMessageGoneFromOpenMailbox = async (connection, uid) => {
23
+ const found = await connection.fetchMessages([uid]);
24
+ if (found.length > 0) {
25
+ return false;
26
+ }
27
+ const matched = await connection.search([["UID", String(uid)]]);
28
+ return !matched.includes(uid);
29
+ };
@@ -0,0 +1,51 @@
1
+ /**
2
+ * The slice of `BodySyncService` the read path needs to lazily materialize
3
+ * deferred per-part objects.
4
+ */
5
+ export interface BodyPartMaterializer {
6
+ ensureBodyPartsStored(accountConfigId: string, accountId: string, messageId: string, bodyStorageKey: string): Promise<unknown>;
7
+ }
8
+ export interface MaterializeLogger {
9
+ warn(obj: Record<string, unknown>, msg: string): void;
10
+ error(obj: Record<string, unknown>, msg: string): void;
11
+ }
12
+ /**
13
+ * Re-arms the body-sync cue for one message (the read-path slice of
14
+ * `BodySyncQueueService`).
15
+ */
16
+ export interface BodySyncCue {
17
+ requestBodySync(input: {
18
+ accountId: string;
19
+ mailboxId: string;
20
+ messageId: string;
21
+ uid?: number;
22
+ }): Promise<void>;
23
+ }
24
+ export interface MaterializeResult {
25
+ /** `false` when the body object was missing — the caller must not serve the body. */
26
+ ready: boolean;
27
+ }
28
+ /**
29
+ * Materialize the deferred per-part storage objects for a message before any
30
+ * part is read. Distinguishes the two failure modes the body contract cares
31
+ * about:
32
+ *
33
+ * - Missing storage object (never synced or lost) — re-arm the SYNC_MESSAGE_BODY
34
+ * cue so the worker (re)stores the body, and report `ready: false`. Not a
35
+ * 500: a retry succeeds once the worker catches up.
36
+ * - Any other error — rethrow so the request 500s and the bug is observable.
37
+ * Never swallowed, never "best-effort".
38
+ *
39
+ * No-op (`ready: true`) when the body isn't stored yet (no `bodyStorageKey`) or
40
+ * when the parts were written eagerly.
41
+ */
42
+ export declare const materializeBodyParts: (materializer: BodyPartMaterializer, args: {
43
+ accountConfigId: string;
44
+ accountId: string;
45
+ mailboxId: string;
46
+ messageId: string;
47
+ uid?: number;
48
+ bodyStorageKey: string | undefined;
49
+ cue: BodySyncCue;
50
+ logger: MaterializeLogger;
51
+ }) => Promise<MaterializeResult>;
@@ -0,0 +1,31 @@
1
+ import { isStorageNotFoundError } from "@remit/storage-service";
2
+ /**
3
+ * Materialize the deferred per-part storage objects for a message before any
4
+ * part is read. Distinguishes the two failure modes the body contract cares
5
+ * about:
6
+ *
7
+ * - Missing storage object (never synced or lost) — re-arm the SYNC_MESSAGE_BODY
8
+ * cue so the worker (re)stores the body, and report `ready: false`. Not a
9
+ * 500: a retry succeeds once the worker catches up.
10
+ * - Any other error — rethrow so the request 500s and the bug is observable.
11
+ * Never swallowed, never "best-effort".
12
+ *
13
+ * No-op (`ready: true`) when the body isn't stored yet (no `bodyStorageKey`) or
14
+ * when the parts were written eagerly.
15
+ */
16
+ export const materializeBodyParts = async (materializer, args) => {
17
+ const { accountConfigId, accountId, mailboxId, messageId, uid } = args;
18
+ if (!args.bodyStorageKey)
19
+ return { ready: true };
20
+ try {
21
+ await materializer.ensureBodyPartsStored(accountConfigId, accountId, messageId, args.bodyStorageKey);
22
+ return { ready: true };
23
+ }
24
+ catch (error) {
25
+ if (!isStorageNotFoundError(error))
26
+ throw error;
27
+ args.logger.warn({ messageId, accountId, mailboxId }, "Body object missing on read; re-arming body-sync cue and signalling retry");
28
+ await args.cue.requestBodySync({ accountId, mailboxId, messageId, uid });
29
+ return { ready: false };
30
+ }
31
+ };
@@ -0,0 +1,345 @@
1
+ import type { IAddressRepository, IEnvelopeRepository, IMailboxRepository, IMailboxSpecialUseRepository, IMessageFlagPushRepository, IMessageFlagRepository, IMessageRepository, IThreadMessageRepository } from "@remit/data-ports";
2
+ import type { JunkRoleMailboxes } from "@remit/data-ports/folder-role";
3
+ import type { ManagedConnectionFactory } from "./connection-factory.js";
4
+ import type { QuarantineService } from "./quarantine.js";
5
+ import type { SyncWarningService } from "./sync-warning.js";
6
+ import type { ImapAddress } from "./types.js";
7
+ /**
8
+ * A From address is only usable when it looks like a real mailbox: both parts
9
+ * present, neither is a known "could not parse" sentinel, and the host carries
10
+ * an actual domain (at least one dot). Detect this structurally so a fabricated
11
+ * string like `missing_mailbox@missing_domain` is never persisted as a sender.
12
+ */
13
+ export declare const isParseableEmailAddress: (address: ImapAddress | undefined) => boolean;
14
+ export type AddressSighting = "junk" | "discarded" | "correspondent";
15
+ /** The account's Junk and Trash mailboxes, resolved once per batch. */
16
+ export interface AccountFolderRoles {
17
+ readonly junkMailboxId: string | null;
18
+ readonly trashMailboxId: string | null;
19
+ /**
20
+ * The same two roles across every account under the config, for the
21
+ * reconcile that follows a save into Junk. The address book is keyed by
22
+ * config, so a predicate over an address weighs sightings from all of them —
23
+ * resolving only this account withholds a sender on one and restores it on
24
+ * the next.
25
+ */
26
+ readonly configJunkRoles: JunkRoleMailboxes;
27
+ }
28
+ /**
29
+ * What a sighting of an address in this mailbox is worth. Junk withholds the
30
+ * sender from autocomplete (#822), Trash decides nothing either way, and
31
+ * anywhere else is a correspondent.
32
+ *
33
+ * Decided by mailbox IDENTITY against the account's resolved roles, not by the
34
+ * folder's flag and name: a role belongs to exactly one folder (RFC 032), and
35
+ * the folder it belongs to is whichever one the user appointed. Reading the
36
+ * name here meant a second folder called `Spam` also withheld its senders,
37
+ * while the folder the user actually appointed harvested every spammer in it as
38
+ * a correspondent (#837).
39
+ */
40
+ export declare const addressSightingIn: (mailboxId: string, roles: AccountFolderRoles) => AddressSighting;
41
+ /**
42
+ * The name an envelope carries for an address, as it should be stored: any
43
+ * claim to be some other address removed (issue #826), the rest of the name
44
+ * kept. Refused wherever a name lands — the address book, the envelope the
45
+ * message header renders, and the sender label on the thread row.
46
+ */
47
+ export declare const harvestedDisplayName: (address: ImapAddress | undefined) => string;
48
+ /**
49
+ * Parse an external `Date:` header into an epoch-millisecond integer.
50
+ *
51
+ * The IMAP envelope `date` is a raw RFC 2822 header copied verbatim from the
52
+ * message. It can be missing, malformed, or in a format `Date` cannot parse —
53
+ * `new Date(raw).getTime()` then yields `NaN`. `NaN` is not a valid integer
54
+ * and ElectroDB rejects it, which previously threw on the envelope upsert and
55
+ * (because the batch aborted on the first rejection) stalled the whole mailbox.
56
+ *
57
+ * When the header is unparseable we fall back to `fallbackMs` — the IMAP
58
+ * server's own INTERNALDATE receive time, always a valid integer. The raw
59
+ * header is preserved separately in `dateRaw`, so nothing is lost.
60
+ */
61
+ export declare const parseHeaderDate: (raw: string | undefined, fallbackMs: number) => {
62
+ value: number;
63
+ usedFallback: boolean;
64
+ };
65
+ /**
66
+ * @deprecated Use ManagedConnectionFactory instead
67
+ */
68
+ export type ImapConnectionFactory = () => {
69
+ connect(): Promise<void>;
70
+ disconnect(): Promise<void>;
71
+ };
72
+ export interface SyncLogger {
73
+ info(obj: Record<string, unknown>, msg: string): void;
74
+ warn(obj: Record<string, unknown>, msg: string): void;
75
+ error(obj: Record<string, unknown>, msg: string): void;
76
+ }
77
+ export interface SyncedMessage {
78
+ messageId: string;
79
+ uid: number;
80
+ }
81
+ export interface SyncMessagesResult {
82
+ syncedCount: number;
83
+ syncedMessageIds: string[];
84
+ syncedMessages: SyncedMessage[];
85
+ hasMore: boolean;
86
+ remainingCount: number;
87
+ /**
88
+ * The round had work to do and moved no cursor. Nothing throws on this
89
+ * path — a message that cannot be applied is caught and held back — so
90
+ * without this flag a mailbox that stops syncing looks exactly like one
91
+ * with nothing to sync. Callers must surface it.
92
+ */
93
+ cursorStalled: boolean;
94
+ }
95
+ /**
96
+ * Pick the UIDs a full-enumeration round should sync, newest first.
97
+ *
98
+ * 1. New messages: UIDs above the forward watermark.
99
+ * 2. Backfill: UIDs below the lowest one synced so far.
100
+ * 3. A mailbox with no watermarks at all syncs everything.
101
+ */
102
+ export declare const selectUidsToSync: (allUids: number[], lastSyncUid: number, highWaterMarkUid: number) => number[];
103
+ export declare class MessageSyncService {
104
+ private connectionFactory;
105
+ private mailboxService;
106
+ /**
107
+ * The account’s folder-role map. Required, not optional: which folder
108
+ * holds the Junk role decides whether a sighting withholds its sender
109
+ * from autocomplete, and a construction without it would quietly harvest
110
+ * spammers as correspondents.
111
+ */
112
+ private mailboxSpecialUseService;
113
+ private messageService;
114
+ private envelopeService;
115
+ private addressService;
116
+ private threadMessageService;
117
+ private syncWarningService;
118
+ /**
119
+ * Pending outbound flag-push markers (#1273). Supplied, an inbound
120
+ * metadata change never overwrites a local flip that IMAP has not been
121
+ * told about yet; omitted, the server always wins.
122
+ */
123
+ private flagPushMarkerService?;
124
+ /**
125
+ * The canonical flag record. Supplied, an inbound metadata change lands
126
+ * on the same record the outbound flip path reads, so a user's next
127
+ * flip is never dismissed as redundant.
128
+ */
129
+ private messageFlagService?;
130
+ /**
131
+ * The set of messages already set aside (issue #72). Message sync never
132
+ * writes a record — only the body path can attribute a failure to the
133
+ * message — but it reads the set so a uid the body path quarantined is
134
+ * not re-fetched on every round.
135
+ */
136
+ private quarantineService?;
137
+ private log;
138
+ constructor(connectionFactory: ManagedConnectionFactory, mailboxService: IMailboxRepository,
139
+ /**
140
+ * The account’s folder-role map. Required, not optional: which folder
141
+ * holds the Junk role decides whether a sighting withholds its sender
142
+ * from autocomplete, and a construction without it would quietly harvest
143
+ * spammers as correspondents.
144
+ */
145
+ mailboxSpecialUseService: IMailboxSpecialUseRepository, messageService: IMessageRepository, envelopeService: IEnvelopeRepository, addressService: IAddressRepository, threadMessageService: IThreadMessageRepository, syncWarningService: SyncWarningService, logger?: SyncLogger,
146
+ /**
147
+ * Pending outbound flag-push markers (#1273). Supplied, an inbound
148
+ * metadata change never overwrites a local flip that IMAP has not been
149
+ * told about yet; omitted, the server always wins.
150
+ */
151
+ flagPushMarkerService?: IMessageFlagPushRepository | undefined,
152
+ /**
153
+ * The canonical flag record. Supplied, an inbound metadata change lands
154
+ * on the same record the outbound flip path reads, so a user's next
155
+ * flip is never dismissed as redundant.
156
+ */
157
+ messageFlagService?: IMessageFlagRepository | undefined,
158
+ /**
159
+ * The set of messages already set aside (issue #72). Message sync never
160
+ * writes a record — only the body path can attribute a failure to the
161
+ * message — but it reads the set so a uid the body path quarantined is
162
+ * not re-fetched on every round.
163
+ */
164
+ quarantineService?: QuarantineService | undefined);
165
+ /**
166
+ * Sync ONE batch of messages for a mailbox using newest-first strategy.
167
+ *
168
+ * Uses dual-watermark tracking:
169
+ * - highWaterMarkUid: highest UID ever seen (detects new messages)
170
+ * - lastSyncUid: lowest UID processed (tracks backfill progress)
171
+ *
172
+ * Returns hasMore=true if there are more messages to sync. The caller
173
+ * should re-enqueue another sync event to continue processing.
174
+ *
175
+ * @param mailboxId - The database mailbox ID
176
+ * @param accountId - The account ID (scopes message/thread identity)
177
+ * @param accountConfigId - The account config ID (used for address linking)
178
+ * @param batchSize - Number of messages to process per batch
179
+ */
180
+ syncMessages(mailboxId: string, accountId: string, accountConfigId: string, batchSize?: number): Promise<SyncMessagesResult>;
181
+ /**
182
+ * Re-key a mailbox's stored UIDs against a new UIDVALIDITY axis (#1272).
183
+ *
184
+ * One envelope-level pass (UID + Message-ID + INTERNALDATE, no body
185
+ * fetches — {@link matchCursorRebuild}) matched against the rows already
186
+ * stored for this mailbox:
187
+ * - Match → rewrite the row's UID mapping in place; bodies and threads are
188
+ * untouched.
189
+ * - Server message with no row → normal new-message sync (the same
190
+ * `trySaveMessage` pipeline the regular batch sync uses).
191
+ * - Row with no counterpart → expunged; removed through `deleteIfPlacement`
192
+ * on the uid the rebuild read (#1283 — the exact same "gone upstream"
193
+ * outcome as a body-sync retry exhaustion finding a stale row), so a row
194
+ * a move re-pointed in the meantime is left alone.
195
+ *
196
+ * Idempotent by construction: re-entering mid-rebuild (crash recovery —
197
+ * the mailbox was left `rebuilding`) simply redoes the same match/rewrite
198
+ * pass, which converges on the same result. `cursorState` is stamped
199
+ * `rebuilding` before any write and only cleared to `normal` after the
200
+ * watermarks are rebuilt, so a crash anywhere in between leaves the
201
+ * mailbox paused rather than falling back to the stale axis.
202
+ */
203
+ private rebuildCursor;
204
+ /**
205
+ * Page through every ThreadMessage row for this mailbox, projecting just
206
+ * the fields {@link matchCursorRebuild} needs. ThreadMessage (not
207
+ * Message) is the read source: it already denormalizes `messageIdHeader`
208
+ * and `internalDate` alongside `uid`, so this needs no per-row Envelope
209
+ * lookup.
210
+ */
211
+ private listExistingCursorRows;
212
+ /**
213
+ * One CHANGEDSINCE round (issue #20).
214
+ *
215
+ * A single `FETCH ... (CHANGEDSINCE <modseq>)` over the whole UID space
216
+ * returns both the messages that arrived and the messages whose metadata
217
+ * changed since the stored watermark, so a flag flipped on another client
218
+ * is picked up without enumerating the folder. Rows already present in
219
+ * this folder take the metadata path — the envelope, body structure and
220
+ * addresses are immutable, so re-writing them for a read-state change would
221
+ * be pure waste; everything else, a row another client moved here from a
222
+ * folder it points at included, takes the normal save pipeline (#1386).
223
+ *
224
+ * Expunges are invisible here (CONDSTORE without QRESYNC never reports
225
+ * them, RFC 7162 Section 3.1.2.1) — they remain the reconcile path's job.
226
+ */
227
+ private syncChangedSince;
228
+ private settleHeaderWarnings;
229
+ /**
230
+ * Apply one message from a CHANGEDSINCE result without ever rejecting —
231
+ * same contract as {@link trySaveMessage}, so a single unapplicable change
232
+ * holds the watermark back instead of failing the round.
233
+ */
234
+ private tryApplyChange;
235
+ private applyChange;
236
+ /**
237
+ * Bring a stored row's read and star state, and the $Junk/$NotJunk
238
+ * keywords on the canonical MessageFlag record, in line with the
239
+ * server's flags.
240
+ *
241
+ * A field with a pending outbound push is left alone: the user flipped it
242
+ * locally, IMAP has not been told yet, and the server's answer is
243
+ * therefore known-stale for that field. Writing it back would revert the
244
+ * flip in front of the user and then push the reverted value.
245
+ */
246
+ private applyServerFlags;
247
+ /**
248
+ * Set or clear one flag on the canonical record. Both repository calls are
249
+ * idempotent, so a re-applied change is a no-op rather than a conflict.
250
+ */
251
+ private setMessageFlag;
252
+ private hasPendingPush;
253
+ /**
254
+ * Mirror one RFC 5788 keyword ($Junk / $NotJunk) from the server into the
255
+ * canonical MessageFlag record, guarded the same way as the system flags:
256
+ * a pending local push wins over the server's known-stale answer.
257
+ *
258
+ * This mailbox is never remit-only — Apple Mail or another IMAP client is
259
+ * routinely connected too — so the keyword is message state, nothing
260
+ * more. It never reaches the Address model: reading another client's
261
+ * keyword as a standing rule about the sender is the exact fight this
262
+ * mirror must not pick.
263
+ */
264
+ private mirrorKeywordFlag;
265
+ /**
266
+ * Messages the forwarded-message repair queued — their part rows dropped,
267
+ * the lost row only on the mail server — are fetched again by UID and saved
268
+ * through the same path as any other sighting, which writes their parts. No
269
+ * watermark moves: these UIDs are already behind them. Each leaves the queue
270
+ * once saved or once the server returns no usable row for it, whatever
271
+ * parts came back; only a failed save keeps it for the next round. Resolves
272
+ * `true` when it touched the connection, which the probe of departures may
273
+ * have left on another folder.
274
+ */
275
+ private restoreMissingStructure;
276
+ /**
277
+ * Fetch a batch of messages using the managed connection.
278
+ * Assumes mailbox is already open from fetchUidsToSync.
279
+ */
280
+ private fetchMessageBatch;
281
+ /**
282
+ * Save a single message without ever rejecting. Any error is caught and
283
+ * reported as a `failed` outcome so it cannot abort the surrounding `pMap`
284
+ * batch — the failed UID is held back from the watermark and retried next
285
+ * cycle. This is the guardrail that stops a single unsaveable message from
286
+ * permanently freezing the mailbox (#817).
287
+ */
288
+ /**
289
+ * Which folders hold Junk and Trash for this account, resolved once per
290
+ * batch rather than per message: the lookup reads an appointment row and
291
+ * the account’s mailbox list, and every message in a batch shares both.
292
+ */
293
+ private folderRolesFor;
294
+ /**
295
+ * Ask the folders this batch's rows point at whether they still hold them
296
+ * (#1146).
297
+ *
298
+ * Between the batch FETCH and the save pass, in all three rounds, and in that
299
+ * position on purpose. It SELECTs another folder on the one connection this
300
+ * sync has, so it may not run while this mailbox's own fetches are still
301
+ * outstanding; and the save pass has to have the answer already, because the
302
+ * transaction that re-points a row is where the answer is spent. Nothing
303
+ * after the save pass touches IMAP, so leaving another folder selected costs
304
+ * the round nothing.
305
+ */
306
+ private probeDepartures;
307
+ private trySaveMessage;
308
+ private saveMessage;
309
+ /**
310
+ * Whether this folder already holds a copy the user made of this message
311
+ * (#75). A copy is a second row under its own derived id, so the sighting is
312
+ * that copy standing where it was put — the original has not moved, and
313
+ * following it would take the mail out of the folder the user copied it from.
314
+ */
315
+ private holdsCopyOf;
316
+ private saveAddresses;
317
+ /**
318
+ * The thread a synced message files under. A message keeps the thread it is
319
+ * already stored in; a reply joins the thread its parent is stored in;
320
+ * otherwise the thread is derived from the root `threadRootOf` names.
321
+ */
322
+ private threadFor;
323
+ /**
324
+ * Joins the rows written on either side of a parent and its reply, after
325
+ * this message's own row exists. Replies that arrived before this message,
326
+ * and so were keyed on its own id, follow it into its thread; and a reply
327
+ * whose parent was saved while it was being saved follows the parent, but
328
+ * only from the thread it derived for itself — a thread it joined or was
329
+ * already stored in is left as it is.
330
+ *
331
+ * Both checks run after the create, so of a parent and a reply saved
332
+ * concurrently at least one sees the other's row: neither is left in a
333
+ * thread of its own.
334
+ */
335
+ private joinThreads;
336
+ /**
337
+ * Create the ThreadMessage for a synced message, then join it to its
338
+ * parent and replies.
339
+ *
340
+ * `category` is the value the caller already holds from the Message it just
341
+ * wrote, so the row starts out agreeing with it rather than defaulting to
342
+ * `uncategorized` behind an already-classified message.
343
+ */
344
+ private createThreadForMessage;
345
+ }