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
@@ -14,11 +14,13 @@ import { parser, compiler } from './handler/imap-handler.js';
14
14
  import { proxyConnection, detachEarlyErrorHandler } from './proxy-connection.js';
15
15
  import { ConnectionDeadline } from './connection-deadline.js';
16
16
  import { downloadMessage, downloadMessageParts } from './download.js';
17
- import { AuthenticationFailure } from './errors.js';
17
+ import { AuthenticationFailure, createImapError } from './errors.js';
18
18
  import imapCommands from './imap-commands.js';
19
19
  import { comparePaths, updateCapabilities, getFolderTree, formatMessageResponse, mergeFetchRows, packMessageRange, normalizePath, expandRange, getColorFlags, hasCapability, isRev2Active, logConnectionError, unrefTimer, clearTimer, parseUintValue, getStringList, getTextValues, emitSafe, buildConnectionError, guardedPromise, guardedReject, MAX_UINT32_DIGITS } from './tools.js';
20
20
  export { AuthenticationFailure, ImapFlowErrorCode } from './errors.js';
21
21
  const GREETING_TIMEOUT = 16 * 1000;
22
+ // Status responses that can carry an ALERT response code
23
+ const ALERT_RESPONSES = new Set(['OK', 'NO', 'BAD', 'BYE', 'PREAUTH']);
22
24
  const UPGRADE_TIMEOUT = 10 * 1000;
23
25
  const SOCKET_TIMEOUT = 5 * 60 * 1000;
24
26
  // Ceiling for any throttle back-off wait. Both the connection-level back-off and the per-command
@@ -42,6 +44,10 @@ const RAW_SENSITIVE_COMMANDS = new Set(['LOGIN', 'AUTHENTICATE']);
42
44
  // Stand-in payload for a withheld raw client frame. Fixed width, so the entry says nothing
43
45
  // about the length of what it replaced.
44
46
  const RAW_HIDDEN_PLACEHOLDER = Buffer.from('(* value hidden *)\r\n').toString('base64');
47
+ // From this size up, a Buffer written as the last part of a command gets its CRLF as a socket
48
+ // write of its own instead of being copied to append it (see write())
49
+ const SEPARATE_LINE_BREAK_SIZE = 64 * 1024;
50
+ const CRLF = Buffer.from('\r\n');
45
51
  // Servers, matched by the name in their ID response, that advertise IMAP4rev2 next to
46
52
  // IMAP4rev1 and accept ENABLE IMAP4rev2, but misbehave once it is enabled. ENABLE cannot
47
53
  // be undone (RFC 5161), so these are kept in IMAP4rev1 mode from the start. Strato's
@@ -342,43 +348,47 @@ export class ImapFlow extends EventEmitter {
342
348
  // commandParts holds the remaining parts (literal data, continuation); the CRLF
343
349
  // delimiter is only added when no more parts remain (the command is complete).
344
350
  let addLineBreak = !this.commandParts.length;
345
- let data;
351
+ // What goes to the socket, one write per entry. A large Buffer (the data of a literal)
352
+ // gets its CRLF as a write of its own, as concatenating would copy all of it.
353
+ let frames;
346
354
  if (typeof chunk === 'string') {
347
355
  if (addLineBreak) {
348
356
  chunk += '\r\n';
349
357
  }
350
- data = Buffer.from(chunk, 'binary');
358
+ frames = [Buffer.from(chunk, 'binary')];
351
359
  }
352
360
  else if (Buffer.isBuffer(chunk)) {
353
- if (addLineBreak) {
354
- data = Buffer.concat([chunk, Buffer.from('\r\n')]);
361
+ if (!addLineBreak) {
362
+ frames = [chunk];
355
363
  }
356
364
  else {
357
- data = chunk;
365
+ frames = chunk.length >= SEPARATE_LINE_BREAK_SIZE ? [chunk, CRLF] : [Buffer.concat([chunk, CRLF])];
358
366
  }
359
367
  }
360
368
  else {
361
369
  return false;
362
370
  }
363
- if (this.logRaw) {
364
- // Client frames of an authentication exchange carry credentials: the LOGIN
365
- // arguments, and for AUTHENTICATE also the continuation writes (SASL PLAIN
366
- // response, AUTH=LOGIN password, OAuth token payload) that bypass send(). The
367
- // parsed command log masks these, so the raw log must withhold them too, but
368
- // `data` still carries the placeholder rather than being dropped - the field is
369
- // part of the documented log format and consumers decode it unconditionally.
370
- this.log.trace({
371
- src: 'c',
372
- msg: 'write to socket',
373
- data: this.rawSensitiveCommand ? RAW_HIDDEN_PLACEHOLDER : data.toString('base64'),
374
- ...(this.rawSensitiveCommand ? { hidden: true } : {}),
375
- compress: !!this._deflate,
376
- secure: !!this.secureConnection,
377
- cid: this.id
378
- });
371
+ for (let data of frames) {
372
+ if (this.logRaw) {
373
+ // Client frames of an authentication exchange carry credentials: the LOGIN
374
+ // arguments, and for AUTHENTICATE also the continuation writes (SASL PLAIN
375
+ // response, AUTH=LOGIN password, OAuth token payload) that bypass send(). The
376
+ // parsed command log masks these, so the raw log must withhold them too, but
377
+ // `data` still carries the placeholder rather than being dropped - the field is
378
+ // part of the documented log format and consumers decode it unconditionally.
379
+ this.log.trace({
380
+ src: 'c',
381
+ msg: 'write to socket',
382
+ data: this.rawSensitiveCommand ? RAW_HIDDEN_PLACEHOLDER : data.toString('base64'),
383
+ ...(this.rawSensitiveCommand ? { hidden: true } : {}),
384
+ compress: !!this._deflate,
385
+ secure: !!this.secureConnection,
386
+ cid: this.id
387
+ });
388
+ }
389
+ this.writeBytesCounter += data.length;
390
+ this.writeSocket.write(data);
379
391
  }
380
- this.writeBytesCounter += data.length;
381
- this.writeSocket.write(data);
382
392
  }
383
393
  /**
384
394
  * Returns byte counters for the current connection.
@@ -404,6 +414,9 @@ export class ImapFlow extends EventEmitter {
404
414
  // and once as a string (for logging, with sensitive data masked).
405
415
  // When LITERAL- or LITERAL+ extensions are available, the compiler can use
406
416
  // non-synchronizing literals to avoid waiting for server "+" continuation.
417
+ // Async although nothing in it awaits: a failure inside it rejects one microtask later, so a
418
+ // close() in the same tick settles the command with NoConnection first (see
419
+ // test/unhandled-rejection-test.ts)
407
420
  /** @internal */
408
421
  async send(data) {
409
422
  if (this.state === this.states.LOGOUT) {
@@ -413,11 +426,11 @@ export class ImapFlow extends EventEmitter {
413
426
  // the socket watchdog.
414
427
  throw this.createNoConnectionError(false, { rejectedFrom: 'sendAfterLogout', command: data.command });
415
428
  }
416
- // Classify before the first await. Every frame of this command - the command line and
417
- // any continuation write that follows it - belongs to it until the next send(), because
429
+ // Classify once per command. Every frame of this command - the command line and any
430
+ // continuation write that follows it - belongs to it until the next send(), because
418
431
  // trySend() keeps one command in flight at a time. Reading currentRequest inside write()
419
- // instead would be racy: rejectCurrentRequest() can clear it while the two compiler
420
- // awaits below are pending, and the credential frame would then be logged in the clear.
432
+ // instead would be racy: rejectCurrentRequest() can clear it before a continuation frame
433
+ // is written, and the credential frame would then be logged in the clear.
421
434
  // Uppercased because the wire protocol is case-insensitive and exec() passes the
422
435
  // caller's spelling through unchanged. The command list covers the mechanisms whose
423
436
  // secret arrives in a continuation frame, which carries no attributes of its own; the
@@ -428,7 +441,7 @@ export class ImapFlow extends EventEmitter {
428
441
  // Compile with asArray=true: splits output into parts for literal handling.
429
442
  // First part is the command text up to the first literal, remaining parts
430
443
  // are stored in this.commandParts and sent after server "+" continuations.
431
- let compiled = await compiler(data, {
444
+ let compiled = compiler(data, {
432
445
  asArray: true,
433
446
  // LITERAL- is part of base IMAP4rev2
434
447
  literalMinus: hasCapability(this, 'LITERAL-') || this.capabilities.has('LITERAL+')
@@ -439,7 +452,7 @@ export class ImapFlow extends EventEmitter {
439
452
  // Compile again for logging with isLogging=true: masks sensitive values
440
453
  // like passwords while producing a human-readable command string
441
454
  if (this.isLogLevelEnabled('debug')) {
442
- let logCompiled = await compiler(data, {
455
+ let logCompiled = compiler(data, {
443
456
  isLogging: true
444
457
  });
445
458
  this.log.debug({ src: 'c', msg: logCompiled.toString(), cid: this.id, comment: options.comment });
@@ -696,9 +709,7 @@ export class ImapFlow extends EventEmitter {
696
709
  // parser would keep waiting on its backpressure callback forever, which is a
697
710
  // silent permanent hang. Fail closed instead.
698
711
  keepReading = false;
699
- let error = new Error('Failed to process server response');
700
- error.code = 'ResponseProcessingFailed';
701
- error._err = err;
712
+ let error = createImapError('Failed to process server response', 'ResponseProcessingFailed', { _err: err });
702
713
  this.log.error({ msg: 'Failed to process server response', err, cid: this.id });
703
714
  this.rejectCurrentRequest(error);
704
715
  this.failProtocol(error);
@@ -749,6 +760,27 @@ export class ImapFlow extends EventEmitter {
749
760
  this.rejectCurrentRequest(err);
750
761
  this.trySend().catch(sendErr => logConnectionError(this, 'Failed to dispatch command', sendErr));
751
762
  }
763
+ // RFC 9051 section 7.1: the text of an ALERT must be presented to the user. It can come
764
+ // with any status response, tagged or untagged, the greeting and BYE included. Called for
765
+ // responses that carry the ALERT code.
766
+ /** @internal */
767
+ reportAlert(parsed) {
768
+ let response = (parsed.command || '').toUpperCase();
769
+ if (!ALERT_RESPONSES.has(response)) {
770
+ return;
771
+ }
772
+ let alert = {
773
+ message: getTextValues(parsed.attributes)
774
+ .map(value => value.trim())
775
+ .join(' '),
776
+ response: response
777
+ };
778
+ if (parsed.tag && parsed.tag !== '*') {
779
+ alert.tag = parsed.tag;
780
+ }
781
+ this.log.info({ msg: 'Server alert', alert: alert.message, response: alert.response, cid: this.id });
782
+ emitSafe(this, 'alert', alert);
783
+ }
752
784
  /**
753
785
  * Handles a single parsed server response: telemetry, continuation requests, response-code
754
786
  * section handlers, untagged handlers and tagged command completion.
@@ -800,7 +832,7 @@ export class ImapFlow extends EventEmitter {
800
832
  // skipped when the level is off.
801
833
  let logLevel = /^\d+$/.test(parsed.command || '') && parsed.attributes && parsed.attributes[0] && parsed.attributes[0].value === 'FETCH' ? 'trace' : 'debug';
802
834
  if (this.isLogLevelEnabled(logLevel)) {
803
- let logCompiled = await compiler(parsed, {
835
+ let logCompiled = compiler(parsed, {
804
836
  isLogging: true
805
837
  });
806
838
  this.log[logLevel]({ src: 's', msg: logCompiled.toString(), cid: this.id, nullBytesRemoved: parsed.nullBytesRemoved });
@@ -843,6 +875,9 @@ export class ImapFlow extends EventEmitter {
843
875
  // dereference must be guarded or one such line tears down the whole connection
844
876
  if (section && section.length && section[0] && section[0].type === 'ATOM' && typeof section[0].value === 'string') {
845
877
  let sectionKey = section[0].value.toUpperCase().trim();
878
+ if (sectionKey === 'ALERT') {
879
+ this.reportAlert(parsed);
880
+ }
846
881
  let sectionHandler = this.getSectionHandler(sectionKey);
847
882
  if (sectionHandler) {
848
883
  try {
@@ -903,12 +938,12 @@ export class ImapFlow extends EventEmitter {
903
938
  // response ambiguous, so reject this request and fail the connection closed.
904
939
  let request = this.requestTagMap.get(parsed.tag);
905
940
  this.requestTagMap.delete(parsed.tag);
906
- let err = new Error('Server sent a tagged response for a command that was not in flight');
907
- err.code = 'UnexpectedTag';
908
- err.details = {
909
- received: parsed.tag,
910
- expected: this.currentRequest ? this.currentRequest.tag : null
911
- };
941
+ let err = createImapError('Server sent a tagged response for a command that was not in flight', 'UnexpectedTag', {
942
+ details: {
943
+ received: parsed.tag,
944
+ expected: this.currentRequest ? this.currentRequest.tag : null
945
+ }
946
+ });
912
947
  this.log.error({ msg: 'Protocol desynchronization', err, cid: this.id });
913
948
  request.reject(err);
914
949
  this.failProtocol(err);
@@ -952,9 +987,9 @@ export class ImapFlow extends EventEmitter {
952
987
  try {
953
988
  err.executedCommand =
954
989
  parsed.tag +
955
- (await compiler(request, {
990
+ compiler(request, {
956
991
  isLogging: true
957
- })).toString();
992
+ }).toString();
958
993
  }
959
994
  catch {
960
995
  // ignore
@@ -1007,9 +1042,7 @@ export class ImapFlow extends EventEmitter {
1007
1042
  break;
1008
1043
  }
1009
1044
  default: {
1010
- let err = new Error('Invalid server response');
1011
- err.code = 'InvalidResponse';
1012
- err.response = parsed;
1045
+ let err = createImapError('Invalid server response', 'InvalidResponse', { response: parsed });
1013
1046
  request.reject(err);
1014
1047
  break;
1015
1048
  }
@@ -1110,11 +1143,7 @@ export class ImapFlow extends EventEmitter {
1110
1143
  this._socketTimeout =
1111
1144
  this._socketTimeout ||
1112
1145
  (() => {
1113
- const timeoutError = () => {
1114
- const err = new Error('Socket timeout');
1115
- err.code = 'ETIMEOUT';
1116
- return err;
1117
- };
1146
+ const timeoutError = () => createImapError('Socket timeout', 'ETIMEOUT');
1118
1147
  if (!this.usable || !this.socket || this.socket.destroyed) {
1119
1148
  this.emitError(timeoutError());
1120
1149
  return;
@@ -1298,7 +1327,10 @@ export class ImapFlow extends EventEmitter {
1298
1327
  // to call deflate.flush() after each IMAP command to push all pending
1299
1328
  // compressed bytes to the server immediately (IMAP is request-response).
1300
1329
  const writeSocket = new PassThrough({
1301
- highWaterMark: 64 * 1024 // 64KB buffer limit to prevent excessive memory usage
1330
+ highWaterMark: 64 * 1024, // 64KB buffer limit to prevent excessive memory usage
1331
+ // read() hands out each written chunk as is: in byte mode it joins whatever is
1332
+ // queued into a new Buffer, which copies every large literal once more
1333
+ readableObjectMode: true
1302
1334
  });
1303
1335
  this.writeSocket = writeSocket;
1304
1336
  /* c8 ignore start */ // destroySoon override is never invoked by ImapFlow (close() calls destroy()); kept for stream API completeness
@@ -1426,9 +1458,7 @@ export class ImapFlow extends EventEmitter {
1426
1458
  // socket; injection that still races in afterwards corrupts the TLS handshake and
1427
1459
  // is rejected there instead (with a generic TLS error rather than STARTTLS_INJECTION).
1428
1460
  const failSTARTTLSInjection = () => {
1429
- let err = new Error('Server sent data after the STARTTLS response and before the TLS handshake; possible plaintext-injection attack');
1430
- err.code = 'STARTTLS_INJECTION';
1431
- err.tlsFailed = true;
1461
+ let err = createImapError('Server sent data after the STARTTLS response and before the TLS handshake; possible plaintext-injection attack', 'STARTTLS_INJECTION', { tlsFailed: true });
1432
1462
  this.closeAfter();
1433
1463
  return err;
1434
1464
  };
@@ -1511,8 +1541,7 @@ export class ImapFlow extends EventEmitter {
1511
1541
  socketPlain.on('error', settle);
1512
1542
  /* c8 ignore start */ // UPGRADE_TIMEOUT is 10s; firing it deterministically would make the test suite hang
1513
1543
  this.upgradeTimeout = setTimeout(() => {
1514
- let err = new Error('Failed to upgrade connection in required time');
1515
- err.code = 'UPGRADE_TIMEOUT';
1544
+ let err = createImapError('Failed to upgrade connection in required time', 'UPGRADE_TIMEOUT');
1516
1545
  settle(err);
1517
1546
  }, UPGRADE_TIMEOUT);
1518
1547
  /* c8 ignore stop */
@@ -2016,9 +2045,7 @@ export class ImapFlow extends EventEmitter {
2016
2045
  async connect() {
2017
2046
  if (this._connectCalled) {
2018
2047
  // Prevent re-using ImapFlow instances by allowing to call connect just once.
2019
- let err = new Error('Can not re-use ImapFlow instance');
2020
- err.code = 'InstanceReused';
2021
- throw err;
2048
+ throw createImapError('Can not re-use ImapFlow instance', 'InstanceReused');
2022
2049
  }
2023
2050
  this._connectCalled = true;
2024
2051
  let closedError = () => this.createNoConnectionError(this.byeReason, { rejectedFrom: 'connect' });
@@ -2106,14 +2133,14 @@ export class ImapFlow extends EventEmitter {
2106
2133
  detachEarlyErrorHandler(socket);
2107
2134
  this.configureSocket(this.socket);
2108
2135
  this.greetingTimeout = setTimeout(() => {
2109
- let err = new Error(
2136
+ let err = createImapError(
2110
2137
  /* c8 ignore next */ // the greeting-timeout test uses a plaintext socket; the secure-socket branch of this hint is not separately exercised
2111
- `Failed to receive greeting from server in required time${!this.secureConnection ? '. Maybe should use TLS?' : ''}`);
2112
- err.code = 'GREETING_TIMEOUT';
2113
- err.details = {
2114
- /* c8 ignore next */ // firing the timeout with the default (large) value would hang the suite, so only the explicit-option path is tested
2115
- greetingTimeout: this.options.greetingTimeout || GREETING_TIMEOUT
2116
- };
2138
+ `Failed to receive greeting from server in required time${!this.secureConnection ? '. Maybe should use TLS?' : ''}`, 'GREETING_TIMEOUT', {
2139
+ details: {
2140
+ /* c8 ignore next */ // firing the timeout with the default (large) value would hang the suite, so only the explicit-option path is tested
2141
+ greetingTimeout: this.options.greetingTimeout || GREETING_TIMEOUT
2142
+ }
2143
+ });
2117
2144
  this.log.error({ err, cid: this.id });
2118
2145
  this.closeAfter();
2119
2146
  reject(err);
@@ -2337,9 +2364,9 @@ export class ImapFlow extends EventEmitter {
2337
2364
  let reject = this.initialReject;
2338
2365
  this.initialResolve = false;
2339
2366
  this.initialReject = false;
2340
- let err = new Error('Unexpected close');
2367
+ let err = createImapError('Unexpected close',
2341
2368
  /* c8 ignore next */ // closing a pending connect over an already-secure socket (the TLS branch) is not separately exercised
2342
- err.code = `ClosedAfterConnect${this.secureConnection ? 'TLS' : 'Text'}`;
2369
+ this.secureConnection ? 'ClosedAfterConnectTLS' : 'ClosedAfterConnectText');
2343
2370
  // Surface the server's BYE reason (e.g. "Too many connections") when the
2344
2371
  // connection was closed by an untagged BYE, so the caller sees why.
2345
2372
  if (this.byeReason) {
@@ -2850,7 +2877,7 @@ export class ImapFlow extends EventEmitter {
2850
2877
  * Appends a new message to a mailbox
2851
2878
  *
2852
2879
  * @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.
2853
- * @param content RFC822 formatted email message
2880
+ * @param content RFC822 formatted email message, as a Buffer, Uint8Array or string
2854
2881
  * @param flags an array of flags to be set for the uploaded message
2855
2882
  * @param idate internal date to be set for the message
2856
2883
  * @returns info about uploaded message
@@ -3152,7 +3179,8 @@ export class ImapFlow extends EventEmitter {
3152
3179
  * // download body part nr '1.2' from latest message
3153
3180
  * let download = await client.download('*', '1.2');
3154
3181
  * if (download.content) {
3155
- * download.content.pipe(fs.createWriteStream(download.meta.filename));
3182
+ * // pipeline() rejects when the download fails, a plain .pipe() would not report it
3183
+ * await stream.promises.pipeline(download.content, fs.createWriteStream(download.meta.filename));
3156
3184
  * }
3157
3185
  */
3158
3186
  async download(range, part, options) {
@@ -3489,9 +3517,7 @@ export class ImapFlow extends EventEmitter {
3489
3517
  const idx = this.locks.indexOf(lockEntry);
3490
3518
  if (idx !== -1) {
3491
3519
  this.locks.splice(idx, 1);
3492
- let err = new Error('Timed out waiting for mailbox lock');
3493
- err.code = 'LockTimeout';
3494
- err.lockId = lockEntry.lockId;
3520
+ let err = createImapError('Timed out waiting for mailbox lock', 'LockTimeout', { lockId: lockEntry.lockId });
3495
3521
  reject(err);
3496
3522
  }
3497
3523
  }, 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/";
@@ -1,4 +1,4 @@
1
1
  // Generated by scripts/build.js from package.json. Do not edit by hand.
2
2
  export const name = "imapflow";
3
- export const version = "2.2.11";
3
+ export const version = "2.4.0";
4
4
  export const homepage = "https://imapflow.com/";
@@ -4,6 +4,7 @@ import net from 'node:net';
4
4
  import tls from 'node:tls';
5
5
  import { ConnectionDeadline } from './connection-deadline.js';
6
6
  import { clearTimer } from './tools.js';
7
+ import { createImapError } from './errors.js';
7
8
  // Cap the CONNECT response buffered before the header terminator, so a proxy that never sends
8
9
  // \r\n\r\n cannot grow memory without bound.
9
10
  const MAX_RESPONSE_HEADER_BYTES = 64 * 1024;
@@ -27,11 +28,7 @@ const redactUrl = (proxyUrl) => {
27
28
  }
28
29
  return redacted.href;
29
30
  };
30
- const proxyError = (message, code) => {
31
- let err = new Error(message);
32
- err.code = code || 'ProxyError';
33
- return err;
34
- };
31
+ const proxyError = (message, code) => createImapError(message, code || 'ProxyError');
35
32
  // URL userinfo is percent-encoded, so it has to be decoded before it can be used as credentials.
36
33
  // A password containing a bare '%' is not valid percent-encoding and makes decodeURIComponent
37
34
  // throw, so such values are used as they came in rather than failing the connection.
@@ -1,5 +1,6 @@
1
1
  /* eslint no-control-regex:0 */
2
2
  import { formatDate, formatFlag, toValidDate, isRev2Active } from './tools.js';
3
+ import { createImapError } from './errors.js';
3
4
  // Matches any character outside the ASCII range
4
5
  const UNICODE_PATTERN = /[^\x00-\x7F]/;
5
6
  /**
@@ -101,9 +102,7 @@ let processDateField = (attributes, term, value) => {
101
102
  * @param message - Error message
102
103
  */
103
104
  let fail = (code, message) => {
104
- let error = new Error(message);
105
- error.code = code;
106
- throw error;
105
+ throw createImapError(message, code);
107
106
  };
108
107
  /**
109
108
  * 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/esm/tools.js CHANGED
@@ -6,7 +6,7 @@ import imapFormalSyntax from './handler/imap-formal-syntax.js';
6
6
  import { createHash } from 'node:crypto';
7
7
  import { JPDecoder } from './jp-decoder.js';
8
8
  import iconv from 'iconv-lite';
9
- import { ImapFlowErrorCode } from './errors.js';
9
+ import { createImapError, ImapFlowErrorCode } from './errors.js';
10
10
  export { AuthenticationFailure } from './errors.js';
11
11
  const FLAG_COLORS = ['red', 'orange', 'yellow', 'green', 'blue', 'purple', 'grey'];
12
12
  // Error codes that only mean the connection is no longer usable. See logConnectionError().
@@ -88,13 +88,7 @@ const CONNECTION_ERROR_SITE_KEYS = ['rejectedFrom', 'command', 'path'];
88
88
  * @returns The stamped error
89
89
  */
90
90
  export function buildConnectionError(cid, code, message, meta) {
91
- const error = new Error(message);
92
- error.code = code;
93
- error.cid = cid;
94
- if (meta) {
95
- Object.assign(error, meta);
96
- }
97
- return error;
91
+ return createImapError(message, code, { cid, ...meta });
98
92
  }
99
93
  /**
100
94
  * Re-stamps an existing connection error for a different rejection site.
@@ -116,6 +110,7 @@ export function buildConnectionError(cid, code, message, meta) {
116
110
  * @returns A separate error describing the same failure at the new site
117
111
  */
118
112
  export function restampConnectionError(err, meta) {
113
+ // the original code is carried over unchanged, whatever its origin
119
114
  let error = buildConnectionError(err.cid, err.code, err.message, err);
120
115
  for (let key of CONNECTION_ERROR_SITE_KEYS) {
121
116
  delete error[key];
@@ -215,12 +210,15 @@ export function logConnectionError(connection, msg, err) {
215
210
  /**
216
211
  * Whether a command failed because the server answered it with a tagged NO or BAD, as opposed
217
212
  * to a lost connection, a timeout or a local failure that says nothing about the server's answer.
213
+ * A throttled command (ETHROTTLE, Microsoft 365 answers it with BAD) was not refused: the server
214
+ * asks to try again later, so a throttled login is not a rejected credential and a throttled
215
+ * IDLE does not mean the server lacks IDLE.
218
216
  *
219
217
  * @param err - The error the command failed with
220
- * @returns True for a tagged NO or BAD
218
+ * @returns True for a tagged NO or BAD that is not throttling
221
219
  */
222
220
  export function isServerRefusal(err) {
223
- return !!err && (err.responseStatus === 'NO' || err.responseStatus === 'BAD');
221
+ return !!err && (err.responseStatus === 'NO' || err.responseStatus === 'BAD') && err.code !== ImapFlowErrorCode.ETHROTTLE;
224
222
  }
225
223
  /**
226
224
  * Checks whether IMAP4rev2 semantics are active for the connection: either the
@@ -466,12 +464,13 @@ export function getTextValues(attributes) {
466
464
  * @param response - Parsed IMAP server response
467
465
  * @returns Compiled response text, or false if no response
468
466
  */
467
+ // Async although nothing in it awaits any more: `imapflow/lib/tools` deep imports may chain on the promise
469
468
  export async function getErrorText(response) {
470
469
  if (!response) {
471
470
  return false;
472
471
  }
473
472
  try {
474
- return (await compiler(response)).toString();
473
+ return compiler(response).toString();
475
474
  }
476
475
  catch {
477
476
  // The wire encoder refuses values that cannot be expressed as a valid IMAP
@@ -480,7 +479,7 @@ export async function getErrorText(response) {
480
479
  // tolerates stray bytes inside an OK/NO/BAD atom, and those bytes then have
481
480
  // no valid re-encoding. This text is diagnostic, so fall back to the logging
482
481
  // encoder rather than replacing the server's error with an encoding failure.
483
- return (await compiler(response, { isLogging: true })).toString();
482
+ return compiler(response, { isLogging: true }).toString();
484
483
  }
485
484
  }
486
485
  /**
@@ -694,6 +693,7 @@ export function mergeFetchRows(rows) {
694
693
  * @param connection - Connection the response arrived on, decodes Gmail labels like mailbox names
695
694
  * @returns Formatted message object with properties like seq, uid, flags, envelope, etc.
696
695
  */
696
+ // Async although nothing in it awaits any more: `imapflow/lib/tools` deep imports may chain on the promise
697
697
  export async function formatMessageResponse(untagged, mailbox, idHashAlgorithm, connection) {
698
698
  let map = {};
699
699
  // The sequence number indexes into mailbox state, so an unusable one is dropped rather
@@ -716,9 +716,9 @@ export async function formatMessageResponse(untagged, mailbox, idHashAlgorithm,
716
716
  let attribute = attributes[i];
717
717
  if (i % 2 === 0) {
718
718
  origin = false;
719
- key = (await compiler({
719
+ key = compiler({
720
720
  attributes: [attribute]
721
- }))
721
+ })
722
722
  .toString()
723
723
  .toLowerCase()
724
724
  .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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "imapflow",
3
- "version": "2.2.11",
3
+ "version": "2.4.0",
4
4
  "description": "IMAP Client for Node",
5
5
  "type": "module",
6
6
  "main": "./dist/cjs/imap-flow.js",
@@ -75,13 +75,13 @@
75
75
  "eslint": "10.12.0",
76
76
  "eslint-config-prettier": "10.1.8",
77
77
  "globals": "17.13.0",
78
- "imapkit": "4.3.1",
78
+ "imapkit": "5.0.1",
79
79
  "prettier": "3.9.9",
80
80
  "tsx": "4.23.15",
81
81
  "types-node-legacy": "npm:@types/node@20.0.0",
82
82
  "typescript": "6.0.3",
83
83
  "typescript-eslint": "8.71.1",
84
- "wrangler": "4.148.0"
84
+ "wrangler": "4.149.0"
85
85
  },
86
86
  "dependencies": {
87
87
  "@zone-eu/mailsplit": "5.4.20",
@@ -95,5 +95,8 @@
95
95
  },
96
96
  "engines": {
97
97
  "node": ">=20.0.0"
98
+ },
99
+ "overrides": {
100
+ "sharp": "0.35.5"
98
101
  }
99
102
  }