imapflow 1.3.6 → 1.4.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.
@@ -0,0 +1,215 @@
1
+ 'use strict';
2
+
3
+ // Exercises connect()'s proxy code path by injecting a mocked proxyConnection
4
+ // (via proxyquire) that returns a cleartext socket connected to an in-process
5
+ // mock IMAP server. Also covers the proxy-setup failure path.
6
+
7
+ const net = require('net');
8
+ const tls = require('tls');
9
+ const proxyquire = require('proxyquire');
10
+ const { cert, key } = require('./fixtures/test-tls');
11
+
12
+ const CAPS = 'IMAP4rev1 ID ENABLE NAMESPACE';
13
+
14
+ // Minimal mock IMAP server (plaintext) sufficient for a full session.
15
+ const createServer = () =>
16
+ net.createServer(socket => {
17
+ socket.on('error', () => {});
18
+ let buf = '';
19
+ socket.on('data', data => {
20
+ buf += data.toString('binary');
21
+ let idx;
22
+ while ((idx = buf.indexOf('\r\n')) >= 0) {
23
+ let line = buf.slice(0, idx);
24
+ buf = buf.slice(idx + 2);
25
+ let parts = line.split(' ');
26
+ let tag = parts[0];
27
+ let cmd = (parts[1] || '').toUpperCase();
28
+ switch (cmd) {
29
+ case 'CAPABILITY':
30
+ socket.write(`* CAPABILITY ${CAPS}\r\n${tag} OK done\r\n`);
31
+ break;
32
+ case 'ID':
33
+ socket.write(`* ID ("name" "mock" "version" "1")\r\n${tag} OK done\r\n`);
34
+ break;
35
+ case 'LOGIN':
36
+ socket.write(`${tag} OK LOGIN done\r\n`);
37
+ break;
38
+ case 'NAMESPACE':
39
+ socket.write(`* NAMESPACE (("" "/")) NIL NIL\r\n${tag} OK done\r\n`);
40
+ break;
41
+ case 'ENABLE':
42
+ socket.write(`${tag} OK done\r\n`);
43
+ break;
44
+ case 'LOGOUT':
45
+ socket.write(`* BYE bye\r\n${tag} OK done\r\n`);
46
+ break;
47
+ default:
48
+ socket.write(`${tag} OK ok\r\n`);
49
+ }
50
+ }
51
+ });
52
+ socket.write(`* OK [CAPABILITY ${CAPS}] ready\r\n`);
53
+ });
54
+
55
+ const listen = server => new Promise(resolve => server.listen(0, '127.0.0.1', () => resolve(server.address().port)));
56
+
57
+ module.exports['Proxy: cleartext connection established through a proxy socket'] = async test => {
58
+ let server = createServer();
59
+ let port = await listen(server);
60
+
61
+ // Mock proxyConnection to hand back a real cleartext socket to our server.
62
+ const { ImapFlow } = proxyquire('../lib/imap-flow', {
63
+ './proxy-connection': {
64
+ proxyConnection: async () => net.connect(port, '127.0.0.1'),
65
+ detachEarlyErrorHandler: () => {}
66
+ }
67
+ });
68
+
69
+ let client = new ImapFlow({
70
+ host: '127.0.0.1',
71
+ port,
72
+ secure: false,
73
+ proxy: 'socks://127.0.0.1:1080',
74
+ disableAutoIdle: true,
75
+ disableCompression: true,
76
+ logger: false,
77
+ auth: { user: 'test', pass: 'secret' }
78
+ });
79
+ client.on('error', () => {});
80
+
81
+ await client.connect();
82
+ test.ok(client.usable, 'session established over the proxied socket');
83
+ test.ok(client.authenticated);
84
+
85
+ await client.logout();
86
+ client.close();
87
+ server.close();
88
+ test.done();
89
+ };
90
+
91
+ module.exports['Proxy: TLS connection established through a proxy socket'] = async test => {
92
+ let server = tls.createServer({ cert, key }, socket => {
93
+ socket.on('error', () => {});
94
+ let buf = '';
95
+ socket.on('data', data => {
96
+ buf += data.toString('binary');
97
+ let idx;
98
+ while ((idx = buf.indexOf('\r\n')) >= 0) {
99
+ let line = buf.slice(0, idx);
100
+ buf = buf.slice(idx + 2);
101
+ let parts = line.split(' ');
102
+ let tag = parts[0];
103
+ let cmd = (parts[1] || '').toUpperCase();
104
+ if (cmd === 'CAPABILITY') socket.write(`* CAPABILITY ${CAPS}\r\n${tag} OK done\r\n`);
105
+ else if (cmd === 'ID') socket.write(`* ID ("name" "m")\r\n${tag} OK done\r\n`);
106
+ else if (cmd === 'NAMESPACE') socket.write(`* NAMESPACE (("" "/")) NIL NIL\r\n${tag} OK done\r\n`);
107
+ else if (cmd === 'LOGOUT') socket.write(`* BYE bye\r\n${tag} OK done\r\n`);
108
+ else socket.write(`${tag} OK ok\r\n`);
109
+ }
110
+ });
111
+ socket.write(`* OK [CAPABILITY ${CAPS}] ready\r\n`);
112
+ });
113
+ let port = await listen(server);
114
+
115
+ // proxyConnection returns a plaintext TCP socket; the client then wraps it in TLS.
116
+ const { ImapFlow } = proxyquire('../lib/imap-flow', {
117
+ './proxy-connection': {
118
+ proxyConnection: async () => net.connect(port, '127.0.0.1'),
119
+ detachEarlyErrorHandler: () => {}
120
+ }
121
+ });
122
+
123
+ let client = new ImapFlow({
124
+ host: '127.0.0.1',
125
+ port,
126
+ secure: true,
127
+ servername: 'localhost',
128
+ tls: { rejectUnauthorized: false },
129
+ proxy: 'socks://127.0.0.1:1080',
130
+ disableAutoIdle: true,
131
+ disableCompression: true,
132
+ logger: false,
133
+ auth: { user: 'test', pass: 'secret' }
134
+ });
135
+ client.on('error', () => {});
136
+
137
+ await client.connect();
138
+ test.ok(client.secureConnection, 'TLS handshake over the proxy socket');
139
+ test.ok(client.usable);
140
+
141
+ await client.logout();
142
+ client.close();
143
+ server.close();
144
+ test.done();
145
+ };
146
+
147
+ module.exports['Proxy: a null proxy socket rejects connect'] = async test => {
148
+ const { ImapFlow } = proxyquire('../lib/imap-flow', {
149
+ './proxy-connection': {
150
+ proxyConnection: async () => null, // proxy setup yields no socket
151
+ detachEarlyErrorHandler: () => {}
152
+ }
153
+ });
154
+
155
+ let client = new ImapFlow({
156
+ host: '127.0.0.1',
157
+ port: 1,
158
+ secure: false,
159
+ proxy: 'socks://127.0.0.1:1080',
160
+ disableAutoIdle: true,
161
+ disableCompression: true,
162
+ logger: false,
163
+ auth: { user: 'test', pass: 'secret' }
164
+ });
165
+ client.on('error', () => {});
166
+
167
+ let err = null;
168
+ try {
169
+ await client.connect();
170
+ } catch (e) {
171
+ err = e;
172
+ }
173
+ test.ok(err, 'connect rejected when proxy returns no socket');
174
+ test.equal(err.code, 'ProxyError');
175
+
176
+ client.close();
177
+ test.done();
178
+ };
179
+
180
+ module.exports['Proxy: proxyConnection throwing rejects connect'] = async test => {
181
+ const { ImapFlow } = proxyquire('../lib/imap-flow', {
182
+ './proxy-connection': {
183
+ proxyConnection: async () => {
184
+ let e = new Error('SOCKS handshake failed');
185
+ e.code = 'ESOCKS';
186
+ throw e;
187
+ },
188
+ detachEarlyErrorHandler: () => {}
189
+ }
190
+ });
191
+
192
+ let client = new ImapFlow({
193
+ host: '127.0.0.1',
194
+ port: 1,
195
+ secure: false,
196
+ proxy: 'socks://127.0.0.1:1080',
197
+ disableAutoIdle: true,
198
+ disableCompression: true,
199
+ logger: false,
200
+ auth: { user: 'test', pass: 'secret' }
201
+ });
202
+ client.on('error', () => {});
203
+
204
+ let err = null;
205
+ try {
206
+ await client.connect();
207
+ } catch (e) {
208
+ err = e;
209
+ }
210
+ test.ok(err);
211
+ test.equal(err.code, 'ESOCKS');
212
+
213
+ client.close();
214
+ test.done();
215
+ };
@@ -0,0 +1,327 @@
1
+ 'use strict';
2
+
3
+ // End-to-end secure-transport tests: a successful STARTTLS upgrade and a direct
4
+ // TLS connection, both against in-process mock servers using a self-signed cert.
5
+ // These cover upgradeToSTARTTLS()'s success path and the TLS branches of connect().
6
+
7
+ const net = require('net');
8
+ const tls = require('tls');
9
+ const { ImapFlow } = require('../lib/imap-flow');
10
+ const { cert, key } = require('./fixtures/test-tls');
11
+
12
+ const CAPS = 'IMAP4rev1 ID ENABLE NAMESPACE';
13
+
14
+ // Shared per-connection IMAP line handler used by both the plaintext and the
15
+ // upgraded TLS phases. Returns responses for the minimal session commands.
16
+ const handleLine = (sock, line, onStartTls) => {
17
+ let parts = line.split(' ');
18
+ let tag = parts[0];
19
+ let cmd = (parts[1] || '').toUpperCase();
20
+ switch (cmd) {
21
+ case 'CAPABILITY':
22
+ sock.write(`* CAPABILITY ${CAPS} STARTTLS\r\n${tag} OK CAPABILITY done\r\n`);
23
+ break;
24
+ case 'STARTTLS':
25
+ sock.write(`${tag} OK Begin TLS\r\n`);
26
+ if (onStartTls) {
27
+ onStartTls();
28
+ }
29
+ break;
30
+ case 'ID':
31
+ sock.write(`* ID ("name" "mock" "version" "1")\r\n${tag} OK ID done\r\n`);
32
+ break;
33
+ case 'LOGIN':
34
+ sock.write(`${tag} OK LOGIN done\r\n`);
35
+ break;
36
+ case 'NAMESPACE':
37
+ sock.write(`* NAMESPACE (("" "/")) NIL NIL\r\n${tag} OK NAMESPACE done\r\n`);
38
+ break;
39
+ case 'ENABLE':
40
+ sock.write(`${tag} OK ENABLE done\r\n`);
41
+ break;
42
+ case 'COMPRESS':
43
+ sock.write(`${tag} NO not now\r\n`);
44
+ break;
45
+ case 'NOOP':
46
+ sock.write(`${tag} OK NOOP done\r\n`);
47
+ break;
48
+ case 'LOGOUT':
49
+ sock.write(`* BYE bye\r\n${tag} OK LOGOUT done\r\n`);
50
+ break;
51
+ default:
52
+ sock.write(`${tag} BAD unknown ${cmd}\r\n`);
53
+ }
54
+ };
55
+
56
+ const lineReader = (sock, onLine) => {
57
+ let buf = '';
58
+ let handler = data => {
59
+ buf += data.toString('binary');
60
+ let idx;
61
+ while ((idx = buf.indexOf('\r\n')) >= 0) {
62
+ let line = buf.slice(0, idx);
63
+ buf = buf.slice(idx + 2);
64
+ onLine(line);
65
+ }
66
+ };
67
+ sock.on('data', handler);
68
+ sock.on('error', () => {});
69
+ return () => sock.removeListener('data', handler);
70
+ };
71
+
72
+ const listen = server => new Promise(resolve => server.listen(0, '127.0.0.1', () => resolve(server.address().port)));
73
+
74
+ // ---------------------------------------------------------------------------
75
+ // STARTTLS happy path
76
+ // ---------------------------------------------------------------------------
77
+
78
+ module.exports['Secure: STARTTLS upgrade completes a session'] = async test => {
79
+ let server = net.createServer(rawSocket => {
80
+ rawSocket.on('error', () => {});
81
+
82
+ let detachPlain;
83
+ detachPlain = lineReader(rawSocket, line => {
84
+ handleLine(rawSocket, line, () => {
85
+ // Upgrade: stop reading plaintext, wrap the socket in TLS
86
+ detachPlain();
87
+ let tlsSocket = new tls.TLSSocket(rawSocket, { isServer: true, key, cert });
88
+ tlsSocket.on('error', () => {});
89
+ tlsSocket.on('secure', () => {});
90
+ lineReader(tlsSocket, l => handleLine(tlsSocket, l));
91
+ });
92
+ });
93
+
94
+ rawSocket.write(`* OK [CAPABILITY ${CAPS} STARTTLS] ready\r\n`);
95
+ });
96
+
97
+ let port = await listen(server);
98
+ let client = new ImapFlow({
99
+ host: '127.0.0.1',
100
+ port,
101
+ secure: false,
102
+ doSTARTTLS: true,
103
+ servername: 'localhost',
104
+ tls: { rejectUnauthorized: false },
105
+ disableAutoIdle: true,
106
+ disableCompression: true,
107
+ logger: false,
108
+ auth: { user: 'test', pass: 'secret' }
109
+ });
110
+ client.on('error', () => {});
111
+
112
+ await client.connect();
113
+ test.ok(client.secureConnection, 'connection upgraded to TLS');
114
+ test.ok(client.authenticated, 'authenticated over TLS');
115
+ test.ok(client.usable);
116
+
117
+ await client.noop();
118
+ await client.logout();
119
+ client.close();
120
+ server.close();
121
+ test.done();
122
+ };
123
+
124
+ // ---------------------------------------------------------------------------
125
+ // Direct TLS connection
126
+ // ---------------------------------------------------------------------------
127
+
128
+ module.exports['Secure: direct TLS connection completes a session'] = async test => {
129
+ let server = tls.createServer({ key, cert }, sock => {
130
+ sock.on('error', () => {});
131
+ lineReader(sock, line => handleLine(sock, line));
132
+ sock.write(`* OK [CAPABILITY ${CAPS}] ready\r\n`);
133
+ });
134
+
135
+ let port = await listen(server);
136
+ let client = new ImapFlow({
137
+ host: '127.0.0.1',
138
+ port,
139
+ secure: true,
140
+ servername: 'localhost',
141
+ tls: { rejectUnauthorized: false },
142
+ disableAutoIdle: true,
143
+ disableCompression: true,
144
+ logger: false,
145
+ auth: { user: 'test', pass: 'secret' }
146
+ });
147
+ client.on('error', () => {});
148
+
149
+ await client.connect();
150
+ test.ok(client.secureConnection);
151
+ test.ok(client.authenticated);
152
+ test.ok(client.tls, 'cipher info recorded');
153
+
154
+ await client.noop();
155
+ await client.logout();
156
+ client.close();
157
+ server.close();
158
+ test.done();
159
+ };
160
+
161
+ // ---------------------------------------------------------------------------
162
+ // STARTTLS required but unsupported -> fail closed
163
+ // ---------------------------------------------------------------------------
164
+
165
+ module.exports['Secure: doSTARTTLS=false skips an advertised upgrade'] = async test => {
166
+ let server = net.createServer(sock => {
167
+ sock.on('error', () => {});
168
+ lineReader(sock, line => handleLine(sock, line)); // advertises STARTTLS in CAPABILITY
169
+ sock.write(`* OK [CAPABILITY ${CAPS} STARTTLS] ready\r\n`);
170
+ });
171
+ let port = await listen(server);
172
+ let client = new ImapFlow({
173
+ host: '127.0.0.1',
174
+ port,
175
+ secure: false,
176
+ doSTARTTLS: false, // explicitly disabled -> stay plaintext
177
+ tls: { rejectUnauthorized: false },
178
+ disableAutoIdle: true,
179
+ disableCompression: true,
180
+ logger: false,
181
+ auth: { user: 'test', pass: 'secret' }
182
+ });
183
+ client.on('error', () => {});
184
+
185
+ await client.connect();
186
+ test.ok(!client.secureConnection, 'stayed on plaintext');
187
+ test.ok(client.authenticated);
188
+
189
+ await client.logout();
190
+ client.close();
191
+ server.close();
192
+ test.done();
193
+ };
194
+
195
+ module.exports['Secure: STARTTLS command rejected by server fails closed'] = async test => {
196
+ let server = net.createServer(sock => {
197
+ sock.on('error', () => {});
198
+ lineReader(sock, line => {
199
+ let parts = line.split(' ');
200
+ let tag = parts[0];
201
+ let cmd = (parts[1] || '').toUpperCase();
202
+ if (cmd === 'CAPABILITY') {
203
+ sock.write(`* CAPABILITY ${CAPS} STARTTLS\r\n${tag} OK done\r\n`);
204
+ } else if (cmd === 'STARTTLS') {
205
+ sock.write(`${tag} NO STARTTLS not available right now\r\n`);
206
+ } else {
207
+ sock.write(`${tag} OK ok\r\n`);
208
+ }
209
+ });
210
+ sock.write(`* OK [CAPABILITY ${CAPS} STARTTLS] ready\r\n`);
211
+ });
212
+ let port = await listen(server);
213
+ let client = new ImapFlow({
214
+ host: '127.0.0.1',
215
+ port,
216
+ secure: false,
217
+ doSTARTTLS: true,
218
+ tls: { rejectUnauthorized: false },
219
+ disableAutoIdle: true,
220
+ disableCompression: true,
221
+ logger: false,
222
+ auth: { user: 'test', pass: 'secret' }
223
+ });
224
+ client.on('error', () => {});
225
+
226
+ let err = null;
227
+ try {
228
+ await client.connect();
229
+ } catch (e) {
230
+ err = e;
231
+ }
232
+ test.ok(err, 'connect rejected when STARTTLS refused');
233
+ test.ok(err.tlsFailed);
234
+
235
+ client.close();
236
+ server.close();
237
+ test.done();
238
+ };
239
+
240
+ module.exports['Secure: STARTTLS handshake error rejects connect'] = async test => {
241
+ // Server acknowledges STARTTLS then sends non-TLS garbage so the client's TLS
242
+ // handshake fails, exercising the dedicated TLS-socket error handler.
243
+ let server = net.createServer(rawSocket => {
244
+ rawSocket.on('error', () => {});
245
+ let detach = lineReader(rawSocket, line => {
246
+ handleLine(rawSocket, line, () => {
247
+ detach();
248
+ // garbage instead of a TLS ServerHello
249
+ rawSocket.write(Buffer.from('this is definitely not a TLS handshake\r\n'.repeat(8)));
250
+ });
251
+ });
252
+ rawSocket.write(`* OK [CAPABILITY ${CAPS} STARTTLS] ready\r\n`);
253
+ });
254
+ let port = await listen(server);
255
+ let client = new ImapFlow({
256
+ host: '127.0.0.1',
257
+ port,
258
+ secure: false,
259
+ doSTARTTLS: true,
260
+ servername: 'localhost',
261
+ tls: { rejectUnauthorized: false },
262
+ disableAutoIdle: true,
263
+ disableCompression: true,
264
+ logger: false,
265
+ auth: { user: 'test', pass: 'secret' }
266
+ });
267
+ client.on('error', () => {});
268
+
269
+ let err = null;
270
+ try {
271
+ await client.connect();
272
+ } catch (e) {
273
+ err = e;
274
+ }
275
+ test.ok(err, 'connect rejected on TLS handshake failure');
276
+ test.ok(err.tlsFailed, 'flagged as a TLS failure');
277
+
278
+ client.close();
279
+ server.close();
280
+ test.done();
281
+ };
282
+
283
+ module.exports['Secure: STARTTLS required but not advertised throws'] = async test => {
284
+ let server = net.createServer(sock => {
285
+ sock.on('error', () => {});
286
+ lineReader(sock, line => {
287
+ let parts = line.split(' ');
288
+ let tag = parts[0];
289
+ let cmd = (parts[1] || '').toUpperCase();
290
+ if (cmd === 'CAPABILITY') {
291
+ // no STARTTLS advertised
292
+ sock.write(`* CAPABILITY ${CAPS}\r\n${tag} OK done\r\n`);
293
+ } else {
294
+ sock.write(`${tag} OK ok\r\n`);
295
+ }
296
+ });
297
+ sock.write(`* OK [CAPABILITY ${CAPS}] ready\r\n`);
298
+ });
299
+
300
+ let port = await listen(server);
301
+ let client = new ImapFlow({
302
+ host: '127.0.0.1',
303
+ port,
304
+ secure: false,
305
+ doSTARTTLS: true,
306
+ servername: 'localhost',
307
+ tls: { rejectUnauthorized: false },
308
+ disableAutoIdle: true,
309
+ disableCompression: true,
310
+ logger: false,
311
+ auth: { user: 'test', pass: 'secret' }
312
+ });
313
+ client.on('error', () => {});
314
+
315
+ let err = null;
316
+ try {
317
+ await client.connect();
318
+ } catch (e) {
319
+ err = e;
320
+ }
321
+ test.ok(err, 'connect rejected');
322
+ test.ok(err.tlsFailed, 'flagged as TLS failure');
323
+
324
+ client.close();
325
+ server.close();
326
+ test.done();
327
+ };