imapflow 1.2.14 → 1.2.16
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 +14 -0
- package/lib/imap-flow.js +20 -24
- package/lib/search-compiler.js +21 -4
- package/package.json +2 -2
- package/test/connection-edge-cases-test.js +8 -16
- package/test/search-compiler-test.js +81 -3
- package/test/unhandled-rejection-test.js +150 -11
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.2.16](https://github.com/postalsys/imapflow/compare/v1.2.15...v1.2.16) (2026-03-19)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* parenthesize compound OR/NOT search conditions (RFC 3501) ([347f668](https://github.com/postalsys/imapflow/commit/347f66827ea089acab88aa78485752d84b7b7f3d)), closes [#106](https://github.com/postalsys/imapflow/issues/106)
|
|
9
|
+
|
|
10
|
+
## [1.2.15](https://github.com/postalsys/imapflow/compare/v1.2.14...v1.2.15) (2026-03-16)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* replace setImmediate with synchronous rejection in close() ([8807221](https://github.com/postalsys/imapflow/commit/8807221b2b3bae55848129b6a4b8d08880ae5de8))
|
|
16
|
+
|
|
3
17
|
## [1.2.14](https://github.com/postalsys/imapflow/compare/v1.2.13...v1.2.14) (2026-03-15)
|
|
4
18
|
|
|
5
19
|
|
package/lib/imap-flow.js
CHANGED
|
@@ -585,7 +585,7 @@ class ImapFlow extends EventEmitter {
|
|
|
585
585
|
});
|
|
586
586
|
|
|
587
587
|
// Prevent unhandled promise rejection if close() rejects this request
|
|
588
|
-
//
|
|
588
|
+
// synchronously before the caller's handler is attached. The rejection
|
|
589
589
|
// still propagates normally to the caller's await/.catch().
|
|
590
590
|
promise.catch(noop);
|
|
591
591
|
|
|
@@ -1728,7 +1728,7 @@ class ImapFlow extends EventEmitter {
|
|
|
1728
1728
|
});
|
|
1729
1729
|
|
|
1730
1730
|
// Prevent unhandled promise rejection if close() rejects the connect
|
|
1731
|
-
// promise
|
|
1731
|
+
// promise synchronously. The rejection still propagates to the caller.
|
|
1732
1732
|
connectPromise.catch(noop);
|
|
1733
1733
|
|
|
1734
1734
|
await connectPromise;
|
|
@@ -1784,9 +1784,10 @@ class ImapFlow extends EventEmitter {
|
|
|
1784
1784
|
this.initialReject = false;
|
|
1785
1785
|
let err = new Error('Unexpected close');
|
|
1786
1786
|
err.code = `ClosedAfterConnect${this.secureConnection ? 'TLS' : 'Text'}`;
|
|
1787
|
-
//
|
|
1788
|
-
//
|
|
1789
|
-
|
|
1787
|
+
// Synchronous rejection is safe: connectPromise.catch(noop) is already
|
|
1788
|
+
// attached, so the rejection is observed immediately. close() is synchronous,
|
|
1789
|
+
// so all cleanup completes before any microtask rejection handler runs.
|
|
1790
|
+
reject(err);
|
|
1790
1791
|
}
|
|
1791
1792
|
|
|
1792
1793
|
if (typeof this.preCheck === 'function') {
|
|
@@ -1828,32 +1829,27 @@ class ImapFlow extends EventEmitter {
|
|
|
1828
1829
|
return error;
|
|
1829
1830
|
};
|
|
1830
1831
|
|
|
1831
|
-
// Reject pending requests
|
|
1832
|
-
//
|
|
1833
|
-
//
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
});
|
|
1832
|
+
// Reject pending requests and locks synchronously. Each exec() and
|
|
1833
|
+
// getMailboxLock() promise already has .catch(noop) attached, so the
|
|
1834
|
+
// rejection is observed immediately and will not trigger
|
|
1835
|
+
// unhandledRejection. close() is synchronous, so all remaining cleanup
|
|
1836
|
+
// runs before any microtask rejection handler fires.
|
|
1837
|
+
let byeReason = this.byeReason;
|
|
1838
|
+
|
|
1839
|
+
for (let request of pendingRequests) {
|
|
1840
|
+
request.reject(createNoConnectionError(byeReason));
|
|
1841
1841
|
}
|
|
1842
1842
|
|
|
1843
1843
|
// Clear current lock - holder will see errors when they try operations
|
|
1844
1844
|
this.currentLock = false;
|
|
1845
1845
|
|
|
1846
|
-
// Reject pending mailbox locks via setImmediate for consistency
|
|
1847
1846
|
if (this.locks && this.locks.length) {
|
|
1848
|
-
let byeReason = this.byeReason;
|
|
1849
1847
|
let pendingLocks = this.locks.splice(0); // Take all locks and clear the array
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
lock.reject(createNoConnectionError(byeReason));
|
|
1854
|
-
}
|
|
1848
|
+
for (let lock of pendingLocks) {
|
|
1849
|
+
if (typeof lock.reject === 'function') {
|
|
1850
|
+
lock.reject(createNoConnectionError(byeReason));
|
|
1855
1851
|
}
|
|
1856
|
-
}
|
|
1852
|
+
}
|
|
1857
1853
|
}
|
|
1858
1854
|
|
|
1859
1855
|
// cleanup compression streams if they exist
|
|
@@ -3634,7 +3630,7 @@ class ImapFlow extends EventEmitter {
|
|
|
3634
3630
|
});
|
|
3635
3631
|
|
|
3636
3632
|
// Prevent unhandled promise rejection if close() rejects this lock
|
|
3637
|
-
//
|
|
3633
|
+
// synchronously. The rejection still propagates to the caller.
|
|
3638
3634
|
lockPromise.catch(noop);
|
|
3639
3635
|
|
|
3640
3636
|
return lockPromise;
|
package/lib/search-compiler.js
CHANGED
|
@@ -142,6 +142,17 @@ module.exports.searchCompiler = (connection, query) => {
|
|
|
142
142
|
* @param {Object} params - Query parameters to process
|
|
143
143
|
*/
|
|
144
144
|
const walk = params => {
|
|
145
|
+
// Walks a query object and wraps the resulting attributes in a
|
|
146
|
+
// sub-array so the IMAP compiler emits parentheses around them.
|
|
147
|
+
// Used when a single search-key is required (NOT, OR operands)
|
|
148
|
+
// but the condition has multiple keys (RFC 3501 Section 6.4.4).
|
|
149
|
+
let walkGrouped = obj => {
|
|
150
|
+
let startIdx = attributes.length;
|
|
151
|
+
walk(obj);
|
|
152
|
+
let subAttrs = attributes.splice(startIdx);
|
|
153
|
+
attributes.push(subAttrs);
|
|
154
|
+
};
|
|
155
|
+
|
|
145
156
|
Object.keys(params || {}).forEach(term => {
|
|
146
157
|
switch (term.toUpperCase()) {
|
|
147
158
|
// Custom sequence range support (non-standard)
|
|
@@ -318,8 +329,11 @@ module.exports.searchCompiler = (connection, query) => {
|
|
|
318
329
|
case 'NOT':
|
|
319
330
|
if (params[term] && typeof params[term] === 'object') {
|
|
320
331
|
attributes.push({ type: 'ATOM', value: 'NOT' });
|
|
321
|
-
|
|
322
|
-
|
|
332
|
+
if (Object.keys(params[term]).length > 1) {
|
|
333
|
+
walkGrouped(params[term]);
|
|
334
|
+
} else {
|
|
335
|
+
walk(params[term]);
|
|
336
|
+
}
|
|
323
337
|
}
|
|
324
338
|
break;
|
|
325
339
|
|
|
@@ -388,7 +402,6 @@ module.exports.searchCompiler = (connection, query) => {
|
|
|
388
402
|
*/
|
|
389
403
|
let walkOrTree = entry => {
|
|
390
404
|
if (Array.isArray(entry)) {
|
|
391
|
-
// Only add OR for multiple items
|
|
392
405
|
if (entry.length > 1) {
|
|
393
406
|
attributes.push({ type: 'ATOM', value: 'OR' });
|
|
394
407
|
}
|
|
@@ -396,7 +409,11 @@ module.exports.searchCompiler = (connection, query) => {
|
|
|
396
409
|
return;
|
|
397
410
|
}
|
|
398
411
|
if (entry && typeof entry === 'object') {
|
|
399
|
-
|
|
412
|
+
if (Object.keys(entry).length > 1) {
|
|
413
|
+
walkGrouped(entry);
|
|
414
|
+
} else {
|
|
415
|
+
walk(entry);
|
|
416
|
+
}
|
|
400
417
|
}
|
|
401
418
|
};
|
|
402
419
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "imapflow",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.16",
|
|
4
4
|
"description": "IMAP Client for Node",
|
|
5
5
|
"main": "lib/imap-flow.js",
|
|
6
6
|
"types": "lib/imap-flow.d.ts",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"libbase64": "1.3.0",
|
|
49
49
|
"libmime": "5.3.7",
|
|
50
50
|
"libqp": "2.1.1",
|
|
51
|
-
"nodemailer": "8.0.
|
|
51
|
+
"nodemailer": "8.0.3",
|
|
52
52
|
"pino": "10.3.1",
|
|
53
53
|
"socks": "2.8.7"
|
|
54
54
|
}
|
|
@@ -863,7 +863,7 @@ module.exports['Connection Edge: Close with empty locks array'] = test => {
|
|
|
863
863
|
test.done();
|
|
864
864
|
};
|
|
865
865
|
|
|
866
|
-
module.exports['Connection Edge: Lock rejection
|
|
866
|
+
module.exports['Connection Edge: Lock rejection happens synchronously during close'] = test => {
|
|
867
867
|
let client = new ImapFlow({
|
|
868
868
|
host: 'imap.example.com',
|
|
869
869
|
port: 993,
|
|
@@ -871,7 +871,6 @@ module.exports['Connection Edge: Lock rejection is deferred via setImmediate'] =
|
|
|
871
871
|
});
|
|
872
872
|
|
|
873
873
|
let rejectionTime = null;
|
|
874
|
-
let closeTime = null;
|
|
875
874
|
|
|
876
875
|
client.locks = [
|
|
877
876
|
{
|
|
@@ -888,16 +887,10 @@ module.exports['Connection Edge: Lock rejection is deferred via setImmediate'] =
|
|
|
888
887
|
client.usable = true;
|
|
889
888
|
|
|
890
889
|
client.close();
|
|
891
|
-
closeTime = Date.now();
|
|
892
890
|
|
|
893
|
-
// Rejection should
|
|
894
|
-
test.
|
|
895
|
-
|
|
896
|
-
setImmediate(() => {
|
|
897
|
-
test.ok(rejectionTime !== null, 'Rejection should happen after setImmediate');
|
|
898
|
-
test.ok(rejectionTime >= closeTime, 'Rejection should happen after close()');
|
|
899
|
-
test.done();
|
|
900
|
-
});
|
|
891
|
+
// Rejection should have happened synchronously during close()
|
|
892
|
+
test.ok(rejectionTime !== null, 'Rejection should happen synchronously during close()');
|
|
893
|
+
test.done();
|
|
901
894
|
};
|
|
902
895
|
|
|
903
896
|
module.exports['Connection Edge: Pending requests and locks both rejected on close'] = test => {
|
|
@@ -940,11 +933,10 @@ module.exports['Connection Edge: Pending requests and locks both rejected on clo
|
|
|
940
933
|
|
|
941
934
|
client.close();
|
|
942
935
|
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
});
|
|
936
|
+
// Rejections happen synchronously during close()
|
|
937
|
+
test.ok(requestRejected, 'Pending request should be rejected');
|
|
938
|
+
test.ok(lockRejected, 'Pending lock should be rejected');
|
|
939
|
+
test.done();
|
|
948
940
|
};
|
|
949
941
|
|
|
950
942
|
module.exports['Connection Edge: exec throws NoConnection when in LOGOUT state'] = async test => {
|
|
@@ -15,9 +15,19 @@ let createMockConnection = (options = {}) => ({
|
|
|
15
15
|
mailbox: options.mailbox || createMockMailbox()
|
|
16
16
|
});
|
|
17
17
|
|
|
18
|
-
// Helper to find attribute by value
|
|
19
|
-
let findAttr = (attrs, value) =>
|
|
20
|
-
let
|
|
18
|
+
// Helper to find attribute by value (recurses into sub-arrays for parenthesized groups)
|
|
19
|
+
let findAttr = (attrs, value) => {
|
|
20
|
+
for (let a of attrs) {
|
|
21
|
+
if (Array.isArray(a)) {
|
|
22
|
+
let found = findAttr(a, value);
|
|
23
|
+
if (found) return found;
|
|
24
|
+
} else if (a.value === value) {
|
|
25
|
+
return a;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return undefined;
|
|
29
|
+
};
|
|
30
|
+
let hasAttr = (attrs, value) => !!findAttr(attrs, value);
|
|
21
31
|
|
|
22
32
|
// ============================================
|
|
23
33
|
// Basic functionality tests
|
|
@@ -659,6 +669,10 @@ module.exports['Search Compiler: NOT with nested conditions'] = test => {
|
|
|
659
669
|
test.ok(hasAttr(compiled, 'NOT'));
|
|
660
670
|
test.ok(hasAttr(compiled, 'SEEN'));
|
|
661
671
|
test.ok(hasAttr(compiled, 'FROM'));
|
|
672
|
+
// Compound NOT conditions should be wrapped in a sub-array (parenthesized)
|
|
673
|
+
// so the server treats them as a single search-key
|
|
674
|
+
test.equal(compiled[0].value, 'NOT');
|
|
675
|
+
test.ok(Array.isArray(compiled[1]), 'compound NOT should be parenthesized');
|
|
662
676
|
test.done();
|
|
663
677
|
};
|
|
664
678
|
|
|
@@ -899,6 +913,70 @@ module.exports['Search Compiler: OR with 5 conditions produces 4 OR atoms'] = te
|
|
|
899
913
|
test.done();
|
|
900
914
|
};
|
|
901
915
|
|
|
916
|
+
module.exports['Search Compiler: OR with compound conditions wraps in parentheses'] = test => {
|
|
917
|
+
let connection = createMockConnection();
|
|
918
|
+
let compiled = searchCompiler(connection, {
|
|
919
|
+
or: [
|
|
920
|
+
{ to: 'a@example.com', from: 'b@example.com' },
|
|
921
|
+
{ to: 'c@example.com', from: 'd@example.com' }
|
|
922
|
+
]
|
|
923
|
+
});
|
|
924
|
+
|
|
925
|
+
// OR should be present
|
|
926
|
+
test.equal(compiled[0].value, 'OR');
|
|
927
|
+
// Each compound condition should be a sub-array (parenthesized)
|
|
928
|
+
test.ok(Array.isArray(compiled[1]), 'first compound operand should be parenthesized');
|
|
929
|
+
test.ok(Array.isArray(compiled[2]), 'second compound operand should be parenthesized');
|
|
930
|
+
// Check contents of parenthesized groups
|
|
931
|
+
test.ok(hasAttr(compiled[1], 'TO'), 'first group should have TO');
|
|
932
|
+
test.ok(hasAttr(compiled[1], 'FROM'), 'first group should have FROM');
|
|
933
|
+
test.ok(hasAttr(compiled[2], 'TO'), 'second group should have TO');
|
|
934
|
+
test.ok(hasAttr(compiled[2], 'FROM'), 'second group should have FROM');
|
|
935
|
+
test.done();
|
|
936
|
+
};
|
|
937
|
+
|
|
938
|
+
module.exports['Search Compiler: OR with single-key conditions stays flat'] = test => {
|
|
939
|
+
let connection = createMockConnection();
|
|
940
|
+
let compiled = searchCompiler(connection, {
|
|
941
|
+
or: [{ from: 'a@example.com' }, { from: 'b@example.com' }]
|
|
942
|
+
});
|
|
943
|
+
|
|
944
|
+
// Single-key conditions should not be wrapped in sub-arrays
|
|
945
|
+
test.equal(compiled[0].value, 'OR');
|
|
946
|
+
test.ok(!Array.isArray(compiled[1]), 'single-key operand should not be parenthesized');
|
|
947
|
+
test.equal(compiled[1].value, 'FROM');
|
|
948
|
+
test.done();
|
|
949
|
+
};
|
|
950
|
+
|
|
951
|
+
module.exports['Search Compiler: OR with 5 compound conditions from issue #106'] = test => {
|
|
952
|
+
let connection = createMockConnection();
|
|
953
|
+
let compiled = searchCompiler(connection, {
|
|
954
|
+
or: [
|
|
955
|
+
{ to: 'myemail@domain.com', from: '@anotherdomain.com' },
|
|
956
|
+
{ to: 'myemail@domain.com', from: '@aseconddomain.com' },
|
|
957
|
+
{ to: 'myemail@domain.com', from: '@athirddomain.fr' },
|
|
958
|
+
{ to: 'anotheremail@domain.com', from: '@anotherdomain.fr' },
|
|
959
|
+
{ to: 'anotheremail@domain.com', from: '@aseconddomain.com' }
|
|
960
|
+
]
|
|
961
|
+
});
|
|
962
|
+
|
|
963
|
+
// 5 conditions need 4 OR atoms
|
|
964
|
+
let orCount = compiled.filter(a => a.value === 'OR').length;
|
|
965
|
+
test.equal(orCount, 4, 'should have exactly 4 OR atoms for 5 conditions');
|
|
966
|
+
|
|
967
|
+
// All compound conditions should be parenthesized (sub-arrays)
|
|
968
|
+
let subArrays = compiled.filter(a => Array.isArray(a));
|
|
969
|
+
test.equal(subArrays.length, 5, 'should have 5 parenthesized groups');
|
|
970
|
+
|
|
971
|
+
// Each group should contain both TO and FROM
|
|
972
|
+
subArrays.forEach((group, i) => {
|
|
973
|
+
test.ok(hasAttr(group, 'TO'), 'group ' + i + ' should have TO');
|
|
974
|
+
test.ok(hasAttr(group, 'FROM'), 'group ' + i + ' should have FROM');
|
|
975
|
+
});
|
|
976
|
+
|
|
977
|
+
test.done();
|
|
978
|
+
};
|
|
979
|
+
|
|
902
980
|
// ============================================
|
|
903
981
|
// Unicode CHARSET in HEADER searches
|
|
904
982
|
// ============================================
|
|
@@ -3,10 +3,11 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* Tests for unhandled rejection prevention.
|
|
5
5
|
*
|
|
6
|
-
* When close() runs, it rejects pending promises
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* rejections escape while the caller
|
|
6
|
+
* When close() runs, it rejects pending promises synchronously. exec() and
|
|
7
|
+
* getMailboxLock() attach .catch(noop) before returning, so the rejection is
|
|
8
|
+
* observed immediately and does not trigger Node.js unhandledRejection.
|
|
9
|
+
* These tests verify that no unhandled rejections escape while the caller
|
|
10
|
+
* still receives the expected error.
|
|
10
11
|
*
|
|
11
12
|
* Key fix: exec() and getMailboxLock() are non-async, returning the promise
|
|
12
13
|
* directly (with .catch(noop)), so the caller gets the same promise object
|
|
@@ -18,8 +19,13 @@
|
|
|
18
19
|
const net = require('net');
|
|
19
20
|
const { ImapFlow } = require('../lib/imap-flow');
|
|
20
21
|
|
|
21
|
-
// Create a
|
|
22
|
-
|
|
22
|
+
// Create a mock IMAP server with optional custom behavior.
|
|
23
|
+
// options.extraCapabilities - additional capabilities (e.g., 'IDLE')
|
|
24
|
+
// options.onCommand(socket, tag, command) - custom handler; return true if handled
|
|
25
|
+
function createMockServer(options) {
|
|
26
|
+
const extraCaps = options && options.extraCapabilities ? ' ' + options.extraCapabilities : '';
|
|
27
|
+
const onCommand = options && options.onCommand;
|
|
28
|
+
|
|
23
29
|
const server = net.createServer(socket => {
|
|
24
30
|
socket.write('* OK Mock IMAP Server ready\r\n');
|
|
25
31
|
|
|
@@ -34,8 +40,12 @@ function createMockServer() {
|
|
|
34
40
|
const tag = parts[0];
|
|
35
41
|
const command = parts[1] ? parts[1].toUpperCase() : '';
|
|
36
42
|
|
|
43
|
+
if (onCommand && onCommand(socket, tag, command, line)) {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
|
|
37
47
|
if (command === 'CAPABILITY') {
|
|
38
|
-
socket.write(
|
|
48
|
+
socket.write(`* CAPABILITY IMAP4rev1 AUTH=PLAIN${extraCaps}\r\n`);
|
|
39
49
|
socket.write(`${tag} OK CAPABILITY completed\r\n`);
|
|
40
50
|
} else if (command === 'LOGIN') {
|
|
41
51
|
socket.write(`${tag} OK LOGIN completed\r\n`);
|
|
@@ -53,6 +63,12 @@ function createMockServer() {
|
|
|
53
63
|
} else if (command === 'ID') {
|
|
54
64
|
socket.write('* ID NIL\r\n');
|
|
55
65
|
socket.write(`${tag} OK ID completed\r\n`);
|
|
66
|
+
} else if (command === 'SELECT' || command === 'EXAMINE') {
|
|
67
|
+
socket.write('* 1 EXISTS\r\n');
|
|
68
|
+
socket.write('* 1 RECENT\r\n');
|
|
69
|
+
socket.write('* OK [UIDVALIDITY 1] UIDs valid\r\n');
|
|
70
|
+
socket.write('* OK [UIDNEXT 2] Predicted next UID\r\n');
|
|
71
|
+
socket.write(`${tag} OK SELECT completed\r\n`);
|
|
56
72
|
} else if (command === 'NOOP') {
|
|
57
73
|
socket.write(`${tag} OK NOOP completed\r\n`);
|
|
58
74
|
} else if (tag && command) {
|
|
@@ -103,9 +119,8 @@ exports['Unhandled Rejection Prevention'] = {
|
|
|
103
119
|
const detector = installRejectionDetector(test);
|
|
104
120
|
|
|
105
121
|
// Create a pending request, then immediately close.
|
|
106
|
-
// close() rejects the request
|
|
107
|
-
//
|
|
108
|
-
// before the caller's handler is attached.
|
|
122
|
+
// close() rejects the request synchronously. The .catch(noop) on the
|
|
123
|
+
// exec() promise ensures the rejection is observed immediately.
|
|
109
124
|
let promise = client.exec('NOOP');
|
|
110
125
|
client.close();
|
|
111
126
|
|
|
@@ -246,7 +261,7 @@ exports['Unhandled Rejection Prevention'] = {
|
|
|
246
261
|
await client.connect();
|
|
247
262
|
|
|
248
263
|
// Request a lock, then immediately close before it resolves.
|
|
249
|
-
// close() rejects pending locks
|
|
264
|
+
// close() rejects pending locks synchronously.
|
|
250
265
|
let lockPromise = client.getMailboxLock('INBOX');
|
|
251
266
|
client.close();
|
|
252
267
|
|
|
@@ -323,5 +338,129 @@ exports['Unhandled Rejection Prevention'] = {
|
|
|
323
338
|
}
|
|
324
339
|
);
|
|
325
340
|
});
|
|
341
|
+
},
|
|
342
|
+
|
|
343
|
+
'BYE during IDLE should not cause unhandled rejection (Death 1)'(test) {
|
|
344
|
+
test.expect(2);
|
|
345
|
+
|
|
346
|
+
const server = createMockServer({
|
|
347
|
+
extraCapabilities: 'IDLE',
|
|
348
|
+
onCommand(socket, tag, command) {
|
|
349
|
+
if (command === 'IDLE') {
|
|
350
|
+
socket.write('+ idling\r\n');
|
|
351
|
+
// After a short delay, send BYE and close (simulates token expiry)
|
|
352
|
+
setTimeout(() => {
|
|
353
|
+
try {
|
|
354
|
+
socket.write('* BYE Session invalidated - AccessTokenExpired\r\n');
|
|
355
|
+
socket.end();
|
|
356
|
+
} catch {
|
|
357
|
+
// socket may already be closed
|
|
358
|
+
}
|
|
359
|
+
}, 50);
|
|
360
|
+
return true;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
server.listen(0, '127.0.0.1', async () => {
|
|
366
|
+
const port = server.address().port;
|
|
367
|
+
|
|
368
|
+
const client = new ImapFlow({
|
|
369
|
+
host: '127.0.0.1',
|
|
370
|
+
port,
|
|
371
|
+
secure: false,
|
|
372
|
+
logger: false,
|
|
373
|
+
auth: {
|
|
374
|
+
user: 'test',
|
|
375
|
+
pass: 'test'
|
|
376
|
+
}
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
const detector = installRejectionDetector(test);
|
|
380
|
+
|
|
381
|
+
try {
|
|
382
|
+
await client.connect();
|
|
383
|
+
await client.mailboxOpen('INBOX');
|
|
384
|
+
|
|
385
|
+
// Start IDLE and wait for the BYE-triggered close
|
|
386
|
+
await new Promise((resolve, reject) => {
|
|
387
|
+
client.idle().catch(() => {
|
|
388
|
+
// Expected: IDLE rejects when BYE arrives
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
client.on('close', () => {
|
|
392
|
+
// Wait for any deferred unhandled rejections to surface
|
|
393
|
+
setTimeout(resolve, 100);
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
// Safety timeout
|
|
397
|
+
setTimeout(() => reject(new Error('Timeout waiting for close')), 5000);
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
detector.check();
|
|
401
|
+
test.ok(true, 'No unhandled rejection during IDLE + BYE');
|
|
402
|
+
} catch (err) {
|
|
403
|
+
detector.check();
|
|
404
|
+
test.ok(false, 'Unexpected error: ' + err.message);
|
|
405
|
+
} finally {
|
|
406
|
+
server.close(() => test.done());
|
|
407
|
+
}
|
|
408
|
+
});
|
|
409
|
+
},
|
|
410
|
+
|
|
411
|
+
'BAD response to FETCH should not cause unhandled rejection (Death 2)'(test) {
|
|
412
|
+
test.expect(3);
|
|
413
|
+
|
|
414
|
+
const server = createMockServer({
|
|
415
|
+
onCommand(socket, tag, command) {
|
|
416
|
+
if (command === 'UID' || command === 'FETCH') {
|
|
417
|
+
// Simulate "Server Unavailable" error for any FETCH variant
|
|
418
|
+
socket.write(`${tag} BAD Server Unavailable. 15\r\n`);
|
|
419
|
+
return true;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
server.listen(0, '127.0.0.1', async () => {
|
|
425
|
+
const port = server.address().port;
|
|
426
|
+
|
|
427
|
+
const client = new ImapFlow({
|
|
428
|
+
host: '127.0.0.1',
|
|
429
|
+
port,
|
|
430
|
+
secure: false,
|
|
431
|
+
logger: false,
|
|
432
|
+
auth: {
|
|
433
|
+
user: 'test',
|
|
434
|
+
pass: 'test'
|
|
435
|
+
}
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
const detector = installRejectionDetector(test);
|
|
439
|
+
|
|
440
|
+
try {
|
|
441
|
+
await client.connect();
|
|
442
|
+
await client.mailboxOpen('INBOX');
|
|
443
|
+
|
|
444
|
+
// Attempt a FETCH that will get BAD response
|
|
445
|
+
try {
|
|
446
|
+
await client.fetchOne('*', { uid: true }, { uid: true });
|
|
447
|
+
test.ok(false, 'fetchOne should have rejected');
|
|
448
|
+
} catch (err) {
|
|
449
|
+
test.equal(err.message, 'Command failed', 'Should get Command failed error');
|
|
450
|
+
test.equal(err.responseStatus, 'BAD', 'Response status should be BAD');
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Wait for any deferred unhandled rejections
|
|
454
|
+
await new Promise(r => setTimeout(r, 100));
|
|
455
|
+
detector.check();
|
|
456
|
+
|
|
457
|
+
client.close();
|
|
458
|
+
} catch (err) {
|
|
459
|
+
detector.check();
|
|
460
|
+
test.ok(false, 'Unexpected error: ' + err.message);
|
|
461
|
+
} finally {
|
|
462
|
+
server.close(() => test.done());
|
|
463
|
+
}
|
|
464
|
+
});
|
|
326
465
|
}
|
|
327
466
|
};
|