@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,213 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { afterEach, beforeEach, describe, it } from "node:test";
3
- import type {
4
- AccountItem,
5
- ISyncWarningRepository,
6
- MailboxItem,
7
- } from "@remit/data-ports";
8
- import { BodySyncService } from "./body-sync.js";
9
- import { MessageSyncService } from "./message-sync.js";
10
- import { SyncWarningService } from "./sync-warning.js";
11
- import {
12
- eml,
13
- type FakeImapConnection,
14
- FakeImapServer,
15
- } from "./test-helpers/fake-imap.js";
16
- import {
17
- messageAtUid,
18
- openStore,
19
- type Store,
20
- seedAccount,
21
- seedMailbox,
22
- silentLog,
23
- syncFolder,
24
- } from "./test-helpers/test-store.js";
25
-
26
- let store: Store;
27
- let imap: FakeImapServer;
28
- let connection: FakeImapConnection;
29
- let account: AccountItem;
30
- let inbox: MailboxItem;
31
-
32
- beforeEach(async () => {
33
- store = await openStore();
34
- imap = new FakeImapServer();
35
- account = await seedAccount(store);
36
- inbox = await seedMailbox(store, account, "INBOX");
37
- imap.addFolder("INBOX");
38
- connection = imap.connection();
39
- });
40
-
41
- afterEach(async () => {
42
- await store.close();
43
- });
44
-
45
- const syncHeaders = (
46
- syncWarnings: ISyncWarningRepository = store.syncWarning,
47
- ) =>
48
- new MessageSyncService(
49
- { getConnection: () => connection, close: async () => {} },
50
- store.mailbox,
51
- store.mailboxSpecialUse,
52
- store.message,
53
- store.envelope,
54
- store.address,
55
- store.threadMessage,
56
- new SyncWarningService(syncWarnings),
57
- silentLog,
58
- store.flagPush,
59
- store.messageFlag,
60
- ).syncMessages(inbox.mailboxId, account.accountId, account.accountConfigId);
61
-
62
- const syncBody = (messageId: string, source: FakeImapConnection) =>
63
- new BodySyncService(
64
- store.message,
65
- store.storage(),
66
- store.threadMessage,
67
- store.address,
68
- store.envelope,
69
- new SyncWarningService(store.syncWarning),
70
- silentLog,
71
- ).syncBodies(
72
- [messageId],
73
- account.accountId,
74
- account.accountConfigId,
75
- {
76
- mailboxId: inbox.mailboxId,
77
- fullPath: "INBOX",
78
- placement: "inbox",
79
- uidValidity: inbox.uidValidity,
80
- },
81
- async () => source,
82
- );
83
-
84
- const warnings = async () =>
85
- (await store.syncWarning.listByMailboxId(inbox.mailboxId)).map(
86
- ({ uid, stage, attempts }) => ({ uid, stage, attempts }),
87
- );
88
-
89
- const servedUidValidity = async (): Promise<number> =>
90
- (await imap.connection().openBox("INBOX")).uidvalidity;
91
-
92
- const plant = (uid: number, uidValidity: number) =>
93
- store.syncWarning.recordFailures([
94
- {
95
- accountConfigId: account.accountConfigId,
96
- accountId: account.accountId,
97
- mailboxId: inbox.mailboxId,
98
- uidValidity,
99
- uid,
100
- stage: "Header",
101
- },
102
- ]);
103
-
104
- const dropEnvelopes = () => {
105
- const fetch = connection.fetchMessages;
106
- connection.fetchMessages = async (uids) =>
107
- (await fetch(uids)).map((row) => ({ ...row, envelope: undefined }));
108
- return () => {
109
- connection.fetchMessages = fetch;
110
- };
111
- };
112
-
113
- describe("a message whose headers fail to sync", () => {
114
- it("keeps one warning that counts every failed round", async () => {
115
- const uid = imap.deliver("INBOX", eml({ "Message-ID": "<a@example.com>" }));
116
- dropEnvelopes();
117
-
118
- for (let round = 0; round < 3; round++) await syncHeaders();
119
-
120
- assert.deepEqual(await warnings(), [{ uid, stage: "Header", attempts: 3 }]);
121
- });
122
-
123
- it("drops the warning once the message syncs", async () => {
124
- imap.deliver("INBOX", eml({ "Message-ID": "<a@example.com>" }));
125
- const restore = dropEnvelopes();
126
- await syncHeaders();
127
- restore();
128
-
129
- await syncHeaders();
130
-
131
- assert.deepEqual(await warnings(), []);
132
- });
133
-
134
- it("fails the round when the warning cannot be written", async () => {
135
- imap.deliver("INBOX", eml({ "Message-ID": "<a@example.com>" }));
136
- dropEnvelopes();
137
- const failing: ISyncWarningRepository = {
138
- ...store.syncWarning,
139
- recordFailures: async () => {
140
- throw new Error("database is locked");
141
- },
142
- };
143
-
144
- await assert.rejects(syncHeaders(failing), /database is locked/);
145
- });
146
-
147
- it("drops a warning from an earlier UIDVALIDITY", async () => {
148
- imap.deliver("INBOX", eml({ "Message-ID": "<a@example.com>" }));
149
- const uidValidity = await servedUidValidity();
150
- await plant(1, uidValidity + 1);
151
-
152
- await syncHeaders();
153
-
154
- assert.deepEqual(await warnings(), []);
155
- });
156
-
157
- it("drops a warning for a uid the server no longer lists", async () => {
158
- const uidValidity = await servedUidValidity();
159
- await plant(40217, uidValidity);
160
-
161
- await syncHeaders();
162
-
163
- assert.deepEqual(await warnings(), []);
164
- });
165
-
166
- it("goes with its mailbox", async () => {
167
- imap.deliver("INBOX", eml({ "Message-ID": "<a@example.com>" }));
168
- dropEnvelopes();
169
- await syncHeaders();
170
-
171
- await store.mailbox.delete(account.accountId, inbox.mailboxId);
172
-
173
- assert.deepEqual(await warnings(), []);
174
- });
175
- });
176
-
177
- describe("a message whose body fails to sync", () => {
178
- it("drops the warning once a header round finds the message gone from the server", async () => {
179
- const uid = imap.deliver("INBOX", eml({ "Message-ID": "<c@example.com>" }));
180
- await syncFolder(store, account, inbox, imap);
181
- const { messageId } = await messageAtUid(store, inbox, uid);
182
- const silent = imap.connection();
183
- silent.fetchMessageBodies = async function* () {
184
- yield* [];
185
- };
186
- for (let round = 0; round < 3; round++) await syncBody(messageId, silent);
187
- assert.deepEqual(await warnings(), [{ uid, stage: "Body", attempts: 3 }]);
188
-
189
- imap.expunge("INBOX", uid);
190
- await syncHeaders();
191
-
192
- assert.deepEqual(await warnings(), []);
193
- });
194
-
195
- it("keeps one warning per failed round and drops it once the body is stored", async () => {
196
- const uid = imap.deliver("INBOX", eml({ "Message-ID": "<b@example.com>" }));
197
- await syncFolder(store, account, inbox, imap);
198
- const { messageId } = await messageAtUid(store, inbox, uid);
199
- const silent = imap.connection();
200
- silent.fetchMessageBodies = async function* () {
201
- yield* [];
202
- };
203
-
204
- for (let round = 0; round < 3; round++) await syncBody(messageId, silent);
205
-
206
- assert.deepEqual(await warnings(), [{ uid, stage: "Body", attempts: 3 }]);
207
-
208
- const result = await syncBody(messageId, imap.connection());
209
-
210
- assert.deepEqual(result.failedMessageIds, []);
211
- assert.deepEqual(await warnings(), []);
212
- });
213
- });
@@ -1,56 +0,0 @@
1
- import type {
2
- ISyncWarningRepository,
3
- SyncWarningItem,
4
- SyncWarningKey,
5
- } from "@remit/data-ports";
6
-
7
- export type SyncWarningContext = Omit<SyncWarningKey, "uid" | "stage">;
8
-
9
- export type ServerUids =
10
- | { kind: "Listed"; uids: readonly number[] }
11
- | { kind: "OnRequest"; list: () => Promise<readonly number[]> }
12
- | { kind: "Unlisted" };
13
-
14
- export interface SyncWarningRound {
15
- failedUids: readonly number[];
16
- syncedUids: readonly number[];
17
- serverUids: ServerUids;
18
- }
19
-
20
- const listedUids = async (
21
- serverUids: ServerUids,
22
- recordedCount: number,
23
- ): Promise<Set<number> | undefined> => {
24
- if (serverUids.kind === "Listed") return new Set(serverUids.uids);
25
- if (serverUids.kind === "OnRequest" && recordedCount > 0) {
26
- return new Set(await serverUids.list());
27
- }
28
- return undefined;
29
- };
30
-
31
- export class SyncWarningService {
32
- constructor(private readonly repository: ISyncWarningRepository) {}
33
-
34
- async settle(
35
- context: SyncWarningContext,
36
- stage: SyncWarningItem["stage"],
37
- round: SyncWarningRound,
38
- ): Promise<void> {
39
- await this.repository.recordFailures(
40
- [...new Set(round.failedUids)].map((uid) => ({ ...context, uid, stage })),
41
- );
42
-
43
- const recorded = await this.repository.listByMailboxId(context.mailboxId);
44
- const synced = new Set(round.syncedUids);
45
- const listed = await listedUids(round.serverUids, recorded.length);
46
- const cleared = recorded
47
- .filter(
48
- (row) =>
49
- (listed !== undefined && !listed.has(row.uid)) ||
50
- (row.stage === stage &&
51
- (row.uidValidity !== context.uidValidity || synced.has(row.uid))),
52
- )
53
- .map((row) => row.syncWarningId);
54
- await this.repository.deleteMany(cleared);
55
- }
56
- }
@@ -1,409 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { describe, it } from "node:test";
3
- import {
4
- advanceChangeCursor,
5
- advanceUidWatermarks,
6
- type ChangeCursor,
7
- dropAppliedPrefix,
8
- formatChangeCursor,
9
- orderByModseq,
10
- parseChangeCursor,
11
- parseModseq,
12
- } from "./sync-watermarks.js";
13
- import type { ImapMessage } from "./types.js";
14
-
15
- const message = (uid: number, modseq?: string): ImapMessage => ({
16
- uid,
17
- seq: uid,
18
- flags: [],
19
- internalDate: new Date(0),
20
- size: 0,
21
- ...(modseq !== undefined ? { modseq } : {}),
22
- });
23
-
24
- /** `count` messages sharing one mod-sequence, as a single STORE produces. */
25
- const tieGroup = (
26
- count: number,
27
- modseq: string,
28
- firstUid: number,
29
- ): ImapMessage[] =>
30
- Array.from({ length: count }, (_, i) => message(firstUid + i, modseq));
31
-
32
- describe("parseModseq", () => {
33
- it("parses a value beyond 2^53 losslessly", () => {
34
- assert.equal(parseModseq("18446744073709551615"), 18446744073709551615n);
35
- });
36
-
37
- it("treats an absent or zero watermark as no watermark", () => {
38
- assert.equal(parseModseq(undefined), 0n);
39
- assert.equal(parseModseq(""), 0n);
40
- assert.equal(parseModseq("0"), 0n);
41
- });
42
- });
43
-
44
- describe("orderByModseq", () => {
45
- it("orders oldest change first so a partial round is resumable", () => {
46
- const ordered = orderByModseq([
47
- message(3, "30"),
48
- message(1, "10"),
49
- message(2, "20"),
50
- ]);
51
-
52
- assert.deepEqual(
53
- ordered.map((m) => m.uid),
54
- [1, 2, 3],
55
- );
56
- });
57
-
58
- it("breaks ties on UID and compares beyond 2^53", () => {
59
- const ordered = orderByModseq([
60
- message(9, "9007199254740993"),
61
- message(4, "9007199254740992"),
62
- message(2, "9007199254740992"),
63
- ]);
64
-
65
- assert.deepEqual(
66
- ordered.map((m) => m.uid),
67
- [2, 4, 9],
68
- );
69
- });
70
- });
71
-
72
- describe("parseChangeCursor / formatChangeCursor", () => {
73
- it("reads a plain value written before the sub-position existed", () => {
74
- assert.deepEqual(parseChangeCursor("900"), {
75
- modseq: 900n,
76
- group: 0n,
77
- uid: 0,
78
- });
79
- });
80
-
81
- it("round-trips a clean boundary as plain digits", () => {
82
- assert.equal(
83
- formatChangeCursor({ modseq: 900n, group: 0n, uid: 0 }),
84
- "900",
85
- );
86
- });
87
-
88
- it("round-trips a position inside a group, implying the fetch point", () => {
89
- assert.equal(
90
- formatChangeCursor({ modseq: 199n, group: 200n, uid: 500 }),
91
- "200:500",
92
- );
93
- assert.deepEqual(parseChangeCursor("200:500"), {
94
- modseq: 199n,
95
- group: 200n,
96
- uid: 500,
97
- });
98
- });
99
-
100
- it("stays inside the field's 32 characters at full 64-bit width", () => {
101
- const widest = formatChangeCursor({
102
- modseq: 18446744073709551614n,
103
- group: 18446744073709551615n,
104
- uid: 4294967295,
105
- });
106
-
107
- assert.equal(widest, "18446744073709551615:4294967295");
108
- assert.ok(widest.length <= 32);
109
- assert.deepEqual(parseChangeCursor(widest), {
110
- modseq: 18446744073709551614n,
111
- group: 18446744073709551615n,
112
- uid: 4294967295,
113
- });
114
- });
115
-
116
- it("treats an absent cursor as no cursor", () => {
117
- assert.deepEqual(parseChangeCursor(undefined), {
118
- modseq: 0n,
119
- group: 0n,
120
- uid: 0,
121
- });
122
- assert.deepEqual(parseChangeCursor("0"), {
123
- modseq: 0n,
124
- group: 0n,
125
- uid: 0,
126
- });
127
- });
128
-
129
- it("degrades a malformed cursor to enumeration instead of throwing", () => {
130
- // A throw here would take the mailbox out of sync permanently, with no
131
- // path left to read the value and repair it.
132
- for (const raw of ["abc", "500:abc", "abc:1", "", ":", "1:2:3"]) {
133
- assert.doesNotThrow(() => parseChangeCursor(raw));
134
- }
135
- assert.equal(parseChangeCursor("abc").modseq, 0n);
136
- assert.equal(parseChangeCursor("500:abc").modseq, 500n);
137
- });
138
- });
139
-
140
- describe("dropAppliedPrefix", () => {
141
- it("passes everything through on a clean boundary", () => {
142
- const ordered = tieGroup(3, "900", 10);
143
-
144
- assert.equal(
145
- dropAppliedPrefix(ordered, { modseq: 800n, group: 0n, uid: 0 }).length,
146
- 3,
147
- );
148
- });
149
-
150
- it("drops the members of the recorded group already applied", () => {
151
- const ordered = [...tieGroup(4, "200", 500), message(50, "300")];
152
-
153
- const remaining = dropAppliedPrefix(ordered, {
154
- modseq: 199n,
155
- group: 200n,
156
- uid: 500,
157
- });
158
-
159
- assert.deepEqual(
160
- remaining.map((m) => m.uid),
161
- [501, 502, 503, 50],
162
- );
163
- });
164
-
165
- it("skips nothing when the recorded group has vanished from the result", () => {
166
- // The group's remainder was expunged, or re-modified onto a higher
167
- // mod-sequence. Inferring the in-progress group from the lowest one
168
- // returned would drop the two leading messages of an unrelated group.
169
- const ordered = [
170
- message(10, "300"),
171
- message(20, "300"),
172
- message(600, "300"),
173
- ];
174
-
175
- const remaining = dropAppliedPrefix(ordered, {
176
- modseq: 199n,
177
- group: 200n,
178
- uid: 500,
179
- });
180
-
181
- assert.deepEqual(
182
- remaining.map((m) => m.uid),
183
- [10, 20, 600],
184
- );
185
- });
186
- });
187
-
188
- describe("advanceChangeCursor", () => {
189
- const noFailures = new Set<number>();
190
- const boundary = (modseq: bigint): ChangeCursor => ({
191
- modseq,
192
- group: 0n,
193
- uid: 0,
194
- });
195
-
196
- it("jumps to the server HIGHESTMODSEQ when the whole set was applied", () => {
197
- const ordered = [message(1, "11"), message(2, "12")];
198
-
199
- const result = advanceChangeCursor({
200
- cursor: boundary(10n),
201
- serverModseq: 20n,
202
- ordered,
203
- batch: ordered,
204
- failedUids: noFailures,
205
- });
206
-
207
- assert.deepEqual(result, { cursor: boundary(20n), hasMore: false });
208
- });
209
-
210
- it("records which group it stopped inside, and where", () => {
211
- // 60 messages marked read by one STORE; the round applies 50.
212
- const ordered = tieGroup(60, "900", 10);
213
-
214
- const result = advanceChangeCursor({
215
- cursor: boundary(800n),
216
- serverModseq: 900n,
217
- ordered,
218
- batch: ordered.slice(0, 50),
219
- failedUids: noFailures,
220
- });
221
-
222
- assert.deepEqual(result, {
223
- cursor: { modseq: 899n, group: 900n, uid: 59 },
224
- hasMore: true,
225
- });
226
- });
227
-
228
- it("resumes a part-applied group and closes it on the boundary", () => {
229
- const ordered = tieGroup(10, "900", 60);
230
-
231
- const result = advanceChangeCursor({
232
- cursor: { modseq: 899n, group: 900n, uid: 59 },
233
- serverModseq: 900n,
234
- ordered,
235
- batch: ordered,
236
- failedUids: noFailures,
237
- });
238
-
239
- assert.deepEqual(result, { cursor: boundary(900n), hasMore: false });
240
- });
241
-
242
- it("stops on the boundary when the next change starts a new group", () => {
243
- const ordered = [...tieGroup(2, "900", 10), ...tieGroup(2, "950", 20)];
244
-
245
- const result = advanceChangeCursor({
246
- cursor: boundary(800n),
247
- serverModseq: 999n,
248
- ordered,
249
- batch: ordered.slice(0, 2),
250
- failedUids: noFailures,
251
- });
252
-
253
- assert.deepEqual(result, { cursor: boundary(900n), hasMore: true });
254
- });
255
-
256
- it("stops below a failure inside a group", () => {
257
- const ordered = tieGroup(4, "900", 10);
258
-
259
- const result = advanceChangeCursor({
260
- cursor: boundary(800n),
261
- serverModseq: 900n,
262
- ordered,
263
- batch: ordered,
264
- failedUids: new Set([12]),
265
- });
266
-
267
- assert.deepEqual(result.cursor, { modseq: 899n, group: 900n, uid: 11 });
268
- });
269
-
270
- it("does not move when the very first change fails", () => {
271
- const ordered = tieGroup(2, "900", 10);
272
-
273
- const result = advanceChangeCursor({
274
- cursor: boundary(800n),
275
- serverModseq: 900n,
276
- ordered,
277
- batch: ordered,
278
- failedUids: new Set([10]),
279
- });
280
-
281
- assert.deepEqual(result.cursor, boundary(800n));
282
- });
283
-
284
- it("keeps every completed group when a later one fails part-way", () => {
285
- const ordered = [...tieGroup(2, "810", 1), ...tieGroup(3, "900", 10)];
286
-
287
- const result = advanceChangeCursor({
288
- cursor: boundary(800n),
289
- serverModseq: 999n,
290
- ordered,
291
- batch: ordered,
292
- failedUids: new Set([11]),
293
- });
294
-
295
- // 810 is complete and 900 is part-applied, so the fetch point sits just
296
- // below 900 and the group is named.
297
- assert.deepEqual(result.cursor, { modseq: 899n, group: 900n, uid: 10 });
298
- });
299
-
300
- it("floors the fetch point at zero when a message carries no MODSEQ", () => {
301
- const ordered = [message(1), message(2)];
302
-
303
- const result = advanceChangeCursor({
304
- cursor: boundary(0n),
305
- serverModseq: 0n,
306
- ordered,
307
- batch: ordered.slice(0, 1),
308
- failedUids: noFailures,
309
- });
310
-
311
- assert.equal(result.cursor.modseq >= 0n, true);
312
- assert.equal(formatChangeCursor(result.cursor).startsWith("-"), false);
313
- });
314
-
315
- it("never moves backwards when the server reports a lower value", () => {
316
- const result = advanceChangeCursor({
317
- cursor: boundary(50n),
318
- serverModseq: 20n,
319
- ordered: [],
320
- batch: [],
321
- failedUids: noFailures,
322
- });
323
-
324
- assert.deepEqual(result.cursor, boundary(50n));
325
- });
326
- });
327
-
328
- describe("advanceUidWatermarks", () => {
329
- const noFailures = new Set<number>();
330
-
331
- it("advances the forward watermark over a clean batch of new mail", () => {
332
- const result = advanceUidWatermarks({
333
- batchUids: [21, 22, 23],
334
- failedUids: noFailures,
335
- lastSyncUid: 5,
336
- highWaterMarkUid: 20,
337
- });
338
-
339
- assert.equal(result.highWaterMarkUid, 23);
340
- assert.equal(result.lastSyncUid, 5);
341
- });
342
-
343
- it("stops the forward watermark below a failed UID rather than jumping it", () => {
344
- const result = advanceUidWatermarks({
345
- batchUids: [21, 22, 23, 24, 25],
346
- failedUids: new Set([23]),
347
- lastSyncUid: 5,
348
- highWaterMarkUid: 20,
349
- });
350
-
351
- assert.equal(result.highWaterMarkUid, 22);
352
- });
353
-
354
- it("keeps a failed backfill UID inside the backfill window", () => {
355
- const result = advanceUidWatermarks({
356
- batchUids: [5, 6, 7],
357
- failedUids: new Set([6]),
358
- lastSyncUid: 10,
359
- highWaterMarkUid: 20,
360
- });
361
-
362
- assert.equal(result.lastSyncUid, 7);
363
- });
364
-
365
- it("lowers the backfill floor to the lowest UID of a clean batch", () => {
366
- const result = advanceUidWatermarks({
367
- batchUids: [5, 6, 7],
368
- failedUids: noFailures,
369
- lastSyncUid: 10,
370
- highWaterMarkUid: 20,
371
- });
372
-
373
- assert.equal(result.lastSyncUid, 5);
374
- });
375
-
376
- it("lets a failure constrain only its own region", () => {
377
- const result = advanceUidWatermarks({
378
- batchUids: [5, 6, 21, 22],
379
- failedUids: new Set([6]),
380
- lastSyncUid: 10,
381
- highWaterMarkUid: 20,
382
- });
383
-
384
- assert.equal(result.highWaterMarkUid, 22);
385
- assert.equal(result.lastSyncUid, 7);
386
- });
387
-
388
- it("sets both watermarks from a fresh sync", () => {
389
- const result = advanceUidWatermarks({
390
- batchUids: [1, 2, 3],
391
- failedUids: noFailures,
392
- lastSyncUid: 0,
393
- highWaterMarkUid: 0,
394
- });
395
-
396
- assert.deepEqual(result, { lastSyncUid: 1, highWaterMarkUid: 3 });
397
- });
398
-
399
- it("never moves a watermark backwards", () => {
400
- const result = advanceUidWatermarks({
401
- batchUids: [21],
402
- failedUids: new Set([21]),
403
- lastSyncUid: 5,
404
- highWaterMarkUid: 20,
405
- });
406
-
407
- assert.deepEqual(result, { lastSyncUid: 5, highWaterMarkUid: 20 });
408
- });
409
- });