imapkit 4.2.0 → 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 CHANGED
@@ -410,21 +410,16 @@ const server = imapkit({
410
410
  ]
411
411
  });
412
412
 
413
- // a rule added later, it returns a handle
414
- const rule = server.script.add({
415
- on: 'response',
416
- command: 'FETCH',
417
- untagged: true,
418
- // strings in the response tree become literals, valid IMAP that a client must handle
419
- mutate: response => {
420
- const walk = list =>
421
- list.forEach((node, i) => (Array.isArray(node) ? walk(node) : typeof node === 'string' && (list[i] = { type: 'LITERAL', value: node })));
422
- walk(response.attributes);
423
- }
424
- });
413
+ // rules added later return handles
414
+ const [literals, late] = server.script.add([
415
+ // every string the grammar allows is sent as a literal, valid IMAP that a client must handle
416
+ { on: 'response', untagged: true, literals: true },
417
+ // the FETCH response of UID 2 arrives after the tagged OK, like some servers do now and then
418
+ { on: 'response', command: 'UID FETCH', untagged: true, match: /UID 2\b/, times: 1, defer: 'tagged' }
419
+ ]);
425
420
  // ... run the client
426
- assert.strictEqual(rule.hits, 1);
427
- rule.remove();
421
+ assert.strictEqual(late.hits, 1);
422
+ literals.remove();
428
423
  ```
429
424
 
430
425
  Every rule watches one event (`on`):
@@ -452,17 +447,19 @@ The matchers of a rule all have to match. Rules are checked in the order they we
452
447
 
453
448
  The actions say what happens instead of the usual behavior. Strings are sent as they are (binary strings, one character per octet, or UTF-8 when they have characters above U+00FF) without an added CRLF, and `$TAG` in a string is replaced with the tag of the command. A Buffer is sent as it is, a function gets the event context and returns a string or a Buffer.
454
449
 
455
- | Action | Events | Effect |
456
- | --------------------- | ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
457
- | `send` | all | output events: bytes sent instead of the output. command and input: bytes sent instead of processing the line |
458
- | `run` | command, input | process the line as usual after `send` (to add output before the real response) |
459
- | `drop` | all | output events: send nothing. command and input: ignore the line, the client gets no answer |
460
- | `mutate` | response, continuation | `(response, context)` gets a copy of the response object before it is compiled and changes it, or returns another one. Continuation requests have a response object only when a plugin sends them with `connection.send()` (the error challenges of XOAUTH2 and OAUTHBEARER), other events ignore `mutate`. A tagged response that does not compile then is sent as `NO [SERVERBUG]`, an untagged one is dropped. Use `send` for output that is not valid IMAP |
461
- | `before`, `after` | output events | bytes sent before or after the output, e.g. an unsolicited response |
462
- | `delay` | all but input | milliseconds to wait before the output goes out, all later output waits behind it. For a command, the wait before the rule acts or the command runs, later commands wait too |
463
- | `chunk`, `chunkDelay` | all | write the bytes in pieces of `chunk` octets, `chunkDelay` milliseconds apart (default 10) |
464
- | `truncate` | all | send only this many octets of the bytes, then close the connection |
465
- | `close` | all | close the connection after the bytes are sent, `'reset'` destroys the socket instead (a TCP RST where the runtime supports it) |
450
+ | Action | Events | Effect |
451
+ | --------------------- | ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
452
+ | `send` | all | output events: bytes sent instead of the output. command and input: bytes sent instead of processing the line |
453
+ | `run` | command, input | process the line as usual after `send` (to add output before the real response) |
454
+ | `drop` | all | output events: send nothing. command and input: ignore the line, the client gets no answer |
455
+ | `mutate` | response, continuation | `(response, context)` gets a copy of the response object before it is compiled and changes it, or returns another one. Continuation requests have a response object only when a plugin sends them with `connection.send()` (the error challenges of XOAUTH2 and OAUTHBEARER), other events ignore `mutate`. A tagged response that does not compile then is sent as `NO [SERVERBUG]`, an untagged one is dropped. Use `send` for output that is not valid IMAP |
456
+ | `literals` | response | sends every string of the response that the grammar allows (string, nstring, astring) as a literal. The positions that take only a quoted string stay quoted: the hierarchy delimiter of LIST, LSUB and NAMESPACE, CHILDINFO values, INTERNALDATE and SAVEDATE, and the media types `"TEXT"` and `"MESSAGE" "RFC822"` in a body structure (RFC 9051 section 9, RFC 5258 section 6). Atoms, response codes and text stay as they are. Works together with `mutate`, after it |
457
+ | `defer` | response | holds an untagged response back: `'tagged'` sends it right after the tagged response of its command, `'next'` with the answer to the next command, before its first response. Needs `untagged: true`. `send`, `before` and `after` change the held output, `drop`, `delay`, `chunk`, `truncate` and `close` can not be combined with it. A response that does not belong to a command (one that arrives in IDLE belongs to IDLE) is held for the next tagged response or command. Held responses are dropped when the connection closes |
458
+ | `before`, `after` | output events | bytes sent before or after the output, e.g. an unsolicited response |
459
+ | `delay` | all but input | milliseconds to wait before the output goes out, all later output waits behind it. For a command, the wait before the rule acts or the command runs, later commands wait too |
460
+ | `chunk`, `chunkDelay` | all | write the bytes in pieces of `chunk` octets, `chunkDelay` milliseconds apart (default 10) |
461
+ | `truncate` | all | send only this many octets of the bytes, then close the connection |
462
+ | `close` | all | close the connection after the bytes are sent, `'reset'` destroys the socket instead (a TCP RST where the runtime supports it), 20 ms after the bytes so that the RST does not overtake them. Input that arrives meanwhile is not processed |
466
463
 
467
464
  A rule needs at least one action, and for command and input events `chunk` and `truncate` need `send`. A rule with only `delay` (and `run`) delays the line and then processes it as usual. Rules are checked when they are added: an unknown option, an option that does not apply to the event, or an invalid value throws a `TypeError`, so a typo can not turn into a rule that never fires.
468
465
 
@@ -56,6 +56,13 @@ export interface ScriptRule {
56
56
  nth?: number | undefined;
57
57
  /** the rule fires this many times at most (default unlimited) */
58
58
  times?: number | undefined;
59
+ /** sends every string of the response that the grammar allows as a literal (response events) */
60
+ literals?: boolean | undefined;
61
+ /**
62
+ * holds an untagged response back: `"tagged"` sends it after the tagged response of its command, `"next"` with the
63
+ * first response that belongs to another command (response events)
64
+ */
65
+ defer?: 'tagged' | 'next' | undefined;
59
66
  /** changes or replaces the response object before it is compiled (response events) */
60
67
  mutate?: ((response: IMAPResponse, context: ScriptContext) => IMAPResponse | void) | undefined;
61
68
  /** bytes to send instead of the output, or instead of answering the command or input line. `$TAG` in a string is the tag */
@@ -170,6 +177,32 @@ export declare class ServerScript {
170
177
  * @param {Buffer} output The output
171
178
  */
172
179
  export declare function sendOutput(connection: IMAPConnection, rule: ScriptRule, context: ScriptContext, output: Buffer): void;
180
+ /** Output held back by `defer`, see releaseDeferred */
181
+ export interface DeferredOutput {
182
+ mode: 'tagged' | 'next';
183
+ /** tag of the command the response belongs to, null for an unsolicited response */
184
+ tag: string | null;
185
+ data: Buffer;
186
+ }
187
+ /**
188
+ * Sends the held back responses whose turn has come (`defer`). Before a response of another command go the ones held
189
+ * with `"next"`, after the tagged response of their command the ones held with `"tagged"`. A response held without a
190
+ * command goes with the next command, or after the next tagged response
191
+ *
192
+ * @param {Object} connection IMAP connection
193
+ * @param {String|null} tag Tag of the command the response that is sent belongs to
194
+ * @param {Boolean} tagged false before the response is sent, true after a tagged response was sent
195
+ */
196
+ export declare function releaseDeferred(connection: IMAPConnection, tag: string | null, tagged: boolean): void;
197
+ /**
198
+ * Turns the strings of a response into literals wherever the grammar allows a string (RFC 9051 section 9: string,
199
+ * nstring and astring). Positions that take only a quoted string stay quoted: the hierarchy delimiter of LIST, LSUB
200
+ * and NAMESPACE, CHILDINFO values (RFC 5258 section 6), date-time values and the quoted media types of a body structure
201
+ *
202
+ * @param {Object} response Response object
203
+ * @return {Object} a copy of the response with literals, the response is not changed
204
+ */
205
+ export declare function literalResponse(response: IMAPResponse): IMAPResponse;
173
206
  /**
174
207
  * Handles a command or input line with the rule that matched it: sends the bytes of `send`, closes the connection,
175
208
  * and processes the line as usual with `run` (or when the rule only delays it)
@@ -2,6 +2,8 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ServerScript = void 0;
4
4
  exports.sendOutput = sendOutput;
5
+ exports.releaseDeferred = releaseDeferred;
6
+ exports.literalResponse = literalResponse;
5
7
  exports.handleLine = handleLine;
6
8
  const EVENTS = ['greeting', 'command', 'input', 'response', 'continuation'];
7
9
  const OUTPUT_EVENTS = ['greeting', 'response', 'continuation'];
@@ -21,6 +23,8 @@ const KEYS = {
21
23
  nth: EVENTS,
22
24
  times: EVENTS,
23
25
  mutate: ['response', 'continuation'],
26
+ literals: ['response'],
27
+ defer: ['response'],
24
28
  send: EVENTS,
25
29
  before: OUTPUT_EVENTS,
26
30
  after: OUTPUT_EVENTS,
@@ -32,7 +36,7 @@ const KEYS = {
32
36
  truncate: EVENTS,
33
37
  close: EVENTS
34
38
  };
35
- const ACTIONS = ['mutate', 'send', 'before', 'after', 'run', 'drop', 'delay', 'chunk', 'truncate', 'close'];
39
+ const ACTIONS = ['mutate', 'literals', 'defer', 'send', 'before', 'after', 'run', 'drop', 'delay', 'chunk', 'truncate', 'close'];
36
40
  const DEFAULT_CHUNK_DELAY = 10;
37
41
  // matchers that list accepted values, kept as Sets so that matching an event allocates nothing
38
42
  const SET_MATCHERS = ['command', 'description', 'session', 'state', 'user', 'mailbox'];
@@ -110,6 +114,22 @@ function validateRule(rule) {
110
114
  throw new TypeError('Script rule option "' + key + '" must be a string, a Buffer or a function');
111
115
  }
112
116
  }
117
+ if (rule.literals !== undefined && typeof rule.literals !== 'boolean') {
118
+ throw new TypeError('Script rule option "literals" must be true or false');
119
+ }
120
+ if (rule.defer !== undefined) {
121
+ if (rule.defer !== 'tagged' && rule.defer !== 'next') {
122
+ throw new TypeError('Script rule option "defer" must be "tagged" or "next"');
123
+ }
124
+ if (rule.untagged !== true) {
125
+ // a tagged response ends its command, there is nothing to hold it back for
126
+ throw new TypeError('Script rule option "defer" needs "untagged": true');
127
+ }
128
+ const conflict = ['drop', 'delay', 'chunk', 'truncate', 'close'].find(key => rule[key] !== undefined && rule[key] !== false);
129
+ if (conflict) {
130
+ throw new TypeError('Script rule option "defer" can not be combined with "' + conflict + '"');
131
+ }
132
+ }
113
133
  if (rule.close !== undefined && typeof rule.close !== 'boolean' && rule.close !== 'reset') {
114
134
  throw new TypeError('Script rule option "close" must be true, false or "reset"');
115
135
  }
@@ -292,8 +312,137 @@ function scriptContext(connection, event, fields) {
292
312
  function sendOutput(connection, rule, context, output) {
293
313
  const replaced = rule.drop ? Buffer.alloc(0) : resolveBytes(rule.send, context) || output;
294
314
  const parts = [resolveBytes(rule.before, context), replaced, resolveBytes(rule.after, context)].filter((part) => !!part);
315
+ if (rule.defer) {
316
+ (connection._deferredOutput ||= []).push({ mode: rule.defer, tag: context.tag, data: Buffer.concat(parts) });
317
+ return;
318
+ }
295
319
  sendBytes(connection, rule, Buffer.concat(parts), rule.delay);
296
320
  }
321
+ /**
322
+ * Sends the held back responses whose turn has come (`defer`). Before a response of another command go the ones held
323
+ * with `"next"`, after the tagged response of their command the ones held with `"tagged"`. A response held without a
324
+ * command goes with the next command, or after the next tagged response
325
+ *
326
+ * @param {Object} connection IMAP connection
327
+ * @param {String|null} tag Tag of the command the response that is sent belongs to
328
+ * @param {Boolean} tagged false before the response is sent, true after a tagged response was sent
329
+ */
330
+ function releaseDeferred(connection, tag, tagged) {
331
+ const ready = [];
332
+ const waiting = [];
333
+ for (const entry of connection._deferredOutput) {
334
+ const due = tagged ? entry.mode === 'tagged' && (entry.tag === null || entry.tag === tag) : entry.mode === 'next' && tag !== null && entry.tag !== tag;
335
+ (due ? ready : waiting).push(entry);
336
+ }
337
+ if (ready.length) {
338
+ connection._deferredOutput = waiting.length ? waiting : null;
339
+ ready.forEach(entry => connection.write(entry.data));
340
+ }
341
+ }
342
+ /**
343
+ * Turns strings into literals: plain strings and STRING nodes, also in lists. Atoms, numbers, NIL, text and
344
+ * response codes (SECTION) stay as they are, a response code takes no literals (RFC 9051 section 9 resp-text-code)
345
+ *
346
+ * @param {*} node Response attribute
347
+ * @return {*} the attribute with literals, a new object where something changed
348
+ */
349
+ function literalValue(node) {
350
+ if (typeof node === 'string') {
351
+ return { type: 'LITERAL', value: node };
352
+ }
353
+ if (Array.isArray(node)) {
354
+ return node.map(literalValue);
355
+ }
356
+ if (node && typeof node === 'object' && node.type === 'STRING') {
357
+ return { type: 'LITERAL', value: node.value };
358
+ }
359
+ return node;
360
+ }
361
+ const nodeText = (node) => String(typeof node === 'string' ? node : node && typeof node === 'object' ? node.value : '').toUpperCase();
362
+ /**
363
+ * Turns the strings of a body structure into literals, except the media types that the grammar writes as quoted
364
+ * strings: `"TEXT"` (media-text) and `"MESSAGE" "RFC822"` or `"GLOBAL"` (media-message, RFC 9051 section 9). A literal
365
+ * "TEXT" would read as a basic part, and the lines count after it would break the grammar
366
+ *
367
+ * @param {*} body Body structure, a list
368
+ * @return {*} the body structure with literals
369
+ */
370
+ function literalBody(body) {
371
+ if (!Array.isArray(body)) {
372
+ return literalValue(body);
373
+ }
374
+ if (Array.isArray(body[0])) {
375
+ // body-type-mpart: the parts, then the subtype and the extension data
376
+ const subtype = body.findIndex(item => !Array.isArray(item));
377
+ return body.map((item, i) => (subtype < 0 || i < subtype ? literalBody(item) : literalValue(item)));
378
+ }
379
+ const type = nodeText(body[0]);
380
+ const message = type === 'MESSAGE' && ['RFC822', 'GLOBAL'].includes(nodeText(body[1]));
381
+ return body.map((item, i) => {
382
+ if ((i === 0 && (type === 'TEXT' || message)) || (i === 1 && message)) {
383
+ return item;
384
+ }
385
+ // body-type-msg: the envelope, then the body of the encapsulated message
386
+ return i === 8 && message ? literalBody(item) : literalValue(item);
387
+ });
388
+ }
389
+ // FETCH items with a date-time value, which is a quoted string (RFC 9051 section 9, SAVEDATE: RFC 8514 section 4)
390
+ const DATE_TIME_ITEMS = new Set(['INTERNALDATE', 'SAVEDATE']);
391
+ /**
392
+ * Turns the strings of a response into literals wherever the grammar allows a string (RFC 9051 section 9: string,
393
+ * nstring and astring). Positions that take only a quoted string stay quoted: the hierarchy delimiter of LIST, LSUB
394
+ * and NAMESPACE, CHILDINFO values (RFC 5258 section 6), date-time values and the quoted media types of a body structure
395
+ *
396
+ * @param {Object} response Response object
397
+ * @return {Object} a copy of the response with literals, the response is not changed
398
+ */
399
+ function literalResponse(response) {
400
+ const attributes = Array.isArray(response.attributes) ? response.attributes : null;
401
+ if (!attributes) {
402
+ return response;
403
+ }
404
+ const command = nodeText(response.command);
405
+ const name = nodeText(attributes[1]);
406
+ let converted;
407
+ if (name === 'FETCH' || name === 'UIDFETCH') {
408
+ // message-data: number SP "FETCH" SP msg-att, the items are name and value pairs
409
+ const items = Array.isArray(attributes[2]) ? attributes[2] : [];
410
+ const fetched = items.map((item, i) => {
411
+ if (i % 2 === 0) {
412
+ return item;
413
+ }
414
+ const key = items[i - 1];
415
+ const itemName = key && !key.section ? nodeText(key) : '';
416
+ if (DATE_TIME_ITEMS.has(itemName)) {
417
+ return item;
418
+ }
419
+ return itemName === 'BODY' || itemName === 'BODYSTRUCTURE' ? literalBody(item) : literalValue(item);
420
+ });
421
+ converted = attributes.map((item, i) => (i === 2 ? fetched : item));
422
+ }
423
+ else if (command === 'LIST' || command === 'LSUB') {
424
+ // mailbox-list: flags, the delimiter (quoted only), the name and the extended data items
425
+ converted = attributes.map((item, i) => {
426
+ if (i === 1) {
427
+ return item;
428
+ }
429
+ if (i === 3 && Array.isArray(item)) {
430
+ return item.map((value, j) => (j % 2 === 1 && nodeText(item[j - 1]) === 'CHILDINFO' ? value : literalValue(value)));
431
+ }
432
+ return literalValue(item);
433
+ });
434
+ }
435
+ else if (command === 'NAMESPACE') {
436
+ // namespace-descr: "(" string SP (DQUOTE QUOTED-CHAR DQUOTE / nil) [extensions] ")"
437
+ converted = attributes.map(namespace => Array.isArray(namespace)
438
+ ? namespace.map(descr => (Array.isArray(descr) ? descr.map((value, i) => (i === 1 ? value : literalValue(value))) : descr))
439
+ : namespace);
440
+ }
441
+ else {
442
+ converted = attributes.map(literalValue);
443
+ }
444
+ return Object.assign({}, response, { attributes: converted });
445
+ }
297
446
  /**
298
447
  * Sends the bytes of a rule with its timing, `truncate` closes the connection after the cut
299
448
  *
@@ -4,7 +4,7 @@ import tls from 'node:tls';
4
4
  import type { CompilerOptions, ParserOptions } from 'imap-handler';
5
5
  import type { ResolvedCommandOptions } from './command-states.js';
6
6
  import { ServerScript } from './script.js';
7
- import type { OutputOperation, ScriptContext, ScriptEvent, ScriptRule } from './script.js';
7
+ import type { DeferredOutput, OutputOperation, ScriptContext, ScriptEvent, ScriptRule } from './script.js';
8
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';
9
9
  declare const TAG_REGEX: RegExp;
10
10
  /**
@@ -509,6 +509,10 @@ declare class IMAPConnection {
509
509
  /** output that waits behind a delay of a script rule, null when output is written right away */
510
510
  _outputQueue: OutputOperation[] | null;
511
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;
512
516
  constructor(server: IMAPServer, socket: net.Socket);
513
517
  /**
514
518
  * Writes protocol output to the client, through the transport layer if there is one
@@ -528,7 +532,7 @@ declare class IMAPConnection {
528
532
  */
529
533
  flushOutput(): void;
530
534
  /**
531
- * Drops the output that waits for a delay of a script rule
535
+ * Drops the output that waits for a delay of a script rule or for its release (`defer`)
532
536
  */
533
537
  clearOutputQueue(): void;
534
538
  /**
@@ -824,6 +828,15 @@ declare class IMAPConnection {
824
828
  * FETCH result. (This may have other names when used, like "affected".)
825
829
  */
826
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;
827
840
  /**
828
841
  * Compiles a response for the wire
829
842
  *
@@ -57,6 +57,8 @@ const bundledCert = __importStar(require("./cert.js"));
57
57
  const script_js_1 = require("./script.js");
58
58
  // longest command line (not counting literals) accepted from a client
59
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;
60
62
  // largest literal accepted after login, override with the maxLiteralSize option
61
63
  const MAX_LITERAL_SIZE = 64 * 1024 * 1024;
62
64
  // largest literal accepted before login, enough for any user name or password
@@ -1261,6 +1263,7 @@ class IMAPConnection {
1261
1263
  this.sessionNumber = ++this.server.sessionCounter;
1262
1264
  this._outputQueue = null;
1263
1265
  this._outputTimer = null;
1266
+ this._deferredOutput = null;
1264
1267
  this.secureConnection = !!this.options.secureConnection;
1265
1268
  this._remainder = '';
1266
1269
  this._command = '';
@@ -1363,7 +1366,7 @@ class IMAPConnection {
1363
1366
  this._outputQueue = null;
1364
1367
  }
1365
1368
  /**
1366
- * Drops the output that waits for a delay of a script rule
1369
+ * Drops the output that waits for a delay of a script rule or for its release (`defer`)
1367
1370
  */
1368
1371
  clearOutputQueue() {
1369
1372
  if (this._outputTimer) {
@@ -1371,6 +1374,7 @@ class IMAPConnection {
1371
1374
  this._outputTimer = null;
1372
1375
  }
1373
1376
  this._outputQueue = null;
1377
+ this._deferredOutput = null;
1374
1378
  }
1375
1379
  /**
1376
1380
  * Writes output through a transport layer, or to the socket
@@ -1401,10 +1405,14 @@ class IMAPConnection {
1401
1405
  return;
1402
1406
  }
1403
1407
  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 ((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);
1408
1416
  if (changed === null) {
1409
1417
  return;
1410
1418
  }
@@ -1478,13 +1486,23 @@ class IMAPConnection {
1478
1486
  this.clearOutputQueue();
1479
1487
  if (mode === 'reset') {
1480
1488
  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
- }
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);
1488
1506
  }
1489
1507
  else if (this.transport) {
1490
1508
  this.transport.end(() => socket.end());
@@ -1621,6 +1639,9 @@ class IMAPConnection {
1621
1639
  onData(chunk) {
1622
1640
  let match;
1623
1641
  let str;
1642
+ if (this._resetting) {
1643
+ return;
1644
+ }
1624
1645
  // everything in one read arrived before anything this read causes to be sent
1625
1646
  this._readCount = (this._readCount || 0) + 1;
1626
1647
  str = (chunk || '').toString('binary');
@@ -2240,6 +2261,24 @@ class IMAPConnection {
2240
2261
  if (compiled === null) {
2241
2262
  return;
2242
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) {
2243
2282
  const event = response.tag === '+' ? 'continuation' : 'response';
2244
2283
  if (!this.server.script.watches(event)) {
2245
2284
  this.write(compiled);
@@ -56,6 +56,13 @@ export interface ScriptRule {
56
56
  nth?: number | undefined;
57
57
  /** the rule fires this many times at most (default unlimited) */
58
58
  times?: number | undefined;
59
+ /** sends every string of the response that the grammar allows as a literal (response events) */
60
+ literals?: boolean | undefined;
61
+ /**
62
+ * holds an untagged response back: `"tagged"` sends it after the tagged response of its command, `"next"` with the
63
+ * first response that belongs to another command (response events)
64
+ */
65
+ defer?: 'tagged' | 'next' | undefined;
59
66
  /** changes or replaces the response object before it is compiled (response events) */
60
67
  mutate?: ((response: IMAPResponse, context: ScriptContext) => IMAPResponse | void) | undefined;
61
68
  /** bytes to send instead of the output, or instead of answering the command or input line. `$TAG` in a string is the tag */
@@ -170,6 +177,32 @@ export declare class ServerScript {
170
177
  * @param {Buffer} output The output
171
178
  */
172
179
  export declare function sendOutput(connection: IMAPConnection, rule: ScriptRule, context: ScriptContext, output: Buffer): void;
180
+ /** Output held back by `defer`, see releaseDeferred */
181
+ export interface DeferredOutput {
182
+ mode: 'tagged' | 'next';
183
+ /** tag of the command the response belongs to, null for an unsolicited response */
184
+ tag: string | null;
185
+ data: Buffer;
186
+ }
187
+ /**
188
+ * Sends the held back responses whose turn has come (`defer`). Before a response of another command go the ones held
189
+ * with `"next"`, after the tagged response of their command the ones held with `"tagged"`. A response held without a
190
+ * command goes with the next command, or after the next tagged response
191
+ *
192
+ * @param {Object} connection IMAP connection
193
+ * @param {String|null} tag Tag of the command the response that is sent belongs to
194
+ * @param {Boolean} tagged false before the response is sent, true after a tagged response was sent
195
+ */
196
+ export declare function releaseDeferred(connection: IMAPConnection, tag: string | null, tagged: boolean): void;
197
+ /**
198
+ * Turns the strings of a response into literals wherever the grammar allows a string (RFC 9051 section 9: string,
199
+ * nstring and astring). Positions that take only a quoted string stay quoted: the hierarchy delimiter of LIST, LSUB
200
+ * and NAMESPACE, CHILDINFO values (RFC 5258 section 6), date-time values and the quoted media types of a body structure
201
+ *
202
+ * @param {Object} response Response object
203
+ * @return {Object} a copy of the response with literals, the response is not changed
204
+ */
205
+ export declare function literalResponse(response: IMAPResponse): IMAPResponse;
173
206
  /**
174
207
  * Handles a command or input line with the rule that matched it: sends the bytes of `send`, closes the connection,
175
208
  * and processes the line as usual with `run` (or when the rule only delays it)
@@ -16,6 +16,8 @@ const KEYS = {
16
16
  nth: EVENTS,
17
17
  times: EVENTS,
18
18
  mutate: ['response', 'continuation'],
19
+ literals: ['response'],
20
+ defer: ['response'],
19
21
  send: EVENTS,
20
22
  before: OUTPUT_EVENTS,
21
23
  after: OUTPUT_EVENTS,
@@ -27,7 +29,7 @@ const KEYS = {
27
29
  truncate: EVENTS,
28
30
  close: EVENTS
29
31
  };
30
- const ACTIONS = ['mutate', 'send', 'before', 'after', 'run', 'drop', 'delay', 'chunk', 'truncate', 'close'];
32
+ const ACTIONS = ['mutate', 'literals', 'defer', 'send', 'before', 'after', 'run', 'drop', 'delay', 'chunk', 'truncate', 'close'];
31
33
  const DEFAULT_CHUNK_DELAY = 10;
32
34
  // matchers that list accepted values, kept as Sets so that matching an event allocates nothing
33
35
  const SET_MATCHERS = ['command', 'description', 'session', 'state', 'user', 'mailbox'];
@@ -105,6 +107,22 @@ function validateRule(rule) {
105
107
  throw new TypeError('Script rule option "' + key + '" must be a string, a Buffer or a function');
106
108
  }
107
109
  }
110
+ if (rule.literals !== undefined && typeof rule.literals !== 'boolean') {
111
+ throw new TypeError('Script rule option "literals" must be true or false');
112
+ }
113
+ if (rule.defer !== undefined) {
114
+ if (rule.defer !== 'tagged' && rule.defer !== 'next') {
115
+ throw new TypeError('Script rule option "defer" must be "tagged" or "next"');
116
+ }
117
+ if (rule.untagged !== true) {
118
+ // a tagged response ends its command, there is nothing to hold it back for
119
+ throw new TypeError('Script rule option "defer" needs "untagged": true');
120
+ }
121
+ const conflict = ['drop', 'delay', 'chunk', 'truncate', 'close'].find(key => rule[key] !== undefined && rule[key] !== false);
122
+ if (conflict) {
123
+ throw new TypeError('Script rule option "defer" can not be combined with "' + conflict + '"');
124
+ }
125
+ }
108
126
  if (rule.close !== undefined && typeof rule.close !== 'boolean' && rule.close !== 'reset') {
109
127
  throw new TypeError('Script rule option "close" must be true, false or "reset"');
110
128
  }
@@ -286,8 +304,137 @@ function scriptContext(connection, event, fields) {
286
304
  export function sendOutput(connection, rule, context, output) {
287
305
  const replaced = rule.drop ? Buffer.alloc(0) : resolveBytes(rule.send, context) || output;
288
306
  const parts = [resolveBytes(rule.before, context), replaced, resolveBytes(rule.after, context)].filter((part) => !!part);
307
+ if (rule.defer) {
308
+ (connection._deferredOutput ||= []).push({ mode: rule.defer, tag: context.tag, data: Buffer.concat(parts) });
309
+ return;
310
+ }
289
311
  sendBytes(connection, rule, Buffer.concat(parts), rule.delay);
290
312
  }
313
+ /**
314
+ * Sends the held back responses whose turn has come (`defer`). Before a response of another command go the ones held
315
+ * with `"next"`, after the tagged response of their command the ones held with `"tagged"`. A response held without a
316
+ * command goes with the next command, or after the next tagged response
317
+ *
318
+ * @param {Object} connection IMAP connection
319
+ * @param {String|null} tag Tag of the command the response that is sent belongs to
320
+ * @param {Boolean} tagged false before the response is sent, true after a tagged response was sent
321
+ */
322
+ export function releaseDeferred(connection, tag, tagged) {
323
+ const ready = [];
324
+ const waiting = [];
325
+ for (const entry of connection._deferredOutput) {
326
+ const due = tagged ? entry.mode === 'tagged' && (entry.tag === null || entry.tag === tag) : entry.mode === 'next' && tag !== null && entry.tag !== tag;
327
+ (due ? ready : waiting).push(entry);
328
+ }
329
+ if (ready.length) {
330
+ connection._deferredOutput = waiting.length ? waiting : null;
331
+ ready.forEach(entry => connection.write(entry.data));
332
+ }
333
+ }
334
+ /**
335
+ * Turns strings into literals: plain strings and STRING nodes, also in lists. Atoms, numbers, NIL, text and
336
+ * response codes (SECTION) stay as they are, a response code takes no literals (RFC 9051 section 9 resp-text-code)
337
+ *
338
+ * @param {*} node Response attribute
339
+ * @return {*} the attribute with literals, a new object where something changed
340
+ */
341
+ function literalValue(node) {
342
+ if (typeof node === 'string') {
343
+ return { type: 'LITERAL', value: node };
344
+ }
345
+ if (Array.isArray(node)) {
346
+ return node.map(literalValue);
347
+ }
348
+ if (node && typeof node === 'object' && node.type === 'STRING') {
349
+ return { type: 'LITERAL', value: node.value };
350
+ }
351
+ return node;
352
+ }
353
+ const nodeText = (node) => String(typeof node === 'string' ? node : node && typeof node === 'object' ? node.value : '').toUpperCase();
354
+ /**
355
+ * Turns the strings of a body structure into literals, except the media types that the grammar writes as quoted
356
+ * strings: `"TEXT"` (media-text) and `"MESSAGE" "RFC822"` or `"GLOBAL"` (media-message, RFC 9051 section 9). A literal
357
+ * "TEXT" would read as a basic part, and the lines count after it would break the grammar
358
+ *
359
+ * @param {*} body Body structure, a list
360
+ * @return {*} the body structure with literals
361
+ */
362
+ function literalBody(body) {
363
+ if (!Array.isArray(body)) {
364
+ return literalValue(body);
365
+ }
366
+ if (Array.isArray(body[0])) {
367
+ // body-type-mpart: the parts, then the subtype and the extension data
368
+ const subtype = body.findIndex(item => !Array.isArray(item));
369
+ return body.map((item, i) => (subtype < 0 || i < subtype ? literalBody(item) : literalValue(item)));
370
+ }
371
+ const type = nodeText(body[0]);
372
+ const message = type === 'MESSAGE' && ['RFC822', 'GLOBAL'].includes(nodeText(body[1]));
373
+ return body.map((item, i) => {
374
+ if ((i === 0 && (type === 'TEXT' || message)) || (i === 1 && message)) {
375
+ return item;
376
+ }
377
+ // body-type-msg: the envelope, then the body of the encapsulated message
378
+ return i === 8 && message ? literalBody(item) : literalValue(item);
379
+ });
380
+ }
381
+ // FETCH items with a date-time value, which is a quoted string (RFC 9051 section 9, SAVEDATE: RFC 8514 section 4)
382
+ const DATE_TIME_ITEMS = new Set(['INTERNALDATE', 'SAVEDATE']);
383
+ /**
384
+ * Turns the strings of a response into literals wherever the grammar allows a string (RFC 9051 section 9: string,
385
+ * nstring and astring). Positions that take only a quoted string stay quoted: the hierarchy delimiter of LIST, LSUB
386
+ * and NAMESPACE, CHILDINFO values (RFC 5258 section 6), date-time values and the quoted media types of a body structure
387
+ *
388
+ * @param {Object} response Response object
389
+ * @return {Object} a copy of the response with literals, the response is not changed
390
+ */
391
+ export function literalResponse(response) {
392
+ const attributes = Array.isArray(response.attributes) ? response.attributes : null;
393
+ if (!attributes) {
394
+ return response;
395
+ }
396
+ const command = nodeText(response.command);
397
+ const name = nodeText(attributes[1]);
398
+ let converted;
399
+ if (name === 'FETCH' || name === 'UIDFETCH') {
400
+ // message-data: number SP "FETCH" SP msg-att, the items are name and value pairs
401
+ const items = Array.isArray(attributes[2]) ? attributes[2] : [];
402
+ const fetched = items.map((item, i) => {
403
+ if (i % 2 === 0) {
404
+ return item;
405
+ }
406
+ const key = items[i - 1];
407
+ const itemName = key && !key.section ? nodeText(key) : '';
408
+ if (DATE_TIME_ITEMS.has(itemName)) {
409
+ return item;
410
+ }
411
+ return itemName === 'BODY' || itemName === 'BODYSTRUCTURE' ? literalBody(item) : literalValue(item);
412
+ });
413
+ converted = attributes.map((item, i) => (i === 2 ? fetched : item));
414
+ }
415
+ else if (command === 'LIST' || command === 'LSUB') {
416
+ // mailbox-list: flags, the delimiter (quoted only), the name and the extended data items
417
+ converted = attributes.map((item, i) => {
418
+ if (i === 1) {
419
+ return item;
420
+ }
421
+ if (i === 3 && Array.isArray(item)) {
422
+ return item.map((value, j) => (j % 2 === 1 && nodeText(item[j - 1]) === 'CHILDINFO' ? value : literalValue(value)));
423
+ }
424
+ return literalValue(item);
425
+ });
426
+ }
427
+ else if (command === 'NAMESPACE') {
428
+ // namespace-descr: "(" string SP (DQUOTE QUOTED-CHAR DQUOTE / nil) [extensions] ")"
429
+ converted = attributes.map(namespace => Array.isArray(namespace)
430
+ ? namespace.map(descr => (Array.isArray(descr) ? descr.map((value, i) => (i === 1 ? value : literalValue(value))) : descr))
431
+ : namespace);
432
+ }
433
+ else {
434
+ converted = attributes.map(literalValue);
435
+ }
436
+ return Object.assign({}, response, { attributes: converted });
437
+ }
291
438
  /**
292
439
  * Sends the bytes of a rule with its timing, `truncate` closes the connection after the cut
293
440
  *
@@ -4,7 +4,7 @@ import tls from 'node:tls';
4
4
  import type { CompilerOptions, ParserOptions } from 'imap-handler';
5
5
  import type { ResolvedCommandOptions } from './command-states.js';
6
6
  import { ServerScript } from './script.js';
7
- import type { OutputOperation, ScriptContext, ScriptEvent, ScriptRule } from './script.js';
7
+ import type { DeferredOutput, OutputOperation, ScriptContext, ScriptEvent, ScriptRule } from './script.js';
8
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';
9
9
  declare const TAG_REGEX: RegExp;
10
10
  /**
@@ -509,6 +509,10 @@ declare class IMAPConnection {
509
509
  /** output that waits behind a delay of a script rule, null when output is written right away */
510
510
  _outputQueue: OutputOperation[] | null;
511
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;
512
516
  constructor(server: IMAPServer, socket: net.Socket);
513
517
  /**
514
518
  * Writes protocol output to the client, through the transport layer if there is one
@@ -528,7 +532,7 @@ declare class IMAPConnection {
528
532
  */
529
533
  flushOutput(): void;
530
534
  /**
531
- * Drops the output that waits for a delay of a script rule
535
+ * Drops the output that waits for a delay of a script rule or for its release (`defer`)
532
536
  */
533
537
  clearOutputQueue(): void;
534
538
  /**
@@ -824,6 +828,15 @@ declare class IMAPConnection {
824
828
  * FETCH result. (This may have other names when used, like "affected".)
825
829
  */
826
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;
827
840
  /**
828
841
  * Compiles a response for the wire
829
842
  *
@@ -14,9 +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 } from './script.js';
17
+ import { ServerScript, sendOutput, handleLine, literalResponse, releaseDeferred } from './script.js';
18
18
  // longest command line (not counting literals) accepted from a client
19
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;
20
22
  // largest literal accepted after login, override with the maxLiteralSize option
21
23
  const MAX_LITERAL_SIZE = 64 * 1024 * 1024;
22
24
  // largest literal accepted before login, enough for any user name or password
@@ -1219,6 +1221,7 @@ class IMAPConnection {
1219
1221
  this.sessionNumber = ++this.server.sessionCounter;
1220
1222
  this._outputQueue = null;
1221
1223
  this._outputTimer = null;
1224
+ this._deferredOutput = null;
1222
1225
  this.secureConnection = !!this.options.secureConnection;
1223
1226
  this._remainder = '';
1224
1227
  this._command = '';
@@ -1321,7 +1324,7 @@ class IMAPConnection {
1321
1324
  this._outputQueue = null;
1322
1325
  }
1323
1326
  /**
1324
- * Drops the output that waits for a delay of a script rule
1327
+ * Drops the output that waits for a delay of a script rule or for its release (`defer`)
1325
1328
  */
1326
1329
  clearOutputQueue() {
1327
1330
  if (this._outputTimer) {
@@ -1329,6 +1332,7 @@ class IMAPConnection {
1329
1332
  this._outputTimer = null;
1330
1333
  }
1331
1334
  this._outputQueue = null;
1335
+ this._deferredOutput = null;
1332
1336
  }
1333
1337
  /**
1334
1338
  * Writes output through a transport layer, or to the socket
@@ -1359,10 +1363,14 @@ class IMAPConnection {
1359
1363
  return;
1360
1364
  }
1361
1365
  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 ((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);
1366
1374
  if (changed === null) {
1367
1375
  return;
1368
1376
  }
@@ -1436,13 +1444,23 @@ class IMAPConnection {
1436
1444
  this.clearOutputQueue();
1437
1445
  if (mode === 'reset') {
1438
1446
  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
- }
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);
1446
1464
  }
1447
1465
  else if (this.transport) {
1448
1466
  this.transport.end(() => socket.end());
@@ -1579,6 +1597,9 @@ class IMAPConnection {
1579
1597
  onData(chunk) {
1580
1598
  let match;
1581
1599
  let str;
1600
+ if (this._resetting) {
1601
+ return;
1602
+ }
1582
1603
  // everything in one read arrived before anything this read causes to be sent
1583
1604
  this._readCount = (this._readCount || 0) + 1;
1584
1605
  str = (chunk || '').toString('binary');
@@ -2198,6 +2219,24 @@ class IMAPConnection {
2198
2219
  if (compiled === null) {
2199
2220
  return;
2200
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) {
2201
2240
  const event = response.tag === '+' ? 'continuation' : 'response';
2202
2241
  if (!this.server.script.watches(event)) {
2203
2242
  this.write(compiled);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "imapkit",
3
- "version": "4.2.0",
3
+ "version": "4.3.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",