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.
- package/.release-please-manifest.json +1 -1
- package/CHANGELOG.md +7 -0
- package/lib/commands/idle.js +23 -2
- package/lib/imap-flow.js +3 -16
- package/lib/tools.js +29 -0
- package/package.json +1 -1
- package/test/unhandled-rejection-test.js +62 -0
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
|
|
package/lib/commands/idle.js
CHANGED
|
@@ -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
|
-
//
|
|
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
|
-
|
|
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
|
@@ -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
|
|