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.
Files changed (44) hide show
  1. package/.github/codeql/codeql-config.yml +12 -0
  2. package/.github/workflows/codeql.yml +102 -0
  3. package/.github/workflows/stale.yml +5 -0
  4. package/.github/workflows/test.yml +3 -0
  5. package/.release-please-manifest.json +1 -1
  6. package/CHANGELOG.md +14 -0
  7. package/lib/commands/authenticate.js +5 -5
  8. package/lib/commands/fetch.js +1 -0
  9. package/lib/commands/logout.js +1 -0
  10. package/lib/commands/select.js +2 -1
  11. package/lib/commands/starttls.js +3 -0
  12. package/lib/commands/store.js +1 -0
  13. package/lib/handler/imap-stream.js +23 -6
  14. package/lib/handler/token-parser.js +0 -5
  15. package/lib/imap-flow.d.ts +5 -0
  16. package/lib/imap-flow.js +205 -29
  17. package/lib/proxy-connection.js +26 -1
  18. package/lib/tools.js +12 -0
  19. package/package.json +1 -1
  20. package/test/commands-branches-test.js +1068 -0
  21. package/test/connection-edge-cases-test.js +138 -1
  22. package/test/fixtures/test-tls.js +8 -0
  23. package/test/handler-branches-test.js +334 -0
  24. package/test/imap-compiler-test.js +47 -0
  25. package/test/imap-flow-compress-test.js +154 -0
  26. package/test/imap-flow-coverage-test.js +609 -0
  27. package/test/imap-flow-fetch-download-test.js +801 -0
  28. package/test/imap-flow-internals-test.js +450 -0
  29. package/test/imap-flow-methods-test.js +738 -0
  30. package/test/imap-flow-proxy-paths-test.js +215 -0
  31. package/test/imap-flow-secure-test.js +327 -0
  32. package/test/imap-flow-server-test.js +1046 -0
  33. package/test/imap-formal-syntax-test.js +18 -0
  34. package/test/imap-parser-test.js +52 -0
  35. package/test/imap-stream-edge-cases-test.js +122 -0
  36. package/test/jp-decoder-test.js +48 -0
  37. package/test/limited-passthrough-test.js +17 -0
  38. package/test/proxy-connection-test.js +86 -0
  39. package/test/reliability-improvements-test.js +88 -0
  40. package/test/search-compiler-test.js +31 -0
  41. package/test/search-test.js +61 -0
  42. package/test/starttls-injection-test.js +181 -0
  43. package/test/token-parser-test.js +64 -0
  44. package/test/tools-test.js +360 -0
@@ -0,0 +1,154 @@
1
+ 'use strict';
2
+
3
+ // End-to-end test of the DEFLATE compression path (RFC 4978). The mock server
4
+ // negotiates COMPRESS and then speaks raw-deflate in both directions, mirroring
5
+ // the client's inflate/deflate pipeline. This covers ImapFlow.compress() and the
6
+ // compression-stream cleanup inside close().
7
+
8
+ const net = require('net');
9
+ const zlib = require('zlib');
10
+ const { ImapFlow } = require('../lib/imap-flow');
11
+
12
+ const CAPS = 'IMAP4rev1 ID ENABLE NAMESPACE COMPRESS=DEFLATE';
13
+
14
+ const listen = server => new Promise(resolve => server.listen(0, '127.0.0.1', () => resolve(server.address().port)));
15
+
16
+ // IMAP session line responder shared before and after compression.
17
+ const respond = (write, line) => {
18
+ let parts = line.split(' ');
19
+ let tag = parts[0];
20
+ let cmd = (parts[1] || '').toUpperCase();
21
+ switch (cmd) {
22
+ case 'CAPABILITY':
23
+ write(`* CAPABILITY ${CAPS}\r\n${tag} OK done\r\n`);
24
+ break;
25
+ case 'ID':
26
+ write(`* ID ("name" "mock" "version" "1")\r\n${tag} OK done\r\n`);
27
+ break;
28
+ case 'LOGIN':
29
+ write(`${tag} OK LOGIN done\r\n`);
30
+ break;
31
+ case 'NAMESPACE':
32
+ write(`* NAMESPACE (("" "/")) NIL NIL\r\n${tag} OK done\r\n`);
33
+ break;
34
+ case 'ENABLE':
35
+ write(`${tag} OK done\r\n`);
36
+ break;
37
+ case 'NOOP':
38
+ write(`${tag} OK NOOP done\r\n`);
39
+ break;
40
+ case 'LOGOUT':
41
+ write(`* BYE bye\r\n${tag} OK done\r\n`);
42
+ break;
43
+ default:
44
+ write(`${tag} BAD unknown ${cmd}\r\n`);
45
+ }
46
+ };
47
+
48
+ module.exports['Compress: session negotiates DEFLATE and runs commands'] = async test => {
49
+ let server = net.createServer(socket => {
50
+ socket.on('error', () => {});
51
+
52
+ let compressed = false;
53
+ let deflate = null;
54
+ let inflate = null;
55
+ let buf = '';
56
+
57
+ // Plain writer (pre-compression)
58
+ let writePlain = str => socket.write(str);
59
+ // Compressed writer: deflate + flush so the client receives complete frames
60
+ let writeDeflate = str => {
61
+ deflate.write(Buffer.from(str, 'binary'));
62
+ deflate.flush();
63
+ };
64
+
65
+ // function declarations (hoisted) because feed() and onLine() are mutually recursive
66
+ let lineBuf = '';
67
+ function onLine(line) {
68
+ let parts = line.split(' ');
69
+ let tag = parts[0];
70
+ let cmd = (parts[1] || '').toUpperCase();
71
+ if (cmd === 'COMPRESS') {
72
+ // Acknowledge, then switch both directions to raw deflate
73
+ writePlain(`${tag} OK Begin compression\r\n`);
74
+ compressed = true;
75
+ deflate = zlib.createDeflateRaw();
76
+ inflate = zlib.createInflateRaw();
77
+ deflate.pipe(socket);
78
+ inflate.on('data', d => feed(d.toString('binary')));
79
+ inflate.on('error', () => {});
80
+ deflate.on('error', () => {});
81
+ return;
82
+ }
83
+ respond(compressed ? writeDeflate : writePlain, line);
84
+ }
85
+
86
+ function feed(chunk) {
87
+ lineBuf += chunk;
88
+ let idx;
89
+ while ((idx = lineBuf.indexOf('\r\n')) >= 0) {
90
+ let line = lineBuf.slice(0, idx);
91
+ lineBuf = lineBuf.slice(idx + 2);
92
+ onLine(line);
93
+ }
94
+ }
95
+
96
+ socket.on('data', data => {
97
+ if (compressed) {
98
+ // After COMPRESS negotiation, all inbound bytes are deflated
99
+ inflate.write(data);
100
+ } else {
101
+ buf += data.toString('binary');
102
+ let idx;
103
+ while ((idx = buf.indexOf('\r\n')) >= 0) {
104
+ let line = buf.slice(0, idx);
105
+ buf = buf.slice(idx + 2);
106
+ // hand off any already-buffered bytes after COMPRESS to inflate
107
+ onLine(line);
108
+ if (compressed && buf.length) {
109
+ inflate.write(Buffer.from(buf, 'binary'));
110
+ buf = '';
111
+ }
112
+ }
113
+ }
114
+ });
115
+
116
+ socket.write(`* OK [CAPABILITY ${CAPS}] ready\r\n`);
117
+ });
118
+
119
+ let port = await listen(server);
120
+ let client = new ImapFlow({
121
+ host: '127.0.0.1',
122
+ port,
123
+ secure: false,
124
+ disableAutoIdle: true,
125
+ disableCompression: false, // enable COMPRESS negotiation
126
+ logger: false,
127
+ auth: { user: 'test', pass: 'secret' }
128
+ });
129
+ client.on('error', () => {});
130
+
131
+ await client.connect();
132
+ test.ok(client.usable);
133
+ test.ok(client._deflate, 'deflate pipeline established');
134
+ test.ok(client._inflate, 'inflate pipeline established');
135
+
136
+ // Run a command through the compressed channel
137
+ await client.noop();
138
+
139
+ // Push a buffer larger than the deflate writable highWaterMark (64KB) straight
140
+ // through the compression pump so its buffer overflows and readNext waits for 'drain'.
141
+ client.state = client.states.AUTHENTICATED;
142
+ client.commandParts = [];
143
+ client.write(Buffer.alloc(128 * 1024, 0x61));
144
+ await new Promise(r => setTimeout(r, 50));
145
+
146
+ await client.logout();
147
+ client.close();
148
+ // close() must tear down the compression streams
149
+ test.ok(!client._deflate, 'deflate cleaned up after close');
150
+ test.ok(!client._inflate, 'inflate cleaned up after close');
151
+
152
+ server.close();
153
+ test.done();
154
+ };