imapflow 1.3.5 → 1.3.7
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/.github/codeql/codeql-config.yml +12 -0
- package/.github/workflows/codeql.yml +102 -0
- package/.github/workflows/stale.yml +5 -0
- package/.github/workflows/test.yml +3 -0
- package/.release-please-manifest.json +1 -1
- package/CHANGELOG.md +14 -0
- package/lib/commands/authenticate.js +5 -5
- package/lib/commands/fetch.js +1 -0
- package/lib/commands/logout.js +1 -0
- package/lib/commands/select.js +2 -1
- package/lib/commands/starttls.js +3 -0
- package/lib/commands/store.js +1 -0
- package/lib/handler/imap-stream.js +23 -6
- package/lib/handler/token-parser.js +0 -5
- package/lib/imap-flow.d.ts +5 -0
- package/lib/imap-flow.js +205 -29
- package/lib/proxy-connection.js +26 -1
- package/lib/tools.js +12 -0
- package/package.json +1 -1
- package/test/commands-branches-test.js +1068 -0
- package/test/connection-edge-cases-test.js +138 -1
- package/test/fixtures/test-tls.js +8 -0
- package/test/handler-branches-test.js +334 -0
- package/test/imap-compiler-test.js +47 -0
- package/test/imap-flow-compress-test.js +154 -0
- package/test/imap-flow-coverage-test.js +609 -0
- package/test/imap-flow-fetch-download-test.js +801 -0
- package/test/imap-flow-internals-test.js +450 -0
- package/test/imap-flow-methods-test.js +738 -0
- package/test/imap-flow-proxy-paths-test.js +215 -0
- package/test/imap-flow-secure-test.js +327 -0
- package/test/imap-flow-server-test.js +1046 -0
- package/test/imap-formal-syntax-test.js +18 -0
- package/test/imap-parser-test.js +52 -0
- package/test/imap-stream-edge-cases-test.js +122 -0
- package/test/jp-decoder-test.js +48 -0
- package/test/limited-passthrough-test.js +17 -0
- package/test/proxy-connection-test.js +86 -0
- package/test/reliability-improvements-test.js +88 -0
- package/test/search-compiler-test.js +31 -0
- package/test/search-test.js +61 -0
- package/test/starttls-injection-test.js +181 -0
- package/test/token-parser-test.js +64 -0
- package/test/tools-test.js +360 -0
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const { ImapFlow } = require('../lib/imap-flow');
|
|
4
4
|
const { EventEmitter } = require('events');
|
|
5
|
+
const net = require('net');
|
|
5
6
|
|
|
6
7
|
// Edge Cases Tests
|
|
7
8
|
|
|
@@ -1539,7 +1540,14 @@ async function setupCompressedClient() {
|
|
|
1539
1540
|
mockSocket.destroy = () => {};
|
|
1540
1541
|
mockSocket.destroyed = false;
|
|
1541
1542
|
client.socket = mockSocket;
|
|
1542
|
-
|
|
1543
|
+
// Mock streamer that tracks destruction like a real stream, so close() drives it into
|
|
1544
|
+
// the destroyed state the post-close guards actually check.
|
|
1545
|
+
client.streamer = Object.assign(new EventEmitter(), {
|
|
1546
|
+
destroyed: false,
|
|
1547
|
+
destroy() {
|
|
1548
|
+
this.destroyed = true;
|
|
1549
|
+
}
|
|
1550
|
+
});
|
|
1543
1551
|
|
|
1544
1552
|
client.run = async command => {
|
|
1545
1553
|
if (command === 'COMPRESS') {
|
|
@@ -1630,6 +1638,33 @@ module.exports['Connection Edge: compress _deflate error after close does not cr
|
|
|
1630
1638
|
test.done();
|
|
1631
1639
|
};
|
|
1632
1640
|
|
|
1641
|
+
module.exports['Connection Edge: compress _inflate error after close does not crash'] = async test => {
|
|
1642
|
+
let { client } = await setupCompressedClient();
|
|
1643
|
+
|
|
1644
|
+
let inflate = client._inflate;
|
|
1645
|
+
|
|
1646
|
+
// close() removes the streamer 'error' listener and destroys the streamer.
|
|
1647
|
+
// A late inflate error must not be forwarded into the destroyed streamer
|
|
1648
|
+
// (which would throw an unhandled 'error' and crash the process).
|
|
1649
|
+
client.close();
|
|
1650
|
+
|
|
1651
|
+
// Confirm we actually reached the post-close state the guard targets.
|
|
1652
|
+
test.ok(client.streamer.destroyed, 'streamer is destroyed after close()');
|
|
1653
|
+
|
|
1654
|
+
// Re-attach an 'error' listener so listenerCount('error') > 0: this isolates the
|
|
1655
|
+
// `!streamer.destroyed` term of the guard. If forwarding still happened, this listener
|
|
1656
|
+
// would throw and escape doesNotThrow — proving the destroyed-check is what suppresses it.
|
|
1657
|
+
client.streamer.on('error', () => {
|
|
1658
|
+
throw new Error('inflate error must not be forwarded into a destroyed streamer');
|
|
1659
|
+
});
|
|
1660
|
+
|
|
1661
|
+
test.doesNotThrow(() => {
|
|
1662
|
+
inflate.emit('error', new Error('late inflate error'));
|
|
1663
|
+
});
|
|
1664
|
+
|
|
1665
|
+
test.done();
|
|
1666
|
+
};
|
|
1667
|
+
|
|
1633
1668
|
// ============================================
|
|
1634
1669
|
// Mailbox Lock Tests
|
|
1635
1670
|
// ============================================
|
|
@@ -1723,3 +1758,105 @@ module.exports['Connection Edge: multiple locks rejected when no connection'] =
|
|
|
1723
1758
|
}
|
|
1724
1759
|
test.done();
|
|
1725
1760
|
};
|
|
1761
|
+
|
|
1762
|
+
// ============================================
|
|
1763
|
+
// reader() survives a write() throw without stalling the stream
|
|
1764
|
+
// ============================================
|
|
1765
|
+
|
|
1766
|
+
module.exports['Connection Edge: reader survives write throw on + continuation'] = async test => {
|
|
1767
|
+
let client = new ImapFlow({
|
|
1768
|
+
host: 'imap.example.com',
|
|
1769
|
+
port: 993,
|
|
1770
|
+
auth: { user: 'test', pass: 'test' },
|
|
1771
|
+
logger: false
|
|
1772
|
+
});
|
|
1773
|
+
|
|
1774
|
+
client.currentRequest = false;
|
|
1775
|
+
client.commandParts = [Buffer.from('literal-bytes')];
|
|
1776
|
+
|
|
1777
|
+
let writeAttempted = false;
|
|
1778
|
+
client.write = () => {
|
|
1779
|
+
writeAttempted = true;
|
|
1780
|
+
throw new Error('write boom');
|
|
1781
|
+
};
|
|
1782
|
+
|
|
1783
|
+
let nextCalled = false;
|
|
1784
|
+
let done = false;
|
|
1785
|
+
client.streamer.read = () => {
|
|
1786
|
+
if (done) {
|
|
1787
|
+
return null;
|
|
1788
|
+
}
|
|
1789
|
+
done = true;
|
|
1790
|
+
return {
|
|
1791
|
+
payload: Buffer.from('+ Ready'),
|
|
1792
|
+
literals: [],
|
|
1793
|
+
next: () => {
|
|
1794
|
+
nextCalled = true;
|
|
1795
|
+
}
|
|
1796
|
+
};
|
|
1797
|
+
};
|
|
1798
|
+
|
|
1799
|
+
let rejected = false;
|
|
1800
|
+
await client.reader().catch(() => {
|
|
1801
|
+
rejected = true;
|
|
1802
|
+
});
|
|
1803
|
+
|
|
1804
|
+
test.ok(writeAttempted, 'the literal-continuation write path was exercised');
|
|
1805
|
+
test.equal(rejected, false, 'reader did not reject on write throw');
|
|
1806
|
+
test.ok(nextCalled, 'data.next() was still called so the stream is not stalled');
|
|
1807
|
+
test.done();
|
|
1808
|
+
};
|
|
1809
|
+
|
|
1810
|
+
// ============================================
|
|
1811
|
+
// BYE greeting surfaces its reason on the connect rejection
|
|
1812
|
+
// ============================================
|
|
1813
|
+
|
|
1814
|
+
module.exports['Connection Edge: BYE greeting surfaces reason on connect rejection'] = async test => {
|
|
1815
|
+
// Deterministic ordering: the server ends the socket only AFTER the client has parsed the
|
|
1816
|
+
// BYE and set byeReason, instead of racing a fixed 50ms timer that could flake on slow CI.
|
|
1817
|
+
let signalByeParsed;
|
|
1818
|
+
let byeParsed = new Promise(resolve => {
|
|
1819
|
+
signalByeParsed = resolve;
|
|
1820
|
+
});
|
|
1821
|
+
|
|
1822
|
+
let server = net.createServer(socket => {
|
|
1823
|
+
socket.on('error', () => {});
|
|
1824
|
+
socket.write('* BYE Server too busy\r\n');
|
|
1825
|
+
byeParsed.then(() => socket.end());
|
|
1826
|
+
});
|
|
1827
|
+
|
|
1828
|
+
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
|
|
1829
|
+
let port = server.address().port;
|
|
1830
|
+
|
|
1831
|
+
let client = new ImapFlow({
|
|
1832
|
+
host: '127.0.0.1',
|
|
1833
|
+
port,
|
|
1834
|
+
secure: false,
|
|
1835
|
+
disableAutoIdle: true,
|
|
1836
|
+
logger: false,
|
|
1837
|
+
auth: { user: 'test', pass: 'test' }
|
|
1838
|
+
});
|
|
1839
|
+
client.on('error', () => {});
|
|
1840
|
+
|
|
1841
|
+
// Signal once the client's BYE handler has run, so byeReason is guaranteed set before close().
|
|
1842
|
+
let origServerBye = client.serverBye.bind(client);
|
|
1843
|
+
client.serverBye = async parsed => {
|
|
1844
|
+
await origServerBye(parsed);
|
|
1845
|
+
signalByeParsed();
|
|
1846
|
+
};
|
|
1847
|
+
|
|
1848
|
+
let connectErr = null;
|
|
1849
|
+
try {
|
|
1850
|
+
await client.connect();
|
|
1851
|
+
test.ok(false, 'connect() should reject after a BYE greeting');
|
|
1852
|
+
} catch (err) {
|
|
1853
|
+
connectErr = err;
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1856
|
+
test.ok(connectErr, 'connect() rejected');
|
|
1857
|
+
test.equal(connectErr.reason, 'Server too busy', 'BYE reason surfaced on the rejection');
|
|
1858
|
+
|
|
1859
|
+
client.close();
|
|
1860
|
+
server.close();
|
|
1861
|
+
test.done();
|
|
1862
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Self-signed certificate + key for in-process TLS/STARTTLS mock servers in tests.
|
|
4
|
+
// Generated once with: openssl req -x509 -newkey rsa:2048 -nodes -subj "/CN=localhost"
|
|
5
|
+
module.exports = {
|
|
6
|
+
cert: "-----BEGIN CERTIFICATE-----\nMIIDCTCCAfGgAwIBAgIUQSc0WP7RlcOqGCdsMaxzCA97mfkwDQYJKoZIhvcNAQEL\nBQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI2MDYwODA0Mzg1NFoXDTM2MDYw\nNTA0Mzg1NFowFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF\nAAOCAQ8AMIIBCgKCAQEA4TYnyK2nEn6wXEgVeEI3I9CDlONvWDO1Y51mslOaUwZR\nfuZp3YMUZDINNdZuFrREkD1WJfClRZtDKEurw9FfjEfA0gsM9RPvkQYXZ2zzhXgW\nx86li0Ra/Zx2RmajsWc+THTEpdOK0nqOqMMKFxiYJ7E+KiJY0X6HqKjdaB9CpZv7\nA1ROJoLjTnsdDG/NT9MCQxFbw05N2Bbbq+McKyXIEzxiZguYLIXsMKCi9MQOozvY\nT8X0AFWpFJCa8mRCW7LQngeSRQQckDONzHyELXnbvgIskr/WcmBEXMraTts3Z3QW\nouee6HFnYzi/pgCtVKX1Wu3oZm/nMblsWtopqg/YDwIDAQABo1MwUTAdBgNVHQ4E\nFgQUF5oycZ3ZaPs1fTaxZb666FQt+KcwHwYDVR0jBBgwFoAUF5oycZ3ZaPs1fTax\nZb666FQt+KcwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAWFk0\nMGw2pdkeU/s5j0T9GL8GjbOVC0G7H8tlx92XhQICmt/9GNHeb5nm84DgqRbkiTwS\nOXPoMsSvVEubnX0j/sqcbi5yQ6/YBG6Vx5E9CnfUbo4m9x1GofDkLdl+e6l636Mj\nWB5kJpNCH9sm2xPdLpl47pv/lwvyYjdy1DaATHn5nMGoM73IeEt0d4xoNN8Bso7B\nTTkjKRJ1QWgI2EjgU93x8tG4AqTj0CeH8Z6DDmfLhvJ6E0hSws4OQSnxWudh2aX8\niN+FkHgRobPS1GVCtibr+FxH1DtBv/grXhAbs4xP3Nf3m3Pn6AE0/lMtto3e67As\np4lf866gU9nbRkODqw==\n-----END CERTIFICATE-----\n",
|
|
7
|
+
key: "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDhNifIracSfrBc\nSBV4Qjcj0IOU429YM7VjnWayU5pTBlF+5mndgxRkMg011m4WtESQPVYl8KVFm0Mo\nS6vD0V+MR8DSCwz1E++RBhdnbPOFeBbHzqWLRFr9nHZGZqOxZz5MdMSl04rSeo6o\nwwoXGJgnsT4qIljRfoeoqN1oH0Klm/sDVE4mguNOex0Mb81P0wJDEVvDTk3YFtur\n4xwrJcgTPGJmC5gshewwoKL0xA6jO9hPxfQAVakUkJryZEJbstCeB5JFBByQM43M\nfIQtedu+AiySv9ZyYERcytpO2zdndBai557ocWdjOL+mAK1UpfVa7ehmb+cxuWxa\n2imqD9gPAgMBAAECggEACgBGqgG+0/JuUIV0S/UyJP6t098kYwKrgR33S99XO21M\nvIRCRUT8kyp/La81D6sNCb6/jvXDJnaA9/l29OjdA7TCr6aFT8jahafYmDjdY5rz\n9Vw25nAFqRIuqm0FGJa2Id0O8oeUuEjFrGGPltD6c3Yf2u0li+rA3tDeQYZ0hjge\nsVXzgI58SbO1Z1XCAXcdbcJOqjpEJ3w99GB5J/B1dDui3KR5Q6+L34HfoW8XmY/X\nCLe+Jcuk06Xa31iZ+0NEMIPOhVoCW//PfgEWD/dfG3jXhk84/Pb8DXaGahOvRPTY\nH1AVrqRBqW6PezR4qU4PdUfU3sp5NEBZl3oaW7/weQKBgQD8CKI7ek00KUhc9j5m\nHGuSR+lUzLziXRBt7xw0JXLa1rfok2RGDeTdIEGSjBr8xsNOe5aga843geoLptNe\n7LRrtJajgHl39NXnblroNidufmXjt9CRgjUsQuIREpjq9NyUGIh8o26/uZLB/Knq\nJ7Uum9YUHI/eEztmI/CZDbnUbQKBgQDkwXaf5UxkY9tsdbw8jHhV5Ypy/hwLXAcI\n90sCI9/mSQAOA9qNa+944MbRfReTseM+OFXHJRHXfuMXPPOrtObtVN94ECwFVePO\n2MI5CHc/SYZ/4Icf+jpPY+TAJtEW/39muwak43cmLhmrSmb9ExMhwfDoQotFBDOQ\nLyPPfMo46wKBgFwpWBHB1kt1sSV1UjoKuHdkXnmI6KUp/OEGZ9zVuNs3W0br5uYL\nHJ6Ttga9vvyiPjcL0HfXFDCODCS4m2CB1DPydIdTapen6zRW0V1KuD6ab+3GgLlE\n0ObfikVN9KcuZR6unfJd3Ri05mqTVxrt7D4f9DekfiNqCvV9vPRq5YI9AoGBAMeS\nYUOucK9EzzIYpVjN1E5cLCWWvDDM4PZZiP8c/UGTfpC7m/i1qf7zW4F4eRFJqM+n\npm5PUPmIoeasLoonETENUskUktM008KqKQhI8oAsTUNTR5AGZsjMhc1+D/zYevXN\nRUaljFWq8ooWJzOzaIWTBfIb3BUiTToFZmCmpmZxAoGAH2W3eDhuKrsE50ntofhZ\nx9Uy0OD30nztyecen26kuljcX7qdWh564mBob75wGSAoYzGSePZjJ/tQ4FznXUes\n6F4gM8Y58d3YZOjNq3xnT7AfaJHNnnjxA4pfnGRVr3wA2vwyrD3McgAPXf+mnB23\nl8nz6wxk97WnaIyo7Ofi/+A=\n-----END PRIVATE KEY-----\n"
|
|
8
|
+
};
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
/* eslint no-unused-expressions: 0, prefer-arrow-callback: 0, new-cap: 0 */
|
|
2
|
+
|
|
3
|
+
'use strict';
|
|
4
|
+
|
|
5
|
+
const proxyquire = require('proxyquire').noCallThru();
|
|
6
|
+
|
|
7
|
+
const { parser, compiler } = require('../lib/handler/imap-handler');
|
|
8
|
+
const { ImapStream } = require('../lib/handler/imap-stream');
|
|
9
|
+
const { ParserInstance } = require('../lib/handler/parser-instance');
|
|
10
|
+
const { TokenParser } = require('../lib/handler/token-parser');
|
|
11
|
+
const { searchCompiler } = require('../lib/search-compiler');
|
|
12
|
+
|
|
13
|
+
let asyncWrapper = (test, handler) => {
|
|
14
|
+
handler(test)
|
|
15
|
+
.then(() => test.done())
|
|
16
|
+
.catch(err => {
|
|
17
|
+
test.ifError(err);
|
|
18
|
+
test.done();
|
|
19
|
+
});
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// ============================================================================
|
|
23
|
+
// imap-compiler.js
|
|
24
|
+
// ============================================================================
|
|
25
|
+
|
|
26
|
+
// formatRespEntry: `return entry` when entry is a Buffer (line 23-25). A literal
|
|
27
|
+
// whose value is a Buffer, compiled in non-logging mode, passes the Buffer straight
|
|
28
|
+
// through formatRespEntry.
|
|
29
|
+
module.exports['imap-compiler: Buffer literal value passes through formatRespEntry'] = test =>
|
|
30
|
+
asyncWrapper(test, async test => {
|
|
31
|
+
let out = (
|
|
32
|
+
await compiler({
|
|
33
|
+
tag: '*',
|
|
34
|
+
command: 'CMD',
|
|
35
|
+
attributes: [{ type: 'LITERAL', value: Buffer.from('hello world') }]
|
|
36
|
+
})
|
|
37
|
+
).toString();
|
|
38
|
+
test.equal(out, '* CMD {11}\r\nhello world');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// number node === 0 -> `Math.round(node) || 0` falls back to 0 (line 117). A bare
|
|
42
|
+
// numeric attribute of 0 exercises the `|| 0` right side.
|
|
43
|
+
module.exports['imap-compiler: bare number 0 attribute uses fallback'] = test =>
|
|
44
|
+
asyncWrapper(test, async test => {
|
|
45
|
+
let out = (
|
|
46
|
+
await compiler({
|
|
47
|
+
tag: '*',
|
|
48
|
+
command: 'CMD',
|
|
49
|
+
attributes: [0]
|
|
50
|
+
})
|
|
51
|
+
).toString();
|
|
52
|
+
test.equal(out, '* CMD 0');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// LITERAL node with empty value -> `!node.value ? 0 : ...` true side (line 133).
|
|
56
|
+
module.exports['imap-compiler: LITERAL with empty string value yields {0}'] = test =>
|
|
57
|
+
asyncWrapper(test, async test => {
|
|
58
|
+
let out = (
|
|
59
|
+
await compiler({
|
|
60
|
+
tag: '*',
|
|
61
|
+
command: 'CMD',
|
|
62
|
+
attributes: [{ type: 'LITERAL', value: '' }]
|
|
63
|
+
})
|
|
64
|
+
).toString();
|
|
65
|
+
test.equal(out, '* CMD {0}\r\n');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// LITERAL- (literalMinus) with literal <= 4096 bytes in asArray mode -> exercises the
|
|
69
|
+
// `(literalMinus && literalLength <= 4096)` branch of canAppend (line 139) and usePlus.
|
|
70
|
+
module.exports['imap-compiler: LITERAL- inlines small literals in asArray mode'] = test =>
|
|
71
|
+
asyncWrapper(test, async test => {
|
|
72
|
+
let out = (
|
|
73
|
+
await compiler(
|
|
74
|
+
{
|
|
75
|
+
tag: '*',
|
|
76
|
+
command: 'CMD',
|
|
77
|
+
attributes: [{ type: 'LITERAL', value: 'small' }]
|
|
78
|
+
},
|
|
79
|
+
{ asArray: true, literalMinus: true }
|
|
80
|
+
)
|
|
81
|
+
).map(entry => entry.toString());
|
|
82
|
+
// LITERAL- still appends a non-synchronizing '+' marker for inlined literals
|
|
83
|
+
test.deepEqual(out, ['* CMD {5+}\r\nsmall']);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// LITERAL- with a literal larger than 4096 bytes in asArray mode -> canAppend false,
|
|
87
|
+
// so the synchronizing-literal split path runs. The new resp segment is seeded with
|
|
88
|
+
// `formatRespEntry(node.value, true) || []`. Using a Buffer value keeps formatRespEntry
|
|
89
|
+
// returning the Buffer (the `|| []` right side, line 156, is the focus together with the
|
|
90
|
+
// split branch).
|
|
91
|
+
module.exports['imap-compiler: LITERAL- large literal splits (synchronizing) in asArray mode'] = test =>
|
|
92
|
+
asyncWrapper(test, async test => {
|
|
93
|
+
let big = Buffer.alloc(5000, 0x61); // 5000 bytes > 4096
|
|
94
|
+
let out = (
|
|
95
|
+
await compiler(
|
|
96
|
+
{
|
|
97
|
+
tag: '*',
|
|
98
|
+
command: 'CMD',
|
|
99
|
+
attributes: [{ type: 'LITERAL', value: big }]
|
|
100
|
+
},
|
|
101
|
+
{ asArray: true, literalMinus: true }
|
|
102
|
+
)
|
|
103
|
+
).map(entry => entry.toString());
|
|
104
|
+
test.equal(out.length, 2);
|
|
105
|
+
test.equal(out[0], '* CMD {5000}\r\n');
|
|
106
|
+
test.equal(out[1], big.toString());
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// Synchronizing literal whose value is not a recognized type -> formatRespEntry(value, true)
|
|
110
|
+
// returns null, so `|| []` kicks in (line 156 right side) and the seeded resp is empty.
|
|
111
|
+
module.exports['imap-compiler: synchronizing literal with non-buffer value seeds empty segment'] = test =>
|
|
112
|
+
asyncWrapper(test, async test => {
|
|
113
|
+
let out = (
|
|
114
|
+
await compiler(
|
|
115
|
+
{
|
|
116
|
+
tag: '*',
|
|
117
|
+
command: 'CMD',
|
|
118
|
+
// value is an object that is neither string/number/Buffer but has a numeric
|
|
119
|
+
// length, so literalLength resolves to 10 while formatRespEntry(value, true)
|
|
120
|
+
// returns null -> the seeded resp falls back to [] (line 156 right side).
|
|
121
|
+
attributes: [{ type: 'LITERAL', value: { length: 10 } }]
|
|
122
|
+
},
|
|
123
|
+
{ asArray: true }
|
|
124
|
+
)
|
|
125
|
+
).map(entry => entry.toString());
|
|
126
|
+
// Only the header segment is emitted; the empty seeded data segment is dropped
|
|
127
|
+
// because `if (resp.length)` is false at the end.
|
|
128
|
+
test.deepEqual(out, ['* CMD {10}\r\n']);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// STRING node with falsy value -> `(node.value || '')` right side (line 165).
|
|
132
|
+
module.exports['imap-compiler: STRING with empty/falsy value compiles to empty quoted string'] = test =>
|
|
133
|
+
asyncWrapper(test, async test => {
|
|
134
|
+
let out = (
|
|
135
|
+
await compiler({
|
|
136
|
+
tag: '*',
|
|
137
|
+
command: 'CMD',
|
|
138
|
+
attributes: [{ type: 'STRING', value: '' }]
|
|
139
|
+
})
|
|
140
|
+
).toString();
|
|
141
|
+
test.equal(out, '* CMD ""');
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// response.attributes that is a single (non-array) object -> `[].concat(response.attributes)`
|
|
145
|
+
// right side of the ternary on line 215.
|
|
146
|
+
module.exports['imap-compiler: single non-array attributes object is wrapped'] = test =>
|
|
147
|
+
asyncWrapper(test, async test => {
|
|
148
|
+
let out = (
|
|
149
|
+
await compiler({
|
|
150
|
+
tag: '*',
|
|
151
|
+
command: 'CMD',
|
|
152
|
+
attributes: { type: 'ATOM', value: 'EXPUNGE' }
|
|
153
|
+
})
|
|
154
|
+
).toString();
|
|
155
|
+
test.equal(out, '* CMD EXPUNGE');
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// ============================================================================
|
|
159
|
+
// imap-stream.js
|
|
160
|
+
// ============================================================================
|
|
161
|
+
|
|
162
|
+
// Provide a logger object so the constructor takes the `this.options.logger` branch
|
|
163
|
+
// (lines 61-62) instead of creating a child logger.
|
|
164
|
+
module.exports['imap-stream: explicit logger option is used as-is'] = test => {
|
|
165
|
+
let calls = [];
|
|
166
|
+
let fakeLogger = {
|
|
167
|
+
trace: m => calls.push(m),
|
|
168
|
+
child: () => {
|
|
169
|
+
throw new Error('child should not be called when a logger is supplied');
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
let stream = new ImapStream({ cid: 'x', logger: fakeLogger });
|
|
173
|
+
test.equal(stream.log, fakeLogger, 'supplied logger object is used directly');
|
|
174
|
+
test.done();
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
// checkLiteralMarker: a line that is all digits + "}\r\n" but has no opening "{" makes
|
|
178
|
+
// the backward scan run off the start of the line, hitting the final `return false`
|
|
179
|
+
// (line 154).
|
|
180
|
+
module.exports['imap-stream: checkLiteralMarker returns false when no opening brace is found'] = test => {
|
|
181
|
+
let stream = new ImapStream({ cid: 'x' });
|
|
182
|
+
// "12}\n": scan finds '}', then digits '2','1', runs past index 0 -> final return false
|
|
183
|
+
test.equal(stream.checkLiteralMarker(Buffer.from('12}\n')), false);
|
|
184
|
+
// also exercise the CRLF variant
|
|
185
|
+
test.equal(stream.checkLiteralMarker(Buffer.from('99}\r\n')), false);
|
|
186
|
+
test.done();
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
// _transform with a string chunk -> `chunk = Buffer.from(chunk, encoding)` (lines 299-301).
|
|
190
|
+
// Call _transform directly with a string so the typeof === 'string' branch runs.
|
|
191
|
+
// The string->Buffer conversion and byte counting happen synchronously, before the
|
|
192
|
+
// chunk is queued for async processing, so readBytesCounter reflects the decoded length.
|
|
193
|
+
module.exports['imap-stream: _transform converts string chunks to Buffers'] = test => {
|
|
194
|
+
let stream = new ImapStream({ cid: 'x' });
|
|
195
|
+
stream._transform('A1 NOOP\r\n', 'utf8', () => {});
|
|
196
|
+
test.equal(stream.readBytesCounter, Buffer.byteLength('A1 NOOP\r\n'), 'decoded byte length was counted');
|
|
197
|
+
stream.destroy();
|
|
198
|
+
test.done();
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
// ============================================================================
|
|
202
|
+
// parser-instance.js
|
|
203
|
+
// ============================================================================
|
|
204
|
+
|
|
205
|
+
// Drive ParserInstance directly so getCommand is reached with a falsy (empty-string)
|
|
206
|
+
// command and a non-"+" tag, exercising the `(this.command || '')` right side (line 71).
|
|
207
|
+
// This state is not produced by normal parsing, so we construct it explicitly.
|
|
208
|
+
module.exports['parser-instance: getCommand falls back to empty string for falsy command'] = test =>
|
|
209
|
+
asyncWrapper(test, async test => {
|
|
210
|
+
let inst = new ParserInstance('* ', {});
|
|
211
|
+
inst.tag = '*'; // not '+', so the early continuation branch is skipped
|
|
212
|
+
// Pre-set command to a value getElement would have produced, then blank the
|
|
213
|
+
// remainder so the `if (!this.command)` re-fetch path throws? No - instead we
|
|
214
|
+
// set command to empty string and clear remainder so getElement is NOT re-run
|
|
215
|
+
// is impossible (empty string is falsy). Use a getElement stub on the instance
|
|
216
|
+
// to deterministically yield an empty command without touching lib/ source.
|
|
217
|
+
inst.getElement = async () => '';
|
|
218
|
+
let command = await inst.getCommand();
|
|
219
|
+
test.equal(command, '', 'command is the empty string');
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
// ============================================================================
|
|
223
|
+
// token-parser.js
|
|
224
|
+
// ============================================================================
|
|
225
|
+
|
|
226
|
+
// Construct TokenParser directly with a falsy str, undefined options and a falsy
|
|
227
|
+
// startPos to cover the `str || ''` (40), `options || {}` (41) and `startPos || 0`
|
|
228
|
+
// (45) fallbacks. These cannot occur through ParserInstance.getAttributes (which
|
|
229
|
+
// guards against an empty remainder), so direct construction is required.
|
|
230
|
+
module.exports['token-parser: constructor fallbacks for empty str / no options / zero startPos'] = test =>
|
|
231
|
+
asyncWrapper(test, async test => {
|
|
232
|
+
let parent = { command: 'FETCH' };
|
|
233
|
+
let tp = new TokenParser(parent, 0, undefined, undefined);
|
|
234
|
+
test.equal(tp.str, '');
|
|
235
|
+
test.deepEqual(tp.options, {});
|
|
236
|
+
test.equal(tp.pos, 0);
|
|
237
|
+
let attrs = await tp.getAttributes();
|
|
238
|
+
test.deepEqual(attrs, []);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
// Cover the `(node.type || '')` fallback (line 85) by walking a tree that contains a
|
|
242
|
+
// closed child node with a falsy type. Normal parsing always assigns a type, so the
|
|
243
|
+
// tree is constructed explicitly.
|
|
244
|
+
module.exports['token-parser: walk handles a closed node with a falsy type'] = test =>
|
|
245
|
+
asyncWrapper(test, async test => {
|
|
246
|
+
let parent = { command: 'FETCH' };
|
|
247
|
+
let tp = new TokenParser(parent, 0, '', {});
|
|
248
|
+
tp.tree = {
|
|
249
|
+
type: 'TREE',
|
|
250
|
+
isClosed: true,
|
|
251
|
+
childNodes: [{ type: false, isClosed: true, value: '', childNodes: [] }]
|
|
252
|
+
};
|
|
253
|
+
let attrs = await tp.getAttributes();
|
|
254
|
+
test.deepEqual(attrs, []);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
// ============================================================================
|
|
258
|
+
// search-compiler.js
|
|
259
|
+
// ============================================================================
|
|
260
|
+
|
|
261
|
+
let createMockConnection = (options = {}) => ({
|
|
262
|
+
capabilities: new Map(options.capabilities || [['IMAP4rev1', true]]),
|
|
263
|
+
enabled: new Set(options.enabled || []),
|
|
264
|
+
mailbox: options.mailbox || {
|
|
265
|
+
flags: new Set(['\\Seen']),
|
|
266
|
+
permanentFlags: new Set(['\\*'])
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
// setOpt NOT branch (lines 46-48): EMAILID/THREADID call setOpt with an unguarded
|
|
271
|
+
// params[term]. Passing the boolean `false` as the value reaches `value === false`
|
|
272
|
+
// and pushes a leading NOT atom. (A `null` value also enters this branch, but setOpt
|
|
273
|
+
// then crashes on `value.toString()` at line 56, so `false` is the only value that
|
|
274
|
+
// drives this branch to completion through the public API.)
|
|
275
|
+
module.exports['search-compiler: threadid false triggers setOpt NOT branch'] = test => {
|
|
276
|
+
let connection = createMockConnection({ capabilities: [['OBJECTID', true]] });
|
|
277
|
+
let attrs = searchCompiler(connection, { threadid: false });
|
|
278
|
+
test.deepEqual(attrs, [
|
|
279
|
+
{ type: 'ATOM', value: 'NOT' },
|
|
280
|
+
{ type: 'ATOM', value: 'THREADID' },
|
|
281
|
+
{ type: 'ATOM', value: 'false' }
|
|
282
|
+
]);
|
|
283
|
+
test.done();
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
// KEYWORD search where the flag is NOT usable via canUseFlag but IS already present in
|
|
287
|
+
// the mailbox flags set -> exercises the `mailbox.flags.has(flag)` right side (line 297).
|
|
288
|
+
module.exports['search-compiler: keyword already present in mailbox flags is accepted'] = test => {
|
|
289
|
+
// permanentFlags has no '\\*', so canUseFlag(mailbox, customFlag) is false, but the
|
|
290
|
+
// flag is in mailbox.flags, so the OR short-circuits to the right operand.
|
|
291
|
+
let connection = createMockConnection({
|
|
292
|
+
mailbox: {
|
|
293
|
+
flags: new Set(['$Label1']),
|
|
294
|
+
permanentFlags: new Set(['\\Seen']) // no '\\*' wildcard -> custom flags not auto-usable
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
let attrs = searchCompiler(connection, { keyword: '$Label1' });
|
|
298
|
+
test.deepEqual(attrs, [
|
|
299
|
+
{ type: 'ATOM', value: 'KEYWORD' },
|
|
300
|
+
{ type: 'ATOM', value: '$Label1' }
|
|
301
|
+
]);
|
|
302
|
+
test.done();
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
// ============================================================================
|
|
306
|
+
// proxy-connection.js
|
|
307
|
+
// ============================================================================
|
|
308
|
+
|
|
309
|
+
// attachEarlyErrorHandler guard (lines 23-25): the proxy client returns a truthy socket
|
|
310
|
+
// that lacks an `.on` method, so the guard returns early.
|
|
311
|
+
module.exports['proxy-connection: socket without .on skips early error handler attach'] = async test => {
|
|
312
|
+
let logger = { info: () => {}, error: () => {} };
|
|
313
|
+
let bareSocket = { write() {}, end() {} }; // truthy, but no .on
|
|
314
|
+
const { proxyConnection } = proxyquire('../lib/proxy-connection', {
|
|
315
|
+
'nodemailer/lib/smtp-connection/http-proxy-client': (url, port, host, cb) => {
|
|
316
|
+
cb(null, bareSocket);
|
|
317
|
+
},
|
|
318
|
+
socks: { SocksClient: {} },
|
|
319
|
+
dns: { promises: { resolve: async () => ['127.0.0.1'] } },
|
|
320
|
+
net: { isIP: () => true }
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
let socket = await proxyConnection(logger, 'http://proxy.example.com:8080', '192.168.1.1', 993);
|
|
324
|
+
test.equal(socket, bareSocket, 'socket returned unchanged');
|
|
325
|
+
test.ok(!('_earlyErrorHandler' in socket), 'no early error handler was attached to a socket without .on');
|
|
326
|
+
test.done();
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
// Sanity: untagged parse still round-trips, just to keep the parser import exercised.
|
|
330
|
+
module.exports['sanity: parser/compiler round-trip'] = test =>
|
|
331
|
+
asyncWrapper(test, async test => {
|
|
332
|
+
let parsed = await parser('* OK Ready', { allowUntagged: true });
|
|
333
|
+
test.equal(parsed.command, 'OK');
|
|
334
|
+
});
|
|
@@ -56,6 +56,53 @@ module.exports['IMAP Compiler: TEXT'] = test =>
|
|
|
56
56
|
)
|
|
57
57
|
);
|
|
58
58
|
|
|
59
|
+
module.exports['IMAP Compiler: NUMBER attribute'] = test =>
|
|
60
|
+
asyncWrapper(test, async test =>
|
|
61
|
+
test.equal(
|
|
62
|
+
(
|
|
63
|
+
await compiler({
|
|
64
|
+
tag: '*',
|
|
65
|
+
command: 'CMD',
|
|
66
|
+
attributes: [
|
|
67
|
+
{
|
|
68
|
+
type: 'NUMBER',
|
|
69
|
+
value: 42
|
|
70
|
+
}
|
|
71
|
+
]
|
|
72
|
+
})
|
|
73
|
+
).toString(),
|
|
74
|
+
'* CMD 42'
|
|
75
|
+
)
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
module.exports['IMAP Compiler: NUMBER attribute defaults missing value to 0'] = test =>
|
|
79
|
+
asyncWrapper(test, async test =>
|
|
80
|
+
test.equal(
|
|
81
|
+
(
|
|
82
|
+
await compiler({
|
|
83
|
+
tag: '*',
|
|
84
|
+
command: 'CMD',
|
|
85
|
+
attributes: [{ type: 'NUMBER' }]
|
|
86
|
+
})
|
|
87
|
+
).toString(),
|
|
88
|
+
'* CMD 0'
|
|
89
|
+
)
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
module.exports['IMAP Compiler: unrecognized value type compiles to empty buffer'] = test =>
|
|
93
|
+
asyncWrapper(test, async test => {
|
|
94
|
+
// A SEQUENCE node whose value is neither string/number/Buffer falls through
|
|
95
|
+
// formatRespEntry to an empty Buffer rather than throwing.
|
|
96
|
+
let out = (
|
|
97
|
+
await compiler({
|
|
98
|
+
tag: '*',
|
|
99
|
+
command: 'CMD',
|
|
100
|
+
attributes: [{ type: 'SEQUENCE', value: { unexpected: true } }]
|
|
101
|
+
})
|
|
102
|
+
).toString();
|
|
103
|
+
test.equal(out, '* CMD ');
|
|
104
|
+
});
|
|
105
|
+
|
|
59
106
|
module.exports['IMAP Compiler: SECTION'] = test =>
|
|
60
107
|
asyncWrapper(test, async test =>
|
|
61
108
|
test.equal(
|