@bobfrankston/iflow 1.0.54 → 1.0.56

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.
@@ -37,10 +37,10 @@ export declare class CompatImapClient {
37
37
  fetchMessagesSinceUid(mailbox: string, sinceUid: number, options?: {
38
38
  source?: boolean;
39
39
  }): Promise<any[]>;
40
- /** Fetch messages by date range */
40
+ /** Fetch messages by date range. Optional onChunk callback for incremental processing. */
41
41
  fetchMessageByDate(mailbox: string, start: Date, end?: Date, options?: {
42
42
  source?: boolean;
43
- }): Promise<any[]>;
43
+ }, onChunk?: (msgs: any[]) => void): Promise<any[]>;
44
44
  /** Fetch a single message by UID */
45
45
  fetchMessageByUid(mailbox: string, uid: number, options?: {
46
46
  source?: boolean;
@@ -102,11 +102,12 @@ export class CompatImapClient {
102
102
  await this.native.closeMailbox();
103
103
  return msgs.map(toCompatMessage);
104
104
  }
105
- /** Fetch messages by date range */
106
- async fetchMessageByDate(mailbox, start, end, options) {
105
+ /** Fetch messages by date range. Optional onChunk callback for incremental processing. */
106
+ async fetchMessageByDate(mailbox, start, end, options, onChunk) {
107
107
  await this.ensureConnected();
108
108
  await this.native.select(mailbox);
109
- const msgs = await this.native.fetchByDate(start, end, options);
109
+ const chunkCb = onChunk ? (raw) => onChunk(raw.map(toCompatMessage)) : undefined;
110
+ const msgs = await this.native.fetchByDate(start, end, options, chunkCb);
110
111
  await this.native.closeMailbox();
111
112
  return msgs.map(toCompatMessage);
112
113
  }
@@ -86,11 +86,11 @@ export declare class NativeImapClient {
86
86
  /** Fetch messages since a UID */
87
87
  fetchSinceUid(sinceUid: number, options?: {
88
88
  source?: boolean;
89
- }): Promise<NativeFetchedMessage[]>;
90
- /** Fetch messages by date range */
89
+ }, onChunk?: (msgs: NativeFetchedMessage[]) => void): Promise<NativeFetchedMessage[]>;
90
+ /** Fetch messages by date range. Optional onChunk callback receives each batch as it arrives. */
91
91
  fetchByDate(since: Date, before?: Date, options?: {
92
92
  source?: boolean;
93
- }): Promise<NativeFetchedMessage[]>;
93
+ }, onChunk?: (msgs: NativeFetchedMessage[]) => void): Promise<NativeFetchedMessage[]>;
94
94
  /** Fetch a single message by UID */
95
95
  fetchMessage(uid: number, options?: {
96
96
  source?: boolean;
@@ -119,6 +119,9 @@ export declare class NativeImapClient {
119
119
  * This is NOT a wall-clock timeout. Timer resets every time data arrives from the server.
120
120
  * A large FETCH returning data continuously will never timeout. */
121
121
  private inactivityTimeout;
122
+ /** Fetch chunk sizes — start small for quick first paint, ramp up for throughput */
123
+ private static INITIAL_CHUNK_SIZE;
124
+ private static MAX_CHUNK_SIZE;
122
125
  /** Active command timer — reset by handleData on every data arrival */
123
126
  private commandTimer;
124
127
  private sendCommand;
@@ -289,26 +289,33 @@ export class NativeImapClient {
289
289
  return this.parseFetchResponses(responses);
290
290
  }
291
291
  /** Fetch messages since a UID */
292
- async fetchSinceUid(sinceUid, options = {}) {
293
- // Search for UIDs first, then fetch in chunks (avoids unbounded single command)
292
+ async fetchSinceUid(sinceUid, options = {}, onChunk) {
294
293
  const uids = await this.search(`UID ${sinceUid + 1}:*`);
295
294
  if (uids.length === 0)
296
295
  return [];
297
- if (uids.length <= 500) {
298
- return this.fetchMessages(uids.join(","), options);
296
+ console.log(` [fetch] ${uids.length} UIDs since ${sinceUid}`);
297
+ if (uids.length <= NativeImapClient.INITIAL_CHUNK_SIZE) {
298
+ const msgs = await this.fetchMessages(uids.join(","), options);
299
+ if (onChunk)
300
+ onChunk(msgs);
301
+ return msgs;
299
302
  }
300
- // Chunk large fetches
301
303
  const allMessages = [];
302
- for (let i = 0; i < uids.length; i += 500) {
303
- const chunk = uids.slice(i, i + 500);
304
+ let chunkSize = NativeImapClient.INITIAL_CHUNK_SIZE;
305
+ for (let i = 0; i < uids.length; i += chunkSize) {
306
+ const chunk = uids.slice(i, i + chunkSize);
304
307
  const msgs = await this.fetchMessages(chunk.join(","), options);
305
308
  allMessages.push(...msgs);
309
+ console.log(` [fetch] ${allMessages.length}/${uids.length} (chunk of ${chunk.length})`);
310
+ if (onChunk)
311
+ onChunk(msgs);
312
+ if (chunkSize < NativeImapClient.MAX_CHUNK_SIZE)
313
+ chunkSize = Math.min(chunkSize * 4, NativeImapClient.MAX_CHUNK_SIZE);
306
314
  }
307
315
  return allMessages;
308
316
  }
309
- /** Fetch messages by date range */
310
- async fetchByDate(since, before, options = {}) {
311
- // First search for UIDs in date range, then fetch
317
+ /** Fetch messages by date range. Optional onChunk callback receives each batch as it arrives. */
318
+ async fetchByDate(since, before, options = {}, onChunk) {
312
319
  const criteria = proto.buildSearchCriteria({
313
320
  since,
314
321
  before: before || undefined,
@@ -316,14 +323,18 @@ export class NativeImapClient {
316
323
  const uids = await this.search(criteria);
317
324
  if (uids.length === 0)
318
325
  return [];
319
- // Fetch in chunks to avoid very long command lines
320
- const chunkSize = 500;
326
+ console.log(` [fetch] ${uids.length} UIDs to fetch`);
321
327
  const allMessages = [];
328
+ let chunkSize = NativeImapClient.INITIAL_CHUNK_SIZE;
322
329
  for (let i = 0; i < uids.length; i += chunkSize) {
323
330
  const chunk = uids.slice(i, i + chunkSize);
324
- const range = chunk.join(",");
325
- const msgs = await this.fetchMessages(range, options);
331
+ const msgs = await this.fetchMessages(chunk.join(","), options);
326
332
  allMessages.push(...msgs);
333
+ console.log(` [fetch] ${allMessages.length}/${uids.length} (chunk of ${chunk.length})`);
334
+ if (onChunk)
335
+ onChunk(msgs);
336
+ if (chunkSize < NativeImapClient.MAX_CHUNK_SIZE)
337
+ chunkSize = Math.min(chunkSize * 4, NativeImapClient.MAX_CHUNK_SIZE);
327
338
  }
328
339
  return allMessages;
329
340
  }
@@ -444,6 +455,9 @@ export class NativeImapClient {
444
455
  * This is NOT a wall-clock timeout. Timer resets every time data arrives from the server.
445
456
  * A large FETCH returning data continuously will never timeout. */
446
457
  inactivityTimeout = 30000;
458
+ /** Fetch chunk sizes — start small for quick first paint, ramp up for throughput */
459
+ static INITIAL_CHUNK_SIZE = 25;
460
+ static MAX_CHUNK_SIZE = 500;
447
461
  /** Active command timer — reset by handleData on every data arrival */
448
462
  commandTimer = null;
449
463
  sendCommand(tag, command) {
@@ -507,11 +521,18 @@ export class NativeImapClient {
507
521
  }
508
522
  processBuffer() {
509
523
  while (true) {
510
- // Check for literal {size}\r\n — reading exact byte count of literal data
524
+ // Check for literal {size}\r\n — reading exact BYTE count of literal data
525
+ // CRITICAL: literalBytes is in octets (from IMAP {N}), but this.buffer is a
526
+ // JavaScript string where multi-byte UTF-8 characters count as 1 character.
527
+ // We must use Buffer.byteLength to find the correct character boundary.
511
528
  if (this.pendingCommand?.literalBytes != null) {
512
- if (this.buffer.length >= this.pendingCommand.literalBytes) {
513
- let literal = this.buffer.substring(0, this.pendingCommand.literalBytes);
514
- this.buffer = this.buffer.substring(this.pendingCommand.literalBytes);
529
+ const neededBytes = this.pendingCommand.literalBytes;
530
+ const bufferBytes = Buffer.byteLength(this.buffer, "utf-8");
531
+ if (bufferBytes >= neededBytes) {
532
+ // Find the character position that corresponds to the byte boundary
533
+ const buf = Buffer.from(this.buffer, "utf-8");
534
+ let literal = buf.subarray(0, neededBytes).toString("utf-8");
535
+ this.buffer = buf.subarray(neededBytes).toString("utf-8");
515
536
  // For non-BODY literals (e.g. display names in ENVELOPE), wrap in quotes
516
537
  // so tokenizeParenList treats them as a single token
517
538
  if (!this.pendingCommand.currentLiteralKey) {
@@ -535,7 +556,7 @@ export class NativeImapClient {
535
556
  continue;
536
557
  }
537
558
  if (this.verbose && this.pendingCommand.literalBytes > 0) {
538
- console.log(` [imap] waiting for literal: need ${this.pendingCommand.literalBytes}, have ${this.buffer.length}`);
559
+ console.log(` [imap] waiting for literal: need ${neededBytes} bytes, have ${bufferBytes} bytes`);
539
560
  }
540
561
  break; // Wait for more data
541
562
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/iflow",
3
- "version": "1.0.54",
3
+ "version": "1.0.56",
4
4
  "description": "IMAP client wrapper library",
5
5
  "main": "index.js",
6
6
  "types": "index.ts",