@bobfrankston/iflow-direct 0.1.27 → 0.1.30
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/gmail.d.ts +2 -0
- package/gmail.js +3 -0
- package/imap-compat.d.ts +7 -0
- package/imap-compat.js +12 -0
- package/imap-native.d.ts +13 -0
- package/imap-native.js +71 -4
- package/imap-protocol.d.ts +4 -0
- package/imap-protocol.js +6 -0
- package/package.json +3 -3
- package/types.d.ts +1 -0
package/gmail.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ export interface GmailOAuthConfig {
|
|
|
12
12
|
inactivityTimeout?: number;
|
|
13
13
|
fetchChunkSize?: number;
|
|
14
14
|
fetchChunkSizeMax?: number;
|
|
15
|
+
greetingTimeout?: number;
|
|
15
16
|
}
|
|
16
17
|
/**
|
|
17
18
|
* Check if email address is Gmail
|
|
@@ -41,5 +42,6 @@ export declare function createAutoImapConfig(config: {
|
|
|
41
42
|
inactivityTimeout?: number;
|
|
42
43
|
fetchChunkSize?: number;
|
|
43
44
|
fetchChunkSizeMax?: number;
|
|
45
|
+
greetingTimeout?: number;
|
|
44
46
|
}): ImapClientConfig;
|
|
45
47
|
//# sourceMappingURL=gmail.d.ts.map
|
package/gmail.js
CHANGED
|
@@ -36,6 +36,7 @@ export function createGmailConfig(config) {
|
|
|
36
36
|
inactivityTimeout: config.inactivityTimeout,
|
|
37
37
|
fetchChunkSize: config.fetchChunkSize,
|
|
38
38
|
fetchChunkSizeMax: config.fetchChunkSizeMax,
|
|
39
|
+
greetingTimeout: config.greetingTimeout,
|
|
39
40
|
};
|
|
40
41
|
}
|
|
41
42
|
/**
|
|
@@ -56,6 +57,7 @@ export function createAutoImapConfig(config) {
|
|
|
56
57
|
inactivityTimeout: config.inactivityTimeout,
|
|
57
58
|
fetchChunkSize: config.fetchChunkSize,
|
|
58
59
|
fetchChunkSizeMax: config.fetchChunkSizeMax,
|
|
60
|
+
greetingTimeout: config.greetingTimeout,
|
|
59
61
|
});
|
|
60
62
|
}
|
|
61
63
|
else {
|
|
@@ -69,6 +71,7 @@ export function createAutoImapConfig(config) {
|
|
|
69
71
|
inactivityTimeout: config.inactivityTimeout,
|
|
70
72
|
fetchChunkSize: config.fetchChunkSize,
|
|
71
73
|
fetchChunkSizeMax: config.fetchChunkSizeMax,
|
|
74
|
+
greetingTimeout: config.greetingTimeout,
|
|
72
75
|
};
|
|
73
76
|
}
|
|
74
77
|
}
|
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;
|
|
@@ -164,6 +173,10 @@ export declare class NativeImapClient {
|
|
|
164
173
|
* 60s accommodates Gmail which is slow on SEARCH for large folders.
|
|
165
174
|
* Overridable via ImapClientConfig.inactivityTimeout — slow Dovecot servers need 180s+. */
|
|
166
175
|
private inactivityTimeout;
|
|
176
|
+
/** Server-greeting timeout in ms — how long to wait for the server's
|
|
177
|
+
* initial banner after TCP/TLS connects. Default 10s; bumped to 30s
|
|
178
|
+
* for slow shared-hosting Dovecot in mailx-imap. */
|
|
179
|
+
private greetingTimeout;
|
|
167
180
|
/** Fetch chunk sizes — start small for quick first paint, ramp up for throughput.
|
|
168
181
|
* Default 25 initial → 500 max. Overridable via ImapClientConfig.fetchChunkSize /
|
|
169
182
|
* fetchChunkSizeMax. Slow servers benefit from smaller chunks (fewer timeouts). */
|
package/imap-native.js
CHANGED
|
@@ -34,6 +34,7 @@ export class NativeImapClient {
|
|
|
34
34
|
this.inactivityTimeout = config.inactivityTimeout ?? 60000;
|
|
35
35
|
this.fetchChunkSize = config.fetchChunkSize ?? 25;
|
|
36
36
|
this.fetchChunkSizeMax = config.fetchChunkSizeMax ?? 500;
|
|
37
|
+
this.greetingTimeout = config.greetingTimeout ?? 10000;
|
|
37
38
|
}
|
|
38
39
|
get connected() { return this._connected; }
|
|
39
40
|
/** Check the underlying transport's connected state — catches silently dead sockets. */
|
|
@@ -116,8 +117,8 @@ export class NativeImapClient {
|
|
|
116
117
|
return new Promise((resolve, reject) => {
|
|
117
118
|
const timeout = setTimeout(() => {
|
|
118
119
|
this.greetingResolve = null;
|
|
119
|
-
reject(new Error(
|
|
120
|
-
},
|
|
120
|
+
reject(new Error(`Greeting timeout (${this.greetingTimeout / 1000}s)`));
|
|
121
|
+
}, this.greetingTimeout);
|
|
121
122
|
this.greetingResolve = (resp) => {
|
|
122
123
|
clearTimeout(timeout);
|
|
123
124
|
resolve(resp);
|
|
@@ -400,26 +401,88 @@ export class NativeImapClient {
|
|
|
400
401
|
since,
|
|
401
402
|
before: before || undefined,
|
|
402
403
|
});
|
|
404
|
+
// SEARCH SINCE on Dovecot can take minutes on a cold mailbox while it
|
|
405
|
+
// (re)builds its date index. Without a "search starting" log, mailx
|
|
406
|
+
// shows nothing for the entire SEARCH window — the user thinks
|
|
407
|
+
// nothing is happening. Bracket the SEARCH with logs so the silence
|
|
408
|
+
// is at least labeled.
|
|
409
|
+
const t0 = Date.now();
|
|
410
|
+
console.log(` [search] SINCE ${since.toISOString().slice(0, 10)}${before ? ` BEFORE ${before.toISOString().slice(0, 10)}` : ""} — running...`);
|
|
403
411
|
const uids = await this.search(criteria);
|
|
412
|
+
console.log(` [search] returned ${uids.length} UIDs in ${Date.now() - t0}ms`);
|
|
404
413
|
if (uids.length === 0)
|
|
405
414
|
return [];
|
|
406
415
|
// Reverse so newest messages (highest UIDs) come first
|
|
407
416
|
uids.reverse();
|
|
408
|
-
// console.log(` [fetch] ${uids.length} UIDs to fetch (newest first)`);
|
|
409
417
|
const allMessages = [];
|
|
410
418
|
let chunkSize = this.fetchChunkSize;
|
|
419
|
+
let chunkIndex = 0;
|
|
411
420
|
for (let i = 0; i < uids.length; i += chunkSize) {
|
|
412
421
|
const chunk = uids.slice(i, i + chunkSize);
|
|
413
422
|
const msgs = await this.fetchMessages(chunk.join(","), options);
|
|
414
423
|
allMessages.push(...msgs);
|
|
415
|
-
|
|
424
|
+
chunkIndex++;
|
|
425
|
+
// Log every 5th chunk + the first one — enough to see progress
|
|
426
|
+
// without spamming. The first-chunk log proves the fetch is alive
|
|
427
|
+
// even when subsequent chunks are slow.
|
|
428
|
+
if (chunkIndex === 1 || chunkIndex % 5 === 0) {
|
|
429
|
+
console.log(` [fetch] ${allMessages.length}/${uids.length} messages (chunk ${chunkIndex})`);
|
|
430
|
+
}
|
|
416
431
|
if (onChunk)
|
|
417
432
|
onChunk(msgs);
|
|
418
433
|
if (chunkSize < this.fetchChunkSizeMax)
|
|
419
434
|
chunkSize = Math.min(chunkSize * 4, this.fetchChunkSizeMax);
|
|
420
435
|
}
|
|
436
|
+
console.log(` [fetch] done — ${allMessages.length} messages in ${Date.now() - t0}ms`);
|
|
421
437
|
return allMessages;
|
|
422
438
|
}
|
|
439
|
+
/** Fetch the most recent N messages by sequence number — avoids
|
|
440
|
+
* SEARCH SINCE which can take minutes on cold Dovecot mailboxes. The
|
|
441
|
+
* selected mailbox's `EXISTS` count is used to compute the range
|
|
442
|
+
* `(EXISTS-N+1):EXISTS`; sequence FETCH is O(1) on the server because
|
|
443
|
+
* it just reads message slots, no INTERNALDATE walk. Caller must have
|
|
444
|
+
* SELECTed the mailbox first (sequence numbers are session-relative). */
|
|
445
|
+
async fetchLatestN(n, options = {}, onChunk) {
|
|
446
|
+
const exists = this.mailboxInfo.exists;
|
|
447
|
+
if (!exists || exists < 1)
|
|
448
|
+
return [];
|
|
449
|
+
const start = Math.max(1, exists - n + 1);
|
|
450
|
+
const range = `${start}:${exists}`;
|
|
451
|
+
const t0 = Date.now();
|
|
452
|
+
console.log(` [fetch-latest] ${this.selectedMailbox || "?"}: sequence ${range} (${Math.min(n, exists)} most recent of ${exists})`);
|
|
453
|
+
const items = ["UID", "FLAGS", "ENVELOPE", "RFC822.SIZE", "INTERNALDATE", "BODY.PEEK[HEADER]"];
|
|
454
|
+
if (options.source)
|
|
455
|
+
items.push("BODY.PEEK[]");
|
|
456
|
+
const tag = proto.nextTag();
|
|
457
|
+
const streamed = [];
|
|
458
|
+
const seenChunks = new Set();
|
|
459
|
+
const cb = (resp) => {
|
|
460
|
+
if (resp.tag !== "*" || resp.type !== "FETCH")
|
|
461
|
+
return;
|
|
462
|
+
const parsed = this.parseFetchResponses([resp]);
|
|
463
|
+
for (const msg of parsed)
|
|
464
|
+
streamed.push(msg);
|
|
465
|
+
// Flush onChunk every ~50 messages without buffering the whole
|
|
466
|
+
// response in memory — same shape as fetchByDate's chunk emit.
|
|
467
|
+
const bucket = Math.floor(streamed.length / 50);
|
|
468
|
+
if (onChunk && !seenChunks.has(bucket)) {
|
|
469
|
+
seenChunks.add(bucket);
|
|
470
|
+
onChunk(parsed);
|
|
471
|
+
}
|
|
472
|
+
};
|
|
473
|
+
await this.sendCommand(tag, proto.seqFetchCommand(tag, range, items), cb);
|
|
474
|
+
if (onChunk && streamed.length > 0) {
|
|
475
|
+
// Final flush in case last bucket didn't trip the threshold.
|
|
476
|
+
const lastBucket = Math.floor(streamed.length / 50);
|
|
477
|
+
if (!seenChunks.has(lastBucket))
|
|
478
|
+
onChunk([]);
|
|
479
|
+
}
|
|
480
|
+
// Reverse so newest-first matches fetchByDate ordering — caller code
|
|
481
|
+
// (mailx) expects that ordering when computing highestUid windows.
|
|
482
|
+
streamed.reverse();
|
|
483
|
+
console.log(` [fetch-latest] done — ${streamed.length} messages in ${Date.now() - t0}ms`);
|
|
484
|
+
return streamed;
|
|
485
|
+
}
|
|
423
486
|
/** Fetch a single message by UID */
|
|
424
487
|
async fetchMessage(uid, options = {}) {
|
|
425
488
|
const msgs = await this.fetchMessages(String(uid), options);
|
|
@@ -666,6 +729,10 @@ export class NativeImapClient {
|
|
|
666
729
|
* 60s accommodates Gmail which is slow on SEARCH for large folders.
|
|
667
730
|
* Overridable via ImapClientConfig.inactivityTimeout — slow Dovecot servers need 180s+. */
|
|
668
731
|
inactivityTimeout;
|
|
732
|
+
/** Server-greeting timeout in ms — how long to wait for the server's
|
|
733
|
+
* initial banner after TCP/TLS connects. Default 10s; bumped to 30s
|
|
734
|
+
* for slow shared-hosting Dovecot in mailx-imap. */
|
|
735
|
+
greetingTimeout;
|
|
669
736
|
/** Fetch chunk sizes — start small for quick first paint, ramp up for throughput.
|
|
670
737
|
* Default 25 initial → 500 max. Overridable via ImapClientConfig.fetchChunkSize /
|
|
671
738
|
* fetchChunkSizeMax. Slow servers benefit from smaller chunks (fewer timeouts). */
|
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.30",
|
|
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
|
}
|