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.
@@ -0,0 +1,481 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ServerScript = void 0;
4
+ exports.sendOutput = sendOutput;
5
+ exports.releaseDeferred = releaseDeferred;
6
+ exports.literalResponse = literalResponse;
7
+ exports.handleLine = handleLine;
8
+ const EVENTS = ['greeting', 'command', 'input', 'response', 'continuation'];
9
+ const OUTPUT_EVENTS = ['greeting', 'response', 'continuation'];
10
+ // the events where a key is valid, every other key is a typo and refused
11
+ const KEYS = {
12
+ on: EVENTS,
13
+ command: ['command', 'input', 'response', 'continuation'],
14
+ tag: ['command', 'input', 'response', 'continuation'],
15
+ description: ['response', 'continuation'],
16
+ session: EVENTS,
17
+ user: EVENTS,
18
+ state: EVENTS,
19
+ mailbox: EVENTS,
20
+ untagged: ['response'],
21
+ match: EVENTS,
22
+ when: EVENTS,
23
+ nth: EVENTS,
24
+ times: EVENTS,
25
+ mutate: ['response', 'continuation'],
26
+ literals: ['response'],
27
+ defer: ['response'],
28
+ send: EVENTS,
29
+ before: OUTPUT_EVENTS,
30
+ after: OUTPUT_EVENTS,
31
+ run: ['command', 'input'],
32
+ drop: EVENTS,
33
+ delay: ['greeting', 'command', 'response', 'continuation'],
34
+ chunk: EVENTS,
35
+ chunkDelay: EVENTS,
36
+ truncate: EVENTS,
37
+ close: EVENTS
38
+ };
39
+ const ACTIONS = ['mutate', 'literals', 'defer', 'send', 'before', 'after', 'run', 'drop', 'delay', 'chunk', 'truncate', 'close'];
40
+ const DEFAULT_CHUNK_DELAY = 10;
41
+ // matchers that list accepted values, kept as Sets so that matching an event allocates nothing
42
+ const SET_MATCHERS = ['command', 'description', 'session', 'state', 'user', 'mailbox'];
43
+ /**
44
+ * Turns bytes of a rule into a Buffer. A string is a binary string (one character per octet) like everywhere
45
+ * in ImapKit, unless it has characters above U+00FF, then it is sent as UTF-8
46
+ */
47
+ function resolveBytes(value, context) {
48
+ if (value === undefined) {
49
+ return null;
50
+ }
51
+ const bytes = typeof value === 'function' ? value(context) : value;
52
+ if (Buffer.isBuffer(bytes)) {
53
+ return bytes;
54
+ }
55
+ const text = String(bytes).replace(/\$TAG/g, () => context.tag || '*');
56
+ return Buffer.from(text, /[\u0100-\uffff]/.test(text) ? 'utf-8' : 'binary');
57
+ }
58
+ /**
59
+ * Checks a rule given by the caller, a mistake is a TypeError right away instead of a rule that silently never fires
60
+ *
61
+ * @param {Object} rule Rule to check
62
+ * @return {RegExp|null} the `match` expression
63
+ */
64
+ function validateRule(rule) {
65
+ if (!rule || typeof rule !== 'object') {
66
+ throw new TypeError('A script rule must be an object');
67
+ }
68
+ if (!EVENTS.includes(rule.on)) {
69
+ throw new TypeError('Script rule "on" must be one of ' + EVENTS.join(', '));
70
+ }
71
+ for (const key of Object.keys(rule)) {
72
+ const events = Object.hasOwn(KEYS, key) ? KEYS[key] : null;
73
+ if (!events) {
74
+ throw new TypeError('Unknown script rule option "' + key + '"');
75
+ }
76
+ if (!events.includes(rule.on)) {
77
+ throw new TypeError('Script rule option "' + key + '" can not be used with "on": "' + rule.on + '"');
78
+ }
79
+ }
80
+ if (!ACTIONS.some(key => rule[key] !== undefined && rule[key] !== false)) {
81
+ throw new TypeError('A script rule needs an action (' + ACTIONS.join(', ') + ')');
82
+ }
83
+ if (rule.drop && (rule.send !== undefined || rule.run)) {
84
+ throw new TypeError('Script rule option "drop" can not be combined with "send" or "run"');
85
+ }
86
+ if (rule.run && (rule.close || rule.truncate !== undefined)) {
87
+ throw new TypeError('Script rule option "run" can not be combined with "close" or "truncate"');
88
+ }
89
+ if (rule.chunkDelay !== undefined && rule.chunk === undefined) {
90
+ throw new TypeError('Script rule option "chunkDelay" needs "chunk"');
91
+ }
92
+ if (!OUTPUT_EVENTS.includes(rule.on) && rule.send === undefined && (rule.chunk !== undefined || rule.truncate !== undefined)) {
93
+ // a command or input line has no output of its own to cut or split
94
+ throw new TypeError('Script rule options "chunk" and "truncate" need "send" with "on": "' + rule.on + '"');
95
+ }
96
+ for (const key of ['nth', 'times', 'chunk']) {
97
+ if (rule[key] !== undefined && !(Number.isInteger(rule[key]) && rule[key] > 0)) {
98
+ throw new TypeError('Script rule option "' + key + '" must be a positive integer');
99
+ }
100
+ }
101
+ for (const key of ['delay', 'chunkDelay', 'truncate']) {
102
+ if (rule[key] !== undefined && !(Number.isInteger(rule[key]) && rule[key] >= 0)) {
103
+ throw new TypeError('Script rule option "' + key + '" must be a non-negative integer');
104
+ }
105
+ }
106
+ for (const key of ['when', 'mutate']) {
107
+ if (rule[key] !== undefined && typeof rule[key] !== 'function') {
108
+ throw new TypeError('Script rule option "' + key + '" must be a function');
109
+ }
110
+ }
111
+ for (const key of ['send', 'before', 'after']) {
112
+ const value = rule[key];
113
+ if (value !== undefined && typeof value !== 'string' && typeof value !== 'function' && !Buffer.isBuffer(value)) {
114
+ throw new TypeError('Script rule option "' + key + '" must be a string, a Buffer or a function');
115
+ }
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
+ }
133
+ if (rule.close !== undefined && typeof rule.close !== 'boolean' && rule.close !== 'reset') {
134
+ throw new TypeError('Script rule option "close" must be true, false or "reset"');
135
+ }
136
+ if (rule.match === undefined) {
137
+ return null;
138
+ }
139
+ if (rule.match instanceof RegExp) {
140
+ // a global or sticky expression would keep its position between events
141
+ return new RegExp(rule.match.source, rule.match.flags.replace(/[gy]/g, ''));
142
+ }
143
+ if (typeof rule.match !== 'string') {
144
+ throw new TypeError('Script rule option "match" must be a string or a RegExp');
145
+ }
146
+ try {
147
+ return new RegExp(rule.match);
148
+ }
149
+ catch (err) {
150
+ throw new TypeError('Script rule option "match" is not a valid regular expression: ' + err.message, { cause: err });
151
+ }
152
+ }
153
+ /**
154
+ * The script of a server, `server.script`
155
+ */
156
+ class ServerScript {
157
+ constructor(server, rules) {
158
+ this.server = server;
159
+ this.entries = [];
160
+ this.watched = new Set();
161
+ if (rules) {
162
+ this.add(rules);
163
+ }
164
+ }
165
+ /** the rules that were added, in the order they are checked */
166
+ get rules() {
167
+ return this.entries.slice();
168
+ }
169
+ add(rules) {
170
+ if (Array.isArray(rules)) {
171
+ // all rules are checked before any is added
172
+ const matches = rules.map(validateRule);
173
+ return rules.map((rule, i) => this.addEntry(rule, matches[i]));
174
+ }
175
+ return this.addEntry(rules, validateRule(rules));
176
+ }
177
+ /** removes every rule */
178
+ clear() {
179
+ this.setEntries([]);
180
+ }
181
+ /**
182
+ * Checks if any rule watches an event, so that the event can skip building a context
183
+ *
184
+ * @param {String} event Event name
185
+ * @return {Boolean} true if a rule watches the event
186
+ */
187
+ watches(event) {
188
+ return this.watched.has(event);
189
+ }
190
+ /**
191
+ * Finds the rule that handles an event of a connection
192
+ *
193
+ * @param {Object} connection IMAP connection
194
+ * @param {String} event Event name
195
+ * @param {Object} fields `data`, and `tag`, `command`, `description`, `response` where they are known
196
+ * @return {Object|null} `{ rule, context }`, or null if no rule handles the event
197
+ */
198
+ check(connection, event, fields) {
199
+ if (!this.watched.has(event)) {
200
+ return null;
201
+ }
202
+ const context = scriptContext(connection, event, fields);
203
+ const rule = this.find(context);
204
+ return rule ? { rule, context } : null;
205
+ }
206
+ setEntries(entries) {
207
+ this.entries = entries;
208
+ this.watched = new Set(entries.map(entry => entry.rule.on));
209
+ }
210
+ addEntry(rule, match) {
211
+ // a copy, so that changing the caller's object later does not change the rule
212
+ const copy = Object.freeze(Object.assign({}, rule));
213
+ const sets = {};
214
+ for (const key of SET_MATCHERS) {
215
+ if (copy[key] !== undefined) {
216
+ // command names are compared in upper case
217
+ const values = [].concat(copy[key]);
218
+ sets[key] = new Set(key === 'command' ? values.map(value => String(value).toUpperCase()) : values);
219
+ }
220
+ }
221
+ const entry = {
222
+ rule: copy,
223
+ match,
224
+ sets,
225
+ matched: 0,
226
+ hits: 0,
227
+ remove: () => this.setEntries(this.entries.filter(item => item !== entry))
228
+ };
229
+ this.setEntries(this.entries.concat(entry));
230
+ return entry;
231
+ }
232
+ /**
233
+ * Finds the rule that handles an event and counts it
234
+ *
235
+ * @param {Object} context Event context
236
+ * @return {Object|null} the rule, or null if no rule handles the event
237
+ */
238
+ find(context) {
239
+ for (const entry of this.entries) {
240
+ const rule = entry.rule;
241
+ if (rule.on !== context.event || !this.matches(entry, context)) {
242
+ continue;
243
+ }
244
+ entry.matched++;
245
+ if (entry.matched < (rule.nth || 1) || (rule.times !== undefined && entry.hits >= rule.times)) {
246
+ continue;
247
+ }
248
+ entry.hits++;
249
+ this.server.emit('script', { rule, event: context.event, session: context.session, tag: context.tag, command: context.command });
250
+ return rule;
251
+ }
252
+ return null;
253
+ }
254
+ matches(entry, context) {
255
+ const rule = entry.rule;
256
+ for (const key of SET_MATCHERS) {
257
+ const set = entry.sets[key];
258
+ const value = context[key];
259
+ if (set && (value === null || value === undefined || !set.has(value))) {
260
+ return false;
261
+ }
262
+ }
263
+ if (rule.tag !== undefined && (context.tag === null || (rule.tag instanceof RegExp ? !rule.tag.test(context.tag) : rule.tag !== context.tag))) {
264
+ return false;
265
+ }
266
+ if (rule.untagged !== undefined && rule.untagged !== (!context.response || context.response.tag === '*')) {
267
+ return false;
268
+ }
269
+ if (entry.match && !entry.match.test(context.data)) {
270
+ return false;
271
+ }
272
+ return !rule.when || !!rule.when(context);
273
+ }
274
+ }
275
+ exports.ServerScript = ServerScript;
276
+ /**
277
+ * Builds the context of an event on a connection
278
+ *
279
+ * @param {Object} connection IMAP connection
280
+ * @param {String} event Event name
281
+ * @param {Object} fields `data`, and `tag`, `command`, `description`, `response` where they are known
282
+ * @return {Object} Event context
283
+ */
284
+ function scriptContext(connection, event, fields) {
285
+ // after its handler returned, a command whose input handler reads the lines (IDLE, AUTHENTICATE) is the one
286
+ // the events belong to
287
+ const running = connection._runningCommand
288
+ ? connection._runningCommand.parsed
289
+ : connection.inputHandler
290
+ ? connection.inputCommand
291
+ : null;
292
+ return Object.assign({
293
+ event,
294
+ connection,
295
+ session: connection.sessionNumber,
296
+ state: connection.state,
297
+ user: connection.username || null,
298
+ mailbox: connection.selectedMailbox ? connection.selectedMailbox.path : null,
299
+ tag: running ? running.tag : null,
300
+ command: running ? String(running.command).toUpperCase() : null
301
+ }, fields);
302
+ }
303
+ /**
304
+ * Sends output through the rule that handles it: the output itself (unless dropped or replaced with `send`)
305
+ * between `before` and `after`, cut by `truncate`, with the timing of `delay` and `chunk`
306
+ *
307
+ * @param {Object} connection IMAP connection
308
+ * @param {Object} rule Rule that handles the output
309
+ * @param {Object} context Event context
310
+ * @param {Buffer} output The output
311
+ */
312
+ function sendOutput(connection, rule, context, output) {
313
+ const replaced = rule.drop ? Buffer.alloc(0) : resolveBytes(rule.send, context) || output;
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
+ }
319
+ sendBytes(connection, rule, Buffer.concat(parts), rule.delay);
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
+ }
446
+ /**
447
+ * Sends the bytes of a rule with its timing, `truncate` closes the connection after the cut
448
+ *
449
+ * @param {Object} connection IMAP connection
450
+ * @param {Object} rule Rule
451
+ * @param {Buffer} data Bytes to send
452
+ * @param {Number} [delay] Milliseconds to wait first
453
+ */
454
+ function sendBytes(connection, rule, data, delay) {
455
+ connection.queueOutput({
456
+ data: rule.truncate === undefined ? data : data.subarray(0, rule.truncate),
457
+ delay,
458
+ chunk: rule.chunk,
459
+ chunkDelay: rule.chunkDelay === undefined ? DEFAULT_CHUNK_DELAY : rule.chunkDelay,
460
+ close: rule.close || (rule.truncate === undefined ? undefined : true)
461
+ });
462
+ }
463
+ /**
464
+ * Handles a command or input line with the rule that matched it: sends the bytes of `send`, closes the connection,
465
+ * and processes the line as usual with `run` (or when the rule only delays it)
466
+ *
467
+ * @param {Object} connection IMAP connection
468
+ * @param {Object} rule Rule that matched
469
+ * @param {Object} context Event context
470
+ * @param {Function} run Processes the line as usual
471
+ */
472
+ function handleLine(connection, rule, context, run) {
473
+ const bytes = resolveBytes(rule.send, context);
474
+ if (bytes || rule.close) {
475
+ sendBytes(connection, rule, bytes || Buffer.alloc(0));
476
+ }
477
+ // a rule that only delays the line does not replace it
478
+ if (rule.run || (!rule.drop && !bytes && !rule.close)) {
479
+ run();
480
+ }
481
+ }
@@ -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
- scheduleCommand(data: string): void;
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