@bobfrankston/iflow-direct 0.1.26 → 0.1.28
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 +7 -0
- package/imap-compat.js +12 -0
- package/imap-native.d.ts +9 -0
- package/imap-native.js +64 -2
- package/imap-protocol.d.ts +4 -0
- package/imap-protocol.js +6 -0
- package/package.json +3 -3
package/imap-compat.d.ts
CHANGED
|
@@ -51,6 +51,13 @@ export declare class CompatImapClient {
|
|
|
51
51
|
fetchMessageByDate(mailbox: string, start: Date, end?: Date, options?: {
|
|
52
52
|
source?: boolean;
|
|
53
53
|
}, onChunk?: (msgs: FetchedMessage[]) => void): Promise<FetchedMessage[]>;
|
|
54
|
+
/** Fetch the most recent N messages by sequence number — sidesteps
|
|
55
|
+
* SEARCH SINCE which can take minutes on cold Dovecot mailboxes. Used
|
|
56
|
+
* by mailx-imap's first-sync path so the user sees something fast even
|
|
57
|
+
* on a wide folder tree. The 30-day backfill is a follow-up step. */
|
|
58
|
+
fetchLatestN(mailbox: string, n: number, options?: {
|
|
59
|
+
source?: boolean;
|
|
60
|
+
}, onChunk?: (msgs: FetchedMessage[]) => void): Promise<FetchedMessage[]>;
|
|
54
61
|
/** Fetch a single message by UID */
|
|
55
62
|
fetchMessageByUid(mailbox: string, uid: number, options?: {
|
|
56
63
|
source?: boolean;
|
package/imap-compat.js
CHANGED
|
@@ -135,6 +135,18 @@ export class CompatImapClient {
|
|
|
135
135
|
await this.native.closeMailbox();
|
|
136
136
|
return msgs.map(m => new FetchedMessage(m));
|
|
137
137
|
}
|
|
138
|
+
/** Fetch the most recent N messages by sequence number — sidesteps
|
|
139
|
+
* SEARCH SINCE which can take minutes on cold Dovecot mailboxes. Used
|
|
140
|
+
* by mailx-imap's first-sync path so the user sees something fast even
|
|
141
|
+
* on a wide folder tree. The 30-day backfill is a follow-up step. */
|
|
142
|
+
async fetchLatestN(mailbox, n, options, onChunk) {
|
|
143
|
+
await this.ensureConnected();
|
|
144
|
+
await this.native.select(mailbox);
|
|
145
|
+
const chunkCb = onChunk ? (raw) => onChunk(raw.map(m => new FetchedMessage(m))) : undefined;
|
|
146
|
+
const msgs = await this.native.fetchLatestN(n, options, chunkCb);
|
|
147
|
+
await this.native.closeMailbox();
|
|
148
|
+
return msgs.map(m => new FetchedMessage(m));
|
|
149
|
+
}
|
|
138
150
|
/** Fetch a single message by UID */
|
|
139
151
|
async fetchMessageByUid(mailbox, uid, options) {
|
|
140
152
|
await this.ensureConnected();
|
package/imap-native.d.ts
CHANGED
|
@@ -119,6 +119,15 @@ export declare class NativeImapClient {
|
|
|
119
119
|
fetchByDate(since: Date, before?: Date, options?: {
|
|
120
120
|
source?: boolean;
|
|
121
121
|
}, onChunk?: (msgs: NativeFetchedMessage[]) => void): Promise<NativeFetchedMessage[]>;
|
|
122
|
+
/** Fetch the most recent N messages by sequence number — avoids
|
|
123
|
+
* SEARCH SINCE which can take minutes on cold Dovecot mailboxes. The
|
|
124
|
+
* selected mailbox's `EXISTS` count is used to compute the range
|
|
125
|
+
* `(EXISTS-N+1):EXISTS`; sequence FETCH is O(1) on the server because
|
|
126
|
+
* it just reads message slots, no INTERNALDATE walk. Caller must have
|
|
127
|
+
* SELECTed the mailbox first (sequence numbers are session-relative). */
|
|
128
|
+
fetchLatestN(n: number, options?: {
|
|
129
|
+
source?: boolean;
|
|
130
|
+
}, onChunk?: (msgs: NativeFetchedMessage[]) => void): Promise<NativeFetchedMessage[]>;
|
|
122
131
|
/** Fetch a single message by UID */
|
|
123
132
|
fetchMessage(uid: number, options?: {
|
|
124
133
|
source?: boolean;
|
package/imap-native.js
CHANGED
|
@@ -400,26 +400,88 @@ export class NativeImapClient {
|
|
|
400
400
|
since,
|
|
401
401
|
before: before || undefined,
|
|
402
402
|
});
|
|
403
|
+
// SEARCH SINCE on Dovecot can take minutes on a cold mailbox while it
|
|
404
|
+
// (re)builds its date index. Without a "search starting" log, mailx
|
|
405
|
+
// shows nothing for the entire SEARCH window — the user thinks
|
|
406
|
+
// nothing is happening. Bracket the SEARCH with logs so the silence
|
|
407
|
+
// is at least labeled.
|
|
408
|
+
const t0 = Date.now();
|
|
409
|
+
console.log(` [search] SINCE ${since.toISOString().slice(0, 10)}${before ? ` BEFORE ${before.toISOString().slice(0, 10)}` : ""} — running...`);
|
|
403
410
|
const uids = await this.search(criteria);
|
|
411
|
+
console.log(` [search] returned ${uids.length} UIDs in ${Date.now() - t0}ms`);
|
|
404
412
|
if (uids.length === 0)
|
|
405
413
|
return [];
|
|
406
414
|
// Reverse so newest messages (highest UIDs) come first
|
|
407
415
|
uids.reverse();
|
|
408
|
-
// console.log(` [fetch] ${uids.length} UIDs to fetch (newest first)`);
|
|
409
416
|
const allMessages = [];
|
|
410
417
|
let chunkSize = this.fetchChunkSize;
|
|
418
|
+
let chunkIndex = 0;
|
|
411
419
|
for (let i = 0; i < uids.length; i += chunkSize) {
|
|
412
420
|
const chunk = uids.slice(i, i + chunkSize);
|
|
413
421
|
const msgs = await this.fetchMessages(chunk.join(","), options);
|
|
414
422
|
allMessages.push(...msgs);
|
|
415
|
-
|
|
423
|
+
chunkIndex++;
|
|
424
|
+
// Log every 5th chunk + the first one — enough to see progress
|
|
425
|
+
// without spamming. The first-chunk log proves the fetch is alive
|
|
426
|
+
// even when subsequent chunks are slow.
|
|
427
|
+
if (chunkIndex === 1 || chunkIndex % 5 === 0) {
|
|
428
|
+
console.log(` [fetch] ${allMessages.length}/${uids.length} messages (chunk ${chunkIndex})`);
|
|
429
|
+
}
|
|
416
430
|
if (onChunk)
|
|
417
431
|
onChunk(msgs);
|
|
418
432
|
if (chunkSize < this.fetchChunkSizeMax)
|
|
419
433
|
chunkSize = Math.min(chunkSize * 4, this.fetchChunkSizeMax);
|
|
420
434
|
}
|
|
435
|
+
console.log(` [fetch] done — ${allMessages.length} messages in ${Date.now() - t0}ms`);
|
|
421
436
|
return allMessages;
|
|
422
437
|
}
|
|
438
|
+
/** Fetch the most recent N messages by sequence number — avoids
|
|
439
|
+
* SEARCH SINCE which can take minutes on cold Dovecot mailboxes. The
|
|
440
|
+
* selected mailbox's `EXISTS` count is used to compute the range
|
|
441
|
+
* `(EXISTS-N+1):EXISTS`; sequence FETCH is O(1) on the server because
|
|
442
|
+
* it just reads message slots, no INTERNALDATE walk. Caller must have
|
|
443
|
+
* SELECTed the mailbox first (sequence numbers are session-relative). */
|
|
444
|
+
async fetchLatestN(n, options = {}, onChunk) {
|
|
445
|
+
const exists = this.mailboxInfo.exists;
|
|
446
|
+
if (!exists || exists < 1)
|
|
447
|
+
return [];
|
|
448
|
+
const start = Math.max(1, exists - n + 1);
|
|
449
|
+
const range = `${start}:${exists}`;
|
|
450
|
+
const t0 = Date.now();
|
|
451
|
+
console.log(` [fetch-latest] ${this.selectedMailbox || "?"}: sequence ${range} (${Math.min(n, exists)} most recent of ${exists})`);
|
|
452
|
+
const items = ["UID", "FLAGS", "ENVELOPE", "RFC822.SIZE", "INTERNALDATE", "BODY.PEEK[HEADER]"];
|
|
453
|
+
if (options.source)
|
|
454
|
+
items.push("BODY.PEEK[]");
|
|
455
|
+
const tag = proto.nextTag();
|
|
456
|
+
const streamed = [];
|
|
457
|
+
const seenChunks = new Set();
|
|
458
|
+
const cb = (resp) => {
|
|
459
|
+
if (resp.tag !== "*" || resp.type !== "FETCH")
|
|
460
|
+
return;
|
|
461
|
+
const parsed = this.parseFetchResponses([resp]);
|
|
462
|
+
for (const msg of parsed)
|
|
463
|
+
streamed.push(msg);
|
|
464
|
+
// Flush onChunk every ~50 messages without buffering the whole
|
|
465
|
+
// response in memory — same shape as fetchByDate's chunk emit.
|
|
466
|
+
const bucket = Math.floor(streamed.length / 50);
|
|
467
|
+
if (onChunk && !seenChunks.has(bucket)) {
|
|
468
|
+
seenChunks.add(bucket);
|
|
469
|
+
onChunk(parsed);
|
|
470
|
+
}
|
|
471
|
+
};
|
|
472
|
+
await this.sendCommand(tag, proto.seqFetchCommand(tag, range, items), cb);
|
|
473
|
+
if (onChunk && streamed.length > 0) {
|
|
474
|
+
// Final flush in case last bucket didn't trip the threshold.
|
|
475
|
+
const lastBucket = Math.floor(streamed.length / 50);
|
|
476
|
+
if (!seenChunks.has(lastBucket))
|
|
477
|
+
onChunk([]);
|
|
478
|
+
}
|
|
479
|
+
// Reverse so newest-first matches fetchByDate ordering — caller code
|
|
480
|
+
// (mailx) expects that ordering when computing highestUid windows.
|
|
481
|
+
streamed.reverse();
|
|
482
|
+
console.log(` [fetch-latest] done — ${streamed.length} messages in ${Date.now() - t0}ms`);
|
|
483
|
+
return streamed;
|
|
484
|
+
}
|
|
423
485
|
/** Fetch a single message by UID */
|
|
424
486
|
async fetchMessage(uid, options = {}) {
|
|
425
487
|
const msgs = await this.fetchMessages(String(uid), options);
|
package/imap-protocol.d.ts
CHANGED
|
@@ -79,6 +79,10 @@ export declare function examineCommand(tag: string, mailbox: string): string;
|
|
|
79
79
|
export declare function statusCommand(tag: string, mailbox: string, items?: string[]): string;
|
|
80
80
|
/** Build UID FETCH command */
|
|
81
81
|
export declare function fetchCommand(tag: string, range: string, items: string[]): string;
|
|
82
|
+
/** Build sequence-number FETCH command (no UID prefix). Used by fetchLatestN
|
|
83
|
+
* to avoid SEARCH SINCE on a cold mailbox — sequence ranges are O(1) on the
|
|
84
|
+
* server because they're just message numbers in the current mailbox. */
|
|
85
|
+
export declare function seqFetchCommand(tag: string, range: string, items: string[]): string;
|
|
82
86
|
/** Build UID SEARCH command */
|
|
83
87
|
export declare function searchCommand(tag: string, criteria: string): string;
|
|
84
88
|
/** Build UID STORE command (set/add/remove flags) */
|
package/imap-protocol.js
CHANGED
|
@@ -48,6 +48,12 @@ export function statusCommand(tag, mailbox, items = ["MESSAGES", "UIDNEXT"]) {
|
|
|
48
48
|
export function fetchCommand(tag, range, items) {
|
|
49
49
|
return buildCommand(tag, `UID FETCH ${range} (${items.join(" ")})`);
|
|
50
50
|
}
|
|
51
|
+
/** Build sequence-number FETCH command (no UID prefix). Used by fetchLatestN
|
|
52
|
+
* to avoid SEARCH SINCE on a cold mailbox — sequence ranges are O(1) on the
|
|
53
|
+
* server because they're just message numbers in the current mailbox. */
|
|
54
|
+
export function seqFetchCommand(tag, range, items) {
|
|
55
|
+
return buildCommand(tag, `FETCH ${range} (${items.join(" ")})`);
|
|
56
|
+
}
|
|
51
57
|
/** Build UID SEARCH command */
|
|
52
58
|
export function searchCommand(tag, criteria) {
|
|
53
59
|
return buildCommand(tag, `UID SEARCH ${criteria}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bobfrankston/iflow-direct",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.28",
|
|
4
4
|
"description": "Direct IMAP client — transport-agnostic, no Node.js dependencies, browser-ready",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.ts",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"author": "Bob Frankston",
|
|
20
20
|
"license": "ISC",
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@bobfrankston/tcp-transport": "^0.1.
|
|
22
|
+
"@bobfrankston/tcp-transport": "^0.1.5"
|
|
23
23
|
},
|
|
24
24
|
"exports": {
|
|
25
25
|
".": {
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
},
|
|
51
51
|
".transformedSnapshot": {
|
|
52
52
|
"dependencies": {
|
|
53
|
-
"@bobfrankston/tcp-transport": "^0.1.
|
|
53
|
+
"@bobfrankston/tcp-transport": "^0.1.5"
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
56
|
}
|