emailengine-app 2.69.0 → 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.
Files changed (74) hide show
  1. package/.github/workflows/deploy.yml +6 -3
  2. package/.github/workflows/release.yaml +2 -0
  3. package/CHANGELOG.md +19 -0
  4. package/Gruntfile.js +3 -1
  5. package/data/google-crawlers.json +1 -1
  6. package/getswagger.sh +40 -4
  7. package/gettext-extract.js +163 -0
  8. package/lib/account.js +73 -47
  9. package/lib/api-routes/account-routes.js +231 -71
  10. package/lib/api-routes/blocklist-routes.js +25 -18
  11. package/lib/api-routes/chat-routes.js +32 -14
  12. package/lib/api-routes/delivery-test-routes.js +30 -5
  13. package/lib/api-routes/export-routes.js +27 -2
  14. package/lib/api-routes/gateway-routes.js +63 -12
  15. package/lib/api-routes/license-routes.js +18 -4
  16. package/lib/api-routes/mailbox-routes.js +33 -7
  17. package/lib/api-routes/message-routes.js +200 -58
  18. package/lib/api-routes/oauth2-app-routes.js +90 -24
  19. package/lib/api-routes/outbox-routes.js +16 -4
  20. package/lib/api-routes/pubsub-routes.js +8 -4
  21. package/lib/api-routes/route-helpers.js +14 -1
  22. package/lib/api-routes/settings-routes.js +51 -25
  23. package/lib/api-routes/stats-routes.js +37 -3
  24. package/lib/api-routes/submit-routes.js +31 -42
  25. package/lib/api-routes/template-routes.js +54 -21
  26. package/lib/api-routes/token-routes.js +67 -67
  27. package/lib/api-routes/webhook-route-routes.js +37 -8
  28. package/lib/autodetect-imap-settings.js +0 -2
  29. package/lib/consts.js +5 -0
  30. package/lib/email-client/base-client.js +28 -6
  31. package/lib/email-client/gmail-client.js +119 -112
  32. package/lib/email-client/imap/subconnection.js +0 -1
  33. package/lib/email-client/imap/sync-operations.js +1 -1
  34. package/lib/email-client/imap-client.js +36 -17
  35. package/lib/email-client/notification-handler.js +1 -4
  36. package/lib/email-client/outlook-client.js +49 -62
  37. package/lib/export.js +37 -1
  38. package/lib/feature-flags.js +2 -2
  39. package/lib/gateway.js +4 -9
  40. package/lib/get-raw-email.js +5 -5
  41. package/lib/imapproxy/imap-core/lib/imap-connection.js +0 -1
  42. package/lib/logger.js +24 -21
  43. package/lib/metrics-collector.js +0 -2
  44. package/lib/oauth2-apps.js +13 -4
  45. package/lib/outbox.js +24 -40
  46. package/lib/redis-operations.js +1 -1
  47. package/lib/schemas.js +403 -83
  48. package/lib/sentry.js +139 -0
  49. package/lib/settings.js +9 -3
  50. package/lib/stream-encrypt.js +1 -1
  51. package/lib/templates.js +1 -1
  52. package/lib/tokens.js +5 -3
  53. package/lib/tools.js +2 -4
  54. package/lib/ui-routes/account-routes.js +7 -4
  55. package/lib/ui-routes/admin-config-routes.js +16 -3
  56. package/lib/ui-routes/oauth-config-routes.js +0 -2
  57. package/lib/ui-routes/route-helpers.js +0 -2
  58. package/lib/ui-routes/unsubscribe-routes.js +0 -2
  59. package/lib/webhooks.js +8 -4
  60. package/package.json +9 -8
  61. package/sbom.json +1 -1
  62. package/server.js +8 -23
  63. package/static/licenses.html +152 -292
  64. package/translations/messages.pot +122 -122
  65. package/update-info.sh +19 -1
  66. package/views/config/logging.hbs +48 -0
  67. package/workers/api.js +11 -32
  68. package/workers/documents.js +2 -22
  69. package/workers/export.js +16 -50
  70. package/workers/imap-proxy.js +3 -23
  71. package/workers/imap.js +2 -22
  72. package/workers/smtp.js +2 -22
  73. package/workers/submit.js +6 -24
  74. package/workers/webhooks.js +2 -22
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 (key in ['generateEmailSummary', 'openAiGenerateEmbeddings'] && value) {
159
- // make sure `notifyText` is enabled
160
- return await redis.hset(`${REDIS_PREFIX}settings`, 'notifyText', 'true');
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') {
@@ -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: list[response.tokens.length] }, tokenData);
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: list[response.tokens.length], err });
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: list[i], err });
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')();
@@ -1517,9 +1515,9 @@ MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEV3QUiYsp13nD9suD1/ZkEXnuMoSg
1517
1515
  .hget(`${REDIS_PREFIX}bull:${queue}:meta`, 'paused')
1518
1516
  .exec();
1519
1517
  if (resActive[0] || resDelayed[0] || resWaiting[0] || resPaused[0] || resMeta[0]) {
1520
- // counting failed
1518
+ // counting failed, skip this queue but keep the rest of the stats response
1521
1519
  logger.error({ msg: 'Failed to count queue length', queue, active: resActive, delayed: resDelayed, waiting: resWaiting });
1522
- return false;
1520
+ continue;
1523
1521
  }
1524
1522
  queues[queue] = {
1525
1523
  active: Number(resActive[1]) || 0,
@@ -1,7 +1,5 @@
1
1
  'use strict';
2
2
 
3
- // NB! This file is processed by the gettext parser (npm run gettext) and can not use newer syntax like ?.
4
-
5
3
  // Admin UI routes for account management: /admin/accounts (listing), /admin/accounts/new and
6
4
  // /accounts/new* (the add-account wizard incl. IMAP autoconfig/test/server steps), and
7
5
  // /admin/accounts/{account}* (view, edit, delete, reconnect, sync, logs, browse). Extracted
@@ -33,7 +31,7 @@ const {
33
31
  readEnvValue,
34
32
  failAction
35
33
  } = require('../tools');
36
- const { settingsSchema, accountIdSchema, defaultAccountTypeSchema } = require('../schemas');
34
+ const { settingsSchema, accountIdSchema, defaultAccountTypeSchema, ACCOUNT_DISPLAY_STATES } = require('../schemas');
37
35
  const { formatAccountData, cachedTemplates } = require('./route-helpers');
38
36
 
39
37
  const { REDIS_PREFIX, DEFAULT_PAGE_SIZE, NONCE_BYTES, MAX_FORM_TTL, DEFAULT_MAX_LOG_LINES } = consts;
@@ -179,6 +177,11 @@ function init(args) {
179
177
  label: 'Disconnected'
180
178
  },
181
179
 
180
+ {
181
+ state: 'paused',
182
+ label: 'Paused'
183
+ },
184
+
182
185
  {
183
186
  state: 'authenticationError',
184
187
  label: 'Authentication failed'
@@ -252,7 +255,7 @@ function init(args) {
252
255
  state: Joi.string()
253
256
  .trim()
254
257
  .empty('')
255
- .valid('init', 'syncing', 'connecting', 'connected', 'authenticationError', 'connectError', 'unset', 'disconnected')
258
+ .valid(...ACCOUNT_DISPLAY_STATES)
256
259
  .example('connected')
257
260
  .description('Filter accounts by state')
258
261
  .label('AccountState')
@@ -1,7 +1,5 @@
1
1
  'use strict';
2
2
 
3
- // NB! This file is processed by the gettext parser (npm run gettext) and can not use newer syntax like ?.
4
-
5
3
  // Admin UI routes for the remaining /admin/config/* pages: webhooks, service URL/branding,
6
4
  // AI (OpenAI) settings, logging, and license. Extracted verbatim from lib/routes-ui.js.
7
5
  // The notificationTypes / configWebhooksSchema / configLoggingSchema consts, the
@@ -131,7 +129,9 @@ for (let type of notificationTypes) {
131
129
 
132
130
  const configLoggingSchema = {
133
131
  all: Joi.boolean().truthy('Y', 'true', '1', 'on').falsy('N', 'false', 0, '').default(false).description('Enable logs for all accounts'),
134
- maxLogLines: Joi.number().integer().empty('').min(0).max(10000000).default(DEFAULT_MAX_LOG_LINES)
132
+ maxLogLines: Joi.number().integer().empty('').min(0).max(10000000).default(DEFAULT_MAX_LOG_LINES),
133
+ sentryEnabled: settingsSchema.sentryEnabled.default(false),
134
+ sentryDsn: settingsSchema.sentryDsn.default('')
135
135
  };
136
136
 
137
137
  async function getOpenAiError(gt) {
@@ -1120,6 +1120,10 @@ return true;`
1120
1120
 
1121
1121
  values.accounts = [].concat(values.accounts || []).join('\n');
1122
1122
 
1123
+ let sentryValues = await settings.getMulti('sentryEnabled', 'sentryDsn');
1124
+ values.sentryEnabled = !!sentryValues.sentryEnabled;
1125
+ values.sentryDsn = sentryValues.sentryDsn || '';
1126
+
1123
1127
  return h.view(
1124
1128
  'config/logging',
1125
1129
  {
@@ -1127,6 +1131,8 @@ return true;`
1127
1131
  menuConfig: true,
1128
1132
  menuConfigLogging: true,
1129
1133
 
1134
+ sentryEnvManaged: !!readEnvValue('SENTRY_DSN'),
1135
+
1130
1136
  values
1131
1137
  },
1132
1138
  {
@@ -1148,6 +1154,13 @@ return true;`
1148
1154
  }
1149
1155
  };
1150
1156
 
1157
+ if (!readEnvValue('SENTRY_DSN')) {
1158
+ // the form renders these fields as disabled when the DSN is pinned by the
1159
+ // environment, so do not overwrite the stored values in that case
1160
+ data.sentryEnabled = !!request.payload.sentryEnabled;
1161
+ data.sentryDsn = request.payload.sentryDsn || '';
1162
+ }
1163
+
1151
1164
  for (let key of Object.keys(data)) {
1152
1165
  await settings.set(key, data[key]);
1153
1166
  }
@@ -1,7 +1,5 @@
1
1
  'use strict';
2
2
 
3
- // NB! This file is processed by the gettext parser (npm run gettext) and can not use newer syntax like ?.
4
-
5
3
  // Admin UI routes for OAuth2 application config (/admin/config/oauth*): listing apps,
6
4
  // per-app view/edit/delete, creating apps, adding accounts, provider subscriptions, and
7
5
  // app verification. Extracted verbatim from lib/routes-ui.js. AZURE_CLOUDS and the
@@ -1,7 +1,5 @@
1
1
  'use strict';
2
2
 
3
- // NB! This file is processed by the gettext parser (npm run gettext) and can not use newer syntax like ?.
4
-
5
3
  // Shared helpers used by more than one extracted UI route module - and still by
6
4
  // lib/routes-ui.js for the route groups not yet extracted. Lifting these here lets each
7
5
  // consumer import the single canonical copy instead of the monolith, so a route group can
@@ -1,7 +1,5 @@
1
1
  'use strict';
2
2
 
3
- // NB! This file is processed by the gettext parser (npm run gettext) and can not use newer syntax like ?.
4
-
5
3
  // Public (unauthenticated) subscription-management routes. These render the unsubscribe
6
4
  // landing page reached from the List-Unsubscribe link in outgoing messages and process
7
5
  // the subscribe/unsubscribe form submission. Extracted verbatim from lib/routes-ui.js.
package/lib/webhooks.js CHANGED
@@ -70,12 +70,12 @@ class WebhooksHandler {
70
70
 
71
71
  try {
72
72
  let webhookMeta = msgpack.decode(entry);
73
- if (webhookErrorFlag && typeof webhookErrorFlag === 'object' && !Object.keys(webhookErrorFlag)) {
73
+ if (webhookErrorFlag && typeof webhookErrorFlag === 'object' && !Object.keys(webhookErrorFlag).length) {
74
74
  webhookErrorFlag = null;
75
75
  }
76
76
  response.webhooks.push(Object.assign(webhookMeta, { tcount, webhookErrorFlag }));
77
77
  } catch (err) {
78
- logger.error({ msg: 'Failed to process webhook', entry: entry.toString('base64') });
78
+ logger.error({ msg: 'Failed to process webhook', entry: entry && entry.toString('base64'), err });
79
79
  continue;
80
80
  }
81
81
  }
@@ -398,7 +398,6 @@ class WebhooksHandler {
398
398
  v = Number(v) || 0;
399
399
  if (v !== this.handlerCacheV) {
400
400
  // changes detected
401
- v = this.handlerCacheV;
402
401
 
403
402
  let webhookIds = await this.redis.smembers(this.getWebhooksIndexKey());
404
403
  webhookIds = [].concat(webhookIds || []).sort((a, b) => -a.localeCompare(b));
@@ -418,7 +417,8 @@ class WebhooksHandler {
418
417
  this.handlerCache.push(handler);
419
418
  } else {
420
419
  // compare existing
421
- let webhookV = await this.redis.hget(this.getWebhooksContentKey(), `${webhookId}:v`);
420
+ // the per-route counter is stored as a string, existing.v is a number (see get())
421
+ let webhookV = Number(await this.redis.hget(this.getWebhooksContentKey(), `${webhookId}:v`)) || 0;
422
422
  if (existing.v !== webhookV) {
423
423
  // update
424
424
  for (let i = this.handlerCache.length - 1; i >= 0; i--) {
@@ -430,6 +430,10 @@ class WebhooksHandler {
430
430
  }
431
431
  }
432
432
  }
433
+
434
+ // mark the cache as current only after a successful refresh, so a failure above
435
+ // leaves the cache stale and the next call retries
436
+ this.handlerCacheV = v;
433
437
  }
434
438
 
435
439
  return this.handlerCache;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "emailengine-app",
3
- "version": "2.69.0",
3
+ "version": "2.70.0",
4
4
  "private": false,
5
5
  "productTitle": "EmailEngine",
6
6
  "description": "Email Sync Engine",
@@ -11,13 +11,13 @@
11
11
  "single": "EE_OPENAPI_VERBOSE=true EENGINE_LOG_RAW=true EENGINE_SECRET=your-encryption-key EENGINE_WORKERS=1 node --inspect server --dbs.redis='redis://127.0.0.1:6379/6' --api.port=7003 --api.host=0.0.0.0 | tee $HOME/ee.log.single.txt | pino-pretty",
12
12
  "gmail": "EE_OPENAPI_VERBOSE=true EENGINE_LOG_RAW=true EENGINE_SECRET=your-encryption-key EENGINE_WORKERS=2 EENGINE_CORS_ORIGIN='*' node --inspect server --dbs.redis='redis://127.0.0.1:6379/11' --api.port=7003 --api.host=0.0.0.0 | tee $HOME/ee.log.gmail.txt | pino-pretty",
13
13
  "test": "NODE_ENV=test grunt",
14
- "lint": "npx eslint lib/**/*.js workers/**/*.js test/**/*.js server.js Gruntfile.js",
14
+ "lint": "npx eslint 'lib/**/*.js' 'workers/**/*.js' 'test/**/*.js' server.js Gruntfile.js",
15
15
  "swagger": "./getswagger.sh",
16
16
  "build-source": "rm -rf node_modules && npm install && rm -rf node_modules && npm ci --omit=dev && rm -rf node_modules/ace-builds node_modules/@postalsys/ee-client && ./update-info.sh",
17
17
  "build-dist": "pkg --compress Brotli package.json && npm install && node winconf.js",
18
18
  "build-dist-fast": "pkg --debug package.json && npm install && node winconf.js",
19
19
  "licenses": "license-checker --excludePackages 'emailengine-app' --json | node list-generate.js > static/licenses.html",
20
- "gettext": "find ./views -name \"*.hbs\" -print0 | xargs -0 xgettext-template -L Handlebars -o translations/messages.pot --force-po && jsxgettext --parser-options '{\"ecmaVersion\": 2018}' workers/api.js lib/tools.js lib/autodetect-imap-settings.js lib/ui-routes/admin-entities-routes.js lib/ui-routes/account-routes.js lib/ui-routes/oauth-config-routes.js lib/ui-routes/admin-config-routes.js lib/ui-routes/route-helpers.js lib/ui-routes/unsubscribe-routes.js -j -o translations/messages.pot",
20
+ "gettext": "find ./views -name \"*.hbs\" -print0 | xargs -0 xgettext-template -L Handlebars -o translations/messages.pot --force-po && node gettext-extract.js",
21
21
  "prepare-docker": "echo \"EE_DOCKER_LEGACY=$EE_DOCKER_LEGACY\" >> system.env && cat system.env",
22
22
  "update": "rm -rf node_modules package-lock.json && ncu -u && npm install && ./copy-static-files.sh && npm run licenses && npm run gettext",
23
23
  "test-gmail-api": "node lib/email-client/gmail-client.js --dbs.redis=redis://127.0.0.1/11",
@@ -43,9 +43,8 @@
43
43
  },
44
44
  "homepage": "https://emailengine.app/",
45
45
  "dependencies": {
46
- "@bugsnag/js": "8.9.0",
47
- "@bull-board/api": "7.2.1",
48
- "@bull-board/hapi": "7.2.1",
46
+ "@bull-board/api": "8.0.0",
47
+ "@bull-board/hapi": "8.0.0",
49
48
  "@elastic/elasticsearch": "8.15.3",
50
49
  "@hapi/accept": "6.0.3",
51
50
  "@hapi/bell": "13.1.0",
@@ -64,6 +63,7 @@
64
63
  "@postalsys/gettext": "4.1.1",
65
64
  "@postalsys/joi-messages": "1.0.5",
66
65
  "@postalsys/templates": "2.0.1",
66
+ "@sentry/node": "10.57.0",
67
67
  "@simplewebauthn/browser": "13.3.0",
68
68
  "@simplewebauthn/server": "13.3.1",
69
69
  "@zone-eu/mailsplit": "5.4.12",
@@ -100,7 +100,7 @@
100
100
  "msgpack5": "6.0.2",
101
101
  "murmurhash": "2.0.1",
102
102
  "nanoid": "3.3.8",
103
- "nodemailer": "8.0.10",
103
+ "nodemailer": "8.0.11",
104
104
  "pino": "10.3.1",
105
105
  "popper.js": "1.16.1",
106
106
  "prom-client": "15.1.3",
@@ -118,6 +118,8 @@
118
118
  },
119
119
  "devDependencies": {
120
120
  "@eslint/js": "10.0.1",
121
+ "acorn": "^8.16.0",
122
+ "acorn-walk": "^8.3.5",
121
123
  "chai": "4.3.10",
122
124
  "eerawlog": "1.5.3",
123
125
  "eslint": "10.4.1",
@@ -125,7 +127,6 @@
125
127
  "grunt-cli": "1.5.0",
126
128
  "grunt-shell-spawn": "0.5.0",
127
129
  "grunt-wait": "0.3.0",
128
- "jsxgettext": "0.11.0",
129
130
  "pino-pretty": "13.0.0",
130
131
  "prettier": "3.8.4",
131
132
  "resedit": "3.0.2",