imapkit 4.1.1 → 4.2.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 +89 -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 +183 -0
- package/dist/cjs/script.js +332 -0
- package/dist/cjs/server.d.ts +94 -1
- package/dist/cjs/server.js +296 -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 +183 -0
- package/dist/esm/script.js +326 -0
- package/dist/esm/server.d.ts +94 -1
- package/dist/esm/server.js +296 -20
- package/dist/esm/types.d.ts +3 -0
- package/package.json +2 -2
package/dist/cjs/server.js
CHANGED
|
@@ -54,6 +54,7 @@ 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;
|
|
59
60
|
// largest literal accepted after login, override with the maxLiteralSize option
|
|
@@ -196,6 +197,9 @@ class IMAPServer extends node_stream_1.Stream {
|
|
|
196
197
|
this.referenceNamespace = false;
|
|
197
198
|
// the session whose command is running, see IMAPServer#notify
|
|
198
199
|
this.activeConnection = null;
|
|
200
|
+
this.sessionCounter = 0;
|
|
201
|
+
// rules that make the server misbehave on purpose, from the script option or server.script.add()
|
|
202
|
+
this.script = new script_js_1.ServerScript(this, this.options.script);
|
|
199
203
|
// users and storage are deep copied, so that runtime changes never leak into
|
|
200
204
|
// the caller's objects or into other servers built from the same fixture.
|
|
201
205
|
// Without a prototype, user names like "__proto__" or "toString" are plain keys
|
|
@@ -1254,6 +1258,9 @@ class IMAPConnection {
|
|
|
1254
1258
|
this.socket = socket;
|
|
1255
1259
|
this.options = this.server.options;
|
|
1256
1260
|
this.state = 'Not Authenticated';
|
|
1261
|
+
this.sessionNumber = ++this.server.sessionCounter;
|
|
1262
|
+
this._outputQueue = null;
|
|
1263
|
+
this._outputTimer = null;
|
|
1257
1264
|
this.secureConnection = !!this.options.secureConnection;
|
|
1258
1265
|
this._remainder = '';
|
|
1259
1266
|
this._command = '';
|
|
@@ -1283,7 +1290,7 @@ class IMAPConnection {
|
|
|
1283
1290
|
this.notificationQueue = [];
|
|
1284
1291
|
this.server.on('notify', this._notificationCallback);
|
|
1285
1292
|
this.server.connections.add(this);
|
|
1286
|
-
this.
|
|
1293
|
+
this.scriptOutput('greeting', '* OK ImapKit ready for rumble\r\n', {});
|
|
1287
1294
|
}
|
|
1288
1295
|
/**
|
|
1289
1296
|
* Writes protocol output to the client, through the transport layer if there is one
|
|
@@ -1292,12 +1299,134 @@ class IMAPConnection {
|
|
|
1292
1299
|
*/
|
|
1293
1300
|
write(data) {
|
|
1294
1301
|
const buffer = typeof data === 'string' ? Buffer.from(data, 'binary') : data;
|
|
1295
|
-
if (this.
|
|
1296
|
-
this.
|
|
1302
|
+
if (this._outputQueue) {
|
|
1303
|
+
this._outputQueue.push({ data: buffer, transport: this.transport });
|
|
1304
|
+
}
|
|
1305
|
+
else {
|
|
1306
|
+
this.writeLayer(buffer, this.transport);
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
/**
|
|
1310
|
+
* Writes output, or puts it in the output queue while earlier output waits for a delay of a script rule.
|
|
1311
|
+
* The transport layer is taken when the output is queued, so output from before COMPRESS is not compressed
|
|
1312
|
+
*
|
|
1313
|
+
* @param {Object} operation `{ data, delay, chunk, chunkDelay, close }`, see OutputOperation in src/script.ts
|
|
1314
|
+
*/
|
|
1315
|
+
queueOutput(operation) {
|
|
1316
|
+
if (!this._outputQueue && !operation.delay && !operation.chunk) {
|
|
1317
|
+
if (operation.data) {
|
|
1318
|
+
this.writeLayer(operation.data, this.transport);
|
|
1319
|
+
}
|
|
1320
|
+
if (operation.close) {
|
|
1321
|
+
this.closeNow(operation.close);
|
|
1322
|
+
}
|
|
1323
|
+
return;
|
|
1324
|
+
}
|
|
1325
|
+
this._outputQueue = this._outputQueue || [];
|
|
1326
|
+
this._outputQueue.push(Object.assign({}, operation, { transport: this.transport }));
|
|
1327
|
+
if (!this._outputTimer) {
|
|
1328
|
+
this.flushOutput();
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
/**
|
|
1332
|
+
* Writes the output queue until it is empty or a delay stops it
|
|
1333
|
+
*/
|
|
1334
|
+
flushOutput() {
|
|
1335
|
+
const queue = this._outputQueue || [];
|
|
1336
|
+
while (queue.length) {
|
|
1337
|
+
const operation = queue[0];
|
|
1338
|
+
if (operation.delay) {
|
|
1339
|
+
const delay = operation.delay;
|
|
1340
|
+
operation.delay = 0;
|
|
1341
|
+
this._outputTimer = setTimeout(() => {
|
|
1342
|
+
this._outputTimer = null;
|
|
1343
|
+
this.flushOutput();
|
|
1344
|
+
}, delay);
|
|
1345
|
+
return;
|
|
1346
|
+
}
|
|
1347
|
+
if (operation.data && operation.chunk && operation.data.length > operation.chunk) {
|
|
1348
|
+
// the rest waits for chunkDelay, like a delay of its own
|
|
1349
|
+
this.writeLayer(operation.data.subarray(0, operation.chunk), operation.transport || null);
|
|
1350
|
+
operation.data = operation.data.subarray(operation.chunk);
|
|
1351
|
+
operation.delay = operation.chunkDelay;
|
|
1352
|
+
continue;
|
|
1353
|
+
}
|
|
1354
|
+
if (operation.data) {
|
|
1355
|
+
this.writeLayer(operation.data, operation.transport || null);
|
|
1356
|
+
}
|
|
1357
|
+
queue.shift();
|
|
1358
|
+
if (operation.close) {
|
|
1359
|
+
this.closeNow(operation.close);
|
|
1360
|
+
return;
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
this._outputQueue = null;
|
|
1364
|
+
}
|
|
1365
|
+
/**
|
|
1366
|
+
* Drops the output that waits for a delay of a script rule
|
|
1367
|
+
*/
|
|
1368
|
+
clearOutputQueue() {
|
|
1369
|
+
if (this._outputTimer) {
|
|
1370
|
+
clearTimeout(this._outputTimer);
|
|
1371
|
+
this._outputTimer = null;
|
|
1372
|
+
}
|
|
1373
|
+
this._outputQueue = null;
|
|
1374
|
+
}
|
|
1375
|
+
/**
|
|
1376
|
+
* Writes output through a transport layer, or to the socket
|
|
1377
|
+
*
|
|
1378
|
+
* @param {Buffer} data Output
|
|
1379
|
+
* @param {Object|null} transport Transport layer
|
|
1380
|
+
*/
|
|
1381
|
+
writeLayer(data, transport) {
|
|
1382
|
+
if (transport) {
|
|
1383
|
+
transport.write(data);
|
|
1297
1384
|
}
|
|
1298
1385
|
else {
|
|
1299
|
-
this.writeRaw(
|
|
1386
|
+
this.writeRaw(data);
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
/**
|
|
1390
|
+
* Sends output through the script rule that handles its event, if there is one
|
|
1391
|
+
*
|
|
1392
|
+
* @param {String} event Event name: greeting, response or continuation
|
|
1393
|
+
* @param {String} output The output as a binary string
|
|
1394
|
+
* @param {Object} fields Context fields of the event (tag, command, description, response)
|
|
1395
|
+
* @param {Function} [compile] Compiles the response that a `mutate` action changed, null drops the output
|
|
1396
|
+
*/
|
|
1397
|
+
scriptOutput(event, output, fields, compile) {
|
|
1398
|
+
const found = this.server.script.check(this, event, Object.assign({}, fields, { data: output }));
|
|
1399
|
+
if (!found) {
|
|
1400
|
+
this.write(output);
|
|
1401
|
+
return;
|
|
1402
|
+
}
|
|
1403
|
+
const { rule, context } = found;
|
|
1404
|
+
if (rule.mutate && compile && context.response) {
|
|
1405
|
+
// a copy, a notification object is shared by every session
|
|
1406
|
+
const copy = cloneResponse(context.response);
|
|
1407
|
+
const changed = compile(rule.mutate(copy, context) || copy);
|
|
1408
|
+
if (changed === null) {
|
|
1409
|
+
return;
|
|
1410
|
+
}
|
|
1411
|
+
context.data = changed;
|
|
1412
|
+
}
|
|
1413
|
+
(0, script_js_1.sendOutput)(this, rule, context, Buffer.from(context.data, 'binary'));
|
|
1414
|
+
}
|
|
1415
|
+
/**
|
|
1416
|
+
* Sends a continuation request, `+ text`
|
|
1417
|
+
*
|
|
1418
|
+
* @param {String} text Human readable text, can be empty (SASL)
|
|
1419
|
+
* @param {String} description Description for script rules
|
|
1420
|
+
* @param {Function} [getLine] Returns the command line received so far, for the tag and the command of a literal continuation
|
|
1421
|
+
*/
|
|
1422
|
+
sendContinuation(text, description, getLine) {
|
|
1423
|
+
const output = '+ ' + text + '\r\n';
|
|
1424
|
+
if (!this.server.script.watches('continuation')) {
|
|
1425
|
+
this.write(output);
|
|
1426
|
+
return;
|
|
1300
1427
|
}
|
|
1428
|
+
const line = getLine ? getLine() : null;
|
|
1429
|
+
this.scriptOutput('continuation', output, line === null ? { description } : { description, tag: getResponseTag(line), command: getLineCommand(line) });
|
|
1301
1430
|
}
|
|
1302
1431
|
/**
|
|
1303
1432
|
* Writes data to the socket, below the transport layer
|
|
@@ -1327,12 +1456,37 @@ class IMAPConnection {
|
|
|
1327
1456
|
* holds, is written out
|
|
1328
1457
|
*/
|
|
1329
1458
|
end() {
|
|
1459
|
+
if (this._outputQueue && this.socket) {
|
|
1460
|
+
// output still waits for a delay of a script rule
|
|
1461
|
+
this._closing = true;
|
|
1462
|
+
this._outputQueue.push({ close: true });
|
|
1463
|
+
return;
|
|
1464
|
+
}
|
|
1465
|
+
this.closeNow(true);
|
|
1466
|
+
}
|
|
1467
|
+
/**
|
|
1468
|
+
* Closes the connection now, after the output written so far
|
|
1469
|
+
*
|
|
1470
|
+
* @param {Boolean|String} mode true ends the connection gracefully, "reset" destroys the socket (script rules)
|
|
1471
|
+
*/
|
|
1472
|
+
closeNow(mode) {
|
|
1330
1473
|
const socket = this.socket;
|
|
1331
1474
|
if (!socket) {
|
|
1332
1475
|
return;
|
|
1333
1476
|
}
|
|
1334
1477
|
this._closing = true;
|
|
1335
|
-
|
|
1478
|
+
this.clearOutputQueue();
|
|
1479
|
+
if (mode === 'reset') {
|
|
1480
|
+
this.discardInput();
|
|
1481
|
+
// resetAndDestroy sends a TCP RST (Node 16.17), not every runtime has it
|
|
1482
|
+
if (typeof socket.resetAndDestroy === 'function') {
|
|
1483
|
+
socket.resetAndDestroy();
|
|
1484
|
+
}
|
|
1485
|
+
else {
|
|
1486
|
+
socket.destroy();
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
else if (this.transport) {
|
|
1336
1490
|
this.transport.end(() => socket.end());
|
|
1337
1491
|
}
|
|
1338
1492
|
else {
|
|
@@ -1433,6 +1587,7 @@ class IMAPConnection {
|
|
|
1433
1587
|
this.transport.destroy();
|
|
1434
1588
|
this.transport = null;
|
|
1435
1589
|
}
|
|
1590
|
+
this.clearOutputQueue();
|
|
1436
1591
|
this.server.removeListener('notify', this._notificationCallback);
|
|
1437
1592
|
this.server.connections.delete(this);
|
|
1438
1593
|
}
|
|
@@ -1448,6 +1603,21 @@ class IMAPConnection {
|
|
|
1448
1603
|
// socket is already gone
|
|
1449
1604
|
}
|
|
1450
1605
|
}
|
|
1606
|
+
/**
|
|
1607
|
+
* Passes a line to the input handler (IDLE, AUTHENTICATE), unless a script rule handles it
|
|
1608
|
+
*
|
|
1609
|
+
* @param {String} line Input line without CRLF
|
|
1610
|
+
*/
|
|
1611
|
+
handleInput(line) {
|
|
1612
|
+
const inputHandler = this.inputHandler;
|
|
1613
|
+
const found = this.server.script.check(this, 'input', { data: line });
|
|
1614
|
+
if (found) {
|
|
1615
|
+
(0, script_js_1.handleLine)(this, found.rule, found.context, () => inputHandler(line));
|
|
1616
|
+
}
|
|
1617
|
+
else {
|
|
1618
|
+
inputHandler(line);
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1451
1621
|
onData(chunk) {
|
|
1452
1622
|
let match;
|
|
1453
1623
|
let str;
|
|
@@ -1519,7 +1689,7 @@ class IMAPConnection {
|
|
|
1519
1689
|
this.sendBad(getResponseTag(line), 'Literal data must wait for the continuation request', 'LITERAL TOO EARLY', line);
|
|
1520
1690
|
}
|
|
1521
1691
|
else if (this.inputHandler) {
|
|
1522
|
-
this.
|
|
1692
|
+
this.handleInput(line);
|
|
1523
1693
|
}
|
|
1524
1694
|
else {
|
|
1525
1695
|
this.scheduleCommand(line);
|
|
@@ -1568,7 +1738,9 @@ class IMAPConnection {
|
|
|
1568
1738
|
this._earlyLiteral = true;
|
|
1569
1739
|
}
|
|
1570
1740
|
else if (!this._earlyLiteral) {
|
|
1571
|
-
|
|
1741
|
+
// the line is needed only by script rules that watch continuations
|
|
1742
|
+
const head = str.substr(0, match.index) + marker;
|
|
1743
|
+
this.sendContinuation('Go ahead', 'LITERAL', () => this._command + head);
|
|
1572
1744
|
}
|
|
1573
1745
|
}
|
|
1574
1746
|
this._remainder = '';
|
|
@@ -1644,12 +1816,7 @@ class IMAPConnection {
|
|
|
1644
1816
|
// not a command, e.g. a SASL response
|
|
1645
1817
|
return literal8 ? refuse('Literal8 is not allowed here') : false;
|
|
1646
1818
|
}
|
|
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
|
-
}
|
|
1819
|
+
const command = getLineCommand(line);
|
|
1653
1820
|
if (!COMMAND_REGEX.test(command) || !this.server.getCommandHandler(command)) {
|
|
1654
1821
|
return refuse('Unknown command');
|
|
1655
1822
|
}
|
|
@@ -2069,6 +2236,26 @@ class IMAPConnection {
|
|
|
2069
2236
|
});
|
|
2070
2237
|
}
|
|
2071
2238
|
}
|
|
2239
|
+
const compiled = this.compileResponse(response);
|
|
2240
|
+
if (compiled === null) {
|
|
2241
|
+
return;
|
|
2242
|
+
}
|
|
2243
|
+
const event = response.tag === '+' ? 'continuation' : 'response';
|
|
2244
|
+
if (!this.server.script.watches(event)) {
|
|
2245
|
+
this.write(compiled);
|
|
2246
|
+
return;
|
|
2247
|
+
}
|
|
2248
|
+
// script rules see the response after every plugin and the core changed it. Without a command, an
|
|
2249
|
+
// unsolicited response belongs to the command that runs or idles
|
|
2250
|
+
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));
|
|
2251
|
+
}
|
|
2252
|
+
/**
|
|
2253
|
+
* Compiles a response for the wire
|
|
2254
|
+
*
|
|
2255
|
+
* @param {Object} response Response object
|
|
2256
|
+
* @return {String|null} the response with its CRLF as a binary string, or null for an untagged response that does not compile
|
|
2257
|
+
*/
|
|
2258
|
+
compileResponse(response) {
|
|
2072
2259
|
let compiled;
|
|
2073
2260
|
try {
|
|
2074
2261
|
compiled = imap_handler_1.default.compiler(response, this.compilerOptions);
|
|
@@ -2079,14 +2266,14 @@ class IMAPConnection {
|
|
|
2079
2266
|
console.log('Failed to compile response: %s', err.message);
|
|
2080
2267
|
}
|
|
2081
2268
|
if (response.tag === '*') {
|
|
2082
|
-
return;
|
|
2269
|
+
return null;
|
|
2083
2270
|
}
|
|
2084
2271
|
compiled = response.tag + ' NO [SERVERBUG] Failed to compile response';
|
|
2085
2272
|
}
|
|
2086
2273
|
if (this.options.debug) {
|
|
2087
2274
|
console.log('SEND: %s', compiled);
|
|
2088
2275
|
}
|
|
2089
|
-
|
|
2276
|
+
return compiled + '\r\n';
|
|
2090
2277
|
}
|
|
2091
2278
|
/**
|
|
2092
2279
|
* Sends a tagged status response to a command
|
|
@@ -2273,9 +2460,23 @@ class IMAPConnection {
|
|
|
2273
2460
|
exportMailboxName(path) {
|
|
2274
2461
|
return path;
|
|
2275
2462
|
}
|
|
2276
|
-
|
|
2463
|
+
/**
|
|
2464
|
+
* Parses a command line and queues the command, or answers it right away when it can not run
|
|
2465
|
+
*
|
|
2466
|
+
* @param {String} data Command line with its literals, without the final CRLF
|
|
2467
|
+
* @param {Boolean} [scripted] The line comes from a script rule with `run`, it is next in the queue
|
|
2468
|
+
*/
|
|
2469
|
+
scheduleCommand(data, scripted) {
|
|
2277
2470
|
let parsed;
|
|
2278
2471
|
const tag = getResponseTag(data);
|
|
2472
|
+
// the rule is chosen when the line arrives, the state it matches is the state at that moment. The rule
|
|
2473
|
+
// acts when the command's turn comes, so that its output keeps the order of the responses
|
|
2474
|
+
const found = !scripted && this.server.script.watches('command') ? this.server.script.check(this, 'command', { data, tag, command: getLineCommand(data) }) : null;
|
|
2475
|
+
if (found) {
|
|
2476
|
+
this._commandQueue.push({ parsed: { tag, command: found.context.command || '' }, data, script: found });
|
|
2477
|
+
this.processQueue();
|
|
2478
|
+
return;
|
|
2479
|
+
}
|
|
2279
2480
|
try {
|
|
2280
2481
|
// server.parserOptions are the defaults of plugins, connection.parserOptions win
|
|
2281
2482
|
parsed = imap_handler_1.default.parser(data, Object.assign({ literalPlus: this.server.literalPlus }, this.server.parserOptions, this.parserOptions));
|
|
@@ -2322,10 +2523,14 @@ class IMAPConnection {
|
|
|
2322
2523
|
this.sendStatus(parsed, data, 'BAD', 'Commands with message sequence numbers must wait for the completion of earlier commands');
|
|
2323
2524
|
return;
|
|
2324
2525
|
}
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2526
|
+
const element = { parsed, data };
|
|
2527
|
+
if (scripted) {
|
|
2528
|
+
// processQueue runs it once the script rule released the queue
|
|
2529
|
+
this._commandQueue.unshift(element);
|
|
2530
|
+
}
|
|
2531
|
+
else {
|
|
2532
|
+
this._commandQueue.push(element);
|
|
2533
|
+
}
|
|
2329
2534
|
this.processQueue();
|
|
2330
2535
|
}
|
|
2331
2536
|
else if (/^AUTHENTICATE /i.test(parsed.command)) {
|
|
@@ -2354,6 +2559,33 @@ class IMAPConnection {
|
|
|
2354
2559
|
}, 'UNKNOWN COMMAND', parsed, data);
|
|
2355
2560
|
}
|
|
2356
2561
|
}
|
|
2562
|
+
/**
|
|
2563
|
+
* Handles a command line with the script rule that matched it, after the rule's delay. With `run` the line
|
|
2564
|
+
* goes through the parser and the command handler as usual afterwards
|
|
2565
|
+
*
|
|
2566
|
+
* @param {Object} element Queued command with the rule
|
|
2567
|
+
* @param {Function} next Releases the queue
|
|
2568
|
+
*/
|
|
2569
|
+
runScriptedCommand(element, next) {
|
|
2570
|
+
const { rule, context } = element.script;
|
|
2571
|
+
const act = () => {
|
|
2572
|
+
if (!this.socket || this._closing) {
|
|
2573
|
+
return next();
|
|
2574
|
+
}
|
|
2575
|
+
(0, script_js_1.handleLine)(this, rule, context, () => {
|
|
2576
|
+
// the line is not running yet, it must not count as an earlier command (RFC 3501 section 5.5)
|
|
2577
|
+
this._runningCommand = null;
|
|
2578
|
+
this.scheduleCommand(element.data, true);
|
|
2579
|
+
});
|
|
2580
|
+
next();
|
|
2581
|
+
};
|
|
2582
|
+
if (rule.delay) {
|
|
2583
|
+
setTimeout(act, rule.delay);
|
|
2584
|
+
}
|
|
2585
|
+
else {
|
|
2586
|
+
act();
|
|
2587
|
+
}
|
|
2588
|
+
}
|
|
2357
2589
|
processQueue(force) {
|
|
2358
2590
|
if (!force && this._processing) {
|
|
2359
2591
|
return;
|
|
@@ -2386,6 +2618,10 @@ class IMAPConnection {
|
|
|
2386
2618
|
this.processQueue(true);
|
|
2387
2619
|
}
|
|
2388
2620
|
};
|
|
2621
|
+
if (element.script) {
|
|
2622
|
+
this.runScriptedCommand(element, next);
|
|
2623
|
+
return;
|
|
2624
|
+
}
|
|
2389
2625
|
if (options.states && options.states.indexOf(this.state) < 0) {
|
|
2390
2626
|
this.sendStatus(element.parsed, element.data, 'BAD', stateError(command, this.state));
|
|
2391
2627
|
return next();
|
|
@@ -2428,7 +2664,12 @@ class IMAPConnection {
|
|
|
2428
2664
|
try {
|
|
2429
2665
|
// changes made while the handler runs are attributed to this session (the `origin` of notifications)
|
|
2430
2666
|
this.server.activeConnection = this;
|
|
2667
|
+
const inputHandler = this.inputHandler;
|
|
2431
2668
|
this.server.getCommandHandler(element.parsed.command)(this, element.parsed, element.data, next);
|
|
2669
|
+
if (this.inputHandler && this.inputHandler !== inputHandler) {
|
|
2670
|
+
// the command reads the lines that follow (IDLE, AUTHENTICATE), script rules match them with it
|
|
2671
|
+
this.inputCommand = { tag: element.parsed.tag, command: element.parsed.command };
|
|
2672
|
+
}
|
|
2432
2673
|
}
|
|
2433
2674
|
catch (E) {
|
|
2434
2675
|
const ex = E;
|
|
@@ -2550,6 +2791,41 @@ class IMAPConnection {
|
|
|
2550
2791
|
}
|
|
2551
2792
|
}
|
|
2552
2793
|
exports.IMAPConnection = IMAPConnection;
|
|
2794
|
+
/**
|
|
2795
|
+
* Copies a response tree for the `mutate` action of a script rule: arrays and plain objects are copied,
|
|
2796
|
+
* other values (Buffers) are shared
|
|
2797
|
+
*
|
|
2798
|
+
* @param {*} value Response or a part of it
|
|
2799
|
+
* @return {*} Copy
|
|
2800
|
+
*/
|
|
2801
|
+
function cloneResponse(value) {
|
|
2802
|
+
if (Array.isArray(value)) {
|
|
2803
|
+
return value.map(cloneResponse);
|
|
2804
|
+
}
|
|
2805
|
+
if (value && typeof value === 'object' && Object.getPrototypeOf(value) === Object.prototype) {
|
|
2806
|
+
const copy = {};
|
|
2807
|
+
for (const key of Object.keys(value)) {
|
|
2808
|
+
copy[key] = cloneResponse(value[key]);
|
|
2809
|
+
}
|
|
2810
|
+
return copy;
|
|
2811
|
+
}
|
|
2812
|
+
return value;
|
|
2813
|
+
}
|
|
2814
|
+
/**
|
|
2815
|
+
* Finds the command name in a command line that may not parse: tag SP command, and for UID and AUTHENTICATE
|
|
2816
|
+
* the word that follows
|
|
2817
|
+
*
|
|
2818
|
+
* @param {String} line Command line
|
|
2819
|
+
* @return {String} Command name in upper case, can be empty
|
|
2820
|
+
*/
|
|
2821
|
+
function getLineCommand(line) {
|
|
2822
|
+
const words = line.match(/^[^ ]* ([^ ]*)(?: ([^ ]*))?/);
|
|
2823
|
+
let command = ((words && words[1]) || '').toUpperCase();
|
|
2824
|
+
if (command === 'UID' || command === 'AUTHENTICATE') {
|
|
2825
|
+
command += ' ' + ((words && words[2]) || '').toUpperCase();
|
|
2826
|
+
}
|
|
2827
|
+
return command;
|
|
2828
|
+
}
|
|
2553
2829
|
/**
|
|
2554
2830
|
* Formats a mailbox name for a response: an atom when possible, otherwise a string. NIL and names
|
|
2555
2831
|
* 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') {
|