imapflow 2.2.11 → 2.4.0

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/CHANGELOG.md +25 -0
  2. package/dist/cjs/commands/append.d.ts +1 -1
  3. package/dist/cjs/commands/append.js +15 -4
  4. package/dist/cjs/commands/compress.js +2 -2
  5. package/dist/cjs/commands/idle.js +9 -9
  6. package/dist/cjs/commands/status.js +2 -4
  7. package/dist/cjs/connection-deadline.d.ts +1 -1
  8. package/dist/cjs/connection-deadline.js +2 -4
  9. package/dist/cjs/download.js +42 -26
  10. package/dist/cjs/errors.d.ts +15 -0
  11. package/dist/cjs/errors.js +22 -0
  12. package/dist/cjs/handler/imap-compiler.d.ts +4 -4
  13. package/dist/cjs/handler/imap-compiler.js +18 -18
  14. package/dist/cjs/handler/imap-parser.js +28 -3
  15. package/dist/cjs/handler/imap-stream.js +9 -8
  16. package/dist/cjs/handler/limits.d.ts +1 -1
  17. package/dist/cjs/handler/limits.js +5 -7
  18. package/dist/cjs/handler/parser-instance.js +23 -38
  19. package/dist/cjs/handler/token-parser.d.ts +12 -0
  20. package/dist/cjs/handler/token-parser.js +44 -104
  21. package/dist/cjs/imap-flow.d.ts +4 -3
  22. package/dist/cjs/imap-flow.js +98 -72
  23. package/dist/cjs/package-info.d.ts +1 -1
  24. package/dist/cjs/package-info.js +1 -1
  25. package/dist/cjs/proxy-connection.js +2 -5
  26. package/dist/cjs/search-compiler.js +2 -3
  27. package/dist/cjs/tools.d.ts +6 -3
  28. package/dist/cjs/tools.js +13 -13
  29. package/dist/cjs/types.d.ts +11 -0
  30. package/dist/esm/commands/append.d.ts +1 -1
  31. package/dist/esm/commands/append.js +15 -4
  32. package/dist/esm/commands/compress.js +2 -2
  33. package/dist/esm/commands/idle.js +9 -9
  34. package/dist/esm/commands/status.js +2 -4
  35. package/dist/esm/connection-deadline.d.ts +1 -1
  36. package/dist/esm/connection-deadline.js +2 -4
  37. package/dist/esm/download.js +42 -26
  38. package/dist/esm/errors.d.ts +15 -0
  39. package/dist/esm/errors.js +21 -0
  40. package/dist/esm/handler/imap-compiler.d.ts +4 -4
  41. package/dist/esm/handler/imap-compiler.js +18 -18
  42. package/dist/esm/handler/imap-parser.js +28 -3
  43. package/dist/esm/handler/imap-stream.js +9 -8
  44. package/dist/esm/handler/limits.d.ts +1 -1
  45. package/dist/esm/handler/limits.js +5 -7
  46. package/dist/esm/handler/parser-instance.js +23 -38
  47. package/dist/esm/handler/token-parser.d.ts +12 -0
  48. package/dist/esm/handler/token-parser.js +44 -104
  49. package/dist/esm/imap-flow.d.ts +4 -3
  50. package/dist/esm/imap-flow.js +99 -73
  51. package/dist/esm/package-info.d.ts +1 -1
  52. package/dist/esm/package-info.js +1 -1
  53. package/dist/esm/proxy-connection.js +2 -5
  54. package/dist/esm/search-compiler.js +2 -3
  55. package/dist/esm/tools.d.ts +6 -3
  56. package/dist/esm/tools.js +14 -14
  57. package/dist/esm/types.d.ts +11 -0
  58. package/package.json +6 -3
@@ -60,6 +60,8 @@ var errors_js_2 = require("./errors.js");
60
60
  Object.defineProperty(exports, "AuthenticationFailure", { enumerable: true, get: function () { return errors_js_2.AuthenticationFailure; } });
61
61
  Object.defineProperty(exports, "ImapFlowErrorCode", { enumerable: true, get: function () { return errors_js_2.ImapFlowErrorCode; } });
62
62
  const GREETING_TIMEOUT = 16 * 1000;
63
+ // Status responses that can carry an ALERT response code
64
+ const ALERT_RESPONSES = new Set(['OK', 'NO', 'BAD', 'BYE', 'PREAUTH']);
63
65
  const UPGRADE_TIMEOUT = 10 * 1000;
64
66
  const SOCKET_TIMEOUT = 5 * 60 * 1000;
65
67
  // Ceiling for any throttle back-off wait. Both the connection-level back-off and the per-command
@@ -83,6 +85,10 @@ const RAW_SENSITIVE_COMMANDS = new Set(['LOGIN', 'AUTHENTICATE']);
83
85
  // Stand-in payload for a withheld raw client frame. Fixed width, so the entry says nothing
84
86
  // about the length of what it replaced.
85
87
  const RAW_HIDDEN_PLACEHOLDER = Buffer.from('(* value hidden *)\r\n').toString('base64');
88
+ // From this size up, a Buffer written as the last part of a command gets its CRLF as a socket
89
+ // write of its own instead of being copied to append it (see write())
90
+ const SEPARATE_LINE_BREAK_SIZE = 64 * 1024;
91
+ const CRLF = Buffer.from('\r\n');
86
92
  // Servers, matched by the name in their ID response, that advertise IMAP4rev2 next to
87
93
  // IMAP4rev1 and accept ENABLE IMAP4rev2, but misbehave once it is enabled. ENABLE cannot
88
94
  // be undone (RFC 5161), so these are kept in IMAP4rev1 mode from the start. Strato's
@@ -383,43 +389,47 @@ class ImapFlow extends node_events_1.EventEmitter {
383
389
  // commandParts holds the remaining parts (literal data, continuation); the CRLF
384
390
  // delimiter is only added when no more parts remain (the command is complete).
385
391
  let addLineBreak = !this.commandParts.length;
386
- let data;
392
+ // What goes to the socket, one write per entry. A large Buffer (the data of a literal)
393
+ // gets its CRLF as a write of its own, as concatenating would copy all of it.
394
+ let frames;
387
395
  if (typeof chunk === 'string') {
388
396
  if (addLineBreak) {
389
397
  chunk += '\r\n';
390
398
  }
391
- data = Buffer.from(chunk, 'binary');
399
+ frames = [Buffer.from(chunk, 'binary')];
392
400
  }
393
401
  else if (Buffer.isBuffer(chunk)) {
394
- if (addLineBreak) {
395
- data = Buffer.concat([chunk, Buffer.from('\r\n')]);
402
+ if (!addLineBreak) {
403
+ frames = [chunk];
396
404
  }
397
405
  else {
398
- data = chunk;
406
+ frames = chunk.length >= SEPARATE_LINE_BREAK_SIZE ? [chunk, CRLF] : [Buffer.concat([chunk, CRLF])];
399
407
  }
400
408
  }
401
409
  else {
402
410
  return false;
403
411
  }
404
- if (this.logRaw) {
405
- // Client frames of an authentication exchange carry credentials: the LOGIN
406
- // arguments, and for AUTHENTICATE also the continuation writes (SASL PLAIN
407
- // response, AUTH=LOGIN password, OAuth token payload) that bypass send(). The
408
- // parsed command log masks these, so the raw log must withhold them too, but
409
- // `data` still carries the placeholder rather than being dropped - the field is
410
- // part of the documented log format and consumers decode it unconditionally.
411
- this.log.trace({
412
- src: 'c',
413
- msg: 'write to socket',
414
- data: this.rawSensitiveCommand ? RAW_HIDDEN_PLACEHOLDER : data.toString('base64'),
415
- ...(this.rawSensitiveCommand ? { hidden: true } : {}),
416
- compress: !!this._deflate,
417
- secure: !!this.secureConnection,
418
- cid: this.id
419
- });
412
+ for (let data of frames) {
413
+ if (this.logRaw) {
414
+ // Client frames of an authentication exchange carry credentials: the LOGIN
415
+ // arguments, and for AUTHENTICATE also the continuation writes (SASL PLAIN
416
+ // response, AUTH=LOGIN password, OAuth token payload) that bypass send(). The
417
+ // parsed command log masks these, so the raw log must withhold them too, but
418
+ // `data` still carries the placeholder rather than being dropped - the field is
419
+ // part of the documented log format and consumers decode it unconditionally.
420
+ this.log.trace({
421
+ src: 'c',
422
+ msg: 'write to socket',
423
+ data: this.rawSensitiveCommand ? RAW_HIDDEN_PLACEHOLDER : data.toString('base64'),
424
+ ...(this.rawSensitiveCommand ? { hidden: true } : {}),
425
+ compress: !!this._deflate,
426
+ secure: !!this.secureConnection,
427
+ cid: this.id
428
+ });
429
+ }
430
+ this.writeBytesCounter += data.length;
431
+ this.writeSocket.write(data);
420
432
  }
421
- this.writeBytesCounter += data.length;
422
- this.writeSocket.write(data);
423
433
  }
424
434
  /**
425
435
  * Returns byte counters for the current connection.
@@ -445,6 +455,9 @@ class ImapFlow extends node_events_1.EventEmitter {
445
455
  // and once as a string (for logging, with sensitive data masked).
446
456
  // When LITERAL- or LITERAL+ extensions are available, the compiler can use
447
457
  // non-synchronizing literals to avoid waiting for server "+" continuation.
458
+ // Async although nothing in it awaits: a failure inside it rejects one microtask later, so a
459
+ // close() in the same tick settles the command with NoConnection first (see
460
+ // test/unhandled-rejection-test.ts)
448
461
  /** @internal */
449
462
  async send(data) {
450
463
  if (this.state === this.states.LOGOUT) {
@@ -454,11 +467,11 @@ class ImapFlow extends node_events_1.EventEmitter {
454
467
  // the socket watchdog.
455
468
  throw this.createNoConnectionError(false, { rejectedFrom: 'sendAfterLogout', command: data.command });
456
469
  }
457
- // Classify before the first await. Every frame of this command - the command line and
458
- // any continuation write that follows it - belongs to it until the next send(), because
470
+ // Classify once per command. Every frame of this command - the command line and any
471
+ // continuation write that follows it - belongs to it until the next send(), because
459
472
  // trySend() keeps one command in flight at a time. Reading currentRequest inside write()
460
- // instead would be racy: rejectCurrentRequest() can clear it while the two compiler
461
- // awaits below are pending, and the credential frame would then be logged in the clear.
473
+ // instead would be racy: rejectCurrentRequest() can clear it before a continuation frame
474
+ // is written, and the credential frame would then be logged in the clear.
462
475
  // Uppercased because the wire protocol is case-insensitive and exec() passes the
463
476
  // caller's spelling through unchanged. The command list covers the mechanisms whose
464
477
  // secret arrives in a continuation frame, which carries no attributes of its own; the
@@ -469,7 +482,7 @@ class ImapFlow extends node_events_1.EventEmitter {
469
482
  // Compile with asArray=true: splits output into parts for literal handling.
470
483
  // First part is the command text up to the first literal, remaining parts
471
484
  // are stored in this.commandParts and sent after server "+" continuations.
472
- let compiled = await (0, imap_handler_js_1.compiler)(data, {
485
+ let compiled = (0, imap_handler_js_1.compiler)(data, {
473
486
  asArray: true,
474
487
  // LITERAL- is part of base IMAP4rev2
475
488
  literalMinus: (0, tools_js_1.hasCapability)(this, 'LITERAL-') || this.capabilities.has('LITERAL+')
@@ -480,7 +493,7 @@ class ImapFlow extends node_events_1.EventEmitter {
480
493
  // Compile again for logging with isLogging=true: masks sensitive values
481
494
  // like passwords while producing a human-readable command string
482
495
  if (this.isLogLevelEnabled('debug')) {
483
- let logCompiled = await (0, imap_handler_js_1.compiler)(data, {
496
+ let logCompiled = (0, imap_handler_js_1.compiler)(data, {
484
497
  isLogging: true
485
498
  });
486
499
  this.log.debug({ src: 'c', msg: logCompiled.toString(), cid: this.id, comment: options.comment });
@@ -737,9 +750,7 @@ class ImapFlow extends node_events_1.EventEmitter {
737
750
  // parser would keep waiting on its backpressure callback forever, which is a
738
751
  // silent permanent hang. Fail closed instead.
739
752
  keepReading = false;
740
- let error = new Error('Failed to process server response');
741
- error.code = 'ResponseProcessingFailed';
742
- error._err = err;
753
+ let error = (0, errors_js_1.createImapError)('Failed to process server response', 'ResponseProcessingFailed', { _err: err });
743
754
  this.log.error({ msg: 'Failed to process server response', err, cid: this.id });
744
755
  this.rejectCurrentRequest(error);
745
756
  this.failProtocol(error);
@@ -790,6 +801,27 @@ class ImapFlow extends node_events_1.EventEmitter {
790
801
  this.rejectCurrentRequest(err);
791
802
  this.trySend().catch(sendErr => (0, tools_js_1.logConnectionError)(this, 'Failed to dispatch command', sendErr));
792
803
  }
804
+ // RFC 9051 section 7.1: the text of an ALERT must be presented to the user. It can come
805
+ // with any status response, tagged or untagged, the greeting and BYE included. Called for
806
+ // responses that carry the ALERT code.
807
+ /** @internal */
808
+ reportAlert(parsed) {
809
+ let response = (parsed.command || '').toUpperCase();
810
+ if (!ALERT_RESPONSES.has(response)) {
811
+ return;
812
+ }
813
+ let alert = {
814
+ message: (0, tools_js_1.getTextValues)(parsed.attributes)
815
+ .map(value => value.trim())
816
+ .join(' '),
817
+ response: response
818
+ };
819
+ if (parsed.tag && parsed.tag !== '*') {
820
+ alert.tag = parsed.tag;
821
+ }
822
+ this.log.info({ msg: 'Server alert', alert: alert.message, response: alert.response, cid: this.id });
823
+ (0, tools_js_1.emitSafe)(this, 'alert', alert);
824
+ }
793
825
  /**
794
826
  * Handles a single parsed server response: telemetry, continuation requests, response-code
795
827
  * section handlers, untagged handlers and tagged command completion.
@@ -841,7 +873,7 @@ class ImapFlow extends node_events_1.EventEmitter {
841
873
  // skipped when the level is off.
842
874
  let logLevel = /^\d+$/.test(parsed.command || '') && parsed.attributes && parsed.attributes[0] && parsed.attributes[0].value === 'FETCH' ? 'trace' : 'debug';
843
875
  if (this.isLogLevelEnabled(logLevel)) {
844
- let logCompiled = await (0, imap_handler_js_1.compiler)(parsed, {
876
+ let logCompiled = (0, imap_handler_js_1.compiler)(parsed, {
845
877
  isLogging: true
846
878
  });
847
879
  this.log[logLevel]({ src: 's', msg: logCompiled.toString(), cid: this.id, nullBytesRemoved: parsed.nullBytesRemoved });
@@ -884,6 +916,9 @@ class ImapFlow extends node_events_1.EventEmitter {
884
916
  // dereference must be guarded or one such line tears down the whole connection
885
917
  if (section && section.length && section[0] && section[0].type === 'ATOM' && typeof section[0].value === 'string') {
886
918
  let sectionKey = section[0].value.toUpperCase().trim();
919
+ if (sectionKey === 'ALERT') {
920
+ this.reportAlert(parsed);
921
+ }
887
922
  let sectionHandler = this.getSectionHandler(sectionKey);
888
923
  if (sectionHandler) {
889
924
  try {
@@ -944,12 +979,12 @@ class ImapFlow extends node_events_1.EventEmitter {
944
979
  // response ambiguous, so reject this request and fail the connection closed.
945
980
  let request = this.requestTagMap.get(parsed.tag);
946
981
  this.requestTagMap.delete(parsed.tag);
947
- let err = new Error('Server sent a tagged response for a command that was not in flight');
948
- err.code = 'UnexpectedTag';
949
- err.details = {
950
- received: parsed.tag,
951
- expected: this.currentRequest ? this.currentRequest.tag : null
952
- };
982
+ let err = (0, errors_js_1.createImapError)('Server sent a tagged response for a command that was not in flight', 'UnexpectedTag', {
983
+ details: {
984
+ received: parsed.tag,
985
+ expected: this.currentRequest ? this.currentRequest.tag : null
986
+ }
987
+ });
953
988
  this.log.error({ msg: 'Protocol desynchronization', err, cid: this.id });
954
989
  request.reject(err);
955
990
  this.failProtocol(err);
@@ -993,9 +1028,9 @@ class ImapFlow extends node_events_1.EventEmitter {
993
1028
  try {
994
1029
  err.executedCommand =
995
1030
  parsed.tag +
996
- (await (0, imap_handler_js_1.compiler)(request, {
1031
+ (0, imap_handler_js_1.compiler)(request, {
997
1032
  isLogging: true
998
- })).toString();
1033
+ }).toString();
999
1034
  }
1000
1035
  catch {
1001
1036
  // ignore
@@ -1048,9 +1083,7 @@ class ImapFlow extends node_events_1.EventEmitter {
1048
1083
  break;
1049
1084
  }
1050
1085
  default: {
1051
- let err = new Error('Invalid server response');
1052
- err.code = 'InvalidResponse';
1053
- err.response = parsed;
1086
+ let err = (0, errors_js_1.createImapError)('Invalid server response', 'InvalidResponse', { response: parsed });
1054
1087
  request.reject(err);
1055
1088
  break;
1056
1089
  }
@@ -1151,11 +1184,7 @@ class ImapFlow extends node_events_1.EventEmitter {
1151
1184
  this._socketTimeout =
1152
1185
  this._socketTimeout ||
1153
1186
  (() => {
1154
- const timeoutError = () => {
1155
- const err = new Error('Socket timeout');
1156
- err.code = 'ETIMEOUT';
1157
- return err;
1158
- };
1187
+ const timeoutError = () => (0, errors_js_1.createImapError)('Socket timeout', 'ETIMEOUT');
1159
1188
  if (!this.usable || !this.socket || this.socket.destroyed) {
1160
1189
  this.emitError(timeoutError());
1161
1190
  return;
@@ -1339,7 +1368,10 @@ class ImapFlow extends node_events_1.EventEmitter {
1339
1368
  // to call deflate.flush() after each IMAP command to push all pending
1340
1369
  // compressed bytes to the server immediately (IMAP is request-response).
1341
1370
  const writeSocket = new node_stream_1.PassThrough({
1342
- highWaterMark: 64 * 1024 // 64KB buffer limit to prevent excessive memory usage
1371
+ highWaterMark: 64 * 1024, // 64KB buffer limit to prevent excessive memory usage
1372
+ // read() hands out each written chunk as is: in byte mode it joins whatever is
1373
+ // queued into a new Buffer, which copies every large literal once more
1374
+ readableObjectMode: true
1343
1375
  });
1344
1376
  this.writeSocket = writeSocket;
1345
1377
  /* c8 ignore start */ // destroySoon override is never invoked by ImapFlow (close() calls destroy()); kept for stream API completeness
@@ -1467,9 +1499,7 @@ class ImapFlow extends node_events_1.EventEmitter {
1467
1499
  // socket; injection that still races in afterwards corrupts the TLS handshake and
1468
1500
  // is rejected there instead (with a generic TLS error rather than STARTTLS_INJECTION).
1469
1501
  const failSTARTTLSInjection = () => {
1470
- let err = new Error('Server sent data after the STARTTLS response and before the TLS handshake; possible plaintext-injection attack');
1471
- err.code = 'STARTTLS_INJECTION';
1472
- err.tlsFailed = true;
1502
+ let err = (0, errors_js_1.createImapError)('Server sent data after the STARTTLS response and before the TLS handshake; possible plaintext-injection attack', 'STARTTLS_INJECTION', { tlsFailed: true });
1473
1503
  this.closeAfter();
1474
1504
  return err;
1475
1505
  };
@@ -1552,8 +1582,7 @@ class ImapFlow extends node_events_1.EventEmitter {
1552
1582
  socketPlain.on('error', settle);
1553
1583
  /* c8 ignore start */ // UPGRADE_TIMEOUT is 10s; firing it deterministically would make the test suite hang
1554
1584
  this.upgradeTimeout = setTimeout(() => {
1555
- let err = new Error('Failed to upgrade connection in required time');
1556
- err.code = 'UPGRADE_TIMEOUT';
1585
+ let err = (0, errors_js_1.createImapError)('Failed to upgrade connection in required time', 'UPGRADE_TIMEOUT');
1557
1586
  settle(err);
1558
1587
  }, UPGRADE_TIMEOUT);
1559
1588
  /* c8 ignore stop */
@@ -2057,9 +2086,7 @@ class ImapFlow extends node_events_1.EventEmitter {
2057
2086
  async connect() {
2058
2087
  if (this._connectCalled) {
2059
2088
  // Prevent re-using ImapFlow instances by allowing to call connect just once.
2060
- let err = new Error('Can not re-use ImapFlow instance');
2061
- err.code = 'InstanceReused';
2062
- throw err;
2089
+ throw (0, errors_js_1.createImapError)('Can not re-use ImapFlow instance', 'InstanceReused');
2063
2090
  }
2064
2091
  this._connectCalled = true;
2065
2092
  let closedError = () => this.createNoConnectionError(this.byeReason, { rejectedFrom: 'connect' });
@@ -2147,14 +2174,14 @@ class ImapFlow extends node_events_1.EventEmitter {
2147
2174
  (0, proxy_connection_js_1.detachEarlyErrorHandler)(socket);
2148
2175
  this.configureSocket(this.socket);
2149
2176
  this.greetingTimeout = setTimeout(() => {
2150
- let err = new Error(
2177
+ let err = (0, errors_js_1.createImapError)(
2151
2178
  /* c8 ignore next */ // the greeting-timeout test uses a plaintext socket; the secure-socket branch of this hint is not separately exercised
2152
- `Failed to receive greeting from server in required time${!this.secureConnection ? '. Maybe should use TLS?' : ''}`);
2153
- err.code = 'GREETING_TIMEOUT';
2154
- err.details = {
2155
- /* c8 ignore next */ // firing the timeout with the default (large) value would hang the suite, so only the explicit-option path is tested
2156
- greetingTimeout: this.options.greetingTimeout || GREETING_TIMEOUT
2157
- };
2179
+ `Failed to receive greeting from server in required time${!this.secureConnection ? '. Maybe should use TLS?' : ''}`, 'GREETING_TIMEOUT', {
2180
+ details: {
2181
+ /* c8 ignore next */ // firing the timeout with the default (large) value would hang the suite, so only the explicit-option path is tested
2182
+ greetingTimeout: this.options.greetingTimeout || GREETING_TIMEOUT
2183
+ }
2184
+ });
2158
2185
  this.log.error({ err, cid: this.id });
2159
2186
  this.closeAfter();
2160
2187
  reject(err);
@@ -2378,9 +2405,9 @@ class ImapFlow extends node_events_1.EventEmitter {
2378
2405
  let reject = this.initialReject;
2379
2406
  this.initialResolve = false;
2380
2407
  this.initialReject = false;
2381
- let err = new Error('Unexpected close');
2408
+ let err = (0, errors_js_1.createImapError)('Unexpected close',
2382
2409
  /* c8 ignore next */ // closing a pending connect over an already-secure socket (the TLS branch) is not separately exercised
2383
- err.code = `ClosedAfterConnect${this.secureConnection ? 'TLS' : 'Text'}`;
2410
+ this.secureConnection ? 'ClosedAfterConnectTLS' : 'ClosedAfterConnectText');
2384
2411
  // Surface the server's BYE reason (e.g. "Too many connections") when the
2385
2412
  // connection was closed by an untagged BYE, so the caller sees why.
2386
2413
  if (this.byeReason) {
@@ -2891,7 +2918,7 @@ class ImapFlow extends node_events_1.EventEmitter {
2891
2918
  * Appends a new message to a mailbox
2892
2919
  *
2893
2920
  * @param path Mailbox path to upload the message to (unicode string). If value is an array then it is joined using current delimiter symbols. Namespace prefix is added automatically if required.
2894
- * @param content RFC822 formatted email message
2921
+ * @param content RFC822 formatted email message, as a Buffer, Uint8Array or string
2895
2922
  * @param flags an array of flags to be set for the uploaded message
2896
2923
  * @param idate internal date to be set for the message
2897
2924
  * @returns info about uploaded message
@@ -3193,7 +3220,8 @@ class ImapFlow extends node_events_1.EventEmitter {
3193
3220
  * // download body part nr '1.2' from latest message
3194
3221
  * let download = await client.download('*', '1.2');
3195
3222
  * if (download.content) {
3196
- * download.content.pipe(fs.createWriteStream(download.meta.filename));
3223
+ * // pipeline() rejects when the download fails, a plain .pipe() would not report it
3224
+ * await stream.promises.pipeline(download.content, fs.createWriteStream(download.meta.filename));
3197
3225
  * }
3198
3226
  */
3199
3227
  async download(range, part, options) {
@@ -3530,9 +3558,7 @@ class ImapFlow extends node_events_1.EventEmitter {
3530
3558
  const idx = this.locks.indexOf(lockEntry);
3531
3559
  if (idx !== -1) {
3532
3560
  this.locks.splice(idx, 1);
3533
- let err = new Error('Timed out waiting for mailbox lock');
3534
- err.code = 'LockTimeout';
3535
- err.lockId = lockEntry.lockId;
3561
+ let err = (0, errors_js_1.createImapError)('Timed out waiting for mailbox lock', 'LockTimeout', { lockId: lockEntry.lockId });
3536
3562
  reject(err);
3537
3563
  }
3538
3564
  }, Number(lockOptions.acquireTimeout));
@@ -1,3 +1,3 @@
1
1
  export declare const name = "imapflow";
2
- export declare const version = "2.2.11";
2
+ export declare const version = "2.4.0";
3
3
  export declare const homepage = "https://imapflow.com/";
@@ -3,5 +3,5 @@
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
4
  exports.homepage = exports.version = exports.name = void 0;
5
5
  exports.name = "imapflow";
6
- exports.version = "2.2.11";
6
+ exports.version = "2.4.0";
7
7
  exports.homepage = "https://imapflow.com/";
@@ -10,6 +10,7 @@ const node_net_1 = __importDefault(require("node:net"));
10
10
  const node_tls_1 = __importDefault(require("node:tls"));
11
11
  const connection_deadline_js_1 = require("./connection-deadline.js");
12
12
  const tools_js_1 = require("./tools.js");
13
+ const errors_js_1 = require("./errors.js");
13
14
  // Cap the CONNECT response buffered before the header terminator, so a proxy that never sends
14
15
  // \r\n\r\n cannot grow memory without bound.
15
16
  const MAX_RESPONSE_HEADER_BYTES = 64 * 1024;
@@ -33,11 +34,7 @@ const redactUrl = (proxyUrl) => {
33
34
  }
34
35
  return redacted.href;
35
36
  };
36
- const proxyError = (message, code) => {
37
- let err = new Error(message);
38
- err.code = code || 'ProxyError';
39
- return err;
40
- };
37
+ const proxyError = (message, code) => (0, errors_js_1.createImapError)(message, code || 'ProxyError');
41
38
  // URL userinfo is percent-encoded, so it has to be decoded before it can be used as credentials.
42
39
  // A password containing a bare '%' is not valid percent-encoding and makes decodeURIComponent
43
40
  // throw, so such values are used as they came in rather than failing the connection.
@@ -3,6 +3,7 @@
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
4
  exports.searchCompiler = void 0;
5
5
  const tools_js_1 = require("./tools.js");
6
+ const errors_js_1 = require("./errors.js");
6
7
  // Matches any character outside the ASCII range
7
8
  const UNICODE_PATTERN = /[^\x00-\x7F]/;
8
9
  /**
@@ -104,9 +105,7 @@ let processDateField = (attributes, term, value) => {
104
105
  * @param message - Error message
105
106
  */
106
107
  let fail = (code, message) => {
107
- let error = new Error(message);
108
- error.code = code;
109
- throw error;
108
+ throw (0, errors_js_1.createImapError)(message, code);
110
109
  };
111
110
  /**
112
111
  * Checks whether any search value was compiled into a literal, which only
@@ -1,6 +1,6 @@
1
1
  import type { Transform } from 'node:stream';
2
2
  import type { ImapFlow } from './imap-flow.js';
3
- import { type ConnectionErrorSite, type ImapFlowError } from './errors.js';
3
+ import { ImapFlowErrorCode, type ConnectionErrorSite, type ImapFlowError } from './errors.js';
4
4
  import type { ImapAttributeList, ImapAttributeNode, ImapResponse } from './handler/types.js';
5
5
  import type { FetchMessageObject, ListResponse, ListTreeResponse, MailboxObject, MessageEnvelopeObject, MessageStructureObject, ImapFlowEvents, StatusQuery } from './types.js';
6
6
  export { AuthenticationFailure } from './errors.js';
@@ -33,7 +33,7 @@ export type CharsetDecoder = Transform & {
33
33
  * @param meta - Fields to stamp on the error
34
34
  * @returns The stamped error
35
35
  */
36
- export declare function buildConnectionError(cid: string, code: string, message: string, meta?: ConnectionErrorSite | undefined): ImapFlowError;
36
+ export declare function buildConnectionError(cid: string, code: ImapFlowErrorCode, message: string, meta?: ConnectionErrorSite | undefined): ImapFlowError;
37
37
  /**
38
38
  * Re-stamps an existing connection error for a different rejection site.
39
39
  *
@@ -122,9 +122,12 @@ export declare function logConnectionError(connection: ImapFlow, msg: string, er
122
122
  /**
123
123
  * Whether a command failed because the server answered it with a tagged NO or BAD, as opposed
124
124
  * to a lost connection, a timeout or a local failure that says nothing about the server's answer.
125
+ * A throttled command (ETHROTTLE, Microsoft 365 answers it with BAD) was not refused: the server
126
+ * asks to try again later, so a throttled login is not a rejected credential and a throttled
127
+ * IDLE does not mean the server lacks IDLE.
125
128
  *
126
129
  * @param err - The error the command failed with
127
- * @returns True for a tagged NO or BAD
130
+ * @returns True for a tagged NO or BAD that is not throttling
128
131
  */
129
132
  export declare function isServerRefusal(err: ImapFlowError | null | undefined): boolean;
130
133
  /**
package/dist/cjs/tools.js CHANGED
@@ -146,13 +146,7 @@ const CONNECTION_ERROR_SITE_KEYS = ['rejectedFrom', 'command', 'path'];
146
146
  * @returns The stamped error
147
147
  */
148
148
  function buildConnectionError(cid, code, message, meta) {
149
- const error = new Error(message);
150
- error.code = code;
151
- error.cid = cid;
152
- if (meta) {
153
- Object.assign(error, meta);
154
- }
155
- return error;
149
+ return (0, errors_js_1.createImapError)(message, code, { cid, ...meta });
156
150
  }
157
151
  /**
158
152
  * Re-stamps an existing connection error for a different rejection site.
@@ -174,6 +168,7 @@ function buildConnectionError(cid, code, message, meta) {
174
168
  * @returns A separate error describing the same failure at the new site
175
169
  */
176
170
  function restampConnectionError(err, meta) {
171
+ // the original code is carried over unchanged, whatever its origin
177
172
  let error = buildConnectionError(err.cid, err.code, err.message, err);
178
173
  for (let key of CONNECTION_ERROR_SITE_KEYS) {
179
174
  delete error[key];
@@ -273,12 +268,15 @@ function logConnectionError(connection, msg, err) {
273
268
  /**
274
269
  * Whether a command failed because the server answered it with a tagged NO or BAD, as opposed
275
270
  * to a lost connection, a timeout or a local failure that says nothing about the server's answer.
271
+ * A throttled command (ETHROTTLE, Microsoft 365 answers it with BAD) was not refused: the server
272
+ * asks to try again later, so a throttled login is not a rejected credential and a throttled
273
+ * IDLE does not mean the server lacks IDLE.
276
274
  *
277
275
  * @param err - The error the command failed with
278
- * @returns True for a tagged NO or BAD
276
+ * @returns True for a tagged NO or BAD that is not throttling
279
277
  */
280
278
  function isServerRefusal(err) {
281
- return !!err && (err.responseStatus === 'NO' || err.responseStatus === 'BAD');
279
+ return !!err && (err.responseStatus === 'NO' || err.responseStatus === 'BAD') && err.code !== errors_js_1.ImapFlowErrorCode.ETHROTTLE;
282
280
  }
283
281
  /**
284
282
  * Checks whether IMAP4rev2 semantics are active for the connection: either the
@@ -524,12 +522,13 @@ function getTextValues(attributes) {
524
522
  * @param response - Parsed IMAP server response
525
523
  * @returns Compiled response text, or false if no response
526
524
  */
525
+ // Async although nothing in it awaits any more: `imapflow/lib/tools` deep imports may chain on the promise
527
526
  async function getErrorText(response) {
528
527
  if (!response) {
529
528
  return false;
530
529
  }
531
530
  try {
532
- return (await (0, imap_handler_js_1.compiler)(response)).toString();
531
+ return (0, imap_handler_js_1.compiler)(response).toString();
533
532
  }
534
533
  catch {
535
534
  // The wire encoder refuses values that cannot be expressed as a valid IMAP
@@ -538,7 +537,7 @@ async function getErrorText(response) {
538
537
  // tolerates stray bytes inside an OK/NO/BAD atom, and those bytes then have
539
538
  // no valid re-encoding. This text is diagnostic, so fall back to the logging
540
539
  // encoder rather than replacing the server's error with an encoding failure.
541
- return (await (0, imap_handler_js_1.compiler)(response, { isLogging: true })).toString();
540
+ return (0, imap_handler_js_1.compiler)(response, { isLogging: true }).toString();
542
541
  }
543
542
  }
544
543
  /**
@@ -752,6 +751,7 @@ function mergeFetchRows(rows) {
752
751
  * @param connection - Connection the response arrived on, decodes Gmail labels like mailbox names
753
752
  * @returns Formatted message object with properties like seq, uid, flags, envelope, etc.
754
753
  */
754
+ // Async although nothing in it awaits any more: `imapflow/lib/tools` deep imports may chain on the promise
755
755
  async function formatMessageResponse(untagged, mailbox, idHashAlgorithm, connection) {
756
756
  let map = {};
757
757
  // The sequence number indexes into mailbox state, so an unusable one is dropped rather
@@ -774,9 +774,9 @@ async function formatMessageResponse(untagged, mailbox, idHashAlgorithm, connect
774
774
  let attribute = attributes[i];
775
775
  if (i % 2 === 0) {
776
776
  origin = false;
777
- key = (await (0, imap_handler_js_1.compiler)({
777
+ key = (0, imap_handler_js_1.compiler)({
778
778
  attributes: [attribute]
779
- }))
779
+ })
780
780
  .toString()
781
781
  .toLowerCase()
782
782
  .replace(/<(\d+)(\.\d+)?>$/, (match, start) => {
@@ -805,6 +805,15 @@ export interface LogEvent {
805
805
  /** Additional log data */
806
806
  [key: string]: any;
807
807
  }
808
+ /** A message the server marked with the ALERT response code, to be shown to the user (RFC 9051 section 7.1) */
809
+ export interface AlertEvent {
810
+ /** The alert text */
811
+ message: string;
812
+ /** The response that carried it */
813
+ response: 'OK' | 'NO' | 'BAD' | 'BYE' | 'PREAUTH';
814
+ /** Tag of the command it completed, missing for an untagged response */
815
+ tag?: string | undefined;
816
+ }
808
817
  export interface ResponseEvent {
809
818
  /** Response type */
810
819
  response: string;
@@ -870,4 +879,6 @@ export interface ImapFlowEvents {
870
879
  log: [entry: LogEvent];
871
880
  /** Response event */
872
881
  response: [response: ResponseEvent];
882
+ /** The server sent an ALERT: text meant for the user, such as a planned shutdown or an account problem */
883
+ alert: [alert: AlertEvent];
873
884
  }
@@ -19,4 +19,4 @@ export interface AppendResult extends AppendResponseObject {
19
19
  * @returns Append result with UID info if available, or undefined if preconditions not met
20
20
  * @throws {Error} If the APPEND command fails or message exceeds APPENDLIMIT
21
21
  */
22
- export default function append(connection: ImapFlow, destination: string | string[], content: Buffer | string, flags?: string | string[] | undefined, idate?: Date | string | false | undefined): Promise<AppendResult | undefined>;
22
+ export default function append(connection: ImapFlow, destination: string | string[], content: Buffer | Uint8Array | string, flags?: string | string[] | undefined, idate?: Date | string | false | undefined): Promise<AppendResult | undefined>;
@@ -1,4 +1,5 @@
1
1
  import { formatFlag, canUseFlag, formatDateTime, normalizePath, encodePath, comparePaths, parseBigIntValue, parseUintValue, MAX_UINT32_DIGITS, getSelectedMailbox, emitSafe, isAuthenticatedState, reportCommandError, logConnectionError } from '../tools.js';
2
+ import { createImapError } from '../errors.js';
2
3
  /**
3
4
  * Appends a message to a mailbox.
4
5
  *
@@ -15,14 +16,24 @@ export default async function append(connection, destination, content, flags, id
15
16
  // nothing to do here
16
17
  return;
17
18
  }
19
+ // The literal size marker is announced from these bytes, so anything that is not
20
+ // bytes (a Readable, a plain object) is refused here instead of desyncing the session
21
+ let message;
18
22
  if (typeof content === 'string') {
19
- content = Buffer.from(content);
23
+ message = Buffer.from(content);
24
+ }
25
+ else if (content instanceof Uint8Array) {
26
+ // a Buffer as is, any other Uint8Array as a view over the same memory, not a copy
27
+ message = Buffer.isBuffer(content) ? content : Buffer.from(content.buffer, content.byteOffset, content.byteLength);
28
+ }
29
+ else {
30
+ throw createImapError('Message content must be a Buffer, Uint8Array or string', 'InvalidMessageContent');
20
31
  }
21
32
  // APPENDLIMIT capability (RFC 7889): server may advertise the maximum message
22
33
  // size it accepts. Check before sending to avoid a wasted round-trip.
23
34
  if (connection.capabilities.has('APPENDLIMIT')) {
24
35
  let appendLimit = connection.capabilities.get('APPENDLIMIT');
25
- if (typeof appendLimit === 'number' && appendLimit < content.length) {
36
+ if (typeof appendLimit === 'number' && appendLimit < message.length) {
26
37
  let err = new Error('Message content too big for APPENDLIMIT=' + appendLimit);
27
38
  err.serverResponseCode = 'APPENDLIMIT';
28
39
  throw err;
@@ -79,9 +90,9 @@ export default async function append(connection, destination, content, flags, id
79
90
  // Regular literals cannot contain NUL bytes per the IMAP grammar.
80
91
  let isLiteral8 = false;
81
92
  if (connection.capabilities.has('BINARY') && !connection.disableBinary) {
82
- isLiteral8 = content.indexOf(Buffer.from([0])) >= 0;
93
+ isLiteral8 = message.indexOf(0) >= 0;
83
94
  }
84
- attributes.push({ type: 'LITERAL', value: content, isLiteral8 });
95
+ attributes.push({ type: 'LITERAL', value: message, isLiteral8 });
85
96
  let map = { destination };
86
97
  if (connection.mailbox && connection.mailbox.path) {
87
98
  map.path = connection.mailbox.path;
@@ -1,3 +1,4 @@
1
+ import { createImapError } from '../errors.js';
1
2
  /**
2
3
  * Requests DEFLATE compression from the server.
3
4
  *
@@ -33,8 +34,7 @@ export default async function compress(connection) {
33
34
  // back its unconsumed tail on unpipe so the transport can feed it into the
34
35
  // inflater - not something a command module can reach from here.
35
36
  if (response.hasTrailingData) {
36
- let error = new Error('Server sent data between the COMPRESS response and the compression layer switch');
37
- error.code = 'COMPRESS_TRAILING_DATA';
37
+ let error = createImapError('Server sent data between the COMPRESS response and the compression layer switch', 'COMPRESS_TRAILING_DATA');
38
38
  connection.log.error({ err: error, cid: connection.id });
39
39
  // Schedule the close before releasing parser backpressure, so the buffered
40
40
  // deflate-framed bytes cannot settle anything before teardown begins. This is