imapflow 1.2.8 → 1.2.10

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 (58) 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 +36 -58
  5. package/eslint.config.js +18 -16
  6. package/lib/charsets.js +15 -0
  7. package/lib/commands/append.js +62 -54
  8. package/lib/commands/authenticate.js +99 -52
  9. package/lib/commands/capability.js +12 -2
  10. package/lib/commands/close.js +11 -1
  11. package/lib/commands/compress.js +10 -1
  12. package/lib/commands/copy.js +18 -1
  13. package/lib/commands/create.js +15 -2
  14. package/lib/commands/delete.js +10 -1
  15. package/lib/commands/enable.js +12 -1
  16. package/lib/commands/expunge.js +18 -2
  17. package/lib/commands/fetch.js +39 -4
  18. package/lib/commands/id.js +22 -3
  19. package/lib/commands/idle.js +39 -4
  20. package/lib/commands/list.js +86 -48
  21. package/lib/commands/login.js +12 -1
  22. package/lib/commands/logout.js +11 -2
  23. package/lib/commands/move.js +17 -1
  24. package/lib/commands/namespace.js +32 -2
  25. package/lib/commands/noop.js +6 -1
  26. package/lib/commands/quota.js +33 -14
  27. package/lib/commands/rename.js +13 -1
  28. package/lib/commands/search.js +16 -1
  29. package/lib/commands/select.js +76 -33
  30. package/lib/commands/starttls.js +6 -1
  31. package/lib/commands/status.js +64 -52
  32. package/lib/commands/store.js +27 -4
  33. package/lib/commands/subscribe.js +7 -1
  34. package/lib/commands/unsubscribe.js +7 -1
  35. package/lib/handler/imap-compiler.js +44 -2
  36. package/lib/handler/imap-formal-syntax.js +51 -3
  37. package/lib/handler/imap-handler.js +8 -0
  38. package/lib/handler/imap-parser.js +23 -2
  39. package/lib/handler/imap-stream.js +84 -31
  40. package/lib/handler/parser-instance.js +61 -1
  41. package/lib/handler/token-parser.js +66 -9
  42. package/lib/imap-commands.js +11 -0
  43. package/lib/imap-flow.d.ts +6 -0
  44. package/lib/imap-flow.js +175 -46
  45. package/lib/jp-decoder.js +10 -0
  46. package/lib/limited-passthrough.js +12 -5
  47. package/lib/proxy-connection.js +18 -12
  48. package/lib/search-compiler.js +3 -11
  49. package/lib/special-use.js +23 -16
  50. package/lib/tools.js +218 -13
  51. package/package.json +5 -17
  52. package/test/commands-integration-test.js +33 -0
  53. package/test/connection-edge-cases-test.js +105 -0
  54. package/test/special-use-test.js +32 -0
  55. package/.babelrc +0 -6
  56. package/.eslintrc +0 -16
  57. package/assets/favicon.ico +0 -0
  58. 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 () => {
@@ -963,7 +1018,7 @@ class ImapFlow extends EventEmitter {
963
1018
  processedChunks = 0;
964
1019
 
965
1020
  let chunk;
966
- while ((chunk = this.writeSocket.read()) !== null) {
1021
+ while (this.writeSocket && (chunk = this.writeSocket.read()) !== null) {
967
1022
  if (this._deflate && this._deflate.write(chunk) === false) {
968
1023
  return this._deflate.once('drain', readNext);
969
1024
  }
@@ -972,6 +1027,9 @@ class ImapFlow extends EventEmitter {
972
1027
  processedChunks++;
973
1028
  if (processedChunks % 100 === 0) {
974
1029
  await new Promise(resolve => setImmediate(resolve));
1030
+ if (!this.writeSocket) {
1031
+ break;
1032
+ }
975
1033
  }
976
1034
  }
977
1035
 
@@ -987,17 +1045,21 @@ class ImapFlow extends EventEmitter {
987
1045
  };
988
1046
 
989
1047
  this.writeSocket.on('readable', () => {
990
- if (!reading) {
1048
+ if (!reading && this.writeSocket) {
991
1049
  readNext();
992
1050
  }
993
1051
  });
994
1052
  this.writeSocket.on('error', err => {
995
- this.socket.emit('error', err);
1053
+ if (this.socket) {
1054
+ this.socket.emit('error', err);
1055
+ }
996
1056
  });
997
1057
 
998
1058
  this._deflate.pipe(this.socket);
999
1059
  this._deflate.on('error', err => {
1000
- this.socket.emit('error', err);
1060
+ if (this.socket) {
1061
+ this.socket.emit('error', err);
1062
+ }
1001
1063
  });
1002
1064
  }
1003
1065
 
@@ -1044,6 +1106,9 @@ class ImapFlow extends EventEmitter {
1044
1106
  return this._failSTARTTLS();
1045
1107
  }
1046
1108
 
1109
+ // STARTTLS upgrade sequence: detach the plain socket from the parser,
1110
+ // wrap it in a TLS socket, then reconnect the new TLS socket to the
1111
+ // parser. The plain socket becomes the underlying transport for TLS.
1047
1112
  this.socket.unpipe(this.streamer);
1048
1113
  let upgraded = await new Promise((resolve, reject) => {
1049
1114
  let socketPlain = this.socket;
@@ -1092,6 +1157,8 @@ class ImapFlow extends EventEmitter {
1092
1157
  return this.close();
1093
1158
  }
1094
1159
 
1160
+ // TLS handshake complete. Reconnect the now-encrypted socket
1161
+ // to the IMAP parser stream and record the cipher details.
1095
1162
  this.secureConnection = true;
1096
1163
  this.upgrading = false;
1097
1164
  this.streamer.secureConnection = true;
@@ -1432,12 +1499,16 @@ class ImapFlow extends EventEmitter {
1432
1499
  return true;
1433
1500
  }
1434
1501
 
1502
+ // Normalizes a message range from various input formats into an IMAP-compatible
1503
+ // sequence string (e.g., "1:5,7,10:*"). Handles: numbers, "*", {all:true},
1504
+ // {uid:value}, search query objects (resolved via SEARCH), and arrays of numbers.
1435
1505
  async resolveRange(range, options) {
1436
1506
  if (typeof range === 'number' || typeof range === 'bigint') {
1437
1507
  range = range.toString();
1438
1508
  }
1439
1509
 
1440
- // special case, some servers allow this, some do not, so replace it with the last known EXISTS value
1510
+ // Replace "*" with the actual message count. Some servers reject bare "*"
1511
+ // in certain commands, and this also forces a sequence query (not UID).
1441
1512
  if (range === '*') {
1442
1513
  if (!this.mailbox.exists) {
1443
1514
  return false;
@@ -1453,7 +1524,8 @@ class ImapFlow extends EventEmitter {
1453
1524
  range = range.uid;
1454
1525
  options.uid = true;
1455
1526
  } else {
1456
- // resolve range by searching
1527
+ // Arbitrary search query object: run SEARCH to resolve it into
1528
+ // a set of UIDs, then pack into a compact range string.
1457
1529
  options.uid = true; // force UIDs instead of sequence numbers
1458
1530
  range = await this.run('SEARCH', range, options);
1459
1531
  if (range && range.length) {
@@ -1688,7 +1760,8 @@ class ImapFlow extends EventEmitter {
1688
1760
  this.initialReject = false;
1689
1761
  let err = new Error('Unexpected close');
1690
1762
  err.code = `ClosedAfterConnect${this.secureConnection ? 'TLS' : 'Text'}`;
1691
- // still has to go through the logic below
1763
+ // Reject via setImmediate so the rest of close() cleanup runs first.
1764
+ // The caller's connect() promise will reject after sockets are torn down.
1692
1765
  setImmediate(() => reject(err));
1693
1766
  }
1694
1767
 
@@ -1731,8 +1804,9 @@ class ImapFlow extends EventEmitter {
1731
1804
  return error;
1732
1805
  };
1733
1806
 
1734
- // Reject pending requests via setImmediate to ensure caller's promise chain
1735
- // is fully set up before rejection (prevents unhandled promise rejections)
1807
+ // Reject pending requests via setImmediate so this synchronous close()
1808
+ // method finishes first. This prevents unhandled promise rejections that
1809
+ // would occur if we rejected inline before callers set up .catch() handlers.
1736
1810
  if (pendingRequests.length) {
1737
1811
  let byeReason = this.byeReason;
1738
1812
  setImmediate(() => {
@@ -1809,6 +1883,11 @@ class ImapFlow extends EventEmitter {
1809
1883
  return;
1810
1884
  }
1811
1885
 
1886
+ // Socket teardown order matters when compression is active:
1887
+ // writeSocket may be a PassThrough (compression) or the raw socket (no compression).
1888
+ // Destroy the underlying socket first, then writeSocket (if different).
1889
+ // The second socket.destroy() block handles the case where writeSocket.destroy()
1890
+ // did not also destroy the underlying socket.
1812
1891
  if (this.socket && !this.socket.destroyed && this.writeSocket !== this.socket) {
1813
1892
  try {
1814
1893
  this.socket.destroy();
@@ -1834,7 +1913,8 @@ class ImapFlow extends EventEmitter {
1834
1913
  }
1835
1914
  }
1836
1915
 
1837
- // Explicit nullification to help garbage collection
1916
+ // Null out all socket and handler references so the GC can collect
1917
+ // them even if the ImapFlow instance itself is still referenced.
1838
1918
  this.socket = null;
1839
1919
  this.writeSocket = null;
1840
1920
  this._inflate = null;
@@ -1870,11 +1950,11 @@ class ImapFlow extends EventEmitter {
1870
1950
  * Returns current quota
1871
1951
  *
1872
1952
  * @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
1953
+ * @returns {Promise<QuotaResponse|Boolean>} Quota information or `false` if QUOTA extension is not supported or requested path does not exist
1874
1954
  *
1875
1955
  * @example
1876
1956
  * let quota = await client.getQuota();
1877
- * console.log(quota.storage.used, quota.storage.available)
1957
+ * console.log(quota.storage.used, quota.storage.limit)
1878
1958
  */
1879
1959
  async getQuota(path) {
1880
1960
  path = path || 'INBOX';
@@ -1938,7 +2018,7 @@ class ImapFlow extends EventEmitter {
1938
2018
  * @property {String} path mailbox path
1939
2019
  * @property {String} name mailbox name (last part of path after delimiter)
1940
2020
  * @property {String} delimiter mailbox path delimiter, usually "." or "/"
1941
- * @property {String[]} flags list of flags for this mailbox
2021
+ * @property {Set<string>} flags list of flags for this mailbox
1942
2022
  * @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
2023
  * @property {Boolean} listed `true` if mailbox was found from the output of LIST command
1944
2024
  * @property {Boolean} subscribed `true` if mailbox was found from the output of LSUB command
@@ -2206,7 +2286,7 @@ class ImapFlow extends EventEmitter {
2206
2286
  * Sets flags for a message or message range
2207
2287
  *
2208
2288
  * @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
2289
+ * @param {string[]} flags Array of flags to set. Only flags that are permitted to set are used, other flags are ignored
2210
2290
  * @param {Object} [options]
2211
2291
  * @param {Boolean} [options.uid] If `true` then uses UID {@link SequenceString} instead of sequence numbers
2212
2292
  * @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 +2320,7 @@ class ImapFlow extends EventEmitter {
2240
2320
  * Adds flags for a message or message range
2241
2321
  *
2242
2322
  * @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
2323
+ * @param {string[]} flags Array of flags to set. Only flags that are permitted to set are used, other flags are ignored
2244
2324
  * @param {Object} [options]
2245
2325
  * @param {Boolean} [options.uid] If `true` then uses UID {@link SequenceString} instead of sequence numbers
2246
2326
  * @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 +2354,7 @@ class ImapFlow extends EventEmitter {
2274
2354
  * Remove specific flags from a message or message range
2275
2355
  *
2276
2356
  * @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
2357
+ * @param {string[]} flags Array of flags to remove. Only flags that are permitted to set are used, other flags are ignored
2278
2358
  * @param {Object} [options]
2279
2359
  * @param {Boolean} [options.uid] If `true` then uses UID {@link SequenceString} instead of sequence numbers
2280
2360
  * @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 +2388,7 @@ class ImapFlow extends EventEmitter {
2308
2388
  * Sets a colored flag for an email. Only supported by mail clients like Apple Mail
2309
2389
  *
2310
2390
  * @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'
2391
+ * @param {string} color The color to set. One of 'red', 'orange', 'yellow', 'green', 'blue', 'purple', and 'grey'
2312
2392
  * @param {Object} [options]
2313
2393
  * @param {Boolean} [options.uid] If `true` then uses UID {@link SequenceString} instead of sequence numbers
2314
2394
  * @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 +2605,13 @@ class ImapFlow extends EventEmitter {
2525
2605
  * @property {boolean | Object} [source] if `true` then include full message in the response
2526
2606
  * @property {Number} [source.start] include full message in the response starting from *start* byte
2527
2607
  * @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)
2608
+ * @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
2609
  * @property {Boolean} [labels] if `true` then include GMail labels in the response (only if server supports `X-GM-EXT-1` extension)
2530
2610
  * @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
2611
  * @property {string[]} [bodyParts] An array of BODYPART identifiers to include in the response
2612
+ * @property {Boolean} [fast] IMAP macro equivalent to `flags`, `internalDate`, `size`
2613
+ * @property {Boolean} [all] IMAP macro equivalent to `flags`, `internalDate`, `size`, `envelope`
2614
+ * @property {Boolean} [full] IMAP macro equivalent to `flags`, `internalDate`, `size`, `envelope`, `bodyStructure`
2532
2615
  */
2533
2616
 
2534
2617
  /**
@@ -2584,7 +2667,7 @@ class ImapFlow extends EventEmitter {
2584
2667
  * @property {Buffer} [source] message source for the requested byte range
2585
2668
  * @property {BigInt} [modseq] message Modseq number. Always included if the server supports CONDSTORE extension
2586
2669
  * @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
2670
+ * @property {String} [threadId] unique thread ID. Only present if server supports `OBJECTID` or `X-GM-EXT-1` extension
2588
2671
  * @property {Set<string>} [labels] a Set of labels. Only present if server supports `X-GM-EXT-1` extension
2589
2672
  * @property {Number} [size] message size
2590
2673
  * @property {Set<string>} [flags] a set of message flags
@@ -2629,6 +2712,11 @@ class ImapFlow extends EventEmitter {
2629
2712
  return false;
2630
2713
  }
2631
2714
 
2715
+ // Push/pull coordination for the async generator pattern:
2716
+ // The FETCH command handler pushes results into rowQueue via onUntaggedFetch.
2717
+ // The generator consumer pulls via getNext(). The `push` callback bridges the
2718
+ // two: when the consumer is waiting and the queue is empty, `push` is set to
2719
+ // a function that wakes up the consumer when new data arrives.
2632
2720
  let finished = false;
2633
2721
  let push = false;
2634
2722
  let rowQueue = [];
@@ -2649,7 +2737,7 @@ class ImapFlow extends EventEmitter {
2649
2737
  return resolve(null);
2650
2738
  }
2651
2739
 
2652
- // wait until data is pushed to queue and try again
2740
+ // No data available yet; register a wakeup callback
2653
2741
  push = () => {
2654
2742
  push = false;
2655
2743
  check();
@@ -2658,6 +2746,10 @@ class ImapFlow extends EventEmitter {
2658
2746
  check();
2659
2747
  });
2660
2748
 
2749
+ // Fire-and-forget the FETCH command. It runs in the background while
2750
+ // the generator yields results. Each untagged FETCH response is paired
2751
+ // with a `next` callback that acts as backpressure: the FETCH handler
2752
+ // won't process the next response until the consumer calls next().
2661
2753
  this.run('FETCH', range, query, {
2662
2754
  uid: !!options.uid,
2663
2755
  binary: options.binary,
@@ -2697,6 +2789,7 @@ class ImapFlow extends EventEmitter {
2697
2789
 
2698
2790
  if (res !== null) {
2699
2791
  yield res.response;
2792
+ // Signal the FETCH handler to process the next untagged response
2700
2793
  res.next();
2701
2794
  }
2702
2795
  }
@@ -2833,8 +2926,9 @@ class ImapFlow extends EventEmitter {
2833
2926
  let uid = false;
2834
2927
 
2835
2928
  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
2929
+ // Special handling for part "1": in single-node emails (no childNodes),
2930
+ // the body is accessed via "TEXT" rather than "1", and headers via
2931
+ // "HEADER" instead of "1.MIME". Check bodyStructure to detect this.
2838
2932
  let response = await this.fetchOne(range, { uid: true, bodyStructure: true }, options);
2839
2933
 
2840
2934
  if (!response) {
@@ -2994,6 +3088,13 @@ class ImapFlow extends EventEmitter {
2994
3088
  let output;
2995
3089
  let fetchAborted = false;
2996
3090
 
3091
+ // Build a decoder pipeline that progressively transforms the raw FETCH data:
3092
+ // 1. Transfer-encoding decoder (base64 or quoted-printable -> binary)
3093
+ // 2. Format decoder (format=flowed -> plain text, if applicable)
3094
+ // 3. Charset decoder (non-UTF-8 -> UTF-8, for text parts only)
3095
+ // 4. Byte limiter (enforces maxBytes cap)
3096
+ // `stream` is the head of the pipeline (where raw chunks are written),
3097
+ // `output` is the tail (what the caller reads from).
2997
3098
  switch (meta.encoding) {
2998
3099
  case 'base64':
2999
3100
  output = stream = new libbase64.Decoder();
@@ -3007,7 +3108,7 @@ class ImapFlow extends EventEmitter {
3007
3108
 
3008
3109
  let isTextNode = ['text/html', 'text/plain', 'text/x-amp-html'].includes(meta.contentType) || (part === '1' && !meta.contentType);
3009
3110
  if ((!meta.disposition || meta.disposition === 'inline') && isTextNode) {
3010
- // flowed text
3111
+ // RFC 3676 format=flowed text: unwrap soft line breaks
3011
3112
  if (meta.flowed) {
3012
3113
  let flowDecoder = new FlowedDecoder({
3013
3114
  delSp: meta.delSp
@@ -3018,7 +3119,8 @@ class ImapFlow extends EventEmitter {
3018
3119
  output = output.pipe(flowDecoder);
3019
3120
  }
3020
3121
 
3021
- // not utf-8 text
3122
+ // Convert non-UTF-8 charsets to UTF-8 via a streaming decoder.
3123
+ // ASCII and UTF-8 need no conversion. Unknown charsets are left as-is.
3022
3124
  if (meta.charset && !['ascii', 'usascii', 'utf8'].includes(meta.charset.toLowerCase().replace(/[^a-z0-9]+/g, ''))) {
3023
3125
  try {
3024
3126
  let decoder = getDecoder(meta.charset);
@@ -3059,6 +3161,9 @@ class ImapFlow extends EventEmitter {
3059
3161
  return stream.write(chunk);
3060
3162
  };
3061
3163
 
3164
+ // Fetch remaining chunks in a loop, writing each to the decoder stream.
3165
+ // Stops when the server returns a short chunk (< chunkSize), the byte
3166
+ // limiter is satisfied, or the consumer destroys the output stream.
3062
3167
  let fetchAllParts = async () => {
3063
3168
  while (hasMore && !limiter.limited && !fetchAborted) {
3064
3169
  let { chunk } = await getNextPart();
@@ -3108,6 +3213,11 @@ class ImapFlow extends EventEmitter {
3108
3213
  }
3109
3214
  };
3110
3215
 
3216
+ // Kick off the download pipeline asynchronously. The first chunk was
3217
+ // already fetched above (to get metadata); write it to the decoder
3218
+ // stream and then fetch remaining chunks via fetchAllParts().
3219
+ // setImmediate ensures the caller gets the {meta, content} return
3220
+ // value before streaming begins.
3111
3221
  setImmediate(() => {
3112
3222
  let writeResult;
3113
3223
  try {
@@ -3180,7 +3290,7 @@ class ImapFlow extends EventEmitter {
3180
3290
  * Fetch multiple attachments as Buffer values
3181
3291
  *
3182
3292
  * @param {SequenceString} range UID or sequence number for the message to fetch
3183
- * @param {String} parts A list of bodystructure parts
3293
+ * @param {String[]} parts A list of bodystructure parts
3184
3294
  * @param {Object} [options]
3185
3295
  * @param {Boolean} [options.uid] If `true` then uses UID number instead of sequence number for `range`
3186
3296
  * @returns {Promise<Object>} Download data object
@@ -3337,11 +3447,14 @@ class ImapFlow extends EventEmitter {
3337
3447
  return result;
3338
3448
  }
3339
3449
 
3450
+ // Mailbox lock queue processor. Implements a mutex pattern: only one lock
3451
+ // is active at a time. When the active lock is released, the next queued
3452
+ // lock is processed. The `processingLock` flag prevents concurrent runs
3453
+ // of this method (which could happen via setImmediate re-entry from release()).
3340
3454
  async processLocks() {
3341
- // Atomic test-and-set to prevent race condition
3342
3455
  const wasProcessing = this.processingLock;
3343
3456
  if (wasProcessing) {
3344
- // Another processor is already running, just exit
3457
+ // Another processor is already running; it will pick up new locks
3345
3458
  this.log.trace({
3346
3459
  msg: 'Mailbox locking queued',
3347
3460
  path: this.mailbox && this.mailbox.path,
@@ -3405,7 +3518,7 @@ class ImapFlow extends EventEmitter {
3405
3518
  }
3406
3519
 
3407
3520
  if (this.mailbox && this.mailbox.path === path && !!this.mailbox.readOnly === !!options.readOnly) {
3408
- // nothing to do here, already selected
3521
+ // Fast path: mailbox is already selected with the right access mode
3409
3522
  this.log.trace({
3410
3523
  msg: 'Mailbox lock acquired [existing]',
3411
3524
  path,
@@ -3415,10 +3528,10 @@ class ImapFlow extends EventEmitter {
3415
3528
  });
3416
3529
  this.currentLock = lock;
3417
3530
  resolve({ path, release });
3418
- break; // Wait for this lock to be released
3531
+ break; // Stop processing; next lock waits for release()
3419
3532
  } else {
3420
3533
  try {
3421
- // Try to open. Throws if mailbox does not exists or can't open
3534
+ // Need to SELECT/EXAMINE a different mailbox
3422
3535
  await this.mailboxOpen(path, options);
3423
3536
  this.log.trace({
3424
3537
  msg: 'Mailbox lock acquired [selected]',
@@ -3432,6 +3545,9 @@ class ImapFlow extends EventEmitter {
3432
3545
  break; // Wait for this lock to be released
3433
3546
  } catch (err) {
3434
3547
  if (err.responseStatus === 'NO') {
3548
+ // SELECT failed with NO -- verify whether the mailbox exists
3549
+ // at all by running LIST. This sets mailboxMissing on the error
3550
+ // so the caller can distinguish "doesn't exist" from other failures.
3435
3551
  try {
3436
3552
  let folders = await this.run('LIST', '', path, { listOnly: true });
3437
3553
  if (!folders || !folders.length) {
@@ -3458,9 +3574,10 @@ class ImapFlow extends EventEmitter {
3458
3574
  } finally {
3459
3575
  this.processingLock = false;
3460
3576
 
3461
- // Check if new locks were added while we were processing
3577
+ // New locks may have been queued while we were processing (e.g.,
3578
+ // a lock that failed immediately and the next getMailboxLock call
3579
+ // arrived before we finished). Schedule another run if needed.
3462
3580
  if (this.locks.length && !this.currentLock) {
3463
- // Process any remaining locks
3464
3581
  setImmediate(() => {
3465
3582
  this.processLocks().catch(err => this.log.error({ err, cid: this.id }));
3466
3583
  });
@@ -3561,6 +3678,14 @@ class ImapFlow extends EventEmitter {
3561
3678
  return synteticLogger;
3562
3679
  }
3563
3680
 
3681
+ /**
3682
+ * Detaches sockets from the IMAP pipeline. Useful for upgrading the connection
3683
+ * (e.g., STARTTLS) or transferring socket ownership.
3684
+ *
3685
+ * @returns {Object} Socket objects
3686
+ * @returns {Object} return.readSocket The read socket (inflated socket if compression is enabled, raw socket otherwise)
3687
+ * @returns {Object} return.writeSocket The write socket
3688
+ */
3564
3689
  unbind() {
3565
3690
  this.socket.unpipe(this.streamer);
3566
3691
  if (this._inflate) {
@@ -3620,6 +3745,9 @@ class ImapFlow extends EventEmitter {
3620
3745
  * @type {Object}
3621
3746
  * @property {String} path mailbox path this event applies to
3622
3747
  * @property {Number} seq sequence number of deleted message
3748
+ * @property {Boolean} vanished `true` if message was expunged via VANISHED response
3749
+ * @property {Number} [uid] UID of expunged message (when `vanished` is `true`)
3750
+ * @property {Boolean} [earlier] `true` for VANISHED EARLIER responses
3623
3751
  * @example
3624
3752
  * client.on('expunge', data=>{
3625
3753
  * console.log(`Message #${data.seq} was deleted from "${data.path}"`);
@@ -3636,6 +3764,7 @@ class ImapFlow extends EventEmitter {
3636
3764
  * @property {Number} [uid] UID number of updated message (if server provided this value)
3637
3765
  * @property {BigInt} [modseq] Updated modseq number for the mailbox (if server provided this value)
3638
3766
  * @property {Set<string>} flags A set of all flags for the updated message
3767
+ * @property {String} [flagColor] flag color like "red", or "yellow". Derived from the `flags` Set using Apple Mail color rules
3639
3768
  * @example
3640
3769
  * client.on('flags', data=>{
3641
3770
  * console.log(`Flag set for #${data.seq} is now "${Array.from(data.flags).join(', ')}"`);
@@ -3671,7 +3800,7 @@ class ImapFlow extends EventEmitter {
3671
3800
  * @type {Object}
3672
3801
  * @example
3673
3802
  * client.on('log', entry => {
3674
- * console.log(`${log.cid} ${log.msg}`);
3803
+ * console.log(`${entry.cid} ${entry.msg}`);
3675
3804
  * });
3676
3805
  */
3677
3806