imapkit 4.1.1 → 4.3.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/README.md +86 -1
- package/bin/help.txt +27 -1
- package/bin/imapkit.js +6 -0
- package/dist/cjs/index.d.ts +1 -0
- package/dist/cjs/plugins/auth-plain.js +1 -1
- package/dist/cjs/plugins/idle.js +1 -1
- package/dist/cjs/plugins/oauthbearer.js +1 -1
- package/dist/cjs/script.d.ts +216 -0
- package/dist/cjs/script.js +481 -0
- package/dist/cjs/server.d.ts +107 -1
- package/dist/cjs/server.js +335 -20
- package/dist/cjs/types.d.ts +3 -0
- package/dist/esm/index.d.ts +1 -0
- package/dist/esm/plugins/auth-plain.js +1 -1
- package/dist/esm/plugins/idle.js +1 -1
- package/dist/esm/plugins/oauthbearer.js +1 -1
- package/dist/esm/script.d.ts +216 -0
- package/dist/esm/script.js +473 -0
- package/dist/esm/server.d.ts +107 -1
- package/dist/esm/server.js +335 -20
- package/dist/esm/types.d.ts +3 -0
- package/package.json +2 -2
package/dist/cjs/server.js
CHANGED
|
@@ -54,8 +54,11 @@ const numbers_js_1 = require("./numbers.js");
|
|
|
54
54
|
const arguments_js_1 = require("./arguments.js");
|
|
55
55
|
const append_js_1 = require("./commands/append.js");
|
|
56
56
|
const bundledCert = __importStar(require("./cert.js"));
|
|
57
|
+
const script_js_1 = require("./script.js");
|
|
57
58
|
// longest command line (not counting literals) accepted from a client
|
|
58
59
|
const MAX_LINE_LENGTH = 1024 * 1024;
|
|
60
|
+
// milliseconds between the last output and the RST of a script rule with `close: 'reset'`
|
|
61
|
+
const RESET_DELAY = 20;
|
|
59
62
|
// largest literal accepted after login, override with the maxLiteralSize option
|
|
60
63
|
const MAX_LITERAL_SIZE = 64 * 1024 * 1024;
|
|
61
64
|
// largest literal accepted before login, enough for any user name or password
|
|
@@ -196,6 +199,9 @@ class IMAPServer extends node_stream_1.Stream {
|
|
|
196
199
|
this.referenceNamespace = false;
|
|
197
200
|
// the session whose command is running, see IMAPServer#notify
|
|
198
201
|
this.activeConnection = null;
|
|
202
|
+
this.sessionCounter = 0;
|
|
203
|
+
// rules that make the server misbehave on purpose, from the script option or server.script.add()
|
|
204
|
+
this.script = new script_js_1.ServerScript(this, this.options.script);
|
|
199
205
|
// users and storage are deep copied, so that runtime changes never leak into
|
|
200
206
|
// the caller's objects or into other servers built from the same fixture.
|
|
201
207
|
// Without a prototype, user names like "__proto__" or "toString" are plain keys
|
|
@@ -1254,6 +1260,10 @@ class IMAPConnection {
|
|
|
1254
1260
|
this.socket = socket;
|
|
1255
1261
|
this.options = this.server.options;
|
|
1256
1262
|
this.state = 'Not Authenticated';
|
|
1263
|
+
this.sessionNumber = ++this.server.sessionCounter;
|
|
1264
|
+
this._outputQueue = null;
|
|
1265
|
+
this._outputTimer = null;
|
|
1266
|
+
this._deferredOutput = null;
|
|
1257
1267
|
this.secureConnection = !!this.options.secureConnection;
|
|
1258
1268
|
this._remainder = '';
|
|
1259
1269
|
this._command = '';
|
|
@@ -1283,7 +1293,7 @@ class IMAPConnection {
|
|
|
1283
1293
|
this.notificationQueue = [];
|
|
1284
1294
|
this.server.on('notify', this._notificationCallback);
|
|
1285
1295
|
this.server.connections.add(this);
|
|
1286
|
-
this.
|
|
1296
|
+
this.scriptOutput('greeting', '* OK ImapKit ready for rumble\r\n', {});
|
|
1287
1297
|
}
|
|
1288
1298
|
/**
|
|
1289
1299
|
* Writes protocol output to the client, through the transport layer if there is one
|
|
@@ -1292,12 +1302,139 @@ class IMAPConnection {
|
|
|
1292
1302
|
*/
|
|
1293
1303
|
write(data) {
|
|
1294
1304
|
const buffer = typeof data === 'string' ? Buffer.from(data, 'binary') : data;
|
|
1295
|
-
if (this.
|
|
1296
|
-
this.
|
|
1305
|
+
if (this._outputQueue) {
|
|
1306
|
+
this._outputQueue.push({ data: buffer, transport: this.transport });
|
|
1307
|
+
}
|
|
1308
|
+
else {
|
|
1309
|
+
this.writeLayer(buffer, this.transport);
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
/**
|
|
1313
|
+
* Writes output, or puts it in the output queue while earlier output waits for a delay of a script rule.
|
|
1314
|
+
* The transport layer is taken when the output is queued, so output from before COMPRESS is not compressed
|
|
1315
|
+
*
|
|
1316
|
+
* @param {Object} operation `{ data, delay, chunk, chunkDelay, close }`, see OutputOperation in src/script.ts
|
|
1317
|
+
*/
|
|
1318
|
+
queueOutput(operation) {
|
|
1319
|
+
if (!this._outputQueue && !operation.delay && !operation.chunk) {
|
|
1320
|
+
if (operation.data) {
|
|
1321
|
+
this.writeLayer(operation.data, this.transport);
|
|
1322
|
+
}
|
|
1323
|
+
if (operation.close) {
|
|
1324
|
+
this.closeNow(operation.close);
|
|
1325
|
+
}
|
|
1326
|
+
return;
|
|
1327
|
+
}
|
|
1328
|
+
this._outputQueue = this._outputQueue || [];
|
|
1329
|
+
this._outputQueue.push(Object.assign({}, operation, { transport: this.transport }));
|
|
1330
|
+
if (!this._outputTimer) {
|
|
1331
|
+
this.flushOutput();
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
/**
|
|
1335
|
+
* Writes the output queue until it is empty or a delay stops it
|
|
1336
|
+
*/
|
|
1337
|
+
flushOutput() {
|
|
1338
|
+
const queue = this._outputQueue || [];
|
|
1339
|
+
while (queue.length) {
|
|
1340
|
+
const operation = queue[0];
|
|
1341
|
+
if (operation.delay) {
|
|
1342
|
+
const delay = operation.delay;
|
|
1343
|
+
operation.delay = 0;
|
|
1344
|
+
this._outputTimer = setTimeout(() => {
|
|
1345
|
+
this._outputTimer = null;
|
|
1346
|
+
this.flushOutput();
|
|
1347
|
+
}, delay);
|
|
1348
|
+
return;
|
|
1349
|
+
}
|
|
1350
|
+
if (operation.data && operation.chunk && operation.data.length > operation.chunk) {
|
|
1351
|
+
// the rest waits for chunkDelay, like a delay of its own
|
|
1352
|
+
this.writeLayer(operation.data.subarray(0, operation.chunk), operation.transport || null);
|
|
1353
|
+
operation.data = operation.data.subarray(operation.chunk);
|
|
1354
|
+
operation.delay = operation.chunkDelay;
|
|
1355
|
+
continue;
|
|
1356
|
+
}
|
|
1357
|
+
if (operation.data) {
|
|
1358
|
+
this.writeLayer(operation.data, operation.transport || null);
|
|
1359
|
+
}
|
|
1360
|
+
queue.shift();
|
|
1361
|
+
if (operation.close) {
|
|
1362
|
+
this.closeNow(operation.close);
|
|
1363
|
+
return;
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
this._outputQueue = null;
|
|
1367
|
+
}
|
|
1368
|
+
/**
|
|
1369
|
+
* Drops the output that waits for a delay of a script rule or for its release (`defer`)
|
|
1370
|
+
*/
|
|
1371
|
+
clearOutputQueue() {
|
|
1372
|
+
if (this._outputTimer) {
|
|
1373
|
+
clearTimeout(this._outputTimer);
|
|
1374
|
+
this._outputTimer = null;
|
|
1375
|
+
}
|
|
1376
|
+
this._outputQueue = null;
|
|
1377
|
+
this._deferredOutput = null;
|
|
1378
|
+
}
|
|
1379
|
+
/**
|
|
1380
|
+
* Writes output through a transport layer, or to the socket
|
|
1381
|
+
*
|
|
1382
|
+
* @param {Buffer} data Output
|
|
1383
|
+
* @param {Object|null} transport Transport layer
|
|
1384
|
+
*/
|
|
1385
|
+
writeLayer(data, transport) {
|
|
1386
|
+
if (transport) {
|
|
1387
|
+
transport.write(data);
|
|
1297
1388
|
}
|
|
1298
1389
|
else {
|
|
1299
|
-
this.writeRaw(
|
|
1390
|
+
this.writeRaw(data);
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
/**
|
|
1394
|
+
* Sends output through the script rule that handles its event, if there is one
|
|
1395
|
+
*
|
|
1396
|
+
* @param {String} event Event name: greeting, response or continuation
|
|
1397
|
+
* @param {String} output The output as a binary string
|
|
1398
|
+
* @param {Object} fields Context fields of the event (tag, command, description, response)
|
|
1399
|
+
* @param {Function} [compile] Compiles the response that a `mutate` action changed, null drops the output
|
|
1400
|
+
*/
|
|
1401
|
+
scriptOutput(event, output, fields, compile) {
|
|
1402
|
+
const found = this.server.script.check(this, event, Object.assign({}, fields, { data: output }));
|
|
1403
|
+
if (!found) {
|
|
1404
|
+
this.write(output);
|
|
1405
|
+
return;
|
|
1406
|
+
}
|
|
1407
|
+
const { rule, context } = found;
|
|
1408
|
+
if ((rule.mutate || rule.literals) && compile && context.response) {
|
|
1409
|
+
let response = context.response;
|
|
1410
|
+
if (rule.mutate) {
|
|
1411
|
+
// a copy, a notification object is shared by every session
|
|
1412
|
+
const copy = cloneResponse(response);
|
|
1413
|
+
response = rule.mutate(copy, context) || copy;
|
|
1414
|
+
}
|
|
1415
|
+
const changed = compile(rule.literals ? (0, script_js_1.literalResponse)(response) : response);
|
|
1416
|
+
if (changed === null) {
|
|
1417
|
+
return;
|
|
1418
|
+
}
|
|
1419
|
+
context.data = changed;
|
|
1300
1420
|
}
|
|
1421
|
+
(0, script_js_1.sendOutput)(this, rule, context, Buffer.from(context.data, 'binary'));
|
|
1422
|
+
}
|
|
1423
|
+
/**
|
|
1424
|
+
* Sends a continuation request, `+ text`
|
|
1425
|
+
*
|
|
1426
|
+
* @param {String} text Human readable text, can be empty (SASL)
|
|
1427
|
+
* @param {String} description Description for script rules
|
|
1428
|
+
* @param {Function} [getLine] Returns the command line received so far, for the tag and the command of a literal continuation
|
|
1429
|
+
*/
|
|
1430
|
+
sendContinuation(text, description, getLine) {
|
|
1431
|
+
const output = '+ ' + text + '\r\n';
|
|
1432
|
+
if (!this.server.script.watches('continuation')) {
|
|
1433
|
+
this.write(output);
|
|
1434
|
+
return;
|
|
1435
|
+
}
|
|
1436
|
+
const line = getLine ? getLine() : null;
|
|
1437
|
+
this.scriptOutput('continuation', output, line === null ? { description } : { description, tag: getResponseTag(line), command: getLineCommand(line) });
|
|
1301
1438
|
}
|
|
1302
1439
|
/**
|
|
1303
1440
|
* Writes data to the socket, below the transport layer
|
|
@@ -1327,12 +1464,47 @@ class IMAPConnection {
|
|
|
1327
1464
|
* holds, is written out
|
|
1328
1465
|
*/
|
|
1329
1466
|
end() {
|
|
1467
|
+
if (this._outputQueue && this.socket) {
|
|
1468
|
+
// output still waits for a delay of a script rule
|
|
1469
|
+
this._closing = true;
|
|
1470
|
+
this._outputQueue.push({ close: true });
|
|
1471
|
+
return;
|
|
1472
|
+
}
|
|
1473
|
+
this.closeNow(true);
|
|
1474
|
+
}
|
|
1475
|
+
/**
|
|
1476
|
+
* Closes the connection now, after the output written so far
|
|
1477
|
+
*
|
|
1478
|
+
* @param {Boolean|String} mode true ends the connection gracefully, "reset" destroys the socket (script rules)
|
|
1479
|
+
*/
|
|
1480
|
+
closeNow(mode) {
|
|
1330
1481
|
const socket = this.socket;
|
|
1331
1482
|
if (!socket) {
|
|
1332
1483
|
return;
|
|
1333
1484
|
}
|
|
1334
1485
|
this._closing = true;
|
|
1335
|
-
|
|
1486
|
+
this.clearOutputQueue();
|
|
1487
|
+
if (mode === 'reset') {
|
|
1488
|
+
this.discardInput();
|
|
1489
|
+
// input that arrives while the reset waits is not processed, also input a transport layer still holds
|
|
1490
|
+
this._resetting = true;
|
|
1491
|
+
socket.pause();
|
|
1492
|
+
// a RST sent in the same tick as the last output can get lost: on macOS the client received the output but
|
|
1493
|
+
// never the RST (#81). Node has no signal for output that reached the peer, so the RST waits a little
|
|
1494
|
+
setTimeout(() => {
|
|
1495
|
+
if (socket.destroyed) {
|
|
1496
|
+
return;
|
|
1497
|
+
}
|
|
1498
|
+
// resetAndDestroy sends a TCP RST (Node 16.17), not every runtime has it
|
|
1499
|
+
if (typeof socket.resetAndDestroy === 'function') {
|
|
1500
|
+
socket.resetAndDestroy();
|
|
1501
|
+
}
|
|
1502
|
+
else {
|
|
1503
|
+
socket.destroy();
|
|
1504
|
+
}
|
|
1505
|
+
}, RESET_DELAY);
|
|
1506
|
+
}
|
|
1507
|
+
else if (this.transport) {
|
|
1336
1508
|
this.transport.end(() => socket.end());
|
|
1337
1509
|
}
|
|
1338
1510
|
else {
|
|
@@ -1433,6 +1605,7 @@ class IMAPConnection {
|
|
|
1433
1605
|
this.transport.destroy();
|
|
1434
1606
|
this.transport = null;
|
|
1435
1607
|
}
|
|
1608
|
+
this.clearOutputQueue();
|
|
1436
1609
|
this.server.removeListener('notify', this._notificationCallback);
|
|
1437
1610
|
this.server.connections.delete(this);
|
|
1438
1611
|
}
|
|
@@ -1448,9 +1621,27 @@ class IMAPConnection {
|
|
|
1448
1621
|
// socket is already gone
|
|
1449
1622
|
}
|
|
1450
1623
|
}
|
|
1624
|
+
/**
|
|
1625
|
+
* Passes a line to the input handler (IDLE, AUTHENTICATE), unless a script rule handles it
|
|
1626
|
+
*
|
|
1627
|
+
* @param {String} line Input line without CRLF
|
|
1628
|
+
*/
|
|
1629
|
+
handleInput(line) {
|
|
1630
|
+
const inputHandler = this.inputHandler;
|
|
1631
|
+
const found = this.server.script.check(this, 'input', { data: line });
|
|
1632
|
+
if (found) {
|
|
1633
|
+
(0, script_js_1.handleLine)(this, found.rule, found.context, () => inputHandler(line));
|
|
1634
|
+
}
|
|
1635
|
+
else {
|
|
1636
|
+
inputHandler(line);
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1451
1639
|
onData(chunk) {
|
|
1452
1640
|
let match;
|
|
1453
1641
|
let str;
|
|
1642
|
+
if (this._resetting) {
|
|
1643
|
+
return;
|
|
1644
|
+
}
|
|
1454
1645
|
// everything in one read arrived before anything this read causes to be sent
|
|
1455
1646
|
this._readCount = (this._readCount || 0) + 1;
|
|
1456
1647
|
str = (chunk || '').toString('binary');
|
|
@@ -1519,7 +1710,7 @@ class IMAPConnection {
|
|
|
1519
1710
|
this.sendBad(getResponseTag(line), 'Literal data must wait for the continuation request', 'LITERAL TOO EARLY', line);
|
|
1520
1711
|
}
|
|
1521
1712
|
else if (this.inputHandler) {
|
|
1522
|
-
this.
|
|
1713
|
+
this.handleInput(line);
|
|
1523
1714
|
}
|
|
1524
1715
|
else {
|
|
1525
1716
|
this.scheduleCommand(line);
|
|
@@ -1568,7 +1759,9 @@ class IMAPConnection {
|
|
|
1568
1759
|
this._earlyLiteral = true;
|
|
1569
1760
|
}
|
|
1570
1761
|
else if (!this._earlyLiteral) {
|
|
1571
|
-
|
|
1762
|
+
// the line is needed only by script rules that watch continuations
|
|
1763
|
+
const head = str.substr(0, match.index) + marker;
|
|
1764
|
+
this.sendContinuation('Go ahead', 'LITERAL', () => this._command + head);
|
|
1572
1765
|
}
|
|
1573
1766
|
}
|
|
1574
1767
|
this._remainder = '';
|
|
@@ -1644,12 +1837,7 @@ class IMAPConnection {
|
|
|
1644
1837
|
// not a command, e.g. a SASL response
|
|
1645
1838
|
return literal8 ? refuse('Literal8 is not allowed here') : false;
|
|
1646
1839
|
}
|
|
1647
|
-
|
|
1648
|
-
const words = line.match(/^[^ ]* ([^ ]*)(?: ([^ ]*))?/);
|
|
1649
|
-
let command = ((words && words[1]) || '').toUpperCase();
|
|
1650
|
-
if (command === 'UID' || command === 'AUTHENTICATE') {
|
|
1651
|
-
command += ' ' + ((words && words[2]) || '').toUpperCase();
|
|
1652
|
-
}
|
|
1840
|
+
const command = getLineCommand(line);
|
|
1653
1841
|
if (!COMMAND_REGEX.test(command) || !this.server.getCommandHandler(command)) {
|
|
1654
1842
|
return refuse('Unknown command');
|
|
1655
1843
|
}
|
|
@@ -2069,6 +2257,44 @@ class IMAPConnection {
|
|
|
2069
2257
|
});
|
|
2070
2258
|
}
|
|
2071
2259
|
}
|
|
2260
|
+
const compiled = this.compileResponse(response);
|
|
2261
|
+
if (compiled === null) {
|
|
2262
|
+
return;
|
|
2263
|
+
}
|
|
2264
|
+
if (this._deferredOutput) {
|
|
2265
|
+
// responses that `defer` rules hold back go before the first response of another command
|
|
2266
|
+
(0, script_js_1.releaseDeferred)(this, parsed && parsed.command && parsed.tag && parsed.tag !== '*' ? parsed.tag : null, false);
|
|
2267
|
+
}
|
|
2268
|
+
this.scriptResponse(response, compiled, description, parsed);
|
|
2269
|
+
if (this._deferredOutput && response.tag !== '*' && response.tag !== '+') {
|
|
2270
|
+
(0, script_js_1.releaseDeferred)(this, response.tag, true);
|
|
2271
|
+
}
|
|
2272
|
+
}
|
|
2273
|
+
/**
|
|
2274
|
+
* Sends a compiled response through the script rules that watch responses or continuations
|
|
2275
|
+
*
|
|
2276
|
+
* @param {Object} response Response object
|
|
2277
|
+
* @param {String} compiled The compiled response
|
|
2278
|
+
* @param {String} [description] Description of the response
|
|
2279
|
+
* @param {Object} [parsed] The command the response belongs to
|
|
2280
|
+
*/
|
|
2281
|
+
scriptResponse(response, compiled, description, parsed) {
|
|
2282
|
+
const event = response.tag === '+' ? 'continuation' : 'response';
|
|
2283
|
+
if (!this.server.script.watches(event)) {
|
|
2284
|
+
this.write(compiled);
|
|
2285
|
+
return;
|
|
2286
|
+
}
|
|
2287
|
+
// script rules see the response after every plugin and the core changed it. Without a command, an
|
|
2288
|
+
// unsolicited response belongs to the command that runs or idles
|
|
2289
|
+
this.scriptOutput(event, compiled, Object.assign({ description: description || null, response }, parsed && parsed.command ? { tag: parsed.tag || null, command: String(parsed.command).toUpperCase() } : {}), output => this.compileResponse(output));
|
|
2290
|
+
}
|
|
2291
|
+
/**
|
|
2292
|
+
* Compiles a response for the wire
|
|
2293
|
+
*
|
|
2294
|
+
* @param {Object} response Response object
|
|
2295
|
+
* @return {String|null} the response with its CRLF as a binary string, or null for an untagged response that does not compile
|
|
2296
|
+
*/
|
|
2297
|
+
compileResponse(response) {
|
|
2072
2298
|
let compiled;
|
|
2073
2299
|
try {
|
|
2074
2300
|
compiled = imap_handler_1.default.compiler(response, this.compilerOptions);
|
|
@@ -2079,14 +2305,14 @@ class IMAPConnection {
|
|
|
2079
2305
|
console.log('Failed to compile response: %s', err.message);
|
|
2080
2306
|
}
|
|
2081
2307
|
if (response.tag === '*') {
|
|
2082
|
-
return;
|
|
2308
|
+
return null;
|
|
2083
2309
|
}
|
|
2084
2310
|
compiled = response.tag + ' NO [SERVERBUG] Failed to compile response';
|
|
2085
2311
|
}
|
|
2086
2312
|
if (this.options.debug) {
|
|
2087
2313
|
console.log('SEND: %s', compiled);
|
|
2088
2314
|
}
|
|
2089
|
-
|
|
2315
|
+
return compiled + '\r\n';
|
|
2090
2316
|
}
|
|
2091
2317
|
/**
|
|
2092
2318
|
* Sends a tagged status response to a command
|
|
@@ -2273,9 +2499,23 @@ class IMAPConnection {
|
|
|
2273
2499
|
exportMailboxName(path) {
|
|
2274
2500
|
return path;
|
|
2275
2501
|
}
|
|
2276
|
-
|
|
2502
|
+
/**
|
|
2503
|
+
* Parses a command line and queues the command, or answers it right away when it can not run
|
|
2504
|
+
*
|
|
2505
|
+
* @param {String} data Command line with its literals, without the final CRLF
|
|
2506
|
+
* @param {Boolean} [scripted] The line comes from a script rule with `run`, it is next in the queue
|
|
2507
|
+
*/
|
|
2508
|
+
scheduleCommand(data, scripted) {
|
|
2277
2509
|
let parsed;
|
|
2278
2510
|
const tag = getResponseTag(data);
|
|
2511
|
+
// the rule is chosen when the line arrives, the state it matches is the state at that moment. The rule
|
|
2512
|
+
// acts when the command's turn comes, so that its output keeps the order of the responses
|
|
2513
|
+
const found = !scripted && this.server.script.watches('command') ? this.server.script.check(this, 'command', { data, tag, command: getLineCommand(data) }) : null;
|
|
2514
|
+
if (found) {
|
|
2515
|
+
this._commandQueue.push({ parsed: { tag, command: found.context.command || '' }, data, script: found });
|
|
2516
|
+
this.processQueue();
|
|
2517
|
+
return;
|
|
2518
|
+
}
|
|
2279
2519
|
try {
|
|
2280
2520
|
// server.parserOptions are the defaults of plugins, connection.parserOptions win
|
|
2281
2521
|
parsed = imap_handler_1.default.parser(data, Object.assign({ literalPlus: this.server.literalPlus }, this.server.parserOptions, this.parserOptions));
|
|
@@ -2322,10 +2562,14 @@ class IMAPConnection {
|
|
|
2322
2562
|
this.sendStatus(parsed, data, 'BAD', 'Commands with message sequence numbers must wait for the completion of earlier commands');
|
|
2323
2563
|
return;
|
|
2324
2564
|
}
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2565
|
+
const element = { parsed, data };
|
|
2566
|
+
if (scripted) {
|
|
2567
|
+
// processQueue runs it once the script rule released the queue
|
|
2568
|
+
this._commandQueue.unshift(element);
|
|
2569
|
+
}
|
|
2570
|
+
else {
|
|
2571
|
+
this._commandQueue.push(element);
|
|
2572
|
+
}
|
|
2329
2573
|
this.processQueue();
|
|
2330
2574
|
}
|
|
2331
2575
|
else if (/^AUTHENTICATE /i.test(parsed.command)) {
|
|
@@ -2354,6 +2598,33 @@ class IMAPConnection {
|
|
|
2354
2598
|
}, 'UNKNOWN COMMAND', parsed, data);
|
|
2355
2599
|
}
|
|
2356
2600
|
}
|
|
2601
|
+
/**
|
|
2602
|
+
* Handles a command line with the script rule that matched it, after the rule's delay. With `run` the line
|
|
2603
|
+
* goes through the parser and the command handler as usual afterwards
|
|
2604
|
+
*
|
|
2605
|
+
* @param {Object} element Queued command with the rule
|
|
2606
|
+
* @param {Function} next Releases the queue
|
|
2607
|
+
*/
|
|
2608
|
+
runScriptedCommand(element, next) {
|
|
2609
|
+
const { rule, context } = element.script;
|
|
2610
|
+
const act = () => {
|
|
2611
|
+
if (!this.socket || this._closing) {
|
|
2612
|
+
return next();
|
|
2613
|
+
}
|
|
2614
|
+
(0, script_js_1.handleLine)(this, rule, context, () => {
|
|
2615
|
+
// the line is not running yet, it must not count as an earlier command (RFC 3501 section 5.5)
|
|
2616
|
+
this._runningCommand = null;
|
|
2617
|
+
this.scheduleCommand(element.data, true);
|
|
2618
|
+
});
|
|
2619
|
+
next();
|
|
2620
|
+
};
|
|
2621
|
+
if (rule.delay) {
|
|
2622
|
+
setTimeout(act, rule.delay);
|
|
2623
|
+
}
|
|
2624
|
+
else {
|
|
2625
|
+
act();
|
|
2626
|
+
}
|
|
2627
|
+
}
|
|
2357
2628
|
processQueue(force) {
|
|
2358
2629
|
if (!force && this._processing) {
|
|
2359
2630
|
return;
|
|
@@ -2386,6 +2657,10 @@ class IMAPConnection {
|
|
|
2386
2657
|
this.processQueue(true);
|
|
2387
2658
|
}
|
|
2388
2659
|
};
|
|
2660
|
+
if (element.script) {
|
|
2661
|
+
this.runScriptedCommand(element, next);
|
|
2662
|
+
return;
|
|
2663
|
+
}
|
|
2389
2664
|
if (options.states && options.states.indexOf(this.state) < 0) {
|
|
2390
2665
|
this.sendStatus(element.parsed, element.data, 'BAD', stateError(command, this.state));
|
|
2391
2666
|
return next();
|
|
@@ -2428,7 +2703,12 @@ class IMAPConnection {
|
|
|
2428
2703
|
try {
|
|
2429
2704
|
// changes made while the handler runs are attributed to this session (the `origin` of notifications)
|
|
2430
2705
|
this.server.activeConnection = this;
|
|
2706
|
+
const inputHandler = this.inputHandler;
|
|
2431
2707
|
this.server.getCommandHandler(element.parsed.command)(this, element.parsed, element.data, next);
|
|
2708
|
+
if (this.inputHandler && this.inputHandler !== inputHandler) {
|
|
2709
|
+
// the command reads the lines that follow (IDLE, AUTHENTICATE), script rules match them with it
|
|
2710
|
+
this.inputCommand = { tag: element.parsed.tag, command: element.parsed.command };
|
|
2711
|
+
}
|
|
2432
2712
|
}
|
|
2433
2713
|
catch (E) {
|
|
2434
2714
|
const ex = E;
|
|
@@ -2550,6 +2830,41 @@ class IMAPConnection {
|
|
|
2550
2830
|
}
|
|
2551
2831
|
}
|
|
2552
2832
|
exports.IMAPConnection = IMAPConnection;
|
|
2833
|
+
/**
|
|
2834
|
+
* Copies a response tree for the `mutate` action of a script rule: arrays and plain objects are copied,
|
|
2835
|
+
* other values (Buffers) are shared
|
|
2836
|
+
*
|
|
2837
|
+
* @param {*} value Response or a part of it
|
|
2838
|
+
* @return {*} Copy
|
|
2839
|
+
*/
|
|
2840
|
+
function cloneResponse(value) {
|
|
2841
|
+
if (Array.isArray(value)) {
|
|
2842
|
+
return value.map(cloneResponse);
|
|
2843
|
+
}
|
|
2844
|
+
if (value && typeof value === 'object' && Object.getPrototypeOf(value) === Object.prototype) {
|
|
2845
|
+
const copy = {};
|
|
2846
|
+
for (const key of Object.keys(value)) {
|
|
2847
|
+
copy[key] = cloneResponse(value[key]);
|
|
2848
|
+
}
|
|
2849
|
+
return copy;
|
|
2850
|
+
}
|
|
2851
|
+
return value;
|
|
2852
|
+
}
|
|
2853
|
+
/**
|
|
2854
|
+
* Finds the command name in a command line that may not parse: tag SP command, and for UID and AUTHENTICATE
|
|
2855
|
+
* the word that follows
|
|
2856
|
+
*
|
|
2857
|
+
* @param {String} line Command line
|
|
2858
|
+
* @return {String} Command name in upper case, can be empty
|
|
2859
|
+
*/
|
|
2860
|
+
function getLineCommand(line) {
|
|
2861
|
+
const words = line.match(/^[^ ]* ([^ ]*)(?: ([^ ]*))?/);
|
|
2862
|
+
let command = ((words && words[1]) || '').toUpperCase();
|
|
2863
|
+
if (command === 'UID' || command === 'AUTHENTICATE') {
|
|
2864
|
+
command += ' ' + ((words && words[2]) || '').toUpperCase();
|
|
2865
|
+
}
|
|
2866
|
+
return command;
|
|
2867
|
+
}
|
|
2553
2868
|
/**
|
|
2554
2869
|
* Formats a mailbox name for a response: an atom when possible, otherwise a string. NIL and names
|
|
2555
2870
|
* like \\Foo would not read back as mailbox names, so these are strings as well
|
package/dist/cjs/types.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { IMAPServer, IMAPConnection } from './server.js';
|
|
2
|
+
import type { ScriptRule } from './script.js';
|
|
2
3
|
export type { IMAPServer, IMAPConnection };
|
|
3
4
|
/**
|
|
4
5
|
* A value of the parsed command or of a response, as imap-handler parses and compiles it: an
|
|
@@ -229,6 +230,8 @@ export interface IMAPServerOptions {
|
|
|
229
230
|
systemFlags?: string[] | undefined;
|
|
230
231
|
/** largest literal accepted after login, in octets */
|
|
231
232
|
maxLiteralSize?: number | undefined;
|
|
233
|
+
/** script rules that make the server misbehave on purpose, see src/script.ts and README "Scripted faults" */
|
|
234
|
+
script?: ScriptRule | ScriptRule[] | undefined;
|
|
232
235
|
[key: string]: any;
|
|
233
236
|
}
|
|
234
237
|
/**
|
package/dist/esm/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import createServer, { TAG_REGEX, IMAPServer, IMAPConnection } from './server.js';
|
|
2
2
|
export { TAG_REGEX, IMAPServer, IMAPConnection };
|
|
3
3
|
export type { Attribute, ParsedCommand, IMAPResponse, Notification, Callback, CommandHandler, CommandOptions, Plugin, IMAPError, Message, Mailbox, StorageNamespace, UserData, IMAPServerOptions } from './types.js';
|
|
4
|
+
export type { ScriptRule, ScriptContext, ScriptEvent, ScriptBytes, ScriptHandle } from './script.js';
|
|
4
5
|
declare const imapkit: typeof createServer & {
|
|
5
6
|
TAG_REGEX: RegExp;
|
|
6
7
|
IMAPServer: typeof IMAPServer;
|
|
@@ -54,7 +54,7 @@ export default function authPlainPlugin(server) {
|
|
|
54
54
|
authenticate(connection, parsed, data, str);
|
|
55
55
|
};
|
|
56
56
|
// Send an empty continuation request to the client
|
|
57
|
-
connection.
|
|
57
|
+
connection.sendContinuation('', 'AUTHENTICATE PLAIN');
|
|
58
58
|
}
|
|
59
59
|
else if (parsed.attributes.length === 1 &&
|
|
60
60
|
// second argument must be Base64 string as ATOM
|
package/dist/esm/plugins/idle.js
CHANGED
|
@@ -63,7 +63,7 @@ export default function idlePlugin(server) {
|
|
|
63
63
|
}, 'INVALID IDLE', parsed, data);
|
|
64
64
|
}
|
|
65
65
|
};
|
|
66
|
-
connection.
|
|
66
|
+
connection.sendContinuation('idling', 'IDLE');
|
|
67
67
|
connection.processNotifications();
|
|
68
68
|
return callback();
|
|
69
69
|
}, { states: states.AUTHENTICATED, noArguments: true });
|
|
@@ -165,7 +165,7 @@ export default function oauthbearerPlugin(server) {
|
|
|
165
165
|
if (!args.length) {
|
|
166
166
|
// without an initial response the client sends its response after an empty challenge
|
|
167
167
|
readResponse(connection, parsed, data, decoded => authenticate(connection, parsed, data, decoded));
|
|
168
|
-
connection.
|
|
168
|
+
connection.sendContinuation('', 'AUTHENTICATE OAUTHBEARER');
|
|
169
169
|
return callback();
|
|
170
170
|
}
|
|
171
171
|
if (args.length !== 1 || !args[0] || args[0].type !== 'ATOM') {
|