@remit/mailbox-service 0.0.112 → 0.0.113

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (270) hide show
  1. package/dist/account-credentials.d.ts +83 -0
  2. package/dist/account-credentials.js +104 -0
  3. package/dist/attribute-mapper.d.ts +36 -0
  4. package/dist/attribute-mapper.js +119 -0
  5. package/dist/body-parse.d.ts +28 -0
  6. package/{src/body-parse.ts → dist/body-parse.js} +18 -26
  7. package/dist/body-part-mapper.d.ts +92 -0
  8. package/dist/body-part-mapper.js +301 -0
  9. package/dist/body-sync-queue.d.ts +59 -0
  10. package/dist/body-sync-queue.js +56 -0
  11. package/dist/body-sync-terminal.d.ts +58 -0
  12. package/dist/body-sync-terminal.js +95 -0
  13. package/dist/body-sync.d.ts +351 -0
  14. package/dist/body-sync.js +1204 -0
  15. package/dist/calendar-parts.d.ts +23 -0
  16. package/dist/calendar-parts.js +18 -0
  17. package/dist/classification-backfill.d.ts +91 -0
  18. package/dist/classification-backfill.js +180 -0
  19. package/dist/classify-message.d.ts +31 -0
  20. package/dist/classify-message.js +89 -0
  21. package/dist/connection-factory.d.ts +54 -0
  22. package/dist/connection-factory.js +54 -0
  23. package/dist/connection-test.d.ts +41 -0
  24. package/dist/connection-test.js +152 -0
  25. package/dist/external-move.d.ts +94 -0
  26. package/dist/external-move.js +173 -0
  27. package/dist/filter-operations.d.ts +42 -0
  28. package/dist/filter-operations.js +211 -0
  29. package/dist/filters/filter-config.d.ts +14 -0
  30. package/dist/filters/filter-config.js +10 -0
  31. package/{src/filters/list-id.ts → dist/filters/list-id.d.ts} +2 -16
  32. package/dist/filters/list-id.js +26 -0
  33. package/dist/filters/match.d.ts +69 -0
  34. package/dist/filters/match.js +165 -0
  35. package/dist/filters/pipeline.d.ts +64 -0
  36. package/dist/filters/pipeline.js +100 -0
  37. package/{src/flag-push-terminal.ts → dist/flag-push-terminal.d.ts} +16 -72
  38. package/dist/flag-push-terminal.js +63 -0
  39. package/dist/flag-push.d.ts +69 -0
  40. package/dist/flag-push.js +111 -0
  41. package/dist/flag-queue.d.ts +175 -0
  42. package/dist/flag-queue.js +291 -0
  43. package/dist/folder-operations.d.ts +39 -0
  44. package/dist/folder-operations.js +112 -0
  45. package/dist/folder-placement.d.ts +13 -0
  46. package/dist/folder-placement.js +23 -0
  47. package/dist/heuristics/authenticationResults.d.ts +10 -0
  48. package/dist/heuristics/authenticationResults.js +187 -0
  49. package/dist/heuristics/classifyByHeaders.d.ts +82 -0
  50. package/dist/heuristics/classifyByHeaders.js +365 -0
  51. package/dist/heuristics/classifyPlacement.d.ts +67 -0
  52. package/dist/heuristics/classifyPlacement.js +153 -0
  53. package/dist/heuristics/machineSenders.d.ts +21 -0
  54. package/{src/heuristics/machineSenders.ts → dist/heuristics/machineSenders.js} +34 -40
  55. package/dist/heuristics/senderMismatch.d.ts +81 -0
  56. package/dist/heuristics/senderMismatch.js +192 -0
  57. package/dist/heuristics/socialDomains.d.ts +14 -0
  58. package/{src/heuristics/socialDomains.ts → dist/heuristics/socialDomains.js} +9 -9
  59. package/dist/heuristics/transactionalDomains.d.ts +18 -0
  60. package/{src/heuristics/transactionalDomains.ts → dist/heuristics/transactionalDomains.js} +8 -8
  61. package/dist/imapflow-connection.d.ts +377 -0
  62. package/dist/imapflow-connection.js +1147 -0
  63. package/dist/index.d.ts +63 -0
  64. package/dist/index.js +65 -0
  65. package/dist/list-id-backfill.d.ts +83 -0
  66. package/dist/list-id-backfill.js +132 -0
  67. package/dist/mailbox-cursor-rebuild.d.ts +74 -0
  68. package/dist/mailbox-cursor-rebuild.js +71 -0
  69. package/{src/mailbox-cursor.ts → dist/mailbox-cursor.d.ts} +20 -98
  70. package/dist/mailbox-cursor.js +102 -0
  71. package/dist/mailbox-intent.d.ts +41 -0
  72. package/{src/mailbox-intent.ts → dist/mailbox-intent.js} +38 -64
  73. package/dist/mailbox-management.d.ts +240 -0
  74. package/dist/mailbox-management.js +576 -0
  75. package/dist/mailbox-presence.d.ts +52 -0
  76. package/{src/mailbox-presence.ts → dist/mailbox-presence.js} +19 -30
  77. package/dist/mailbox-queue.d.ts +126 -0
  78. package/dist/mailbox-queue.js +214 -0
  79. package/dist/mailbox-sync.d.ts +93 -0
  80. package/dist/mailbox-sync.js +490 -0
  81. package/dist/mailbox-upstream.d.ts +38 -0
  82. package/{src/mailbox-upstream.ts → dist/mailbox-upstream.js} +37 -47
  83. package/dist/message-category.d.ts +51 -0
  84. package/{src/message-category.ts → dist/message-category.js} +28 -71
  85. package/dist/message-move.d.ts +285 -0
  86. package/dist/message-move.js +858 -0
  87. package/dist/message-parser.d.ts +14 -0
  88. package/dist/message-parser.js +17 -0
  89. package/{src/message-presence.ts → dist/message-presence.d.ts} +1 -13
  90. package/dist/message-presence.js +29 -0
  91. package/dist/message-read.d.ts +51 -0
  92. package/dist/message-read.js +31 -0
  93. package/dist/message-sync.d.ts +345 -0
  94. package/dist/message-sync.js +1358 -0
  95. package/dist/mime-walker.d.ts +102 -0
  96. package/dist/mime-walker.js +198 -0
  97. package/dist/organize-job.d.ts +20 -0
  98. package/dist/organize-job.js +37 -0
  99. package/dist/outbox-attachment-content.d.ts +37 -0
  100. package/dist/outbox-attachment-content.js +50 -0
  101. package/dist/outbox-attachment-filename.d.ts +19 -0
  102. package/dist/outbox-attachment-filename.js +64 -0
  103. package/dist/outbox-attachment.d.ts +148 -0
  104. package/dist/outbox-attachment.js +273 -0
  105. package/dist/outbox-queue.d.ts +73 -0
  106. package/dist/outbox-queue.js +284 -0
  107. package/{src/outbox-status.ts → dist/outbox-status.d.ts} +1 -6
  108. package/dist/outbox-status.js +18 -0
  109. package/dist/ownership.d.ts +24 -0
  110. package/dist/ownership.js +83 -0
  111. package/dist/placement-move-terminal.d.ts +67 -0
  112. package/dist/placement-move-terminal.js +88 -0
  113. package/dist/placement-move.d.ts +83 -0
  114. package/dist/placement-move.js +216 -0
  115. package/{src/placement-settled.ts → dist/placement-settled.d.ts} +13 -68
  116. package/dist/placement-settled.js +75 -0
  117. package/dist/quarantine.d.ts +95 -0
  118. package/dist/quarantine.js +131 -0
  119. package/dist/refusal.d.ts +31 -0
  120. package/dist/refusal.js +3 -0
  121. package/{src/restore-source-placement.ts → dist/restore-source-placement.d.ts} +29 -97
  122. package/dist/restore-source-placement.js +79 -0
  123. package/dist/search-text-backfill.d.ts +15 -0
  124. package/dist/search-text-backfill.js +33 -0
  125. package/dist/snippet.d.ts +61 -0
  126. package/dist/snippet.js +129 -0
  127. package/dist/spam-report.d.ts +86 -0
  128. package/dist/spam-report.js +241 -0
  129. package/dist/sync-warning.d.ts +21 -0
  130. package/dist/sync-warning.js +26 -0
  131. package/dist/sync-watermarks.d.ts +138 -0
  132. package/dist/sync-watermarks.js +205 -0
  133. package/dist/test-helpers/fake-imap.d.ts +151 -0
  134. package/dist/test-helpers/fake-imap.js +508 -0
  135. package/dist/test-helpers/folder-roles.d.ts +34 -0
  136. package/dist/test-helpers/folder-roles.js +47 -0
  137. package/dist/test-helpers/mime-tree-generator.d.ts +82 -0
  138. package/dist/test-helpers/mime-tree-generator.js +329 -0
  139. package/dist/test-helpers/test-store.d.ts +69 -0
  140. package/dist/test-helpers/test-store.js +193 -0
  141. package/dist/text/normalizer.d.ts +20 -0
  142. package/dist/text/normalizer.js +102 -0
  143. package/dist/thread-message-rows.d.ts +69 -0
  144. package/dist/thread-message-rows.js +40 -0
  145. package/dist/thread-root.d.ts +7 -0
  146. package/dist/thread-root.js +25 -0
  147. package/dist/types.d.ts +343 -0
  148. package/dist/types.js +19 -0
  149. package/package.json +86 -88
  150. package/src/account-credentials.ts +0 -210
  151. package/src/attribute-mapper.test.ts +0 -104
  152. package/src/attribute-mapper.ts +0 -148
  153. package/src/body-parse.test.ts +0 -55
  154. package/src/body-part-mapper.property.test.ts +0 -354
  155. package/src/body-part-mapper.ts +0 -410
  156. package/src/body-sync-queue.test.ts +0 -64
  157. package/src/body-sync-queue.ts +0 -131
  158. package/src/body-sync-terminal.test.ts +0 -148
  159. package/src/body-sync-terminal.ts +0 -174
  160. package/src/body-sync.test.ts +0 -925
  161. package/src/body-sync.ts +0 -1891
  162. package/src/calendar-parts.ts +0 -41
  163. package/src/classification-backfill.test.ts +0 -285
  164. package/src/classification-backfill.ts +0 -363
  165. package/src/classify-message.test.ts +0 -142
  166. package/src/classify-message.ts +0 -116
  167. package/src/connection-factory.ts +0 -102
  168. package/src/connection-test.ts +0 -208
  169. package/src/credentials.test.ts +0 -181
  170. package/src/external-move.test.ts +0 -262
  171. package/src/external-move.ts +0 -285
  172. package/src/filter-operations.test.ts +0 -319
  173. package/src/filter-operations.ts +0 -377
  174. package/src/filter-update.test.ts +0 -313
  175. package/src/filters/filter-config.test.ts +0 -87
  176. package/src/filters/filter-config.ts +0 -34
  177. package/src/filters/list-id.test.ts +0 -50
  178. package/src/filters/match.test.ts +0 -392
  179. package/src/filters/match.ts +0 -225
  180. package/src/filters/pipeline.test.ts +0 -199
  181. package/src/filters/pipeline.ts +0 -170
  182. package/src/flag-push.test.ts +0 -112
  183. package/src/flag-push.ts +0 -198
  184. package/src/flag-queue.test.ts +0 -197
  185. package/src/flag-queue.ts +0 -509
  186. package/src/folder-operations.test.ts +0 -273
  187. package/src/folder-operations.ts +0 -217
  188. package/src/folder-placement.ts +0 -56
  189. package/src/forwarded-message.test.ts +0 -122
  190. package/src/heuristics/authenticationResults.test.ts +0 -189
  191. package/src/heuristics/authenticationResults.ts +0 -212
  192. package/src/heuristics/classifyByHeaders.test.ts +0 -889
  193. package/src/heuristics/classifyByHeaders.ts +0 -467
  194. package/src/heuristics/classifyPlacement.test.ts +0 -366
  195. package/src/heuristics/classifyPlacement.ts +0 -211
  196. package/src/heuristics/classifyRealMail.test.ts +0 -242
  197. package/src/heuristics/machineSenders.test.ts +0 -101
  198. package/src/heuristics/senderMismatch.test.ts +0 -346
  199. package/src/heuristics/senderMismatch.ts +0 -259
  200. package/src/imapflow-connect-retry.test.ts +0 -97
  201. package/src/imapflow-connection.test.ts +0 -503
  202. package/src/imapflow-connection.ts +0 -1476
  203. package/src/index.ts +0 -397
  204. package/src/list-id-backfill.test.ts +0 -207
  205. package/src/list-id-backfill.ts +0 -274
  206. package/src/mailbox-cursor-rebuild.test.ts +0 -171
  207. package/src/mailbox-cursor-rebuild.ts +0 -146
  208. package/src/mailbox-cursor.test.ts +0 -141
  209. package/src/mailbox-delete-intent.test.ts +0 -119
  210. package/src/mailbox-management.test.ts +0 -243
  211. package/src/mailbox-management.ts +0 -838
  212. package/src/mailbox-presence.test.ts +0 -74
  213. package/src/mailbox-queue.ts +0 -397
  214. package/src/mailbox-rename-intent.test.ts +0 -220
  215. package/src/mailbox-rename-subtree.test.ts +0 -287
  216. package/src/mailbox-sync.test.ts +0 -286
  217. package/src/mailbox-sync.ts +0 -724
  218. package/src/message-move.test.ts +0 -726
  219. package/src/message-move.ts +0 -1518
  220. package/src/message-parser.ts +0 -26
  221. package/src/message-presence.test.ts +0 -50
  222. package/src/message-read.ts +0 -87
  223. package/src/message-save-retry.test.ts +0 -90
  224. package/src/message-sync-flags.test.ts +0 -167
  225. package/src/message-sync.test.ts +0 -762
  226. package/src/message-sync.ts +0 -2133
  227. package/src/mime-walker.test.ts +0 -311
  228. package/src/mime-walker.ts +0 -306
  229. package/src/organize-job.ts +0 -93
  230. package/src/outbox-attachment-content.test.ts +0 -153
  231. package/src/outbox-attachment-content.ts +0 -104
  232. package/src/outbox-attachment-filename.test.ts +0 -132
  233. package/src/outbox-attachment-filename.ts +0 -76
  234. package/src/outbox-attachment.test.ts +0 -626
  235. package/src/outbox-attachment.ts +0 -536
  236. package/src/outbox-failed-recovery.test.ts +0 -299
  237. package/src/outbox-queue.test.ts +0 -315
  238. package/src/outbox-queue.ts +0 -525
  239. package/src/ownership.ts +0 -140
  240. package/src/placement-move-terminal.ts +0 -145
  241. package/src/placement-move.ts +0 -363
  242. package/src/placement-settled.test.ts +0 -147
  243. package/src/placement.test.ts +0 -404
  244. package/src/quarantine.test.ts +0 -133
  245. package/src/quarantine.ts +0 -229
  246. package/src/refusal.ts +0 -29
  247. package/src/snippet.test.ts +0 -213
  248. package/src/snippet.ts +0 -152
  249. package/src/spam-report.test.ts +0 -329
  250. package/src/spam-report.ts +0 -357
  251. package/src/sync-warning.test.ts +0 -213
  252. package/src/sync-warning.ts +0 -56
  253. package/src/sync-watermarks.test.ts +0 -409
  254. package/src/sync-watermarks.ts +0 -319
  255. package/src/system-flag-wire-format.test.ts +0 -77
  256. package/src/test-helpers/fake-imap.ts +0 -690
  257. package/src/test-helpers/folder-roles.ts +0 -66
  258. package/src/test-helpers/mime-tree-generator.ts +0 -461
  259. package/src/test-helpers/test-queue.ts +0 -56
  260. package/src/test-helpers/test-store.ts +0 -299
  261. package/src/text/normalizer.test.ts +0 -223
  262. package/src/text/normalizer.ts +0 -140
  263. package/src/thread-message-rows.ts +0 -77
  264. package/src/thread-root.test.ts +0 -29
  265. package/src/thread-root.ts +0 -51
  266. package/src/types/mailparser-augment.d.ts +0 -23
  267. package/src/types/natural-porter-stemmer.d.ts +0 -50
  268. package/src/types/stopword.d.ts +0 -69
  269. package/src/types.ts +0 -371
  270. package/tsconfig.json +0 -8
@@ -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
+ }