emailengine-app 2.68.1 → 2.69.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 +2 -0
- package/.github/workflows/release.yaml +4 -0
- package/CHANGELOG.md +40 -0
- package/config/default.toml +2 -0
- package/data/google-crawlers.json +7 -1
- package/lib/account.js +62 -25
- package/lib/api-routes/account-routes.js +493 -75
- package/lib/api-routes/blocklist-routes.js +337 -0
- package/lib/api-routes/delivery-test-routes.js +321 -0
- package/lib/api-routes/export-routes.js +1 -12
- package/lib/api-routes/gateway-routes.js +376 -0
- package/lib/api-routes/license-routes.js +142 -0
- package/lib/api-routes/mailbox-routes.js +318 -0
- package/lib/api-routes/message-routes.js +21 -129
- package/lib/api-routes/oauth2-app-routes.js +631 -0
- package/lib/api-routes/outbox-routes.js +173 -0
- package/lib/api-routes/pubsub-routes.js +98 -0
- package/lib/api-routes/route-helpers.js +45 -0
- package/lib/api-routes/settings-routes.js +331 -0
- package/lib/api-routes/stats-routes.js +77 -0
- package/lib/api-routes/submit-routes.js +472 -0
- package/lib/api-routes/template-routes.js +7 -55
- package/lib/api-routes/token-routes.js +297 -0
- package/lib/api-routes/webhook-route-routes.js +152 -0
- package/lib/email-client/gmail-client.js +14 -0
- package/lib/email-client/imap/mailbox.js +34 -11
- package/lib/email-client/imap/subconnection.js +20 -12
- package/lib/email-client/imap/sync-operations.js +130 -2
- package/lib/email-client/imap-client.js +116 -58
- package/lib/email-client/outlook-client.js +85 -13
- package/lib/export.js +60 -19
- 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 -23
- package/lib/imapproxy/imap-core/lib/imap-server.js +24 -0
- package/lib/imapproxy/imap-core/lib/imap-stream.js +26 -0
- package/lib/message-port-stream.js +113 -16
- package/lib/reject-worker-calls.js +42 -0
- package/lib/routes-ui.js +37 -8778
- package/lib/schemas.js +26 -1
- package/lib/tools.js +68 -0
- package/lib/ui-routes/account-routes.js +40 -210
- package/lib/ui-routes/admin-config-routes.js +913 -487
- 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} +371 -91
- package/lib/ui-routes/route-helpers.js +316 -0
- package/lib/ui-routes/smtp-test-routes.js +236 -0
- package/lib/ui-routes/unsubscribe-routes.js +234 -0
- package/lib/webhook-request.js +36 -0
- package/package.json +8 -8
- package/sbom.json +1 -1
- package/server.js +214 -16
- package/static/licenses.html +12 -12
- package/translations/messages.pot +129 -149
- package/views/dashboard.hbs +7 -26
- package/views/internals/index.hbs +15 -0
- package/views/tokens/index.hbs +9 -0
- package/workers/api.js +198 -4401
- package/workers/export.js +87 -54
- package/workers/imap.js +29 -13
- package/workers/submit.js +20 -11
- package/workers/webhooks.js +6 -20
package/server.js
CHANGED
|
@@ -72,7 +72,8 @@ const {
|
|
|
72
72
|
getRedisStats,
|
|
73
73
|
threadStats,
|
|
74
74
|
httpAgent,
|
|
75
|
-
reloadHttpProxyAgent
|
|
75
|
+
reloadHttpProxyAgent,
|
|
76
|
+
maybeReloadHttpProxyAgent
|
|
76
77
|
} = require('./lib/tools');
|
|
77
78
|
const MetricsCollector = require('./lib/metrics-collector');
|
|
78
79
|
|
|
@@ -148,6 +149,8 @@ const { QueueEvents } = require('bullmq');
|
|
|
148
149
|
|
|
149
150
|
const getSecret = require('./lib/get-secret');
|
|
150
151
|
|
|
152
|
+
const { rejectWorkerCalls } = require('./lib/reject-worker-calls');
|
|
153
|
+
|
|
151
154
|
const msgpack = require('msgpack5')();
|
|
152
155
|
|
|
153
156
|
// Initialize default configuration values if not set
|
|
@@ -220,6 +223,10 @@ config.dbs.redis = readEnvValue('EENGINE_REDIS') || readEnvValue('REDIS_URL') ||
|
|
|
220
223
|
config.workers.imap = getWorkerCount(readEnvValue('EENGINE_WORKERS') || config.workers.imap) || 4;
|
|
221
224
|
config.workers.webhooks = Number(readEnvValue('EENGINE_WORKERS_WEBHOOKS')) || config.workers.webhooks || 1;
|
|
222
225
|
config.workers.submit = Number(readEnvValue('EENGINE_WORKERS_SUBMIT')) || config.workers.submit || 1;
|
|
226
|
+
// API worker count. Values >1 require SO_REUSEPORT (Linux); on unsupported platforms it falls back to 1 at startup.
|
|
227
|
+
// Uses getWorkerCount() for parity with EENGINE_WORKERS (supports "cpus"); Math.floor avoids a fractional
|
|
228
|
+
// count over-spawning, Math.max keeps at least one API worker.
|
|
229
|
+
config.workers.api = Math.max(1, Math.floor(getWorkerCount(readEnvValue('EENGINE_WORKERS_API') || config.workers.api)));
|
|
223
230
|
|
|
224
231
|
config.api.port =
|
|
225
232
|
(hasEnvValue('EENGINE_PORT') && Number(readEnvValue('EENGINE_PORT'))) || (hasEnvValue('PORT') && Number(readEnvValue('PORT'))) || config.api.port;
|
|
@@ -275,6 +282,12 @@ const NO_ACTIVE_HANDLER_RESP_ERR = new Error('No active handler for requested ac
|
|
|
275
282
|
NO_ACTIVE_HANDLER_RESP_ERR.statusCode = 503;
|
|
276
283
|
NO_ACTIVE_HANDLER_RESP_ERR.code = 'WorkerNotAvailable';
|
|
277
284
|
|
|
285
|
+
// Shared rejection for in-flight calls whose target worker terminated mid-request.
|
|
286
|
+
// Reused across concurrent rejections - never attach per-call fields to this instance.
|
|
287
|
+
const WORKER_DIED_RESP_ERR = new Error('Worker handling the request terminated before completion. Try again.');
|
|
288
|
+
WORKER_DIED_RESP_ERR.statusCode = 503;
|
|
289
|
+
WORKER_DIED_RESP_ERR.code = 'WorkerNotAvailable';
|
|
290
|
+
|
|
278
291
|
// Update check intervals
|
|
279
292
|
const UPGRADE_CHECK_TIMEOUT = 1 * 24 * 3600 * 1000; // 24 hours
|
|
280
293
|
const LICENSE_CHECK_TIMEOUT = 20 * 60 * 1000; // 20 minutes
|
|
@@ -315,6 +328,7 @@ const THREAD_NAMES = {
|
|
|
315
328
|
*/
|
|
316
329
|
const THREAD_CONFIG_VALUES = {
|
|
317
330
|
imap: { key: 'EENGINE_WORKERS', value: config.workers.imap },
|
|
331
|
+
api: { key: 'EENGINE_WORKERS_API', value: config.workers.api },
|
|
318
332
|
submit: { key: 'EENGINE_WORKERS_SUBMIT', value: config.workers.submit },
|
|
319
333
|
webhooks: { key: 'EENGINE_WORKERS_WEBHOOKS', value: config.workers.webhooks },
|
|
320
334
|
export: { key: 'EENGINE_WORKERS_EXPORT', value: config.workers.export || 1 }
|
|
@@ -326,6 +340,97 @@ const queueEvents = {};
|
|
|
326
340
|
// Unique run index for this server instance
|
|
327
341
|
let runIndex;
|
|
328
342
|
|
|
343
|
+
// Whether multiple API workers may share the listen port via SO_REUSEPORT.
|
|
344
|
+
// Determined once at startup by probeReusePort(); false means a single API worker.
|
|
345
|
+
let apiWorkerReusePort = false;
|
|
346
|
+
|
|
347
|
+
// Requested API worker count (EENGINE_WORKERS_API), surfaced on /admin/internals so operators can
|
|
348
|
+
// see when more workers were requested than could be started. Whether we fell back to a single
|
|
349
|
+
// worker is derived: `apiWorkersRequested > 1 && !apiWorkerReusePort`.
|
|
350
|
+
let apiWorkersRequested = 1;
|
|
351
|
+
|
|
352
|
+
// Why a multi-worker request fell back to a single API worker, surfaced on /admin/internals so the
|
|
353
|
+
// banner can state the real cause. Set during probeReusePort():
|
|
354
|
+
// - 'platform': SO_REUSEPORT is unsupported on this OS/Node build (Linux with Node < 23.1, macOS, Windows)
|
|
355
|
+
// - 'port': the probe could not test the port (transient conflict / permission), so support is unknown
|
|
356
|
+
// Stays null when SO_REUSEPORT is supported or only one worker was requested.
|
|
357
|
+
let apiWorkerReusePortFallbackReason = null;
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Probe whether SO_REUSEPORT can load-balance across multiple sockets on this
|
|
361
|
+
* platform. Binds two listeners to the same host:port with reusePort enabled;
|
|
362
|
+
* both must succeed for load balancing to work. It is unsupported on macOS,
|
|
363
|
+
* Windows, and Node < 23.1, where either the first bind fails outright (e.g.
|
|
364
|
+
* ENOTSUP) or - when the reusePort option is silently ignored (Node < 23.1) -
|
|
365
|
+
* the first bind succeeds as a plain bind and the second fails with EADDRINUSE.
|
|
366
|
+
* The `stage` field tells the caller where the probe failed so it can separate a
|
|
367
|
+
* genuine platform/capability limitation from a transient port conflict:
|
|
368
|
+
* - 'probe': the first bind failed, so the port could not even be tested. An
|
|
369
|
+
* EADDRINUSE/EACCES code here means the port is unavailable (transient); any
|
|
370
|
+
* other code means the platform rejected reusePort.
|
|
371
|
+
* - 'reuse': the first bind succeeded but the second did not, so reusePort is
|
|
372
|
+
* not honored on this platform/Node - a capability limitation, even though
|
|
373
|
+
* the failing code is EADDRINUSE.
|
|
374
|
+
* - null: SO_REUSEPORT is supported.
|
|
375
|
+
* @param {string} host - Bind address
|
|
376
|
+
* @param {number} port - Listen port
|
|
377
|
+
* @returns {Promise<{supported: boolean, code: (string|null), stage: (string|null)}>}
|
|
378
|
+
* Whether SO_REUSEPORT load balancing works, the failing bind's error code (or
|
|
379
|
+
* null), and which bind failed ('probe', 'reuse', or null when supported)
|
|
380
|
+
*/
|
|
381
|
+
async function probeReusePort(host, port) {
|
|
382
|
+
const net = require('net');
|
|
383
|
+
|
|
384
|
+
const open = () =>
|
|
385
|
+
new Promise(resolve => {
|
|
386
|
+
let srv = net.createServer();
|
|
387
|
+
srv.once('error', err => resolve({ srv: null, code: (err && err.code) || null }));
|
|
388
|
+
try {
|
|
389
|
+
srv.listen({ host, port, reusePort: true }, () => resolve({ srv, code: null }));
|
|
390
|
+
} catch (err) {
|
|
391
|
+
resolve({ srv: null, code: (err && err.code) || null });
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
const close = srv =>
|
|
396
|
+
new Promise(resolve => {
|
|
397
|
+
if (!srv) {
|
|
398
|
+
return resolve();
|
|
399
|
+
}
|
|
400
|
+
srv.close(() => resolve());
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
let first = await open();
|
|
404
|
+
if (!first.srv) {
|
|
405
|
+
// Could not even bind the first probe socket, so the port itself could not be tested.
|
|
406
|
+
return { supported: false, code: first.code, stage: 'probe' };
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
let second = await open();
|
|
410
|
+
await close(first.srv);
|
|
411
|
+
await close(second.srv);
|
|
412
|
+
|
|
413
|
+
if (second.srv) {
|
|
414
|
+
return { supported: true, code: null, stage: null };
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// The first bind succeeded but a second listener on the same host:port did not, so the kernel is
|
|
418
|
+
// not load-balancing via SO_REUSEPORT - the option was accepted/ignored but not honored (e.g.
|
|
419
|
+
// Linux with Node < 23.1, where reusePort is an unrecognized listen() option). This is a platform
|
|
420
|
+
// limitation, not a port conflict, even though the failing code is EADDRINUSE.
|
|
421
|
+
return { supported: false, code: second.code, stage: 'reuse' };
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Compute per-worker spawn options. API workers receive their index and whether to
|
|
426
|
+
* bind with SO_REUSEPORT; other worker types need no extra workerData. Keeps the
|
|
427
|
+
* startup and respawn paths deriving spawn options from a single place.
|
|
428
|
+
* @param {string} type - Worker type
|
|
429
|
+
* @param {number} workerIndex - Zero-based index within the worker type
|
|
430
|
+
* @returns {Object|undefined} Spawn options for spawnWorker(), or undefined
|
|
431
|
+
*/
|
|
432
|
+
const workerSpawnOpts = (type, workerIndex) => (type === 'api' ? { workerData: { workerIndex, reusePort: apiWorkerReusePort } } : undefined);
|
|
433
|
+
|
|
329
434
|
// Prepared configuration handling
|
|
330
435
|
let preparedSettings = false;
|
|
331
436
|
const preparedSettingsString = readEnvValue('EENGINE_SETTINGS') || config.settings;
|
|
@@ -682,7 +787,7 @@ let updateServerState = async (type, state, payload) => {
|
|
|
682
787
|
for (let worker of workers.get('api')) {
|
|
683
788
|
let callPayload = {
|
|
684
789
|
cmd: 'change',
|
|
685
|
-
type:
|
|
790
|
+
type: `${type}ServerState`,
|
|
686
791
|
key: state,
|
|
687
792
|
payload: payload || null
|
|
688
793
|
};
|
|
@@ -930,9 +1035,11 @@ async function sendWebhook(account, event, data) {
|
|
|
930
1035
|
/**
|
|
931
1036
|
* Spawn a new worker thread of the specified type
|
|
932
1037
|
* @param {string} type - Worker type (imap, api, webhooks, submit, documents, smtp, imapProxy)
|
|
1038
|
+
* @param {Object} [opts] - Optional spawn options
|
|
1039
|
+
* @param {Object} [opts.workerData] - Data passed to the worker thread (e.g. API worker index)
|
|
933
1040
|
* @returns {Promise<number|void>} Thread ID if successful
|
|
934
1041
|
*/
|
|
935
|
-
let spawnWorker = async type => {
|
|
1042
|
+
let spawnWorker = async (type, opts) => {
|
|
936
1043
|
// Don't spawn workers during shutdown
|
|
937
1044
|
if (isClosing) {
|
|
938
1045
|
return;
|
|
@@ -966,6 +1073,7 @@ let spawnWorker = async type => {
|
|
|
966
1073
|
let worker = new WorkerThread(pathlib.join(__dirname, 'workers', `${type.replace(/[A-Z]/g, c => `-${c.toLowerCase()}`)}.js`), {
|
|
967
1074
|
argv,
|
|
968
1075
|
env: SHARE_ENV,
|
|
1076
|
+
workerData: opts && opts.workerData,
|
|
969
1077
|
trackUnmanagedFds: true
|
|
970
1078
|
});
|
|
971
1079
|
metrics.threadStarts.inc();
|
|
@@ -1013,6 +1121,13 @@ let spawnWorker = async type => {
|
|
|
1013
1121
|
onlineWorkers.delete(worker);
|
|
1014
1122
|
metrics.threadStops.inc();
|
|
1015
1123
|
|
|
1124
|
+
// Fail any in-flight calls routed to this worker right away, so callers
|
|
1125
|
+
// get a fast retryable error instead of hanging until their own timeout.
|
|
1126
|
+
let rejectedCalls = rejectWorkerCalls(callQueue, worker, WORKER_DIED_RESP_ERR);
|
|
1127
|
+
if (rejectedCalls) {
|
|
1128
|
+
logger.info({ msg: 'Rejected in-flight calls for exited worker', type, threadId: worker.threadId, rejectedCalls });
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1016
1131
|
workers.get(type).delete(worker);
|
|
1017
1132
|
|
|
1018
1133
|
// Update server state for proxy servers
|
|
@@ -1117,9 +1232,9 @@ let spawnWorker = async type => {
|
|
|
1117
1232
|
logger.error({ msg: 'Worker unexpectedly exited', exitCode, type });
|
|
1118
1233
|
}
|
|
1119
1234
|
|
|
1120
|
-
// Respawn worker after delay
|
|
1235
|
+
// Respawn worker after delay, preserving any spawn options (e.g. API worker index)
|
|
1121
1236
|
await new Promise(r => setTimeout(r, 1000));
|
|
1122
|
-
await spawnWorker(type);
|
|
1237
|
+
await spawnWorker(type, opts);
|
|
1123
1238
|
};
|
|
1124
1239
|
|
|
1125
1240
|
// Handle worker exit
|
|
@@ -1325,10 +1440,8 @@ let spawnWorker = async type => {
|
|
|
1325
1440
|
}
|
|
1326
1441
|
|
|
1327
1442
|
case 'settings':
|
|
1328
|
-
// Reload HTTP proxy agent in the main thread
|
|
1329
|
-
|
|
1330
|
-
reloadHttpProxyAgent().catch(err => logger.error({ msg: 'Failed to reload HTTP proxy agent', err }));
|
|
1331
|
-
}
|
|
1443
|
+
// Reload the HTTP proxy agent in the main thread when proxy settings change
|
|
1444
|
+
maybeReloadHttpProxyAgent(message.data);
|
|
1332
1445
|
|
|
1333
1446
|
// Forward settings changes to all IMAP workers
|
|
1334
1447
|
availableIMAPWorkers.forEach(worker => {
|
|
@@ -1339,8 +1452,8 @@ let spawnWorker = async type => {
|
|
|
1339
1452
|
}
|
|
1340
1453
|
});
|
|
1341
1454
|
|
|
1342
|
-
// Forward settings changes to webhooks, submit, and export workers
|
|
1343
|
-
for (let type of ['webhooks', 'submit', 'export']) {
|
|
1455
|
+
// Forward settings changes to API, webhooks, submit, and export workers
|
|
1456
|
+
for (let type of ['api', 'webhooks', 'submit', 'export']) {
|
|
1344
1457
|
let typeWorkers = workers.get(type);
|
|
1345
1458
|
if (typeWorkers) {
|
|
1346
1459
|
typeWorkers.forEach(worker => {
|
|
@@ -1499,7 +1612,8 @@ async function call(worker, message, transferList) {
|
|
|
1499
1612
|
reject(err);
|
|
1500
1613
|
}, ttl);
|
|
1501
1614
|
|
|
1502
|
-
// Store callback info
|
|
1615
|
+
// Store callback info. `worker` lets us reject this entry immediately if
|
|
1616
|
+
// the target worker terminates, instead of waiting out the timeout.
|
|
1503
1617
|
callQueue.set(mid, {
|
|
1504
1618
|
resolve: result => {
|
|
1505
1619
|
clearTimeout(timer);
|
|
@@ -1509,7 +1623,8 @@ async function call(worker, message, transferList) {
|
|
|
1509
1623
|
clearTimeout(timer);
|
|
1510
1624
|
reject(err);
|
|
1511
1625
|
},
|
|
1512
|
-
timer
|
|
1626
|
+
timer,
|
|
1627
|
+
worker
|
|
1513
1628
|
});
|
|
1514
1629
|
|
|
1515
1630
|
try {
|
|
@@ -1850,7 +1965,7 @@ let licenseCheckHandler = async opts => {
|
|
|
1850
1965
|
default:
|
|
1851
1966
|
if (config.workers && config.workers[type]) {
|
|
1852
1967
|
for (let i = 0; i < config.workers[type]; i++) {
|
|
1853
|
-
await spawnWorker(type);
|
|
1968
|
+
await spawnWorker(type, workerSpawnOpts(type, i));
|
|
1854
1969
|
}
|
|
1855
1970
|
}
|
|
1856
1971
|
}
|
|
@@ -2588,6 +2703,16 @@ async function onCommand(worker, message) {
|
|
|
2588
2703
|
return await getThreadsInfo();
|
|
2589
2704
|
}
|
|
2590
2705
|
|
|
2706
|
+
case 'apiWorkerScaling': {
|
|
2707
|
+
// Reported on /admin/internals so operators can see when EENGINE_WORKERS_API > 1
|
|
2708
|
+
// was requested but only one worker started, and why (reason: 'platform' | 'port').
|
|
2709
|
+
return {
|
|
2710
|
+
requested: apiWorkersRequested,
|
|
2711
|
+
fallback: apiWorkersRequested > 1 && !apiWorkerReusePort,
|
|
2712
|
+
reason: apiWorkerReusePortFallbackReason
|
|
2713
|
+
};
|
|
2714
|
+
}
|
|
2715
|
+
|
|
2591
2716
|
case 'worker-accounts': {
|
|
2592
2717
|
// Get accounts assigned to a specific worker thread
|
|
2593
2718
|
const { threadId, page = 1, pageSize = 20 } = message;
|
|
@@ -3192,8 +3317,81 @@ const startApplication = async () => {
|
|
|
3192
3317
|
|
|
3193
3318
|
// -- START WORKER THREADS
|
|
3194
3319
|
|
|
3195
|
-
// Start API server first for health checks
|
|
3196
|
-
|
|
3320
|
+
// Start API server first for health checks.
|
|
3321
|
+
// Optionally run several API workers that share the listen port via SO_REUSEPORT.
|
|
3322
|
+
apiWorkersRequested = config.workers.api;
|
|
3323
|
+
// Effective count we actually start; drops to 1 if SO_REUSEPORT is unavailable. Kept as a local
|
|
3324
|
+
// so we don't mutate the parsed config object that is read elsewhere.
|
|
3325
|
+
let effectiveApiWorkers = config.workers.api;
|
|
3326
|
+
if (config.workers.api > 1) {
|
|
3327
|
+
const probe = await probeReusePort(config.api.host, config.api.port);
|
|
3328
|
+
apiWorkerReusePort = probe.supported;
|
|
3329
|
+
if (!apiWorkerReusePort) {
|
|
3330
|
+
// Only a first-bind EADDRINUSE/EACCES (stage 'probe') means we genuinely could not test
|
|
3331
|
+
// the port - a transient conflict or permission issue, which the single API worker we
|
|
3332
|
+
// start next will surface. A failed second bind (stage 'reuse') instead means reusePort
|
|
3333
|
+
// is accepted but not honored on this platform/Node (e.g. Node < 23.1), which is a
|
|
3334
|
+
// capability limitation rather than a port conflict, so it falls through to the
|
|
3335
|
+
// "not available on this platform" message below.
|
|
3336
|
+
if (probe.stage === 'probe' && (probe.code === 'EADDRINUSE' || probe.code === 'EACCES')) {
|
|
3337
|
+
apiWorkerReusePortFallbackReason = 'port';
|
|
3338
|
+
logger.warn({
|
|
3339
|
+
msg: 'Could not probe SO_REUSEPORT because the API port is unavailable; starting a single API worker',
|
|
3340
|
+
requested: apiWorkersRequested,
|
|
3341
|
+
code: probe.code,
|
|
3342
|
+
host: config.api.host,
|
|
3343
|
+
port: config.api.port
|
|
3344
|
+
});
|
|
3345
|
+
} else {
|
|
3346
|
+
apiWorkerReusePortFallbackReason = 'platform';
|
|
3347
|
+
logger.warn({
|
|
3348
|
+
msg: 'Multiple API workers requested but SO_REUSEPORT is not available on this platform; starting a single API worker',
|
|
3349
|
+
requested: apiWorkersRequested,
|
|
3350
|
+
code: probe.code
|
|
3351
|
+
});
|
|
3352
|
+
}
|
|
3353
|
+
effectiveApiWorkers = 1;
|
|
3354
|
+
// The /admin/internals warning banner (via the apiWorkerScaling command) tells the
|
|
3355
|
+
// operator only one worker started; the thread-config popover keeps showing the
|
|
3356
|
+
// configured EENGINE_WORKERS_API value rather than being rewritten here.
|
|
3357
|
+
} else {
|
|
3358
|
+
logger.info({ msg: 'SO_REUSEPORT is available; starting multiple API workers', workers: effectiveApiWorkers });
|
|
3359
|
+
}
|
|
3360
|
+
}
|
|
3361
|
+
|
|
3362
|
+
let apiPromises = [];
|
|
3363
|
+
for (let i = 0; i < effectiveApiWorkers; i++) {
|
|
3364
|
+
apiPromises.push(spawnWorker('api', workerSpawnOpts('api', i)));
|
|
3365
|
+
}
|
|
3366
|
+
|
|
3367
|
+
// Tolerate partial API-worker startup: proceed as long as at least one worker reached 'ready'
|
|
3368
|
+
// (the spawnWorker exit handler respawns any that failed). Only abort startup if EVERY API
|
|
3369
|
+
// worker failed, preserving the original single-worker "no API worker => exit" behavior.
|
|
3370
|
+
let apiResults = await Promise.allSettled(apiPromises);
|
|
3371
|
+
let apiReady = apiResults.filter(result => result.status === 'fulfilled').length;
|
|
3372
|
+
let apiFailed = apiResults.length - apiReady;
|
|
3373
|
+
if (apiFailed) {
|
|
3374
|
+
logger.error({
|
|
3375
|
+
msg: 'Some API workers failed to start; they will be respawned',
|
|
3376
|
+
requested: apiWorkersRequested,
|
|
3377
|
+
attempted: apiResults.length,
|
|
3378
|
+
ready: apiReady,
|
|
3379
|
+
failed: apiFailed,
|
|
3380
|
+
errors: apiResults
|
|
3381
|
+
.filter(result => result.status === 'rejected')
|
|
3382
|
+
.map(result => ({
|
|
3383
|
+
message: result.reason && result.reason.message,
|
|
3384
|
+
exitCode: result.reason && result.reason.exitCode,
|
|
3385
|
+
threadId: result.reason && result.reason.threadId
|
|
3386
|
+
}))
|
|
3387
|
+
});
|
|
3388
|
+
}
|
|
3389
|
+
if (!apiReady) {
|
|
3390
|
+
// No API worker came up at all - fatal, same as the original single-worker behavior. This
|
|
3391
|
+
// rejects startApplication(), which logs fatal and exits the process.
|
|
3392
|
+
throw new Error('No API worker thread could be started');
|
|
3393
|
+
}
|
|
3394
|
+
logger.info({ msg: 'API workers started', requested: apiWorkersRequested, ready: apiReady });
|
|
3197
3395
|
|
|
3198
3396
|
// Small delay to allow API to start
|
|
3199
3397
|
await new Promise(r => setTimeout(r, 100));
|
package/static/licenses.html
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<!doctype html><html><head><meta charset="utf-8"><title>EmailEngine Licenses</title><meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/css/bootstrap.min.css" integrity="sha384-zCbKRCUGaJDkqS1kPbPd7TveP5iyJE0EjAuZQTgFLD2ylzuqKfdKlfG/eSrtxUkn" crossorigin="anonymous"></head><body>
|
|
2
2
|
<div class="container-fluid">
|
|
3
|
-
<h1>EmailEngine v2.68.
|
|
3
|
+
<h1>EmailEngine v2.68.1</h1><p>EmailEngine includes code from the following software packages:</p>
|
|
4
4
|
<table class="table table-sm">
|
|
5
5
|
<tr><thead class="thead-dark"><th>Package</th><th>Version</th><th>License</th><th>Publisher</th><th>Publisher's Email</th><th>Package URL</th></tr>
|
|
6
6
|
<tbody>
|
|
@@ -146,7 +146,7 @@
|
|
|
146
146
|
</tr>
|
|
147
147
|
<tr>
|
|
148
148
|
<td><a href="https://npmjs.com/package/@bull-board/api">@bull-board/api</a></td>
|
|
149
|
-
<td>7.1
|
|
149
|
+
<td>7.2.1</td>
|
|
150
150
|
<td>MIT</td>
|
|
151
151
|
<td>felixmosh</td>
|
|
152
152
|
<td></td>
|
|
@@ -156,7 +156,7 @@
|
|
|
156
156
|
</tr>
|
|
157
157
|
<tr>
|
|
158
158
|
<td><a href="https://npmjs.com/package/@bull-board/hapi">@bull-board/hapi</a></td>
|
|
159
|
-
<td>7.1
|
|
159
|
+
<td>7.2.1</td>
|
|
160
160
|
<td>MIT</td>
|
|
161
161
|
<td>felixmosh</td>
|
|
162
162
|
<td></td>
|
|
@@ -166,7 +166,7 @@
|
|
|
166
166
|
</tr>
|
|
167
167
|
<tr>
|
|
168
168
|
<td><a href="https://npmjs.com/package/@bull-board/ui">@bull-board/ui</a></td>
|
|
169
|
-
<td>7.1
|
|
169
|
+
<td>7.2.1</td>
|
|
170
170
|
<td>MIT</td>
|
|
171
171
|
<td>felixmosh</td>
|
|
172
172
|
<td></td>
|
|
@@ -216,7 +216,7 @@
|
|
|
216
216
|
</tr>
|
|
217
217
|
<tr>
|
|
218
218
|
<td><a href="https://npmjs.com/package/@csstools/css-syntax-patches-for-csstree">@csstools/css-syntax-patches-for-csstree</a></td>
|
|
219
|
-
<td>1.1.
|
|
219
|
+
<td>1.1.5</td>
|
|
220
220
|
<td>MIT-0</td>
|
|
221
221
|
<td></td>
|
|
222
222
|
<td></td>
|
|
@@ -1806,7 +1806,7 @@
|
|
|
1806
1806
|
</tr>
|
|
1807
1807
|
<tr>
|
|
1808
1808
|
<td><a href="https://npmjs.com/package/bullmq">bullmq</a></td>
|
|
1809
|
-
<td>5.
|
|
1809
|
+
<td>5.78.0</td>
|
|
1810
1810
|
<td>MIT</td>
|
|
1811
1811
|
<td>Taskforce.sh Inc.</td>
|
|
1812
1812
|
<td></td>
|
|
@@ -2466,7 +2466,7 @@
|
|
|
2466
2466
|
</tr>
|
|
2467
2467
|
<tr>
|
|
2468
2468
|
<td><a href="https://npmjs.com/package/ejs">ejs</a></td>
|
|
2469
|
-
<td>
|
|
2469
|
+
<td>6.0.1</td>
|
|
2470
2470
|
<td>Apache-2.0</td>
|
|
2471
2471
|
<td>Matthew Eernisse</td>
|
|
2472
2472
|
<td>matthew.eernisse@gmail.com</td>
|
|
@@ -3656,7 +3656,7 @@
|
|
|
3656
3656
|
</tr>
|
|
3657
3657
|
<tr>
|
|
3658
3658
|
<td><a href="https://npmjs.com/package/imapflow">imapflow</a></td>
|
|
3659
|
-
<td>1.
|
|
3659
|
+
<td>1.4.0</td>
|
|
3660
3660
|
<td>MIT</td>
|
|
3661
3661
|
<td>Postal Systems OÜ</td>
|
|
3662
3662
|
<td></td>
|
|
@@ -3736,7 +3736,7 @@
|
|
|
3736
3736
|
</tr>
|
|
3737
3737
|
<tr>
|
|
3738
3738
|
<td><a href="https://npmjs.com/package/ioredis">ioredis</a></td>
|
|
3739
|
-
<td>5.11.
|
|
3739
|
+
<td>5.11.1</td>
|
|
3740
3740
|
<td>MIT</td>
|
|
3741
3741
|
<td>Zihua Li</td>
|
|
3742
3742
|
<td>i@zihua.li</td>
|
|
@@ -5146,7 +5146,7 @@
|
|
|
5146
5146
|
</tr>
|
|
5147
5147
|
<tr>
|
|
5148
5148
|
<td><a href="https://npmjs.com/package/prettier">prettier</a></td>
|
|
5149
|
-
<td>3.8.
|
|
5149
|
+
<td>3.8.4</td>
|
|
5150
5150
|
<td>MIT</td>
|
|
5151
5151
|
<td>James Long</td>
|
|
5152
5152
|
<td></td>
|
|
@@ -5696,7 +5696,7 @@
|
|
|
5696
5696
|
</tr>
|
|
5697
5697
|
<tr>
|
|
5698
5698
|
<td><a href="https://npmjs.com/package/side-channel">side-channel</a></td>
|
|
5699
|
-
<td>1.1.
|
|
5699
|
+
<td>1.1.1</td>
|
|
5700
5700
|
<td>MIT</td>
|
|
5701
5701
|
<td>Jordan Harband</td>
|
|
5702
5702
|
<td>ljharb@gmail.com</td>
|
|
@@ -6086,7 +6086,7 @@
|
|
|
6086
6086
|
</tr>
|
|
6087
6087
|
<tr>
|
|
6088
6088
|
<td><a href="https://npmjs.com/package/thread-stream">thread-stream</a></td>
|
|
6089
|
-
<td>3.
|
|
6089
|
+
<td>3.2.0</td>
|
|
6090
6090
|
<td>MIT</td>
|
|
6091
6091
|
<td>Matteo Collina</td>
|
|
6092
6092
|
<td>hello@matteocollina.com</td>
|