imapflow 1.7.4 → 1.7.5

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.
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "1.7.4"
2
+ ".": "1.7.5"
3
3
  }
package/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.7.5](https://github.com/postalsys/imapflow/compare/v1.7.4...v1.7.5) (2026-08-24)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * **errors:** give an IDLE-break waiter its own rejection site ([9534551](https://github.com/postalsys/imapflow/commit/95345519fa32ee26bff239a2ac90f6b8ddccb559))
9
+
3
10
  ## [1.7.4](https://github.com/postalsys/imapflow/compare/v1.7.3...v1.7.4) (2026-08-24)
4
11
 
5
12
 
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- const { guardedPromise, hasCapability, logConnectionError, unrefTimer } = require('../tools.js');
3
+ const { buildConnectionError, guardedPromise, hasCapability, logConnectionError, unrefTimer } = require('../tools.js');
4
4
 
5
5
  const NOOP_INTERVAL = 2 * 60 * 1000;
6
6
 
@@ -27,6 +27,27 @@ function claimIdling(connection) {
27
27
  };
28
28
  }
29
29
 
30
+ /**
31
+ * Builds the rejection a queued IDLE-break waiter receives when the IDLE command itself fails.
32
+ *
33
+ * The waiters are separate promises with separate consumers, so handing them the command's own
34
+ * error would report a waiter that escapes as `rejectedFrom: 'pendingRequest'` - the site where
35
+ * the IDLE command was rejected, not the site where the waiter was. That is precisely the
36
+ * attribution the markers exist to give. Every own field of the original is carried over, because
37
+ * a waiter rejection reaches callers through run() and they branch on `responseStatus`,
38
+ * `serverResponseCode` and friends; only the site marker differs, and the original travels on as
39
+ * `cause` so the command failure behind it stays visible.
40
+ *
41
+ * @param {Object} connection - IMAP connection instance
42
+ * @param {Error} err - The error that failed the IDLE command
43
+ * @returns {Error} Error stamped for the waiter site
44
+ */
45
+ function preCheckWaiterError(connection, err) {
46
+ let error = buildConnectionError(connection.id, err.code, err.message, Object.assign({}, err, { rejectedFrom: 'preCheckWaiter' }));
47
+ error.cause = err;
48
+ return error;
49
+ }
50
+
30
51
  /**
31
52
  * Runs a single IDLE session on the connection.
32
53
  *
@@ -140,7 +161,7 @@ async function runIdle(connection) {
140
161
  logConnectionError(connection, 'IDLE session failed', err);
141
162
  while (preCheckWaitQueue.length) {
142
163
  let { reject } = preCheckWaitQueue.shift();
143
- reject(err);
164
+ reject(preCheckWaiterError(connection, err));
144
165
  }
145
166
  return false;
146
167
  } finally {
package/lib/imap-flow.js CHANGED
@@ -43,6 +43,7 @@ const {
43
43
  parseUintValue,
44
44
  isUnsafeKey,
45
45
  getStringList,
46
+ buildConnectionError,
46
47
  guardedPromise,
47
48
  guardedReject,
48
49
  MAX_UINT32_DIGITS
@@ -2539,23 +2540,9 @@ class ImapFlow extends EventEmitter {
2539
2540
  setImmediate(() => this.close());
2540
2541
  }
2541
2542
 
2542
- // Builds an error describing a connection that is gone. Every such error is stamped here so
2543
- // the stamping cannot drift between sites: the connection id always travels on it, and each
2544
- // site names itself through `meta` (`rejectedFrom`, plus the command or mailbox path it
2545
- // belongs to).
2546
- //
2547
- // A stack trace only records where an error was built, and close() hands a rejection to every
2548
- // pending request and every queued lock in the same tick, so without these an error that
2549
- // reaches a global unhandledRejection handler arrives with nothing that identifies the
2550
- // connection it came from, let alone which of the rejected promises carried it.
2543
+ // Connection-scoped wrapper around the shared stamping helper; see buildConnectionError().
2551
2544
  createConnectionError(code, message, meta) {
2552
- const error = new Error(message);
2553
- error.code = code;
2554
- error.cid = this.id;
2555
- if (meta) {
2556
- Object.assign(error, meta);
2557
- }
2558
- return error;
2545
+ return buildConnectionError(this.id, code, message, meta);
2559
2546
  }
2560
2547
 
2561
2548
  // The standard "connection not available" error, optionally annotated with the server's BYE
package/lib/tools.js CHANGED
@@ -75,6 +75,35 @@ const noop = () => {};
75
75
  const tools = {
76
76
  noop,
77
77
 
78
+ /**
79
+ * Builds an error describing a connection that is gone, stamped so it can be traced back to
80
+ * where it came from: the connection id always travels on it, and each site names itself
81
+ * through `meta` (`rejectedFrom`, plus the command or mailbox path it belongs to).
82
+ *
83
+ * A stack trace only records where an error was built, and close() hands a rejection to every
84
+ * pending request and every queued lock in the same tick, so without these an error that
85
+ * reaches a global unhandledRejection handler arrives with nothing that identifies the
86
+ * connection it came from, let alone which of the rejected promises carried it.
87
+ *
88
+ * Takes the connection id rather than the connection, so the stamping stays in one place
89
+ * without every caller having to be a full ImapFlow instance.
90
+ *
91
+ * @param {String} cid - Connection id
92
+ * @param {String} code - Error code, e.g. 'NoConnection'
93
+ * @param {String} message - Error message
94
+ * @param {Object} [meta] - Fields to stamp on the error
95
+ * @returns {Error} The stamped error
96
+ */
97
+ buildConnectionError(cid, code, message, meta) {
98
+ const error = new Error(message);
99
+ error.code = code;
100
+ error.cid = cid;
101
+ if (meta) {
102
+ Object.assign(error, meta);
103
+ }
104
+ return error;
105
+ },
106
+
78
107
  /**
79
108
  * Creates a promise whose rejection is observed as soon as it exists.
80
109
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "imapflow",
3
- "version": "1.7.4",
3
+ "version": "1.7.5",
4
4
  "description": "IMAP Client for Node",
5
5
  "main": "lib/imap-flow.js",
6
6
  "types": "lib/imap-flow.d.ts",
@@ -439,6 +439,68 @@ exports['Unhandled Rejection Prevention'] = {
439
439
  }, 100);
440
440
  },
441
441
 
442
+ 'a preCheck() waiter rejection names its own site, not the IDLE command'(test) {
443
+ test.expect(6);
444
+
445
+ // IDLE is never acknowledged with a "+", so preCheck() cannot send DONE and its waiter
446
+ // stays queued until close() tears the IDLE command down.
447
+ const server = createMockServer({
448
+ extraCapabilities: 'IDLE',
449
+ onCommand(socket, tag, command) {
450
+ if (command === 'IDLE') {
451
+ return true;
452
+ }
453
+ }
454
+ });
455
+
456
+ server.listen(0, '127.0.0.1', async () => {
457
+ const port = server.address().port;
458
+
459
+ const client = new ImapFlow({
460
+ host: '127.0.0.1',
461
+ port,
462
+ secure: false,
463
+ logger: false,
464
+ disableAutoIdle: true,
465
+ id: 'waiter-cid',
466
+ auth: {
467
+ user: 'test',
468
+ pass: 'test'
469
+ }
470
+ });
471
+
472
+ try {
473
+ await client.connect();
474
+ await client.mailboxOpen('INBOX');
475
+
476
+ client.idle().catch(() => {
477
+ // Expected: IDLE rejects when the connection is closed
478
+ });
479
+
480
+ await new Promise(r => setTimeout(r, 100));
481
+
482
+ let waiter = client.preCheck();
483
+ client.close();
484
+
485
+ try {
486
+ await waiter;
487
+ test.ok(false, 'the waiter should have rejected');
488
+ } catch (err) {
489
+ test.equal(err.rejectedFrom, 'preCheckWaiter', 'the waiter names its own rejection site');
490
+ test.equal(err.cid, 'waiter-cid', 'the waiter error carries the connection id');
491
+ test.equal(err.code, 'NoConnection', 'the original code is preserved for callers that branch on it');
492
+ test.ok(err.cause, 'the command failure travels on as the cause');
493
+ test.equal(err.cause.rejectedFrom, 'pendingRequest', 'the cause still names the command site');
494
+ test.equal(err.cause.command, 'IDLE', 'the cause still names the command');
495
+ }
496
+ } catch (err) {
497
+ test.ok(false, 'Unexpected error: ' + err.message);
498
+ } finally {
499
+ server.close(() => test.done());
500
+ }
501
+ });
502
+ },
503
+
442
504
  'preCheck() waiter rejected by close() should not cause unhandled rejection'(test) {
443
505
  test.expect(2);
444
506