@bobfrankston/iflow-direct 0.1.42 → 0.1.44

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.
@@ -35,6 +35,7 @@ export declare class FetchedMessage implements NativeFetchedMessage {
35
35
  flagged: boolean;
36
36
  answered: boolean;
37
37
  draft: boolean;
38
+ modSeq?: number;
38
39
  parsedHeader: string[][];
39
40
  headerSet: HeaderMap;
40
41
  constructor(init: NativeFetchedMessage);
@@ -40,6 +40,7 @@ export class FetchedMessage {
40
40
  flagged;
41
41
  answered;
42
42
  draft;
43
+ modSeq;
43
44
  // Parsed-header state
44
45
  parsedHeader;
45
46
  headerSet;
package/imap-compat.d.ts CHANGED
@@ -106,6 +106,52 @@ 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
+ /** Snapshot of the most recently SELECTed mailbox's info. Callers use this
110
+ * to read `highestModSeq` after any operation that did a SELECT — useful
111
+ * for seeding the QRESYNC modSeq watermark on the very first sync of a
112
+ * folder (before the QRESYNC path is eligible). */
113
+ getCurrentMailboxInfo(): {
114
+ uidValidity: number;
115
+ uidNext: number;
116
+ exists: number;
117
+ highestModSeq?: number;
118
+ };
119
+ /** RFC 5161 ENABLE — activates an IMAP extension for the remainder of
120
+ * the session. QRESYNC must be enabled before any SELECT for
121
+ * `* VANISHED` responses + automatic `MODSEQ` to start flowing.
122
+ * Returns the set of extensions the server confirmed are active. */
123
+ enable(extensions: string[]): Promise<Set<string>>;
124
+ /** Convenience: enable QRESYNC if the server advertises it. Returns
125
+ * true if QRESYNC is active on the session afterwards. Idempotent —
126
+ * safe to call once per connection at connect time. */
127
+ enableQresync(): Promise<boolean>;
128
+ /** Fast resync of a mailbox via RFC 7162 QRESYNC. Caller supplies the
129
+ * `uidValidity` and last-seen `modSeq` from its prior visit; the
130
+ * server replies with `* VANISHED` for UIDs expunged since then and
131
+ * unsolicited `* FETCH` for state changes (flag updates etc.) since
132
+ * then, plus the current `HIGHESTMODSEQ` for the caller to persist
133
+ * as its new watermark.
134
+ *
135
+ * Returns:
136
+ * - `uidValidityChanged`: server's UIDVALIDITY no longer matches; the
137
+ * caller's UID set is stale and MUST be full-resynced.
138
+ * - `vanishedUids`: authoritative deletion list. No client-side diff.
139
+ * - `changedMessages`: state-change FETCH responses since the prior
140
+ * modSeq — each carries `uid`, `flags`, and `modSeq` (and possibly
141
+ * other FETCH items the caller asked for).
142
+ * - `newHighestModSeq`: persist this for the next resync.
143
+ *
144
+ * Pre-requisite: `enableQresync()` returned true once on the session.
145
+ * If the server doesn't support QRESYNC, fall back to the older
146
+ * `fetchMessagesSinceUid` / set-diff path. */
147
+ resyncFolder(mailbox: string, uidValidity: number, modSeq: number): Promise<{
148
+ uidValidityChanged: boolean;
149
+ vanishedUids: number[];
150
+ changedMessages: FetchedMessage[];
151
+ newHighestModSeq: number | undefined;
152
+ exists: number;
153
+ uidNext: number;
154
+ }>;
109
155
  /** Watch a mailbox for new messages (IDLE). Optionally engage RFC 5465
110
156
  * NOTIFY so the server also pushes STATUS responses for non-selected
111
157
  * mailboxes named in `opts.notifySpec` — `opts.onMailboxStatus` fires
package/imap-compat.js CHANGED
@@ -279,6 +279,64 @@ export class CompatImapClient {
279
279
  getCapabilities() {
280
280
  return this.native.getCapabilities();
281
281
  }
282
+ /** Snapshot of the most recently SELECTed mailbox's info. Callers use this
283
+ * to read `highestModSeq` after any operation that did a SELECT — useful
284
+ * for seeding the QRESYNC modSeq watermark on the very first sync of a
285
+ * folder (before the QRESYNC path is eligible). */
286
+ getCurrentMailboxInfo() {
287
+ const info = this.native.getMailboxInfo?.();
288
+ return info || { uidValidity: 0, uidNext: 0, exists: 0 };
289
+ }
290
+ /** RFC 5161 ENABLE — activates an IMAP extension for the remainder of
291
+ * the session. QRESYNC must be enabled before any SELECT for
292
+ * `* VANISHED` responses + automatic `MODSEQ` to start flowing.
293
+ * Returns the set of extensions the server confirmed are active. */
294
+ async enable(extensions) {
295
+ await this.ensureConnected();
296
+ return this.native.enable(extensions);
297
+ }
298
+ /** Convenience: enable QRESYNC if the server advertises it. Returns
299
+ * true if QRESYNC is active on the session afterwards. Idempotent —
300
+ * safe to call once per connection at connect time. */
301
+ async enableQresync() {
302
+ const caps = this.getCapabilities();
303
+ if (!caps.has("QRESYNC"))
304
+ return false;
305
+ const enabled = await this.enable(["QRESYNC"]);
306
+ return enabled.has("QRESYNC");
307
+ }
308
+ /** Fast resync of a mailbox via RFC 7162 QRESYNC. Caller supplies the
309
+ * `uidValidity` and last-seen `modSeq` from its prior visit; the
310
+ * server replies with `* VANISHED` for UIDs expunged since then and
311
+ * unsolicited `* FETCH` for state changes (flag updates etc.) since
312
+ * then, plus the current `HIGHESTMODSEQ` for the caller to persist
313
+ * as its new watermark.
314
+ *
315
+ * Returns:
316
+ * - `uidValidityChanged`: server's UIDVALIDITY no longer matches; the
317
+ * caller's UID set is stale and MUST be full-resynced.
318
+ * - `vanishedUids`: authoritative deletion list. No client-side diff.
319
+ * - `changedMessages`: state-change FETCH responses since the prior
320
+ * modSeq — each carries `uid`, `flags`, and `modSeq` (and possibly
321
+ * other FETCH items the caller asked for).
322
+ * - `newHighestModSeq`: persist this for the next resync.
323
+ *
324
+ * Pre-requisite: `enableQresync()` returned true once on the session.
325
+ * If the server doesn't support QRESYNC, fall back to the older
326
+ * `fetchMessagesSinceUid` / set-diff path. */
327
+ async resyncFolder(mailbox, uidValidity, modSeq) {
328
+ await this.ensureConnected();
329
+ const result = await this.native.select(mailbox, { uidValidity, modSeq });
330
+ const changedMessages = result.changedMessages.map(m => new FetchedMessage(m));
331
+ return {
332
+ uidValidityChanged: result.uidValidityChanged,
333
+ vanishedUids: result.vanishedUids,
334
+ changedMessages,
335
+ newHighestModSeq: result.highestModSeq,
336
+ exists: result.exists,
337
+ uidNext: result.uidNext,
338
+ };
339
+ }
282
340
  /** Watch a mailbox for new messages (IDLE). Optionally engage RFC 5465
283
341
  * NOTIFY so the server also pushes STATUS responses for non-selected
284
342
  * 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;
@@ -62,10 +67,15 @@ export interface QresyncParams {
62
67
  knownUids?: string;
63
68
  }
64
69
  /** Result of a QRESYNC-enabled SELECT. The caller drains `vanishedUids`
65
- * (delete from local store), processes any FETCH responses (upsert state
70
+ * (delete from local store), processes `changedMessages` (upsert state
66
71
  * changes), and saves the new `highestModSeq` for the next resync. */
67
72
  export interface SelectResult extends MailboxInfo {
68
73
  vanishedUids: number[];
74
+ /** Untagged `* FETCH` responses the server emitted during the QRESYNC
75
+ * SELECT — typically flag/MODSEQ changes for messages whose state
76
+ * shifted between the caller's previous modSeq and now. Empty for
77
+ * non-QRESYNC SELECTs. */
78
+ changedMessages: NativeFetchedMessage[];
69
79
  /** True if the server's UIDVALIDITY changed since the caller's last
70
80
  * visit — in that case knownUids/modSeq are invalid and the caller
71
81
  * must full-resync from scratch. */
@@ -141,6 +151,15 @@ export declare class NativeImapClient {
141
151
  * without re-issuing CAPABILITY. Returns a defensive copy so callers
142
152
  * can't mutate internal state. */
143
153
  getCapabilities(): Set<string>;
154
+ /** Snapshot of the currently-SELECTed mailbox's info. Lets the compat
155
+ * client read `highestModSeq` etc. after any select-implying operation
156
+ * (fetchMessagesSinceUid, etc.) without re-issuing SELECT. */
157
+ getMailboxInfo(): {
158
+ uidValidity: number;
159
+ uidNext: number;
160
+ exists: number;
161
+ highestModSeq?: number;
162
+ };
144
163
  private parseCapabilities;
145
164
  logout(): Promise<void>;
146
165
  /** SELECT a mailbox. Optional QRESYNC params trigger RFC 7162 fast resync —
package/imap-native.js CHANGED
@@ -236,6 +236,17 @@ export class NativeImapClient {
236
236
  getCapabilities() {
237
237
  return new Set(this.capabilities);
238
238
  }
239
+ /** Snapshot of the currently-SELECTed mailbox's info. Lets the compat
240
+ * client read `highestModSeq` etc. after any select-implying operation
241
+ * (fetchMessagesSinceUid, etc.) without re-issuing SELECT. */
242
+ getMailboxInfo() {
243
+ return {
244
+ uidValidity: this.mailboxInfo.uidValidity,
245
+ uidNext: this.mailboxInfo.uidNext,
246
+ exists: this.mailboxInfo.exists,
247
+ highestModSeq: this.mailboxInfo.highestModSeq,
248
+ };
249
+ }
239
250
  parseCapabilities(text) {
240
251
  const caps = text.replace(/^CAPABILITY\s*/i, "").split(/\s+/);
241
252
  this.capabilities.clear();
@@ -271,6 +282,10 @@ export class NativeImapClient {
271
282
  this.mailboxInfo.highestModSeq = undefined;
272
283
  const vanishedUids = [];
273
284
  const priorUidValidity = qresync?.uidValidity;
285
+ // Untagged FETCH responses emitted during a QRESYNC SELECT carry
286
+ // state-change deltas the server is volunteering up-front; parse
287
+ // them through the same path as normal FETCH so the shape matches.
288
+ const fetchResponses = responses.filter(r => r.tag === "*" && r.type === "FETCH");
274
289
  for (const r of responses) {
275
290
  if (r.tag !== "*")
276
291
  continue;
@@ -311,9 +326,13 @@ export class NativeImapClient {
311
326
  this.selectedMailbox = mailbox;
312
327
  const uidValidityChanged = priorUidValidity !== undefined
313
328
  && this.mailboxInfo.uidValidity !== priorUidValidity;
329
+ const changedMessages = fetchResponses.length > 0
330
+ ? this.parseFetchResponses(fetchResponses)
331
+ : [];
314
332
  return {
315
333
  ...this.mailboxInfo,
316
334
  vanishedUids,
335
+ changedMessages,
317
336
  uidValidityChanged,
318
337
  };
319
338
  }
@@ -1336,6 +1355,14 @@ export class NativeImapClient {
1336
1355
  const sizeMatch = r.text.match(/RFC822\.SIZE\s+(\d+)/);
1337
1356
  if (sizeMatch)
1338
1357
  msg.size = parseInt(sizeMatch[1]);
1358
+ // Extract MODSEQ (RFC 7162). Format: `MODSEQ (12345)`. Server
1359
+ // emits this when the session has ENABLE QRESYNC/CONDSTORE
1360
+ // active OR when the client explicitly asks for MODSEQ in the
1361
+ // FETCH command. Track on every FETCH so the caller can update
1362
+ // its `last_modseq` watermark.
1363
+ const modSeqMatch = r.text.match(/MODSEQ\s+\((\d+)\)/);
1364
+ if (modSeqMatch)
1365
+ msg.modSeq = parseInt(modSeqMatch[1]);
1339
1366
  // Extract INTERNALDATE
1340
1367
  const dateMatch = r.text.match(/INTERNALDATE\s+"([^"]+)"/);
1341
1368
  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.44",
4
4
  "description": "Direct IMAP client — transport-agnostic, no Node.js dependencies, browser-ready",
5
5
  "main": "index.js",
6
6
  "types": "index.ts",