imapflow 1.2.7 → 1.2.9

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.
Files changed (54) hide show
  1. package/.ncurc.js +1 -1
  2. package/.release-please-manifest.json +1 -1
  3. package/CHANGELOG.md +14 -0
  4. package/README.md +3 -3
  5. package/lib/charsets.js +15 -0
  6. package/lib/commands/append.js +62 -54
  7. package/lib/commands/authenticate.js +99 -52
  8. package/lib/commands/capability.js +12 -2
  9. package/lib/commands/close.js +11 -1
  10. package/lib/commands/compress.js +10 -1
  11. package/lib/commands/copy.js +18 -1
  12. package/lib/commands/create.js +15 -2
  13. package/lib/commands/delete.js +10 -1
  14. package/lib/commands/enable.js +12 -1
  15. package/lib/commands/expunge.js +18 -2
  16. package/lib/commands/fetch.js +39 -4
  17. package/lib/commands/id.js +22 -3
  18. package/lib/commands/idle.js +39 -4
  19. package/lib/commands/list.js +86 -48
  20. package/lib/commands/login.js +12 -1
  21. package/lib/commands/logout.js +11 -2
  22. package/lib/commands/move.js +17 -1
  23. package/lib/commands/namespace.js +32 -2
  24. package/lib/commands/noop.js +6 -1
  25. package/lib/commands/quota.js +33 -14
  26. package/lib/commands/rename.js +13 -1
  27. package/lib/commands/search.js +16 -1
  28. package/lib/commands/select.js +76 -33
  29. package/lib/commands/starttls.js +6 -1
  30. package/lib/commands/status.js +64 -52
  31. package/lib/commands/store.js +27 -4
  32. package/lib/commands/subscribe.js +7 -1
  33. package/lib/commands/unsubscribe.js +7 -1
  34. package/lib/handler/imap-compiler.js +44 -2
  35. package/lib/handler/imap-formal-syntax.js +51 -3
  36. package/lib/handler/imap-handler.js +8 -0
  37. package/lib/handler/imap-parser.js +23 -2
  38. package/lib/handler/imap-stream.js +84 -31
  39. package/lib/handler/parser-instance.js +61 -1
  40. package/lib/handler/token-parser.js +66 -9
  41. package/lib/imap-commands.js +11 -0
  42. package/lib/imap-flow.d.ts +6 -0
  43. package/lib/imap-flow.js +164 -42
  44. package/lib/jp-decoder.js +10 -0
  45. package/lib/limited-passthrough.js +12 -5
  46. package/lib/proxy-connection.js +18 -12
  47. package/lib/search-compiler.js +3 -11
  48. package/lib/special-use.js +23 -16
  49. package/lib/tools.js +218 -13
  50. package/package.json +4 -11
  51. package/test/commands-integration-test.js +33 -0
  52. package/test/special-use-test.js +32 -0
  53. package/assets/favicon.ico +0 -0
  54. package/jsdoc.json +0 -28
package/lib/imap-flow.js CHANGED
@@ -65,7 +65,7 @@ const states = {
65
65
  * @property {Set<string>} permanentFlags A Set of flags available to use in this mailbox. If it is not set or includes special flag "\\\*" then any flag can be used.
66
66
  * @property {String} [mailboxId] unique mailbox ID if server has `OBJECTID` extension enabled
67
67
  * @property {BigInt} [highestModseq] latest known modseq value if server has CONDSTORE or XYMHIGHESTMODSEQ enabled
68
- * @property {String} [noModseq] if true then the server doesn't support the persistent storage of mod-sequences for the mailbox
68
+ * @property {Boolean} [noModseq] if true then the server doesn't support the persistent storage of mod-sequences for the mailbox
69
69
  * @property {BigInt} uidValidity Mailbox `UIDVALIDITY` value
70
70
  * @property {Number} uidNext Next predicted UID
71
71
  * @property {Number} exists Messages in this folder
@@ -308,7 +308,7 @@ class ImapFlow extends EventEmitter {
308
308
  this.commandParts = [];
309
309
 
310
310
  /**
311
- * Active IMAP capabilities. Value is either `true` for togglabe capabilities (eg. `UIDPLUS`)
311
+ * Active IMAP capabilities. Value is either `true` for toggleable capabilities (eg. `UIDPLUS`)
312
312
  * or a number for capabilities with a value (eg. `APPENDLIMIT`)
313
313
  * @type {Map<string, boolean|number>}
314
314
  */
@@ -372,10 +372,12 @@ class ImapFlow extends EventEmitter {
372
372
 
373
373
  this.disableBinary = !!this.options.disableBinary;
374
374
 
375
- // Named error handler for proper cleanup
375
+ // Named error handler for proper cleanup. Certain error codes represent
376
+ // expected socket/network issues (buffer exhaustion, connection reset, broken pipe,
377
+ // timeout, unreachable host) that just need a silent connection close rather
378
+ // than emitting an error event to the caller.
376
379
  this._streamerErrorHandler = err => {
377
380
  if (['Z_BUF_ERROR', 'ECONNRESET', 'EPIPE', 'ETIMEDOUT', 'EHOSTUNREACH'].includes(err.code)) {
378
- // just close the connection, usually nothing but noise
379
381
  this.closeAfter();
380
382
  return;
381
383
  }
@@ -430,6 +432,9 @@ class ImapFlow extends EventEmitter {
430
432
  return;
431
433
  }
432
434
 
435
+ // Append CRLF only to the final part of a command. When sending literals,
436
+ // commandParts holds the remaining parts (literal data, continuation); the CRLF
437
+ // delimiter is only added when no more parts remain (the command is complete).
433
438
  let addLineBreak = !this.commandParts.length;
434
439
  if (typeof chunk === 'string') {
435
440
  if (addLineBreak) {
@@ -460,6 +465,14 @@ class ImapFlow extends EventEmitter {
460
465
  this.writeSocket.write(chunk);
461
466
  }
462
467
 
468
+ /**
469
+ * Returns byte counters for the current connection.
470
+ *
471
+ * @param {Boolean} [reset] If `true` then resets the byte counters after returning the current values
472
+ * @returns {Object} Byte counters
473
+ * @returns {Number} return.sent Bytes sent to server
474
+ * @returns {Number} return.received Bytes received from server
475
+ */
463
476
  stats(reset) {
464
477
  let result = {
465
478
  sent: this.writeBytesCounter || 0,
@@ -476,6 +489,11 @@ class ImapFlow extends EventEmitter {
476
489
  return result;
477
490
  }
478
491
 
492
+ // Compiles and sends an IMAP command to the server. The command is compiled
493
+ // twice: once as an array (for sending, with literal data split into parts)
494
+ // and once as a string (for logging, with sensitive data masked).
495
+ // When LITERAL- or LITERAL+ extensions are available, the compiler can use
496
+ // non-synchronizing literals to avoid waiting for server "+" continuation.
479
497
  async send(data) {
480
498
  if (this.state === this.states.LOGOUT) {
481
499
  // already logged out
@@ -491,12 +509,17 @@ class ImapFlow extends EventEmitter {
491
509
  return;
492
510
  }
493
511
 
512
+ // Compile with asArray=true: splits output into parts for literal handling.
513
+ // First part is the command text up to the first literal, remaining parts
514
+ // are stored in this.commandParts and sent after server "+" continuations.
494
515
  let compiled = await compiler(data, {
495
516
  asArray: true,
496
517
  literalMinus: this.capabilities.has('LITERAL-') || this.capabilities.has('LITERAL+')
497
518
  });
498
519
  this.commandParts = compiled;
499
520
 
521
+ // Compile again for logging with isLogging=true: masks sensitive values
522
+ // like passwords while producing a human-readable command string
500
523
  let logCompiled = await compiler(data, {
501
524
  isLogging: true
502
525
  });
@@ -505,6 +528,9 @@ class ImapFlow extends EventEmitter {
505
528
 
506
529
  this.log.debug({ src: 'c', msg: logCompiled.toString(), cid: this.id, comment: options.comment });
507
530
 
531
+ // Send the first part (command text). If there are literal parts,
532
+ // the server will respond with "+" continuations and reader() will
533
+ // send each remaining part from this.commandParts.
508
534
  this.write(this.commandParts.shift());
509
535
 
510
536
  if (typeof options.onSend === 'function') {
@@ -553,20 +579,29 @@ class ImapFlow extends EventEmitter {
553
579
  });
554
580
  }
555
581
 
582
+ // Resolves the handler for an untagged server response. IMAP untagged responses
583
+ // come in two forms:
584
+ // * CAPABILITY ... (keyword as command)
585
+ // * 42 FETCH (...) (numeric prefix + keyword)
586
+ // For numeric-prefixed responses, we extract the keyword (FETCH, EXISTS, EXPUNGE, etc.)
587
+ // and look up the handler by that keyword instead.
588
+ // Handler priority: command-specific handlers (registered per exec() call) take
589
+ // precedence over global handlers (registered on the connection).
556
590
  getUntaggedHandler(command, attributes) {
557
591
  if (/^[0-9]+$/.test(command)) {
558
592
  let type = attributes && attributes.length && typeof attributes[0].value === 'string' ? attributes[0].value.toUpperCase() : false;
559
593
  if (type) {
560
- // EXISTS, EXPUNGE, RECENT, FETCH etc
561
594
  command = type;
562
595
  }
563
596
  }
564
597
 
565
598
  command = command.toUpperCase().trim();
599
+ // Check command-specific handler first (registered in exec() options.untagged)
566
600
  if (this.currentRequest && this.currentRequest.options && this.currentRequest.options.untagged && this.currentRequest.options.untagged[command]) {
567
601
  return this.currentRequest.options.untagged[command];
568
602
  }
569
603
 
604
+ // Fall back to global handler (e.g., for CAPABILITY, BYE, etc.)
570
605
  if (this.untaggedHandlers[command]) {
571
606
  return this.untaggedHandlers[command];
572
607
  }
@@ -618,12 +653,16 @@ class ImapFlow extends EventEmitter {
618
653
  this.log.debug({ src: 's', msg: logCompiled.toString(), cid: this.id, nullBytesRemoved: parsed.nullBytesRemoved });
619
654
  }
620
655
 
656
+ // IMAP "+" (continuation request) handling. The server sends "+" in two cases:
657
+ // 1. During IDLE or AUTHENTICATE, where a custom handler (onPlusTag) processes it
658
+ // 2. During literal data transfer, where we send the next queued literal chunk
621
659
  if (parsed.tag === '+' && this.currentRequest && this.currentRequest.options && typeof this.currentRequest.options.onPlusTag === 'function') {
622
660
  await this.currentRequest.options.onPlusTag(parsed);
623
661
  data.next();
624
662
  continue;
625
663
  }
626
664
 
665
+ // Server acknowledged our literal size with "+", send the actual literal data
627
666
  if (parsed.tag === '+' && this.commandParts.length) {
628
667
  let content = this.commandParts.shift();
629
668
  this.write(content);
@@ -706,8 +745,9 @@ class ImapFlow extends EventEmitter {
706
745
 
707
746
  let throttleDelay = false;
708
747
 
709
- // MS365 throttling
710
- // tag BAD Request is throttled. Suggested Backoff Time: 92415 milliseconds
748
+ // MS365 throttling detection: Office 365 returns BAD with a human-readable
749
+ // backoff time when rate limits are hit. Parse the delay from the response text.
750
+ // Example: "tag BAD Request is throttled. Suggested Backoff Time: 92415 milliseconds"
711
751
  if (/Request is throttled/i.test(txt) && /Backoff Time/i.test(txt)) {
712
752
  let throttlingMatch = txt.match(/Backoff Time[:=\s]+(\d+)/i);
713
753
  if (throttlingMatch && throttlingMatch[1] && !isNaN(throttlingMatch[1])) {
@@ -722,7 +762,8 @@ class ImapFlow extends EventEmitter {
722
762
 
723
763
  let delayResponse = throttleDelay;
724
764
  if (delayResponse > 5 * 60 * 1000) {
725
- // max delay cap
765
+ // Cap wait at 5 minutes to avoid hanging connections indefinitely.
766
+ // The server-suggested delay can be very large.
726
767
  delayResponse = 5 * 60 * 1000;
727
768
  }
728
769
 
@@ -756,6 +797,10 @@ class ImapFlow extends EventEmitter {
756
797
  }
757
798
 
758
799
  setEventHandlers() {
800
+ // Bind the 'readable' event to kick off the reader loop.
801
+ // The `this.reading` flag acts as a concurrency guard: if reader()
802
+ // is already running, new 'readable' events are ignored. The reader
803
+ // loop will keep draining data until the stream returns null.
759
804
  this.socketReadable = () => {
760
805
  if (!this.reading) {
761
806
  this.reading = true;
@@ -912,7 +957,12 @@ class ImapFlow extends EventEmitter {
912
957
  return; // was not able to negotiate compression
913
958
  }
914
959
 
915
- // create deflate/inflate streams with rate limiting options
960
+ // Set up DEFLATE compression (RFC 4978). After COMPRESS is negotiated,
961
+ // all data in both directions is wrapped in a zlib DEFLATE stream.
962
+ // The incoming pipeline becomes: socket -> inflate -> streamer (parser).
963
+ // The outgoing pipeline uses a manual pump (see readNext below) instead
964
+ // of a normal pipe, because we need to flush after every IMAP command
965
+ // to ensure the server receives complete commands promptly.
916
966
  this._deflate = zlib.createDeflateRaw({
917
967
  windowBits: 15,
918
968
  level: zlib.constants.Z_DEFAULT_COMPRESSION, // Use default compression level (6)
@@ -924,7 +974,8 @@ class ImapFlow extends EventEmitter {
924
974
  chunkSize: 16 * 1024 // Process in 16KB chunks to prevent CPU blocking
925
975
  });
926
976
 
927
- // route incoming socket via inflate stream
977
+ // Reroute incoming data through inflate: socket -> inflate -> streamer.
978
+ // The streamer's compress flag tells it to expect deflated framing.
928
979
  this.socket.unpipe(this.streamer);
929
980
  this.streamer.compress = true;
930
981
  this.socket.pipe(this._inflate).pipe(this.streamer);
@@ -932,7 +983,10 @@ class ImapFlow extends EventEmitter {
932
983
  this.streamer.emit('error', err);
933
984
  });
934
985
 
935
- // route outgoing socket via deflate stream with rate limiting
986
+ // For outgoing data, replace the writeSocket with a PassThrough buffer.
987
+ // We can't pipe writeSocket -> deflate -> socket directly because we need
988
+ // to call deflate.flush() after each IMAP command to push all pending
989
+ // compressed bytes to the server immediately (IMAP is request-response).
936
990
  this.writeSocket = new PassThrough({
937
991
  highWaterMark: 64 * 1024 // 64KB buffer limit to prevent excessive memory usage
938
992
  });
@@ -953,8 +1007,9 @@ class ImapFlow extends EventEmitter {
953
1007
  get: () => !this.socket || this.socket.destroyed
954
1008
  });
955
1009
 
956
- // we need to force flush deflated data to socket so we can't
957
- // use normal pipes for this.writeSocket -> this._deflate -> this.socket
1010
+ // Manual pump loop: reads chunks from writeSocket, pushes them into
1011
+ // deflate, and flushes when the buffer is drained. This ensures each
1012
+ // IMAP command is fully compressed and flushed to the socket immediately.
958
1013
  let reading = false;
959
1014
  let processedChunks = 0;
960
1015
  let readNext = async () => {
@@ -1044,6 +1099,9 @@ class ImapFlow extends EventEmitter {
1044
1099
  return this._failSTARTTLS();
1045
1100
  }
1046
1101
 
1102
+ // STARTTLS upgrade sequence: detach the plain socket from the parser,
1103
+ // wrap it in a TLS socket, then reconnect the new TLS socket to the
1104
+ // parser. The plain socket becomes the underlying transport for TLS.
1047
1105
  this.socket.unpipe(this.streamer);
1048
1106
  let upgraded = await new Promise((resolve, reject) => {
1049
1107
  let socketPlain = this.socket;
@@ -1092,6 +1150,8 @@ class ImapFlow extends EventEmitter {
1092
1150
  return this.close();
1093
1151
  }
1094
1152
 
1153
+ // TLS handshake complete. Reconnect the now-encrypted socket
1154
+ // to the IMAP parser stream and record the cipher details.
1095
1155
  this.secureConnection = true;
1096
1156
  this.upgrading = false;
1097
1157
  this.streamer.secureConnection = true;
@@ -1432,12 +1492,16 @@ class ImapFlow extends EventEmitter {
1432
1492
  return true;
1433
1493
  }
1434
1494
 
1495
+ // Normalizes a message range from various input formats into an IMAP-compatible
1496
+ // sequence string (e.g., "1:5,7,10:*"). Handles: numbers, "*", {all:true},
1497
+ // {uid:value}, search query objects (resolved via SEARCH), and arrays of numbers.
1435
1498
  async resolveRange(range, options) {
1436
1499
  if (typeof range === 'number' || typeof range === 'bigint') {
1437
1500
  range = range.toString();
1438
1501
  }
1439
1502
 
1440
- // special case, some servers allow this, some do not, so replace it with the last known EXISTS value
1503
+ // Replace "*" with the actual message count. Some servers reject bare "*"
1504
+ // in certain commands, and this also forces a sequence query (not UID).
1441
1505
  if (range === '*') {
1442
1506
  if (!this.mailbox.exists) {
1443
1507
  return false;
@@ -1453,7 +1517,8 @@ class ImapFlow extends EventEmitter {
1453
1517
  range = range.uid;
1454
1518
  options.uid = true;
1455
1519
  } else {
1456
- // resolve range by searching
1520
+ // Arbitrary search query object: run SEARCH to resolve it into
1521
+ // a set of UIDs, then pack into a compact range string.
1457
1522
  options.uid = true; // force UIDs instead of sequence numbers
1458
1523
  range = await this.run('SEARCH', range, options);
1459
1524
  if (range && range.length) {
@@ -1688,7 +1753,8 @@ class ImapFlow extends EventEmitter {
1688
1753
  this.initialReject = false;
1689
1754
  let err = new Error('Unexpected close');
1690
1755
  err.code = `ClosedAfterConnect${this.secureConnection ? 'TLS' : 'Text'}`;
1691
- // still has to go through the logic below
1756
+ // Reject via setImmediate so the rest of close() cleanup runs first.
1757
+ // The caller's connect() promise will reject after sockets are torn down.
1692
1758
  setImmediate(() => reject(err));
1693
1759
  }
1694
1760
 
@@ -1731,8 +1797,9 @@ class ImapFlow extends EventEmitter {
1731
1797
  return error;
1732
1798
  };
1733
1799
 
1734
- // Reject pending requests via setImmediate to ensure caller's promise chain
1735
- // is fully set up before rejection (prevents unhandled promise rejections)
1800
+ // Reject pending requests via setImmediate so this synchronous close()
1801
+ // method finishes first. This prevents unhandled promise rejections that
1802
+ // would occur if we rejected inline before callers set up .catch() handlers.
1736
1803
  if (pendingRequests.length) {
1737
1804
  let byeReason = this.byeReason;
1738
1805
  setImmediate(() => {
@@ -1809,6 +1876,11 @@ class ImapFlow extends EventEmitter {
1809
1876
  return;
1810
1877
  }
1811
1878
 
1879
+ // Socket teardown order matters when compression is active:
1880
+ // writeSocket may be a PassThrough (compression) or the raw socket (no compression).
1881
+ // Destroy the underlying socket first, then writeSocket (if different).
1882
+ // The second socket.destroy() block handles the case where writeSocket.destroy()
1883
+ // did not also destroy the underlying socket.
1812
1884
  if (this.socket && !this.socket.destroyed && this.writeSocket !== this.socket) {
1813
1885
  try {
1814
1886
  this.socket.destroy();
@@ -1834,7 +1906,8 @@ class ImapFlow extends EventEmitter {
1834
1906
  }
1835
1907
  }
1836
1908
 
1837
- // Explicit nullification to help garbage collection
1909
+ // Null out all socket and handler references so the GC can collect
1910
+ // them even if the ImapFlow instance itself is still referenced.
1838
1911
  this.socket = null;
1839
1912
  this.writeSocket = null;
1840
1913
  this._inflate = null;
@@ -1870,11 +1943,11 @@ class ImapFlow extends EventEmitter {
1870
1943
  * Returns current quota
1871
1944
  *
1872
1945
  * @param {String} [path] Optional mailbox path if you want to check quota for specific folder
1873
- * @returns {Promise<QuotaResponse|Boolean>} Quota information or `false` if QUTOA extension is not supported or requested path does not exist
1946
+ * @returns {Promise<QuotaResponse|Boolean>} Quota information or `false` if QUOTA extension is not supported or requested path does not exist
1874
1947
  *
1875
1948
  * @example
1876
1949
  * let quota = await client.getQuota();
1877
- * console.log(quota.storage.used, quota.storage.available)
1950
+ * console.log(quota.storage.used, quota.storage.limit)
1878
1951
  */
1879
1952
  async getQuota(path) {
1880
1953
  path = path || 'INBOX';
@@ -1938,7 +2011,7 @@ class ImapFlow extends EventEmitter {
1938
2011
  * @property {String} path mailbox path
1939
2012
  * @property {String} name mailbox name (last part of path after delimiter)
1940
2013
  * @property {String} delimiter mailbox path delimiter, usually "." or "/"
1941
- * @property {String[]} flags list of flags for this mailbox
2014
+ * @property {Set<string>} flags list of flags for this mailbox
1942
2015
  * @property {String} specialUse one of special-use flags (if applicable): "\All", "\Archive", "\Drafts", "\Flagged", "\Junk", "\Sent", "\Trash". Additionally INBOX has non-standard "\Inbox" flag set
1943
2016
  * @property {Boolean} listed `true` if mailbox was found from the output of LIST command
1944
2017
  * @property {Boolean} subscribed `true` if mailbox was found from the output of LSUB command
@@ -2206,7 +2279,7 @@ class ImapFlow extends EventEmitter {
2206
2279
  * Sets flags for a message or message range
2207
2280
  *
2208
2281
  * @param {SequenceString | Number[] | SearchObject} range Range to filter the messages
2209
- * @param {string[]} Array of flags to set. Only flags that are permitted to set are used, other flags are ignored
2282
+ * @param {string[]} flags Array of flags to set. Only flags that are permitted to set are used, other flags are ignored
2210
2283
  * @param {Object} [options]
2211
2284
  * @param {Boolean} [options.uid] If `true` then uses UID {@link SequenceString} instead of sequence numbers
2212
2285
  * @param {BigInt} [options.unchangedSince] If set then only messages with a lower or equal `modseq` value are updated. Ignored if server does not support `CONDSTORE` extension.
@@ -2240,7 +2313,7 @@ class ImapFlow extends EventEmitter {
2240
2313
  * Adds flags for a message or message range
2241
2314
  *
2242
2315
  * @param {SequenceString | Number[] | SearchObject} range Range to filter the messages
2243
- * @param {string[]} Array of flags to set. Only flags that are permitted to set are used, other flags are ignored
2316
+ * @param {string[]} flags Array of flags to set. Only flags that are permitted to set are used, other flags are ignored
2244
2317
  * @param {Object} [options]
2245
2318
  * @param {Boolean} [options.uid] If `true` then uses UID {@link SequenceString} instead of sequence numbers
2246
2319
  * @param {BigInt} [options.unchangedSince] If set then only messages with a lower or equal `modseq` value are updated. Ignored if server does not support `CONDSTORE` extension.
@@ -2274,7 +2347,7 @@ class ImapFlow extends EventEmitter {
2274
2347
  * Remove specific flags from a message or message range
2275
2348
  *
2276
2349
  * @param {SequenceString | Number[] | SearchObject} range Range to filter the messages
2277
- * @param {string[]} Array of flags to remove. Only flags that are permitted to set are used, other flags are ignored
2350
+ * @param {string[]} flags Array of flags to remove. Only flags that are permitted to set are used, other flags are ignored
2278
2351
  * @param {Object} [options]
2279
2352
  * @param {Boolean} [options.uid] If `true` then uses UID {@link SequenceString} instead of sequence numbers
2280
2353
  * @param {BigInt} [options.unchangedSince] If set then only messages with a lower or equal `modseq` value are updated. Ignored if server does not support `CONDSTORE` extension.
@@ -2308,7 +2381,7 @@ class ImapFlow extends EventEmitter {
2308
2381
  * Sets a colored flag for an email. Only supported by mail clients like Apple Mail
2309
2382
  *
2310
2383
  * @param {SequenceString | Number[] | SearchObject} range Range to filter the messages
2311
- * @param {string} The color to set. One of 'red', 'orange', 'yellow', 'green', 'blue', 'purple', and 'grey'
2384
+ * @param {string} color The color to set. One of 'red', 'orange', 'yellow', 'green', 'blue', 'purple', and 'grey'
2312
2385
  * @param {Object} [options]
2313
2386
  * @param {Boolean} [options.uid] If `true` then uses UID {@link SequenceString} instead of sequence numbers
2314
2387
  * @param {BigInt} [options.unchangedSince] If set then only messages with a lower or equal `modseq` value are updated. Ignored if server does not support `CONDSTORE` extension.
@@ -2525,10 +2598,13 @@ class ImapFlow extends EventEmitter {
2525
2598
  * @property {boolean | Object} [source] if `true` then include full message in the response
2526
2599
  * @property {Number} [source.start] include full message in the response starting from *start* byte
2527
2600
  * @property {Number} [source.maxLength] include full message in the response, up to *maxLength* bytes
2528
- * @property {String} [threadId] if `true` then include thread ID in the response (only if server supports either `OBJECTID` or `X-GM-EXT-1` extensions)
2601
+ * @property {Boolean} [threadId] if `true` then include thread ID in the response (only if server supports either `OBJECTID` or `X-GM-EXT-1` extensions)
2529
2602
  * @property {Boolean} [labels] if `true` then include GMail labels in the response (only if server supports `X-GM-EXT-1` extension)
2530
2603
  * @property {boolean | string[]} [headers] if `true` then includes full headers of the message in the response. If the value is an array of header keys then includes only headers listed in the array
2531
2604
  * @property {string[]} [bodyParts] An array of BODYPART identifiers to include in the response
2605
+ * @property {Boolean} [fast] IMAP macro equivalent to `flags`, `internalDate`, `size`
2606
+ * @property {Boolean} [all] IMAP macro equivalent to `flags`, `internalDate`, `size`, `envelope`
2607
+ * @property {Boolean} [full] IMAP macro equivalent to `flags`, `internalDate`, `size`, `envelope`, `bodyStructure`
2532
2608
  */
2533
2609
 
2534
2610
  /**
@@ -2584,7 +2660,7 @@ class ImapFlow extends EventEmitter {
2584
2660
  * @property {Buffer} [source] message source for the requested byte range
2585
2661
  * @property {BigInt} [modseq] message Modseq number. Always included if the server supports CONDSTORE extension
2586
2662
  * @property {String} [emailId] unique email ID. Always included if server supports `OBJECTID` or `X-GM-EXT-1` extensions
2587
- * @property {String} [threadid] unique thread ID. Only present if server supports `OBJECTID` or `X-GM-EXT-1` extension
2663
+ * @property {String} [threadId] unique thread ID. Only present if server supports `OBJECTID` or `X-GM-EXT-1` extension
2588
2664
  * @property {Set<string>} [labels] a Set of labels. Only present if server supports `X-GM-EXT-1` extension
2589
2665
  * @property {Number} [size] message size
2590
2666
  * @property {Set<string>} [flags] a set of message flags
@@ -2629,6 +2705,11 @@ class ImapFlow extends EventEmitter {
2629
2705
  return false;
2630
2706
  }
2631
2707
 
2708
+ // Push/pull coordination for the async generator pattern:
2709
+ // The FETCH command handler pushes results into rowQueue via onUntaggedFetch.
2710
+ // The generator consumer pulls via getNext(). The `push` callback bridges the
2711
+ // two: when the consumer is waiting and the queue is empty, `push` is set to
2712
+ // a function that wakes up the consumer when new data arrives.
2632
2713
  let finished = false;
2633
2714
  let push = false;
2634
2715
  let rowQueue = [];
@@ -2649,7 +2730,7 @@ class ImapFlow extends EventEmitter {
2649
2730
  return resolve(null);
2650
2731
  }
2651
2732
 
2652
- // wait until data is pushed to queue and try again
2733
+ // No data available yet; register a wakeup callback
2653
2734
  push = () => {
2654
2735
  push = false;
2655
2736
  check();
@@ -2658,6 +2739,10 @@ class ImapFlow extends EventEmitter {
2658
2739
  check();
2659
2740
  });
2660
2741
 
2742
+ // Fire-and-forget the FETCH command. It runs in the background while
2743
+ // the generator yields results. Each untagged FETCH response is paired
2744
+ // with a `next` callback that acts as backpressure: the FETCH handler
2745
+ // won't process the next response until the consumer calls next().
2661
2746
  this.run('FETCH', range, query, {
2662
2747
  uid: !!options.uid,
2663
2748
  binary: options.binary,
@@ -2697,6 +2782,7 @@ class ImapFlow extends EventEmitter {
2697
2782
 
2698
2783
  if (res !== null) {
2699
2784
  yield res.response;
2785
+ // Signal the FETCH handler to process the next untagged response
2700
2786
  res.next();
2701
2787
  }
2702
2788
  }
@@ -2833,8 +2919,9 @@ class ImapFlow extends EventEmitter {
2833
2919
  let uid = false;
2834
2920
 
2835
2921
  if (part === '1') {
2836
- // First part has special conditions for single node emails as
2837
- // the mime parts for root node are not 1 and 1.MIME but TEXT and HEADERS
2922
+ // Special handling for part "1": in single-node emails (no childNodes),
2923
+ // the body is accessed via "TEXT" rather than "1", and headers via
2924
+ // "HEADER" instead of "1.MIME". Check bodyStructure to detect this.
2838
2925
  let response = await this.fetchOne(range, { uid: true, bodyStructure: true }, options);
2839
2926
 
2840
2927
  if (!response) {
@@ -2994,6 +3081,13 @@ class ImapFlow extends EventEmitter {
2994
3081
  let output;
2995
3082
  let fetchAborted = false;
2996
3083
 
3084
+ // Build a decoder pipeline that progressively transforms the raw FETCH data:
3085
+ // 1. Transfer-encoding decoder (base64 or quoted-printable -> binary)
3086
+ // 2. Format decoder (format=flowed -> plain text, if applicable)
3087
+ // 3. Charset decoder (non-UTF-8 -> UTF-8, for text parts only)
3088
+ // 4. Byte limiter (enforces maxBytes cap)
3089
+ // `stream` is the head of the pipeline (where raw chunks are written),
3090
+ // `output` is the tail (what the caller reads from).
2997
3091
  switch (meta.encoding) {
2998
3092
  case 'base64':
2999
3093
  output = stream = new libbase64.Decoder();
@@ -3007,7 +3101,7 @@ class ImapFlow extends EventEmitter {
3007
3101
 
3008
3102
  let isTextNode = ['text/html', 'text/plain', 'text/x-amp-html'].includes(meta.contentType) || (part === '1' && !meta.contentType);
3009
3103
  if ((!meta.disposition || meta.disposition === 'inline') && isTextNode) {
3010
- // flowed text
3104
+ // RFC 3676 format=flowed text: unwrap soft line breaks
3011
3105
  if (meta.flowed) {
3012
3106
  let flowDecoder = new FlowedDecoder({
3013
3107
  delSp: meta.delSp
@@ -3018,7 +3112,8 @@ class ImapFlow extends EventEmitter {
3018
3112
  output = output.pipe(flowDecoder);
3019
3113
  }
3020
3114
 
3021
- // not utf-8 text
3115
+ // Convert non-UTF-8 charsets to UTF-8 via a streaming decoder.
3116
+ // ASCII and UTF-8 need no conversion. Unknown charsets are left as-is.
3022
3117
  if (meta.charset && !['ascii', 'usascii', 'utf8'].includes(meta.charset.toLowerCase().replace(/[^a-z0-9]+/g, ''))) {
3023
3118
  try {
3024
3119
  let decoder = getDecoder(meta.charset);
@@ -3059,6 +3154,9 @@ class ImapFlow extends EventEmitter {
3059
3154
  return stream.write(chunk);
3060
3155
  };
3061
3156
 
3157
+ // Fetch remaining chunks in a loop, writing each to the decoder stream.
3158
+ // Stops when the server returns a short chunk (< chunkSize), the byte
3159
+ // limiter is satisfied, or the consumer destroys the output stream.
3062
3160
  let fetchAllParts = async () => {
3063
3161
  while (hasMore && !limiter.limited && !fetchAborted) {
3064
3162
  let { chunk } = await getNextPart();
@@ -3108,6 +3206,11 @@ class ImapFlow extends EventEmitter {
3108
3206
  }
3109
3207
  };
3110
3208
 
3209
+ // Kick off the download pipeline asynchronously. The first chunk was
3210
+ // already fetched above (to get metadata); write it to the decoder
3211
+ // stream and then fetch remaining chunks via fetchAllParts().
3212
+ // setImmediate ensures the caller gets the {meta, content} return
3213
+ // value before streaming begins.
3111
3214
  setImmediate(() => {
3112
3215
  let writeResult;
3113
3216
  try {
@@ -3180,7 +3283,7 @@ class ImapFlow extends EventEmitter {
3180
3283
  * Fetch multiple attachments as Buffer values
3181
3284
  *
3182
3285
  * @param {SequenceString} range UID or sequence number for the message to fetch
3183
- * @param {String} parts A list of bodystructure parts
3286
+ * @param {String[]} parts A list of bodystructure parts
3184
3287
  * @param {Object} [options]
3185
3288
  * @param {Boolean} [options.uid] If `true` then uses UID number instead of sequence number for `range`
3186
3289
  * @returns {Promise<Object>} Download data object
@@ -3337,11 +3440,14 @@ class ImapFlow extends EventEmitter {
3337
3440
  return result;
3338
3441
  }
3339
3442
 
3443
+ // Mailbox lock queue processor. Implements a mutex pattern: only one lock
3444
+ // is active at a time. When the active lock is released, the next queued
3445
+ // lock is processed. The `processingLock` flag prevents concurrent runs
3446
+ // of this method (which could happen via setImmediate re-entry from release()).
3340
3447
  async processLocks() {
3341
- // Atomic test-and-set to prevent race condition
3342
3448
  const wasProcessing = this.processingLock;
3343
3449
  if (wasProcessing) {
3344
- // Another processor is already running, just exit
3450
+ // Another processor is already running; it will pick up new locks
3345
3451
  this.log.trace({
3346
3452
  msg: 'Mailbox locking queued',
3347
3453
  path: this.mailbox && this.mailbox.path,
@@ -3405,7 +3511,7 @@ class ImapFlow extends EventEmitter {
3405
3511
  }
3406
3512
 
3407
3513
  if (this.mailbox && this.mailbox.path === path && !!this.mailbox.readOnly === !!options.readOnly) {
3408
- // nothing to do here, already selected
3514
+ // Fast path: mailbox is already selected with the right access mode
3409
3515
  this.log.trace({
3410
3516
  msg: 'Mailbox lock acquired [existing]',
3411
3517
  path,
@@ -3415,10 +3521,10 @@ class ImapFlow extends EventEmitter {
3415
3521
  });
3416
3522
  this.currentLock = lock;
3417
3523
  resolve({ path, release });
3418
- break; // Wait for this lock to be released
3524
+ break; // Stop processing; next lock waits for release()
3419
3525
  } else {
3420
3526
  try {
3421
- // Try to open. Throws if mailbox does not exists or can't open
3527
+ // Need to SELECT/EXAMINE a different mailbox
3422
3528
  await this.mailboxOpen(path, options);
3423
3529
  this.log.trace({
3424
3530
  msg: 'Mailbox lock acquired [selected]',
@@ -3432,6 +3538,9 @@ class ImapFlow extends EventEmitter {
3432
3538
  break; // Wait for this lock to be released
3433
3539
  } catch (err) {
3434
3540
  if (err.responseStatus === 'NO') {
3541
+ // SELECT failed with NO -- verify whether the mailbox exists
3542
+ // at all by running LIST. This sets mailboxMissing on the error
3543
+ // so the caller can distinguish "doesn't exist" from other failures.
3435
3544
  try {
3436
3545
  let folders = await this.run('LIST', '', path, { listOnly: true });
3437
3546
  if (!folders || !folders.length) {
@@ -3458,9 +3567,10 @@ class ImapFlow extends EventEmitter {
3458
3567
  } finally {
3459
3568
  this.processingLock = false;
3460
3569
 
3461
- // Check if new locks were added while we were processing
3570
+ // New locks may have been queued while we were processing (e.g.,
3571
+ // a lock that failed immediately and the next getMailboxLock call
3572
+ // arrived before we finished). Schedule another run if needed.
3462
3573
  if (this.locks.length && !this.currentLock) {
3463
- // Process any remaining locks
3464
3574
  setImmediate(() => {
3465
3575
  this.processLocks().catch(err => this.log.error({ err, cid: this.id }));
3466
3576
  });
@@ -3561,6 +3671,14 @@ class ImapFlow extends EventEmitter {
3561
3671
  return synteticLogger;
3562
3672
  }
3563
3673
 
3674
+ /**
3675
+ * Detaches sockets from the IMAP pipeline. Useful for upgrading the connection
3676
+ * (e.g., STARTTLS) or transferring socket ownership.
3677
+ *
3678
+ * @returns {Object} Socket objects
3679
+ * @returns {Object} return.readSocket The read socket (inflated socket if compression is enabled, raw socket otherwise)
3680
+ * @returns {Object} return.writeSocket The write socket
3681
+ */
3564
3682
  unbind() {
3565
3683
  this.socket.unpipe(this.streamer);
3566
3684
  if (this._inflate) {
@@ -3620,6 +3738,9 @@ class ImapFlow extends EventEmitter {
3620
3738
  * @type {Object}
3621
3739
  * @property {String} path mailbox path this event applies to
3622
3740
  * @property {Number} seq sequence number of deleted message
3741
+ * @property {Boolean} vanished `true` if message was expunged via VANISHED response
3742
+ * @property {Number} [uid] UID of expunged message (when `vanished` is `true`)
3743
+ * @property {Boolean} [earlier] `true` for VANISHED EARLIER responses
3623
3744
  * @example
3624
3745
  * client.on('expunge', data=>{
3625
3746
  * console.log(`Message #${data.seq} was deleted from "${data.path}"`);
@@ -3636,6 +3757,7 @@ class ImapFlow extends EventEmitter {
3636
3757
  * @property {Number} [uid] UID number of updated message (if server provided this value)
3637
3758
  * @property {BigInt} [modseq] Updated modseq number for the mailbox (if server provided this value)
3638
3759
  * @property {Set<string>} flags A set of all flags for the updated message
3760
+ * @property {String} [flagColor] flag color like "red", or "yellow". Derived from the `flags` Set using Apple Mail color rules
3639
3761
  * @example
3640
3762
  * client.on('flags', data=>{
3641
3763
  * console.log(`Flag set for #${data.seq} is now "${Array.from(data.flags).join(', ')}"`);
@@ -3671,7 +3793,7 @@ class ImapFlow extends EventEmitter {
3671
3793
  * @type {Object}
3672
3794
  * @example
3673
3795
  * client.on('log', entry => {
3674
- * console.log(`${log.cid} ${log.msg}`);
3796
+ * console.log(`${entry.cid} ${entry.msg}`);
3675
3797
  * });
3676
3798
  */
3677
3799
 
package/lib/jp-decoder.js CHANGED
@@ -3,6 +3,11 @@
3
3
  const { Transform } = require('stream');
4
4
  const encodingJapanese = require('encoding-japanese');
5
5
 
6
+ // A Transform stream for decoding Japanese character sets (Shift_JIS, EUC-JP, ISO-2022-JP).
7
+ // Unlike iconv-lite which can decode incrementally, encoding-japanese requires the complete
8
+ // input buffer for accurate charset detection and stateful decoding (especially ISO-2022-JP
9
+ // which uses escape sequences to switch between ASCII and multi-byte modes). Therefore,
10
+ // this stream buffers all input during _transform and performs the actual decoding in _flush.
6
11
  class JPDecoder extends Transform {
7
12
  constructor(charset) {
8
13
  super();
@@ -12,6 +17,8 @@ class JPDecoder extends Transform {
12
17
  this.chunklen = 0;
13
18
  }
14
19
 
20
+ // Buffer all incoming chunks; no decoding happens here because Japanese charsets
21
+ // require the complete input for accurate conversion.
15
22
  _transform(chunk, encoding, done) {
16
23
  if (typeof chunk === 'string') {
17
24
  chunk = Buffer.from(chunk, encoding);
@@ -22,6 +29,9 @@ class JPDecoder extends Transform {
22
29
  done();
23
30
  }
24
31
 
32
+ // Perform the actual charset conversion once all input has been received.
33
+ // Uses the encoding-japanese library to convert from the source charset to Unicode.
34
+ // On failure (corrupt or unrecognizable data), passes through the raw bytes unchanged.
25
35
  _flush(done) {
26
36
  let input = Buffer.concat(this.chunks, this.chunklen);
27
37
  try {