@bobfrankston/iflow-direct 0.1.40 → 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 +42 -1
- package/imap-native.js +56 -4
- package/imap-protocol.d.ts +20 -2
- package/imap-protocol.js +65 -6
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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();
|
package/imap-protocol.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
37
|
-
|
|
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) {
|
|
@@ -399,9 +442,25 @@ function parseAddressList(token) {
|
|
|
399
442
|
const parts = tokenizeParenList(group);
|
|
400
443
|
if (parts.length >= 4) {
|
|
401
444
|
const name = decodeImapString(unquote(parts[0]));
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
445
|
+
let mailbox = unquote(parts[2]);
|
|
446
|
+
let host = unquote(parts[3]);
|
|
447
|
+
// Dovecot stamps "MISSING_MAILBOX" / "MISSING_DOMAIN" into the
|
|
448
|
+
// ENVELOPE tuple when the source From: header didn't parse cleanly
|
|
449
|
+
// (RFC 3501 forbids NIL in those slots when any address part is
|
|
450
|
+
// present). Strip the sentinels so we don't render
|
|
451
|
+
// "user@iecc.com@MISSING_DOMAIN" — observed in the wild from
|
|
452
|
+
// Outlook's malformed `From: "name" <"local@domain">` over-quoting.
|
|
453
|
+
if (host === "MISSING_DOMAIN")
|
|
454
|
+
host = "";
|
|
455
|
+
if (mailbox === "MISSING_MAILBOX")
|
|
456
|
+
mailbox = "";
|
|
457
|
+
// Recover from Outlook's quoted-string pathology: when the local
|
|
458
|
+
// part itself contains an "@" the entire mailbox value is already
|
|
459
|
+
// a full address, so don't append @host again. Same hand applies
|
|
460
|
+
// to any server that mis-parses an over-quoted From header.
|
|
461
|
+
const address = mailbox.includes("@")
|
|
462
|
+
? mailbox
|
|
463
|
+
: (mailbox && host ? `${mailbox}@${host}` : mailbox || "");
|
|
405
464
|
addrs.push({ name, address });
|
|
406
465
|
}
|
|
407
466
|
}
|