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 +22 -25
- package/dist/cjs/script.d.ts +33 -0
- package/dist/cjs/script.js +150 -1
- package/dist/cjs/server.d.ts +15 -2
- package/dist/cjs/server.js +51 -12
- package/dist/esm/script.d.ts +33 -0
- package/dist/esm/script.js +148 -1
- package/dist/esm/server.d.ts +15 -2
- package/dist/esm/server.js +52 -13
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -410,21 +410,16 @@ const server = imapkit({
|
|
|
410
410
|
]
|
|
411
411
|
});
|
|
412
412
|
|
|
413
|
-
//
|
|
414
|
-
const
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
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(
|
|
427
|
-
|
|
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
|
-
| `
|
|
462
|
-
| `
|
|
463
|
-
| `
|
|
464
|
-
| `
|
|
465
|
-
| `
|
|
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
|
|
package/dist/cjs/script.d.ts
CHANGED
|
@@ -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)
|
package/dist/cjs/script.js
CHANGED
|
@@ -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
|
*
|
package/dist/cjs/server.d.ts
CHANGED
|
@@ -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
|
*
|
package/dist/cjs/server.js
CHANGED
|
@@ -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
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
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
|
-
//
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
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);
|
package/dist/esm/script.d.ts
CHANGED
|
@@ -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)
|
package/dist/esm/script.js
CHANGED
|
@@ -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
|
*
|
package/dist/esm/server.d.ts
CHANGED
|
@@ -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
|
*
|
package/dist/esm/server.js
CHANGED
|
@@ -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
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
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
|
-
//
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
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);
|