UNPKG

imapflow

Version:

IMAP Client for Node

155 lines (136 loc) 5.47 kB
'use strict'; // End-to-end test of the DEFLATE compression path (RFC 4978). The mock server // negotiates COMPRESS and then speaks raw-deflate in both directions, mirroring // the client's inflate/deflate pipeline. This covers ImapFlow.compress() and the // compression-stream cleanup inside close(). const net = require('net'); const zlib = require('zlib'); const { ImapFlow } = require('../lib/imap-flow'); const CAPS = 'IMAP4rev1 ID ENABLE NAMESPACE COMPRESS=DEFLATE'; const listen = server => new Promise(resolve => server.listen(0, '127.0.0.1', () => resolve(server.address().port))); // IMAP session line responder shared before and after compression. const respond = (write, line) => { let parts = line.split(' '); let tag = parts[0]; let cmd = (parts[1] || '').toUpperCase(); switch (cmd) { case 'CAPABILITY': write(`* CAPABILITY ${CAPS}\r\n${tag} OK done\r\n`); break; case 'ID': write(`* ID ("name" "mock" "version" "1")\r\n${tag} OK done\r\n`); break; case 'LOGIN': write(`${tag} OK LOGIN done\r\n`); break; case 'NAMESPACE': write(`* NAMESPACE (("" "/")) NIL NIL\r\n${tag} OK done\r\n`); break; case 'ENABLE': write(`${tag} OK done\r\n`); break; case 'NOOP': write(`${tag} OK NOOP done\r\n`); break; case 'LOGOUT': write(`* BYE bye\r\n${tag} OK done\r\n`); break; default: write(`${tag} BAD unknown ${cmd}\r\n`); } }; module.exports['Compress: session negotiates DEFLATE and runs commands'] = async test => { let server = net.createServer(socket => { socket.on('error', () => {}); let compressed = false; let deflate = null; let inflate = null; let buf = ''; // Plain writer (pre-compression) let writePlain = str => socket.write(str); // Compressed writer: deflate + flush so the client receives complete frames let writeDeflate = str => { deflate.write(Buffer.from(str, 'binary')); deflate.flush(); }; // function declarations (hoisted) because feed() and onLine() are mutually recursive let lineBuf = ''; function onLine(line) { let parts = line.split(' '); let tag = parts[0]; let cmd = (parts[1] || '').toUpperCase(); if (cmd === 'COMPRESS') { // Acknowledge, then switch both directions to raw deflate writePlain(`${tag} OK Begin compression\r\n`); compressed = true; deflate = zlib.createDeflateRaw(); inflate = zlib.createInflateRaw(); deflate.pipe(socket); inflate.on('data', d => feed(d.toString('binary'))); inflate.on('error', () => {}); deflate.on('error', () => {}); return; } respond(compressed ? writeDeflate : writePlain, line); } function feed(chunk) { lineBuf += chunk; let idx; while ((idx = lineBuf.indexOf('\r\n')) >= 0) { let line = lineBuf.slice(0, idx); lineBuf = lineBuf.slice(idx + 2); onLine(line); } } socket.on('data', data => { if (compressed) { // After COMPRESS negotiation, all inbound bytes are deflated inflate.write(data); } else { buf += data.toString('binary'); let idx; while ((idx = buf.indexOf('\r\n')) >= 0) { let line = buf.slice(0, idx); buf = buf.slice(idx + 2); // hand off any already-buffered bytes after COMPRESS to inflate onLine(line); if (compressed && buf.length) { inflate.write(Buffer.from(buf, 'binary')); buf = ''; } } } }); socket.write(`* OK [CAPABILITY ${CAPS}] ready\r\n`); }); let port = await listen(server); let client = new ImapFlow({ host: '127.0.0.1', port, secure: false, disableAutoIdle: true, disableCompression: false, // enable COMPRESS negotiation logger: false, auth: { user: 'test', pass: 'secret' } }); client.on('error', () => {}); await client.connect(); test.ok(client.usable); test.ok(client._deflate, 'deflate pipeline established'); test.ok(client._inflate, 'inflate pipeline established'); // Run a command through the compressed channel await client.noop(); // Push a buffer larger than the deflate writable highWaterMark (64KB) straight // through the compression pump so its buffer overflows and readNext waits for 'drain'. client.state = client.states.AUTHENTICATED; client.commandParts = []; client.write(Buffer.alloc(128 * 1024, 0x61)); await new Promise(r => setTimeout(r, 50)); await client.logout(); client.close(); // close() must tear down the compression streams test.ok(!client._deflate, 'deflate cleaned up after close'); test.ok(!client._inflate, 'inflate cleaned up after close'); server.close(); test.done(); };