imapflow 1.7.2 → 1.7.3
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 +8 -0
- package/lib/commands/fetch.js +1 -1
- package/lib/commands/idle.js +4 -2
- package/lib/imap-flow.js +53 -72
- package/lib/tools.js +41 -0
- package/package.json +1 -1
- package/test/fetch-generator-test.js +2 -0
- package/test/imapflow-test.js +2 -0
- package/test/unhandled-rejection-test.js +92 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.7.3](https://github.com/postalsys/imapflow/compare/v1.7.2...v1.7.3) (2026-08-24)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* **errors:** make the rejection guard structural and stamp every connection error ([67a4afc](https://github.com/postalsys/imapflow/commit/67a4afcc06a2699ea327e98459590901f2efb24c))
|
|
9
|
+
* **errors:** name the rejection site on connection errors and guard the IDLE preCheck waiter ([55764f0](https://github.com/postalsys/imapflow/commit/55764f056f5aeb9771e493e9b659872f01ee7995))
|
|
10
|
+
|
|
3
11
|
## [1.7.2](https://github.com/postalsys/imapflow/compare/v1.7.1...v1.7.2) (2026-08-21)
|
|
4
12
|
|
|
5
13
|
|
package/lib/commands/fetch.js
CHANGED
|
@@ -254,7 +254,7 @@ module.exports = async (connection, range, query, options) => {
|
|
|
254
254
|
// the FETCH on a connection that is already gone.
|
|
255
255
|
let aborted = await connection.throttleWait(delay);
|
|
256
256
|
if (aborted) {
|
|
257
|
-
throw connection.createNoConnectionError(connection.byeReason);
|
|
257
|
+
throw connection.createNoConnectionError(connection.byeReason, { rejectedFrom: 'throttleAbort', command: 'FETCH' });
|
|
258
258
|
}
|
|
259
259
|
|
|
260
260
|
retryCount++;
|
package/lib/commands/idle.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const { hasCapability, logConnectionError, unrefTimer } = require('../tools.js');
|
|
3
|
+
const { guardedPromise, hasCapability, logConnectionError, unrefTimer } = require('../tools.js');
|
|
4
4
|
|
|
5
5
|
const NOOP_INTERVAL = 2 * 60 * 1000;
|
|
6
6
|
|
|
@@ -85,7 +85,9 @@ async function runIdle(connection) {
|
|
|
85
85
|
// Public interface for breaking IDLE. Returns a promise that resolves when
|
|
86
86
|
// IDLE is actually broken and the connection is free for other commands.
|
|
87
87
|
let connectionPreCheck = () => {
|
|
88
|
-
|
|
88
|
+
// Guarded: the catch block below rejects every queued waiter synchronously while
|
|
89
|
+
// close() tears the connection down. See guardedPromise().
|
|
90
|
+
let handler = guardedPromise((resolve, reject) => {
|
|
89
91
|
preCheckWaitQueue.push({ resolve, reject });
|
|
90
92
|
});
|
|
91
93
|
|
package/lib/imap-flow.js
CHANGED
|
@@ -43,13 +43,13 @@ const {
|
|
|
43
43
|
parseUintValue,
|
|
44
44
|
isUnsafeKey,
|
|
45
45
|
getStringList,
|
|
46
|
+
guardedPromise,
|
|
47
|
+
guardedReject,
|
|
46
48
|
MAX_UINT32_DIGITS
|
|
47
49
|
} = require('./tools');
|
|
48
50
|
|
|
49
51
|
const imapCommands = require('./imap-commands.js');
|
|
50
52
|
|
|
51
|
-
const noop = () => {};
|
|
52
|
-
|
|
53
53
|
const GREETING_TIMEOUT = 16 * 1000;
|
|
54
54
|
const UPGRADE_TIMEOUT = 10 * 1000;
|
|
55
55
|
|
|
@@ -677,16 +677,12 @@ class ImapFlow extends EventEmitter {
|
|
|
677
677
|
write(chunk) {
|
|
678
678
|
if (!this.socket || this.socket.destroyed) {
|
|
679
679
|
// do not write after connection end or logout
|
|
680
|
-
|
|
681
|
-
error.code = 'NoConnection';
|
|
682
|
-
throw error;
|
|
680
|
+
throw this.createConnectionError('NoConnection', 'Socket is already closed', { rejectedFrom: 'writeNoSocket' });
|
|
683
681
|
}
|
|
684
682
|
|
|
685
683
|
if (this.state === this.states.LOGOUT) {
|
|
686
684
|
// should not happen
|
|
687
|
-
|
|
688
|
-
error.code = 'StateLogout';
|
|
689
|
-
throw error;
|
|
685
|
+
throw this.createConnectionError('StateLogout', 'Can not send data after logged out', { rejectedFrom: 'writeAfterLogout' });
|
|
690
686
|
}
|
|
691
687
|
|
|
692
688
|
if (this.writeSocket.destroyed) {
|
|
@@ -771,9 +767,7 @@ class ImapFlow extends EventEmitter {
|
|
|
771
767
|
let request = this.requestTagMap.get(data.tag);
|
|
772
768
|
if (request) {
|
|
773
769
|
this.requestTagMap.delete(request.tag);
|
|
774
|
-
|
|
775
|
-
error.code = 'NoConnection';
|
|
776
|
-
request.reject(error);
|
|
770
|
+
request.reject(this.createNoConnectionError(false, { rejectedFrom: 'sendAfterLogout', command: request.command }));
|
|
777
771
|
}
|
|
778
772
|
}
|
|
779
773
|
return;
|
|
@@ -864,26 +858,20 @@ class ImapFlow extends EventEmitter {
|
|
|
864
858
|
|
|
865
859
|
exec(command, attributes, options) {
|
|
866
860
|
if (this.state === this.states.LOGOUT || this.isClosed) {
|
|
867
|
-
|
|
868
|
-
error.code = 'NoConnection';
|
|
869
|
-
let p = Promise.reject(error);
|
|
870
|
-
p.catch(noop);
|
|
871
|
-
return p;
|
|
861
|
+
return guardedReject(this.createNoConnectionError(false, { rejectedFrom: 'execClosed', command }));
|
|
872
862
|
}
|
|
873
863
|
|
|
874
864
|
if (!this.socket || this.socket.destroyed) {
|
|
875
|
-
|
|
876
|
-
error.code = 'EConnectionClosed';
|
|
877
|
-
let p = Promise.reject(error);
|
|
878
|
-
p.catch(noop);
|
|
879
|
-
return p;
|
|
865
|
+
return guardedReject(this.createConnectionError('EConnectionClosed', 'Connection closed', { rejectedFrom: 'execNoSocket', command }));
|
|
880
866
|
}
|
|
881
867
|
|
|
882
868
|
let tag = (++this.tagCounter).toString(16).toUpperCase();
|
|
883
869
|
|
|
884
870
|
options = options || {};
|
|
885
871
|
|
|
886
|
-
|
|
872
|
+
// Guarded: close() rejects this request synchronously, possibly before the caller has
|
|
873
|
+
// attached its handler. See guardedPromise().
|
|
874
|
+
return guardedPromise((resolve, reject) => {
|
|
887
875
|
this.requestTagMap.set(tag, { command, attributes, options, resolve, reject });
|
|
888
876
|
this.requestQueue.push({ tag, command, attributes, options });
|
|
889
877
|
// trySend() settles dispatch failures itself, by rejecting the affected
|
|
@@ -891,13 +879,6 @@ class ImapFlow extends EventEmitter {
|
|
|
891
879
|
// dispatch machinery itself can never surface as a floating rejection.
|
|
892
880
|
this.trySend().catch(err => logConnectionError(this, 'Failed to dispatch command', err));
|
|
893
881
|
});
|
|
894
|
-
|
|
895
|
-
// Prevent unhandled promise rejection if close() rejects this request
|
|
896
|
-
// synchronously before the caller's handler is attached. The rejection
|
|
897
|
-
// still propagates normally to the caller's await/.catch().
|
|
898
|
-
promise.catch(noop);
|
|
899
|
-
|
|
900
|
-
return promise;
|
|
901
882
|
}
|
|
902
883
|
|
|
903
884
|
// Resolves an untagged server response to the keyword it is dispatched on. IMAP untagged
|
|
@@ -1366,7 +1347,7 @@ class ImapFlow extends EventEmitter {
|
|
|
1366
1347
|
// Connection closed during back-off: reject promptly with a
|
|
1367
1348
|
// connection error (carrying any server BYE reason) instead of
|
|
1368
1349
|
// waiting out the throttle delay.
|
|
1369
|
-
request.reject(this.createNoConnectionError(this.byeReason));
|
|
1350
|
+
request.reject(this.createNoConnectionError(this.byeReason, { rejectedFrom: 'throttleAbort', command: request.command }));
|
|
1370
1351
|
break;
|
|
1371
1352
|
}
|
|
1372
1353
|
}
|
|
@@ -1865,9 +1846,7 @@ class ImapFlow extends EventEmitter {
|
|
|
1865
1846
|
try {
|
|
1866
1847
|
/* c8 ignore start */ // race: connection closed during the TLS handshake window
|
|
1867
1848
|
if (this.isClosed) {
|
|
1868
|
-
|
|
1869
|
-
err.code = 'NoConnection';
|
|
1870
|
-
return settle(err);
|
|
1849
|
+
return settle(this.createNoConnectionError(false, { rejectedFrom: 'tlsUpgrade' }));
|
|
1871
1850
|
}
|
|
1872
1851
|
/* c8 ignore stop */
|
|
1873
1852
|
|
|
@@ -2433,7 +2412,8 @@ class ImapFlow extends EventEmitter {
|
|
|
2433
2412
|
}
|
|
2434
2413
|
}
|
|
2435
2414
|
|
|
2436
|
-
|
|
2415
|
+
// Guarded: close() rejects a pending connect() synchronously. See guardedPromise().
|
|
2416
|
+
let connectPromise = guardedPromise((resolve, reject) => {
|
|
2437
2417
|
// Whatever the proxy phase already used is gone from the budget
|
|
2438
2418
|
this.connectTimeout = setTimeout(() => {
|
|
2439
2419
|
let err = deadline.error();
|
|
@@ -2533,10 +2513,6 @@ class ImapFlow extends EventEmitter {
|
|
|
2533
2513
|
this.socket.on('error', this._connectErrorHandler);
|
|
2534
2514
|
});
|
|
2535
2515
|
|
|
2536
|
-
// Prevent unhandled promise rejection if close() rejects the connect
|
|
2537
|
-
// promise synchronously. The rejection still propagates to the caller.
|
|
2538
|
-
connectPromise.catch(noop);
|
|
2539
|
-
|
|
2540
2516
|
await connectPromise;
|
|
2541
2517
|
}
|
|
2542
2518
|
|
|
@@ -2563,11 +2539,29 @@ class ImapFlow extends EventEmitter {
|
|
|
2563
2539
|
setImmediate(() => this.close());
|
|
2564
2540
|
}
|
|
2565
2541
|
|
|
2566
|
-
// Builds
|
|
2567
|
-
//
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
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.
|
|
2551
|
+
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;
|
|
2559
|
+
}
|
|
2560
|
+
|
|
2561
|
+
// The standard "connection not available" error, optionally annotated with the server's BYE
|
|
2562
|
+
// reason. Single source of truth so every NoConnection rejection is consistent.
|
|
2563
|
+
createNoConnectionError(byeReason, meta) {
|
|
2564
|
+
const error = this.createConnectionError('NoConnection', 'Connection not available', meta);
|
|
2571
2565
|
if (byeReason) {
|
|
2572
2566
|
error.reason = byeReason;
|
|
2573
2567
|
}
|
|
@@ -2612,9 +2606,7 @@ class ImapFlow extends EventEmitter {
|
|
|
2612
2606
|
if (typeof this._upgradeReject === 'function') {
|
|
2613
2607
|
let reject = this._upgradeReject;
|
|
2614
2608
|
this._upgradeReject = null;
|
|
2615
|
-
|
|
2616
|
-
err.code = 'NoConnection';
|
|
2617
|
-
reject(err);
|
|
2609
|
+
reject(this.createNoConnectionError(false, { rejectedFrom: 'upgrade' }));
|
|
2618
2610
|
}
|
|
2619
2611
|
|
|
2620
2612
|
if (typeof this.initialReject === 'function' && !this.options.verifyOnly) {
|
|
@@ -2630,9 +2622,9 @@ class ImapFlow extends EventEmitter {
|
|
|
2630
2622
|
if (this.byeReason) {
|
|
2631
2623
|
err.reason = this.byeReason;
|
|
2632
2624
|
}
|
|
2633
|
-
// Synchronous rejection is safe: connectPromise
|
|
2634
|
-
//
|
|
2635
|
-
//
|
|
2625
|
+
// Synchronous rejection is safe: connectPromise was built by guardedPromise(),
|
|
2626
|
+
// so the rejection is already observed. close() is synchronous, so all cleanup
|
|
2627
|
+
// completes before any microtask rejection handler runs.
|
|
2636
2628
|
reject(err);
|
|
2637
2629
|
}
|
|
2638
2630
|
|
|
@@ -2690,18 +2682,14 @@ class ImapFlow extends EventEmitter {
|
|
|
2690
2682
|
}
|
|
2691
2683
|
}
|
|
2692
2684
|
|
|
2693
|
-
//
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
//
|
|
2697
|
-
// getMailboxLock() promise already has .catch(noop) attached, so the
|
|
2698
|
-
// rejection is observed immediately and will not trigger
|
|
2699
|
-
// unhandledRejection. close() is synchronous, so all remaining cleanup
|
|
2700
|
-
// runs before any microtask rejection handler fires.
|
|
2685
|
+
// Reject pending requests and locks synchronously. Every promise rejected here was
|
|
2686
|
+
// built by guardedPromise(), so its rejection is already observed and cannot trigger
|
|
2687
|
+
// unhandledRejection. close() is synchronous, so all remaining cleanup runs before
|
|
2688
|
+
// any microtask rejection handler fires.
|
|
2701
2689
|
let byeReason = this.byeReason;
|
|
2702
2690
|
|
|
2703
2691
|
for (let request of pendingRequests) {
|
|
2704
|
-
request.reject(createNoConnectionError(byeReason));
|
|
2692
|
+
request.reject(this.createNoConnectionError(byeReason, { rejectedFrom: 'pendingRequest', command: request.command }));
|
|
2705
2693
|
}
|
|
2706
2694
|
|
|
2707
2695
|
// Clear current lock - holder will see errors when they try operations.
|
|
@@ -2720,7 +2708,7 @@ class ImapFlow extends EventEmitter {
|
|
|
2720
2708
|
lock.acquireTimer = null;
|
|
2721
2709
|
}
|
|
2722
2710
|
if (typeof lock.reject === 'function') {
|
|
2723
|
-
lock.reject(createNoConnectionError(byeReason));
|
|
2711
|
+
lock.reject(this.createNoConnectionError(byeReason, { rejectedFrom: 'mailboxLock', path: lock.path }));
|
|
2724
2712
|
}
|
|
2725
2713
|
}
|
|
2726
2714
|
}
|
|
@@ -3720,9 +3708,7 @@ class ImapFlow extends EventEmitter {
|
|
|
3720
3708
|
lastRes = res;
|
|
3721
3709
|
|
|
3722
3710
|
if (this.isClosed || !this.socket || this.socket.destroyed) {
|
|
3723
|
-
|
|
3724
|
-
error.code = 'EConnectionClosed';
|
|
3725
|
-
throw error;
|
|
3711
|
+
throw this.createConnectionError('EConnectionClosed', 'Connection closed', { rejectedFrom: 'fetchStream', command: 'FETCH' });
|
|
3726
3712
|
}
|
|
3727
3713
|
|
|
3728
3714
|
yield res.response;
|
|
@@ -4427,7 +4413,7 @@ class ImapFlow extends EventEmitter {
|
|
|
4427
4413
|
}
|
|
4428
4414
|
|
|
4429
4415
|
if (!this.socket || this.socket.destroyed) {
|
|
4430
|
-
throw this.createNoConnectionError();
|
|
4416
|
+
throw this.createNoConnectionError(false, { rejectedFrom: 'noSocket', command });
|
|
4431
4417
|
}
|
|
4432
4418
|
|
|
4433
4419
|
clearTimeout(this.idleStartTimer);
|
|
@@ -4470,7 +4456,7 @@ class ImapFlow extends EventEmitter {
|
|
|
4470
4456
|
}
|
|
4471
4457
|
|
|
4472
4458
|
if (!this.socket || this.socket.destroyed) {
|
|
4473
|
-
throw this.createNoConnectionError();
|
|
4459
|
+
throw this.createNoConnectionError(false, { rejectedFrom: 'noSocket', command });
|
|
4474
4460
|
}
|
|
4475
4461
|
|
|
4476
4462
|
let handler = this.commands.get(command);
|
|
@@ -4587,9 +4573,7 @@ class ImapFlow extends EventEmitter {
|
|
|
4587
4573
|
|
|
4588
4574
|
if (!this.usable || !this.socket || this.socket.destroyed) {
|
|
4589
4575
|
this.log.trace({ msg: 'Failed to acquire mailbox lock', path, lockId, idling: this.idling });
|
|
4590
|
-
|
|
4591
|
-
error.code = 'NoConnection';
|
|
4592
|
-
reject(error);
|
|
4576
|
+
reject(this.createNoConnectionError(false, { rejectedFrom: 'mailboxLock', path }));
|
|
4593
4577
|
continue; // Process next lock in queue
|
|
4594
4578
|
}
|
|
4595
4579
|
|
|
@@ -4713,7 +4697,8 @@ class ImapFlow extends EventEmitter {
|
|
|
4713
4697
|
: null
|
|
4714
4698
|
});
|
|
4715
4699
|
|
|
4716
|
-
|
|
4700
|
+
// Guarded: close() rejects every queued lock synchronously. See guardedPromise().
|
|
4701
|
+
let lockPromise = guardedPromise((resolve, reject) => {
|
|
4717
4702
|
let lockEntry = { resolve, reject, path, options, lockId };
|
|
4718
4703
|
this.locks.push(lockEntry);
|
|
4719
4704
|
|
|
@@ -4737,10 +4722,6 @@ class ImapFlow extends EventEmitter {
|
|
|
4737
4722
|
this.processLocks().catch(err => reject(err));
|
|
4738
4723
|
});
|
|
4739
4724
|
|
|
4740
|
-
// Prevent unhandled promise rejection if close() rejects this lock
|
|
4741
|
-
// synchronously. The rejection still propagates to the caller.
|
|
4742
|
-
lockPromise.catch(noop);
|
|
4743
|
-
|
|
4744
4725
|
return lockPromise;
|
|
4745
4726
|
}
|
|
4746
4727
|
|
package/lib/tools.js
CHANGED
|
@@ -69,7 +69,48 @@ class AuthenticationFailure extends Error {
|
|
|
69
69
|
authenticationFailed = true;
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
// Deliberate no-op, used as the observer a guarded promise attaches to its own rejection.
|
|
73
|
+
const noop = () => {};
|
|
74
|
+
|
|
72
75
|
const tools = {
|
|
76
|
+
noop,
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Creates a promise whose rejection is observed as soon as it exists.
|
|
80
|
+
*
|
|
81
|
+
* close() rejects every promise it owns - the in-flight and queued commands, the pending
|
|
82
|
+
* connect(), the queued mailbox locks, the waiters for an IDLE break - synchronously, from a
|
|
83
|
+
* socket event. A consumer that only reaches its `await` a microtask later has not attached a
|
|
84
|
+
* handler yet at the moment Node decides whether the rejection was observed, and the whole
|
|
85
|
+
* worker dies on the resulting unhandledRejection. The pre-attached observer settles that
|
|
86
|
+
* question; the rejection still propagates normally to whoever awaits the returned promise.
|
|
87
|
+
*
|
|
88
|
+
* Creation and guarding are one call because splitting them is what actually goes wrong: the
|
|
89
|
+
* guard was hand-attached at three of the four sites and the fourth (the IDLE-break waiter)
|
|
90
|
+
* went unguarded, on exactly the path a server BYE takes.
|
|
91
|
+
*
|
|
92
|
+
* @param {Function} executor - Promise executor, (resolve, reject) => {}
|
|
93
|
+
* @returns {Promise} The promise, with its rejection already observed
|
|
94
|
+
*/
|
|
95
|
+
guardedPromise(executor) {
|
|
96
|
+
let promise = new Promise(executor);
|
|
97
|
+
promise.catch(noop);
|
|
98
|
+
return promise;
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* The already-rejected form of guardedPromise(), for a call that has to hand back a rejected
|
|
103
|
+
* promise rather than throw.
|
|
104
|
+
*
|
|
105
|
+
* @param {Error} error - Rejection reason
|
|
106
|
+
* @returns {Promise} Rejected promise, with its rejection already observed
|
|
107
|
+
*/
|
|
108
|
+
guardedReject(error) {
|
|
109
|
+
let promise = Promise.reject(error);
|
|
110
|
+
promise.catch(noop);
|
|
111
|
+
return promise;
|
|
112
|
+
},
|
|
113
|
+
|
|
73
114
|
/**
|
|
74
115
|
* Detaches a background timer from the event loop, so it cannot keep the process alive on its
|
|
75
116
|
* own. Applied to every background timer (auto-IDLE, IDLE restart, fallback polling, throttle
|
package/package.json
CHANGED
|
@@ -5,10 +5,12 @@ const { ImapFlow } = require('../lib/imap-flow');
|
|
|
5
5
|
// Helper: create a minimal mock context with the fetch() generator bound to it.
|
|
6
6
|
// The `run` override controls how untagged FETCH responses are delivered.
|
|
7
7
|
const createFetchContext = runOverride => ({
|
|
8
|
+
id: 'test-cid',
|
|
8
9
|
mailbox: { path: 'INBOX', exists: 10 },
|
|
9
10
|
isClosed: false,
|
|
10
11
|
socket: { destroyed: false },
|
|
11
12
|
resolveRange: async range => range,
|
|
13
|
+
createConnectionError: ImapFlow.prototype.createConnectionError,
|
|
12
14
|
run: runOverride
|
|
13
15
|
});
|
|
14
16
|
|
package/test/imapflow-test.js
CHANGED
|
@@ -47,10 +47,12 @@ module.exports['Create imapflow instance with custom logger'] = async test => {
|
|
|
47
47
|
// Helpers for testing prototype methods on a mock context
|
|
48
48
|
// ---------------------------------------------------------------------------
|
|
49
49
|
const createFetchContext = runOverride => ({
|
|
50
|
+
id: 'test-cid',
|
|
50
51
|
mailbox: { path: 'INBOX', exists: 10 },
|
|
51
52
|
isClosed: false,
|
|
52
53
|
socket: { destroyed: false },
|
|
53
54
|
resolveRange: async range => range,
|
|
55
|
+
createConnectionError: ImapFlow.prototype.createConnectionError,
|
|
54
56
|
run: runOverride
|
|
55
57
|
});
|
|
56
58
|
|
|
@@ -408,6 +408,98 @@ exports['Unhandled Rejection Prevention'] = {
|
|
|
408
408
|
});
|
|
409
409
|
},
|
|
410
410
|
|
|
411
|
+
'close() stamps the rejection site, command and connection id on the error'(test) {
|
|
412
|
+
test.expect(5);
|
|
413
|
+
|
|
414
|
+
const client = new ImapFlow({
|
|
415
|
+
host: '127.0.0.1',
|
|
416
|
+
port: 1,
|
|
417
|
+
secure: false,
|
|
418
|
+
logger: false,
|
|
419
|
+
id: 'test-cid'
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
client.state = client.states.AUTHENTICATED;
|
|
423
|
+
client.socket = new net.Socket();
|
|
424
|
+
|
|
425
|
+
const detector = installRejectionDetector(test);
|
|
426
|
+
|
|
427
|
+
let promise = client.exec('NOOP');
|
|
428
|
+
client.close();
|
|
429
|
+
|
|
430
|
+
setTimeout(() => {
|
|
431
|
+
detector.check();
|
|
432
|
+
promise.catch(err => {
|
|
433
|
+
test.equal(err.code, 'NoConnection', 'caller should receive NoConnection error');
|
|
434
|
+
test.equal(err.rejectedFrom, 'pendingRequest', 'error should name the rejection site');
|
|
435
|
+
test.equal(err.command, 'NOOP', 'error should name the command it belonged to');
|
|
436
|
+
test.equal(err.cid, 'test-cid', 'error should carry the connection id');
|
|
437
|
+
test.done();
|
|
438
|
+
});
|
|
439
|
+
}, 100);
|
|
440
|
+
},
|
|
441
|
+
|
|
442
|
+
'preCheck() waiter rejected by close() should not cause unhandled rejection'(test) {
|
|
443
|
+
test.expect(2);
|
|
444
|
+
|
|
445
|
+
// Never acknowledge IDLE with a "+" continuation. preCheck() can only send DONE once
|
|
446
|
+
// the server has acknowledged, so its waiter stays queued until close() tears the
|
|
447
|
+
// IDLE command down and runIdle() rejects everything still waiting.
|
|
448
|
+
const server = createMockServer({
|
|
449
|
+
extraCapabilities: 'IDLE',
|
|
450
|
+
onCommand(socket, tag, command) {
|
|
451
|
+
if (command === 'IDLE') {
|
|
452
|
+
return true;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
server.listen(0, '127.0.0.1', async () => {
|
|
458
|
+
const port = server.address().port;
|
|
459
|
+
|
|
460
|
+
const client = new ImapFlow({
|
|
461
|
+
host: '127.0.0.1',
|
|
462
|
+
port,
|
|
463
|
+
secure: false,
|
|
464
|
+
logger: false,
|
|
465
|
+
disableAutoIdle: true,
|
|
466
|
+
auth: {
|
|
467
|
+
user: 'test',
|
|
468
|
+
pass: 'test'
|
|
469
|
+
}
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
const detector = installRejectionDetector(test);
|
|
473
|
+
|
|
474
|
+
try {
|
|
475
|
+
await client.connect();
|
|
476
|
+
await client.mailboxOpen('INBOX');
|
|
477
|
+
|
|
478
|
+
client.idle().catch(() => {
|
|
479
|
+
// Expected: IDLE rejects when the connection is closed
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
// Let the IDLE command reach the server and install preCheck()
|
|
483
|
+
await new Promise(r => setTimeout(r, 100));
|
|
484
|
+
test.equal(typeof client.preCheck, 'function', 'IDLE should have installed preCheck()');
|
|
485
|
+
|
|
486
|
+
// Request an IDLE break and drop the returned promise on the floor, so the
|
|
487
|
+
// queued waiter has no handler of its own when close() rejects it
|
|
488
|
+
client.preCheck();
|
|
489
|
+
|
|
490
|
+
client.close();
|
|
491
|
+
|
|
492
|
+
await new Promise(r => setTimeout(r, 100));
|
|
493
|
+
detector.check();
|
|
494
|
+
} catch (err) {
|
|
495
|
+
detector.check();
|
|
496
|
+
test.ok(false, 'Unexpected error: ' + err.message);
|
|
497
|
+
} finally {
|
|
498
|
+
server.close(() => test.done());
|
|
499
|
+
}
|
|
500
|
+
});
|
|
501
|
+
},
|
|
502
|
+
|
|
411
503
|
'BAD response to FETCH should not cause unhandled rejection (Death 2)'(test) {
|
|
412
504
|
test.expect(3);
|
|
413
505
|
|