@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,26 @@
1
+ const listedUids = async (serverUids, recordedCount) => {
2
+ if (serverUids.kind === "Listed")
3
+ return new Set(serverUids.uids);
4
+ if (serverUids.kind === "OnRequest" && recordedCount > 0) {
5
+ return new Set(await serverUids.list());
6
+ }
7
+ return undefined;
8
+ };
9
+ export class SyncWarningService {
10
+ repository;
11
+ constructor(repository) {
12
+ this.repository = repository;
13
+ }
14
+ async settle(context, stage, round) {
15
+ await this.repository.recordFailures([...new Set(round.failedUids)].map((uid) => ({ ...context, uid, stage })));
16
+ const recorded = await this.repository.listByMailboxId(context.mailboxId);
17
+ const synced = new Set(round.syncedUids);
18
+ const listed = await listedUids(round.serverUids, recorded.length);
19
+ const cleared = recorded
20
+ .filter((row) => (listed !== undefined && !listed.has(row.uid)) ||
21
+ (row.stage === stage &&
22
+ (row.uidValidity !== context.uidValidity || synced.has(row.uid))))
23
+ .map((row) => row.syncWarningId);
24
+ await this.repository.deleteMany(cleared);
25
+ }
26
+ }
@@ -0,0 +1,138 @@
1
+ import type { ImapMessage } from "./types.js";
2
+ export declare const parseModseq: (raw: string | undefined) => bigint;
3
+ /**
4
+ * Order a CHANGEDSINCE result by mod-sequence, oldest change first, UID
5
+ * breaking ties.
6
+ *
7
+ * Mod-sequence order is what makes a partial round resumable: a batch taken
8
+ * off the front can advance the watermark to the last change it applied and
9
+ * the next round resumes there.
10
+ */
11
+ export declare const orderByModseq: (messages: ImapMessage[]) => ImapMessage[];
12
+ /**
13
+ * The message-sync cursor: how far CHANGEDSINCE has been applied.
14
+ *
15
+ * `modseq` is what the next fetch asks from. `group` and `uid` describe a
16
+ * mod-sequence a previous round left part-applied: which one, and how far into
17
+ * it the round got. `group` is 0 when the cursor sits on a clean boundary.
18
+ *
19
+ * The sub-position exists because one STORE assigns the SAME mod-sequence to
20
+ * every message it touched (RFC 7162 permits it and servers do it), so "select
21
+ * all, mark read" arrives as one group of arbitrary size. CHANGEDSINCE is
22
+ * strictly greater-than, so a cursor that advanced to a partly-applied group
23
+ * would never see its tail again; a cursor that refused to split it would have
24
+ * to apply the whole group in one round, which for a large mailbox does not
25
+ * finish, and every retry would repeat the same unfinished work. The
26
+ * sub-position is what lets one group span rounds without ever claiming more
27
+ * than was applied.
28
+ *
29
+ * The group is recorded rather than inferred from the fetch result. The
30
+ * in-progress group is NOT reliably the lowest mod-sequence returned: its
31
+ * remaining members can disappear between rounds, by being expunged (which
32
+ * CONDSTORE simply stops reporting) or by being modified again onto a higher
33
+ * mod-sequence. Inferring it then points the skip at an unrelated group and
34
+ * silently drops that group's leading messages.
35
+ */
36
+ export interface ChangeCursor {
37
+ /** Fetch from here — everything above it is still owed. */
38
+ modseq: bigint;
39
+ /** The mod-sequence left part-applied, or 0 on a clean boundary. */
40
+ group: bigint;
41
+ /** Highest UID applied within `group`. */
42
+ uid: number;
43
+ }
44
+ /**
45
+ * Read a stored cursor.
46
+ *
47
+ * Two forms: `"<modseq>"` is a clean boundary — every value written before the
48
+ * sub-position existed, which is what they always meant. `"<group>:<uid>"` is a
49
+ * position inside a group; the fetch point is implied, because a round only
50
+ * ever stops inside a group once everything below it has been applied.
51
+ *
52
+ * Storing the implied fetch point instead of a third number is what keeps the
53
+ * value inside the field's 32 characters: two 64-bit mod-sequences and a UID
54
+ * would not fit.
55
+ */
56
+ export declare const parseChangeCursor: (raw: string | undefined) => ChangeCursor;
57
+ /** Render a cursor. A clean boundary stays plain digits. */
58
+ export declare const formatChangeCursor: (cursor: ChangeCursor) => string;
59
+ /** True when the cursor is far enough along to fetch incrementally at all. */
60
+ export declare const hasChangeCursor: (cursor: ChangeCursor) => boolean;
61
+ /**
62
+ * Drop the part of a part-applied group that a previous round already applied.
63
+ *
64
+ * The fetch asks from below the in-progress group, so a resumed round is served
65
+ * that group again in full. Its applied members are recognised by position
66
+ * alone — the fetch row carries both its mod-sequence and its UID — so skipping
67
+ * them costs no lookup. Rows of any other mod-sequence are never skipped, which
68
+ * is what makes a vanished remainder harmless instead of lossy.
69
+ */
70
+ export declare const dropAppliedPrefix: (ordered: ImapMessage[], cursor: ChangeCursor) => ImapMessage[];
71
+ export interface ChangeCursorAdvanceInput {
72
+ /** The cursor this round started from. */
73
+ cursor: ChangeCursor;
74
+ /** HIGHESTMODSEQ the server reported before this round fetched anything. */
75
+ serverModseq: bigint;
76
+ /** The changed set still to apply, in {@link orderByModseq} order. */
77
+ ordered: ImapMessage[];
78
+ /** The prefix of `ordered` this round processed. */
79
+ batch: ImapMessage[];
80
+ /** UIDs whose save threw and must be re-applied. */
81
+ failedUids: ReadonlySet<number>;
82
+ }
83
+ export interface ChangeCursorAdvance {
84
+ cursor: ChangeCursor;
85
+ /** True when changes remain that this round did not process. */
86
+ hasMore: boolean;
87
+ }
88
+ /**
89
+ * Decide how far the cursor may move after a round.
90
+ *
91
+ * It moves over the leading run of applied messages and stops at the first one
92
+ * that was not — a failure, or simply the end of the batch. Everything after
93
+ * that point is treated as unapplied even if it succeeded, which costs an
94
+ * idempotent re-apply next round and buys a cursor that can never claim more
95
+ * than it did.
96
+ *
97
+ * Where it stops decides the shape: on a group boundary the cursor is that
98
+ * group's mod-sequence with no sub-position; inside a group it keeps the last
99
+ * complete mod-sequence and records how far into the next one it got.
100
+ *
101
+ * A round that consumed everything without a failure jumps to the server's
102
+ * HIGHESTMODSEQ rather than the last message's mod-sequence. Those are not the
103
+ * same number: a mod-sequence also moves for events that return no message (an
104
+ * expunge, a change to a message the fetch filtered out), and stopping short
105
+ * would re-deliver them on every subsequent round. That value is read before
106
+ * the fetch, so anything changing while the round runs lands above it and
107
+ * arrives next time.
108
+ */
109
+ export declare const advanceChangeCursor: ({ cursor, serverModseq, ordered, batch, failedUids, }: ChangeCursorAdvanceInput) => ChangeCursorAdvance;
110
+ export interface UidWatermarkInput {
111
+ /** UIDs this round fetched. */
112
+ batchUids: number[];
113
+ /** UIDs whose save threw. */
114
+ failedUids: ReadonlySet<number>;
115
+ /** Lowest UID synced so far — the backfill floor. 0 before any sync. */
116
+ lastSyncUid: number;
117
+ /** Highest UID synced so far — the forward watermark. */
118
+ highWaterMarkUid: number;
119
+ }
120
+ export interface UidWatermarks {
121
+ lastSyncUid: number;
122
+ highWaterMarkUid: number;
123
+ }
124
+ /**
125
+ * Decide how far each UID watermark may move after a round.
126
+ *
127
+ * The two watermarks bound disjoint regions — new mail above
128
+ * `highWaterMarkUid`, backfill below `lastSyncUid` — so a failure constrains
129
+ * only the watermark whose region it sits in.
130
+ *
131
+ * The forward watermark stops below the lowest failure above it. Raising it
132
+ * over a failed UID would drop that UID out of the selection set entirely,
133
+ * and the following round, finding nothing left to enumerate, would seed the
134
+ * mod-sequence watermark over a message that was never stored. The backfill
135
+ * floor stays above the highest failure below it, for the same reason in the
136
+ * other direction.
137
+ */
138
+ export declare const advanceUidWatermarks: ({ batchUids, failedUids, lastSyncUid, highWaterMarkUid, }: UidWatermarkInput) => UidWatermarks;
@@ -0,0 +1,205 @@
1
+ /**
2
+ * Cursor arithmetic for message sync.
3
+ *
4
+ * One rule governs every function here: a cursor may never move past work
5
+ * that has not been durably applied. A watermark that steps over a message —
6
+ * because a batch was cut in the wrong place, or because a failure sat below
7
+ * a success — drops that message for good, since every selection this service
8
+ * makes is a comparison against the watermark and nothing else.
9
+ *
10
+ * The rule applies on two axes: mod-sequence (the CONDSTORE path) and UID
11
+ * (the enumeration path).
12
+ */
13
+ /**
14
+ * A mod-sequence is an unsigned 64-bit value, so it is stored as decimal
15
+ * digits and only ever compared as a BigInt. `undefined` covers rows written
16
+ * before the field existed, and "0" is the server's own way of saying it has
17
+ * no mod-sequence to report.
18
+ *
19
+ * Anything that is not plain digits reads as no mod-sequence rather than
20
+ * throwing. A cursor is read at the very top of a sync round, so a value this
21
+ * cannot parse would take the mailbox out of sync permanently, and leave no
22
+ * path to repair it — every route to the field runs through here. Degrading to
23
+ * 0 sends the mailbox back to enumeration, which reseeds it.
24
+ */
25
+ const DECIMAL_DIGITS = /^\d+$/;
26
+ export const parseModseq = (raw) => {
27
+ if (!raw || !DECIMAL_DIGITS.test(raw))
28
+ return 0n;
29
+ const value = BigInt(raw);
30
+ return value > 0n ? value : 0n;
31
+ };
32
+ /**
33
+ * Order a CHANGEDSINCE result by mod-sequence, oldest change first, UID
34
+ * breaking ties.
35
+ *
36
+ * Mod-sequence order is what makes a partial round resumable: a batch taken
37
+ * off the front can advance the watermark to the last change it applied and
38
+ * the next round resumes there.
39
+ */
40
+ export const orderByModseq = (messages) => [...messages].sort((a, b) => {
41
+ const left = parseModseq(a.modseq);
42
+ const right = parseModseq(b.modseq);
43
+ if (left === right)
44
+ return a.uid - b.uid;
45
+ return left < right ? -1 : 1;
46
+ });
47
+ /**
48
+ * Read a stored cursor.
49
+ *
50
+ * Two forms: `"<modseq>"` is a clean boundary — every value written before the
51
+ * sub-position existed, which is what they always meant. `"<group>:<uid>"` is a
52
+ * position inside a group; the fetch point is implied, because a round only
53
+ * ever stops inside a group once everything below it has been applied.
54
+ *
55
+ * Storing the implied fetch point instead of a third number is what keeps the
56
+ * value inside the field's 32 characters: two 64-bit mod-sequences and a UID
57
+ * would not fit.
58
+ */
59
+ export const parseChangeCursor = (raw) => {
60
+ const [left, right] = (raw ?? "").split(":");
61
+ if (right === undefined) {
62
+ return { modseq: parseModseq(left), group: 0n, uid: 0 };
63
+ }
64
+ const group = parseModseq(left);
65
+ const uid = DECIMAL_DIGITS.test(right) ? Number.parseInt(right, 10) : 0;
66
+ if (group === 0n || uid === 0) {
67
+ return { modseq: group, group: 0n, uid: 0 };
68
+ }
69
+ return { modseq: group - 1n, group, uid };
70
+ };
71
+ /** Render a cursor. A clean boundary stays plain digits. */
72
+ export const formatChangeCursor = (cursor) => cursor.group > 0n && cursor.uid > 0
73
+ ? `${cursor.group.toString()}:${cursor.uid}`
74
+ : cursor.modseq.toString();
75
+ /** True when the cursor is far enough along to fetch incrementally at all. */
76
+ export const hasChangeCursor = (cursor) => cursor.modseq > 0n || cursor.group > 0n;
77
+ /**
78
+ * Drop the part of a part-applied group that a previous round already applied.
79
+ *
80
+ * The fetch asks from below the in-progress group, so a resumed round is served
81
+ * that group again in full. Its applied members are recognised by position
82
+ * alone — the fetch row carries both its mod-sequence and its UID — so skipping
83
+ * them costs no lookup. Rows of any other mod-sequence are never skipped, which
84
+ * is what makes a vanished remainder harmless instead of lossy.
85
+ */
86
+ export const dropAppliedPrefix = (ordered, cursor) => {
87
+ if (cursor.group === 0n || cursor.uid === 0)
88
+ return ordered;
89
+ return ordered.filter((message) => parseModseq(message.modseq) !== cursor.group || message.uid > cursor.uid);
90
+ };
91
+ /**
92
+ * Decide how far the cursor may move after a round.
93
+ *
94
+ * It moves over the leading run of applied messages and stops at the first one
95
+ * that was not — a failure, or simply the end of the batch. Everything after
96
+ * that point is treated as unapplied even if it succeeded, which costs an
97
+ * idempotent re-apply next round and buys a cursor that can never claim more
98
+ * than it did.
99
+ *
100
+ * Where it stops decides the shape: on a group boundary the cursor is that
101
+ * group's mod-sequence with no sub-position; inside a group it keeps the last
102
+ * complete mod-sequence and records how far into the next one it got.
103
+ *
104
+ * A round that consumed everything without a failure jumps to the server's
105
+ * HIGHESTMODSEQ rather than the last message's mod-sequence. Those are not the
106
+ * same number: a mod-sequence also moves for events that return no message (an
107
+ * expunge, a change to a message the fetch filtered out), and stopping short
108
+ * would re-deliver them on every subsequent round. That value is read before
109
+ * the fetch, so anything changing while the round runs lands above it and
110
+ * arrives next time.
111
+ */
112
+ export const advanceChangeCursor = ({ cursor, serverModseq, ordered, batch, failedUids, }) => {
113
+ const applied = [];
114
+ for (const message of batch) {
115
+ if (failedUids.has(message.uid))
116
+ break;
117
+ applied.push(message);
118
+ }
119
+ const hasMore = ordered.length > batch.length;
120
+ const next = ordered[applied.length];
121
+ if (applied.length === 0) {
122
+ return { cursor, hasMore };
123
+ }
124
+ const last = applied[applied.length - 1];
125
+ const lastModseq = parseModseq(last.modseq);
126
+ if (next === undefined) {
127
+ return {
128
+ cursor: {
129
+ modseq: serverModseq > lastModseq ? serverModseq : lastModseq,
130
+ group: 0n,
131
+ uid: 0,
132
+ },
133
+ hasMore,
134
+ };
135
+ }
136
+ const nextModseq = parseModseq(next.modseq);
137
+ if (nextModseq > lastModseq) {
138
+ return { cursor: { modseq: lastModseq, group: 0n, uid: 0 }, hasMore };
139
+ }
140
+ // Stopped inside a group. Everything below it was applied — the round works
141
+ // through `ordered` in order — so the fetch point is the value just below
142
+ // the group, and the group itself is recorded so the resumed round skips by
143
+ // identity rather than by guessing which group it is looking at.
144
+ //
145
+ // "Everything below it was applied" holds because `ordered` is the COMPLETE
146
+ // set of unapplied changes: the fetch is CHANGEDSINCE over `1:*`. Narrowing
147
+ // that fetch by UID range would leave changes below the group unapplied and
148
+ // unrepresented here, and this cursor would then step over them.
149
+ //
150
+ // The floor matters when a server returns a message with no MODSEQ at all:
151
+ // `nextModseq` is then 0 and the cursor must not go negative.
152
+ const fetchFrom = nextModseq > 0n ? nextModseq - 1n : 0n;
153
+ return {
154
+ cursor: { modseq: fetchFrom, group: nextModseq, uid: last.uid },
155
+ hasMore,
156
+ };
157
+ };
158
+ /**
159
+ * Decide how far each UID watermark may move after a round.
160
+ *
161
+ * The two watermarks bound disjoint regions — new mail above
162
+ * `highWaterMarkUid`, backfill below `lastSyncUid` — so a failure constrains
163
+ * only the watermark whose region it sits in.
164
+ *
165
+ * The forward watermark stops below the lowest failure above it. Raising it
166
+ * over a failed UID would drop that UID out of the selection set entirely,
167
+ * and the following round, finding nothing left to enumerate, would seed the
168
+ * mod-sequence watermark over a message that was never stored. The backfill
169
+ * floor stays above the highest failure below it, for the same reason in the
170
+ * other direction.
171
+ */
172
+ export const advanceUidWatermarks = ({ batchUids, failedUids, lastSyncUid, highWaterMarkUid, }) => {
173
+ const isFreshSync = lastSyncUid === 0;
174
+ const forwardUids = batchUids.filter((uid) => uid > highWaterMarkUid);
175
+ const forwardFailures = forwardUids.filter((uid) => failedUids.has(uid));
176
+ const forwardLimit = forwardFailures.length
177
+ ? Math.min(...forwardFailures)
178
+ : Number.POSITIVE_INFINITY;
179
+ const forwardApplied = forwardUids.filter((uid) => !failedUids.has(uid) && uid < forwardLimit);
180
+ const nextHighWaterMark = Math.max(highWaterMarkUid, ...forwardApplied);
181
+ const backfillUids = batchUids.filter((uid) => isFreshSync || uid < lastSyncUid);
182
+ const backfillFailures = backfillUids.filter((uid) => failedUids.has(uid));
183
+ // The floor must stay strictly above a failed UID for it to stay selectable.
184
+ const backfillFloor = backfillFailures.length
185
+ ? Math.max(...backfillFailures) + 1
186
+ : 0;
187
+ const backfillApplied = backfillUids.filter((uid) => !failedUids.has(uid) && uid >= backfillFloor);
188
+ // The floor drops to the lowest UID applied above the failure line, or to
189
+ // the line itself when everything below it is being retried. With no
190
+ // failures and nothing applied there is nothing to move it.
191
+ let candidate = lastSyncUid;
192
+ if (backfillApplied.length > 0) {
193
+ candidate = Math.min(...backfillApplied);
194
+ }
195
+ else if (backfillFailures.length > 0) {
196
+ candidate = backfillFloor;
197
+ }
198
+ const nextLastSyncUid = isFreshSync
199
+ ? candidate
200
+ : Math.min(lastSyncUid, candidate);
201
+ return {
202
+ lastSyncUid: nextLastSyncUid,
203
+ highWaterMarkUid: nextHighWaterMark,
204
+ };
205
+ };
@@ -0,0 +1,151 @@
1
+ import { Readable } from "node:stream";
2
+ import type { FlatMailboxInfo, IImapConnection, ImapBodyStructure, ImapBoxStatus, ImapConnectionState, ImapEnvelopeSnapshot, ImapMailboxStatus, ImapMessage, ImapNamespaces } from "../types.js";
3
+ interface StoredMessage {
4
+ raw: Buffer;
5
+ flags: Set<string>;
6
+ modseq: bigint;
7
+ internalDate: Date;
8
+ /**
9
+ * What BODYSTRUCTURE answers; read off the top-level headers when unset,
10
+ * and left out of the FETCH row when `null`.
11
+ */
12
+ bodyStructure?: ImapBodyStructure | null;
13
+ }
14
+ interface Folder {
15
+ uidValidity: number;
16
+ uidNext: number;
17
+ attributes: string[];
18
+ subscribed: boolean;
19
+ messages: Map<number, StoredMessage>;
20
+ }
21
+ export type FakeImapVerb = "NAMESPACE" | "LIST" | "STATUS" | "SELECT" | "EXAMINE" | "CLOSE" | "SEARCH" | "FETCH" | "BODY" | "STORE" | "MOVE" | "COPY" | "EXPUNGE" | "CREATE" | "RENAME" | "DELETE" | "SUBSCRIBE" | "UNSUBSCRIBE" | "APPEND";
22
+ /** An error shaped the way ImapFlow reports a tagged NO from the server. */
23
+ export declare const imapError: (code: string, text: string) => Error;
24
+ export declare const eml: (headers: Record<string, string>, body?: string) => Buffer;
25
+ /**
26
+ * An IMAP server held in memory, behind the same connection port the imapflow
27
+ * adapter implements. It refuses what a server refuses: a folder that does not
28
+ * exist is `NONEXISTENT`, a transfer or APPEND to one is `TRYCREATE`, and a
29
+ * SEARCH it does not model throws rather than matching everything. FETCH rows
30
+ * are derived from the raw bytes the way the adapter derives them. `log`
31
+ * records every command that would reach the wire.
32
+ */
33
+ export declare class FakeImapServer {
34
+ private readonly folders;
35
+ private modseq;
36
+ private nextUidValidity;
37
+ readonly log: string[];
38
+ readonly failNext: Partial<Record<FakeImapVerb, Error>>;
39
+ readonly ignoreNext: Set<FakeImapVerb>;
40
+ condstore: boolean;
41
+ uidplus: boolean;
42
+ namespacePrefix: string;
43
+ connections: number;
44
+ onFetchBody: (uid: number) => Promise<void>;
45
+ addFolder(path: string, options?: {
46
+ attributes?: string[];
47
+ uidValidity?: number;
48
+ }): void;
49
+ hasFolder(path: string): boolean;
50
+ /** Another client deleted the folder. */
51
+ removeFolder(path: string): void;
52
+ isSubscribed(path: string): boolean;
53
+ folder(path: string): Folder;
54
+ paths(): string[];
55
+ deliver(path: string, raw: Buffer, options?: {
56
+ flags?: string[];
57
+ uid?: number;
58
+ bodyStructure?: ImapBodyStructure | null;
59
+ }): number;
60
+ /** Another client expunged the message. */
61
+ expunge(path: string, uid: number): void;
62
+ /** Another client expunged everything in the folder. */
63
+ empty(path: string): void;
64
+ /** One STORE from another client: every message it touches shares the new mod-sequence. */
65
+ store(path: string, uids: number[], flags: string[]): void;
66
+ /** The server renumbered the folder: a new UIDVALIDITY, UIDs from 1. */
67
+ renumber(path: string, uidValidity: number): void;
68
+ flagsOf(path: string, uid: number): string[];
69
+ uids(path: string, messageIdHeader?: string): number[];
70
+ /** What the server holds in a folder, as another client would read it. */
71
+ messages(path: string): Array<{
72
+ uid: number;
73
+ messageIdHeader: string;
74
+ flags: string[];
75
+ raw: Buffer;
76
+ }>;
77
+ /** The server changed UIDVALIDITY without renumbering what it serves. */
78
+ setUidValidity(path: string, uidValidity: number): void;
79
+ uidValidityOf(path: string): number;
80
+ highestModseq(): bigint;
81
+ bumpModseq(): bigint;
82
+ commands(verb: FakeImapVerb): string[];
83
+ run(verb: FakeImapVerb, detail: string): void;
84
+ createFolder(path: string): boolean;
85
+ renameFolder(oldPath: string, newPath: string): void;
86
+ deleteFolder(path: string): void;
87
+ connection(): FakeImapConnection;
88
+ }
89
+ export declare class FakeImapConnection implements IImapConnection {
90
+ private readonly server;
91
+ readonly state: ImapConnectionState;
92
+ readonly isConnected = true;
93
+ private selected;
94
+ constructor(server: FakeImapServer);
95
+ private open;
96
+ private uidList;
97
+ connect: () => Promise<void>;
98
+ disconnect: () => Promise<void>;
99
+ getNamespaces: () => Promise<ImapNamespaces>;
100
+ listMailboxes: () => Promise<FlatMailboxInfo[]>;
101
+ openBox: (path: string, readOnly?: boolean) => Promise<ImapBoxStatus>;
102
+ closeBox: () => Promise<void>;
103
+ search: (criteria: unknown[]) => Promise<number[]>;
104
+ private toImapMessage;
105
+ private fetchRows;
106
+ fetchMessages: (uids: number[]) => Promise<ImapMessage[]>;
107
+ supportsCondstore: () => boolean;
108
+ fetchMessagesChangedSince: (sinceModseq: bigint) => Promise<ImapMessage[]>;
109
+ fetchEnvelopeSnapshots: (uids: number[]) => Promise<ImapEnvelopeSnapshot[]>;
110
+ fetchMessageBody: (uid: number) => Promise<Buffer | null>;
111
+ fetchMessageBodies(uids: number[]): AsyncGenerator<{
112
+ uid: number;
113
+ source: Readable;
114
+ }>;
115
+ private touch;
116
+ addFlags: (uids: number[], flags: string[]) => Promise<void>;
117
+ removeFlags: (uids: number[], flags: string[]) => Promise<void>;
118
+ setFlags: (uids: number[], flags: string[]) => Promise<void>;
119
+ createMailbox: (path: string) => Promise<{
120
+ path: string;
121
+ created: boolean;
122
+ }>;
123
+ deleteMailbox: (path: string) => Promise<{
124
+ path: string;
125
+ }>;
126
+ renameMailbox: (oldPath: string, requested: string) => Promise<{
127
+ path: string;
128
+ newPath: string;
129
+ }>;
130
+ subscribeMailbox: (path: string) => Promise<void>;
131
+ unsubscribeMailbox: (path: string) => Promise<void>;
132
+ private transfer;
133
+ moveMessages: (uids: number[], destination: string) => Promise<{
134
+ destination: string;
135
+ uidValidity: number;
136
+ uidMap: Map<number, number>;
137
+ }>;
138
+ copyMessages: (uids: number[], destination: string) => Promise<{
139
+ destination: string;
140
+ uidValidity: number;
141
+ uidMap: Map<number, number>;
142
+ }>;
143
+ deleteMessages: (uids: number[]) => Promise<number>;
144
+ getMailboxStatus: (path: string) => Promise<ImapMailboxStatus>;
145
+ append: (mailbox: string, message: string | Buffer, flags?: string[]) => Promise<{
146
+ destination: string;
147
+ uidValidity: number;
148
+ uid: number;
149
+ }>;
150
+ }
151
+ export {};