imapflow 1.7.7 → 2.0.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.
- package/CHANGELOG.md +27 -0
- package/README.md +8 -2
- package/dist/cjs/charsets.d.ts +1 -0
- package/dist/cjs/charsets.js +294 -0
- package/dist/cjs/commands/append.d.ts +22 -0
- package/dist/cjs/commands/append.js +151 -0
- package/dist/cjs/commands/authenticate.d.ts +24 -0
- package/dist/cjs/commands/authenticate.js +223 -0
- package/dist/cjs/commands/capability.d.ts +8 -0
- package/dist/cjs/commands/capability.js +32 -0
- package/dist/cjs/commands/close.d.ts +8 -0
- package/dist/cjs/commands/close.js +39 -0
- package/dist/cjs/commands/compress.d.ts +8 -0
- package/dist/cjs/commands/compress.js +56 -0
- package/dist/cjs/commands/copy.d.ts +13 -0
- package/dist/cjs/commands/copy.js +44 -0
- package/dist/cjs/commands/copyuid-parser.d.ts +11 -0
- package/dist/cjs/commands/copyuid-parser.js +32 -0
- package/dist/cjs/commands/create.d.ts +11 -0
- package/dist/cjs/commands/create.js +80 -0
- package/dist/cjs/commands/delete.d.ts +11 -0
- package/dist/cjs/commands/delete.js +40 -0
- package/dist/cjs/commands/enable.d.ts +9 -0
- package/dist/cjs/commands/enable.js +61 -0
- package/dist/cjs/commands/esearch-parser.d.ts +17 -0
- package/dist/cjs/commands/esearch-parser.js +91 -0
- package/dist/cjs/commands/expunge.d.ts +12 -0
- package/dist/cjs/commands/expunge.js +60 -0
- package/dist/cjs/commands/fetch.d.ts +30 -0
- package/dist/cjs/commands/fetch.js +241 -0
- package/dist/cjs/commands/id.d.ts +10 -0
- package/dist/cjs/commands/id.js +80 -0
- package/dist/cjs/commands/idle.d.ts +9 -0
- package/dist/cjs/commands/idle.js +347 -0
- package/dist/cjs/commands/list.d.ts +16 -0
- package/dist/cjs/commands/list.js +518 -0
- package/dist/cjs/commands/login.d.ts +11 -0
- package/dist/cjs/commands/login.js +42 -0
- package/dist/cjs/commands/logout.d.ts +8 -0
- package/dist/cjs/commands/logout.js +47 -0
- package/dist/cjs/commands/move.d.ts +13 -0
- package/dist/cjs/commands/move.js +57 -0
- package/dist/cjs/commands/namespace.d.ts +25 -0
- package/dist/cjs/commands/namespace.js +139 -0
- package/dist/cjs/commands/noop.d.ts +8 -0
- package/dist/cjs/commands/noop.js +22 -0
- package/dist/cjs/commands/quota.d.ts +10 -0
- package/dist/cjs/commands/quota.js +119 -0
- package/dist/cjs/commands/rename.d.ts +12 -0
- package/dist/cjs/commands/rename.js +48 -0
- package/dist/cjs/commands/search.d.ts +15 -0
- package/dist/cjs/commands/search.js +228 -0
- package/dist/cjs/commands/select.d.ts +25 -0
- package/dist/cjs/commands/select.js +250 -0
- package/dist/cjs/commands/starttls.d.ts +8 -0
- package/dist/cjs/commands/starttls.js +30 -0
- package/dist/cjs/commands/status-fields.d.ts +14 -0
- package/dist/cjs/commands/status-fields.js +61 -0
- package/dist/cjs/commands/status.d.ts +12 -0
- package/dist/cjs/commands/status.js +108 -0
- package/dist/cjs/commands/store.d.ts +19 -0
- package/dist/cjs/commands/store.js +93 -0
- package/dist/cjs/commands/subscribe.d.ts +9 -0
- package/dist/cjs/commands/subscribe.js +31 -0
- package/dist/cjs/commands/unsubscribe.d.ts +9 -0
- package/dist/cjs/commands/unsubscribe.js +31 -0
- package/dist/cjs/connection-deadline.d.ts +49 -0
- package/dist/cjs/connection-deadline.js +91 -0
- package/dist/cjs/errors.d.ts +83 -0
- package/dist/cjs/errors.js +13 -0
- package/dist/cjs/handler/imap-compiler.d.ts +24 -0
- package/dist/cjs/handler/imap-compiler.js +285 -0
- package/dist/cjs/handler/imap-formal-syntax.d.ts +28 -0
- package/dist/cjs/handler/imap-formal-syntax.js +121 -0
- package/dist/cjs/handler/imap-handler.d.ts +9 -0
- package/dist/cjs/handler/imap-handler.js +10 -0
- package/dist/cjs/handler/imap-parser.d.ts +16 -0
- package/dist/cjs/handler/imap-parser.js +90 -0
- package/dist/cjs/handler/imap-stream.d.ts +181 -0
- package/dist/cjs/handler/imap-stream.js +446 -0
- package/dist/cjs/handler/limits.d.ts +25 -0
- package/dist/cjs/handler/limits.js +51 -0
- package/dist/cjs/handler/parser-instance.d.ts +68 -0
- package/dist/cjs/handler/parser-instance.js +223 -0
- package/dist/cjs/handler/token-parser.d.ts +91 -0
- package/dist/cjs/handler/token-parser.js +673 -0
- package/dist/cjs/handler/types.d.ts +91 -0
- package/dist/cjs/handler/types.js +4 -0
- package/dist/cjs/imap-commands.d.ts +16 -0
- package/dist/cjs/imap-commands.js +74 -0
- package/dist/cjs/imap-flow.d.ts +676 -0
- package/dist/cjs/imap-flow.js +3949 -0
- package/dist/cjs/jp-decoder.d.ts +12 -0
- package/dist/cjs/jp-decoder.js +79 -0
- package/dist/cjs/limited-passthrough.d.ts +25 -0
- package/dist/cjs/limited-passthrough.js +54 -0
- package/dist/cjs/logger.d.ts +3 -0
- package/dist/cjs/logger.js +11 -0
- package/dist/cjs/package-info.d.ts +3 -0
- package/dist/cjs/package-info.js +7 -0
- package/dist/cjs/package.json +3 -0
- package/dist/cjs/proxy-connection.d.ts +33 -0
- package/dist/cjs/proxy-connection.js +392 -0
- package/dist/cjs/search-compiler.d.ts +34 -0
- package/dist/cjs/search-compiler.js +476 -0
- package/dist/cjs/special-use.d.ts +22 -0
- package/dist/cjs/special-use.js +911 -0
- package/dist/cjs/tools.d.ts +427 -0
- package/dist/cjs/tools.js +1496 -0
- package/{lib/imap-flow.d.ts → dist/cjs/types.d.ts} +386 -516
- package/dist/cjs/types.js +5 -0
- package/dist/esm/charsets.d.ts +1 -0
- package/{lib → dist/esm}/charsets.js +1 -6
- package/dist/esm/commands/append.d.ts +22 -0
- package/{lib → dist/esm}/commands/append.js +22 -52
- package/dist/esm/commands/authenticate.d.ts +24 -0
- package/{lib → dist/esm}/commands/authenticate.js +62 -87
- package/dist/esm/commands/capability.d.ts +8 -0
- package/{lib → dist/esm}/commands/capability.js +6 -9
- package/dist/esm/commands/close.d.ts +8 -0
- package/{lib → dist/esm}/commands/close.js +6 -10
- package/dist/esm/commands/compress.d.ts +8 -0
- package/{lib → dist/esm}/commands/compress.js +7 -11
- package/dist/esm/commands/copy.d.ts +13 -0
- package/{lib → dist/esm}/commands/copy.js +12 -20
- package/dist/esm/commands/copyuid-parser.d.ts +11 -0
- package/{lib → dist/esm}/commands/copyuid-parser.js +9 -15
- package/dist/esm/commands/create.d.ts +11 -0
- package/{lib → dist/esm}/commands/create.js +13 -27
- package/dist/esm/commands/delete.d.ts +11 -0
- package/{lib → dist/esm}/commands/delete.js +9 -14
- package/dist/esm/commands/enable.d.ts +9 -0
- package/{lib → dist/esm}/commands/enable.js +23 -30
- package/dist/esm/commands/esearch-parser.d.ts +17 -0
- package/dist/esm/commands/esearch-parser.js +88 -0
- package/dist/esm/commands/expunge.d.ts +12 -0
- package/{lib → dist/esm}/commands/expunge.js +17 -22
- package/dist/esm/commands/fetch.d.ts +30 -0
- package/{lib → dist/esm}/commands/fetch.js +32 -64
- package/dist/esm/commands/id.d.ts +10 -0
- package/{lib → dist/esm}/commands/id.js +17 -23
- package/dist/esm/commands/idle.d.ts +9 -0
- package/{lib → dist/esm}/commands/idle.js +47 -81
- package/dist/esm/commands/list.d.ts +16 -0
- package/{lib → dist/esm}/commands/list.js +56 -121
- package/dist/esm/commands/login.d.ts +11 -0
- package/{lib → dist/esm}/commands/login.js +10 -15
- package/dist/esm/commands/logout.d.ts +8 -0
- package/{lib → dist/esm}/commands/logout.js +9 -11
- package/dist/esm/commands/move.d.ts +13 -0
- package/{lib → dist/esm}/commands/move.js +13 -21
- package/dist/esm/commands/namespace.d.ts +25 -0
- package/{lib → dist/esm}/commands/namespace.js +34 -44
- package/dist/esm/commands/noop.d.ts +8 -0
- package/{lib → dist/esm}/commands/noop.js +6 -7
- package/dist/esm/commands/quota.d.ts +10 -0
- package/{lib → dist/esm}/commands/quota.js +18 -36
- package/dist/esm/commands/rename.d.ts +12 -0
- package/{lib → dist/esm}/commands/rename.js +10 -15
- package/dist/esm/commands/search.d.ts +15 -0
- package/{lib → dist/esm}/commands/search.js +36 -135
- package/dist/esm/commands/select.d.ts +25 -0
- package/{lib → dist/esm}/commands/select.js +33 -64
- package/dist/esm/commands/starttls.d.ts +8 -0
- package/{lib → dist/esm}/commands/starttls.js +6 -8
- package/dist/esm/commands/status-fields.d.ts +14 -0
- package/{lib → dist/esm}/commands/status-fields.js +5 -16
- package/dist/esm/commands/status.d.ts +12 -0
- package/{lib → dist/esm}/commands/status.js +18 -29
- package/dist/esm/commands/store.d.ts +19 -0
- package/{lib → dist/esm}/commands/store.js +24 -37
- package/dist/esm/commands/subscribe.d.ts +9 -0
- package/{lib → dist/esm}/commands/subscribe.js +8 -12
- package/dist/esm/commands/unsubscribe.d.ts +9 -0
- package/{lib → dist/esm}/commands/unsubscribe.js +8 -12
- package/dist/esm/connection-deadline.d.ts +49 -0
- package/{lib → dist/esm}/connection-deadline.js +14 -25
- package/dist/esm/errors.d.ts +83 -0
- package/dist/esm/errors.js +9 -0
- package/dist/esm/handler/imap-compiler.d.ts +24 -0
- package/{lib → dist/esm}/handler/imap-compiler.js +22 -80
- package/dist/esm/handler/imap-formal-syntax.d.ts +28 -0
- package/dist/esm/handler/imap-formal-syntax.js +117 -0
- package/dist/esm/handler/imap-handler.d.ts +9 -0
- package/dist/esm/handler/imap-handler.js +9 -0
- package/dist/esm/handler/imap-parser.d.ts +16 -0
- package/{lib → dist/esm}/handler/imap-parser.js +31 -44
- package/dist/esm/handler/imap-stream.d.ts +181 -0
- package/{lib → dist/esm}/handler/imap-stream.js +29 -121
- package/dist/esm/handler/limits.d.ts +25 -0
- package/{lib → dist/esm}/handler/limits.js +13 -22
- package/dist/esm/handler/parser-instance.d.ts +68 -0
- package/{lib → dist/esm}/handler/parser-instance.js +19 -47
- package/dist/esm/handler/token-parser.d.ts +91 -0
- package/{lib → dist/esm}/handler/token-parser.js +71 -155
- package/dist/esm/handler/types.d.ts +91 -0
- package/dist/esm/handler/types.js +3 -0
- package/dist/esm/imap-commands.d.ts +16 -0
- package/dist/esm/imap-commands.js +67 -0
- package/dist/esm/imap-flow.d.ts +676 -0
- package/{lib → dist/esm}/imap-flow.js +785 -1802
- package/dist/esm/jp-decoder.d.ts +12 -0
- package/{lib → dist/esm}/jp-decoder.js +6 -21
- package/dist/esm/limited-passthrough.d.ts +25 -0
- package/{lib → dist/esm}/limited-passthrough.js +7 -20
- package/dist/esm/logger.d.ts +3 -0
- package/dist/esm/logger.js +4 -0
- package/dist/esm/package-info.d.ts +3 -0
- package/dist/esm/package-info.js +4 -0
- package/dist/esm/package.json +3 -0
- package/dist/esm/proxy-connection.d.ts +33 -0
- package/{lib → dist/esm}/proxy-connection.js +56 -127
- package/dist/esm/search-compiler.d.ts +34 -0
- package/{lib → dist/esm}/search-compiler.js +54 -110
- package/dist/esm/special-use.d.ts +22 -0
- package/dist/esm/special-use.js +907 -0
- package/dist/esm/tools.d.ts +427 -0
- package/dist/esm/tools.js +1446 -0
- package/dist/esm/types.d.ts +828 -0
- package/dist/esm/types.js +4 -0
- package/package.json +60 -20
- package/.gitattributes +0 -1
- package/.github/CODE_OF_CONDUCT.md +0 -76
- package/.github/FUNDING.yml +0 -4
- package/.github/ISSUE_TEMPLATE/bug_report.md +0 -40
- package/.github/ISSUE_TEMPLATE/feature_request.md +0 -19
- package/.github/contributing.md +0 -17
- package/.github/workflows/release.yaml +0 -36
- package/.github/workflows/stale.yml +0 -29
- package/.github/workflows/test.yml +0 -51
- package/.ncurc.js +0 -4
- package/.prettierignore +0 -4
- package/.prettierrc.js +0 -8
- package/.release-please-manifest.json +0 -3
- package/CLAUDE.md +0 -104
- package/Gruntfile.js +0 -23
- package/eslint.config.js +0 -45
- package/lib/handler/imap-formal-syntax.js +0 -189
- package/lib/handler/imap-handler.js +0 -17
- package/lib/imap-commands.js +0 -45
- package/lib/logger.js +0 -5
- package/lib/special-use.js +0 -923
- package/lib/tools.js +0 -1612
- package/release-please-config.json +0 -10
- package/test/authentication-test.js +0 -101
- package/test/auto-idle-test.js +0 -470
- package/test/bodystructure-test.js +0 -899
- package/test/charsets-test.js +0 -161
- package/test/commands-branches-test.js +0 -1095
- package/test/commands-integration-test.js +0 -11124
- package/test/commands-test.js +0 -73
- package/test/connection-edge-cases-test.js +0 -1828
- package/test/connection-test.js +0 -162
- package/test/copyuid-parser-test.js +0 -173
- package/test/fetch-generator-test.js +0 -218
- package/test/fixtures/fake-timers.js +0 -115
- package/test/fixtures/serialized-mimetorture.js +0 -2738
- package/test/fixtures/test-client.js +0 -57
- package/test/fixtures/test-tls.js +0 -8
- package/test/handler-branches-test.js +0 -310
- package/test/idle-polling-test.js +0 -518
- package/test/imap-compiler-test.js +0 -809
- package/test/imap-flow-compress-test.js +0 -166
- package/test/imap-flow-coverage-test.js +0 -612
- package/test/imap-flow-fetch-download-test.js +0 -873
- package/test/imap-flow-internals-test.js +0 -725
- package/test/imap-flow-methods-test.js +0 -889
- package/test/imap-flow-proxy-paths-test.js +0 -366
- package/test/imap-flow-secure-test.js +0 -573
- package/test/imap-flow-server-test.js +0 -1474
- package/test/imap-formal-syntax-test.js +0 -293
- package/test/imap-parser-test.js +0 -1474
- package/test/imap-stream-edge-cases-test.js +0 -666
- package/test/imap-stream-test.js +0 -177
- package/test/imapflow-test.js +0 -258
- package/test/integration/README.md +0 -52
- package/test/integration/dovecot-test.conf +0 -27
- package/test/integration/rev2-live-test.js +0 -431
- package/test/integration/run-rev2-tests.sh +0 -75
- package/test/integration-test.js +0 -83
- package/test/jp-decoder-test.js +0 -304
- package/test/limited-passthrough-test.js +0 -299
- package/test/memory-cleanup-test.js +0 -144
- package/test/memory-leak-test.js +0 -667
- package/test/parser-limits-test.js +0 -292
- package/test/proxy-connection-test.js +0 -738
- package/test/reliability-improvements-test.js +0 -548
- package/test/search-compiler-test.js +0 -1300
- package/test/search-test.js +0 -329
- package/test/special-use-test.js +0 -418
- package/test/starttls-injection-test.js +0 -181
- package/test/tag-correlation-test.js +0 -333
- package/test/timer-policy-test.js +0 -227
- package/test/token-parser-test.js +0 -456
- package/test/tools-test.js +0 -2013
- package/test/unhandled-rejection-test.js +0 -593
|
@@ -1,548 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
// Tests for reliability/stability improvements: lock identity, acquireTimeout,
|
|
4
|
-
// maxLockHoldTime diagnostic, capability-clear-on-STARTTLS, and related
|
|
5
|
-
// cleanup paths.
|
|
6
|
-
|
|
7
|
-
const { ImapFlow } = require('../lib/imap-flow');
|
|
8
|
-
const iconv = require('iconv-lite');
|
|
9
|
-
|
|
10
|
-
// Helper: client with stubbed socket/usable state so the fast path of
|
|
11
|
-
// getMailboxLock() can grant locks synchronously (no network I/O).
|
|
12
|
-
const makeClient = (overrides = {}) => {
|
|
13
|
-
let client = new ImapFlow({
|
|
14
|
-
host: 'imap.example.com',
|
|
15
|
-
port: 993,
|
|
16
|
-
auth: { user: 'test', pass: 'test' },
|
|
17
|
-
logger: false,
|
|
18
|
-
...overrides
|
|
19
|
-
});
|
|
20
|
-
client.socket = { destroyed: false, destroy: () => {} };
|
|
21
|
-
client.usable = true;
|
|
22
|
-
client.mailbox = { path: 'INBOX', readOnly: false };
|
|
23
|
-
return client;
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
// Helper: wait for any queued setImmediate callbacks (processLocks reschedules
|
|
27
|
-
// itself via setImmediate after a release).
|
|
28
|
-
const drain = () => new Promise(resolve => setImmediate(resolve));
|
|
29
|
-
|
|
30
|
-
// ============================================================================
|
|
31
|
-
// release() identity check
|
|
32
|
-
// ============================================================================
|
|
33
|
-
|
|
34
|
-
module.exports['Reliability: stale release() does not clear replacement lock'] = async test => {
|
|
35
|
-
let client = makeClient();
|
|
36
|
-
|
|
37
|
-
// Grant L1 via fast path
|
|
38
|
-
let lockA = await client.getMailboxLock('INBOX');
|
|
39
|
-
let staleRelease = lockA.release;
|
|
40
|
-
|
|
41
|
-
// Release L1 normally
|
|
42
|
-
staleRelease();
|
|
43
|
-
await drain();
|
|
44
|
-
test.equal(client.currentLock, false, 'L1 released');
|
|
45
|
-
|
|
46
|
-
// Grant L2
|
|
47
|
-
let lockB = await client.getMailboxLock('INBOX');
|
|
48
|
-
test.ok(client.currentLock, 'L2 active');
|
|
49
|
-
let currentBeforeStale = client.currentLock;
|
|
50
|
-
|
|
51
|
-
// Stale call — must NOT clear L2's hold
|
|
52
|
-
staleRelease();
|
|
53
|
-
test.equal(client.currentLock, currentBeforeStale, 'L2 still active after stale release');
|
|
54
|
-
|
|
55
|
-
lockB.release();
|
|
56
|
-
await drain();
|
|
57
|
-
test.done();
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
module.exports['Reliability: double release() is idempotent'] = async test => {
|
|
61
|
-
let client = makeClient();
|
|
62
|
-
|
|
63
|
-
let lock = await client.getMailboxLock('INBOX');
|
|
64
|
-
|
|
65
|
-
// First release clears
|
|
66
|
-
lock.release();
|
|
67
|
-
await drain();
|
|
68
|
-
test.equal(client.currentLock, false);
|
|
69
|
-
|
|
70
|
-
// Second release is a no-op — must not throw, must not affect state
|
|
71
|
-
test.doesNotThrow(() => lock.release());
|
|
72
|
-
test.equal(client.currentLock, false);
|
|
73
|
-
|
|
74
|
-
test.done();
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
// ============================================================================
|
|
78
|
-
// acquireTimeout
|
|
79
|
-
// ============================================================================
|
|
80
|
-
|
|
81
|
-
module.exports['Reliability: acquireTimeout rejects with LockTimeout code'] = async test => {
|
|
82
|
-
let client = makeClient();
|
|
83
|
-
|
|
84
|
-
// Hold the first lock so the next one queues
|
|
85
|
-
let lockA = await client.getMailboxLock('INBOX');
|
|
86
|
-
|
|
87
|
-
let start = Date.now();
|
|
88
|
-
try {
|
|
89
|
-
await client.getMailboxLock('INBOX', { acquireTimeout: 30 });
|
|
90
|
-
test.ok(false, 'Should have timed out');
|
|
91
|
-
} catch (err) {
|
|
92
|
-
let elapsed = Date.now() - start;
|
|
93
|
-
test.equal(err.code, 'LockTimeout');
|
|
94
|
-
test.ok(err.message.includes('Timed out'));
|
|
95
|
-
test.ok(typeof err.lockId === 'number');
|
|
96
|
-
test.ok(elapsed >= 25, `expected to wait ~30ms, got ${elapsed}`);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// Original lock must still be held
|
|
100
|
-
test.ok(client.currentLock, 'L1 still held after L2 timeout');
|
|
101
|
-
|
|
102
|
-
lockA.release();
|
|
103
|
-
await drain();
|
|
104
|
-
test.done();
|
|
105
|
-
};
|
|
106
|
-
|
|
107
|
-
module.exports['Reliability: acquireTimeout cleared when lock is granted'] = async test => {
|
|
108
|
-
let client = makeClient();
|
|
109
|
-
|
|
110
|
-
// Fast path grants immediately; timer never fires
|
|
111
|
-
let lock = await client.getMailboxLock('INBOX', { acquireTimeout: 50 });
|
|
112
|
-
test.ok(lock);
|
|
113
|
-
test.ok(client.currentLock, 'L1 granted');
|
|
114
|
-
|
|
115
|
-
// Wait past the timeout — no rejection should occur post-grant, no dangling timer
|
|
116
|
-
await new Promise(r => setTimeout(r, 80));
|
|
117
|
-
test.ok(client.currentLock, 'Still held after timer would have fired');
|
|
118
|
-
|
|
119
|
-
lock.release();
|
|
120
|
-
await drain();
|
|
121
|
-
test.done();
|
|
122
|
-
};
|
|
123
|
-
|
|
124
|
-
module.exports['Reliability: acquireTimeout cleared on close()'] = async test => {
|
|
125
|
-
let client = makeClient();
|
|
126
|
-
// Hold L1 so L2 queues
|
|
127
|
-
let lockA = await client.getMailboxLock('INBOX');
|
|
128
|
-
|
|
129
|
-
let rejectedCode = null;
|
|
130
|
-
client.getMailboxLock('INBOX', { acquireTimeout: 10_000 }).catch(err => {
|
|
131
|
-
rejectedCode = err.code;
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
// Immediately close — pending lock should reject with NoConnection (not LockTimeout),
|
|
135
|
-
// and its acquireTimer should be cleared so it never fires afterward.
|
|
136
|
-
client.close();
|
|
137
|
-
|
|
138
|
-
await drain();
|
|
139
|
-
await new Promise(r => setTimeout(r, 30));
|
|
140
|
-
test.equal(rejectedCode, 'NoConnection', 'Pending lock rejects with NoConnection on close');
|
|
141
|
-
|
|
142
|
-
// Keep reference so linter doesn't complain
|
|
143
|
-
test.ok(lockA);
|
|
144
|
-
test.done();
|
|
145
|
-
};
|
|
146
|
-
|
|
147
|
-
// ============================================================================
|
|
148
|
-
// maxLockHoldTime diagnostic
|
|
149
|
-
// ============================================================================
|
|
150
|
-
|
|
151
|
-
module.exports['Reliability: maxLockHoldTime warning fires when lock held past threshold'] = async test => {
|
|
152
|
-
let warnings = [];
|
|
153
|
-
let client = makeClient();
|
|
154
|
-
client.log.warn = obj => warnings.push(obj);
|
|
155
|
-
|
|
156
|
-
let lock = await client.getMailboxLock('INBOX', { maxLockHoldTime: 30 });
|
|
157
|
-
await new Promise(r => setTimeout(r, 60));
|
|
158
|
-
|
|
159
|
-
let hit = warnings.find(w => w && w.msg === 'Mailbox lock held for a long time');
|
|
160
|
-
test.ok(hit, 'Warning log must fire');
|
|
161
|
-
test.ok(typeof hit.heldFor === 'number' && hit.heldFor >= 25);
|
|
162
|
-
|
|
163
|
-
lock.release();
|
|
164
|
-
await drain();
|
|
165
|
-
test.done();
|
|
166
|
-
};
|
|
167
|
-
|
|
168
|
-
module.exports['Reliability: maxLockHoldTime=0 disables the warning'] = async test => {
|
|
169
|
-
let warnings = [];
|
|
170
|
-
let client = makeClient();
|
|
171
|
-
client.log.warn = obj => warnings.push(obj);
|
|
172
|
-
|
|
173
|
-
let lock = await client.getMailboxLock('INBOX', { maxLockHoldTime: 0 });
|
|
174
|
-
await new Promise(r => setTimeout(r, 30));
|
|
175
|
-
|
|
176
|
-
test.ok(!warnings.some(w => w && w.msg === 'Mailbox lock held for a long time'), 'No warn when disabled');
|
|
177
|
-
|
|
178
|
-
lock.release();
|
|
179
|
-
await drain();
|
|
180
|
-
test.done();
|
|
181
|
-
};
|
|
182
|
-
|
|
183
|
-
module.exports['Reliability: maxLockHoldTime=false disables the warning'] = async test => {
|
|
184
|
-
let warnings = [];
|
|
185
|
-
let client = makeClient();
|
|
186
|
-
client.log.warn = obj => warnings.push(obj);
|
|
187
|
-
|
|
188
|
-
let lock = await client.getMailboxLock('INBOX', { maxLockHoldTime: false });
|
|
189
|
-
await new Promise(r => setTimeout(r, 30));
|
|
190
|
-
|
|
191
|
-
test.ok(!warnings.some(w => w && w.msg === 'Mailbox lock held for a long time'));
|
|
192
|
-
|
|
193
|
-
lock.release();
|
|
194
|
-
await drain();
|
|
195
|
-
test.done();
|
|
196
|
-
};
|
|
197
|
-
|
|
198
|
-
module.exports['Reliability: per-call maxLockHoldTime overrides constructor option'] = async test => {
|
|
199
|
-
let warnings = [];
|
|
200
|
-
// Constructor sets a long threshold; per-call sets a short one
|
|
201
|
-
let client = makeClient({ maxLockHoldTime: 10_000 });
|
|
202
|
-
client.log.warn = obj => warnings.push(obj);
|
|
203
|
-
|
|
204
|
-
let lock = await client.getMailboxLock('INBOX', { maxLockHoldTime: 20 });
|
|
205
|
-
await new Promise(r => setTimeout(r, 50));
|
|
206
|
-
|
|
207
|
-
test.ok(
|
|
208
|
-
warnings.some(w => w && w.msg === 'Mailbox lock held for a long time'),
|
|
209
|
-
'Per-call override must take effect'
|
|
210
|
-
);
|
|
211
|
-
|
|
212
|
-
lock.release();
|
|
213
|
-
await drain();
|
|
214
|
-
test.done();
|
|
215
|
-
};
|
|
216
|
-
|
|
217
|
-
module.exports['Reliability: held-lock timer cleared on release (does not fire after)'] = async test => {
|
|
218
|
-
let warnings = [];
|
|
219
|
-
let client = makeClient();
|
|
220
|
-
client.log.warn = obj => warnings.push(obj);
|
|
221
|
-
|
|
222
|
-
let lock = await client.getMailboxLock('INBOX', { maxLockHoldTime: 50 });
|
|
223
|
-
lock.release();
|
|
224
|
-
await drain();
|
|
225
|
-
|
|
226
|
-
// Wait longer than the threshold — no warning should appear because release cleared it
|
|
227
|
-
await new Promise(r => setTimeout(r, 80));
|
|
228
|
-
test.ok(!warnings.some(w => w && w.msg === 'Mailbox lock held for a long time'));
|
|
229
|
-
|
|
230
|
-
test.done();
|
|
231
|
-
};
|
|
232
|
-
|
|
233
|
-
module.exports['Reliability: held-lock timer cleared on close()'] = async test => {
|
|
234
|
-
let warnings = [];
|
|
235
|
-
let client = makeClient();
|
|
236
|
-
client.log.warn = obj => warnings.push(obj);
|
|
237
|
-
|
|
238
|
-
let lock = await client.getMailboxLock('INBOX', { maxLockHoldTime: 30 });
|
|
239
|
-
test.ok(lock);
|
|
240
|
-
client.close();
|
|
241
|
-
|
|
242
|
-
await new Promise(r => setTimeout(r, 60));
|
|
243
|
-
test.ok(!warnings.some(w => w && w.msg === 'Mailbox lock held for a long time'));
|
|
244
|
-
|
|
245
|
-
test.done();
|
|
246
|
-
};
|
|
247
|
-
|
|
248
|
-
// ============================================================================
|
|
249
|
-
// STARTTLS capability reset
|
|
250
|
-
// ============================================================================
|
|
251
|
-
|
|
252
|
-
module.exports['Reliability: STARTTLS code path clears capabilities before re-fetch'] = async test => {
|
|
253
|
-
// Verify the clear() calls happen by driving the same branch directly.
|
|
254
|
-
let client = new ImapFlow({
|
|
255
|
-
host: 'imap.example.com',
|
|
256
|
-
port: 993,
|
|
257
|
-
auth: { user: 'test', pass: 'test' },
|
|
258
|
-
logger: false
|
|
259
|
-
});
|
|
260
|
-
|
|
261
|
-
client.capabilities.set('LOGINDISABLED', true);
|
|
262
|
-
client.capabilities.set('STARTTLS', true);
|
|
263
|
-
client.authCapabilities.set('AUTH=PLAIN', false);
|
|
264
|
-
|
|
265
|
-
// Stub the run() that would re-fetch CAPABILITY post-TLS
|
|
266
|
-
let ran = [];
|
|
267
|
-
client.run = async command => {
|
|
268
|
-
ran.push(command);
|
|
269
|
-
return true;
|
|
270
|
-
};
|
|
271
|
-
|
|
272
|
-
client.expectCapabilityUpdate = true;
|
|
273
|
-
// Execute the same statements that the STARTTLS-success branch runs
|
|
274
|
-
// (guards the behavior contract: caches are cleared before re-fetch)
|
|
275
|
-
if (client.expectCapabilityUpdate) {
|
|
276
|
-
client.capabilities.clear();
|
|
277
|
-
client.authCapabilities.clear();
|
|
278
|
-
await client.run('CAPABILITY');
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
test.equal(client.capabilities.size, 0, 'capabilities map cleared');
|
|
282
|
-
test.equal(client.authCapabilities.size, 0, 'authCapabilities map cleared');
|
|
283
|
-
test.deepEqual(ran, ['CAPABILITY']);
|
|
284
|
-
|
|
285
|
-
test.done();
|
|
286
|
-
};
|
|
287
|
-
|
|
288
|
-
// ============================================================================
|
|
289
|
-
// Handler try/catch (smoke tests via direct invocation)
|
|
290
|
-
// ============================================================================
|
|
291
|
-
|
|
292
|
-
module.exports['Reliability: sectionHandler throw is caught (no handler -> no effect)'] = async test => {
|
|
293
|
-
let client = new ImapFlow({
|
|
294
|
-
host: 'imap.example.com',
|
|
295
|
-
port: 993,
|
|
296
|
-
auth: { user: 'test', pass: 'test' },
|
|
297
|
-
logger: false
|
|
298
|
-
});
|
|
299
|
-
|
|
300
|
-
// Install a section handler that throws
|
|
301
|
-
client.getSectionHandler = () => async () => {
|
|
302
|
-
throw new Error('handler boom');
|
|
303
|
-
};
|
|
304
|
-
|
|
305
|
-
// The production path awaits the handler inside a try/catch in reader().
|
|
306
|
-
// Replicate that contract: the wrapping semantics here should not reject.
|
|
307
|
-
let handler = client.getSectionHandler('TEST');
|
|
308
|
-
let caught = false;
|
|
309
|
-
try {
|
|
310
|
-
// Mimic reader() — it does: try { await handler(...) } catch (err) { log.warn(...) }
|
|
311
|
-
try {
|
|
312
|
-
await handler([]);
|
|
313
|
-
} catch (err) {
|
|
314
|
-
caught = true;
|
|
315
|
-
test.ok(err.message.includes('boom'));
|
|
316
|
-
}
|
|
317
|
-
} catch (unexpected) {
|
|
318
|
-
test.ok(false, 'Outer catch should not observe: ' + unexpected.message);
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
test.ok(caught, 'Thrown error is captured by inner try/catch');
|
|
322
|
-
test.done();
|
|
323
|
-
};
|
|
324
|
-
|
|
325
|
-
module.exports['Reliability: onPlusTag throw is caught (no handler -> no effect)'] = async test => {
|
|
326
|
-
let client = new ImapFlow({
|
|
327
|
-
host: 'imap.example.com',
|
|
328
|
-
port: 993,
|
|
329
|
-
auth: { user: 'test', pass: 'test' },
|
|
330
|
-
logger: false
|
|
331
|
-
});
|
|
332
|
-
test.ok(client);
|
|
333
|
-
|
|
334
|
-
// Simulate a currentRequest with a throwing onPlusTag
|
|
335
|
-
let onPlusTag = async () => {
|
|
336
|
-
throw new Error('plus tag boom');
|
|
337
|
-
};
|
|
338
|
-
|
|
339
|
-
// The reader() path wraps this call in try/catch; replicate the contract.
|
|
340
|
-
let caught = false;
|
|
341
|
-
try {
|
|
342
|
-
await onPlusTag({});
|
|
343
|
-
} catch (err) {
|
|
344
|
-
caught = true;
|
|
345
|
-
test.ok(err.message.includes('boom'));
|
|
346
|
-
}
|
|
347
|
-
test.ok(caught);
|
|
348
|
-
test.done();
|
|
349
|
-
};
|
|
350
|
-
|
|
351
|
-
// ============================================================================
|
|
352
|
-
// Charset decoder defensive listener
|
|
353
|
-
// ============================================================================
|
|
354
|
-
|
|
355
|
-
module.exports['Reliability: decoder emit(error) does not crash when user has not attached listener'] = test => {
|
|
356
|
-
// Pattern: after a getDecoder() + defensive .on('error') + decoder.emit('error', err),
|
|
357
|
-
// the process does not throw (because at least one listener was registered).
|
|
358
|
-
let decoder = iconv.decodeStream('latin1');
|
|
359
|
-
|
|
360
|
-
// Attach the same kind of safety listener the production code installs
|
|
361
|
-
let warned = 0;
|
|
362
|
-
decoder.on('error', () => {
|
|
363
|
-
warned++;
|
|
364
|
-
});
|
|
365
|
-
|
|
366
|
-
// Simulate forwarding a source error into the decoder
|
|
367
|
-
test.doesNotThrow(() => {
|
|
368
|
-
decoder.emit('error', new Error('source stream failed'));
|
|
369
|
-
});
|
|
370
|
-
test.equal(warned, 1);
|
|
371
|
-
|
|
372
|
-
test.done();
|
|
373
|
-
};
|
|
374
|
-
|
|
375
|
-
// ============================================================================
|
|
376
|
-
// Throttle back-off timer is tracked and abortable on close()
|
|
377
|
-
// ============================================================================
|
|
378
|
-
|
|
379
|
-
// Feed a single throttling BAD response into reader() once, then null.
|
|
380
|
-
const stubThrottleResponse = (client, backoffMs) => {
|
|
381
|
-
let request = { tag: 'A001', command: 'FETCH', resolve: () => {}, reject: () => {} };
|
|
382
|
-
client.requestTagMap = new Map([['A001', request]]);
|
|
383
|
-
// The tagged response may only complete a command that was actually written to the socket, so
|
|
384
|
-
// the stub has to present A001 as the active, already sent request - otherwise it is protocol
|
|
385
|
-
// desynchronization.
|
|
386
|
-
client.currentRequest = { tag: 'A001', command: 'FETCH', sent: true };
|
|
387
|
-
|
|
388
|
-
let done = false;
|
|
389
|
-
client.streamer.read = () => {
|
|
390
|
-
if (done) {
|
|
391
|
-
return null;
|
|
392
|
-
}
|
|
393
|
-
done = true;
|
|
394
|
-
return {
|
|
395
|
-
payload: Buffer.from(`A001 BAD Request is throttled. Suggested Backoff Time: ${backoffMs} milliseconds`),
|
|
396
|
-
literals: [],
|
|
397
|
-
next: () => {}
|
|
398
|
-
};
|
|
399
|
-
};
|
|
400
|
-
|
|
401
|
-
return request;
|
|
402
|
-
};
|
|
403
|
-
|
|
404
|
-
module.exports['Reliability: throttle back-off aborts promptly on close()'] = async test => {
|
|
405
|
-
let client = new ImapFlow({
|
|
406
|
-
host: 'imap.example.com',
|
|
407
|
-
port: 993,
|
|
408
|
-
auth: { user: 'test', pass: 'test' },
|
|
409
|
-
logger: false
|
|
410
|
-
});
|
|
411
|
-
client.socket = { destroyed: false, destroy: () => {} };
|
|
412
|
-
client.writeSocket = client.socket;
|
|
413
|
-
|
|
414
|
-
let rejected = null;
|
|
415
|
-
let request = stubThrottleResponse(client, 300000); // 5 min back-off
|
|
416
|
-
request.reject = err => {
|
|
417
|
-
rejected = err;
|
|
418
|
-
};
|
|
419
|
-
|
|
420
|
-
let start = Date.now();
|
|
421
|
-
let readerDone = client.reader().catch(() => {});
|
|
422
|
-
|
|
423
|
-
// Let reader() reach the (tracked) back-off wait.
|
|
424
|
-
await new Promise(r => setTimeout(r, 50));
|
|
425
|
-
test.equal(client._throttleWaits.size, 1, 'back-off wait is tracked while waiting');
|
|
426
|
-
|
|
427
|
-
client.close();
|
|
428
|
-
await new Promise(r => setImmediate(r));
|
|
429
|
-
|
|
430
|
-
test.ok(rejected, 'request rejected promptly after close()');
|
|
431
|
-
test.equal(rejected.code, 'NoConnection', 'rejected with connection error, not ETHROTTLE');
|
|
432
|
-
test.equal(client._throttleWaits.size, 0, 'throttle wait cleared on close()');
|
|
433
|
-
test.ok(Date.now() - start < 5000, 'settled well under the 5-minute cap');
|
|
434
|
-
|
|
435
|
-
await readerDone;
|
|
436
|
-
test.done();
|
|
437
|
-
};
|
|
438
|
-
|
|
439
|
-
module.exports['Reliability: throttle back-off still rejects ETHROTTLE on normal expiry'] = async test => {
|
|
440
|
-
let client = new ImapFlow({
|
|
441
|
-
host: 'imap.example.com',
|
|
442
|
-
port: 993,
|
|
443
|
-
auth: { user: 'test', pass: 'test' },
|
|
444
|
-
logger: false
|
|
445
|
-
});
|
|
446
|
-
|
|
447
|
-
let rejected = null;
|
|
448
|
-
let request = stubThrottleResponse(client, 50); // 50ms back-off
|
|
449
|
-
request.reject = err => {
|
|
450
|
-
rejected = err;
|
|
451
|
-
};
|
|
452
|
-
|
|
453
|
-
let readerDone = client.reader().catch(() => {});
|
|
454
|
-
|
|
455
|
-
await new Promise(r => setTimeout(r, 250));
|
|
456
|
-
|
|
457
|
-
test.ok(rejected, 'request rejected after the back-off elapses');
|
|
458
|
-
test.equal(rejected.code, 'ETHROTTLE', 'normal expiry still rejects ETHROTTLE');
|
|
459
|
-
test.equal(rejected.throttleReset, 50, 'throttleReset preserved');
|
|
460
|
-
test.equal(client._throttleWaits.size, 0, 'throttle wait cleared after normal expiry');
|
|
461
|
-
|
|
462
|
-
await readerDone;
|
|
463
|
-
client.close();
|
|
464
|
-
test.done();
|
|
465
|
-
};
|
|
466
|
-
|
|
467
|
-
// ---------------------------------------------------------------------------
|
|
468
|
-
// reader(): the parser backpressure callback is a resource that must always be released
|
|
469
|
-
// ---------------------------------------------------------------------------
|
|
470
|
-
|
|
471
|
-
module.exports['Reliability: an unexpected response-handling failure releases the parser and fails closed'] = async test => {
|
|
472
|
-
// Several steps of response handling (log compilation, response shape assumptions, a handler
|
|
473
|
-
// bug) sit outside the parse try block. A throw there used to propagate out of the reader loop
|
|
474
|
-
// and leave ImapStream waiting on its backpressure callback forever - a silent permanent hang.
|
|
475
|
-
let client = new ImapFlow({
|
|
476
|
-
host: 'imap.example.com',
|
|
477
|
-
port: 993,
|
|
478
|
-
auth: { user: 'test', pass: 'test' },
|
|
479
|
-
logger: false
|
|
480
|
-
});
|
|
481
|
-
client.socket = { destroyed: false, destroy: () => {} };
|
|
482
|
-
client.writeSocket = client.socket;
|
|
483
|
-
|
|
484
|
-
let rejected = null;
|
|
485
|
-
let request = { tag: 'A001', command: 'NOOP', resolve: () => {}, reject: err => (rejected = err) };
|
|
486
|
-
client.requestTagMap = new Map([['A001', request]]);
|
|
487
|
-
client.currentRequest = { tag: 'A001', command: 'NOOP', sent: true };
|
|
488
|
-
|
|
489
|
-
let errors = [];
|
|
490
|
-
client.on('error', err => errors.push(err));
|
|
491
|
-
|
|
492
|
-
let released = 0;
|
|
493
|
-
let served = false;
|
|
494
|
-
client.streamer.read = () => {
|
|
495
|
-
if (served) {
|
|
496
|
-
return null;
|
|
497
|
-
}
|
|
498
|
-
served = true;
|
|
499
|
-
return {
|
|
500
|
-
payload: Buffer.from('A001 OK NOOP done'),
|
|
501
|
-
literals: [],
|
|
502
|
-
next: () => released++
|
|
503
|
-
};
|
|
504
|
-
};
|
|
505
|
-
|
|
506
|
-
// Stand in for any unexpected failure during response handling
|
|
507
|
-
client.handleResponse = async () => {
|
|
508
|
-
throw new Error('handler blew up');
|
|
509
|
-
};
|
|
510
|
-
|
|
511
|
-
await client.reader();
|
|
512
|
-
|
|
513
|
-
test.equal(released, 1, 'the parser backpressure callback is released exactly once');
|
|
514
|
-
test.ok(rejected, 'the in-flight request is rejected instead of hanging');
|
|
515
|
-
test.equal(rejected.code, 'ResponseProcessingFailed');
|
|
516
|
-
test.ok(client.streamer.destroyed, 'the parser stream is destroyed, so nothing further is parsed');
|
|
517
|
-
|
|
518
|
-
await new Promise(resolve => setImmediate(resolve));
|
|
519
|
-
test.ok(
|
|
520
|
-
errors.some(err => err.code === 'ResponseProcessingFailed'),
|
|
521
|
-
'the failure is reported to the caller'
|
|
522
|
-
);
|
|
523
|
-
|
|
524
|
-
client.close();
|
|
525
|
-
test.done();
|
|
526
|
-
};
|
|
527
|
-
|
|
528
|
-
module.exports['Reliability: releaseStreamData is idempotent'] = async test => {
|
|
529
|
-
let client = new ImapFlow({
|
|
530
|
-
host: 'imap.example.com',
|
|
531
|
-
port: 993,
|
|
532
|
-
auth: { user: 'test', pass: 'test' },
|
|
533
|
-
logger: false
|
|
534
|
-
});
|
|
535
|
-
|
|
536
|
-
let released = 0;
|
|
537
|
-
let data = { next: () => released++ };
|
|
538
|
-
|
|
539
|
-
client.releaseStreamData(data);
|
|
540
|
-
client.releaseStreamData(data);
|
|
541
|
-
client.releaseStreamData(data);
|
|
542
|
-
|
|
543
|
-
test.equal(released, 1, 'a readable item is only ever released once');
|
|
544
|
-
test.doesNotThrow(() => client.releaseStreamData(null), 'releasing nothing is a no-op');
|
|
545
|
-
|
|
546
|
-
client.close();
|
|
547
|
-
test.done();
|
|
548
|
-
};
|