@bobfrankston/iflow 1.0.43 → 1.0.45

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.
@@ -141,15 +141,9 @@ export class NativeImapClient {
141
141
  this.capabilities.add(c.toUpperCase());
142
142
  }
143
143
  async logout() {
144
- // Send LOGOUT but don't wait just close the transport immediately.
145
- // The server will clean up the session when the socket closes.
146
- try {
147
- if (this._connected) {
148
- const tag = proto.nextTag();
149
- this.transport.write(proto.logoutCommand(tag)).catch(() => { });
150
- }
151
- }
152
- catch { /* ignore */ }
144
+ // Force-close the transport immediately. The LOGOUT command is a courtesy
145
+ // the server cleans up the session when the socket closes.
146
+ // Sending LOGOUT and waiting caused hangs and connection leaks.
153
147
  this.pendingCommand = null;
154
148
  this.transport.close();
155
149
  this._connected = false;
@@ -471,15 +465,23 @@ export class NativeImapClient {
471
465
  }
472
466
  processBuffer() {
473
467
  while (true) {
474
- // Check for literal {size}\r\n
468
+ // Check for literal {size}\r\n — reading exact byte count of literal data
475
469
  if (this.pendingCommand?.literalBytes != null) {
476
470
  if (this.buffer.length >= this.pendingCommand.literalBytes) {
477
471
  const literal = this.buffer.substring(0, this.pendingCommand.literalBytes);
478
472
  this.buffer = this.buffer.substring(this.pendingCommand.literalBytes);
479
473
  this.pendingCommand.literalBuffer = (this.pendingCommand.literalBuffer || "") + literal;
480
474
  this.pendingCommand.literalBytes = undefined;
475
+ if (this.verbose)
476
+ console.log(` [imap] literal consumed, ${literal.length} bytes, buffer remaining: ${this.buffer.length}`);
477
+ // After consuming literal, check if the NEXT part has another literal
478
+ // (e.g., BODY[HEADER] {500}\r\n<data>BODY[] {2000}\r\n<data>)\r\n)
479
+ // Continue to process the next line/literal from the buffer
481
480
  continue;
482
481
  }
482
+ if (this.verbose && this.pendingCommand.literalBytes > 0) {
483
+ console.log(` [imap] waiting for literal: need ${this.pendingCommand.literalBytes}, have ${this.buffer.length}`);
484
+ }
483
485
  break; // Wait for more data
484
486
  }
485
487
  const lineEnd = this.buffer.indexOf("\r\n");
@@ -487,24 +489,34 @@ export class NativeImapClient {
487
489
  break;
488
490
  const line = this.buffer.substring(0, lineEnd + 2);
489
491
  this.buffer = this.buffer.substring(lineEnd + 2);
490
- // Check for literal announcement {size}
492
+ // Check for literal announcement {size}\r\n at end of line
491
493
  const literalMatch = line.match(/\{(\d+)\}\r\n$/);
492
494
  if (literalMatch && this.pendingCommand) {
493
- this.pendingCommand.literalBytes = parseInt(literalMatch[1]);
494
- // Store the line prefix before the literal
495
- const linePrefix = line.substring(0, line.indexOf(`{${literalMatch[1]}}`));
496
- this.pendingCommand.literalBuffer = linePrefix;
495
+ const size = parseInt(literalMatch[1]);
496
+ // Store the line prefix before {size} — will be prepended after literal is consumed
497
+ const linePrefix = line.substring(0, literalMatch.index);
498
+ if (this.pendingCommand.literalBuffer) {
499
+ // Already have buffered data from a previous literal — append this prefix
500
+ this.pendingCommand.literalBuffer += linePrefix;
501
+ }
502
+ else {
503
+ this.pendingCommand.literalBuffer = linePrefix;
504
+ }
505
+ this.pendingCommand.literalBytes = size;
506
+ if (this.verbose)
507
+ console.log(` [imap] literal announced: ${size} bytes, prefix: ${linePrefix.length} chars`);
497
508
  continue;
498
509
  }
499
- // If we have buffered literal data, prepend it
510
+ // If we have buffered literal data, prepend it to this line (the closing part after the literal)
500
511
  let fullLine = line;
501
512
  if (this.pendingCommand?.literalBuffer) {
502
513
  fullLine = this.pendingCommand.literalBuffer + line;
503
514
  this.pendingCommand.literalBuffer = undefined;
504
515
  }
505
516
  const resp = proto.parseResponseLine(fullLine);
506
- if (this.verbose && resp.raw.length < 200) {
507
- console.log(` [imap] < ${resp.raw}`);
517
+ if (this.verbose) {
518
+ const display = resp.raw.length > 200 ? resp.raw.substring(0, 200) + `... (${resp.raw.length} bytes)` : resp.raw;
519
+ console.log(` [imap] < [tag=${resp.tag} type=${resp.type}] ${display}`);
508
520
  }
509
521
  // Server greeting — resolve readGreeting() promise
510
522
  if (this.greetingResolve && resp.tag === "*" && (resp.type === "OK" || resp.type === "PREAUTH")) {
@@ -116,8 +116,8 @@ export function parseResponseLine(line) {
116
116
  // Untagged response
117
117
  if (trimmed.startsWith("* ")) {
118
118
  const rest = trimmed.substring(2);
119
- // Check for numeric response (e.g., "* 5 EXISTS")
120
- const numMatch = rest.match(/^(\d+)\s+(\S+)(.*)$/);
119
+ // Check for numeric response (e.g., "* 5 EXISTS", "* 131441 FETCH (...)")
120
+ const numMatch = rest.match(/^(\d+)\s+(\S+)/);
121
121
  if (numMatch) {
122
122
  return { tag: "*", type: numMatch[2].toUpperCase(), text: rest, raw: trimmed };
123
123
  }
@@ -247,12 +247,26 @@ function decodeImapString(s) {
247
247
  return "";
248
248
  return s.replace(/=\?([^?]+)\?([BQ])\?([^?]+)\?=/gi, (_match, charset, encoding, text) => {
249
249
  try {
250
+ const cs = charset.toLowerCase().replace(/^utf8$/, "utf-8");
250
251
  if (encoding.toUpperCase() === "B") {
251
- return Buffer.from(text, "base64").toString("utf-8");
252
+ return Buffer.from(text, "base64").toString(cs);
252
253
  }
253
254
  else {
254
- // Quoted-printable
255
- return text.replace(/=([0-9A-F]{2})/gi, (_, hex) => String.fromCharCode(parseInt(hex, 16))).replace(/_/g, " ");
255
+ // Quoted-printable: collect bytes then decode with charset
256
+ const decoded = text.replace(/_/g, " ");
257
+ const bytes = [];
258
+ let i = 0;
259
+ while (i < decoded.length) {
260
+ if (decoded[i] === "=" && i + 2 < decoded.length) {
261
+ bytes.push(parseInt(decoded.substring(i + 1, i + 3), 16));
262
+ i += 3;
263
+ }
264
+ else {
265
+ bytes.push(decoded.charCodeAt(i));
266
+ i++;
267
+ }
268
+ }
269
+ return Buffer.from(bytes).toString(cs);
256
270
  }
257
271
  }
258
272
  catch {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/iflow",
3
- "version": "1.0.43",
3
+ "version": "1.0.45",
4
4
  "description": "IMAP client wrapper library",
5
5
  "main": "index.js",
6
6
  "types": "index.ts",