@bobfrankston/iflow-direct 0.1.68 → 0.1.71
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 +10 -0
- package/imap-compat.js +20 -0
- package/imap-native.d.ts +26 -1
- package/imap-native.js +64 -2
- package/imap-protocol.d.ts +9 -1
- package/imap-protocol.js +21 -0
- package/package.json +1 -1
package/imap-compat.d.ts
CHANGED
|
@@ -65,6 +65,13 @@ export declare class CompatImapClient {
|
|
|
65
65
|
}): Promise<FetchedMessage | null>;
|
|
66
66
|
/** Get message count via STATUS (does not require SELECT) */
|
|
67
67
|
getMessagesCount(mailbox: string): Promise<number>;
|
|
68
|
+
/** Mailbox size in octets + message count. STATUS=SIZE when the server has it, else a
|
|
69
|
+
* size-only UID FETCH. See NativeImapClient.getFolderSize. (2026-09-16, Claude Code) */
|
|
70
|
+
getFolderSize(mailbox: string): Promise<{
|
|
71
|
+
messages: number;
|
|
72
|
+
bytes: number;
|
|
73
|
+
method: "status" | "fetch";
|
|
74
|
+
}>;
|
|
68
75
|
/** Get all UIDs in a mailbox */
|
|
69
76
|
getUids(mailbox: string): Promise<number[]>;
|
|
70
77
|
/** Get UIDs whose INTERNALDATE is on/after `since`. Bounded version of
|
|
@@ -98,6 +105,9 @@ export declare class CompatImapClient {
|
|
|
98
105
|
searchByHeader(mailbox: string, headerName: string, headerValue: string): Promise<number[]>;
|
|
99
106
|
/** Delete a message by UID */
|
|
100
107
|
deleteMessageByUid(mailbox: string, uid: number): Promise<void>;
|
|
108
|
+
/** Delete many messages by UID in one mailbox — chunked STORE + one
|
|
109
|
+
* EXPUNGE (see ImapNative.deleteMessages). 2026-09-16 Claude Code. */
|
|
110
|
+
deleteMessagesByUid(mailbox: string, uids: number[]): Promise<void>;
|
|
101
111
|
/** Move a message between mailboxes (same server) */
|
|
102
112
|
moveMessage(msg: any, fromMailbox: string, toMailbox: string): Promise<void>;
|
|
103
113
|
/** Add flags to a message */
|
package/imap-compat.js
CHANGED
|
@@ -160,6 +160,12 @@ export class CompatImapClient {
|
|
|
160
160
|
await this.ensureConnected();
|
|
161
161
|
return this.native.getMessageCount(mailbox);
|
|
162
162
|
}
|
|
163
|
+
/** Mailbox size in octets + message count. STATUS=SIZE when the server has it, else a
|
|
164
|
+
* size-only UID FETCH. See NativeImapClient.getFolderSize. (2026-09-16, Claude Code) */
|
|
165
|
+
async getFolderSize(mailbox) {
|
|
166
|
+
await this.ensureConnected();
|
|
167
|
+
return this.native.getFolderSize(mailbox);
|
|
168
|
+
}
|
|
163
169
|
/** Get all UIDs in a mailbox */
|
|
164
170
|
async getUids(mailbox) {
|
|
165
171
|
await this.ensureConnected();
|
|
@@ -233,6 +239,20 @@ export class CompatImapClient {
|
|
|
233
239
|
await this.native.deleteMessage(uid);
|
|
234
240
|
await this.native.closeMailbox();
|
|
235
241
|
}
|
|
242
|
+
/** Delete many messages by UID in one mailbox — chunked STORE + one
|
|
243
|
+
* EXPUNGE (see ImapNative.deleteMessages). 2026-09-16 Claude Code. */
|
|
244
|
+
async deleteMessagesByUid(mailbox, uids) {
|
|
245
|
+
if (!uids.length)
|
|
246
|
+
return;
|
|
247
|
+
await this.ensureConnected();
|
|
248
|
+
await this.native.select(mailbox);
|
|
249
|
+
try {
|
|
250
|
+
await this.native.deleteMessages(uids);
|
|
251
|
+
}
|
|
252
|
+
finally {
|
|
253
|
+
await this.native.closeMailbox();
|
|
254
|
+
}
|
|
255
|
+
}
|
|
236
256
|
/** Move a message between mailboxes (same server) */
|
|
237
257
|
async moveMessage(msg, fromMailbox, toMailbox) {
|
|
238
258
|
await this.ensureConnected();
|
package/imap-native.d.ts
CHANGED
|
@@ -192,7 +192,24 @@ export declare class NativeImapClient {
|
|
|
192
192
|
/** Close the currently selected mailbox */
|
|
193
193
|
closeMailbox(): Promise<void>;
|
|
194
194
|
listFolders(): Promise<NativeFolder[]>;
|
|
195
|
-
getStatus(mailbox: string): Promise<proto.StatusData>;
|
|
195
|
+
getStatus(mailbox: string, items?: string[]): Promise<proto.StatusData>;
|
|
196
|
+
/**
|
|
197
|
+
* Total size of a mailbox in octets plus its message count (for imail -sizes; added
|
|
198
|
+
* 2026-09-16 by Claude Code, Fable 5.1, at Bob's direction).
|
|
199
|
+
*
|
|
200
|
+
* Fast path: RFC 8438 `STATUS (MESSAGES SIZE)` — one round trip, no message traffic —
|
|
201
|
+
* when the server advertises STATUS=SIZE (Dovecot, Gmail do).
|
|
202
|
+
* Fallback: EXAMINE (read-only) + `UID FETCH 1:* (UID RFC822.SIZE)` summed here. That
|
|
203
|
+
* is one FETCH line per message but no envelopes or headers, so it stays cheap even
|
|
204
|
+
* on a folder of tens of thousands of messages.
|
|
205
|
+
*
|
|
206
|
+
* `method` says which path produced the number so a log can show it.
|
|
207
|
+
*/
|
|
208
|
+
getFolderSize(mailbox: string): Promise<{
|
|
209
|
+
messages: number;
|
|
210
|
+
bytes: number;
|
|
211
|
+
method: "status" | "fetch";
|
|
212
|
+
}>;
|
|
196
213
|
createMailbox(mailbox: string): Promise<void>;
|
|
197
214
|
deleteMailbox(mailbox: string): Promise<void>;
|
|
198
215
|
renameMailbox(from: string, to: string): Promise<void>;
|
|
@@ -256,6 +273,14 @@ export declare class NativeImapClient {
|
|
|
256
273
|
moveMessage(uid: number, destination: string): Promise<void>;
|
|
257
274
|
/** Delete a message by UID (flag + expunge) */
|
|
258
275
|
deleteMessage(uid: number): Promise<void>;
|
|
276
|
+
/** Delete many messages in the selected mailbox: one `UID STORE <set>
|
|
277
|
+
* +FLAGS.SILENT (\Deleted)` per chunk, then a single EXPUNGE.
|
|
278
|
+
* 2026-09-16 — Claude Code (Fable 5.1), at Bob's direction. Emptying
|
|
279
|
+
* Trash looped deleteMessage() per UID — a STORE and an EXPUNGE round
|
|
280
|
+
* trip for every message — so a large Trash took minutes and the
|
|
281
|
+
* caller's IPC timed out ("mailxapi timeout: emptyFolder"). Chunked so a
|
|
282
|
+
* ten-thousand-UID set never produces a command line the server rejects. */
|
|
283
|
+
deleteMessages(uids: number[], chunkSize?: number): Promise<void>;
|
|
259
284
|
/** Expunge deleted messages */
|
|
260
285
|
expunge(): Promise<void>;
|
|
261
286
|
/** Append a message to a mailbox */
|
package/imap-native.js
CHANGED
|
@@ -469,9 +469,11 @@ export class NativeImapClient {
|
|
|
469
469
|
console.error(` [imap] ${unparsed} LIST responses could not be parsed (${responses.length} total responses)`);
|
|
470
470
|
return folders;
|
|
471
471
|
}
|
|
472
|
-
|
|
472
|
+
// 2026-09-16 — Claude Code (Fable 5.1), at Bob's direction: `items` parameter added so a
|
|
473
|
+
// caller can ask for RFC 8438 SIZE. Default unchanged, so existing callers see no difference.
|
|
474
|
+
async getStatus(mailbox, items = ["MESSAGES", "UIDNEXT", "UNSEEN"]) {
|
|
473
475
|
const tag = proto.nextTag();
|
|
474
|
-
const responses = await this.sendCommand(tag, proto.statusCommand(tag, mailbox,
|
|
476
|
+
const responses = await this.sendCommand(tag, proto.statusCommand(tag, mailbox, items));
|
|
475
477
|
for (const r of responses) {
|
|
476
478
|
if (r.tag === "*" && r.type === "STATUS") {
|
|
477
479
|
const data = proto.parseStatusResponse(r.text);
|
|
@@ -481,6 +483,46 @@ export class NativeImapClient {
|
|
|
481
483
|
}
|
|
482
484
|
return {};
|
|
483
485
|
}
|
|
486
|
+
/**
|
|
487
|
+
* Total size of a mailbox in octets plus its message count (for imail -sizes; added
|
|
488
|
+
* 2026-09-16 by Claude Code, Fable 5.1, at Bob's direction).
|
|
489
|
+
*
|
|
490
|
+
* Fast path: RFC 8438 `STATUS (MESSAGES SIZE)` — one round trip, no message traffic —
|
|
491
|
+
* when the server advertises STATUS=SIZE (Dovecot, Gmail do).
|
|
492
|
+
* Fallback: EXAMINE (read-only) + `UID FETCH 1:* (UID RFC822.SIZE)` summed here. That
|
|
493
|
+
* is one FETCH line per message but no envelopes or headers, so it stays cheap even
|
|
494
|
+
* on a folder of tens of thousands of messages.
|
|
495
|
+
*
|
|
496
|
+
* `method` says which path produced the number so a log can show it.
|
|
497
|
+
*/
|
|
498
|
+
async getFolderSize(mailbox) {
|
|
499
|
+
if (this.capabilities.has("STATUS=SIZE")) {
|
|
500
|
+
const st = await this.getStatus(mailbox, ["MESSAGES", "SIZE"]);
|
|
501
|
+
if (typeof st.size === "number")
|
|
502
|
+
return { messages: st.messages || 0, bytes: st.size, method: "status" };
|
|
503
|
+
// Server claimed STATUS=SIZE but answered without SIZE — fall through to counting.
|
|
504
|
+
}
|
|
505
|
+
const info = await this.examine(mailbox);
|
|
506
|
+
try {
|
|
507
|
+
if (!info.exists)
|
|
508
|
+
return { messages: 0, bytes: 0, method: "fetch" };
|
|
509
|
+
let messages = 0, bytes = 0;
|
|
510
|
+
const tag = proto.nextTag();
|
|
511
|
+
await this.sendCommand(tag, proto.fetchCommand(tag, "1:*", ["UID", "RFC822.SIZE"]), (r) => {
|
|
512
|
+
if (r.tag !== "*" || r.type !== "FETCH")
|
|
513
|
+
return;
|
|
514
|
+
const m = r.text.match(/RFC822\.SIZE\s+(\d+)/);
|
|
515
|
+
if (!m)
|
|
516
|
+
return;
|
|
517
|
+
messages++;
|
|
518
|
+
bytes += parseInt(m[1]);
|
|
519
|
+
});
|
|
520
|
+
return { messages, bytes, method: "fetch" };
|
|
521
|
+
}
|
|
522
|
+
finally {
|
|
523
|
+
await this.closeMailbox();
|
|
524
|
+
}
|
|
525
|
+
}
|
|
484
526
|
async createMailbox(mailbox) {
|
|
485
527
|
const tag = proto.nextTag();
|
|
486
528
|
const responses = await this.sendCommand(tag, proto.createCommand(tag, mailbox));
|
|
@@ -786,6 +828,26 @@ export class NativeImapClient {
|
|
|
786
828
|
await this.addFlags(uid, ["\\Deleted"]);
|
|
787
829
|
await this.expunge();
|
|
788
830
|
}
|
|
831
|
+
/** Delete many messages in the selected mailbox: one `UID STORE <set>
|
|
832
|
+
* +FLAGS.SILENT (\Deleted)` per chunk, then a single EXPUNGE.
|
|
833
|
+
* 2026-09-16 — Claude Code (Fable 5.1), at Bob's direction. Emptying
|
|
834
|
+
* Trash looped deleteMessage() per UID — a STORE and an EXPUNGE round
|
|
835
|
+
* trip for every message — so a large Trash took minutes and the
|
|
836
|
+
* caller's IPC timed out ("mailxapi timeout: emptyFolder"). Chunked so a
|
|
837
|
+
* ten-thousand-UID set never produces a command line the server rejects. */
|
|
838
|
+
async deleteMessages(uids, chunkSize = 1000) {
|
|
839
|
+
if (!uids.length)
|
|
840
|
+
return;
|
|
841
|
+
for (let i = 0; i < uids.length; i += chunkSize) {
|
|
842
|
+
const set = proto.uidSequenceSet(uids.slice(i, i + chunkSize));
|
|
843
|
+
const tag = proto.nextTag();
|
|
844
|
+
const responses = await this.sendCommand(tag, proto.storeCommand(tag, set, "+FLAGS.SILENT", ["\\Deleted"]));
|
|
845
|
+
const tagged = responses.find(r => r.tag === tag);
|
|
846
|
+
if (!tagged || tagged.type !== "OK")
|
|
847
|
+
throw new Error(`STORE +FLAGS \\Deleted (${uids.length} uids) failed: ${tagged?.text || "unknown"}`);
|
|
848
|
+
}
|
|
849
|
+
await this.expunge();
|
|
850
|
+
}
|
|
789
851
|
/** Expunge deleted messages */
|
|
790
852
|
async expunge() {
|
|
791
853
|
const tag = proto.nextTag();
|
package/imap-protocol.d.ts
CHANGED
|
@@ -58,6 +58,9 @@ export interface StatusData {
|
|
|
58
58
|
uidNext?: number;
|
|
59
59
|
uidValidity?: number;
|
|
60
60
|
unseen?: number;
|
|
61
|
+
/** Total mailbox size in octets — RFC 8438 `STATUS (SIZE)`, only when the
|
|
62
|
+
* server advertises STATUS=SIZE and the caller asked for it. */
|
|
63
|
+
size?: number;
|
|
61
64
|
}
|
|
62
65
|
/** Generate a unique command tag */
|
|
63
66
|
export declare function nextTag(): string;
|
|
@@ -104,7 +107,12 @@ export declare function seqFetchCommand(tag: string, range: string, items: strin
|
|
|
104
107
|
/** Build UID SEARCH command */
|
|
105
108
|
export declare function searchCommand(tag: string, criteria: string): string;
|
|
106
109
|
/** Build UID STORE command (set/add/remove flags) */
|
|
107
|
-
|
|
110
|
+
/** `uid` may be a single UID or an RFC 3501 sequence set ("1:500,502") —
|
|
111
|
+
* 2026-09-16 Claude Code (Fable 5.1): needed so one STORE can flag a whole
|
|
112
|
+
* folder for deletion instead of one round trip per message. */
|
|
113
|
+
export declare function storeCommand(tag: string, uid: number | string, action: string, flags: string[]): string;
|
|
114
|
+
/** Compress sorted UIDs into an RFC 3501 sequence set: [1,2,3,7,9,10] → "1:3,7,9:10". */
|
|
115
|
+
export declare function uidSequenceSet(uids: number[]): string;
|
|
108
116
|
/** Build UID COPY command */
|
|
109
117
|
export declare function copyCommand(tag: string, uid: number, destination: string): string;
|
|
110
118
|
/** Build UID MOVE command */
|
package/imap-protocol.js
CHANGED
|
@@ -110,9 +110,25 @@ export function searchCommand(tag, criteria) {
|
|
|
110
110
|
return buildCommand(tag, `UID SEARCH ${criteria}`);
|
|
111
111
|
}
|
|
112
112
|
/** Build UID STORE command (set/add/remove flags) */
|
|
113
|
+
/** `uid` may be a single UID or an RFC 3501 sequence set ("1:500,502") —
|
|
114
|
+
* 2026-09-16 Claude Code (Fable 5.1): needed so one STORE can flag a whole
|
|
115
|
+
* folder for deletion instead of one round trip per message. */
|
|
113
116
|
export function storeCommand(tag, uid, action, flags) {
|
|
114
117
|
return buildCommand(tag, `UID STORE ${uid} ${action} (${flags.join(" ")})`);
|
|
115
118
|
}
|
|
119
|
+
/** Compress sorted UIDs into an RFC 3501 sequence set: [1,2,3,7,9,10] → "1:3,7,9:10". */
|
|
120
|
+
export function uidSequenceSet(uids) {
|
|
121
|
+
const sorted = Array.from(new Set(uids)).sort((a, b) => a - b);
|
|
122
|
+
const parts = [];
|
|
123
|
+
for (let i = 0; i < sorted.length;) {
|
|
124
|
+
let j = i;
|
|
125
|
+
while (j + 1 < sorted.length && sorted[j + 1] === sorted[j] + 1)
|
|
126
|
+
j++;
|
|
127
|
+
parts.push(j > i ? `${sorted[i]}:${sorted[j]}` : String(sorted[i]));
|
|
128
|
+
i = j + 1;
|
|
129
|
+
}
|
|
130
|
+
return parts.join(",");
|
|
131
|
+
}
|
|
116
132
|
/** Build UID COPY command */
|
|
117
133
|
export function copyCommand(tag, uid, destination) {
|
|
118
134
|
return buildCommand(tag, `UID COPY ${uid} ${quoteMailbox(destination)}`);
|
|
@@ -239,6 +255,9 @@ export function parseStatusResponse(text) {
|
|
|
239
255
|
data.uidValidity = val;
|
|
240
256
|
else if (key === "UNSEEN")
|
|
241
257
|
data.unseen = val;
|
|
258
|
+
// 2026-09-16 — Claude Code (Fable 5.1), at Bob's direction: RFC 8438 SIZE, for imail -sizes.
|
|
259
|
+
else if (key === "SIZE")
|
|
260
|
+
data.size = val;
|
|
242
261
|
}
|
|
243
262
|
return data;
|
|
244
263
|
}
|
|
@@ -267,6 +286,8 @@ export function parseStatusResponseFull(text) {
|
|
|
267
286
|
data.uidValidity = val;
|
|
268
287
|
else if (key === "UNSEEN")
|
|
269
288
|
data.unseen = val;
|
|
289
|
+
else if (key === "SIZE")
|
|
290
|
+
data.size = val; // RFC 8438 (2026-09-16)
|
|
270
291
|
}
|
|
271
292
|
return { mailbox, data };
|
|
272
293
|
}
|