@bobfrankston/iflow-direct 0.1.41 → 0.1.42

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-native.d.ts CHANGED
@@ -43,6 +43,33 @@ export interface MailboxInfo {
43
43
  uidValidity: number;
44
44
  flags: string[];
45
45
  permanentFlags: string[];
46
+ /** Per RFC 7162. Present when the server is CONDSTORE/QRESYNC-capable
47
+ * and includes [HIGHESTMODSEQ N] in the SELECT-OK response (or after
48
+ * ENABLE QRESYNC). Use as the `since-modseq` argument to subsequent
49
+ * resyncs to ask the server "what changed since this point?" */
50
+ highestModSeq?: number;
51
+ }
52
+ /** Parameters for `SELECT mailbox (QRESYNC (uidvalidity modseq))` per RFC 7162.
53
+ * Caller persists `(uidValidity, modSeq)` per folder; after ENABLE QRESYNC
54
+ * has been issued once on the session, the next SELECT replies with
55
+ * `* VANISHED (EARLIER) <uids>` for every message the server expunged since
56
+ * `modSeq`, plus `* FETCH` for every message whose state changed since then.
57
+ * No client-side UID diffing, no tombstones — the server tells you the
58
+ * authoritative deletions. */
59
+ export interface QresyncParams {
60
+ uidValidity: number;
61
+ modSeq: number;
62
+ knownUids?: string;
63
+ }
64
+ /** Result of a QRESYNC-enabled SELECT. The caller drains `vanishedUids`
65
+ * (delete from local store), processes any FETCH responses (upsert state
66
+ * changes), and saves the new `highestModSeq` for the next resync. */
67
+ export interface SelectResult extends MailboxInfo {
68
+ vanishedUids: number[];
69
+ /** True if the server's UIDVALIDITY changed since the caller's last
70
+ * visit — in that case knownUids/modSeq are invalid and the caller
71
+ * must full-resync from scratch. */
72
+ uidValidityChanged: boolean;
46
73
  }
47
74
  export declare class NativeImapClient {
48
75
  private transport;
@@ -116,7 +143,21 @@ export declare class NativeImapClient {
116
143
  getCapabilities(): Set<string>;
117
144
  private parseCapabilities;
118
145
  logout(): Promise<void>;
119
- select(mailbox: string): Promise<MailboxInfo>;
146
+ /** SELECT a mailbox. Optional QRESYNC params trigger RFC 7162 fast resync —
147
+ * on a re-visit the server replies with `* VANISHED (EARLIER) <uids>` for
148
+ * every UID expunged since the caller's last `modSeq`, plus `* FETCH`
149
+ * for every state change since then. The caller must have called
150
+ * `enable(["QRESYNC"])` once on the session for this to take effect.
151
+ * Returns a `SelectResult` rather than a bare `MailboxInfo` so VANISHED
152
+ * is delivered to the caller. The result is upcast-compatible with
153
+ * `MailboxInfo` for code paths that don't care about VANISHED. */
154
+ select(mailbox: string, qresync?: QresyncParams): Promise<SelectResult>;
155
+ /** RFC 5161 ENABLE. Activates an extension for the rest of the session.
156
+ * QRESYNC requires this once before any SELECT for VANISHED responses
157
+ * to be emitted. Capability-gate at the caller — emitting ENABLE for
158
+ * an unadvertised extension is a no-op on compliant servers but spams
159
+ * the response stream. */
160
+ enable(extensions: string[]): Promise<Set<string>>;
120
161
  examine(mailbox: string): Promise<MailboxInfo>;
121
162
  /** Close the currently selected mailbox */
122
163
  closeMailbox(): Promise<void>;
package/imap-native.js CHANGED
@@ -251,14 +251,26 @@ export class NativeImapClient {
251
251
  this._connected = false;
252
252
  }
253
253
  // ── Mailbox Operations ──
254
- async select(mailbox) {
254
+ /** SELECT a mailbox. Optional QRESYNC params trigger RFC 7162 fast resync —
255
+ * on a re-visit the server replies with `* VANISHED (EARLIER) <uids>` for
256
+ * every UID expunged since the caller's last `modSeq`, plus `* FETCH`
257
+ * for every state change since then. The caller must have called
258
+ * `enable(["QRESYNC"])` once on the session for this to take effect.
259
+ * Returns a `SelectResult` rather than a bare `MailboxInfo` so VANISHED
260
+ * is delivered to the caller. The result is upcast-compatible with
261
+ * `MailboxInfo` for code paths that don't care about VANISHED. */
262
+ async select(mailbox, qresync) {
255
263
  const tag = proto.nextTag();
256
- const responses = await this.sendCommand(tag, proto.selectCommand(tag, mailbox));
264
+ const responses = await this.sendCommand(tag, proto.selectCommand(tag, mailbox, qresync));
257
265
  const tagged = responses.find(r => r.tag === tag);
258
266
  if (!tagged || tagged.type !== "OK") {
259
267
  throw new Error(`SELECT ${mailbox} failed: ${tagged?.text || "unknown"}`);
260
268
  }
261
- // Parse mailbox info from untagged responses
269
+ // Reset transient fields each SELECT so a stale value from a previous
270
+ // mailbox doesn't leak into the new one.
271
+ this.mailboxInfo.highestModSeq = undefined;
272
+ const vanishedUids = [];
273
+ const priorUidValidity = qresync?.uidValidity;
262
274
  for (const r of responses) {
263
275
  if (r.tag !== "*")
264
276
  continue;
@@ -281,10 +293,50 @@ export class NativeImapClient {
281
293
  const permMatch = r.text.match(/PERMANENTFLAGS\s+\(([^)]*)\)/i);
282
294
  if (permMatch)
283
295
  this.mailboxInfo.permanentFlags = permMatch[1].split(/\s+/).filter(Boolean);
296
+ const modSeqMatch = r.text.match(/HIGHESTMODSEQ\s+(\d+)/i);
297
+ if (modSeqMatch)
298
+ this.mailboxInfo.highestModSeq = parseInt(modSeqMatch[1]);
299
+ }
300
+ else if (r.type === "VANISHED") {
301
+ // `* VANISHED (EARLIER) 1:5,8,12` or `* VANISHED 1:5,8,12` —
302
+ // strip the optional `(EARLIER)` modifier and expand the UID
303
+ // set. EARLIER means "deleted before your last sync", which
304
+ // is the QRESYNC case; without it, the message just vanished
305
+ // mid-session (post-EXPUNGE on another client).
306
+ const text = r.text.replace(/^\(EARLIER\)\s*/i, "").trim();
307
+ for (const uid of proto.parseUidSet(text))
308
+ vanishedUids.push(uid);
284
309
  }
285
310
  }
286
311
  this.selectedMailbox = mailbox;
287
- return { ...this.mailboxInfo };
312
+ const uidValidityChanged = priorUidValidity !== undefined
313
+ && this.mailboxInfo.uidValidity !== priorUidValidity;
314
+ return {
315
+ ...this.mailboxInfo,
316
+ vanishedUids,
317
+ uidValidityChanged,
318
+ };
319
+ }
320
+ /** RFC 5161 ENABLE. Activates an extension for the rest of the session.
321
+ * QRESYNC requires this once before any SELECT for VANISHED responses
322
+ * to be emitted. Capability-gate at the caller — emitting ENABLE for
323
+ * an unadvertised extension is a no-op on compliant servers but spams
324
+ * the response stream. */
325
+ async enable(extensions) {
326
+ const tag = proto.nextTag();
327
+ const responses = await this.sendCommand(tag, proto.enableCommand(tag, extensions));
328
+ const tagged = responses.find(r => r.tag === tag);
329
+ if (!tagged || tagged.type !== "OK") {
330
+ throw new Error(`ENABLE ${extensions.join(" ")} failed: ${tagged?.text || "unknown"}`);
331
+ }
332
+ const enabled = new Set();
333
+ for (const r of responses) {
334
+ if (r.tag === "*" && r.type === "ENABLED") {
335
+ for (const ext of r.text.split(/\s+/).filter(Boolean))
336
+ enabled.add(ext.toUpperCase());
337
+ }
338
+ }
339
+ return enabled;
288
340
  }
289
341
  async examine(mailbox) {
290
342
  const tag = proto.nextTag();
@@ -71,8 +71,26 @@ export declare function loginCommand(tag: string, user: string, pass: string): s
71
71
  export declare function xoauth2Command(tag: string, user: string, token: string): string;
72
72
  /** Build LIST command */
73
73
  export declare function listCommand(tag: string, ref?: string, pattern?: string): string;
74
- /** Build SELECT command */
75
- export declare function selectCommand(tag: string, mailbox: string): string;
74
+ /** Build SELECT command. Optional `qresync` triggers RFC 7162 fast resync:
75
+ * the server emits `* VANISHED (EARLIER) <uid-set>` for every UID expunged
76
+ * since `modSeq` and `* FETCH` for every state change since then, in lieu
77
+ * of the caller having to diff UID sets. Capability-gated by caller;
78
+ * requires `ENABLE QRESYNC` already issued on the session. */
79
+ export declare function selectCommand(tag: string, mailbox: string, qresync?: {
80
+ uidValidity: number;
81
+ modSeq: number;
82
+ knownUids?: string;
83
+ }): string;
84
+ /** Build ENABLE command per RFC 5161. Common extensions to enable: QRESYNC,
85
+ * CONDSTORE, UTF8=ACCEPT. Must be issued before any SELECT for the
86
+ * extension to take effect on the session. */
87
+ export declare function enableCommand(tag: string, extensions: string[]): string;
88
+ /** Expand an RFC 3501 UID set (e.g. `"1:5,8,12,20:*"`) into a flat number
89
+ * list. Does NOT resolve `*` — caller passes a known upper bound only
90
+ * when it has one; here `*` is treated as a sentinel and skipped so the
91
+ * call site can decide what to do (typically: ignore — VANISHED never
92
+ * emits `*` because that would be open-ended). */
93
+ export declare function parseUidSet(set: string): number[];
76
94
  /** Build EXAMINE command (read-only SELECT) */
77
95
  export declare function examineCommand(tag: string, mailbox: string): string;
78
96
  /** Build STATUS command */
package/imap-protocol.js CHANGED
@@ -32,9 +32,52 @@ export function xoauth2Command(tag, user, token) {
32
32
  export function listCommand(tag, ref = '""', pattern = '"*"') {
33
33
  return buildCommand(tag, `LIST ${ref} ${pattern}`);
34
34
  }
35
- /** Build SELECT command */
36
- export function selectCommand(tag, mailbox) {
37
- return buildCommand(tag, `SELECT ${quoteMailbox(mailbox)}`);
35
+ /** Build SELECT command. Optional `qresync` triggers RFC 7162 fast resync:
36
+ * the server emits `* VANISHED (EARLIER) <uid-set>` for every UID expunged
37
+ * since `modSeq` and `* FETCH` for every state change since then, in lieu
38
+ * of the caller having to diff UID sets. Capability-gated by caller;
39
+ * requires `ENABLE QRESYNC` already issued on the session. */
40
+ export function selectCommand(tag, mailbox, qresync) {
41
+ if (!qresync)
42
+ return buildCommand(tag, `SELECT ${quoteMailbox(mailbox)}`);
43
+ let params = `${qresync.uidValidity} ${qresync.modSeq}`;
44
+ if (qresync.knownUids)
45
+ params += ` ${qresync.knownUids}`;
46
+ return buildCommand(tag, `SELECT ${quoteMailbox(mailbox)} (QRESYNC (${params}))`);
47
+ }
48
+ /** Build ENABLE command per RFC 5161. Common extensions to enable: QRESYNC,
49
+ * CONDSTORE, UTF8=ACCEPT. Must be issued before any SELECT for the
50
+ * extension to take effect on the session. */
51
+ export function enableCommand(tag, extensions) {
52
+ return buildCommand(tag, `ENABLE ${extensions.join(" ")}`);
53
+ }
54
+ /** Expand an RFC 3501 UID set (e.g. `"1:5,8,12,20:*"`) into a flat number
55
+ * list. Does NOT resolve `*` — caller passes a known upper bound only
56
+ * when it has one; here `*` is treated as a sentinel and skipped so the
57
+ * call site can decide what to do (typically: ignore — VANISHED never
58
+ * emits `*` because that would be open-ended). */
59
+ export function parseUidSet(set) {
60
+ const out = [];
61
+ for (const part of set.split(",")) {
62
+ const trimmed = part.trim();
63
+ if (!trimmed)
64
+ continue;
65
+ const range = trimmed.split(":");
66
+ if (range.length === 1) {
67
+ const n = parseInt(range[0], 10);
68
+ if (Number.isFinite(n))
69
+ out.push(n);
70
+ }
71
+ else if (range.length === 2) {
72
+ const lo = parseInt(range[0], 10);
73
+ const hi = range[1] === "*" ? lo : parseInt(range[1], 10);
74
+ if (!Number.isFinite(lo) || !Number.isFinite(hi) || hi < lo)
75
+ continue;
76
+ for (let u = lo; u <= hi; u++)
77
+ out.push(u);
78
+ }
79
+ }
80
+ return out;
38
81
  }
39
82
  /** Build EXAMINE command (read-only SELECT) */
40
83
  export function examineCommand(tag, mailbox) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/iflow-direct",
3
- "version": "0.1.41",
3
+ "version": "0.1.42",
4
4
  "description": "Direct IMAP client — transport-agnostic, no Node.js dependencies, browser-ready",
5
5
  "main": "index.js",
6
6
  "types": "index.ts",