@diskd-ai/email-client-mcp 0.1.0

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 (81) hide show
  1. package/LICENSE +155 -0
  2. package/README.md +110 -0
  3. package/dist/config/loader.d.ts +12 -0
  4. package/dist/config/loader.js +42 -0
  5. package/dist/config/loader.js.map +1 -0
  6. package/dist/config/schema.d.ts +512 -0
  7. package/dist/config/schema.js +66 -0
  8. package/dist/config/schema.js.map +1 -0
  9. package/dist/domain/errors.d.ts +40 -0
  10. package/dist/domain/errors.js +46 -0
  11. package/dist/domain/errors.js.map +1 -0
  12. package/dist/domain/result.d.ts +30 -0
  13. package/dist/domain/result.js +12 -0
  14. package/dist/domain/result.js.map +1 -0
  15. package/dist/imap/fetch.d.ts +44 -0
  16. package/dist/imap/fetch.js +127 -0
  17. package/dist/imap/fetch.js.map +1 -0
  18. package/dist/imap/mapper.d.ts +70 -0
  19. package/dist/imap/mapper.js +140 -0
  20. package/dist/imap/mapper.js.map +1 -0
  21. package/dist/imap/pool.d.ts +26 -0
  22. package/dist/imap/pool.js +92 -0
  23. package/dist/imap/pool.js.map +1 -0
  24. package/dist/imap/virtualFolders.d.ts +11 -0
  25. package/dist/imap/virtualFolders.js +20 -0
  26. package/dist/imap/virtualFolders.js.map +1 -0
  27. package/dist/sdk/diskdClient.d.ts +21 -0
  28. package/dist/sdk/diskdClient.js +39 -0
  29. package/dist/sdk/diskdClient.js.map +1 -0
  30. package/dist/server.d.ts +17 -0
  31. package/dist/server.js +180 -0
  32. package/dist/server.js.map +1 -0
  33. package/dist/store/conventions.d.ts +31 -0
  34. package/dist/store/conventions.js +53 -0
  35. package/dist/store/conventions.js.map +1 -0
  36. package/dist/store/driveStore.d.ts +36 -0
  37. package/dist/store/driveStore.js +95 -0
  38. package/dist/store/driveStore.js.map +1 -0
  39. package/dist/store/payloadTypes.d.ts +62 -0
  40. package/dist/store/payloadTypes.js +11 -0
  41. package/dist/store/payloadTypes.js.map +1 -0
  42. package/dist/sync/backoff.d.ts +18 -0
  43. package/dist/sync/backoff.js +24 -0
  44. package/dist/sync/backoff.js.map +1 -0
  45. package/dist/sync/sync.d.ts +85 -0
  46. package/dist/sync/sync.js +383 -0
  47. package/dist/sync/sync.js.map +1 -0
  48. package/dist/sync/watcher.d.ts +43 -0
  49. package/dist/sync/watcher.js +141 -0
  50. package/dist/sync/watcher.js.map +1 -0
  51. package/dist/tools/bulkAction.d.ts +41 -0
  52. package/dist/tools/bulkAction.js +82 -0
  53. package/dist/tools/bulkAction.js.map +1 -0
  54. package/dist/tools/findMailboxFolder.d.ts +33 -0
  55. package/dist/tools/findMailboxFolder.js +45 -0
  56. package/dist/tools/findMailboxFolder.js.map +1 -0
  57. package/dist/tools/getEmail.d.ts +41 -0
  58. package/dist/tools/getEmail.js +74 -0
  59. package/dist/tools/getEmail.js.map +1 -0
  60. package/dist/tools/getEmails.d.ts +38 -0
  61. package/dist/tools/getEmails.js +72 -0
  62. package/dist/tools/getEmails.js.map +1 -0
  63. package/dist/tools/getWatcherStatus.d.ts +9 -0
  64. package/dist/tools/getWatcherStatus.js +8 -0
  65. package/dist/tools/getWatcherStatus.js.map +1 -0
  66. package/dist/tools/listAccounts.d.ts +19 -0
  67. package/dist/tools/listAccounts.js +18 -0
  68. package/dist/tools/listAccounts.js.map +1 -0
  69. package/dist/tools/listEmails.d.ts +70 -0
  70. package/dist/tools/listEmails.js +101 -0
  71. package/dist/tools/listEmails.js.map +1 -0
  72. package/dist/tools/listMailboxFolder.d.ts +33 -0
  73. package/dist/tools/listMailboxFolder.js +60 -0
  74. package/dist/tools/listMailboxFolder.js.map +1 -0
  75. package/dist/tools/moveEmail.d.ts +36 -0
  76. package/dist/tools/moveEmail.js +52 -0
  77. package/dist/tools/moveEmail.js.map +1 -0
  78. package/dist/tools/registry.d.ts +17 -0
  79. package/dist/tools/registry.js +64 -0
  80. package/dist/tools/registry.js.map +1 -0
  81. package/package.json +46 -0
@@ -0,0 +1,383 @@
1
+ /**
2
+ * The reliability core. `runSyncOnce(account)` performs one watcher
3
+ * pass for one account.
4
+ *
5
+ * Invariants enforced here:
6
+ * 1. `lastSyncedUid` is advanced ONLY after a successful upsertBatch.
7
+ * A crash mid-tick or a Drive 5xx leaves the checkpoint untouched,
8
+ * so the next tick re-fetches the unprocessed UIDs (idempotent
9
+ * because externalId is `${UIDVALIDITY}:${UID}`).
10
+ * 2. UIDVALIDITY mismatch -> drop+recreate folder, resync from UID 1.
11
+ * 3. Folder metadata is the source of truth across restarts. The
12
+ * watcher never relies on in-memory state to resume.
13
+ * 4. Folders deleted in IMAP are pruned from Drive at the end of a tick.
14
+ * 5. A bounded sliding-window flag reconciliation catches drift on
15
+ * already-synced UIDs without re-downloading bodies.
16
+ *
17
+ * The function takes injected dependencies (drive store, IMAP client
18
+ * factory, mapper) so unit tests can drive it without real I/O.
19
+ */
20
+ import { isOAuthAccount } from "../config/schema.js";
21
+ import { errorMessage, imapError } from "../domain/errors.js";
22
+ import { Err } from "../domain/result.js";
23
+ import { toStoredPayload } from "../imap/mapper.js";
24
+ import { externalIdFor, sanitizeMailboxId } from "../store/conventions.js";
25
+ const BATCH_SIZE = 50;
26
+ const parseSyncState = (raw) => {
27
+ if (raw === null)
28
+ return null;
29
+ const uv = raw.uidValidity;
30
+ const un = raw.uidNext;
31
+ const ls = raw.lastSyncedUid;
32
+ if (typeof uv !== "number" || typeof un !== "number" || typeof ls !== "number")
33
+ return null;
34
+ return {
35
+ uidValidity: uv,
36
+ uidNext: un,
37
+ lastSyncedUid: ls,
38
+ lastSyncStartedAt: typeof raw.lastSyncStartedAt === "string" ? raw.lastSyncStartedAt : null,
39
+ lastSyncFinishedAt: typeof raw.lastSyncFinishedAt === "string" ? raw.lastSyncFinishedAt : null,
40
+ lastSyncError: typeof raw.lastSyncError === "string" ? raw.lastSyncError : null,
41
+ };
42
+ };
43
+ const range = (lo, hi) => {
44
+ const out = [];
45
+ for (let from = lo; from <= hi; from += BATCH_SIZE) {
46
+ const to = Math.min(from + BATCH_SIZE - 1, hi);
47
+ out.push([from, to]);
48
+ }
49
+ return out;
50
+ };
51
+ const flagsAreEqual = (a, b) => {
52
+ if (a.length !== b.length)
53
+ return false;
54
+ const sa = new Set(a);
55
+ for (const x of b)
56
+ if (!sa.has(x))
57
+ return false;
58
+ return true;
59
+ };
60
+ /**
61
+ * Sync one folder. Returns the per-folder report; never throws.
62
+ * `lastSyncedUid` is advanced only after each successful batch upsert.
63
+ */
64
+ const syncFolder = async (deps, account, mailboxId, folderPath, flagWindow) => {
65
+ const folderId = folderPath;
66
+ const startIso = deps.now().toISOString();
67
+ const statusR = await deps.imap.folderStatus(account.name, folderPath);
68
+ if (statusR.tag === "Err") {
69
+ return {
70
+ folderId,
71
+ newMessages: 0,
72
+ reconciledFlags: 0,
73
+ uidValidityRolled: false,
74
+ error: errorMessage(statusR.error),
75
+ };
76
+ }
77
+ const status = statusR.value;
78
+ const existing = await deps.drive.getFolder(mailboxId, folderId);
79
+ if (existing.tag === "Err") {
80
+ return {
81
+ folderId,
82
+ newMessages: 0,
83
+ reconciledFlags: 0,
84
+ uidValidityRolled: false,
85
+ error: errorMessage(existing.error),
86
+ };
87
+ }
88
+ let state = parseSyncState(existing.value?.metadata ?? null);
89
+ let uidValidityRolled = false;
90
+ // UIDVALIDITY rollover: drop the folder and start over.
91
+ if (state !== null && state.uidValidity !== status.uidValidity) {
92
+ uidValidityRolled = true;
93
+ const del = await deps.drive.deleteFolder(mailboxId, folderId);
94
+ if (del.tag === "Err") {
95
+ return {
96
+ folderId,
97
+ newMessages: 0,
98
+ reconciledFlags: 0,
99
+ uidValidityRolled,
100
+ error: errorMessage(del.error),
101
+ };
102
+ }
103
+ state = null;
104
+ }
105
+ const fromUid = (state?.lastSyncedUid ?? 0) + 1;
106
+ const toUid = status.uidNext - 1;
107
+ let lastSynced = state?.lastSyncedUid ?? 0;
108
+ let newMessages = 0;
109
+ // Initial folder upsert with fresh metadata. We rewrite metadata at
110
+ // each successful batch so the on-disk checkpoint mirrors progress.
111
+ const writeState = async (next) => deps.drive.upsertFolder(mailboxId, folderId, folderId, next);
112
+ if (state === null) {
113
+ const init = {
114
+ uidValidity: status.uidValidity,
115
+ uidNext: status.uidNext,
116
+ lastSyncedUid: 0,
117
+ lastSyncStartedAt: startIso,
118
+ lastSyncFinishedAt: null,
119
+ lastSyncError: null,
120
+ };
121
+ const w = await writeState(init);
122
+ if (w.tag === "Err") {
123
+ return {
124
+ folderId,
125
+ newMessages: 0,
126
+ reconciledFlags: 0,
127
+ uidValidityRolled,
128
+ error: errorMessage(w.error),
129
+ };
130
+ }
131
+ state = init;
132
+ }
133
+ else {
134
+ const w = await writeState({
135
+ ...state,
136
+ uidNext: status.uidNext,
137
+ lastSyncStartedAt: startIso,
138
+ lastSyncError: null,
139
+ });
140
+ if (w.tag === "Err") {
141
+ return {
142
+ folderId,
143
+ newMessages: 0,
144
+ reconciledFlags: 0,
145
+ uidValidityRolled,
146
+ error: errorMessage(w.error),
147
+ };
148
+ }
149
+ }
150
+ if (toUid >= fromUid) {
151
+ for (const [batchFrom, batchTo] of range(fromUid, toUid)) {
152
+ const payloads = [];
153
+ const externalIds = [];
154
+ let highestUidInBatch = lastSynced;
155
+ try {
156
+ for await (const bundle of deps.imap.fetchRange(account.name, folderPath, batchFrom, batchTo)) {
157
+ const uid = bundle.imapMessage.uid;
158
+ if (uid > highestUidInBatch)
159
+ highestUidInBatch = uid;
160
+ const payload = toStoredPayload(bundle.imapMessage, {
161
+ accountId: account.name,
162
+ mailbox: folderPath,
163
+ uidValidity: status.uidValidity,
164
+ fetchedAt: deps.now(),
165
+ bodyText: bundle.bodyText,
166
+ bodyHtml: bundle.bodyHtml,
167
+ truncated: false,
168
+ });
169
+ payloads.push(payload);
170
+ externalIds.push(externalIdFor(status.uidValidity, uid));
171
+ }
172
+ }
173
+ catch (cause) {
174
+ // Imap fetch failure: leave checkpoint untouched, surface error.
175
+ const e = imapError(account.name, `fetch UID ${batchFrom}:${batchTo}`, cause);
176
+ const w = await writeState({
177
+ ...state,
178
+ lastSyncError: errorMessage(e),
179
+ lastSyncFinishedAt: deps.now().toISOString(),
180
+ });
181
+ if (w.tag === "Err") {
182
+ return {
183
+ folderId,
184
+ newMessages,
185
+ reconciledFlags: 0,
186
+ uidValidityRolled,
187
+ error: errorMessage(w.error),
188
+ };
189
+ }
190
+ return {
191
+ folderId,
192
+ newMessages,
193
+ reconciledFlags: 0,
194
+ uidValidityRolled,
195
+ error: errorMessage(e),
196
+ };
197
+ }
198
+ if (payloads.length === 0) {
199
+ // Range was empty -- advance the checkpoint to the batch top
200
+ // anyway, we now know there is nothing to fetch in there.
201
+ lastSynced = batchTo;
202
+ }
203
+ else {
204
+ const ups = await deps.drive.upsertMessages(mailboxId, folderId, payloads, externalIds);
205
+ if (ups.tag === "Err") {
206
+ // Critical: do NOT advance lastSyncedUid. Next tick replays.
207
+ const w = await writeState({
208
+ ...state,
209
+ lastSyncError: errorMessage(ups.error),
210
+ lastSyncFinishedAt: deps.now().toISOString(),
211
+ });
212
+ if (w.tag === "Err") {
213
+ return {
214
+ folderId,
215
+ newMessages,
216
+ reconciledFlags: 0,
217
+ uidValidityRolled,
218
+ error: errorMessage(w.error),
219
+ };
220
+ }
221
+ return {
222
+ folderId,
223
+ newMessages,
224
+ reconciledFlags: 0,
225
+ uidValidityRolled,
226
+ error: errorMessage(ups.error),
227
+ };
228
+ }
229
+ newMessages += ups.value.inserted + ups.value.updated;
230
+ lastSynced = highestUidInBatch;
231
+ }
232
+ // Checkpoint: advance lastSyncedUid after the batch is durable.
233
+ const checkpoint = {
234
+ ...state,
235
+ uidValidity: status.uidValidity,
236
+ uidNext: status.uidNext,
237
+ lastSyncedUid: lastSynced,
238
+ lastSyncFinishedAt: deps.now().toISOString(),
239
+ lastSyncError: null,
240
+ };
241
+ const w = await writeState(checkpoint);
242
+ if (w.tag === "Err") {
243
+ return {
244
+ folderId,
245
+ newMessages,
246
+ reconciledFlags: 0,
247
+ uidValidityRolled,
248
+ error: errorMessage(w.error),
249
+ };
250
+ }
251
+ state = checkpoint;
252
+ }
253
+ }
254
+ // Sliding-window flag reconciliation. Bounds drift to flagWindow UIDs.
255
+ let reconciledFlags = 0;
256
+ if (flagWindow > 0 && lastSynced > 0) {
257
+ const reconFrom = Math.max(1, lastSynced - flagWindow + 1);
258
+ try {
259
+ const updated = [];
260
+ const updatedIds = [];
261
+ for await (const env of deps.imap.fetchEnvelopesRange(account.name, folderPath, reconFrom, lastSynced)) {
262
+ const incoming = toStoredPayload(env, {
263
+ accountId: account.name,
264
+ mailbox: folderPath,
265
+ uidValidity: status.uidValidity,
266
+ fetchedAt: deps.now(),
267
+ bodyText: null,
268
+ bodyHtml: null,
269
+ truncated: false,
270
+ });
271
+ // Always upsert -- the store is opaque, but flagsAreEqual would
272
+ // require a read-back. Bounded set keeps cost predictable.
273
+ // We keep the shape stable; bodyText/Html stay null in this mode
274
+ // but flags/labels reflect the latest IMAP state.
275
+ if (!flagsAreEqual(incoming.flags, [])) {
276
+ updated.push(incoming);
277
+ updatedIds.push(externalIdFor(status.uidValidity, env.uid));
278
+ }
279
+ else {
280
+ updated.push(incoming);
281
+ updatedIds.push(externalIdFor(status.uidValidity, env.uid));
282
+ }
283
+ }
284
+ if (updated.length > 0) {
285
+ const ups = await deps.drive.upsertMessages(mailboxId, folderId, updated, updatedIds);
286
+ if (ups.tag === "Err") {
287
+ // Reconciliation is best-effort; record but don't advance error state.
288
+ return {
289
+ folderId,
290
+ newMessages,
291
+ reconciledFlags: 0,
292
+ uidValidityRolled,
293
+ error: errorMessage(ups.error),
294
+ };
295
+ }
296
+ reconciledFlags = updated.length;
297
+ }
298
+ }
299
+ catch (cause) {
300
+ // Best-effort step; keep main result.
301
+ const e = imapError(account.name, "flag reconciliation", cause);
302
+ return {
303
+ folderId,
304
+ newMessages,
305
+ reconciledFlags: 0,
306
+ uidValidityRolled,
307
+ error: errorMessage(e),
308
+ };
309
+ }
310
+ }
311
+ return { folderId, newMessages, reconciledFlags, uidValidityRolled, error: null };
312
+ };
313
+ /**
314
+ * Run one sync tick for one account. Sequentially walks all folders,
315
+ * then prunes Drive folders that no longer exist in IMAP.
316
+ */
317
+ export const runSyncOnce = async (deps, account, watcher) => {
318
+ const startedAt = deps.now().toISOString();
319
+ const mailboxId = sanitizeMailboxId(account.name);
320
+ const displayName = isOAuthAccount(account)
321
+ ? account.email
322
+ : (account.full_name ?? account.email);
323
+ const ens = await deps.drive.ensureMailbox(mailboxId, displayName);
324
+ if (ens.tag === "Err") {
325
+ return {
326
+ accountId: account.name,
327
+ folders: [],
328
+ prunedFolders: 0,
329
+ startedAt,
330
+ finishedAt: deps.now().toISOString(),
331
+ error: errorMessage(ens.error),
332
+ };
333
+ }
334
+ const foldersR = await deps.imap.listFolders(account.name);
335
+ if (foldersR.tag === "Err") {
336
+ return {
337
+ accountId: account.name,
338
+ folders: [],
339
+ prunedFolders: 0,
340
+ startedAt,
341
+ finishedAt: deps.now().toISOString(),
342
+ error: errorMessage(foldersR.error),
343
+ };
344
+ }
345
+ const allFolders = foldersR.value;
346
+ const filteredFolders = (() => {
347
+ if (watcher.folders === undefined || watcher.folders.length === 0)
348
+ return allFolders;
349
+ const allow = new Set(watcher.folders);
350
+ return allFolders.filter((f) => allow.has(f.path));
351
+ })();
352
+ const reports = [];
353
+ for (const f of filteredFolders) {
354
+ const r = await syncFolder(deps, account, mailboxId, f.path, watcher.flag_reconcile_window);
355
+ reports.push(r);
356
+ }
357
+ // Prune Drive folders that disappeared from IMAP.
358
+ let prunedFolders = 0;
359
+ const driveFoldersR = await deps.drive.listFolders(mailboxId);
360
+ if (driveFoldersR.tag === "Ok") {
361
+ const imapPaths = new Set(filteredFolders.map((f) => f.path));
362
+ for (const df of driveFoldersR.value) {
363
+ if (!imapPaths.has(df.folderId)) {
364
+ const del = await deps.drive.deleteFolder(mailboxId, df.folderId);
365
+ if (del.tag === "Ok")
366
+ prunedFolders += 1;
367
+ }
368
+ }
369
+ }
370
+ const finishedAt = deps.now().toISOString();
371
+ const firstError = reports.find((r) => r.error !== null)?.error ?? null;
372
+ return {
373
+ accountId: account.name,
374
+ folders: reports,
375
+ prunedFolders,
376
+ startedAt,
377
+ finishedAt,
378
+ error: firstError,
379
+ };
380
+ };
381
+ // Re-export for tests.
382
+ export const __test__ = { Err };
383
+ //# sourceMappingURL=sync.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sync.js","sourceRoot":"","sources":["../../src/sync/sync.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAgB,cAAc,EAAwB,MAAM,qBAAqB,CAAC;AACzF,OAAO,EAAiB,YAAY,EAAkB,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAC7F,OAAO,EAAE,GAAG,EAAe,MAAM,qBAAqB,CAAC;AACvD,OAAO,EAA2B,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAC7E,OAAO,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAG3E,MAAM,UAAU,GAAG,EAAE,CAAC;AA6EtB,MAAM,cAAc,GAAG,CAAC,GAA6C,EAAoB,EAAE;IACzF,IAAI,GAAG,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IAC9B,MAAM,EAAE,GAAG,GAAG,CAAC,WAAW,CAAC;IAC3B,MAAM,EAAE,GAAG,GAAG,CAAC,OAAO,CAAC;IACvB,MAAM,EAAE,GAAG,GAAG,CAAC,aAAa,CAAC;IAC7B,IAAI,OAAO,EAAE,KAAK,QAAQ,IAAI,OAAO,EAAE,KAAK,QAAQ,IAAI,OAAO,EAAE,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC;IAC5F,OAAO;QACL,WAAW,EAAE,EAAE;QACf,OAAO,EAAE,EAAE;QACX,aAAa,EAAE,EAAE;QACjB,iBAAiB,EAAE,OAAO,GAAG,CAAC,iBAAiB,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAC,CAAC,IAAI;QAC3F,kBAAkB,EAAE,OAAO,GAAG,CAAC,kBAAkB,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,kBAAkB,CAAC,CAAC,CAAC,IAAI;QAC9F,aAAa,EAAE,OAAO,GAAG,CAAC,aAAa,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC,CAAC,IAAI;KAChF,CAAC;AACJ,CAAC,CAAC;AAEF,MAAM,KAAK,GAAG,CAAC,EAAU,EAAE,EAAU,EAA+B,EAAE;IACpE,MAAM,GAAG,GAAuB,EAAE,CAAC;IACnC,KAAK,IAAI,IAAI,GAAG,EAAE,EAAE,IAAI,IAAI,EAAE,EAAE,IAAI,IAAI,UAAU,EAAE,CAAC;QACnD,MAAM,EAAE,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,GAAG,UAAU,GAAG,CAAC,EAAE,EAAE,CAAC,CAAC;QAC/C,GAAG,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,CAAC;IACvB,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC,CAAC;AAEF,MAAM,aAAa,GAAG,CAAC,CAAoB,EAAE,CAAoB,EAAW,EAAE;IAC5E,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,MAAM;QAAE,OAAO,KAAK,CAAC;IACxC,MAAM,EAAE,GAAG,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC;IACtB,KAAK,MAAM,CAAC,IAAI,CAAC;QAAE,IAAI,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;YAAE,OAAO,KAAK,CAAC;IAChD,OAAO,IAAI,CAAC;AACd,CAAC,CAAC;AAEF;;;GAGG;AACH,MAAM,UAAU,GAAG,KAAK,EACtB,IAAc,EACd,OAAgB,EAChB,SAAiB,EACjB,UAAkB,EAClB,UAAkB,EACS,EAAE;IAC7B,MAAM,QAAQ,GAAG,UAAU,CAAC;IAC5B,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE,CAAC;IAE1C,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,YAAY,CAAC,OAAO,CAAC,IAAI,EAAE,UAAU,CAAC,CAAC;IACvE,IAAI,OAAO,CAAC,GAAG,KAAK,KAAK,EAAE,CAAC;QAC1B,OAAO;YACL,QAAQ;YACR,WAAW,EAAE,CAAC;YACd,eAAe,EAAE,CAAC;YAClB,iBAAiB,EAAE,KAAK;YACxB,KAAK,EAAE,YAAY,CAAC,OAAO,CAAC,KAAK,CAAC;SACnC,CAAC;IACJ,CAAC;IACD,MAAM,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC;IAE7B,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;IACjE,IAAI,QAAQ,CAAC,GAAG,KAAK,KAAK,EAAE,CAAC;QAC3B,OAAO;YACL,QAAQ;YACR,WAAW,EAAE,CAAC;YACd,eAAe,EAAE,CAAC;YAClB,iBAAiB,EAAE,KAAK;YACxB,KAAK,EAAE,YAAY,CAAC,QAAQ,CAAC,KAAK,CAAC;SACpC,CAAC;IACJ,CAAC;IACD,IAAI,KAAK,GAAG,cAAc,CAAC,QAAQ,CAAC,KAAK,EAAE,QAAQ,IAAI,IAAI,CAAC,CAAC;IAC7D,IAAI,iBAAiB,GAAG,KAAK,CAAC;IAE9B,wDAAwD;IACxD,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,CAAC,WAAW,KAAK,MAAM,CAAC,WAAW,EAAE,CAAC;QAC/D,iBAAiB,GAAG,IAAI,CAAC;QACzB,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;QAC/D,IAAI,GAAG,CAAC,GAAG,KAAK,KAAK,EAAE,CAAC;YACtB,OAAO;gBACL,QAAQ;gBACR,WAAW,EAAE,CAAC;gBACd,eAAe,EAAE,CAAC;gBAClB,iBAAiB;gBACjB,KAAK,EAAE,YAAY,CAAC,GAAG,CAAC,KAAK,CAAC;aAC/B,CAAC;QACJ,CAAC;QACD,KAAK,GAAG,IAAI,CAAC;IACf,CAAC;IAED,MAAM,OAAO,GAAG,CAAC,KAAK,EAAE,aAAa,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC;IAChD,MAAM,KAAK,GAAG,MAAM,CAAC,OAAO,GAAG,CAAC,CAAC;IAEjC,IAAI,UAAU,GAAG,KAAK,EAAE,aAAa,IAAI,CAAC,CAAC;IAC3C,IAAI,WAAW,GAAG,CAAC,CAAC;IAEpB,oEAAoE;IACpE,oEAAoE;IACpE,MAAM,UAAU,GAAG,KAAK,EAAE,IAAe,EAAmC,EAAE,CAC5E,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,SAAS,EAAE,QAAQ,EAAE,QAAQ,EAAE,IAAI,CAAC,CAAC;IAE/D,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;QACnB,MAAM,IAAI,GAAc;YACtB,WAAW,EAAE,MAAM,CAAC,WAAW;YAC/B,OAAO,EAAE,MAAM,CAAC,OAAO;YACvB,aAAa,EAAE,CAAC;YAChB,iBAAiB,EAAE,QAAQ;YAC3B,kBAAkB,EAAE,IAAI;YACxB,aAAa,EAAE,IAAI;SACpB,CAAC;QACF,MAAM,CAAC,GAAG,MAAM,UAAU,CAAC,IAAI,CAAC,CAAC;QACjC,IAAI,CAAC,CAAC,GAAG,KAAK,KAAK,EAAE,CAAC;YACpB,OAAO;gBACL,QAAQ;gBACR,WAAW,EAAE,CAAC;gBACd,eAAe,EAAE,CAAC;gBAClB,iBAAiB;gBACjB,KAAK,EAAE,YAAY,CAAC,CAAC,CAAC,KAAK,CAAC;aAC7B,CAAC;QACJ,CAAC;QACD,KAAK,GAAG,IAAI,CAAC;IACf,CAAC;SAAM,CAAC;QACN,MAAM,CAAC,GAAG,MAAM,UAAU,CAAC;YACzB,GAAG,KAAK;YACR,OAAO,EAAE,MAAM,CAAC,OAAO;YACvB,iBAAiB,EAAE,QAAQ;YAC3B,aAAa,EAAE,IAAI;SACpB,CAAC,CAAC;QACH,IAAI,CAAC,CAAC,GAAG,KAAK,KAAK,EAAE,CAAC;YACpB,OAAO;gBACL,QAAQ;gBACR,WAAW,EAAE,CAAC;gBACd,eAAe,EAAE,CAAC;gBAClB,iBAAiB;gBACjB,KAAK,EAAE,YAAY,CAAC,CAAC,CAAC,KAAK,CAAC;aAC7B,CAAC;QACJ,CAAC;IACH,CAAC;IAED,IAAI,KAAK,IAAI,OAAO,EAAE,CAAC;QACrB,KAAK,MAAM,CAAC,SAAS,EAAE,OAAO,CAAC,IAAI,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,EAAE,CAAC;YACzD,MAAM,QAAQ,GAAyB,EAAE,CAAC;YAC1C,MAAM,WAAW,GAAa,EAAE,CAAC;YACjC,IAAI,iBAAiB,GAAG,UAAU,CAAC;YACnC,IAAI,CAAC;gBACH,IAAI,KAAK,EAAE,MAAM,MAAM,IAAI,IAAI,CAAC,IAAI,CAAC,UAAU,CAC7C,OAAO,CAAC,IAAI,EACZ,UAAU,EACV,SAAS,EACT,OAAO,CACR,EAAE,CAAC;oBACF,MAAM,GAAG,GAAG,MAAM,CAAC,WAAW,CAAC,GAAG,CAAC;oBACnC,IAAI,GAAG,GAAG,iBAAiB;wBAAE,iBAAiB,GAAG,GAAG,CAAC;oBACrD,MAAM,OAAO,GAAG,eAAe,CAAC,MAAM,CAAC,WAAW,EAAE;wBAClD,SAAS,EAAE,OAAO,CAAC,IAAI;wBACvB,OAAO,EAAE,UAAU;wBACnB,WAAW,EAAE,MAAM,CAAC,WAAW;wBAC/B,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE;wBACrB,QAAQ,EAAE,MAAM,CAAC,QAAQ;wBACzB,QAAQ,EAAE,MAAM,CAAC,QAAQ;wBACzB,SAAS,EAAE,KAAK;qBACjB,CAAC,CAAC;oBACH,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;oBACvB,WAAW,CAAC,IAAI,CAAC,aAAa,CAAC,MAAM,CAAC,WAAW,EAAE,GAAG,CAAC,CAAC,CAAC;gBAC3D,CAAC;YACH,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,iEAAiE;gBACjE,MAAM,CAAC,GAAG,SAAS,CAAC,OAAO,CAAC,IAAI,EAAE,aAAa,SAAS,IAAI,OAAO,EAAE,EAAE,KAAK,CAAC,CAAC;gBAC9E,MAAM,CAAC,GAAG,MAAM,UAAU,CAAC;oBACzB,GAAI,KAAmB;oBACvB,aAAa,EAAE,YAAY,CAAC,CAAC,CAAC;oBAC9B,kBAAkB,EAAE,IAAI,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE;iBAC7C,CAAC,CAAC;gBACH,IAAI,CAAC,CAAC,GAAG,KAAK,KAAK,EAAE,CAAC;oBACpB,OAAO;wBACL,QAAQ;wBACR,WAAW;wBACX,eAAe,EAAE,CAAC;wBAClB,iBAAiB;wBACjB,KAAK,EAAE,YAAY,CAAC,CAAC,CAAC,KAAK,CAAC;qBAC7B,CAAC;gBACJ,CAAC;gBACD,OAAO;oBACL,QAAQ;oBACR,WAAW;oBACX,eAAe,EAAE,CAAC;oBAClB,iBAAiB;oBACjB,KAAK,EAAE,YAAY,CAAC,CAAC,CAAC;iBACvB,CAAC;YACJ,CAAC;YACD,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;gBAC1B,6DAA6D;gBAC7D,0DAA0D;gBAC1D,UAAU,GAAG,OAAO,CAAC;YACvB,CAAC;iBAAM,CAAC;gBACN,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,SAAS,EAAE,QAAQ,EAAE,QAAQ,EAAE,WAAW,CAAC,CAAC;gBACxF,IAAI,GAAG,CAAC,GAAG,KAAK,KAAK,EAAE,CAAC;oBACtB,6DAA6D;oBAC7D,MAAM,CAAC,GAAG,MAAM,UAAU,CAAC;wBACzB,GAAI,KAAmB;wBACvB,aAAa,EAAE,YAAY,CAAC,GAAG,CAAC,KAAK,CAAC;wBACtC,kBAAkB,EAAE,IAAI,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE;qBAC7C,CAAC,CAAC;oBACH,IAAI,CAAC,CAAC,GAAG,KAAK,KAAK,EAAE,CAAC;wBACpB,OAAO;4BACL,QAAQ;4BACR,WAAW;4BACX,eAAe,EAAE,CAAC;4BAClB,iBAAiB;4BACjB,KAAK,EAAE,YAAY,CAAC,CAAC,CAAC,KAAK,CAAC;yBAC7B,CAAC;oBACJ,CAAC;oBACD,OAAO;wBACL,QAAQ;wBACR,WAAW;wBACX,eAAe,EAAE,CAAC;wBAClB,iBAAiB;wBACjB,KAAK,EAAE,YAAY,CAAC,GAAG,CAAC,KAAK,CAAC;qBAC/B,CAAC;gBACJ,CAAC;gBACD,WAAW,IAAI,GAAG,CAAC,KAAK,CAAC,QAAQ,GAAG,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC;gBACtD,UAAU,GAAG,iBAAiB,CAAC;YACjC,CAAC;YACD,gEAAgE;YAChE,MAAM,UAAU,GAAc;gBAC5B,GAAI,KAAmB;gBACvB,WAAW,EAAE,MAAM,CAAC,WAAW;gBAC/B,OAAO,EAAE,MAAM,CAAC,OAAO;gBACvB,aAAa,EAAE,UAAU;gBACzB,kBAAkB,EAAE,IAAI,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE;gBAC5C,aAAa,EAAE,IAAI;aACpB,CAAC;YACF,MAAM,CAAC,GAAG,MAAM,UAAU,CAAC,UAAU,CAAC,CAAC;YACvC,IAAI,CAAC,CAAC,GAAG,KAAK,KAAK,EAAE,CAAC;gBACpB,OAAO;oBACL,QAAQ;oBACR,WAAW;oBACX,eAAe,EAAE,CAAC;oBAClB,iBAAiB;oBACjB,KAAK,EAAE,YAAY,CAAC,CAAC,CAAC,KAAK,CAAC;iBAC7B,CAAC;YACJ,CAAC;YACD,KAAK,GAAG,UAAU,CAAC;QACrB,CAAC;IACH,CAAC;IAED,uEAAuE;IACvE,IAAI,eAAe,GAAG,CAAC,CAAC;IACxB,IAAI,UAAU,GAAG,CAAC,IAAI,UAAU,GAAG,CAAC,EAAE,CAAC;QACrC,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,UAAU,GAAG,UAAU,GAAG,CAAC,CAAC,CAAC;QAC3D,IAAI,CAAC;YACH,MAAM,OAAO,GAAyB,EAAE,CAAC;YACzC,MAAM,UAAU,GAAa,EAAE,CAAC;YAChC,IAAI,KAAK,EAAE,MAAM,GAAG,IAAI,IAAI,CAAC,IAAI,CAAC,mBAAmB,CACnD,OAAO,CAAC,IAAI,EACZ,UAAU,EACV,SAAS,EACT,UAAU,CACX,EAAE,CAAC;gBACF,MAAM,QAAQ,GAAG,eAAe,CAAC,GAAG,EAAE;oBACpC,SAAS,EAAE,OAAO,CAAC,IAAI;oBACvB,OAAO,EAAE,UAAU;oBACnB,WAAW,EAAE,MAAM,CAAC,WAAW;oBAC/B,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE;oBACrB,QAAQ,EAAE,IAAI;oBACd,QAAQ,EAAE,IAAI;oBACd,SAAS,EAAE,KAAK;iBACjB,CAAC,CAAC;gBACH,gEAAgE;gBAChE,2DAA2D;gBAC3D,iEAAiE;gBACjE,kDAAkD;gBAClD,IAAI,CAAC,aAAa,CAAC,QAAQ,CAAC,KAAK,EAAE,EAAE,CAAC,EAAE,CAAC;oBACvC,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;oBACvB,UAAU,CAAC,IAAI,CAAC,aAAa,CAAC,MAAM,CAAC,WAAW,EAAE,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC;gBAC9D,CAAC;qBAAM,CAAC;oBACN,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;oBACvB,UAAU,CAAC,IAAI,CAAC,aAAa,CAAC,MAAM,CAAC,WAAW,EAAE,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC;gBAC9D,CAAC;YACH,CAAC;YACD,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBACvB,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,SAAS,EAAE,QAAQ,EAAE,OAAO,EAAE,UAAU,CAAC,CAAC;gBACtF,IAAI,GAAG,CAAC,GAAG,KAAK,KAAK,EAAE,CAAC;oBACtB,uEAAuE;oBACvE,OAAO;wBACL,QAAQ;wBACR,WAAW;wBACX,eAAe,EAAE,CAAC;wBAClB,iBAAiB;wBACjB,KAAK,EAAE,YAAY,CAAC,GAAG,CAAC,KAAK,CAAC;qBAC/B,CAAC;gBACJ,CAAC;gBACD,eAAe,GAAG,OAAO,CAAC,MAAM,CAAC;YACnC,CAAC;QACH,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,sCAAsC;YACtC,MAAM,CAAC,GAAG,SAAS,CAAC,OAAO,CAAC,IAAI,EAAE,qBAAqB,EAAE,KAAK,CAAC,CAAC;YAChE,OAAO;gBACL,QAAQ;gBACR,WAAW;gBACX,eAAe,EAAE,CAAC;gBAClB,iBAAiB;gBACjB,KAAK,EAAE,YAAY,CAAC,CAAC,CAAC;aACvB,CAAC;QACJ,CAAC;IACH,CAAC;IAED,OAAO,EAAE,QAAQ,EAAE,WAAW,EAAE,eAAe,EAAE,iBAAiB,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;AACpF,CAAC,CAAC;AAEF;;;GAGG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,KAAK,EAC9B,IAAc,EACd,OAAgB,EAChB,OAAwB,EACH,EAAE;IACvB,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE,CAAC;IAC3C,MAAM,SAAS,GAAG,iBAAiB,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IAClD,MAAM,WAAW,GAAG,cAAc,CAAC,OAAO,CAAC;QACzC,CAAC,CAAC,OAAO,CAAC,KAAK;QACf,CAAC,CAAC,CAAC,OAAO,CAAC,SAAS,IAAI,OAAO,CAAC,KAAK,CAAC,CAAC;IAEzC,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,aAAa,CAAC,SAAS,EAAE,WAAW,CAAC,CAAC;IACnE,IAAI,GAAG,CAAC,GAAG,KAAK,KAAK,EAAE,CAAC;QACtB,OAAO;YACL,SAAS,EAAE,OAAO,CAAC,IAAI;YACvB,OAAO,EAAE,EAAE;YACX,aAAa,EAAE,CAAC;YAChB,SAAS;YACT,UAAU,EAAE,IAAI,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE;YACpC,KAAK,EAAE,YAAY,CAAC,GAAG,CAAC,KAAK,CAAC;SAC/B,CAAC;IACJ,CAAC;IAED,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3D,IAAI,QAAQ,CAAC,GAAG,KAAK,KAAK,EAAE,CAAC;QAC3B,OAAO;YACL,SAAS,EAAE,OAAO,CAAC,IAAI;YACvB,OAAO,EAAE,EAAE;YACX,aAAa,EAAE,CAAC;YAChB,SAAS;YACT,UAAU,EAAE,IAAI,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE;YACpC,KAAK,EAAE,YAAY,CAAC,QAAQ,CAAC,KAAK,CAAC;SACpC,CAAC;IACJ,CAAC;IACD,MAAM,UAAU,GAAG,QAAQ,CAAC,KAAK,CAAC;IAClC,MAAM,eAAe,GAAG,CAAC,GAAG,EAAE;QAC5B,IAAI,OAAO,CAAC,OAAO,KAAK,SAAS,IAAI,OAAO,CAAC,OAAO,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,UAAU,CAAC;QACrF,MAAM,KAAK,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;QACvC,OAAO,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;IACrD,CAAC,CAAC,EAAE,CAAC;IAEL,MAAM,OAAO,GAAuB,EAAE,CAAC;IACvC,KAAK,MAAM,CAAC,IAAI,eAAe,EAAE,CAAC;QAChC,MAAM,CAAC,GAAG,MAAM,UAAU,CAAC,IAAI,EAAE,OAAO,EAAE,SAAS,EAAE,CAAC,CAAC,IAAI,EAAE,OAAO,CAAC,qBAAqB,CAAC,CAAC;QAC5F,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,kDAAkD;IAClD,IAAI,aAAa,GAAG,CAAC,CAAC;IACtB,MAAM,aAAa,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,SAAS,CAAC,CAAC;IAC9D,IAAI,aAAa,CAAC,GAAG,KAAK,IAAI,EAAE,CAAC;QAC/B,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC,eAAe,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;QAC9D,KAAK,MAAM,EAAE,IAAI,aAAa,CAAC,KAAK,EAAE,CAAC;YACrC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC;gBAChC,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,SAAS,EAAE,EAAE,CAAC,QAAQ,CAAC,CAAC;gBAClE,IAAI,GAAG,CAAC,GAAG,KAAK,IAAI;oBAAE,aAAa,IAAI,CAAC,CAAC;YAC3C,CAAC;QACH,CAAC;IACH,CAAC;IAED,MAAM,UAAU,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE,CAAC;IAC5C,MAAM,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,KAAK,IAAI,CAAC,EAAE,KAAK,IAAI,IAAI,CAAC;IACxE,OAAO;QACL,SAAS,EAAE,OAAO,CAAC,IAAI;QACvB,OAAO,EAAE,OAAO;QAChB,aAAa;QACb,SAAS;QACT,UAAU;QACV,KAAK,EAAE,UAAU;KAClB,CAAC;AACJ,CAAC,CAAC;AAEF,uBAAuB;AACvB,MAAM,CAAC,MAAM,QAAQ,GAAG,EAAE,GAAG,EAAE,CAAC"}
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Interval-based watcher.
3
+ *
4
+ * Reliability properties:
5
+ * - Per-account in-flight lock: a tick that runs longer than the
6
+ * interval will not be doubled-up. The next interval simply skips
7
+ * the busy account (logged) -- it never queues.
8
+ * - Hard floor of `MIN_TICK_INTERVAL_MS` per spec ("no more often
9
+ * than 1 minute"). Configured intervals below the floor are clamped.
10
+ * - Status snapshot exposes per-account / per-folder progress for
11
+ * `get_watcher_status`.
12
+ * - Errors are caught at the per-account boundary and recorded into
13
+ * the snapshot; one bad account does not stop the others.
14
+ */
15
+ import type { Account, WatcherSettings } from "../config/schema.js";
16
+ import { type SyncDeps, type SyncReport } from "./sync.js";
17
+ export type WatcherFolderStatus = {
18
+ readonly folderId: string;
19
+ readonly newMessages: number;
20
+ readonly reconciledFlags: number;
21
+ readonly uidValidityRolled: boolean;
22
+ readonly error: string | null;
23
+ };
24
+ export type WatcherAccountStatus = {
25
+ readonly accountId: string;
26
+ readonly currentlySyncing: boolean;
27
+ readonly lastTickStartedAt: string | null;
28
+ readonly lastTickFinishedAt: string | null;
29
+ readonly lastTickError: string | null;
30
+ readonly folders: readonly WatcherFolderStatus[];
31
+ };
32
+ export type WatcherStatus = {
33
+ readonly running: boolean;
34
+ readonly intervalMs: number;
35
+ readonly accounts: readonly WatcherAccountStatus[];
36
+ };
37
+ export type Watcher = {
38
+ readonly start: () => void;
39
+ readonly stop: () => Promise<void>;
40
+ readonly status: () => WatcherStatus;
41
+ readonly runOnceNow: (accountId?: string) => Promise<readonly SyncReport[]>;
42
+ };
43
+ export declare const buildWatcher: (deps: SyncDeps, accounts: readonly Account[], watcher: WatcherSettings, log: (msg: string, extra?: Readonly<Record<string, unknown>>) => void) => Watcher;
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Interval-based watcher.
3
+ *
4
+ * Reliability properties:
5
+ * - Per-account in-flight lock: a tick that runs longer than the
6
+ * interval will not be doubled-up. The next interval simply skips
7
+ * the busy account (logged) -- it never queues.
8
+ * - Hard floor of `MIN_TICK_INTERVAL_MS` per spec ("no more often
9
+ * than 1 minute"). Configured intervals below the floor are clamped.
10
+ * - Status snapshot exposes per-account / per-folder progress for
11
+ * `get_watcher_status`.
12
+ * - Errors are caught at the per-account boundary and recorded into
13
+ * the snapshot; one bad account does not stop the others.
14
+ */
15
+ import { clampTickInterval } from "./backoff.js";
16
+ import { runSyncOnce } from "./sync.js";
17
+ const reportToStatus = (rep) => ({
18
+ accountId: rep.accountId,
19
+ currentlySyncing: false,
20
+ lastTickStartedAt: rep.startedAt,
21
+ lastTickFinishedAt: rep.finishedAt,
22
+ lastTickError: rep.error,
23
+ folders: rep.folders.map((f) => ({
24
+ folderId: f.folderId,
25
+ newMessages: f.newMessages,
26
+ reconciledFlags: f.reconciledFlags,
27
+ uidValidityRolled: f.uidValidityRolled,
28
+ error: f.error,
29
+ })),
30
+ });
31
+ export const buildWatcher = (deps, accounts, watcher, log) => {
32
+ const intervalMs = clampTickInterval(watcher.interval_ms);
33
+ const inFlight = new Map();
34
+ const lastStatus = new Map();
35
+ for (const a of accounts) {
36
+ lastStatus.set(a.name, {
37
+ accountId: a.name,
38
+ currentlySyncing: false,
39
+ lastTickStartedAt: null,
40
+ lastTickFinishedAt: null,
41
+ lastTickError: null,
42
+ folders: [],
43
+ });
44
+ }
45
+ let timer = null;
46
+ let stopped = false;
47
+ const runForAccount = async (acct) => {
48
+ const existing = inFlight.get(acct.name);
49
+ if (existing !== undefined) {
50
+ log("watcher.skip-overlap", { accountId: acct.name });
51
+ return existing;
52
+ }
53
+ const cur = lastStatus.get(acct.name);
54
+ if (cur !== undefined) {
55
+ lastStatus.set(acct.name, { ...cur, currentlySyncing: true });
56
+ }
57
+ const p = (async () => {
58
+ try {
59
+ const rep = await runSyncOnce(deps, acct, watcher);
60
+ lastStatus.set(acct.name, reportToStatus(rep));
61
+ if (rep.error !== null)
62
+ log("watcher.tick-err", { accountId: acct.name, error: rep.error });
63
+ else
64
+ log("watcher.tick-ok", {
65
+ accountId: acct.name,
66
+ folders: rep.folders.length,
67
+ newMessages: rep.folders.reduce((s, f) => s + f.newMessages, 0),
68
+ });
69
+ return rep;
70
+ }
71
+ catch (cause) {
72
+ const msg = cause?.message ?? String(cause);
73
+ const finishedAt = deps.now().toISOString();
74
+ const rep = {
75
+ accountId: acct.name,
76
+ folders: [],
77
+ prunedFolders: 0,
78
+ startedAt: cur?.lastTickStartedAt ?? finishedAt,
79
+ finishedAt,
80
+ error: msg,
81
+ };
82
+ lastStatus.set(acct.name, reportToStatus(rep));
83
+ log("watcher.tick-throw", { accountId: acct.name, error: msg });
84
+ return rep;
85
+ }
86
+ })();
87
+ inFlight.set(acct.name, p);
88
+ try {
89
+ return await p;
90
+ }
91
+ finally {
92
+ inFlight.delete(acct.name);
93
+ }
94
+ };
95
+ const tick = async () => {
96
+ if (stopped)
97
+ return;
98
+ // Run all accounts in parallel; in-flight lock per account makes overlap safe.
99
+ const work = accounts.map((a) => runForAccount(a));
100
+ await Promise.all(work);
101
+ };
102
+ const start = () => {
103
+ if (timer !== null)
104
+ return;
105
+ if (!watcher.enabled) {
106
+ log("watcher.disabled");
107
+ return;
108
+ }
109
+ log("watcher.start", { intervalMs, accounts: accounts.map((a) => a.name) });
110
+ // Fire one tick immediately on boot so first sync does not wait a full interval.
111
+ void tick();
112
+ timer = setInterval(() => {
113
+ void tick();
114
+ }, intervalMs);
115
+ };
116
+ const stop = async () => {
117
+ stopped = true;
118
+ if (timer !== null) {
119
+ clearInterval(timer);
120
+ timer = null;
121
+ }
122
+ // Wait for any in-flight ticks so we never tear state down mid-batch.
123
+ await Promise.all(Array.from(inFlight.values()));
124
+ };
125
+ const status = () => ({
126
+ running: timer !== null,
127
+ intervalMs,
128
+ accounts: Array.from(lastStatus.values()),
129
+ });
130
+ const runOnceNow = async (accountId) => {
131
+ if (accountId !== undefined) {
132
+ const acct = accounts.find((a) => a.name === accountId);
133
+ if (acct === undefined)
134
+ return [];
135
+ return [await runForAccount(acct)];
136
+ }
137
+ return await Promise.all(accounts.map((a) => runForAccount(a)));
138
+ };
139
+ return { start, stop, status, runOnceNow };
140
+ };
141
+ //# sourceMappingURL=watcher.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"watcher.js","sourceRoot":"","sources":["../../src/sync/watcher.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,41 @@
1
+ /**
2
+ * MCP tool: bulk_action -- batch operation on a UID list (max 100).
3
+ *
4
+ * Actions:
5
+ * - mark_read / mark_unread: STORE +/-FLAGS \\Seen
6
+ * - flag / unflag: STORE +/-FLAGS \\Flagged
7
+ * - move: MOVE to targetMailbox (refuses virtual sources)
8
+ * - delete: EXPUNGE-style delete
9
+ *
10
+ * Returns per-UID success/failure detail so callers can recover.
11
+ */
12
+ import { z } from "zod";
13
+ import { type AppError } from "../domain/errors.js";
14
+ import { type Result } from "../domain/result.js";
15
+ import type { ImapPool } from "../imap/pool.js";
16
+ export declare const bulkActionInput: z.ZodObject<{
17
+ account: z.ZodString;
18
+ sourceMailbox: z.ZodString;
19
+ uids: z.ZodArray<z.ZodNumber, "many">;
20
+ action: z.ZodEnum<["mark_read", "mark_unread", "flag", "unflag", "move", "delete"]>;
21
+ targetMailbox: z.ZodOptional<z.ZodString>;
22
+ }, "strict", z.ZodTypeAny, {
23
+ account: string;
24
+ sourceMailbox: string;
25
+ uids: number[];
26
+ action: "delete" | "mark_read" | "mark_unread" | "flag" | "unflag" | "move";
27
+ targetMailbox?: string | undefined;
28
+ }, {
29
+ account: string;
30
+ sourceMailbox: string;
31
+ uids: number[];
32
+ action: "delete" | "mark_read" | "mark_unread" | "flag" | "unflag" | "move";
33
+ targetMailbox?: string | undefined;
34
+ }>;
35
+ export type BulkActionInput = z.infer<typeof bulkActionInput>;
36
+ export type BulkActionResult = {
37
+ readonly action: BulkActionInput["action"];
38
+ readonly succeeded: number;
39
+ readonly failedUids: readonly number[];
40
+ };
41
+ export declare const bulkAction: (pool: ImapPool, input: BulkActionInput) => Promise<Result<AppError, BulkActionResult>>;