emailengine-app 2.68.1 → 2.70.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/.github/workflows/deploy.yml +8 -3
- package/.github/workflows/release.yaml +6 -0
- package/CHANGELOG.md +59 -0
- package/Gruntfile.js +3 -1
- package/config/default.toml +2 -0
- package/data/google-crawlers.json +7 -1
- package/getswagger.sh +40 -4
- package/gettext-extract.js +163 -0
- package/lib/account.js +135 -72
- package/lib/api-routes/account-routes.js +684 -106
- package/lib/api-routes/blocklist-routes.js +344 -0
- package/lib/api-routes/chat-routes.js +32 -14
- package/lib/api-routes/delivery-test-routes.js +346 -0
- package/lib/api-routes/export-routes.js +28 -14
- package/lib/api-routes/gateway-routes.js +427 -0
- package/lib/api-routes/license-routes.js +156 -0
- package/lib/api-routes/mailbox-routes.js +344 -0
- package/lib/api-routes/message-routes.js +221 -187
- package/lib/api-routes/oauth2-app-routes.js +697 -0
- package/lib/api-routes/outbox-routes.js +185 -0
- package/lib/api-routes/pubsub-routes.js +102 -0
- package/lib/api-routes/route-helpers.js +58 -0
- package/lib/api-routes/settings-routes.js +357 -0
- package/lib/api-routes/stats-routes.js +111 -0
- package/lib/api-routes/submit-routes.js +461 -0
- package/lib/api-routes/template-routes.js +60 -75
- package/lib/api-routes/token-routes.js +297 -0
- package/lib/api-routes/webhook-route-routes.js +181 -0
- package/lib/autodetect-imap-settings.js +0 -2
- package/lib/consts.js +5 -0
- package/lib/email-client/base-client.js +28 -6
- package/lib/email-client/gmail-client.js +133 -112
- package/lib/email-client/imap/mailbox.js +34 -11
- package/lib/email-client/imap/subconnection.js +20 -13
- package/lib/email-client/imap/sync-operations.js +131 -3
- package/lib/email-client/imap-client.js +152 -75
- package/lib/email-client/notification-handler.js +1 -4
- package/lib/email-client/outlook-client.js +134 -75
- package/lib/export.js +97 -20
- package/lib/feature-flags.js +2 -2
- package/lib/gateway.js +4 -9
- package/lib/get-raw-email.js +5 -5
- package/lib/imapproxy/imap-core/lib/commands/starttls.js +18 -0
- package/lib/imapproxy/imap-core/lib/imap-command.js +6 -1
- package/lib/imapproxy/imap-core/lib/imap-connection.js +106 -24
- package/lib/imapproxy/imap-core/lib/imap-server.js +24 -0
- package/lib/imapproxy/imap-core/lib/imap-stream.js +26 -0
- package/lib/logger.js +24 -21
- package/lib/message-port-stream.js +113 -16
- package/lib/metrics-collector.js +0 -2
- package/lib/oauth2-apps.js +13 -4
- package/lib/outbox.js +24 -40
- package/lib/redis-operations.js +1 -1
- package/lib/reject-worker-calls.js +42 -0
- package/lib/routes-ui.js +37 -8778
- package/lib/schemas.js +429 -84
- package/lib/sentry.js +139 -0
- package/lib/settings.js +9 -3
- package/lib/stream-encrypt.js +1 -1
- package/lib/templates.js +1 -1
- package/lib/tokens.js +5 -3
- package/lib/tools.js +70 -4
- package/lib/ui-routes/account-routes.js +45 -212
- package/lib/ui-routes/admin-config-routes.js +928 -489
- package/lib/ui-routes/admin-entities-routes.js +1 -0
- package/lib/ui-routes/auth-routes.js +1339 -0
- package/lib/ui-routes/dashboard-routes.js +188 -0
- package/lib/ui-routes/document-store-routes.js +800 -0
- package/lib/ui-routes/export-routes.js +217 -0
- package/lib/ui-routes/internals-routes.js +354 -0
- package/lib/ui-routes/network-config-routes.js +759 -0
- package/lib/ui-routes/{oauth-routes.js → oauth-config-routes.js} +369 -91
- package/lib/ui-routes/route-helpers.js +314 -0
- package/lib/ui-routes/smtp-test-routes.js +236 -0
- package/lib/ui-routes/unsubscribe-routes.js +232 -0
- package/lib/webhook-request.js +36 -0
- package/lib/webhooks.js +8 -4
- package/package.json +13 -12
- package/sbom.json +1 -1
- package/server.js +222 -39
- package/static/licenses.html +160 -300
- package/translations/messages.pot +112 -132
- package/update-info.sh +19 -1
- package/views/config/logging.hbs +48 -0
- package/views/dashboard.hbs +7 -26
- package/views/internals/index.hbs +15 -0
- package/views/tokens/index.hbs +9 -0
- package/workers/api.js +200 -4424
- package/workers/documents.js +2 -22
- package/workers/export.js +103 -104
- package/workers/imap-proxy.js +3 -23
- package/workers/imap.js +32 -36
- package/workers/smtp.js +2 -22
- package/workers/submit.js +26 -35
- package/workers/webhooks.js +9 -43
|
@@ -369,9 +369,14 @@ class IMAPCommand {
|
|
|
369
369
|
countBadResponses() {
|
|
370
370
|
this.connection._badCount++;
|
|
371
371
|
if (this.connection._badCount > MAX_BAD_COMMANDS) {
|
|
372
|
+
// Stop reading first so a command pipelined right after the offending input is not
|
|
373
|
+
// dispatched during the graceful-close window.
|
|
374
|
+
this.connection._stopReading();
|
|
372
375
|
this.connection.clearNotificationListener();
|
|
373
376
|
this.connection.send('* BYE Too many protocol errors');
|
|
374
|
-
|
|
377
|
+
// Graceful close so the BYE is flushed to the client before teardown (close() ends
|
|
378
|
+
// the socket and force-destroys via its own failsafe timer).
|
|
379
|
+
setImmediate(() => this.connection.close());
|
|
375
380
|
return false;
|
|
376
381
|
}
|
|
377
382
|
return true;
|
|
@@ -14,6 +14,12 @@ const packageInfo = require('../../../../package');
|
|
|
14
14
|
|
|
15
15
|
const SOCKET_TIMEOUT = 5 * 60 * 1000;
|
|
16
16
|
|
|
17
|
+
// Idle timeout applied to a proxied connection after auth handoff. Generous enough
|
|
18
|
+
// to not interrupt IMAP IDLE (which can sit idle for ~29 minutes) while still
|
|
19
|
+
// preventing a connection from being held open forever. Overridable via the
|
|
20
|
+
// proxyTimeout server option; 0 disables it (legacy behaviour).
|
|
21
|
+
const PROXY_SOCKET_TIMEOUT = 30 * 60 * 1000;
|
|
22
|
+
|
|
17
23
|
/**
|
|
18
24
|
* Creates a handler for new socket
|
|
19
25
|
*
|
|
@@ -52,11 +58,15 @@ class IMAPConnection extends EventEmitter {
|
|
|
52
58
|
this._upgrading = false;
|
|
53
59
|
|
|
54
60
|
// Parser instance for the incoming stream
|
|
55
|
-
this._parser = new IMAPStream();
|
|
61
|
+
this._parser = new IMAPStream({ maxLineLength: this._server.options.maxLineLength });
|
|
56
62
|
|
|
57
63
|
// Set handler for incoming commands
|
|
58
64
|
this._parser.oncommand = this._onCommand.bind(this);
|
|
59
65
|
|
|
66
|
+
// Close the connection if the parser rejects the input (e.g. an over-long
|
|
67
|
+
// command line) instead of letting the error reach the global handler.
|
|
68
|
+
this._parser.on('error', err => this._onParserError(err));
|
|
69
|
+
|
|
60
70
|
// Manage multi part command
|
|
61
71
|
this._currentCommand = false;
|
|
62
72
|
|
|
@@ -194,8 +204,12 @@ class IMAPConnection extends EventEmitter {
|
|
|
194
204
|
}
|
|
195
205
|
|
|
196
206
|
unbind() {
|
|
207
|
+
// Cancel any armed graceful-close failsafe so it cannot destroy the socket after we
|
|
208
|
+
// have handed it off to the proxy backend.
|
|
209
|
+
clearTimeout(this._closingTimeout);
|
|
210
|
+
|
|
197
211
|
this.writeStream.unpipe(this._socket);
|
|
198
|
-
this.
|
|
212
|
+
this._stopReading();
|
|
199
213
|
|
|
200
214
|
if (this._onCloseHandler) {
|
|
201
215
|
this._socket.removeListener('close', this._onCloseHandler);
|
|
@@ -206,8 +220,29 @@ class IMAPConnection extends EventEmitter {
|
|
|
206
220
|
if (this._onErrorHandler) {
|
|
207
221
|
this._socket.removeListener('error', this._onErrorHandler);
|
|
208
222
|
}
|
|
223
|
+
// Drop the auth-phase protocol timeout handler - after handoff it must not
|
|
224
|
+
// run, it would write IMAP responses into the proxied byte stream.
|
|
225
|
+
this._socket.setTimeout(0);
|
|
209
226
|
if (this._onTimeoutHandler) {
|
|
210
|
-
this._socket.
|
|
227
|
+
this._socket.removeListener('timeout', this._onTimeoutHandler);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Re-arm a generous idle timeout so the proxied connection cannot be held
|
|
231
|
+
// open indefinitely (fd/connection exhaustion). Use a plain socket destroy
|
|
232
|
+
// rather than the protocol-level handler. proxyTimeout: 0 disables it.
|
|
233
|
+
let proxyTimeout = this._server.options.proxyTimeout;
|
|
234
|
+
if (proxyTimeout === undefined || proxyTimeout === null) {
|
|
235
|
+
proxyTimeout = PROXY_SOCKET_TIMEOUT;
|
|
236
|
+
}
|
|
237
|
+
if (proxyTimeout) {
|
|
238
|
+
this._socket.setTimeout(proxyTimeout);
|
|
239
|
+
this._socket.once('timeout', () => {
|
|
240
|
+
try {
|
|
241
|
+
this._socket.destroy();
|
|
242
|
+
} catch (err) {
|
|
243
|
+
// ignore
|
|
244
|
+
}
|
|
245
|
+
});
|
|
211
246
|
}
|
|
212
247
|
|
|
213
248
|
// After handoff to proxy mode the connection's own close/end handlers are gone,
|
|
@@ -281,42 +316,53 @@ class IMAPConnection extends EventEmitter {
|
|
|
281
316
|
/**
|
|
282
317
|
* Close socket
|
|
283
318
|
*/
|
|
284
|
-
close(
|
|
319
|
+
close() {
|
|
285
320
|
if (this._closed || this._closing) {
|
|
286
321
|
return;
|
|
287
322
|
}
|
|
288
323
|
|
|
289
324
|
if (!this._socket.destroyed && this._socket.writable) {
|
|
290
|
-
this._socket
|
|
325
|
+
this._socket.end();
|
|
291
326
|
}
|
|
292
327
|
|
|
293
328
|
this._server.connections.delete(this);
|
|
294
329
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
this.
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
}
|
|
330
|
+
// allow socket to close gracefully in 1500ms or force it to close
|
|
331
|
+
this._closingTimeout = setTimeout(() => {
|
|
332
|
+
if (this._closed) {
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
301
335
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
336
|
+
try {
|
|
337
|
+
this._socket.destroy();
|
|
338
|
+
} catch (err) {
|
|
339
|
+
// ignore
|
|
340
|
+
}
|
|
307
341
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
}
|
|
342
|
+
setImmediate(() => this._onClose());
|
|
343
|
+
}, 1500);
|
|
311
344
|
|
|
312
345
|
this._closing = true;
|
|
313
|
-
if (force) {
|
|
314
|
-
setImmediate(() => this._onClose());
|
|
315
|
-
}
|
|
316
346
|
}
|
|
317
347
|
|
|
318
348
|
// PRIVATE METHODS
|
|
319
349
|
|
|
350
|
+
/**
|
|
351
|
+
* Stop feeding the socket into the command parser. Called before teardown so a client
|
|
352
|
+
* cannot pipeline another command (e.g. LOGIN) after we have decided to close - which
|
|
353
|
+
* would otherwise be dispatched during the graceful-close window. Safe to call more than
|
|
354
|
+
* once; a no-op once the parser has been torn down in _onClose().
|
|
355
|
+
*/
|
|
356
|
+
_stopReading() {
|
|
357
|
+
try {
|
|
358
|
+
if (this._parser) {
|
|
359
|
+
this._socket.unpipe(this._parser);
|
|
360
|
+
}
|
|
361
|
+
} catch (err) {
|
|
362
|
+
// ignore
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
320
366
|
/**
|
|
321
367
|
* Setup socket event handlers
|
|
322
368
|
*/
|
|
@@ -334,6 +380,34 @@ class IMAPConnection extends EventEmitter {
|
|
|
334
380
|
this._socket.pipe(this._parser);
|
|
335
381
|
}
|
|
336
382
|
|
|
383
|
+
/**
|
|
384
|
+
* Fired when the incoming-stream parser rejects the input (e.g. an over-long
|
|
385
|
+
* command line). Stops feeding the parser, tells the client, and closes.
|
|
386
|
+
* @param {Error} err - Parser error
|
|
387
|
+
*/
|
|
388
|
+
_onParserError(err) {
|
|
389
|
+
this._stopReading();
|
|
390
|
+
|
|
391
|
+
this.logger.info(
|
|
392
|
+
{
|
|
393
|
+
tnx: 'parser',
|
|
394
|
+
cid: this.id
|
|
395
|
+
},
|
|
396
|
+
'Closing connection: %s',
|
|
397
|
+
err && err.message
|
|
398
|
+
);
|
|
399
|
+
|
|
400
|
+
try {
|
|
401
|
+
this.send('* BYE ' + ((err && err.message) || 'Protocol error'));
|
|
402
|
+
} catch (E) {
|
|
403
|
+
// ignore
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Graceful close so the BYE is flushed before teardown; close() force-destroys via
|
|
407
|
+
// its own failsafe timer if the socket does not close on its own.
|
|
408
|
+
setImmediate(() => this.close());
|
|
409
|
+
}
|
|
410
|
+
|
|
337
411
|
/**
|
|
338
412
|
* Fired when the socket is closed
|
|
339
413
|
* @event
|
|
@@ -489,8 +563,12 @@ class IMAPConnection extends EventEmitter {
|
|
|
489
563
|
return;
|
|
490
564
|
}
|
|
491
565
|
|
|
566
|
+
// Stop reading first so a command pipelined right after the timeout is not dispatched
|
|
567
|
+
// during the graceful-close window. Graceful close still flushes the BYE before teardown
|
|
568
|
+
// (close() has a failsafe timer).
|
|
569
|
+
this._stopReading();
|
|
492
570
|
this.send('* BYE Idle timeout, closing connection');
|
|
493
|
-
setImmediate(() => this.close(
|
|
571
|
+
setImmediate(() => this.close());
|
|
494
572
|
}
|
|
495
573
|
|
|
496
574
|
/**
|
|
@@ -504,6 +582,11 @@ class IMAPConnection extends EventEmitter {
|
|
|
504
582
|
|
|
505
583
|
callback = callback || (() => false);
|
|
506
584
|
|
|
585
|
+
if (this._closing || this._closed) {
|
|
586
|
+
// connection is tearing down - do not dispatch further commands
|
|
587
|
+
return callback();
|
|
588
|
+
}
|
|
589
|
+
|
|
507
590
|
if (this._upgrading) {
|
|
508
591
|
// ignore any commands before TLS upgrade is finished
|
|
509
592
|
return callback();
|
|
@@ -777,7 +860,6 @@ class IMAPConnection extends EventEmitter {
|
|
|
777
860
|
if (existsResponse && !changed) {
|
|
778
861
|
// send cached EXISTS response
|
|
779
862
|
this.writeStream.write(existsResponse);
|
|
780
|
-
existsResponse = false;
|
|
781
863
|
}
|
|
782
864
|
|
|
783
865
|
if (changed) {
|
|
@@ -12,6 +12,10 @@ const base32 = require('base32.js');
|
|
|
12
12
|
|
|
13
13
|
const CLOSE_TIMEOUT = 1 * 1000; // how much to wait until pending connections are terminated
|
|
14
14
|
|
|
15
|
+
// A PROXY protocol v1 header is at most ~107 bytes. Bound the buffer so a trusted
|
|
16
|
+
// proxy source that never sends a newline cannot grow memory without limit.
|
|
17
|
+
const MAX_PROXY_HEADER_SIZE = 256;
|
|
18
|
+
|
|
15
19
|
/**
|
|
16
20
|
* Creates a IMAP server instance.
|
|
17
21
|
*
|
|
@@ -273,6 +277,26 @@ class IMAPServer extends EventEmitter {
|
|
|
273
277
|
}
|
|
274
278
|
chunks.push(chunk);
|
|
275
279
|
chunklen += chunk.length;
|
|
280
|
+
|
|
281
|
+
if (chunklen > MAX_PROXY_HEADER_SIZE) {
|
|
282
|
+
socket.removeListener('readable', socketReader);
|
|
283
|
+
try {
|
|
284
|
+
socket.end('* BAD Invalid PROXY header\r\n');
|
|
285
|
+
} catch (E) {
|
|
286
|
+
// ignore
|
|
287
|
+
}
|
|
288
|
+
// end() only half-closes the socket; arm a short timeout so the fd is
|
|
289
|
+
// released even if the peer never closes its side (fd exhaustion).
|
|
290
|
+
socket.setTimeout(CLOSE_TIMEOUT);
|
|
291
|
+
socket.once('timeout', () => {
|
|
292
|
+
try {
|
|
293
|
+
socket.destroy();
|
|
294
|
+
} catch (E) {
|
|
295
|
+
// ignore
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
276
300
|
}
|
|
277
301
|
};
|
|
278
302
|
socket.on('readable', socketReader);
|
|
@@ -4,6 +4,11 @@ const stream = require('stream');
|
|
|
4
4
|
const Writable = stream.Writable;
|
|
5
5
|
const PassThrough = stream.PassThrough;
|
|
6
6
|
|
|
7
|
+
// Upper bound for a single command line (no CRLF yet). Generous - real IMAP command
|
|
8
|
+
// lines are short and bulk data is sent as size-bounded literals - but it prevents a
|
|
9
|
+
// client that never sends a newline from growing memory without limit (OOM).
|
|
10
|
+
const MAX_LINE_LENGTH = 1 * 1024 * 1024;
|
|
11
|
+
|
|
7
12
|
/**
|
|
8
13
|
* Incoming IMAP stream parser. Detects and emits command payloads.
|
|
9
14
|
* If literal values are encountered the command payload is split into parts
|
|
@@ -21,6 +26,9 @@ class IMAPStream extends Writable {
|
|
|
21
26
|
this.options = options || {};
|
|
22
27
|
Writable.call(this, this.options);
|
|
23
28
|
|
|
29
|
+
// largest single command line we will buffer before giving up
|
|
30
|
+
this._maxLineLength = Number(this.options.maxLineLength) || MAX_LINE_LENGTH;
|
|
31
|
+
|
|
24
32
|
// unprocessed chars from the last parsing iteration
|
|
25
33
|
this._remainder = '';
|
|
26
34
|
this._literal = false;
|
|
@@ -40,6 +48,18 @@ class IMAPStream extends Writable {
|
|
|
40
48
|
throw new Error('Command handler is not set');
|
|
41
49
|
}
|
|
42
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Discards any buffered or partial parser state. Used on TLS upgrade so that
|
|
53
|
+
* plaintext pipelined before STARTTLS cannot be parsed as commands inside the
|
|
54
|
+
* encrypted session (STARTTLS command injection).
|
|
55
|
+
*/
|
|
56
|
+
reset() {
|
|
57
|
+
this._remainder = '';
|
|
58
|
+
this._literal = false;
|
|
59
|
+
this._literalReady = false;
|
|
60
|
+
this._expecting = 0;
|
|
61
|
+
}
|
|
62
|
+
|
|
43
63
|
// PRIVATE METHODS
|
|
44
64
|
|
|
45
65
|
/**
|
|
@@ -107,6 +127,12 @@ class IMAPStream extends Writable {
|
|
|
107
127
|
pos += line.length + match[0].length;
|
|
108
128
|
} else {
|
|
109
129
|
this._remainder = pos < data.length ? data.substr(pos) : '';
|
|
130
|
+
if (this._remainder.length > this._maxLineLength) {
|
|
131
|
+
// Drop the buffered data and fail the stream - the connection layer
|
|
132
|
+
// closes the socket on a parser error.
|
|
133
|
+
this._remainder = '';
|
|
134
|
+
return done(new Error('Command line too long'));
|
|
135
|
+
}
|
|
110
136
|
return done();
|
|
111
137
|
}
|
|
112
138
|
|
package/lib/logger.js
CHANGED
|
@@ -19,22 +19,13 @@ let logger = pino({
|
|
|
19
19
|
log(object) {
|
|
20
20
|
if (object.err && ['TypeError', 'RangeError'].includes(object.err.name)) {
|
|
21
21
|
if (logger.notifyError) {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
let meta = {};
|
|
23
|
+
for (let key of ['msg', 'path', 'cid']) {
|
|
24
|
+
if (object[key]) {
|
|
25
|
+
meta[key] = object[key];
|
|
25
26
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
for (let key of ['msg', 'path', 'cid']) {
|
|
29
|
-
if (object[key]) {
|
|
30
|
-
meta[key] = object[key];
|
|
31
|
-
hasMeta = true;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
if (hasMeta) {
|
|
35
|
-
event.addMetadata('ee', meta);
|
|
36
|
-
}
|
|
37
|
-
});
|
|
27
|
+
}
|
|
28
|
+
logger.notifyError(object.err, { user: object.account, meta });
|
|
38
29
|
}
|
|
39
30
|
}
|
|
40
31
|
return object;
|
|
@@ -49,6 +40,22 @@ if (threadId) {
|
|
|
49
40
|
logger = logger.child({ tid: threadId });
|
|
50
41
|
}
|
|
51
42
|
|
|
43
|
+
// An error that reaches the global handlers leaves the process in an unknown state,
|
|
44
|
+
// so the process must always exit. If error tracking is enabled, report the error
|
|
45
|
+
// first and allow a short flush window for the delivery.
|
|
46
|
+
function fatalShutdown(code, err) {
|
|
47
|
+
if (logger.notifyError) {
|
|
48
|
+
logger.notifyError(err);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
let exit = () => process.exit(code);
|
|
52
|
+
if (logger.flushNotifications) {
|
|
53
|
+
logger.flushNotifications().then(exit, exit);
|
|
54
|
+
} else {
|
|
55
|
+
setTimeout(exit, 10);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
52
59
|
process.on('uncaughtException', err => {
|
|
53
60
|
logger.fatal({
|
|
54
61
|
msg: 'uncaughtException',
|
|
@@ -56,9 +63,7 @@ process.on('uncaughtException', err => {
|
|
|
56
63
|
err
|
|
57
64
|
});
|
|
58
65
|
|
|
59
|
-
|
|
60
|
-
setTimeout(() => process.exit(1), 10);
|
|
61
|
-
}
|
|
66
|
+
fatalShutdown(1, err);
|
|
62
67
|
});
|
|
63
68
|
|
|
64
69
|
process.on('unhandledRejection', err => {
|
|
@@ -68,9 +73,7 @@ process.on('unhandledRejection', err => {
|
|
|
68
73
|
err
|
|
69
74
|
});
|
|
70
75
|
|
|
71
|
-
|
|
72
|
-
setTimeout(() => process.exit(2), 10);
|
|
73
|
-
}
|
|
76
|
+
fatalShutdown(2, err);
|
|
74
77
|
});
|
|
75
78
|
|
|
76
79
|
module.exports = logger;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const { Writable, Readable } = require('stream');
|
|
3
|
+
const { Writable, Readable, pipeline } = require('stream');
|
|
4
4
|
|
|
5
5
|
// const { MessageChannel } = require('worker_threads');
|
|
6
6
|
// const { port1, port2 } = new MessageChannel();
|
|
@@ -9,6 +9,41 @@ class MessagePortWritable extends Writable {
|
|
|
9
9
|
constructor(messagePort) {
|
|
10
10
|
super();
|
|
11
11
|
this.messagePort = messagePort;
|
|
12
|
+
this.portClosed = false;
|
|
13
|
+
|
|
14
|
+
// The reader side posts { cancel: true } when it is torn down (e.g. the
|
|
15
|
+
// HTTP client aborted the download). Stop the transfer so the upstream
|
|
16
|
+
// source - and the IMAP lock it holds - is released.
|
|
17
|
+
this.onPortMessage = message => {
|
|
18
|
+
if (message && message.cancel) {
|
|
19
|
+
this.destroy();
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
this.messagePort.on('message', this.onPortMessage);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
postToPort(message) {
|
|
26
|
+
if (this.portClosed) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
this.messagePort.postMessage(message);
|
|
31
|
+
} catch (err) {
|
|
32
|
+
// ignore - the channel may already be torn down
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
closePort() {
|
|
37
|
+
if (this.portClosed) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
this.portClosed = true;
|
|
41
|
+
this.messagePort.removeListener('message', this.onPortMessage);
|
|
42
|
+
try {
|
|
43
|
+
this.messagePort.close();
|
|
44
|
+
} catch (err) {
|
|
45
|
+
// ignore
|
|
46
|
+
}
|
|
12
47
|
}
|
|
13
48
|
|
|
14
49
|
_write(chunk, encoding, done) {
|
|
@@ -20,24 +55,25 @@ class MessagePortWritable extends Writable {
|
|
|
20
55
|
chunk = Buffer.from(chunk, encoding);
|
|
21
56
|
}
|
|
22
57
|
|
|
23
|
-
this.
|
|
24
|
-
value: chunk,
|
|
25
|
-
done: false
|
|
26
|
-
});
|
|
58
|
+
this.postToPort({ value: chunk, done: false });
|
|
27
59
|
done();
|
|
28
60
|
}
|
|
29
61
|
|
|
30
62
|
_final(done) {
|
|
31
|
-
this.
|
|
32
|
-
|
|
33
|
-
});
|
|
34
|
-
try {
|
|
35
|
-
this.messagePort.close();
|
|
36
|
-
} catch (err) {
|
|
37
|
-
//ignore
|
|
38
|
-
}
|
|
63
|
+
this.postToPort({ done: true });
|
|
64
|
+
this.closePort();
|
|
39
65
|
done();
|
|
40
66
|
}
|
|
67
|
+
|
|
68
|
+
_destroy(err, done) {
|
|
69
|
+
// Abnormal termination: tell the reader so a truncated transfer is not
|
|
70
|
+
// mistaken for a complete message, then release the port.
|
|
71
|
+
if (err) {
|
|
72
|
+
this.postToPort({ error: err.message || 'Stream error' });
|
|
73
|
+
}
|
|
74
|
+
this.closePort();
|
|
75
|
+
done(err);
|
|
76
|
+
}
|
|
41
77
|
}
|
|
42
78
|
|
|
43
79
|
class MessagePortReadable extends Readable {
|
|
@@ -46,17 +82,28 @@ class MessagePortReadable extends Readable {
|
|
|
46
82
|
this.messagePort = messagePort;
|
|
47
83
|
|
|
48
84
|
this.canRead = false;
|
|
85
|
+
this.portClosed = false;
|
|
49
86
|
|
|
50
87
|
this.readableQueue = [];
|
|
51
|
-
this.
|
|
52
|
-
if (message
|
|
88
|
+
this.onPortMessage = message => {
|
|
89
|
+
if (!message) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (message.error) {
|
|
93
|
+
// The producer failed mid-transfer - surface it as a stream error
|
|
94
|
+
// rather than letting the consumer believe it received everything.
|
|
95
|
+
this.destroy(new Error(message.error));
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (message.done || message.value) {
|
|
53
99
|
this.readableQueue.push(message);
|
|
54
100
|
|
|
55
101
|
if (this.canRead && this.readableQueue.length === 1) {
|
|
56
102
|
this._processReading();
|
|
57
103
|
}
|
|
58
104
|
}
|
|
59
|
-
}
|
|
105
|
+
};
|
|
106
|
+
this.messagePort.on('message', this.onPortMessage);
|
|
60
107
|
}
|
|
61
108
|
|
|
62
109
|
_processReading() {
|
|
@@ -73,7 +120,57 @@ class MessagePortReadable extends Readable {
|
|
|
73
120
|
this.canRead = true;
|
|
74
121
|
this._processReading();
|
|
75
122
|
}
|
|
123
|
+
|
|
124
|
+
_destroy(err, done) {
|
|
125
|
+
// Consumer is gone (client aborted, error, or clean end): tell the producer
|
|
126
|
+
// to stop and release the port + listener so nothing leaks across threads.
|
|
127
|
+
if (!this.portClosed) {
|
|
128
|
+
this.portClosed = true;
|
|
129
|
+
try {
|
|
130
|
+
this.messagePort.postMessage({ cancel: true });
|
|
131
|
+
} catch (cancelErr) {
|
|
132
|
+
// ignore - the peer may already be closed
|
|
133
|
+
}
|
|
134
|
+
this.messagePort.removeListener('message', this.onPortMessage);
|
|
135
|
+
try {
|
|
136
|
+
this.messagePort.close();
|
|
137
|
+
} catch (closeErr) {
|
|
138
|
+
// ignore
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
done(err);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Pipes a readable source into a MessagePortWritable so that an error on either
|
|
147
|
+
* side tears the transfer down instead of surfacing as an unhandled 'error'
|
|
148
|
+
* event. A mid-download failure on an IMAP source stream would otherwise crash
|
|
149
|
+
* the worker (and every account assigned to it).
|
|
150
|
+
*
|
|
151
|
+
* @param {Readable} source - Source stream (e.g. an IMAP download stream)
|
|
152
|
+
* @param {MessagePortWritable} writable - Destination bound to a MessagePort
|
|
153
|
+
* @param {Object} [logger] - Optional logger used to report transfer failures
|
|
154
|
+
*/
|
|
155
|
+
function pipeToMessagePort(source, writable, logger) {
|
|
156
|
+
pipeline(source, writable, err => {
|
|
157
|
+
if (!err || !logger) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
// A consumer that aborts mid-download closes the destination early; that
|
|
161
|
+
// is expected (not a failure) so it should not be logged at error level.
|
|
162
|
+
if (err.code === 'ERR_STREAM_PREMATURE_CLOSE') {
|
|
163
|
+
if (typeof logger.debug === 'function') {
|
|
164
|
+
logger.debug({ msg: 'Message stream transfer aborted by consumer', err });
|
|
165
|
+
}
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
if (typeof logger.error === 'function') {
|
|
169
|
+
logger.error({ msg: 'Message stream transfer failed', err });
|
|
170
|
+
}
|
|
171
|
+
});
|
|
76
172
|
}
|
|
77
173
|
|
|
78
174
|
module.exports.MessagePortWritable = MessagePortWritable;
|
|
79
175
|
module.exports.MessagePortReadable = MessagePortReadable;
|
|
176
|
+
module.exports.pipeToMessagePort = pipeToMessagePort;
|
package/lib/metrics-collector.js
CHANGED
package/lib/oauth2-apps.js
CHANGED
|
@@ -339,10 +339,6 @@ class OAuth2AppsHandler {
|
|
|
339
339
|
apps: []
|
|
340
340
|
};
|
|
341
341
|
|
|
342
|
-
if (idList.length <= startPos) {
|
|
343
|
-
return response;
|
|
344
|
-
}
|
|
345
|
-
|
|
346
342
|
//let keys = idList.slice(startPos, startPos + pageSize);
|
|
347
343
|
let keys = idList;
|
|
348
344
|
|
|
@@ -406,6 +402,8 @@ class OAuth2AppsHandler {
|
|
|
406
402
|
});
|
|
407
403
|
}
|
|
408
404
|
|
|
405
|
+
// Recount after undecodable entries were skipped and query filters were applied
|
|
406
|
+
response.total = response.apps.length;
|
|
409
407
|
response.pages = Math.ceil(response.apps.length / pageSize);
|
|
410
408
|
response.apps = response.apps.slice(startPos, startPos + pageSize);
|
|
411
409
|
|
|
@@ -747,6 +745,17 @@ class OAuth2AppsHandler {
|
|
|
747
745
|
|
|
748
746
|
async update(id, data, opts) {
|
|
749
747
|
opts = opts || {};
|
|
748
|
+
|
|
749
|
+
// `tenant` is a UI-style alias for an Outlook directory tenant ID. Only honor it when
|
|
750
|
+
// the caller explicitly selected it via authority='tenant' (the UI form convention) -
|
|
751
|
+
// a stray tenant value alone must not overwrite the stored authority.
|
|
752
|
+
if (data && typeof data === 'object' && 'tenant' in data) {
|
|
753
|
+
if (data.tenant && data.authority === 'tenant') {
|
|
754
|
+
data.authority = data.tenant;
|
|
755
|
+
}
|
|
756
|
+
delete data.tenant;
|
|
757
|
+
}
|
|
758
|
+
|
|
750
759
|
if (LEGACY_KEYS.includes(id)) {
|
|
751
760
|
// legacy
|
|
752
761
|
return await this.updateLegacyApp(id, data);
|