@bobfrankston/iflow-direct 0.1.42 → 0.1.43

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.
package/imap-compat.d.ts CHANGED
@@ -106,6 +106,42 @@ export declare class CompatImapClient {
106
106
  /** Cached CAPABILITY set parsed at connect/login. Callers gate
107
107
  * optional features (NOTIFY, QRESYNC, MOVE, ...) on this. */
108
108
  getCapabilities(): Set<string>;
109
+ /** RFC 5161 ENABLE — activates an IMAP extension for the remainder of
110
+ * the session. QRESYNC must be enabled before any SELECT for
111
+ * `* VANISHED` responses + automatic `MODSEQ` to start flowing.
112
+ * Returns the set of extensions the server confirmed are active. */
113
+ enable(extensions: string[]): Promise<Set<string>>;
114
+ /** Convenience: enable QRESYNC if the server advertises it. Returns
115
+ * true if QRESYNC is active on the session afterwards. Idempotent —
116
+ * safe to call once per connection at connect time. */
117
+ enableQresync(): Promise<boolean>;
118
+ /** Fast resync of a mailbox via RFC 7162 QRESYNC. Caller supplies the
119
+ * `uidValidity` and last-seen `modSeq` from its prior visit; the
120
+ * server replies with `* VANISHED` for UIDs expunged since then and
121
+ * unsolicited `* FETCH` for state changes (flag updates etc.) since
122
+ * then, plus the current `HIGHESTMODSEQ` for the caller to persist
123
+ * as its new watermark.
124
+ *
125
+ * Returns:
126
+ * - `uidValidityChanged`: server's UIDVALIDITY no longer matches; the
127
+ * caller's UID set is stale and MUST be full-resynced.
128
+ * - `vanishedUids`: authoritative deletion list. No client-side diff.
129
+ * - `changedMessages`: state-change FETCH responses since the prior
130
+ * modSeq — each carries `uid`, `flags`, and `modSeq` (and possibly
131
+ * other FETCH items the caller asked for).
132
+ * - `newHighestModSeq`: persist this for the next resync.
133
+ *
134
+ * Pre-requisite: `enableQresync()` returned true once on the session.
135
+ * If the server doesn't support QRESYNC, fall back to the older
136
+ * `fetchMessagesSinceUid` / set-diff path. */
137
+ resyncFolder(mailbox: string, uidValidity: number, modSeq: number): Promise<{
138
+ uidValidityChanged: boolean;
139
+ vanishedUids: number[];
140
+ changedMessages: FetchedMessage[];
141
+ newHighestModSeq: number | undefined;
142
+ exists: number;
143
+ uidNext: number;
144
+ }>;
109
145
  /** Watch a mailbox for new messages (IDLE). Optionally engage RFC 5465
110
146
  * NOTIFY so the server also pushes STATUS responses for non-selected
111
147
  * mailboxes named in `opts.notifySpec` — `opts.onMailboxStatus` fires
package/imap-compat.js CHANGED
@@ -279,6 +279,61 @@ export class CompatImapClient {
279
279
  getCapabilities() {
280
280
  return this.native.getCapabilities();
281
281
  }
282
+ /** RFC 5161 ENABLE — activates an IMAP extension for the remainder of
283
+ * the session. QRESYNC must be enabled before any SELECT for
284
+ * `* VANISHED` responses + automatic `MODSEQ` to start flowing.
285
+ * Returns the set of extensions the server confirmed are active. */
286
+ async enable(extensions) {
287
+ await this.ensureConnected();
288
+ return this.native.enable(extensions);
289
+ }
290
+ /** Convenience: enable QRESYNC if the server advertises it. Returns
291
+ * true if QRESYNC is active on the session afterwards. Idempotent —
292
+ * safe to call once per connection at connect time. */
293
+ async enableQresync() {
294
+ const caps = this.getCapabilities();
295
+ if (!caps.has("QRESYNC"))
296
+ return false;
297
+ const enabled = await this.enable(["QRESYNC"]);
298
+ return enabled.has("QRESYNC");
299
+ }
300
+ /** Fast resync of a mailbox via RFC 7162 QRESYNC. Caller supplies the
301
+ * `uidValidity` and last-seen `modSeq` from its prior visit; the
302
+ * server replies with `* VANISHED` for UIDs expunged since then and
303
+ * unsolicited `* FETCH` for state changes (flag updates etc.) since
304
+ * then, plus the current `HIGHESTMODSEQ` for the caller to persist
305
+ * as its new watermark.
306
+ *
307
+ * Returns:
308
+ * - `uidValidityChanged`: server's UIDVALIDITY no longer matches; the
309
+ * caller's UID set is stale and MUST be full-resynced.
310
+ * - `vanishedUids`: authoritative deletion list. No client-side diff.
311
+ * - `changedMessages`: state-change FETCH responses since the prior
312
+ * modSeq — each carries `uid`, `flags`, and `modSeq` (and possibly
313
+ * other FETCH items the caller asked for).
314
+ * - `newHighestModSeq`: persist this for the next resync.
315
+ *
316
+ * Pre-requisite: `enableQresync()` returned true once on the session.
317
+ * If the server doesn't support QRESYNC, fall back to the older
318
+ * `fetchMessagesSinceUid` / set-diff path. */
319
+ async resyncFolder(mailbox, uidValidity, modSeq) {
320
+ await this.ensureConnected();
321
+ const result = await this.native.select(mailbox, { uidValidity, modSeq });
322
+ // Untagged FETCH responses emitted during a QRESYNC SELECT are
323
+ // pre-parsed by the native layer into NativeFetchedMessage objects;
324
+ // expose them as FetchedMessage to compat callers. (The native
325
+ // select() doesn't currently surface these — that wiring is in
326
+ // imap-native.ts:select via the FETCH-during-select case.)
327
+ const changedMessages = result.changedMessages?.map((m) => new FetchedMessage(m)) || [];
328
+ return {
329
+ uidValidityChanged: result.uidValidityChanged,
330
+ vanishedUids: result.vanishedUids,
331
+ changedMessages,
332
+ newHighestModSeq: result.highestModSeq,
333
+ exists: result.exists,
334
+ uidNext: result.uidNext,
335
+ };
336
+ }
282
337
  /** Watch a mailbox for new messages (IDLE). Optionally engage RFC 5465
283
338
  * NOTIFY so the server also pushes STATUS responses for non-selected
284
339
  * mailboxes named in `opts.notifySpec` — `opts.onMailboxStatus` fires
package/imap-native.d.ts CHANGED
@@ -30,6 +30,11 @@ export interface NativeFetchedMessage {
30
30
  flagged: boolean;
31
31
  answered: boolean;
32
32
  draft: boolean;
33
+ /** Per-message modification sequence (RFC 7162). Present when the
34
+ * server is CONDSTORE/QRESYNC-capable and the client either has
35
+ * ENABLE QRESYNC active or asked for MODSEQ explicitly. Used by the
36
+ * caller to track its own `last_modseq` watermark per folder. */
37
+ modSeq?: number;
33
38
  }
34
39
  export interface NativeFolder {
35
40
  path: string;
package/imap-native.js CHANGED
@@ -1336,6 +1336,14 @@ export class NativeImapClient {
1336
1336
  const sizeMatch = r.text.match(/RFC822\.SIZE\s+(\d+)/);
1337
1337
  if (sizeMatch)
1338
1338
  msg.size = parseInt(sizeMatch[1]);
1339
+ // Extract MODSEQ (RFC 7162). Format: `MODSEQ (12345)`. Server
1340
+ // emits this when the session has ENABLE QRESYNC/CONDSTORE
1341
+ // active OR when the client explicitly asks for MODSEQ in the
1342
+ // FETCH command. Track on every FETCH so the caller can update
1343
+ // its `last_modseq` watermark.
1344
+ const modSeqMatch = r.text.match(/MODSEQ\s+\((\d+)\)/);
1345
+ if (modSeqMatch)
1346
+ msg.modSeq = parseInt(modSeqMatch[1]);
1339
1347
  // Extract INTERNALDATE
1340
1348
  const dateMatch = r.text.match(/INTERNALDATE\s+"([^"]+)"/);
1341
1349
  if (dateMatch)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/iflow-direct",
3
- "version": "0.1.42",
3
+ "version": "0.1.43",
4
4
  "description": "Direct IMAP client — transport-agnostic, no Node.js dependencies, browser-ready",
5
5
  "main": "index.js",
6
6
  "types": "index.ts",