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.
@@ -54,8 +54,11 @@ const numbers_js_1 = require("./numbers.js");
54
54
  const arguments_js_1 = require("./arguments.js");
55
55
  const append_js_1 = require("./commands/append.js");
56
56
  const bundledCert = __importStar(require("./cert.js"));
57
+ const script_js_1 = require("./script.js");
57
58
  // longest command line (not counting literals) accepted from a client
58
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;
59
62
  // largest literal accepted after login, override with the maxLiteralSize option
60
63
  const MAX_LITERAL_SIZE = 64 * 1024 * 1024;
61
64
  // largest literal accepted before login, enough for any user name or password
@@ -196,6 +199,9 @@ class IMAPServer extends node_stream_1.Stream {
196
199
  this.referenceNamespace = false;
197
200
  // the session whose command is running, see IMAPServer#notify
198
201
  this.activeConnection = null;
202
+ this.sessionCounter = 0;
203
+ // rules that make the server misbehave on purpose, from the script option or server.script.add()
204
+ this.script = new script_js_1.ServerScript(this, this.options.script);
199
205
  // users and storage are deep copied, so that runtime changes never leak into
200
206
  // the caller's objects or into other servers built from the same fixture.
201
207
  // Without a prototype, user names like "__proto__" or "toString" are plain keys
@@ -1254,6 +1260,10 @@ class IMAPConnection {
1254
1260
  this.socket = socket;
1255
1261
  this.options = this.server.options;
1256
1262
  this.state = 'Not Authenticated';
1263
+ this.sessionNumber = ++this.server.sessionCounter;
1264
+ this._outputQueue = null;
1265
+ this._outputTimer = null;
1266
+ this._deferredOutput = null;
1257
1267
  this.secureConnection = !!this.options.secureConnection;
1258
1268
  this._remainder = '';
1259
1269
  this._command = '';
@@ -1283,7 +1293,7 @@ class IMAPConnection {
1283
1293
  this.notificationQueue = [];
1284
1294
  this.server.on('notify', this._notificationCallback);
1285
1295
  this.server.connections.add(this);
1286
- this.write('* OK ImapKit ready for rumble\r\n');
1296
+ this.scriptOutput('greeting', '* OK ImapKit ready for rumble\r\n', {});
1287
1297
  }
1288
1298
  /**
1289
1299
  * Writes protocol output to the client, through the transport layer if there is one
@@ -1292,12 +1302,139 @@ class IMAPConnection {
1292
1302
  */
1293
1303
  write(data) {
1294
1304
  const buffer = typeof data === 'string' ? Buffer.from(data, 'binary') : data;
1295
- if (this.transport) {
1296
- this.transport.write(buffer);
1305
+ if (this._outputQueue) {
1306
+ this._outputQueue.push({ data: buffer, transport: this.transport });
1307
+ }
1308
+ else {
1309
+ this.writeLayer(buffer, this.transport);
1310
+ }
1311
+ }
1312
+ /**
1313
+ * Writes output, or puts it in the output queue while earlier output waits for a delay of a script rule.
1314
+ * The transport layer is taken when the output is queued, so output from before COMPRESS is not compressed
1315
+ *
1316
+ * @param {Object} operation `{ data, delay, chunk, chunkDelay, close }`, see OutputOperation in src/script.ts
1317
+ */
1318
+ queueOutput(operation) {
1319
+ if (!this._outputQueue && !operation.delay && !operation.chunk) {
1320
+ if (operation.data) {
1321
+ this.writeLayer(operation.data, this.transport);
1322
+ }
1323
+ if (operation.close) {
1324
+ this.closeNow(operation.close);
1325
+ }
1326
+ return;
1327
+ }
1328
+ this._outputQueue = this._outputQueue || [];
1329
+ this._outputQueue.push(Object.assign({}, operation, { transport: this.transport }));
1330
+ if (!this._outputTimer) {
1331
+ this.flushOutput();
1332
+ }
1333
+ }
1334
+ /**
1335
+ * Writes the output queue until it is empty or a delay stops it
1336
+ */
1337
+ flushOutput() {
1338
+ const queue = this._outputQueue || [];
1339
+ while (queue.length) {
1340
+ const operation = queue[0];
1341
+ if (operation.delay) {
1342
+ const delay = operation.delay;
1343
+ operation.delay = 0;
1344
+ this._outputTimer = setTimeout(() => {
1345
+ this._outputTimer = null;
1346
+ this.flushOutput();
1347
+ }, delay);
1348
+ return;
1349
+ }
1350
+ if (operation.data && operation.chunk && operation.data.length > operation.chunk) {
1351
+ // the rest waits for chunkDelay, like a delay of its own
1352
+ this.writeLayer(operation.data.subarray(0, operation.chunk), operation.transport || null);
1353
+ operation.data = operation.data.subarray(operation.chunk);
1354
+ operation.delay = operation.chunkDelay;
1355
+ continue;
1356
+ }
1357
+ if (operation.data) {
1358
+ this.writeLayer(operation.data, operation.transport || null);
1359
+ }
1360
+ queue.shift();
1361
+ if (operation.close) {
1362
+ this.closeNow(operation.close);
1363
+ return;
1364
+ }
1365
+ }
1366
+ this._outputQueue = null;
1367
+ }
1368
+ /**
1369
+ * Drops the output that waits for a delay of a script rule or for its release (`defer`)
1370
+ */
1371
+ clearOutputQueue() {
1372
+ if (this._outputTimer) {
1373
+ clearTimeout(this._outputTimer);
1374
+ this._outputTimer = null;
1375
+ }
1376
+ this._outputQueue = null;
1377
+ this._deferredOutput = null;
1378
+ }
1379
+ /**
1380
+ * Writes output through a transport layer, or to the socket
1381
+ *
1382
+ * @param {Buffer} data Output
1383
+ * @param {Object|null} transport Transport layer
1384
+ */
1385
+ writeLayer(data, transport) {
1386
+ if (transport) {
1387
+ transport.write(data);
1297
1388
  }
1298
1389
  else {
1299
- this.writeRaw(buffer);
1390
+ this.writeRaw(data);
1391
+ }
1392
+ }
1393
+ /**
1394
+ * Sends output through the script rule that handles its event, if there is one
1395
+ *
1396
+ * @param {String} event Event name: greeting, response or continuation
1397
+ * @param {String} output The output as a binary string
1398
+ * @param {Object} fields Context fields of the event (tag, command, description, response)
1399
+ * @param {Function} [compile] Compiles the response that a `mutate` action changed, null drops the output
1400
+ */
1401
+ scriptOutput(event, output, fields, compile) {
1402
+ const found = this.server.script.check(this, event, Object.assign({}, fields, { data: output }));
1403
+ if (!found) {
1404
+ this.write(output);
1405
+ return;
1406
+ }
1407
+ const { rule, context } = found;
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);
1416
+ if (changed === null) {
1417
+ return;
1418
+ }
1419
+ context.data = changed;
1300
1420
  }
1421
+ (0, script_js_1.sendOutput)(this, rule, context, Buffer.from(context.data, 'binary'));
1422
+ }
1423
+ /**
1424
+ * Sends a continuation request, `+ text`
1425
+ *
1426
+ * @param {String} text Human readable text, can be empty (SASL)
1427
+ * @param {String} description Description for script rules
1428
+ * @param {Function} [getLine] Returns the command line received so far, for the tag and the command of a literal continuation
1429
+ */
1430
+ sendContinuation(text, description, getLine) {
1431
+ const output = '+ ' + text + '\r\n';
1432
+ if (!this.server.script.watches('continuation')) {
1433
+ this.write(output);
1434
+ return;
1435
+ }
1436
+ const line = getLine ? getLine() : null;
1437
+ this.scriptOutput('continuation', output, line === null ? { description } : { description, tag: getResponseTag(line), command: getLineCommand(line) });
1301
1438
  }
1302
1439
  /**
1303
1440
  * Writes data to the socket, below the transport layer
@@ -1327,12 +1464,47 @@ class IMAPConnection {
1327
1464
  * holds, is written out
1328
1465
  */
1329
1466
  end() {
1467
+ if (this._outputQueue && this.socket) {
1468
+ // output still waits for a delay of a script rule
1469
+ this._closing = true;
1470
+ this._outputQueue.push({ close: true });
1471
+ return;
1472
+ }
1473
+ this.closeNow(true);
1474
+ }
1475
+ /**
1476
+ * Closes the connection now, after the output written so far
1477
+ *
1478
+ * @param {Boolean|String} mode true ends the connection gracefully, "reset" destroys the socket (script rules)
1479
+ */
1480
+ closeNow(mode) {
1330
1481
  const socket = this.socket;
1331
1482
  if (!socket) {
1332
1483
  return;
1333
1484
  }
1334
1485
  this._closing = true;
1335
- if (this.transport) {
1486
+ this.clearOutputQueue();
1487
+ if (mode === 'reset') {
1488
+ this.discardInput();
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);
1506
+ }
1507
+ else if (this.transport) {
1336
1508
  this.transport.end(() => socket.end());
1337
1509
  }
1338
1510
  else {
@@ -1433,6 +1605,7 @@ class IMAPConnection {
1433
1605
  this.transport.destroy();
1434
1606
  this.transport = null;
1435
1607
  }
1608
+ this.clearOutputQueue();
1436
1609
  this.server.removeListener('notify', this._notificationCallback);
1437
1610
  this.server.connections.delete(this);
1438
1611
  }
@@ -1448,9 +1621,27 @@ class IMAPConnection {
1448
1621
  // socket is already gone
1449
1622
  }
1450
1623
  }
1624
+ /**
1625
+ * Passes a line to the input handler (IDLE, AUTHENTICATE), unless a script rule handles it
1626
+ *
1627
+ * @param {String} line Input line without CRLF
1628
+ */
1629
+ handleInput(line) {
1630
+ const inputHandler = this.inputHandler;
1631
+ const found = this.server.script.check(this, 'input', { data: line });
1632
+ if (found) {
1633
+ (0, script_js_1.handleLine)(this, found.rule, found.context, () => inputHandler(line));
1634
+ }
1635
+ else {
1636
+ inputHandler(line);
1637
+ }
1638
+ }
1451
1639
  onData(chunk) {
1452
1640
  let match;
1453
1641
  let str;
1642
+ if (this._resetting) {
1643
+ return;
1644
+ }
1454
1645
  // everything in one read arrived before anything this read causes to be sent
1455
1646
  this._readCount = (this._readCount || 0) + 1;
1456
1647
  str = (chunk || '').toString('binary');
@@ -1519,7 +1710,7 @@ class IMAPConnection {
1519
1710
  this.sendBad(getResponseTag(line), 'Literal data must wait for the continuation request', 'LITERAL TOO EARLY', line);
1520
1711
  }
1521
1712
  else if (this.inputHandler) {
1522
- this.inputHandler(line);
1713
+ this.handleInput(line);
1523
1714
  }
1524
1715
  else {
1525
1716
  this.scheduleCommand(line);
@@ -1568,7 +1759,9 @@ class IMAPConnection {
1568
1759
  this._earlyLiteral = true;
1569
1760
  }
1570
1761
  else if (!this._earlyLiteral) {
1571
- this.write('+ Go ahead\r\n');
1762
+ // the line is needed only by script rules that watch continuations
1763
+ const head = str.substr(0, match.index) + marker;
1764
+ this.sendContinuation('Go ahead', 'LITERAL', () => this._command + head);
1572
1765
  }
1573
1766
  }
1574
1767
  this._remainder = '';
@@ -1644,12 +1837,7 @@ class IMAPConnection {
1644
1837
  // not a command, e.g. a SASL response
1645
1838
  return literal8 ? refuse('Literal8 is not allowed here') : false;
1646
1839
  }
1647
- // tag SP command, and for UID and AUTHENTICATE the word that follows
1648
- const words = line.match(/^[^ ]* ([^ ]*)(?: ([^ ]*))?/);
1649
- let command = ((words && words[1]) || '').toUpperCase();
1650
- if (command === 'UID' || command === 'AUTHENTICATE') {
1651
- command += ' ' + ((words && words[2]) || '').toUpperCase();
1652
- }
1840
+ const command = getLineCommand(line);
1653
1841
  if (!COMMAND_REGEX.test(command) || !this.server.getCommandHandler(command)) {
1654
1842
  return refuse('Unknown command');
1655
1843
  }
@@ -2069,6 +2257,44 @@ class IMAPConnection {
2069
2257
  });
2070
2258
  }
2071
2259
  }
2260
+ const compiled = this.compileResponse(response);
2261
+ if (compiled === null) {
2262
+ return;
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) {
2282
+ const event = response.tag === '+' ? 'continuation' : 'response';
2283
+ if (!this.server.script.watches(event)) {
2284
+ this.write(compiled);
2285
+ return;
2286
+ }
2287
+ // script rules see the response after every plugin and the core changed it. Without a command, an
2288
+ // unsolicited response belongs to the command that runs or idles
2289
+ this.scriptOutput(event, compiled, Object.assign({ description: description || null, response }, parsed && parsed.command ? { tag: parsed.tag || null, command: String(parsed.command).toUpperCase() } : {}), output => this.compileResponse(output));
2290
+ }
2291
+ /**
2292
+ * Compiles a response for the wire
2293
+ *
2294
+ * @param {Object} response Response object
2295
+ * @return {String|null} the response with its CRLF as a binary string, or null for an untagged response that does not compile
2296
+ */
2297
+ compileResponse(response) {
2072
2298
  let compiled;
2073
2299
  try {
2074
2300
  compiled = imap_handler_1.default.compiler(response, this.compilerOptions);
@@ -2079,14 +2305,14 @@ class IMAPConnection {
2079
2305
  console.log('Failed to compile response: %s', err.message);
2080
2306
  }
2081
2307
  if (response.tag === '*') {
2082
- return;
2308
+ return null;
2083
2309
  }
2084
2310
  compiled = response.tag + ' NO [SERVERBUG] Failed to compile response';
2085
2311
  }
2086
2312
  if (this.options.debug) {
2087
2313
  console.log('SEND: %s', compiled);
2088
2314
  }
2089
- this.write(compiled + '\r\n');
2315
+ return compiled + '\r\n';
2090
2316
  }
2091
2317
  /**
2092
2318
  * Sends a tagged status response to a command
@@ -2273,9 +2499,23 @@ class IMAPConnection {
2273
2499
  exportMailboxName(path) {
2274
2500
  return path;
2275
2501
  }
2276
- scheduleCommand(data) {
2502
+ /**
2503
+ * Parses a command line and queues the command, or answers it right away when it can not run
2504
+ *
2505
+ * @param {String} data Command line with its literals, without the final CRLF
2506
+ * @param {Boolean} [scripted] The line comes from a script rule with `run`, it is next in the queue
2507
+ */
2508
+ scheduleCommand(data, scripted) {
2277
2509
  let parsed;
2278
2510
  const tag = getResponseTag(data);
2511
+ // the rule is chosen when the line arrives, the state it matches is the state at that moment. The rule
2512
+ // acts when the command's turn comes, so that its output keeps the order of the responses
2513
+ const found = !scripted && this.server.script.watches('command') ? this.server.script.check(this, 'command', { data, tag, command: getLineCommand(data) }) : null;
2514
+ if (found) {
2515
+ this._commandQueue.push({ parsed: { tag, command: found.context.command || '' }, data, script: found });
2516
+ this.processQueue();
2517
+ return;
2518
+ }
2279
2519
  try {
2280
2520
  // server.parserOptions are the defaults of plugins, connection.parserOptions win
2281
2521
  parsed = imap_handler_1.default.parser(data, Object.assign({ literalPlus: this.server.literalPlus }, this.server.parserOptions, this.parserOptions));
@@ -2322,10 +2562,14 @@ class IMAPConnection {
2322
2562
  this.sendStatus(parsed, data, 'BAD', 'Commands with message sequence numbers must wait for the completion of earlier commands');
2323
2563
  return;
2324
2564
  }
2325
- this._commandQueue.push({
2326
- parsed: parsed,
2327
- data: data
2328
- });
2565
+ const element = { parsed, data };
2566
+ if (scripted) {
2567
+ // processQueue runs it once the script rule released the queue
2568
+ this._commandQueue.unshift(element);
2569
+ }
2570
+ else {
2571
+ this._commandQueue.push(element);
2572
+ }
2329
2573
  this.processQueue();
2330
2574
  }
2331
2575
  else if (/^AUTHENTICATE /i.test(parsed.command)) {
@@ -2354,6 +2598,33 @@ class IMAPConnection {
2354
2598
  }, 'UNKNOWN COMMAND', parsed, data);
2355
2599
  }
2356
2600
  }
2601
+ /**
2602
+ * Handles a command line with the script rule that matched it, after the rule's delay. With `run` the line
2603
+ * goes through the parser and the command handler as usual afterwards
2604
+ *
2605
+ * @param {Object} element Queued command with the rule
2606
+ * @param {Function} next Releases the queue
2607
+ */
2608
+ runScriptedCommand(element, next) {
2609
+ const { rule, context } = element.script;
2610
+ const act = () => {
2611
+ if (!this.socket || this._closing) {
2612
+ return next();
2613
+ }
2614
+ (0, script_js_1.handleLine)(this, rule, context, () => {
2615
+ // the line is not running yet, it must not count as an earlier command (RFC 3501 section 5.5)
2616
+ this._runningCommand = null;
2617
+ this.scheduleCommand(element.data, true);
2618
+ });
2619
+ next();
2620
+ };
2621
+ if (rule.delay) {
2622
+ setTimeout(act, rule.delay);
2623
+ }
2624
+ else {
2625
+ act();
2626
+ }
2627
+ }
2357
2628
  processQueue(force) {
2358
2629
  if (!force && this._processing) {
2359
2630
  return;
@@ -2386,6 +2657,10 @@ class IMAPConnection {
2386
2657
  this.processQueue(true);
2387
2658
  }
2388
2659
  };
2660
+ if (element.script) {
2661
+ this.runScriptedCommand(element, next);
2662
+ return;
2663
+ }
2389
2664
  if (options.states && options.states.indexOf(this.state) < 0) {
2390
2665
  this.sendStatus(element.parsed, element.data, 'BAD', stateError(command, this.state));
2391
2666
  return next();
@@ -2428,7 +2703,12 @@ class IMAPConnection {
2428
2703
  try {
2429
2704
  // changes made while the handler runs are attributed to this session (the `origin` of notifications)
2430
2705
  this.server.activeConnection = this;
2706
+ const inputHandler = this.inputHandler;
2431
2707
  this.server.getCommandHandler(element.parsed.command)(this, element.parsed, element.data, next);
2708
+ if (this.inputHandler && this.inputHandler !== inputHandler) {
2709
+ // the command reads the lines that follow (IDLE, AUTHENTICATE), script rules match them with it
2710
+ this.inputCommand = { tag: element.parsed.tag, command: element.parsed.command };
2711
+ }
2432
2712
  }
2433
2713
  catch (E) {
2434
2714
  const ex = E;
@@ -2550,6 +2830,41 @@ class IMAPConnection {
2550
2830
  }
2551
2831
  }
2552
2832
  exports.IMAPConnection = IMAPConnection;
2833
+ /**
2834
+ * Copies a response tree for the `mutate` action of a script rule: arrays and plain objects are copied,
2835
+ * other values (Buffers) are shared
2836
+ *
2837
+ * @param {*} value Response or a part of it
2838
+ * @return {*} Copy
2839
+ */
2840
+ function cloneResponse(value) {
2841
+ if (Array.isArray(value)) {
2842
+ return value.map(cloneResponse);
2843
+ }
2844
+ if (value && typeof value === 'object' && Object.getPrototypeOf(value) === Object.prototype) {
2845
+ const copy = {};
2846
+ for (const key of Object.keys(value)) {
2847
+ copy[key] = cloneResponse(value[key]);
2848
+ }
2849
+ return copy;
2850
+ }
2851
+ return value;
2852
+ }
2853
+ /**
2854
+ * Finds the command name in a command line that may not parse: tag SP command, and for UID and AUTHENTICATE
2855
+ * the word that follows
2856
+ *
2857
+ * @param {String} line Command line
2858
+ * @return {String} Command name in upper case, can be empty
2859
+ */
2860
+ function getLineCommand(line) {
2861
+ const words = line.match(/^[^ ]* ([^ ]*)(?: ([^ ]*))?/);
2862
+ let command = ((words && words[1]) || '').toUpperCase();
2863
+ if (command === 'UID' || command === 'AUTHENTICATE') {
2864
+ command += ' ' + ((words && words[2]) || '').toUpperCase();
2865
+ }
2866
+ return command;
2867
+ }
2553
2868
  /**
2554
2869
  * Formats a mailbox name for a response: an atom when possible, otherwise a string. NIL and names
2555
2870
  * like \\Foo would not read back as mailbox names, so these are strings as well
@@ -1,4 +1,5 @@
1
1
  import type { IMAPServer, IMAPConnection } from './server.js';
2
+ import type { ScriptRule } from './script.js';
2
3
  export type { IMAPServer, IMAPConnection };
3
4
  /**
4
5
  * A value of the parsed command or of a response, as imap-handler parses and compiles it: an
@@ -229,6 +230,8 @@ export interface IMAPServerOptions {
229
230
  systemFlags?: string[] | undefined;
230
231
  /** largest literal accepted after login, in octets */
231
232
  maxLiteralSize?: number | undefined;
233
+ /** script rules that make the server misbehave on purpose, see src/script.ts and README "Scripted faults" */
234
+ script?: ScriptRule | ScriptRule[] | undefined;
232
235
  [key: string]: any;
233
236
  }
234
237
  /**
@@ -1,6 +1,7 @@
1
1
  import createServer, { TAG_REGEX, IMAPServer, IMAPConnection } from './server.js';
2
2
  export { TAG_REGEX, IMAPServer, IMAPConnection };
3
3
  export type { Attribute, ParsedCommand, IMAPResponse, Notification, Callback, CommandHandler, CommandOptions, Plugin, IMAPError, Message, Mailbox, StorageNamespace, UserData, IMAPServerOptions } from './types.js';
4
+ export type { ScriptRule, ScriptContext, ScriptEvent, ScriptBytes, ScriptHandle } from './script.js';
4
5
  declare const imapkit: typeof createServer & {
5
6
  TAG_REGEX: RegExp;
6
7
  IMAPServer: typeof IMAPServer;
@@ -54,7 +54,7 @@ export default function authPlainPlugin(server) {
54
54
  authenticate(connection, parsed, data, str);
55
55
  };
56
56
  // Send an empty continuation request to the client
57
- connection.write('+ \r\n');
57
+ connection.sendContinuation('', 'AUTHENTICATE PLAIN');
58
58
  }
59
59
  else if (parsed.attributes.length === 1 &&
60
60
  // second argument must be Base64 string as ATOM
@@ -63,7 +63,7 @@ export default function idlePlugin(server) {
63
63
  }, 'INVALID IDLE', parsed, data);
64
64
  }
65
65
  };
66
- connection.write('+ idling\r\n');
66
+ connection.sendContinuation('idling', 'IDLE');
67
67
  connection.processNotifications();
68
68
  return callback();
69
69
  }, { states: states.AUTHENTICATED, noArguments: true });
@@ -165,7 +165,7 @@ export default function oauthbearerPlugin(server) {
165
165
  if (!args.length) {
166
166
  // without an initial response the client sends its response after an empty challenge
167
167
  readResponse(connection, parsed, data, decoded => authenticate(connection, parsed, data, decoded));
168
- connection.write('+ \r\n');
168
+ connection.sendContinuation('', 'AUTHENTICATE OAUTHBEARER');
169
169
  return callback();
170
170
  }
171
171
  if (args.length !== 1 || !args[0] || args[0].type !== 'ATOM') {