emailengine-app 2.69.0 → 2.71.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 (97) hide show
  1. package/.github/workflows/deploy.yml +6 -3
  2. package/.github/workflows/release.yaml +2 -0
  3. package/.github/workflows/test.yml +73 -12
  4. package/.ncurc.js +3 -3
  5. package/CHANGELOG.md +37 -0
  6. package/Gruntfile.js +21 -23
  7. package/bin/emailengine.js +8 -1
  8. package/config/default.toml +5 -0
  9. package/config/test.toml +5 -0
  10. package/data/google-crawlers.json +1 -1
  11. package/getswagger.sh +44 -4
  12. package/gettext-extract.js +163 -0
  13. package/lib/account.js +104 -72
  14. package/lib/api-routes/account-routes.js +231 -71
  15. package/lib/api-routes/blocklist-routes.js +25 -18
  16. package/lib/api-routes/chat-routes.js +32 -14
  17. package/lib/api-routes/delivery-test-routes.js +30 -5
  18. package/lib/api-routes/export-routes.js +27 -2
  19. package/lib/api-routes/gateway-routes.js +63 -12
  20. package/lib/api-routes/license-routes.js +18 -4
  21. package/lib/api-routes/mailbox-routes.js +33 -7
  22. package/lib/api-routes/message-routes.js +291 -145
  23. package/lib/api-routes/oauth2-app-routes.js +90 -24
  24. package/lib/api-routes/outbox-routes.js +16 -4
  25. package/lib/api-routes/pubsub-routes.js +8 -4
  26. package/lib/api-routes/route-helpers.js +14 -1
  27. package/lib/api-routes/settings-routes.js +51 -25
  28. package/lib/api-routes/stats-routes.js +37 -3
  29. package/lib/api-routes/submit-routes.js +31 -42
  30. package/lib/api-routes/template-routes.js +54 -21
  31. package/lib/api-routes/token-routes.js +67 -67
  32. package/lib/api-routes/webhook-route-routes.js +37 -8
  33. package/lib/autodetect-imap-settings.js +0 -2
  34. package/lib/consts.js +5 -0
  35. package/lib/document-store.js +22 -1
  36. package/lib/email-client/base-client.js +31 -8
  37. package/lib/email-client/gmail-client.js +119 -112
  38. package/lib/email-client/imap/mailbox.js +2 -2
  39. package/lib/email-client/imap/subconnection.js +0 -1
  40. package/lib/email-client/imap/sync-operations.js +1 -1
  41. package/lib/email-client/imap-client.js +36 -17
  42. package/lib/email-client/notification-handler.js +3 -6
  43. package/lib/email-client/outlook-client.js +49 -62
  44. package/lib/export.js +49 -1
  45. package/lib/feature-flags.js +8 -2
  46. package/lib/gateway.js +4 -9
  47. package/lib/get-raw-email.js +5 -5
  48. package/lib/imapproxy/imap-core/lib/imap-connection.js +0 -1
  49. package/lib/license-beacon.js +367 -0
  50. package/lib/logger.js +35 -22
  51. package/lib/metrics-collector.js +0 -2
  52. package/lib/oauth2-apps.js +13 -4
  53. package/lib/outbox.js +24 -40
  54. package/lib/redis-operations.js +1 -1
  55. package/lib/routes-ui.js +2 -1
  56. package/lib/schemas.js +403 -83
  57. package/lib/sentry.js +139 -0
  58. package/lib/settings.js +9 -3
  59. package/lib/stream-encrypt.js +1 -1
  60. package/lib/templates.js +1 -1
  61. package/lib/tokens.js +5 -3
  62. package/lib/tools.js +28 -6
  63. package/lib/ui-routes/account-routes.js +7 -4
  64. package/lib/ui-routes/admin-config-routes.js +20 -6
  65. package/lib/ui-routes/document-store-routes.js +7 -1
  66. package/lib/ui-routes/oauth-config-routes.js +0 -2
  67. package/lib/ui-routes/route-helpers.js +0 -2
  68. package/lib/ui-routes/unsubscribe-routes.js +0 -2
  69. package/lib/webhooks.js +8 -4
  70. package/package.json +23 -19
  71. package/sbom.json +1 -1
  72. package/server.js +38 -31
  73. package/static/licenses.html +171 -391
  74. package/translations/de.mo +0 -0
  75. package/translations/de.po +154 -142
  76. package/translations/et.mo +0 -0
  77. package/translations/et.po +129 -131
  78. package/translations/fr.mo +0 -0
  79. package/translations/fr.po +133 -136
  80. package/translations/ja.mo +0 -0
  81. package/translations/ja.po +126 -129
  82. package/translations/messages.pot +107 -107
  83. package/translations/nl.mo +0 -0
  84. package/translations/nl.po +128 -130
  85. package/translations/pl.mo +0 -0
  86. package/translations/pl.po +125 -128
  87. package/update-info.sh +19 -1
  88. package/views/config/logging.hbs +48 -0
  89. package/views/dashboard.hbs +22 -0
  90. package/workers/api.js +33 -37
  91. package/workers/documents.js +2 -22
  92. package/workers/export.js +73 -92
  93. package/workers/imap-proxy.js +3 -23
  94. package/workers/imap.js +2 -22
  95. package/workers/smtp.js +2 -22
  96. package/workers/submit.js +6 -24
  97. package/workers/webhooks.js +2 -22
package/lib/account.js CHANGED
@@ -20,6 +20,7 @@ const { deepStrictEqual, strictEqual } = require('assert');
20
20
  const { encrypt, decrypt } = require('./encrypt');
21
21
  const { oauth2Apps, LEGACY_KEYS, isApiBasedApp } = require('./oauth2-apps');
22
22
  const settings = require('./settings');
23
+ const { isDocumentStoreEnabled, documentStoreFeatureEnabled } = require('./document-store');
23
24
  const redisScanDelete = require('./redis-scan-delete');
24
25
  const { customAlphabet } = require('nanoid');
25
26
  const Lock = require('ioredfour');
@@ -945,7 +946,7 @@ class Account {
945
946
  throw error;
946
947
  }
947
948
 
948
- let state = false;
949
+ let state;
949
950
  if (result[0][1] && result[0][1].account) {
950
951
  // existing user
951
952
  state = 'existing';
@@ -1048,28 +1049,33 @@ class Account {
1048
1049
  };
1049
1050
  }
1050
1051
 
1051
- try {
1052
- let queueKeep = (await settings.get('queueKeep')) || true;
1053
- let serviceUrl = (await settings.get('serviceUrl')) || null;
1052
+ // Only notify the documents queue when the deprecated Document Store feature is enabled.
1053
+ // When it is off the documents worker is not running, so an enqueued job would never be
1054
+ // consumed and would pile up in Redis.
1055
+ if (documentStoreFeatureEnabled) {
1056
+ try {
1057
+ let queueKeep = (await settings.get('queueKeep')) || true;
1058
+ let serviceUrl = (await settings.get('serviceUrl')) || null;
1054
1059
 
1055
- let payload = {
1056
- serviceUrl,
1057
- account: this.account,
1058
- date: new Date().toISOString(),
1059
- event: ACCOUNT_DELETED_NOTIFY
1060
- };
1060
+ let payload = {
1061
+ serviceUrl,
1062
+ account: this.account,
1063
+ date: new Date().toISOString(),
1064
+ event: ACCOUNT_DELETED_NOTIFY
1065
+ };
1061
1066
 
1062
- await this.documentsQueue.add(ACCOUNT_DELETED_NOTIFY, payload, {
1063
- removeOnComplete: queueKeep,
1064
- removeOnFail: queueKeep,
1065
- attempts: 10,
1066
- backoff: {
1067
- type: 'exponential',
1068
- delay: 5000
1069
- }
1070
- });
1071
- } catch (err) {
1072
- this.logger.error({ msg: 'Failed to add entry to documents queue', err });
1067
+ await this.documentsQueue.add(ACCOUNT_DELETED_NOTIFY, payload, {
1068
+ removeOnComplete: queueKeep,
1069
+ removeOnFail: queueKeep,
1070
+ attempts: 10,
1071
+ backoff: {
1072
+ type: 'exponential',
1073
+ delay: 5000
1074
+ }
1075
+ });
1076
+ } catch (err) {
1077
+ this.logger.error({ msg: 'Failed to add entry to documents queue', err });
1078
+ }
1073
1079
  }
1074
1080
 
1075
1081
  await this.call({
@@ -1267,16 +1273,7 @@ class Account {
1267
1273
  } catch (err) {
1268
1274
  // should not happen
1269
1275
  if (logger.notifyError) {
1270
- logger.notifyError(err, event => {
1271
- if (this.account) {
1272
- event.setUser(this.account);
1273
- }
1274
-
1275
- event.addMetadata('ee', {
1276
- path,
1277
- mailboxListing: typeof mailboxListing
1278
- });
1279
- });
1276
+ logger.notifyError(err, { user: this.account, meta: { path, mailboxListing: typeof mailboxListing } });
1280
1277
  }
1281
1278
 
1282
1279
  let message = 'Failed to process stored mailbox listing';
@@ -1290,22 +1287,44 @@ class Account {
1290
1287
  return mailboxes;
1291
1288
  }
1292
1289
 
1290
+ // Worker backends return false for entities they can not find (e.g. an unknown mailbox path
1291
+ // or message ID on IMAP). Convert such results into a 404 error for the API.
1292
+ assertFound(result, message, code) {
1293
+ if (!result) {
1294
+ let error = Boom.boomify(new Error(message), { statusCode: 404 });
1295
+ error.output.payload.code = code;
1296
+ throw error;
1297
+ }
1298
+ return result;
1299
+ }
1300
+
1301
+ assertMessageFound(result) {
1302
+ return this.assertFound(result, 'Requested message was not found', 'MessageNotFound');
1303
+ }
1304
+
1305
+ assertFolderFound(result) {
1306
+ return this.assertFound(result, 'Requested mailbox folder was not found', 'FolderNotFound');
1307
+ }
1308
+
1293
1309
  async updateMessage(message, updates) {
1294
1310
  await this.loadAccountData(this.account, true);
1295
1311
 
1296
- return await this.call({
1312
+ let result = await this.call({
1297
1313
  cmd: 'updateMessage',
1298
1314
  account: this.account,
1299
1315
  message,
1300
1316
  updates,
1301
1317
  timeout: this.timeout
1302
1318
  });
1319
+
1320
+ // IMAP backend returns false for unknown messages and folders
1321
+ return this.assertMessageFound(result);
1303
1322
  }
1304
1323
 
1305
1324
  async updateMessages(path, search, updates) {
1306
1325
  await this.loadAccountData(this.account, true);
1307
1326
 
1308
- return await this.call({
1327
+ let result = await this.call({
1309
1328
  cmd: 'updateMessages',
1310
1329
  account: this.account,
1311
1330
  path,
@@ -1313,6 +1332,9 @@ class Account {
1313
1332
  updates,
1314
1333
  timeout: this.timeout
1315
1334
  });
1335
+
1336
+ // IMAP backend returns false for unknown folders
1337
+ return this.assertFolderFound(result);
1316
1338
  }
1317
1339
 
1318
1340
  async listMailboxes(query) {
@@ -1335,7 +1357,8 @@ class Account {
1335
1357
 
1336
1358
  async moveMessage(message, target, options) {
1337
1359
  await this.loadAccountData(this.account, true);
1338
- return await this.call({
1360
+
1361
+ let result = await this.call({
1339
1362
  cmd: 'moveMessage',
1340
1363
  account: this.account,
1341
1364
  message,
@@ -1343,11 +1366,15 @@ class Account {
1343
1366
  options,
1344
1367
  timeout: this.timeout
1345
1368
  });
1369
+
1370
+ // IMAP backend returns false for unknown messages and folders
1371
+ return this.assertMessageFound(result);
1346
1372
  }
1347
1373
 
1348
1374
  async moveMessages(source, search, target) {
1349
1375
  await this.loadAccountData(this.account, true);
1350
- return await this.call({
1376
+
1377
+ let result = await this.call({
1351
1378
  cmd: 'moveMessages',
1352
1379
  account: this.account,
1353
1380
  source,
@@ -1355,24 +1382,30 @@ class Account {
1355
1382
  target,
1356
1383
  timeout: this.timeout
1357
1384
  });
1385
+
1386
+ // IMAP backend returns false for unknown folders
1387
+ return this.assertFolderFound(result);
1358
1388
  }
1359
1389
 
1360
1390
  async deleteMessage(message, force) {
1361
1391
  await this.loadAccountData(this.account, true);
1362
1392
 
1363
- return await this.call({
1393
+ let result = await this.call({
1364
1394
  cmd: 'deleteMessage',
1365
1395
  account: this.account,
1366
1396
  message,
1367
1397
  force,
1368
1398
  timeout: this.timeout
1369
1399
  });
1400
+
1401
+ // IMAP backend returns false for unknown messages and folders
1402
+ return this.assertMessageFound(result);
1370
1403
  }
1371
1404
 
1372
1405
  async deleteMessages(path, search, force) {
1373
1406
  await this.loadAccountData(this.account, true);
1374
1407
 
1375
- return await this.call({
1408
+ let result = await this.call({
1376
1409
  cmd: 'deleteMessages',
1377
1410
  account: this.account,
1378
1411
  path,
@@ -1380,6 +1413,9 @@ class Account {
1380
1413
  force,
1381
1414
  timeout: this.timeout
1382
1415
  });
1416
+
1417
+ // IMAP backend returns false for unknown folders
1418
+ return this.assertFolderFound(result);
1383
1419
  }
1384
1420
 
1385
1421
  async getQuota() {
@@ -1428,7 +1464,7 @@ class Account {
1428
1464
  }
1429
1465
 
1430
1466
  async getText(text, options) {
1431
- if (options.documentStore && (await settings.get('documentStoreEnabled'))) {
1467
+ if (options.documentStore && (await isDocumentStoreEnabled())) {
1432
1468
  await this.loadAccountData(this.account, false);
1433
1469
 
1434
1470
  const { index, client } = this.esClient;
@@ -1447,13 +1483,7 @@ class Account {
1447
1483
  results: getResult && getResult._source ? 1 : 0
1448
1484
  });
1449
1485
 
1450
- if (!getResult || !getResult._source) {
1451
- let message = 'Requested message was not found';
1452
- let error = Boom.boomify(new Error(message), { statusCode: 404 });
1453
- throw error;
1454
- }
1455
-
1456
- let messageData = getResult._source;
1486
+ let messageData = this.assertMessageFound(getResult && getResult._source);
1457
1487
  let response = {};
1458
1488
 
1459
1489
  response.hasMore = false;
@@ -1473,13 +1503,16 @@ class Account {
1473
1503
 
1474
1504
  await this.loadAccountData(this.account, true);
1475
1505
 
1476
- return await this.call({
1506
+ let textData = await this.call({
1477
1507
  cmd: 'getText',
1478
1508
  account: this.account,
1479
1509
  text,
1480
1510
  options,
1481
1511
  timeout: this.timeout
1482
1512
  });
1513
+
1514
+ // IMAP backend returns false for unknown messages and folders
1515
+ return this.assertMessageFound(textData);
1483
1516
  }
1484
1517
 
1485
1518
  async getMessage(message, options) {
@@ -1489,7 +1522,7 @@ class Account {
1489
1522
  options.preProcessHtml = true;
1490
1523
  }
1491
1524
 
1492
- if (options.documentStore && (await settings.get('documentStoreEnabled'))) {
1525
+ if (options.documentStore && (await isDocumentStoreEnabled())) {
1493
1526
  await this.loadAccountData(this.account, false);
1494
1527
 
1495
1528
  const { index, client } = this.esClient;
@@ -1521,13 +1554,7 @@ class Account {
1521
1554
  results: getResult && getResult._source ? 1 : 0
1522
1555
  });
1523
1556
 
1524
- if (!getResult || !getResult._source) {
1525
- let message = 'Requested message was not found';
1526
- let error = Boom.boomify(new Error(message), { statusCode: 404 });
1527
- throw error;
1528
- }
1529
-
1530
- let messageData = getResult._source;
1557
+ let messageData = this.assertMessageFound(getResult && getResult._source);
1531
1558
 
1532
1559
  // restore headers and text object as per the API response
1533
1560
  let headersObj = {};
@@ -1632,6 +1659,9 @@ class Account {
1632
1659
 
1633
1660
  if (options.markAsSeen && (!messageData.flags || !messageData.flags.includes('\\Seen'))) {
1634
1661
  // mark message as seen
1662
+ if (!messageData.flags) {
1663
+ messageData.flags = [];
1664
+ }
1635
1665
  messageData.flags.push('\\Seen');
1636
1666
  // do not wait until the update is completed, return immediately
1637
1667
  this.updateMessage(message, { flags: { add: ['\\Seen'] } }).catch(err => {
@@ -1661,13 +1691,7 @@ class Account {
1661
1691
  timeout: this.timeout
1662
1692
  });
1663
1693
 
1664
- if (!messageData) {
1665
- let message = 'Requested message was not found';
1666
- let error = Boom.boomify(new Error(message), { statusCode: 404 });
1667
- throw error;
1668
- }
1669
-
1670
- return messageData;
1694
+ return this.assertMessageFound(messageData);
1671
1695
  }
1672
1696
 
1673
1697
  async getMessages(messageIds, options) {
@@ -1682,7 +1706,7 @@ class Account {
1682
1706
  }
1683
1707
 
1684
1708
  async listMessages(query) {
1685
- if (query.documentStore && (await settings.get('documentStoreEnabled'))) {
1709
+ if (query.documentStore && (await isDocumentStoreEnabled())) {
1686
1710
  await this.loadAccountData(this.account, false);
1687
1711
 
1688
1712
  const { index, client } = this.esClient;
@@ -1801,7 +1825,7 @@ class Account {
1801
1825
 
1802
1826
  await this.loadAccountData(this.account, true);
1803
1827
 
1804
- return await this.call(
1828
+ let listing = await this.call(
1805
1829
  Object.assign(
1806
1830
  {
1807
1831
  cmd: 'listMessages',
@@ -1811,11 +1835,14 @@ class Account {
1811
1835
  { timeout: this.timeout }
1812
1836
  )
1813
1837
  );
1838
+
1839
+ // IMAP and Gmail backends return false for unknown folders
1840
+ return this.assertFolderFound(listing);
1814
1841
  }
1815
1842
 
1816
1843
  async searchMessages(query, searchOpts) {
1817
1844
  searchOpts = searchOpts || {};
1818
- if (query.documentStore && (await settings.get('documentStoreEnabled'))) {
1845
+ if (query.documentStore && (await isDocumentStoreEnabled())) {
1819
1846
  if (!searchOpts.unified) {
1820
1847
  await this.loadAccountData(this.account, false);
1821
1848
  }
@@ -2086,11 +2113,11 @@ class Account {
2086
2113
  let sizeMatch = {};
2087
2114
 
2088
2115
  if (query.search.larger) {
2089
- dateMatch.gte = query.search.larger;
2116
+ sizeMatch.gte = query.search.larger;
2090
2117
  }
2091
2118
 
2092
2119
  if (query.search.smaller) {
2093
- dateMatch.lte = query.search.smaller;
2120
+ sizeMatch.lte = query.search.smaller;
2094
2121
  }
2095
2122
 
2096
2123
  if (Object.keys(sizeMatch).length) {
@@ -2198,7 +2225,7 @@ class Account {
2198
2225
 
2199
2226
  await this.loadAccountData(this.account, true);
2200
2227
 
2201
- return await this.call(
2228
+ let listing = await this.call(
2202
2229
  Object.assign(
2203
2230
  {
2204
2231
  cmd: 'listMessages',
@@ -2208,6 +2235,9 @@ class Account {
2208
2235
  { timeout: this.timeout }
2209
2236
  )
2210
2237
  );
2238
+
2239
+ // IMAP and Gmail backends return false for unknown folders
2240
+ return this.assertFolderFound(listing);
2211
2241
  }
2212
2242
 
2213
2243
  async uploadMessage(data) {
@@ -2322,7 +2352,7 @@ class Account {
2322
2352
  }
2323
2353
  } catch (err) {
2324
2354
  this.logger.error({ msg: 'Failed to get lock', lockKey, err });
2325
- if (Boom.isBoom) {
2355
+ if (Boom.isBoom(err)) {
2326
2356
  throw err;
2327
2357
  }
2328
2358
  let error = Boom.boomify(new Error('Failed to get flush lock, try again later'), { statusCode: 500 });
@@ -2377,11 +2407,13 @@ class Account {
2377
2407
  // scan and delete keys
2378
2408
  await redisScanDelete(this.redis, this.logger, `${REDIS_PREFIX}iam:${this.account}:*`);
2379
2409
 
2380
- if (await settings.get('documentStoreEnabled')) {
2410
+ if (await isDocumentStoreEnabled()) {
2381
2411
  // Flush ElasticSearch index for this account
2382
2412
  const { index, client } = this.esClient;
2383
2413
  if (!client) {
2384
- return;
2414
+ // Account data in Redis was already flushed, only the index cleanup was skipped
2415
+ this.logger.error({ msg: 'Document store is enabled but the ElasticSearch client is not available', action: 'flush' });
2416
+ return true;
2385
2417
  }
2386
2418
 
2387
2419
  let deleteResult = {};
@@ -2592,7 +2624,7 @@ class Account {
2592
2624
  account: accountData.account,
2593
2625
  user: authData.user,
2594
2626
  accessToken: authData.accessToken,
2595
- provider: accountData.oauth2.auth.provider,
2627
+ provider: accountData.oauth2.provider,
2596
2628
  registeredScopes: accountData.oauth2.scope,
2597
2629
  cached: false
2598
2630
  };
@@ -2626,7 +2658,7 @@ class Account {
2626
2658
  account: accountData.account,
2627
2659
  user: accountData.oauth2.auth.user,
2628
2660
  accessToken,
2629
- provider: accountData.oauth2.auth.provider,
2661
+ provider: accountData.oauth2.provider,
2630
2662
  registeredScopes: accountData.oauth2.scope,
2631
2663
  expires:
2632
2664
  accountData.oauth2.expires && typeof accountData.oauth2.expires.toISOString === 'function'