@remit/mailbox-service 0.0.112 → 0.0.114

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 +129 -0
  78. package/dist/mailbox-queue.js +216 -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 +280 -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
@@ -1,319 +0,0 @@
1
- import type { ImapMessage } from "./types.js";
2
-
3
- /**
4
- * Cursor arithmetic for message sync.
5
- *
6
- * One rule governs every function here: a cursor may never move past work
7
- * that has not been durably applied. A watermark that steps over a message —
8
- * because a batch was cut in the wrong place, or because a failure sat below
9
- * a success — drops that message for good, since every selection this service
10
- * makes is a comparison against the watermark and nothing else.
11
- *
12
- * The rule applies on two axes: mod-sequence (the CONDSTORE path) and UID
13
- * (the enumeration path).
14
- */
15
-
16
- /**
17
- * A mod-sequence is an unsigned 64-bit value, so it is stored as decimal
18
- * digits and only ever compared as a BigInt. `undefined` covers rows written
19
- * before the field existed, and "0" is the server's own way of saying it has
20
- * no mod-sequence to report.
21
- *
22
- * Anything that is not plain digits reads as no mod-sequence rather than
23
- * throwing. A cursor is read at the very top of a sync round, so a value this
24
- * cannot parse would take the mailbox out of sync permanently, and leave no
25
- * path to repair it — every route to the field runs through here. Degrading to
26
- * 0 sends the mailbox back to enumeration, which reseeds it.
27
- */
28
- const DECIMAL_DIGITS = /^\d+$/;
29
-
30
- export const parseModseq = (raw: string | undefined): bigint => {
31
- if (!raw || !DECIMAL_DIGITS.test(raw)) return 0n;
32
- const value = BigInt(raw);
33
- return value > 0n ? value : 0n;
34
- };
35
-
36
- /**
37
- * Order a CHANGEDSINCE result by mod-sequence, oldest change first, UID
38
- * breaking ties.
39
- *
40
- * Mod-sequence order is what makes a partial round resumable: a batch taken
41
- * off the front can advance the watermark to the last change it applied and
42
- * the next round resumes there.
43
- */
44
- export const orderByModseq = (messages: ImapMessage[]): ImapMessage[] =>
45
- [...messages].sort((a, b) => {
46
- const left = parseModseq(a.modseq);
47
- const right = parseModseq(b.modseq);
48
- if (left === right) return a.uid - b.uid;
49
- return left < right ? -1 : 1;
50
- });
51
-
52
- /**
53
- * The message-sync cursor: how far CHANGEDSINCE has been applied.
54
- *
55
- * `modseq` is what the next fetch asks from. `group` and `uid` describe a
56
- * mod-sequence a previous round left part-applied: which one, and how far into
57
- * it the round got. `group` is 0 when the cursor sits on a clean boundary.
58
- *
59
- * The sub-position exists because one STORE assigns the SAME mod-sequence to
60
- * every message it touched (RFC 7162 permits it and servers do it), so "select
61
- * all, mark read" arrives as one group of arbitrary size. CHANGEDSINCE is
62
- * strictly greater-than, so a cursor that advanced to a partly-applied group
63
- * would never see its tail again; a cursor that refused to split it would have
64
- * to apply the whole group in one round, which for a large mailbox does not
65
- * finish, and every retry would repeat the same unfinished work. The
66
- * sub-position is what lets one group span rounds without ever claiming more
67
- * than was applied.
68
- *
69
- * The group is recorded rather than inferred from the fetch result. The
70
- * in-progress group is NOT reliably the lowest mod-sequence returned: its
71
- * remaining members can disappear between rounds, by being expunged (which
72
- * CONDSTORE simply stops reporting) or by being modified again onto a higher
73
- * mod-sequence. Inferring it then points the skip at an unrelated group and
74
- * silently drops that group's leading messages.
75
- */
76
- export interface ChangeCursor {
77
- /** Fetch from here — everything above it is still owed. */
78
- modseq: bigint;
79
- /** The mod-sequence left part-applied, or 0 on a clean boundary. */
80
- group: bigint;
81
- /** Highest UID applied within `group`. */
82
- uid: number;
83
- }
84
-
85
- /**
86
- * Read a stored cursor.
87
- *
88
- * Two forms: `"<modseq>"` is a clean boundary — every value written before the
89
- * sub-position existed, which is what they always meant. `"<group>:<uid>"` is a
90
- * position inside a group; the fetch point is implied, because a round only
91
- * ever stops inside a group once everything below it has been applied.
92
- *
93
- * Storing the implied fetch point instead of a third number is what keeps the
94
- * value inside the field's 32 characters: two 64-bit mod-sequences and a UID
95
- * would not fit.
96
- */
97
- export const parseChangeCursor = (raw: string | undefined): ChangeCursor => {
98
- const [left, right] = (raw ?? "").split(":");
99
-
100
- if (right === undefined) {
101
- return { modseq: parseModseq(left), group: 0n, uid: 0 };
102
- }
103
-
104
- const group = parseModseq(left);
105
- const uid = DECIMAL_DIGITS.test(right) ? Number.parseInt(right, 10) : 0;
106
- if (group === 0n || uid === 0) {
107
- return { modseq: group, group: 0n, uid: 0 };
108
- }
109
-
110
- return { modseq: group - 1n, group, uid };
111
- };
112
-
113
- /** Render a cursor. A clean boundary stays plain digits. */
114
- export const formatChangeCursor = (cursor: ChangeCursor): string =>
115
- cursor.group > 0n && cursor.uid > 0
116
- ? `${cursor.group.toString()}:${cursor.uid}`
117
- : cursor.modseq.toString();
118
-
119
- /** True when the cursor is far enough along to fetch incrementally at all. */
120
- export const hasChangeCursor = (cursor: ChangeCursor): boolean =>
121
- cursor.modseq > 0n || cursor.group > 0n;
122
-
123
- /**
124
- * Drop the part of a part-applied group that a previous round already applied.
125
- *
126
- * The fetch asks from below the in-progress group, so a resumed round is served
127
- * that group again in full. Its applied members are recognised by position
128
- * alone — the fetch row carries both its mod-sequence and its UID — so skipping
129
- * them costs no lookup. Rows of any other mod-sequence are never skipped, which
130
- * is what makes a vanished remainder harmless instead of lossy.
131
- */
132
- export const dropAppliedPrefix = (
133
- ordered: ImapMessage[],
134
- cursor: ChangeCursor,
135
- ): ImapMessage[] => {
136
- if (cursor.group === 0n || cursor.uid === 0) return ordered;
137
- return ordered.filter(
138
- (message) =>
139
- parseModseq(message.modseq) !== cursor.group || message.uid > cursor.uid,
140
- );
141
- };
142
-
143
- export interface ChangeCursorAdvanceInput {
144
- /** The cursor this round started from. */
145
- cursor: ChangeCursor;
146
- /** HIGHESTMODSEQ the server reported before this round fetched anything. */
147
- serverModseq: bigint;
148
- /** The changed set still to apply, in {@link orderByModseq} order. */
149
- ordered: ImapMessage[];
150
- /** The prefix of `ordered` this round processed. */
151
- batch: ImapMessage[];
152
- /** UIDs whose save threw and must be re-applied. */
153
- failedUids: ReadonlySet<number>;
154
- }
155
-
156
- export interface ChangeCursorAdvance {
157
- cursor: ChangeCursor;
158
- /** True when changes remain that this round did not process. */
159
- hasMore: boolean;
160
- }
161
-
162
- /**
163
- * Decide how far the cursor may move after a round.
164
- *
165
- * It moves over the leading run of applied messages and stops at the first one
166
- * that was not — a failure, or simply the end of the batch. Everything after
167
- * that point is treated as unapplied even if it succeeded, which costs an
168
- * idempotent re-apply next round and buys a cursor that can never claim more
169
- * than it did.
170
- *
171
- * Where it stops decides the shape: on a group boundary the cursor is that
172
- * group's mod-sequence with no sub-position; inside a group it keeps the last
173
- * complete mod-sequence and records how far into the next one it got.
174
- *
175
- * A round that consumed everything without a failure jumps to the server's
176
- * HIGHESTMODSEQ rather than the last message's mod-sequence. Those are not the
177
- * same number: a mod-sequence also moves for events that return no message (an
178
- * expunge, a change to a message the fetch filtered out), and stopping short
179
- * would re-deliver them on every subsequent round. That value is read before
180
- * the fetch, so anything changing while the round runs lands above it and
181
- * arrives next time.
182
- */
183
- export const advanceChangeCursor = ({
184
- cursor,
185
- serverModseq,
186
- ordered,
187
- batch,
188
- failedUids,
189
- }: ChangeCursorAdvanceInput): ChangeCursorAdvance => {
190
- const applied: ImapMessage[] = [];
191
- for (const message of batch) {
192
- if (failedUids.has(message.uid)) break;
193
- applied.push(message);
194
- }
195
-
196
- const hasMore = ordered.length > batch.length;
197
- const next = ordered[applied.length];
198
-
199
- if (applied.length === 0) {
200
- return { cursor, hasMore };
201
- }
202
-
203
- const last = applied[applied.length - 1];
204
- const lastModseq = parseModseq(last.modseq);
205
-
206
- if (next === undefined) {
207
- return {
208
- cursor: {
209
- modseq: serverModseq > lastModseq ? serverModseq : lastModseq,
210
- group: 0n,
211
- uid: 0,
212
- },
213
- hasMore,
214
- };
215
- }
216
-
217
- const nextModseq = parseModseq(next.modseq);
218
- if (nextModseq > lastModseq) {
219
- return { cursor: { modseq: lastModseq, group: 0n, uid: 0 }, hasMore };
220
- }
221
-
222
- // Stopped inside a group. Everything below it was applied — the round works
223
- // through `ordered` in order — so the fetch point is the value just below
224
- // the group, and the group itself is recorded so the resumed round skips by
225
- // identity rather than by guessing which group it is looking at.
226
- //
227
- // "Everything below it was applied" holds because `ordered` is the COMPLETE
228
- // set of unapplied changes: the fetch is CHANGEDSINCE over `1:*`. Narrowing
229
- // that fetch by UID range would leave changes below the group unapplied and
230
- // unrepresented here, and this cursor would then step over them.
231
- //
232
- // The floor matters when a server returns a message with no MODSEQ at all:
233
- // `nextModseq` is then 0 and the cursor must not go negative.
234
- const fetchFrom = nextModseq > 0n ? nextModseq - 1n : 0n;
235
- return {
236
- cursor: { modseq: fetchFrom, group: nextModseq, uid: last.uid },
237
- hasMore,
238
- };
239
- };
240
-
241
- export interface UidWatermarkInput {
242
- /** UIDs this round fetched. */
243
- batchUids: number[];
244
- /** UIDs whose save threw. */
245
- failedUids: ReadonlySet<number>;
246
- /** Lowest UID synced so far — the backfill floor. 0 before any sync. */
247
- lastSyncUid: number;
248
- /** Highest UID synced so far — the forward watermark. */
249
- highWaterMarkUid: number;
250
- }
251
-
252
- export interface UidWatermarks {
253
- lastSyncUid: number;
254
- highWaterMarkUid: number;
255
- }
256
-
257
- /**
258
- * Decide how far each UID watermark may move after a round.
259
- *
260
- * The two watermarks bound disjoint regions — new mail above
261
- * `highWaterMarkUid`, backfill below `lastSyncUid` — so a failure constrains
262
- * only the watermark whose region it sits in.
263
- *
264
- * The forward watermark stops below the lowest failure above it. Raising it
265
- * over a failed UID would drop that UID out of the selection set entirely,
266
- * and the following round, finding nothing left to enumerate, would seed the
267
- * mod-sequence watermark over a message that was never stored. The backfill
268
- * floor stays above the highest failure below it, for the same reason in the
269
- * other direction.
270
- */
271
- export const advanceUidWatermarks = ({
272
- batchUids,
273
- failedUids,
274
- lastSyncUid,
275
- highWaterMarkUid,
276
- }: UidWatermarkInput): UidWatermarks => {
277
- const isFreshSync = lastSyncUid === 0;
278
-
279
- const forwardUids = batchUids.filter((uid) => uid > highWaterMarkUid);
280
- const forwardFailures = forwardUids.filter((uid) => failedUids.has(uid));
281
- const forwardLimit = forwardFailures.length
282
- ? Math.min(...forwardFailures)
283
- : Number.POSITIVE_INFINITY;
284
- const forwardApplied = forwardUids.filter(
285
- (uid) => !failedUids.has(uid) && uid < forwardLimit,
286
- );
287
- const nextHighWaterMark = Math.max(highWaterMarkUid, ...forwardApplied);
288
-
289
- const backfillUids = batchUids.filter(
290
- (uid) => isFreshSync || uid < lastSyncUid,
291
- );
292
- const backfillFailures = backfillUids.filter((uid) => failedUids.has(uid));
293
- // The floor must stay strictly above a failed UID for it to stay selectable.
294
- const backfillFloor = backfillFailures.length
295
- ? Math.max(...backfillFailures) + 1
296
- : 0;
297
- const backfillApplied = backfillUids.filter(
298
- (uid) => !failedUids.has(uid) && uid >= backfillFloor,
299
- );
300
-
301
- // The floor drops to the lowest UID applied above the failure line, or to
302
- // the line itself when everything below it is being retried. With no
303
- // failures and nothing applied there is nothing to move it.
304
- let candidate = lastSyncUid;
305
- if (backfillApplied.length > 0) {
306
- candidate = Math.min(...backfillApplied);
307
- } else if (backfillFailures.length > 0) {
308
- candidate = backfillFloor;
309
- }
310
-
311
- const nextLastSyncUid = isFreshSync
312
- ? candidate
313
- : Math.min(lastSyncUid, candidate);
314
-
315
- return {
316
- lastSyncUid: nextLastSyncUid,
317
- highWaterMarkUid: nextHighWaterMark,
318
- };
319
- };
@@ -1,77 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { describe, it } from "node:test";
3
- import {
4
- MailboxAttribute,
5
- MessageKeywordFlag,
6
- MessageSystemFlag,
7
- } from "@remit/domain-enums";
8
-
9
- /**
10
- * RFC 9051 §2.3.2 system flags and §7.3.1 mailbox attributes are
11
- * backslash-prefixed on the wire, and these enum values reach an IMAP server
12
- * verbatim: `handleFlagPush` passes a marker's `flagName` straight to
13
- * `ImapFlowConnection.addFlags`/`removeFlags`. A value that has lost its
14
- * backslash is still a legal STORE argument — the server takes it as a custom
15
- * keyword — so the push silently sets the wrong thing rather than failing.
16
- *
17
- * These enums are generated, and the emitter writes their values into
18
- * JavaScript source where an unescaped `\S` collapses to `S` (issue #64).
19
- * Pin the exact bytes so a regression fails here instead of on a real
20
- * mailbox.
21
- */
22
- const SYSTEM_FLAG_WIRE_FORMAT = {
23
- Seen: "\\Seen",
24
- Answered: "\\Answered",
25
- Flagged: "\\Flagged",
26
- Deleted: "\\Deleted",
27
- Draft: "\\Draft",
28
- } as const;
29
-
30
- const MAILBOX_ATTRIBUTE_WIRE_FORMAT = {
31
- NonExistent: "\\NonExistent",
32
- NoInferiors: "\\Noinferiors",
33
- NoSelect: "\\Noselect",
34
- HasChildren: "\\HasChildren",
35
- HasNoChildren: "\\HasNoChildren",
36
- Marked: "\\Marked",
37
- Unmarked: "\\Unmarked",
38
- Subscribed: "\\Subscribed",
39
- Remote: "\\Remote",
40
- } as const;
41
-
42
- describe("IMAP system flag wire format (issue #64)", () => {
43
- it("carries the RFC 9051 backslash prefix on every member", () => {
44
- assert.deepEqual({ ...MessageSystemFlag }, { ...SYSTEM_FLAG_WIRE_FORMAT });
45
- });
46
-
47
- for (const [name, wireValue] of Object.entries(SYSTEM_FLAG_WIRE_FORMAT)) {
48
- it(`${name} is one backslash followed by the flag name`, () => {
49
- assert.equal(wireValue.charCodeAt(0), 0x5c);
50
- assert.equal(wireValue.slice(1), name);
51
- assert.equal(wireValue.length, name.length + 1);
52
- });
53
- }
54
-
55
- it("leaves keyword flags unprefixed", () => {
56
- for (const wireValue of Object.values(MessageKeywordFlag)) {
57
- assert.equal(wireValue.startsWith("\\"), false);
58
- assert.equal(wireValue.startsWith("$"), true);
59
- }
60
- });
61
- });
62
-
63
- describe("IMAP mailbox attribute wire format (issue #64)", () => {
64
- it("carries the RFC 9051 backslash prefix on every member", () => {
65
- assert.deepEqual(
66
- { ...MailboxAttribute },
67
- { ...MAILBOX_ATTRIBUTE_WIRE_FORMAT },
68
- );
69
- });
70
-
71
- it("prefixes every value with exactly one backslash", () => {
72
- for (const wireValue of Object.values(MailboxAttribute)) {
73
- assert.equal(wireValue.charCodeAt(0), 0x5c);
74
- assert.equal(wireValue.charCodeAt(1) === 0x5c, false);
75
- }
76
- });
77
- });