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/esm/server.d.ts
CHANGED
|
@@ -3,6 +3,8 @@ import net from 'node:net';
|
|
|
3
3
|
import tls from 'node:tls';
|
|
4
4
|
import type { CompilerOptions, ParserOptions } from 'imap-handler';
|
|
5
5
|
import type { ResolvedCommandOptions } from './command-states.js';
|
|
6
|
+
import { ServerScript } from './script.js';
|
|
7
|
+
import type { DeferredOutput, OutputOperation, ScriptContext, ScriptEvent, ScriptRule } from './script.js';
|
|
6
8
|
import type { AppendCheck, AppendCheckOptions, AppendDataHandler, AppendMessage, CapabilityCheck, CheckResult, ClosedCheck, CommandCheck, CommandContext, CommandOptions, CommandHandler, ConnectionHandler, ConnectionState, CopyHandler, FetchHandler, IMAPResponse, IMAPServerOptions, ListedMailbox, LiteralFilter, Mailbox, MailboxHandler, MailboxStatus, Message, MessageFilter, MessageHandler, MessageRange, Namespace, Notification, NotifyEvent, NotifyFilter, OutputHandler, ParsedCommand, RangeLimit, Refusal, SearchAccessCheck, SearchHandler, SearchLimit, ServerStorage, StatusHandler, StorageMailbox, StorageMessage, StoreHandler, SubscriptionStandIn, Transport, UrlAccessCheck, UserData } from './types.js';
|
|
7
9
|
declare const TAG_REGEX: RegExp;
|
|
8
10
|
/**
|
|
@@ -58,6 +60,10 @@ declare class IMAPServer extends Stream {
|
|
|
58
60
|
uidvalidityCounter: number;
|
|
59
61
|
subscriptions: Set<string>;
|
|
60
62
|
folderCache: Record<string, Mailbox>;
|
|
63
|
+
/** scripted faults, see src/script.ts */
|
|
64
|
+
script: ServerScript;
|
|
65
|
+
/** connections accepted so far, the number of a connection is `connection.sessionNumber` */
|
|
66
|
+
sessionCounter: number;
|
|
61
67
|
constructor(options?: IMAPServerOptions);
|
|
62
68
|
/**
|
|
63
69
|
* Starts accepting connections, takes the arguments of `net.Server#listen()`
|
|
@@ -446,6 +452,11 @@ declare class IMAPServer extends Stream {
|
|
|
446
452
|
interface QueuedCommand {
|
|
447
453
|
parsed: ParsedCommand;
|
|
448
454
|
data: string;
|
|
455
|
+
/** a command line that a script rule handles instead of the parser and the command handler */
|
|
456
|
+
script?: {
|
|
457
|
+
rule: ScriptRule;
|
|
458
|
+
context: ScriptContext;
|
|
459
|
+
} | undefined;
|
|
449
460
|
}
|
|
450
461
|
declare class IMAPConnection {
|
|
451
462
|
[key: string]: any;
|
|
@@ -471,6 +482,13 @@ declare class IMAPConnection {
|
|
|
471
482
|
/** notifications are sent right away instead of before the next tagged response (IDLE) */
|
|
472
483
|
directNotifications: boolean;
|
|
473
484
|
notificationQueue: Notification[];
|
|
485
|
+
/** the number of the connection, 1 for the first one the server accepted (script rules match it) */
|
|
486
|
+
sessionNumber: number;
|
|
487
|
+
/** the tag and name of the command whose input handler reads the lines that follow (IDLE, AUTHENTICATE) */
|
|
488
|
+
inputCommand: {
|
|
489
|
+
tag: string;
|
|
490
|
+
command: string;
|
|
491
|
+
} | null | undefined;
|
|
474
492
|
_remainder: string;
|
|
475
493
|
_command: string;
|
|
476
494
|
_literalRemaining: number;
|
|
@@ -488,6 +506,13 @@ declare class IMAPConnection {
|
|
|
488
506
|
command: string;
|
|
489
507
|
read: number | undefined;
|
|
490
508
|
} | undefined;
|
|
509
|
+
/** output that waits behind a delay of a script rule, null when output is written right away */
|
|
510
|
+
_outputQueue: OutputOperation[] | null;
|
|
511
|
+
_outputTimer: ReturnType<typeof setTimeout> | null;
|
|
512
|
+
/** untagged responses that `defer` rules hold back, see releaseDeferred in src/script.ts */
|
|
513
|
+
_deferredOutput: DeferredOutput[] | null;
|
|
514
|
+
/** a script rule closes the connection with a RST, see closeNow */
|
|
515
|
+
_resetting: boolean | undefined;
|
|
491
516
|
constructor(server: IMAPServer, socket: net.Socket);
|
|
492
517
|
/**
|
|
493
518
|
* Writes protocol output to the client, through the transport layer if there is one
|
|
@@ -495,6 +520,45 @@ declare class IMAPConnection {
|
|
|
495
520
|
* @param {Buffer|String} data Data to send, a string is sent as a binary string
|
|
496
521
|
*/
|
|
497
522
|
write(data: Buffer | string): void;
|
|
523
|
+
/**
|
|
524
|
+
* Writes output, or puts it in the output queue while earlier output waits for a delay of a script rule.
|
|
525
|
+
* The transport layer is taken when the output is queued, so output from before COMPRESS is not compressed
|
|
526
|
+
*
|
|
527
|
+
* @param {Object} operation `{ data, delay, chunk, chunkDelay, close }`, see OutputOperation in src/script.ts
|
|
528
|
+
*/
|
|
529
|
+
queueOutput(operation: OutputOperation): void;
|
|
530
|
+
/**
|
|
531
|
+
* Writes the output queue until it is empty or a delay stops it
|
|
532
|
+
*/
|
|
533
|
+
flushOutput(): void;
|
|
534
|
+
/**
|
|
535
|
+
* Drops the output that waits for a delay of a script rule or for its release (`defer`)
|
|
536
|
+
*/
|
|
537
|
+
clearOutputQueue(): void;
|
|
538
|
+
/**
|
|
539
|
+
* Writes output through a transport layer, or to the socket
|
|
540
|
+
*
|
|
541
|
+
* @param {Buffer} data Output
|
|
542
|
+
* @param {Object|null} transport Transport layer
|
|
543
|
+
*/
|
|
544
|
+
writeLayer(data: Buffer, transport: Transport | null): void;
|
|
545
|
+
/**
|
|
546
|
+
* Sends output through the script rule that handles its event, if there is one
|
|
547
|
+
*
|
|
548
|
+
* @param {String} event Event name: greeting, response or continuation
|
|
549
|
+
* @param {String} output The output as a binary string
|
|
550
|
+
* @param {Object} fields Context fields of the event (tag, command, description, response)
|
|
551
|
+
* @param {Function} [compile] Compiles the response that a `mutate` action changed, null drops the output
|
|
552
|
+
*/
|
|
553
|
+
scriptOutput(event: ScriptEvent, output: string, fields: Partial<ScriptContext>, compile?: (response: IMAPResponse) => string | null): void;
|
|
554
|
+
/**
|
|
555
|
+
* Sends a continuation request, `+ text`
|
|
556
|
+
*
|
|
557
|
+
* @param {String} text Human readable text, can be empty (SASL)
|
|
558
|
+
* @param {String} description Description for script rules
|
|
559
|
+
* @param {Function} [getLine] Returns the command line received so far, for the tag and the command of a literal continuation
|
|
560
|
+
*/
|
|
561
|
+
sendContinuation(text: string, description: string, getLine?: () => string): void;
|
|
498
562
|
/**
|
|
499
563
|
* Writes data to the socket, below the transport layer
|
|
500
564
|
*
|
|
@@ -512,6 +576,12 @@ declare class IMAPConnection {
|
|
|
512
576
|
* holds, is written out
|
|
513
577
|
*/
|
|
514
578
|
end(): void;
|
|
579
|
+
/**
|
|
580
|
+
* Closes the connection now, after the output written so far
|
|
581
|
+
*
|
|
582
|
+
* @param {Boolean|String} mode true ends the connection gracefully, "reset" destroys the socket (script rules)
|
|
583
|
+
*/
|
|
584
|
+
closeNow(mode: boolean | 'reset'): void;
|
|
515
585
|
/**
|
|
516
586
|
* Sends an untagged BYE, drops unprocessed input and closes the connection (RFC 3501 section 7.1.5)
|
|
517
587
|
*
|
|
@@ -564,6 +634,12 @@ declare class IMAPConnection {
|
|
|
564
634
|
closeMailbox(): void;
|
|
565
635
|
onClose(): void;
|
|
566
636
|
onError(err: Error): void;
|
|
637
|
+
/**
|
|
638
|
+
* Passes a line to the input handler (IDLE, AUTHENTICATE), unless a script rule handles it
|
|
639
|
+
*
|
|
640
|
+
* @param {String} line Input line without CRLF
|
|
641
|
+
*/
|
|
642
|
+
handleInput(line: string): void;
|
|
567
643
|
onData(chunk: Buffer): void;
|
|
568
644
|
/**
|
|
569
645
|
* Reads literal data that the current command is waiting for. The data of a command that was
|
|
@@ -752,6 +828,22 @@ declare class IMAPConnection {
|
|
|
752
828
|
* FETCH result. (This may have other names when used, like "affected".)
|
|
753
829
|
*/
|
|
754
830
|
send(response: IMAPResponse, description?: string, parsed?: CommandContext | null, data?: string | null, ...extra: any[]): void;
|
|
831
|
+
/**
|
|
832
|
+
* Sends a compiled response through the script rules that watch responses or continuations
|
|
833
|
+
*
|
|
834
|
+
* @param {Object} response Response object
|
|
835
|
+
* @param {String} compiled The compiled response
|
|
836
|
+
* @param {String} [description] Description of the response
|
|
837
|
+
* @param {Object} [parsed] The command the response belongs to
|
|
838
|
+
*/
|
|
839
|
+
scriptResponse(response: IMAPResponse, compiled: string, description?: string, parsed?: CommandContext | null): void;
|
|
840
|
+
/**
|
|
841
|
+
* Compiles a response for the wire
|
|
842
|
+
*
|
|
843
|
+
* @param {Object} response Response object
|
|
844
|
+
* @return {String|null} the response with its CRLF as a binary string, or null for an untagged response that does not compile
|
|
845
|
+
*/
|
|
846
|
+
compileResponse(response: IMAPResponse): string | null;
|
|
755
847
|
/**
|
|
756
848
|
* Sends a tagged status response to a command
|
|
757
849
|
*
|
|
@@ -848,7 +940,21 @@ declare class IMAPConnection {
|
|
|
848
940
|
* @return {String} Mailbox name as a binary string
|
|
849
941
|
*/
|
|
850
942
|
exportMailboxName(path: string): string;
|
|
851
|
-
|
|
943
|
+
/**
|
|
944
|
+
* Parses a command line and queues the command, or answers it right away when it can not run
|
|
945
|
+
*
|
|
946
|
+
* @param {String} data Command line with its literals, without the final CRLF
|
|
947
|
+
* @param {Boolean} [scripted] The line comes from a script rule with `run`, it is next in the queue
|
|
948
|
+
*/
|
|
949
|
+
scheduleCommand(data: string, scripted?: boolean): void;
|
|
950
|
+
/**
|
|
951
|
+
* Handles a command line with the script rule that matched it, after the rule's delay. With `run` the line
|
|
952
|
+
* goes through the parser and the command handler as usual afterwards
|
|
953
|
+
*
|
|
954
|
+
* @param {Object} element Queued command with the rule
|
|
955
|
+
* @param {Function} next Releases the queue
|
|
956
|
+
*/
|
|
957
|
+
runScriptedCommand(element: QueuedCommand, next: () => void): void;
|
|
852
958
|
processQueue(force?: boolean): void;
|
|
853
959
|
/**
|
|
854
960
|
* Removes messages with \Deleted flag
|
package/dist/esm/server.js
CHANGED
|
@@ -14,8 +14,11 @@ 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, literalResponse, releaseDeferred } from './script.js';
|
|
17
18
|
// longest command line (not counting literals) accepted from a client
|
|
18
19
|
const MAX_LINE_LENGTH = 1024 * 1024;
|
|
20
|
+
// milliseconds between the last output and the RST of a script rule with `close: 'reset'`
|
|
21
|
+
const RESET_DELAY = 20;
|
|
19
22
|
// largest literal accepted after login, override with the maxLiteralSize option
|
|
20
23
|
const MAX_LITERAL_SIZE = 64 * 1024 * 1024;
|
|
21
24
|
// largest literal accepted before login, enough for any user name or password
|
|
@@ -155,6 +158,9 @@ class IMAPServer extends Stream {
|
|
|
155
158
|
this.referenceNamespace = false;
|
|
156
159
|
// the session whose command is running, see IMAPServer#notify
|
|
157
160
|
this.activeConnection = null;
|
|
161
|
+
this.sessionCounter = 0;
|
|
162
|
+
// rules that make the server misbehave on purpose, from the script option or server.script.add()
|
|
163
|
+
this.script = new ServerScript(this, this.options.script);
|
|
158
164
|
// users and storage are deep copied, so that runtime changes never leak into
|
|
159
165
|
// the caller's objects or into other servers built from the same fixture.
|
|
160
166
|
// Without a prototype, user names like "__proto__" or "toString" are plain keys
|
|
@@ -1212,6 +1218,10 @@ class IMAPConnection {
|
|
|
1212
1218
|
this.socket = socket;
|
|
1213
1219
|
this.options = this.server.options;
|
|
1214
1220
|
this.state = 'Not Authenticated';
|
|
1221
|
+
this.sessionNumber = ++this.server.sessionCounter;
|
|
1222
|
+
this._outputQueue = null;
|
|
1223
|
+
this._outputTimer = null;
|
|
1224
|
+
this._deferredOutput = null;
|
|
1215
1225
|
this.secureConnection = !!this.options.secureConnection;
|
|
1216
1226
|
this._remainder = '';
|
|
1217
1227
|
this._command = '';
|
|
@@ -1241,7 +1251,7 @@ class IMAPConnection {
|
|
|
1241
1251
|
this.notificationQueue = [];
|
|
1242
1252
|
this.server.on('notify', this._notificationCallback);
|
|
1243
1253
|
this.server.connections.add(this);
|
|
1244
|
-
this.
|
|
1254
|
+
this.scriptOutput('greeting', '* OK ImapKit ready for rumble\r\n', {});
|
|
1245
1255
|
}
|
|
1246
1256
|
/**
|
|
1247
1257
|
* Writes protocol output to the client, through the transport layer if there is one
|
|
@@ -1250,12 +1260,139 @@ class IMAPConnection {
|
|
|
1250
1260
|
*/
|
|
1251
1261
|
write(data) {
|
|
1252
1262
|
const buffer = typeof data === 'string' ? Buffer.from(data, 'binary') : data;
|
|
1253
|
-
if (this.
|
|
1254
|
-
this.
|
|
1263
|
+
if (this._outputQueue) {
|
|
1264
|
+
this._outputQueue.push({ data: buffer, transport: this.transport });
|
|
1265
|
+
}
|
|
1266
|
+
else {
|
|
1267
|
+
this.writeLayer(buffer, this.transport);
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
/**
|
|
1271
|
+
* Writes output, or puts it in the output queue while earlier output waits for a delay of a script rule.
|
|
1272
|
+
* The transport layer is taken when the output is queued, so output from before COMPRESS is not compressed
|
|
1273
|
+
*
|
|
1274
|
+
* @param {Object} operation `{ data, delay, chunk, chunkDelay, close }`, see OutputOperation in src/script.ts
|
|
1275
|
+
*/
|
|
1276
|
+
queueOutput(operation) {
|
|
1277
|
+
if (!this._outputQueue && !operation.delay && !operation.chunk) {
|
|
1278
|
+
if (operation.data) {
|
|
1279
|
+
this.writeLayer(operation.data, this.transport);
|
|
1280
|
+
}
|
|
1281
|
+
if (operation.close) {
|
|
1282
|
+
this.closeNow(operation.close);
|
|
1283
|
+
}
|
|
1284
|
+
return;
|
|
1285
|
+
}
|
|
1286
|
+
this._outputQueue = this._outputQueue || [];
|
|
1287
|
+
this._outputQueue.push(Object.assign({}, operation, { transport: this.transport }));
|
|
1288
|
+
if (!this._outputTimer) {
|
|
1289
|
+
this.flushOutput();
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
/**
|
|
1293
|
+
* Writes the output queue until it is empty or a delay stops it
|
|
1294
|
+
*/
|
|
1295
|
+
flushOutput() {
|
|
1296
|
+
const queue = this._outputQueue || [];
|
|
1297
|
+
while (queue.length) {
|
|
1298
|
+
const operation = queue[0];
|
|
1299
|
+
if (operation.delay) {
|
|
1300
|
+
const delay = operation.delay;
|
|
1301
|
+
operation.delay = 0;
|
|
1302
|
+
this._outputTimer = setTimeout(() => {
|
|
1303
|
+
this._outputTimer = null;
|
|
1304
|
+
this.flushOutput();
|
|
1305
|
+
}, delay);
|
|
1306
|
+
return;
|
|
1307
|
+
}
|
|
1308
|
+
if (operation.data && operation.chunk && operation.data.length > operation.chunk) {
|
|
1309
|
+
// the rest waits for chunkDelay, like a delay of its own
|
|
1310
|
+
this.writeLayer(operation.data.subarray(0, operation.chunk), operation.transport || null);
|
|
1311
|
+
operation.data = operation.data.subarray(operation.chunk);
|
|
1312
|
+
operation.delay = operation.chunkDelay;
|
|
1313
|
+
continue;
|
|
1314
|
+
}
|
|
1315
|
+
if (operation.data) {
|
|
1316
|
+
this.writeLayer(operation.data, operation.transport || null);
|
|
1317
|
+
}
|
|
1318
|
+
queue.shift();
|
|
1319
|
+
if (operation.close) {
|
|
1320
|
+
this.closeNow(operation.close);
|
|
1321
|
+
return;
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
this._outputQueue = null;
|
|
1325
|
+
}
|
|
1326
|
+
/**
|
|
1327
|
+
* Drops the output that waits for a delay of a script rule or for its release (`defer`)
|
|
1328
|
+
*/
|
|
1329
|
+
clearOutputQueue() {
|
|
1330
|
+
if (this._outputTimer) {
|
|
1331
|
+
clearTimeout(this._outputTimer);
|
|
1332
|
+
this._outputTimer = null;
|
|
1333
|
+
}
|
|
1334
|
+
this._outputQueue = null;
|
|
1335
|
+
this._deferredOutput = null;
|
|
1336
|
+
}
|
|
1337
|
+
/**
|
|
1338
|
+
* Writes output through a transport layer, or to the socket
|
|
1339
|
+
*
|
|
1340
|
+
* @param {Buffer} data Output
|
|
1341
|
+
* @param {Object|null} transport Transport layer
|
|
1342
|
+
*/
|
|
1343
|
+
writeLayer(data, transport) {
|
|
1344
|
+
if (transport) {
|
|
1345
|
+
transport.write(data);
|
|
1255
1346
|
}
|
|
1256
1347
|
else {
|
|
1257
|
-
this.writeRaw(
|
|
1348
|
+
this.writeRaw(data);
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
/**
|
|
1352
|
+
* Sends output through the script rule that handles its event, if there is one
|
|
1353
|
+
*
|
|
1354
|
+
* @param {String} event Event name: greeting, response or continuation
|
|
1355
|
+
* @param {String} output The output as a binary string
|
|
1356
|
+
* @param {Object} fields Context fields of the event (tag, command, description, response)
|
|
1357
|
+
* @param {Function} [compile] Compiles the response that a `mutate` action changed, null drops the output
|
|
1358
|
+
*/
|
|
1359
|
+
scriptOutput(event, output, fields, compile) {
|
|
1360
|
+
const found = this.server.script.check(this, event, Object.assign({}, fields, { data: output }));
|
|
1361
|
+
if (!found) {
|
|
1362
|
+
this.write(output);
|
|
1363
|
+
return;
|
|
1364
|
+
}
|
|
1365
|
+
const { rule, context } = found;
|
|
1366
|
+
if ((rule.mutate || rule.literals) && compile && context.response) {
|
|
1367
|
+
let response = context.response;
|
|
1368
|
+
if (rule.mutate) {
|
|
1369
|
+
// a copy, a notification object is shared by every session
|
|
1370
|
+
const copy = cloneResponse(response);
|
|
1371
|
+
response = rule.mutate(copy, context) || copy;
|
|
1372
|
+
}
|
|
1373
|
+
const changed = compile(rule.literals ? literalResponse(response) : response);
|
|
1374
|
+
if (changed === null) {
|
|
1375
|
+
return;
|
|
1376
|
+
}
|
|
1377
|
+
context.data = changed;
|
|
1258
1378
|
}
|
|
1379
|
+
sendOutput(this, rule, context, Buffer.from(context.data, 'binary'));
|
|
1380
|
+
}
|
|
1381
|
+
/**
|
|
1382
|
+
* Sends a continuation request, `+ text`
|
|
1383
|
+
*
|
|
1384
|
+
* @param {String} text Human readable text, can be empty (SASL)
|
|
1385
|
+
* @param {String} description Description for script rules
|
|
1386
|
+
* @param {Function} [getLine] Returns the command line received so far, for the tag and the command of a literal continuation
|
|
1387
|
+
*/
|
|
1388
|
+
sendContinuation(text, description, getLine) {
|
|
1389
|
+
const output = '+ ' + text + '\r\n';
|
|
1390
|
+
if (!this.server.script.watches('continuation')) {
|
|
1391
|
+
this.write(output);
|
|
1392
|
+
return;
|
|
1393
|
+
}
|
|
1394
|
+
const line = getLine ? getLine() : null;
|
|
1395
|
+
this.scriptOutput('continuation', output, line === null ? { description } : { description, tag: getResponseTag(line), command: getLineCommand(line) });
|
|
1259
1396
|
}
|
|
1260
1397
|
/**
|
|
1261
1398
|
* Writes data to the socket, below the transport layer
|
|
@@ -1285,12 +1422,47 @@ class IMAPConnection {
|
|
|
1285
1422
|
* holds, is written out
|
|
1286
1423
|
*/
|
|
1287
1424
|
end() {
|
|
1425
|
+
if (this._outputQueue && this.socket) {
|
|
1426
|
+
// output still waits for a delay of a script rule
|
|
1427
|
+
this._closing = true;
|
|
1428
|
+
this._outputQueue.push({ close: true });
|
|
1429
|
+
return;
|
|
1430
|
+
}
|
|
1431
|
+
this.closeNow(true);
|
|
1432
|
+
}
|
|
1433
|
+
/**
|
|
1434
|
+
* Closes the connection now, after the output written so far
|
|
1435
|
+
*
|
|
1436
|
+
* @param {Boolean|String} mode true ends the connection gracefully, "reset" destroys the socket (script rules)
|
|
1437
|
+
*/
|
|
1438
|
+
closeNow(mode) {
|
|
1288
1439
|
const socket = this.socket;
|
|
1289
1440
|
if (!socket) {
|
|
1290
1441
|
return;
|
|
1291
1442
|
}
|
|
1292
1443
|
this._closing = true;
|
|
1293
|
-
|
|
1444
|
+
this.clearOutputQueue();
|
|
1445
|
+
if (mode === 'reset') {
|
|
1446
|
+
this.discardInput();
|
|
1447
|
+
// input that arrives while the reset waits is not processed, also input a transport layer still holds
|
|
1448
|
+
this._resetting = true;
|
|
1449
|
+
socket.pause();
|
|
1450
|
+
// a RST sent in the same tick as the last output can get lost: on macOS the client received the output but
|
|
1451
|
+
// never the RST (#81). Node has no signal for output that reached the peer, so the RST waits a little
|
|
1452
|
+
setTimeout(() => {
|
|
1453
|
+
if (socket.destroyed) {
|
|
1454
|
+
return;
|
|
1455
|
+
}
|
|
1456
|
+
// resetAndDestroy sends a TCP RST (Node 16.17), not every runtime has it
|
|
1457
|
+
if (typeof socket.resetAndDestroy === 'function') {
|
|
1458
|
+
socket.resetAndDestroy();
|
|
1459
|
+
}
|
|
1460
|
+
else {
|
|
1461
|
+
socket.destroy();
|
|
1462
|
+
}
|
|
1463
|
+
}, RESET_DELAY);
|
|
1464
|
+
}
|
|
1465
|
+
else if (this.transport) {
|
|
1294
1466
|
this.transport.end(() => socket.end());
|
|
1295
1467
|
}
|
|
1296
1468
|
else {
|
|
@@ -1391,6 +1563,7 @@ class IMAPConnection {
|
|
|
1391
1563
|
this.transport.destroy();
|
|
1392
1564
|
this.transport = null;
|
|
1393
1565
|
}
|
|
1566
|
+
this.clearOutputQueue();
|
|
1394
1567
|
this.server.removeListener('notify', this._notificationCallback);
|
|
1395
1568
|
this.server.connections.delete(this);
|
|
1396
1569
|
}
|
|
@@ -1406,9 +1579,27 @@ class IMAPConnection {
|
|
|
1406
1579
|
// socket is already gone
|
|
1407
1580
|
}
|
|
1408
1581
|
}
|
|
1582
|
+
/**
|
|
1583
|
+
* Passes a line to the input handler (IDLE, AUTHENTICATE), unless a script rule handles it
|
|
1584
|
+
*
|
|
1585
|
+
* @param {String} line Input line without CRLF
|
|
1586
|
+
*/
|
|
1587
|
+
handleInput(line) {
|
|
1588
|
+
const inputHandler = this.inputHandler;
|
|
1589
|
+
const found = this.server.script.check(this, 'input', { data: line });
|
|
1590
|
+
if (found) {
|
|
1591
|
+
handleLine(this, found.rule, found.context, () => inputHandler(line));
|
|
1592
|
+
}
|
|
1593
|
+
else {
|
|
1594
|
+
inputHandler(line);
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1409
1597
|
onData(chunk) {
|
|
1410
1598
|
let match;
|
|
1411
1599
|
let str;
|
|
1600
|
+
if (this._resetting) {
|
|
1601
|
+
return;
|
|
1602
|
+
}
|
|
1412
1603
|
// everything in one read arrived before anything this read causes to be sent
|
|
1413
1604
|
this._readCount = (this._readCount || 0) + 1;
|
|
1414
1605
|
str = (chunk || '').toString('binary');
|
|
@@ -1477,7 +1668,7 @@ class IMAPConnection {
|
|
|
1477
1668
|
this.sendBad(getResponseTag(line), 'Literal data must wait for the continuation request', 'LITERAL TOO EARLY', line);
|
|
1478
1669
|
}
|
|
1479
1670
|
else if (this.inputHandler) {
|
|
1480
|
-
this.
|
|
1671
|
+
this.handleInput(line);
|
|
1481
1672
|
}
|
|
1482
1673
|
else {
|
|
1483
1674
|
this.scheduleCommand(line);
|
|
@@ -1526,7 +1717,9 @@ class IMAPConnection {
|
|
|
1526
1717
|
this._earlyLiteral = true;
|
|
1527
1718
|
}
|
|
1528
1719
|
else if (!this._earlyLiteral) {
|
|
1529
|
-
|
|
1720
|
+
// the line is needed only by script rules that watch continuations
|
|
1721
|
+
const head = str.substr(0, match.index) + marker;
|
|
1722
|
+
this.sendContinuation('Go ahead', 'LITERAL', () => this._command + head);
|
|
1530
1723
|
}
|
|
1531
1724
|
}
|
|
1532
1725
|
this._remainder = '';
|
|
@@ -1602,12 +1795,7 @@ class IMAPConnection {
|
|
|
1602
1795
|
// not a command, e.g. a SASL response
|
|
1603
1796
|
return literal8 ? refuse('Literal8 is not allowed here') : false;
|
|
1604
1797
|
}
|
|
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
|
-
}
|
|
1798
|
+
const command = getLineCommand(line);
|
|
1611
1799
|
if (!COMMAND_REGEX.test(command) || !this.server.getCommandHandler(command)) {
|
|
1612
1800
|
return refuse('Unknown command');
|
|
1613
1801
|
}
|
|
@@ -2027,6 +2215,44 @@ class IMAPConnection {
|
|
|
2027
2215
|
});
|
|
2028
2216
|
}
|
|
2029
2217
|
}
|
|
2218
|
+
const compiled = this.compileResponse(response);
|
|
2219
|
+
if (compiled === null) {
|
|
2220
|
+
return;
|
|
2221
|
+
}
|
|
2222
|
+
if (this._deferredOutput) {
|
|
2223
|
+
// responses that `defer` rules hold back go before the first response of another command
|
|
2224
|
+
releaseDeferred(this, parsed && parsed.command && parsed.tag && parsed.tag !== '*' ? parsed.tag : null, false);
|
|
2225
|
+
}
|
|
2226
|
+
this.scriptResponse(response, compiled, description, parsed);
|
|
2227
|
+
if (this._deferredOutput && response.tag !== '*' && response.tag !== '+') {
|
|
2228
|
+
releaseDeferred(this, response.tag, true);
|
|
2229
|
+
}
|
|
2230
|
+
}
|
|
2231
|
+
/**
|
|
2232
|
+
* Sends a compiled response through the script rules that watch responses or continuations
|
|
2233
|
+
*
|
|
2234
|
+
* @param {Object} response Response object
|
|
2235
|
+
* @param {String} compiled The compiled response
|
|
2236
|
+
* @param {String} [description] Description of the response
|
|
2237
|
+
* @param {Object} [parsed] The command the response belongs to
|
|
2238
|
+
*/
|
|
2239
|
+
scriptResponse(response, compiled, description, parsed) {
|
|
2240
|
+
const event = response.tag === '+' ? 'continuation' : 'response';
|
|
2241
|
+
if (!this.server.script.watches(event)) {
|
|
2242
|
+
this.write(compiled);
|
|
2243
|
+
return;
|
|
2244
|
+
}
|
|
2245
|
+
// script rules see the response after every plugin and the core changed it. Without a command, an
|
|
2246
|
+
// unsolicited response belongs to the command that runs or idles
|
|
2247
|
+
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));
|
|
2248
|
+
}
|
|
2249
|
+
/**
|
|
2250
|
+
* Compiles a response for the wire
|
|
2251
|
+
*
|
|
2252
|
+
* @param {Object} response Response object
|
|
2253
|
+
* @return {String|null} the response with its CRLF as a binary string, or null for an untagged response that does not compile
|
|
2254
|
+
*/
|
|
2255
|
+
compileResponse(response) {
|
|
2030
2256
|
let compiled;
|
|
2031
2257
|
try {
|
|
2032
2258
|
compiled = imapHandler.compiler(response, this.compilerOptions);
|
|
@@ -2037,14 +2263,14 @@ class IMAPConnection {
|
|
|
2037
2263
|
console.log('Failed to compile response: %s', err.message);
|
|
2038
2264
|
}
|
|
2039
2265
|
if (response.tag === '*') {
|
|
2040
|
-
return;
|
|
2266
|
+
return null;
|
|
2041
2267
|
}
|
|
2042
2268
|
compiled = response.tag + ' NO [SERVERBUG] Failed to compile response';
|
|
2043
2269
|
}
|
|
2044
2270
|
if (this.options.debug) {
|
|
2045
2271
|
console.log('SEND: %s', compiled);
|
|
2046
2272
|
}
|
|
2047
|
-
|
|
2273
|
+
return compiled + '\r\n';
|
|
2048
2274
|
}
|
|
2049
2275
|
/**
|
|
2050
2276
|
* Sends a tagged status response to a command
|
|
@@ -2231,9 +2457,23 @@ class IMAPConnection {
|
|
|
2231
2457
|
exportMailboxName(path) {
|
|
2232
2458
|
return path;
|
|
2233
2459
|
}
|
|
2234
|
-
|
|
2460
|
+
/**
|
|
2461
|
+
* Parses a command line and queues the command, or answers it right away when it can not run
|
|
2462
|
+
*
|
|
2463
|
+
* @param {String} data Command line with its literals, without the final CRLF
|
|
2464
|
+
* @param {Boolean} [scripted] The line comes from a script rule with `run`, it is next in the queue
|
|
2465
|
+
*/
|
|
2466
|
+
scheduleCommand(data, scripted) {
|
|
2235
2467
|
let parsed;
|
|
2236
2468
|
const tag = getResponseTag(data);
|
|
2469
|
+
// the rule is chosen when the line arrives, the state it matches is the state at that moment. The rule
|
|
2470
|
+
// acts when the command's turn comes, so that its output keeps the order of the responses
|
|
2471
|
+
const found = !scripted && this.server.script.watches('command') ? this.server.script.check(this, 'command', { data, tag, command: getLineCommand(data) }) : null;
|
|
2472
|
+
if (found) {
|
|
2473
|
+
this._commandQueue.push({ parsed: { tag, command: found.context.command || '' }, data, script: found });
|
|
2474
|
+
this.processQueue();
|
|
2475
|
+
return;
|
|
2476
|
+
}
|
|
2237
2477
|
try {
|
|
2238
2478
|
// server.parserOptions are the defaults of plugins, connection.parserOptions win
|
|
2239
2479
|
parsed = imapHandler.parser(data, Object.assign({ literalPlus: this.server.literalPlus }, this.server.parserOptions, this.parserOptions));
|
|
@@ -2280,10 +2520,14 @@ class IMAPConnection {
|
|
|
2280
2520
|
this.sendStatus(parsed, data, 'BAD', 'Commands with message sequence numbers must wait for the completion of earlier commands');
|
|
2281
2521
|
return;
|
|
2282
2522
|
}
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2523
|
+
const element = { parsed, data };
|
|
2524
|
+
if (scripted) {
|
|
2525
|
+
// processQueue runs it once the script rule released the queue
|
|
2526
|
+
this._commandQueue.unshift(element);
|
|
2527
|
+
}
|
|
2528
|
+
else {
|
|
2529
|
+
this._commandQueue.push(element);
|
|
2530
|
+
}
|
|
2287
2531
|
this.processQueue();
|
|
2288
2532
|
}
|
|
2289
2533
|
else if (/^AUTHENTICATE /i.test(parsed.command)) {
|
|
@@ -2312,6 +2556,33 @@ class IMAPConnection {
|
|
|
2312
2556
|
}, 'UNKNOWN COMMAND', parsed, data);
|
|
2313
2557
|
}
|
|
2314
2558
|
}
|
|
2559
|
+
/**
|
|
2560
|
+
* Handles a command line with the script rule that matched it, after the rule's delay. With `run` the line
|
|
2561
|
+
* goes through the parser and the command handler as usual afterwards
|
|
2562
|
+
*
|
|
2563
|
+
* @param {Object} element Queued command with the rule
|
|
2564
|
+
* @param {Function} next Releases the queue
|
|
2565
|
+
*/
|
|
2566
|
+
runScriptedCommand(element, next) {
|
|
2567
|
+
const { rule, context } = element.script;
|
|
2568
|
+
const act = () => {
|
|
2569
|
+
if (!this.socket || this._closing) {
|
|
2570
|
+
return next();
|
|
2571
|
+
}
|
|
2572
|
+
handleLine(this, rule, context, () => {
|
|
2573
|
+
// the line is not running yet, it must not count as an earlier command (RFC 3501 section 5.5)
|
|
2574
|
+
this._runningCommand = null;
|
|
2575
|
+
this.scheduleCommand(element.data, true);
|
|
2576
|
+
});
|
|
2577
|
+
next();
|
|
2578
|
+
};
|
|
2579
|
+
if (rule.delay) {
|
|
2580
|
+
setTimeout(act, rule.delay);
|
|
2581
|
+
}
|
|
2582
|
+
else {
|
|
2583
|
+
act();
|
|
2584
|
+
}
|
|
2585
|
+
}
|
|
2315
2586
|
processQueue(force) {
|
|
2316
2587
|
if (!force && this._processing) {
|
|
2317
2588
|
return;
|
|
@@ -2344,6 +2615,10 @@ class IMAPConnection {
|
|
|
2344
2615
|
this.processQueue(true);
|
|
2345
2616
|
}
|
|
2346
2617
|
};
|
|
2618
|
+
if (element.script) {
|
|
2619
|
+
this.runScriptedCommand(element, next);
|
|
2620
|
+
return;
|
|
2621
|
+
}
|
|
2347
2622
|
if (options.states && options.states.indexOf(this.state) < 0) {
|
|
2348
2623
|
this.sendStatus(element.parsed, element.data, 'BAD', stateError(command, this.state));
|
|
2349
2624
|
return next();
|
|
@@ -2386,7 +2661,12 @@ class IMAPConnection {
|
|
|
2386
2661
|
try {
|
|
2387
2662
|
// changes made while the handler runs are attributed to this session (the `origin` of notifications)
|
|
2388
2663
|
this.server.activeConnection = this;
|
|
2664
|
+
const inputHandler = this.inputHandler;
|
|
2389
2665
|
this.server.getCommandHandler(element.parsed.command)(this, element.parsed, element.data, next);
|
|
2666
|
+
if (this.inputHandler && this.inputHandler !== inputHandler) {
|
|
2667
|
+
// the command reads the lines that follow (IDLE, AUTHENTICATE), script rules match them with it
|
|
2668
|
+
this.inputCommand = { tag: element.parsed.tag, command: element.parsed.command };
|
|
2669
|
+
}
|
|
2390
2670
|
}
|
|
2391
2671
|
catch (E) {
|
|
2392
2672
|
const ex = E;
|
|
@@ -2507,6 +2787,41 @@ class IMAPConnection {
|
|
|
2507
2787
|
}, mailbox, ignoreSelf || ignoreExists ? this : false);
|
|
2508
2788
|
}
|
|
2509
2789
|
}
|
|
2790
|
+
/**
|
|
2791
|
+
* Copies a response tree for the `mutate` action of a script rule: arrays and plain objects are copied,
|
|
2792
|
+
* other values (Buffers) are shared
|
|
2793
|
+
*
|
|
2794
|
+
* @param {*} value Response or a part of it
|
|
2795
|
+
* @return {*} Copy
|
|
2796
|
+
*/
|
|
2797
|
+
function cloneResponse(value) {
|
|
2798
|
+
if (Array.isArray(value)) {
|
|
2799
|
+
return value.map(cloneResponse);
|
|
2800
|
+
}
|
|
2801
|
+
if (value && typeof value === 'object' && Object.getPrototypeOf(value) === Object.prototype) {
|
|
2802
|
+
const copy = {};
|
|
2803
|
+
for (const key of Object.keys(value)) {
|
|
2804
|
+
copy[key] = cloneResponse(value[key]);
|
|
2805
|
+
}
|
|
2806
|
+
return copy;
|
|
2807
|
+
}
|
|
2808
|
+
return value;
|
|
2809
|
+
}
|
|
2810
|
+
/**
|
|
2811
|
+
* Finds the command name in a command line that may not parse: tag SP command, and for UID and AUTHENTICATE
|
|
2812
|
+
* the word that follows
|
|
2813
|
+
*
|
|
2814
|
+
* @param {String} line Command line
|
|
2815
|
+
* @return {String} Command name in upper case, can be empty
|
|
2816
|
+
*/
|
|
2817
|
+
function getLineCommand(line) {
|
|
2818
|
+
const words = line.match(/^[^ ]* ([^ ]*)(?: ([^ ]*))?/);
|
|
2819
|
+
let command = ((words && words[1]) || '').toUpperCase();
|
|
2820
|
+
if (command === 'UID' || command === 'AUTHENTICATE') {
|
|
2821
|
+
command += ' ' + ((words && words[2]) || '').toUpperCase();
|
|
2822
|
+
}
|
|
2823
|
+
return command;
|
|
2824
|
+
}
|
|
2510
2825
|
/**
|
|
2511
2826
|
* Formats a mailbox name for a response: an atom when possible, otherwise a string. NIL and names
|
|
2512
2827
|
* like \\Foo would not read back as mailbox names, so these are strings as well
|