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
package/lib/sentry.js
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const packageData = require('../package.json');
|
|
4
|
+
const logger = require('./logger');
|
|
5
|
+
const { readEnvValue } = require('./tools');
|
|
6
|
+
const { COMMUNITY_SENTRY_DSN } = require('./consts');
|
|
7
|
+
|
|
8
|
+
const SENTRY_SETTINGS_CHECK_INTERVAL = 60 * 1000;
|
|
9
|
+
|
|
10
|
+
let Sentry;
|
|
11
|
+
let workerName;
|
|
12
|
+
let activeDsn = false;
|
|
13
|
+
|
|
14
|
+
function startSentry(dsn) {
|
|
15
|
+
// require lazily, the SDK loads several hundred modules in every worker thread,
|
|
16
|
+
// so only pay that cost when error tracking is actually enabled
|
|
17
|
+
if (!Sentry) {
|
|
18
|
+
Sentry = require('@sentry/node');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
Sentry.init({
|
|
22
|
+
dsn,
|
|
23
|
+
release: packageData.version,
|
|
24
|
+
// Error capture only: skip the OpenTelemetry setup and the default
|
|
25
|
+
// integrations that patch http/fetch/console on hot paths. Sentry's own
|
|
26
|
+
// uncaughtException/unhandledRejection integrations are left out on
|
|
27
|
+
// purpose - they do not run in worker threads and do not guarantee a
|
|
28
|
+
// process exit, so lib/logger.js owns reporting and exiting instead.
|
|
29
|
+
skipOpenTelemetrySetup: true,
|
|
30
|
+
defaultIntegrations: false,
|
|
31
|
+
integrations: [
|
|
32
|
+
Sentry.eventFiltersIntegration(),
|
|
33
|
+
Sentry.functionToStringIntegration(),
|
|
34
|
+
Sentry.linkedErrorsIntegration(),
|
|
35
|
+
Sentry.contextLinesIntegration(),
|
|
36
|
+
Sentry.nodeContextIntegration(),
|
|
37
|
+
Sentry.modulesIntegration()
|
|
38
|
+
],
|
|
39
|
+
initialScope: {
|
|
40
|
+
tags: { worker: workerName }
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
logger.notifyError = (err, opts) => {
|
|
45
|
+
let captureContext = {};
|
|
46
|
+
if (opts?.user) {
|
|
47
|
+
captureContext.user = { id: `${opts.user}` };
|
|
48
|
+
}
|
|
49
|
+
if (opts?.meta && Object.keys(opts.meta).length) {
|
|
50
|
+
captureContext.contexts = { ee: opts.meta };
|
|
51
|
+
}
|
|
52
|
+
Sentry.captureException(err, captureContext);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// the global exception handlers in lib/logger.js wait for this before exiting
|
|
56
|
+
logger.flushNotifications = () => Sentry.flush(2000);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Tag all events with the installation identity, so reports from different
|
|
60
|
+
// EmailEngine instances sharing the same DSN can be told apart. Read once per
|
|
61
|
+
// SDK start - a license swapped at runtime is reflected after the next restart.
|
|
62
|
+
async function applyIdentityTags() {
|
|
63
|
+
const settings = require('./settings');
|
|
64
|
+
let { serviceId, tract } = await settings.getMulti('serviceId', 'tract');
|
|
65
|
+
let tags = {};
|
|
66
|
+
if (serviceId) {
|
|
67
|
+
tags.instance = serviceId;
|
|
68
|
+
}
|
|
69
|
+
if (tract?.key) {
|
|
70
|
+
tags.license = tract.key;
|
|
71
|
+
}
|
|
72
|
+
Sentry.getGlobalScope().setTags(tags);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function applySentryState(dsn) {
|
|
76
|
+
dsn = dsn || false;
|
|
77
|
+
if (dsn === activeDsn) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (activeDsn) {
|
|
82
|
+
delete logger.notifyError;
|
|
83
|
+
delete logger.flushNotifications;
|
|
84
|
+
await Sentry.close(2000);
|
|
85
|
+
logger.info({ msg: 'Disabled Sentry error reporting', worker: workerName });
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (dsn) {
|
|
89
|
+
startSentry(dsn);
|
|
90
|
+
logger.info({ msg: 'Enabled Sentry error reporting', worker: workerName });
|
|
91
|
+
|
|
92
|
+
// identity lookup failure must not break error reporting, events just lack the tags
|
|
93
|
+
applyIdentityTags().catch(err => {
|
|
94
|
+
logger.error({ msg: 'Failed to apply Sentry identity tags', worker: workerName, err });
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
activeDsn = dsn;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function checkSentrySettings() {
|
|
102
|
+
const settings = require('./settings');
|
|
103
|
+
let { sentryEnabled, sentryDsn } = await settings.getMulti('sentryEnabled', 'sentryDsn');
|
|
104
|
+
await applySentryState(sentryEnabled ? sentryDsn || COMMUNITY_SENTRY_DSN : false);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Initialize Sentry error tracking. If the SENTRY_DSN environment variable is set,
|
|
108
|
+
// it pins the configuration and runtime settings are ignored. Otherwise the
|
|
109
|
+
// `sentryEnabled` and `sentryDsn` settings are applied at runtime and re-checked
|
|
110
|
+
// periodically, so error reporting can be toggled from the admin UI without a
|
|
111
|
+
// restart. While disabled, logger.notifyError stays undefined and the global
|
|
112
|
+
// exception handlers in lib/logger.js exit without waiting for a delivery flush.
|
|
113
|
+
function initSentry(worker) {
|
|
114
|
+
workerName = worker;
|
|
115
|
+
|
|
116
|
+
let envDsn = readEnvValue('SENTRY_DSN');
|
|
117
|
+
if (envDsn) {
|
|
118
|
+
applySentryState(envDsn).catch(err => {
|
|
119
|
+
logger.error({ msg: 'Failed to initialize Sentry', worker: workerName, err });
|
|
120
|
+
});
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Settings changes are detected by polling instead of the {cmd: 'settings'}
|
|
125
|
+
// broadcast because that broadcast does not reach all the threads that
|
|
126
|
+
// initialize Sentry (smtp, imap-proxy, documents, and the main thread).
|
|
127
|
+
let checkSettings = async () => {
|
|
128
|
+
try {
|
|
129
|
+
await checkSentrySettings();
|
|
130
|
+
} catch (err) {
|
|
131
|
+
logger.error({ msg: 'Failed to apply Sentry settings', worker: workerName, err });
|
|
132
|
+
}
|
|
133
|
+
setTimeout(checkSettings, SENTRY_SETTINGS_CHECK_INTERVAL).unref();
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
setImmediate(checkSettings);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
module.exports = { initSentry };
|
package/lib/settings.js
CHANGED
|
@@ -13,9 +13,12 @@ config.service = config.service || {};
|
|
|
13
13
|
const ENCRYPTED_KEYS = [
|
|
14
14
|
'gmailClientSecret',
|
|
15
15
|
'outlookClientSecret',
|
|
16
|
+
'mailRuClientSecret',
|
|
16
17
|
'cookiePassword',
|
|
17
18
|
'smtpServerPassword',
|
|
19
|
+
'imapProxyServerPassword',
|
|
18
20
|
'serviceSecret',
|
|
21
|
+
'serviceKey',
|
|
19
22
|
'gmailServiceKey',
|
|
20
23
|
'gmailServiceExternalAccount',
|
|
21
24
|
'documentStorePassword',
|
|
@@ -100,6 +103,9 @@ module.exports = {
|
|
|
100
103
|
formatSettingValue(key, value) {
|
|
101
104
|
switch (key) {
|
|
102
105
|
case 'serviceUrl': {
|
|
106
|
+
if (!value) {
|
|
107
|
+
return value;
|
|
108
|
+
}
|
|
103
109
|
let urlObj = new URL(value);
|
|
104
110
|
return urlObj.origin;
|
|
105
111
|
}
|
|
@@ -155,9 +161,9 @@ module.exports = {
|
|
|
155
161
|
}
|
|
156
162
|
}
|
|
157
163
|
|
|
158
|
-
if (
|
|
159
|
-
// make sure `notifyText` is enabled
|
|
160
|
-
|
|
164
|
+
if (['generateEmailSummary', 'openAiGenerateEmbeddings'].includes(key) && formattedValue) {
|
|
165
|
+
// AI processing needs access to message text content, so make sure `notifyText` is enabled as well
|
|
166
|
+
await module.exports.set('notifyText', true);
|
|
161
167
|
}
|
|
162
168
|
|
|
163
169
|
if (/^openAi/.test(key) || key === 'generateEmailSummary') {
|
package/lib/stream-encrypt.js
CHANGED
|
@@ -162,7 +162,7 @@ class DecryptStream extends Transform {
|
|
|
162
162
|
decrypted = Buffer.concat([decipher.update(encryptedData), decipher.final()]);
|
|
163
163
|
} catch (err) {
|
|
164
164
|
if (err.message.includes('auth')) {
|
|
165
|
-
throw new Error('Decryption failed: invalid secret or corrupted data');
|
|
165
|
+
throw new Error('Decryption failed: invalid secret or corrupted data', { cause: err });
|
|
166
166
|
}
|
|
167
167
|
throw err;
|
|
168
168
|
}
|
package/lib/templates.js
CHANGED
|
@@ -53,7 +53,7 @@ class TemplateHandler {
|
|
|
53
53
|
let templateMeta = msgpack.decode(entry);
|
|
54
54
|
response.templates.push(templateMeta);
|
|
55
55
|
} catch (err) {
|
|
56
|
-
logger.error({ msg: 'Failed to process template', entry: entry.toString('base64') });
|
|
56
|
+
logger.error({ msg: 'Failed to process template', entry: entry && entry.toString('base64'), err });
|
|
57
57
|
continue;
|
|
58
58
|
}
|
|
59
59
|
}
|
package/lib/tokens.js
CHANGED
|
@@ -325,16 +325,18 @@ module.exports = {
|
|
|
325
325
|
let lastEntry = false;
|
|
326
326
|
for (let i = 0; i < detailList.length; i++) {
|
|
327
327
|
let entry = detailList[i];
|
|
328
|
+
// Each token occupies two consecutive multi results (data + access info)
|
|
329
|
+
let tokenHash = list[Math.floor(i / 2)];
|
|
328
330
|
if (i % 2 === 0) {
|
|
329
331
|
lastEntry = false;
|
|
330
332
|
if (entry[1]) {
|
|
331
333
|
try {
|
|
332
334
|
let tokenData = msgpack.decode(entry[1]);
|
|
333
335
|
tokenData.created = new Date(tokenData.created);
|
|
334
|
-
lastEntry = Object.assign({ id:
|
|
336
|
+
lastEntry = Object.assign({ id: tokenHash }, tokenData);
|
|
335
337
|
response.tokens.push(lastEntry);
|
|
336
338
|
} catch (err) {
|
|
337
|
-
logger.error({ msg: 'Failed to process token data', hash:
|
|
339
|
+
logger.error({ msg: 'Failed to process token data', hash: tokenHash, err });
|
|
338
340
|
}
|
|
339
341
|
}
|
|
340
342
|
} else if (lastEntry && entry[1]) {
|
|
@@ -343,7 +345,7 @@ module.exports = {
|
|
|
343
345
|
accessData.time = accessData.time ? new Date(accessData.time) : null;
|
|
344
346
|
lastEntry.access = accessData;
|
|
345
347
|
} catch (err) {
|
|
346
|
-
logger.error({ msg: 'Failed to process token data', hash:
|
|
348
|
+
logger.error({ msg: 'Failed to process token data', hash: tokenHash, err });
|
|
347
349
|
}
|
|
348
350
|
}
|
|
349
351
|
}
|
package/lib/tools.js
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
/* eslint no-bitwise: 0 */
|
|
2
2
|
|
|
3
|
-
// NB! This file is processed by gettext parser and can not use newer syntax like ?.
|
|
4
|
-
|
|
5
3
|
'use strict';
|
|
6
4
|
|
|
7
5
|
const msgpack = require('msgpack5')();
|
|
@@ -145,6 +143,14 @@ async function reloadHttpProxyAgent() {
|
|
|
145
143
|
}
|
|
146
144
|
}
|
|
147
145
|
|
|
146
|
+
// Reload the shared HTTP proxy agent, but only when proxy-related settings actually changed.
|
|
147
|
+
// Centralizes the 'settings' message handling shared by the main thread and every worker.
|
|
148
|
+
function maybeReloadHttpProxyAgent(data) {
|
|
149
|
+
if (data && ('httpProxyEnabled' in data || 'httpProxyUrl' in data)) {
|
|
150
|
+
reloadHttpProxyAgent().catch(err => logger.error({ msg: 'Failed to reload HTTP proxy agent', err }));
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
148
154
|
async function _doReloadHttpProxyAgent() {
|
|
149
155
|
let enabled, proxyUrl;
|
|
150
156
|
try {
|
|
@@ -1387,6 +1393,9 @@ MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEV3QUiYsp13nD9suD1/ZkEXnuMoSg
|
|
|
1387
1393
|
let redisSoftware;
|
|
1388
1394
|
let redisCluster = false;
|
|
1389
1395
|
|
|
1396
|
+
// Consolidated list of Redis health issues, rendered as dashboard warning banners
|
|
1397
|
+
let redisWarnings = [];
|
|
1398
|
+
|
|
1390
1399
|
try {
|
|
1391
1400
|
let redisInfo = await module.exports.getRedisStats(redis);
|
|
1392
1401
|
|
|
@@ -1434,6 +1443,61 @@ MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEV3QUiYsp13nD9suD1/ZkEXnuMoSg
|
|
|
1434
1443
|
softwareDetails = `Amazon MemoryDB`;
|
|
1435
1444
|
redisSoftware = 'memorydb';
|
|
1436
1445
|
}
|
|
1446
|
+
|
|
1447
|
+
// Incompatible backend: Amazon ElastiCache
|
|
1448
|
+
if (redisSoftware === 'elasticache') {
|
|
1449
|
+
redisWarnings.push({
|
|
1450
|
+
key: 'elasticache',
|
|
1451
|
+
color: 'danger',
|
|
1452
|
+
title: 'Redis compatibility warning',
|
|
1453
|
+
details: [
|
|
1454
|
+
'EmailEngine is incompatible with Amazon ElastiCache as the database backend.',
|
|
1455
|
+
'Please switch to a standard Redis instance. Using ElastiCache with EmailEngine can result in data loss and some features may not work correctly.'
|
|
1456
|
+
]
|
|
1457
|
+
});
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
// Incompatible backend: Redis Cluster
|
|
1461
|
+
if (redisCluster) {
|
|
1462
|
+
redisWarnings.push({
|
|
1463
|
+
key: 'cluster',
|
|
1464
|
+
color: 'danger',
|
|
1465
|
+
title: 'Redis compatibility warning',
|
|
1466
|
+
details: ['EmailEngine is incompatible with a Redis Cluster setup.', 'Please switch to a standard Redis primary instance.']
|
|
1467
|
+
});
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
// EmailEngine uses Redis as a database, not a cache. Any eviction silently drops sync state,
|
|
1471
|
+
// which makes already-synced messages look new and triggers duplicate webhooks.
|
|
1472
|
+
let maxmemoryPolicy = typeof redisInfo.maxmemory_policy === 'string' ? redisInfo.maxmemory_policy : null;
|
|
1473
|
+
let maxmemory = Number(redisInfo.maxmemory) || 0;
|
|
1474
|
+
let evictedKeys = Number(redisInfo.evicted_keys) || 0;
|
|
1475
|
+
|
|
1476
|
+
if (evictedKeys > 0) {
|
|
1477
|
+
// Definitive: Redis has already evicted keys
|
|
1478
|
+
redisWarnings.push({
|
|
1479
|
+
key: 'evicted-keys',
|
|
1480
|
+
color: 'danger',
|
|
1481
|
+
title: 'Redis eviction detected',
|
|
1482
|
+
details: [
|
|
1483
|
+
`Redis has evicted ${evictedKeys.toLocaleString('en-US')} key${evictedKeys === 1 ? '' : 's'} after reaching its memory limit.`,
|
|
1484
|
+
'EmailEngine uses Redis as its primary database, not a cache. Evicted keys cause already-synced messages to be reprocessed - generating duplicate webhooks - and can lead to data loss.',
|
|
1485
|
+
'Set the Redis "maxmemory-policy" to "noeviction" and provision enough memory so Redis never reaches its limit.'
|
|
1486
|
+
]
|
|
1487
|
+
});
|
|
1488
|
+
} else if (maxmemoryPolicy && maxmemoryPolicy !== 'noeviction') {
|
|
1489
|
+
// Latent risk: an eviction policy is configured but nothing has been evicted yet
|
|
1490
|
+
redisWarnings.push({
|
|
1491
|
+
key: 'maxmemory-policy',
|
|
1492
|
+
color: maxmemory > 0 ? 'danger' : 'warning',
|
|
1493
|
+
title: 'Unsafe Redis eviction policy',
|
|
1494
|
+
details: [
|
|
1495
|
+
`The Redis "maxmemory-policy" is set to "${maxmemoryPolicy}", but EmailEngine requires "noeviction".`,
|
|
1496
|
+
'With any other policy, Redis can delete EmailEngine sync state under memory pressure, causing already-synced messages to be reprocessed - generating duplicate webhooks - and possible data loss.',
|
|
1497
|
+
'Set "maxmemory-policy noeviction" and make sure Redis has enough memory for your workload.'
|
|
1498
|
+
]
|
|
1499
|
+
});
|
|
1500
|
+
}
|
|
1437
1501
|
} catch (err) {
|
|
1438
1502
|
logger.error({ msg: 'Failed to get stats', err });
|
|
1439
1503
|
redisVersion = err.message;
|
|
@@ -1451,9 +1515,9 @@ MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEV3QUiYsp13nD9suD1/ZkEXnuMoSg
|
|
|
1451
1515
|
.hget(`${REDIS_PREFIX}bull:${queue}:meta`, 'paused')
|
|
1452
1516
|
.exec();
|
|
1453
1517
|
if (resActive[0] || resDelayed[0] || resWaiting[0] || resPaused[0] || resMeta[0]) {
|
|
1454
|
-
// counting failed
|
|
1518
|
+
// counting failed, skip this queue but keep the rest of the stats response
|
|
1455
1519
|
logger.error({ msg: 'Failed to count queue length', queue, active: resActive, delayed: resDelayed, waiting: resWaiting });
|
|
1456
|
-
|
|
1520
|
+
continue;
|
|
1457
1521
|
}
|
|
1458
1522
|
queues[queue] = {
|
|
1459
1523
|
active: Number(resActive[1]) || 0,
|
|
@@ -1477,6 +1541,7 @@ MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEV3QUiYsp13nD9suD1/ZkEXnuMoSg
|
|
|
1477
1541
|
redis: `${redisVersion}${softwareDetails ? ` (${softwareDetails})` : ''}`,
|
|
1478
1542
|
redisSoftware,
|
|
1479
1543
|
redisCluster,
|
|
1544
|
+
redisWarnings,
|
|
1480
1545
|
imapflow: ImapFlow.version || 'please upgrade',
|
|
1481
1546
|
bullmq: bullmqPackage.version,
|
|
1482
1547
|
arch: process.arch,
|
|
@@ -1963,6 +2028,7 @@ MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEV3QUiYsp13nD9suD1/ZkEXnuMoSg
|
|
|
1963
2028
|
|
|
1964
2029
|
httpAgent,
|
|
1965
2030
|
reloadHttpProxyAgent,
|
|
2031
|
+
maybeReloadHttpProxyAgent,
|
|
1966
2032
|
createSocksAgent,
|
|
1967
2033
|
|
|
1968
2034
|
get fetchAgent() {
|