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/esm/server.js
CHANGED
|
@@ -14,6 +14,7 @@ import { isSequenceSet } from './numbers.js';
|
|
|
14
14
|
import { restoreNilAtoms } from './arguments.js';
|
|
15
15
|
import { refuseMissingTarget } from './commands/append.js';
|
|
16
16
|
import * as bundledCert from './cert.js';
|
|
17
|
+
import { ServerScript, sendOutput, handleLine } from './script.js';
|
|
17
18
|
// longest command line (not counting literals) accepted from a client
|
|
18
19
|
const MAX_LINE_LENGTH = 1024 * 1024;
|
|
19
20
|
// largest literal accepted after login, override with the maxLiteralSize option
|
|
@@ -155,6 +156,9 @@ class IMAPServer extends Stream {
|
|
|
155
156
|
this.referenceNamespace = false;
|
|
156
157
|
// the session whose command is running, see IMAPServer#notify
|
|
157
158
|
this.activeConnection = null;
|
|
159
|
+
this.sessionCounter = 0;
|
|
160
|
+
// rules that make the server misbehave on purpose, from the script option or server.script.add()
|
|
161
|
+
this.script = new ServerScript(this, this.options.script);
|
|
158
162
|
// users and storage are deep copied, so that runtime changes never leak into
|
|
159
163
|
// the caller's objects or into other servers built from the same fixture.
|
|
160
164
|
// Without a prototype, user names like "__proto__" or "toString" are plain keys
|
|
@@ -1212,6 +1216,9 @@ class IMAPConnection {
|
|
|
1212
1216
|
this.socket = socket;
|
|
1213
1217
|
this.options = this.server.options;
|
|
1214
1218
|
this.state = 'Not Authenticated';
|
|
1219
|
+
this.sessionNumber = ++this.server.sessionCounter;
|
|
1220
|
+
this._outputQueue = null;
|
|
1221
|
+
this._outputTimer = null;
|
|
1215
1222
|
this.secureConnection = !!this.options.secureConnection;
|
|
1216
1223
|
this._remainder = '';
|
|
1217
1224
|
this._command = '';
|
|
@@ -1241,7 +1248,7 @@ class IMAPConnection {
|
|
|
1241
1248
|
this.notificationQueue = [];
|
|
1242
1249
|
this.server.on('notify', this._notificationCallback);
|
|
1243
1250
|
this.server.connections.add(this);
|
|
1244
|
-
this.
|
|
1251
|
+
this.scriptOutput('greeting', '* OK ImapKit ready for rumble\r\n', {});
|
|
1245
1252
|
}
|
|
1246
1253
|
/**
|
|
1247
1254
|
* Writes protocol output to the client, through the transport layer if there is one
|
|
@@ -1250,12 +1257,134 @@ class IMAPConnection {
|
|
|
1250
1257
|
*/
|
|
1251
1258
|
write(data) {
|
|
1252
1259
|
const buffer = typeof data === 'string' ? Buffer.from(data, 'binary') : data;
|
|
1253
|
-
if (this.
|
|
1254
|
-
this.
|
|
1260
|
+
if (this._outputQueue) {
|
|
1261
|
+
this._outputQueue.push({ data: buffer, transport: this.transport });
|
|
1262
|
+
}
|
|
1263
|
+
else {
|
|
1264
|
+
this.writeLayer(buffer, this.transport);
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
/**
|
|
1268
|
+
* Writes output, or puts it in the output queue while earlier output waits for a delay of a script rule.
|
|
1269
|
+
* The transport layer is taken when the output is queued, so output from before COMPRESS is not compressed
|
|
1270
|
+
*
|
|
1271
|
+
* @param {Object} operation `{ data, delay, chunk, chunkDelay, close }`, see OutputOperation in src/script.ts
|
|
1272
|
+
*/
|
|
1273
|
+
queueOutput(operation) {
|
|
1274
|
+
if (!this._outputQueue && !operation.delay && !operation.chunk) {
|
|
1275
|
+
if (operation.data) {
|
|
1276
|
+
this.writeLayer(operation.data, this.transport);
|
|
1277
|
+
}
|
|
1278
|
+
if (operation.close) {
|
|
1279
|
+
this.closeNow(operation.close);
|
|
1280
|
+
}
|
|
1281
|
+
return;
|
|
1282
|
+
}
|
|
1283
|
+
this._outputQueue = this._outputQueue || [];
|
|
1284
|
+
this._outputQueue.push(Object.assign({}, operation, { transport: this.transport }));
|
|
1285
|
+
if (!this._outputTimer) {
|
|
1286
|
+
this.flushOutput();
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
/**
|
|
1290
|
+
* Writes the output queue until it is empty or a delay stops it
|
|
1291
|
+
*/
|
|
1292
|
+
flushOutput() {
|
|
1293
|
+
const queue = this._outputQueue || [];
|
|
1294
|
+
while (queue.length) {
|
|
1295
|
+
const operation = queue[0];
|
|
1296
|
+
if (operation.delay) {
|
|
1297
|
+
const delay = operation.delay;
|
|
1298
|
+
operation.delay = 0;
|
|
1299
|
+
this._outputTimer = setTimeout(() => {
|
|
1300
|
+
this._outputTimer = null;
|
|
1301
|
+
this.flushOutput();
|
|
1302
|
+
}, delay);
|
|
1303
|
+
return;
|
|
1304
|
+
}
|
|
1305
|
+
if (operation.data && operation.chunk && operation.data.length > operation.chunk) {
|
|
1306
|
+
// the rest waits for chunkDelay, like a delay of its own
|
|
1307
|
+
this.writeLayer(operation.data.subarray(0, operation.chunk), operation.transport || null);
|
|
1308
|
+
operation.data = operation.data.subarray(operation.chunk);
|
|
1309
|
+
operation.delay = operation.chunkDelay;
|
|
1310
|
+
continue;
|
|
1311
|
+
}
|
|
1312
|
+
if (operation.data) {
|
|
1313
|
+
this.writeLayer(operation.data, operation.transport || null);
|
|
1314
|
+
}
|
|
1315
|
+
queue.shift();
|
|
1316
|
+
if (operation.close) {
|
|
1317
|
+
this.closeNow(operation.close);
|
|
1318
|
+
return;
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
this._outputQueue = null;
|
|
1322
|
+
}
|
|
1323
|
+
/**
|
|
1324
|
+
* Drops the output that waits for a delay of a script rule
|
|
1325
|
+
*/
|
|
1326
|
+
clearOutputQueue() {
|
|
1327
|
+
if (this._outputTimer) {
|
|
1328
|
+
clearTimeout(this._outputTimer);
|
|
1329
|
+
this._outputTimer = null;
|
|
1330
|
+
}
|
|
1331
|
+
this._outputQueue = null;
|
|
1332
|
+
}
|
|
1333
|
+
/**
|
|
1334
|
+
* Writes output through a transport layer, or to the socket
|
|
1335
|
+
*
|
|
1336
|
+
* @param {Buffer} data Output
|
|
1337
|
+
* @param {Object|null} transport Transport layer
|
|
1338
|
+
*/
|
|
1339
|
+
writeLayer(data, transport) {
|
|
1340
|
+
if (transport) {
|
|
1341
|
+
transport.write(data);
|
|
1255
1342
|
}
|
|
1256
1343
|
else {
|
|
1257
|
-
this.writeRaw(
|
|
1344
|
+
this.writeRaw(data);
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
/**
|
|
1348
|
+
* Sends output through the script rule that handles its event, if there is one
|
|
1349
|
+
*
|
|
1350
|
+
* @param {String} event Event name: greeting, response or continuation
|
|
1351
|
+
* @param {String} output The output as a binary string
|
|
1352
|
+
* @param {Object} fields Context fields of the event (tag, command, description, response)
|
|
1353
|
+
* @param {Function} [compile] Compiles the response that a `mutate` action changed, null drops the output
|
|
1354
|
+
*/
|
|
1355
|
+
scriptOutput(event, output, fields, compile) {
|
|
1356
|
+
const found = this.server.script.check(this, event, Object.assign({}, fields, { data: output }));
|
|
1357
|
+
if (!found) {
|
|
1358
|
+
this.write(output);
|
|
1359
|
+
return;
|
|
1360
|
+
}
|
|
1361
|
+
const { rule, context } = found;
|
|
1362
|
+
if (rule.mutate && compile && context.response) {
|
|
1363
|
+
// a copy, a notification object is shared by every session
|
|
1364
|
+
const copy = cloneResponse(context.response);
|
|
1365
|
+
const changed = compile(rule.mutate(copy, context) || copy);
|
|
1366
|
+
if (changed === null) {
|
|
1367
|
+
return;
|
|
1368
|
+
}
|
|
1369
|
+
context.data = changed;
|
|
1370
|
+
}
|
|
1371
|
+
sendOutput(this, rule, context, Buffer.from(context.data, 'binary'));
|
|
1372
|
+
}
|
|
1373
|
+
/**
|
|
1374
|
+
* Sends a continuation request, `+ text`
|
|
1375
|
+
*
|
|
1376
|
+
* @param {String} text Human readable text, can be empty (SASL)
|
|
1377
|
+
* @param {String} description Description for script rules
|
|
1378
|
+
* @param {Function} [getLine] Returns the command line received so far, for the tag and the command of a literal continuation
|
|
1379
|
+
*/
|
|
1380
|
+
sendContinuation(text, description, getLine) {
|
|
1381
|
+
const output = '+ ' + text + '\r\n';
|
|
1382
|
+
if (!this.server.script.watches('continuation')) {
|
|
1383
|
+
this.write(output);
|
|
1384
|
+
return;
|
|
1258
1385
|
}
|
|
1386
|
+
const line = getLine ? getLine() : null;
|
|
1387
|
+
this.scriptOutput('continuation', output, line === null ? { description } : { description, tag: getResponseTag(line), command: getLineCommand(line) });
|
|
1259
1388
|
}
|
|
1260
1389
|
/**
|
|
1261
1390
|
* Writes data to the socket, below the transport layer
|
|
@@ -1285,12 +1414,37 @@ class IMAPConnection {
|
|
|
1285
1414
|
* holds, is written out
|
|
1286
1415
|
*/
|
|
1287
1416
|
end() {
|
|
1417
|
+
if (this._outputQueue && this.socket) {
|
|
1418
|
+
// output still waits for a delay of a script rule
|
|
1419
|
+
this._closing = true;
|
|
1420
|
+
this._outputQueue.push({ close: true });
|
|
1421
|
+
return;
|
|
1422
|
+
}
|
|
1423
|
+
this.closeNow(true);
|
|
1424
|
+
}
|
|
1425
|
+
/**
|
|
1426
|
+
* Closes the connection now, after the output written so far
|
|
1427
|
+
*
|
|
1428
|
+
* @param {Boolean|String} mode true ends the connection gracefully, "reset" destroys the socket (script rules)
|
|
1429
|
+
*/
|
|
1430
|
+
closeNow(mode) {
|
|
1288
1431
|
const socket = this.socket;
|
|
1289
1432
|
if (!socket) {
|
|
1290
1433
|
return;
|
|
1291
1434
|
}
|
|
1292
1435
|
this._closing = true;
|
|
1293
|
-
|
|
1436
|
+
this.clearOutputQueue();
|
|
1437
|
+
if (mode === 'reset') {
|
|
1438
|
+
this.discardInput();
|
|
1439
|
+
// resetAndDestroy sends a TCP RST (Node 16.17), not every runtime has it
|
|
1440
|
+
if (typeof socket.resetAndDestroy === 'function') {
|
|
1441
|
+
socket.resetAndDestroy();
|
|
1442
|
+
}
|
|
1443
|
+
else {
|
|
1444
|
+
socket.destroy();
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
else if (this.transport) {
|
|
1294
1448
|
this.transport.end(() => socket.end());
|
|
1295
1449
|
}
|
|
1296
1450
|
else {
|
|
@@ -1391,6 +1545,7 @@ class IMAPConnection {
|
|
|
1391
1545
|
this.transport.destroy();
|
|
1392
1546
|
this.transport = null;
|
|
1393
1547
|
}
|
|
1548
|
+
this.clearOutputQueue();
|
|
1394
1549
|
this.server.removeListener('notify', this._notificationCallback);
|
|
1395
1550
|
this.server.connections.delete(this);
|
|
1396
1551
|
}
|
|
@@ -1406,6 +1561,21 @@ class IMAPConnection {
|
|
|
1406
1561
|
// socket is already gone
|
|
1407
1562
|
}
|
|
1408
1563
|
}
|
|
1564
|
+
/**
|
|
1565
|
+
* Passes a line to the input handler (IDLE, AUTHENTICATE), unless a script rule handles it
|
|
1566
|
+
*
|
|
1567
|
+
* @param {String} line Input line without CRLF
|
|
1568
|
+
*/
|
|
1569
|
+
handleInput(line) {
|
|
1570
|
+
const inputHandler = this.inputHandler;
|
|
1571
|
+
const found = this.server.script.check(this, 'input', { data: line });
|
|
1572
|
+
if (found) {
|
|
1573
|
+
handleLine(this, found.rule, found.context, () => inputHandler(line));
|
|
1574
|
+
}
|
|
1575
|
+
else {
|
|
1576
|
+
inputHandler(line);
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1409
1579
|
onData(chunk) {
|
|
1410
1580
|
let match;
|
|
1411
1581
|
let str;
|
|
@@ -1477,7 +1647,7 @@ class IMAPConnection {
|
|
|
1477
1647
|
this.sendBad(getResponseTag(line), 'Literal data must wait for the continuation request', 'LITERAL TOO EARLY', line);
|
|
1478
1648
|
}
|
|
1479
1649
|
else if (this.inputHandler) {
|
|
1480
|
-
this.
|
|
1650
|
+
this.handleInput(line);
|
|
1481
1651
|
}
|
|
1482
1652
|
else {
|
|
1483
1653
|
this.scheduleCommand(line);
|
|
@@ -1526,7 +1696,9 @@ class IMAPConnection {
|
|
|
1526
1696
|
this._earlyLiteral = true;
|
|
1527
1697
|
}
|
|
1528
1698
|
else if (!this._earlyLiteral) {
|
|
1529
|
-
|
|
1699
|
+
// the line is needed only by script rules that watch continuations
|
|
1700
|
+
const head = str.substr(0, match.index) + marker;
|
|
1701
|
+
this.sendContinuation('Go ahead', 'LITERAL', () => this._command + head);
|
|
1530
1702
|
}
|
|
1531
1703
|
}
|
|
1532
1704
|
this._remainder = '';
|
|
@@ -1602,12 +1774,7 @@ class IMAPConnection {
|
|
|
1602
1774
|
// not a command, e.g. a SASL response
|
|
1603
1775
|
return literal8 ? refuse('Literal8 is not allowed here') : false;
|
|
1604
1776
|
}
|
|
1605
|
-
|
|
1606
|
-
const words = line.match(/^[^ ]* ([^ ]*)(?: ([^ ]*))?/);
|
|
1607
|
-
let command = ((words && words[1]) || '').toUpperCase();
|
|
1608
|
-
if (command === 'UID' || command === 'AUTHENTICATE') {
|
|
1609
|
-
command += ' ' + ((words && words[2]) || '').toUpperCase();
|
|
1610
|
-
}
|
|
1777
|
+
const command = getLineCommand(line);
|
|
1611
1778
|
if (!COMMAND_REGEX.test(command) || !this.server.getCommandHandler(command)) {
|
|
1612
1779
|
return refuse('Unknown command');
|
|
1613
1780
|
}
|
|
@@ -2027,6 +2194,26 @@ class IMAPConnection {
|
|
|
2027
2194
|
});
|
|
2028
2195
|
}
|
|
2029
2196
|
}
|
|
2197
|
+
const compiled = this.compileResponse(response);
|
|
2198
|
+
if (compiled === null) {
|
|
2199
|
+
return;
|
|
2200
|
+
}
|
|
2201
|
+
const event = response.tag === '+' ? 'continuation' : 'response';
|
|
2202
|
+
if (!this.server.script.watches(event)) {
|
|
2203
|
+
this.write(compiled);
|
|
2204
|
+
return;
|
|
2205
|
+
}
|
|
2206
|
+
// script rules see the response after every plugin and the core changed it. Without a command, an
|
|
2207
|
+
// unsolicited response belongs to the command that runs or idles
|
|
2208
|
+
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));
|
|
2209
|
+
}
|
|
2210
|
+
/**
|
|
2211
|
+
* Compiles a response for the wire
|
|
2212
|
+
*
|
|
2213
|
+
* @param {Object} response Response object
|
|
2214
|
+
* @return {String|null} the response with its CRLF as a binary string, or null for an untagged response that does not compile
|
|
2215
|
+
*/
|
|
2216
|
+
compileResponse(response) {
|
|
2030
2217
|
let compiled;
|
|
2031
2218
|
try {
|
|
2032
2219
|
compiled = imapHandler.compiler(response, this.compilerOptions);
|
|
@@ -2037,14 +2224,14 @@ class IMAPConnection {
|
|
|
2037
2224
|
console.log('Failed to compile response: %s', err.message);
|
|
2038
2225
|
}
|
|
2039
2226
|
if (response.tag === '*') {
|
|
2040
|
-
return;
|
|
2227
|
+
return null;
|
|
2041
2228
|
}
|
|
2042
2229
|
compiled = response.tag + ' NO [SERVERBUG] Failed to compile response';
|
|
2043
2230
|
}
|
|
2044
2231
|
if (this.options.debug) {
|
|
2045
2232
|
console.log('SEND: %s', compiled);
|
|
2046
2233
|
}
|
|
2047
|
-
|
|
2234
|
+
return compiled + '\r\n';
|
|
2048
2235
|
}
|
|
2049
2236
|
/**
|
|
2050
2237
|
* Sends a tagged status response to a command
|
|
@@ -2231,9 +2418,23 @@ class IMAPConnection {
|
|
|
2231
2418
|
exportMailboxName(path) {
|
|
2232
2419
|
return path;
|
|
2233
2420
|
}
|
|
2234
|
-
|
|
2421
|
+
/**
|
|
2422
|
+
* Parses a command line and queues the command, or answers it right away when it can not run
|
|
2423
|
+
*
|
|
2424
|
+
* @param {String} data Command line with its literals, without the final CRLF
|
|
2425
|
+
* @param {Boolean} [scripted] The line comes from a script rule with `run`, it is next in the queue
|
|
2426
|
+
*/
|
|
2427
|
+
scheduleCommand(data, scripted) {
|
|
2235
2428
|
let parsed;
|
|
2236
2429
|
const tag = getResponseTag(data);
|
|
2430
|
+
// the rule is chosen when the line arrives, the state it matches is the state at that moment. The rule
|
|
2431
|
+
// acts when the command's turn comes, so that its output keeps the order of the responses
|
|
2432
|
+
const found = !scripted && this.server.script.watches('command') ? this.server.script.check(this, 'command', { data, tag, command: getLineCommand(data) }) : null;
|
|
2433
|
+
if (found) {
|
|
2434
|
+
this._commandQueue.push({ parsed: { tag, command: found.context.command || '' }, data, script: found });
|
|
2435
|
+
this.processQueue();
|
|
2436
|
+
return;
|
|
2437
|
+
}
|
|
2237
2438
|
try {
|
|
2238
2439
|
// server.parserOptions are the defaults of plugins, connection.parserOptions win
|
|
2239
2440
|
parsed = imapHandler.parser(data, Object.assign({ literalPlus: this.server.literalPlus }, this.server.parserOptions, this.parserOptions));
|
|
@@ -2280,10 +2481,14 @@ class IMAPConnection {
|
|
|
2280
2481
|
this.sendStatus(parsed, data, 'BAD', 'Commands with message sequence numbers must wait for the completion of earlier commands');
|
|
2281
2482
|
return;
|
|
2282
2483
|
}
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2484
|
+
const element = { parsed, data };
|
|
2485
|
+
if (scripted) {
|
|
2486
|
+
// processQueue runs it once the script rule released the queue
|
|
2487
|
+
this._commandQueue.unshift(element);
|
|
2488
|
+
}
|
|
2489
|
+
else {
|
|
2490
|
+
this._commandQueue.push(element);
|
|
2491
|
+
}
|
|
2287
2492
|
this.processQueue();
|
|
2288
2493
|
}
|
|
2289
2494
|
else if (/^AUTHENTICATE /i.test(parsed.command)) {
|
|
@@ -2312,6 +2517,33 @@ class IMAPConnection {
|
|
|
2312
2517
|
}, 'UNKNOWN COMMAND', parsed, data);
|
|
2313
2518
|
}
|
|
2314
2519
|
}
|
|
2520
|
+
/**
|
|
2521
|
+
* Handles a command line with the script rule that matched it, after the rule's delay. With `run` the line
|
|
2522
|
+
* goes through the parser and the command handler as usual afterwards
|
|
2523
|
+
*
|
|
2524
|
+
* @param {Object} element Queued command with the rule
|
|
2525
|
+
* @param {Function} next Releases the queue
|
|
2526
|
+
*/
|
|
2527
|
+
runScriptedCommand(element, next) {
|
|
2528
|
+
const { rule, context } = element.script;
|
|
2529
|
+
const act = () => {
|
|
2530
|
+
if (!this.socket || this._closing) {
|
|
2531
|
+
return next();
|
|
2532
|
+
}
|
|
2533
|
+
handleLine(this, rule, context, () => {
|
|
2534
|
+
// the line is not running yet, it must not count as an earlier command (RFC 3501 section 5.5)
|
|
2535
|
+
this._runningCommand = null;
|
|
2536
|
+
this.scheduleCommand(element.data, true);
|
|
2537
|
+
});
|
|
2538
|
+
next();
|
|
2539
|
+
};
|
|
2540
|
+
if (rule.delay) {
|
|
2541
|
+
setTimeout(act, rule.delay);
|
|
2542
|
+
}
|
|
2543
|
+
else {
|
|
2544
|
+
act();
|
|
2545
|
+
}
|
|
2546
|
+
}
|
|
2315
2547
|
processQueue(force) {
|
|
2316
2548
|
if (!force && this._processing) {
|
|
2317
2549
|
return;
|
|
@@ -2344,6 +2576,10 @@ class IMAPConnection {
|
|
|
2344
2576
|
this.processQueue(true);
|
|
2345
2577
|
}
|
|
2346
2578
|
};
|
|
2579
|
+
if (element.script) {
|
|
2580
|
+
this.runScriptedCommand(element, next);
|
|
2581
|
+
return;
|
|
2582
|
+
}
|
|
2347
2583
|
if (options.states && options.states.indexOf(this.state) < 0) {
|
|
2348
2584
|
this.sendStatus(element.parsed, element.data, 'BAD', stateError(command, this.state));
|
|
2349
2585
|
return next();
|
|
@@ -2386,7 +2622,12 @@ class IMAPConnection {
|
|
|
2386
2622
|
try {
|
|
2387
2623
|
// changes made while the handler runs are attributed to this session (the `origin` of notifications)
|
|
2388
2624
|
this.server.activeConnection = this;
|
|
2625
|
+
const inputHandler = this.inputHandler;
|
|
2389
2626
|
this.server.getCommandHandler(element.parsed.command)(this, element.parsed, element.data, next);
|
|
2627
|
+
if (this.inputHandler && this.inputHandler !== inputHandler) {
|
|
2628
|
+
// the command reads the lines that follow (IDLE, AUTHENTICATE), script rules match them with it
|
|
2629
|
+
this.inputCommand = { tag: element.parsed.tag, command: element.parsed.command };
|
|
2630
|
+
}
|
|
2390
2631
|
}
|
|
2391
2632
|
catch (E) {
|
|
2392
2633
|
const ex = E;
|
|
@@ -2507,6 +2748,41 @@ class IMAPConnection {
|
|
|
2507
2748
|
}, mailbox, ignoreSelf || ignoreExists ? this : false);
|
|
2508
2749
|
}
|
|
2509
2750
|
}
|
|
2751
|
+
/**
|
|
2752
|
+
* Copies a response tree for the `mutate` action of a script rule: arrays and plain objects are copied,
|
|
2753
|
+
* other values (Buffers) are shared
|
|
2754
|
+
*
|
|
2755
|
+
* @param {*} value Response or a part of it
|
|
2756
|
+
* @return {*} Copy
|
|
2757
|
+
*/
|
|
2758
|
+
function cloneResponse(value) {
|
|
2759
|
+
if (Array.isArray(value)) {
|
|
2760
|
+
return value.map(cloneResponse);
|
|
2761
|
+
}
|
|
2762
|
+
if (value && typeof value === 'object' && Object.getPrototypeOf(value) === Object.prototype) {
|
|
2763
|
+
const copy = {};
|
|
2764
|
+
for (const key of Object.keys(value)) {
|
|
2765
|
+
copy[key] = cloneResponse(value[key]);
|
|
2766
|
+
}
|
|
2767
|
+
return copy;
|
|
2768
|
+
}
|
|
2769
|
+
return value;
|
|
2770
|
+
}
|
|
2771
|
+
/**
|
|
2772
|
+
* Finds the command name in a command line that may not parse: tag SP command, and for UID and AUTHENTICATE
|
|
2773
|
+
* the word that follows
|
|
2774
|
+
*
|
|
2775
|
+
* @param {String} line Command line
|
|
2776
|
+
* @return {String} Command name in upper case, can be empty
|
|
2777
|
+
*/
|
|
2778
|
+
function getLineCommand(line) {
|
|
2779
|
+
const words = line.match(/^[^ ]* ([^ ]*)(?: ([^ ]*))?/);
|
|
2780
|
+
let command = ((words && words[1]) || '').toUpperCase();
|
|
2781
|
+
if (command === 'UID' || command === 'AUTHENTICATE') {
|
|
2782
|
+
command += ' ' + ((words && words[2]) || '').toUpperCase();
|
|
2783
|
+
}
|
|
2784
|
+
return command;
|
|
2785
|
+
}
|
|
2510
2786
|
/**
|
|
2511
2787
|
* Formats a mailbox name for a response: an atom when possible, otherwise a string. NIL and names
|
|
2512
2788
|
* like \\Foo would not read back as mailbox names, so these are strings as well
|
package/dist/esm/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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "imapkit",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.2.0",
|
|
4
4
|
"description": "Scriptable, strictly RFC compliant in-memory IMAP server for testing IMAP clients",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/cjs/index.js",
|
|
@@ -93,7 +93,7 @@
|
|
|
93
93
|
"eslint": "10.12.0",
|
|
94
94
|
"eslint-config-prettier": "10.1.8",
|
|
95
95
|
"globals": "17.13.0",
|
|
96
|
-
"imapflow": "2.2.
|
|
96
|
+
"imapflow": "2.2.9",
|
|
97
97
|
"prettier": "3.9.9",
|
|
98
98
|
"tsx": "4.23.15",
|
|
99
99
|
"typescript": "6.0.3",
|