emailengine-app 2.70.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 (43) hide show
  1. package/.github/workflows/test.yml +73 -12
  2. package/.ncurc.js +3 -3
  3. package/CHANGELOG.md +18 -0
  4. package/Gruntfile.js +19 -23
  5. package/bin/emailengine.js +8 -1
  6. package/config/default.toml +5 -0
  7. package/config/test.toml +5 -0
  8. package/data/google-crawlers.json +1 -1
  9. package/getswagger.sh +4 -0
  10. package/lib/account.js +31 -25
  11. package/lib/api-routes/message-routes.js +125 -121
  12. package/lib/document-store.js +22 -1
  13. package/lib/email-client/base-client.js +3 -2
  14. package/lib/email-client/imap/mailbox.js +2 -2
  15. package/lib/email-client/notification-handler.js +2 -2
  16. package/lib/export.js +12 -0
  17. package/lib/feature-flags.js +6 -0
  18. package/lib/license-beacon.js +367 -0
  19. package/lib/logger.js +11 -1
  20. package/lib/routes-ui.js +2 -1
  21. package/lib/tools.js +26 -2
  22. package/lib/ui-routes/admin-config-routes.js +4 -3
  23. package/lib/ui-routes/document-store-routes.js +7 -1
  24. package/package.json +19 -16
  25. package/sbom.json +1 -1
  26. package/server.js +30 -8
  27. package/static/licenses.html +43 -123
  28. package/translations/de.mo +0 -0
  29. package/translations/de.po +154 -142
  30. package/translations/et.mo +0 -0
  31. package/translations/et.po +129 -131
  32. package/translations/fr.mo +0 -0
  33. package/translations/fr.po +133 -136
  34. package/translations/ja.mo +0 -0
  35. package/translations/ja.po +126 -129
  36. package/translations/messages.pot +37 -37
  37. package/translations/nl.mo +0 -0
  38. package/translations/nl.po +128 -130
  39. package/translations/pl.mo +0 -0
  40. package/translations/pl.po +125 -128
  41. package/views/dashboard.hbs +22 -0
  42. package/workers/api.js +22 -5
  43. package/workers/export.js +58 -43
@@ -23,20 +23,80 @@ jobs:
23
23
  uses: actions/setup-node@v6
24
24
  with:
25
25
  node-version: 24
26
+ cache: npm
26
27
  - run: npm install
27
28
 
28
29
  - name: Run License Checks
29
30
  run: |
30
31
  npm run licenses
31
32
 
32
- test:
33
- name: Test Suite
34
- timeout-minutes: 15 # Increased timeout for Gmail API tests
35
- strategy:
36
- matrix:
37
- node: [24.x]
38
- os: [ubuntu-24.04]
39
- runs-on: ${{ matrix.os }}
33
+ lint:
34
+ name: Lint
35
+ runs-on: ubuntu-24.04
36
+ timeout-minutes: 10
37
+ steps:
38
+ - uses: actions/checkout@v6
39
+ - name: Use Node.js 24
40
+ uses: actions/setup-node@v6
41
+ with:
42
+ node-version: 24
43
+ cache: npm
44
+ - run: npm install
45
+ - name: Run ESLint
46
+ run: |
47
+ npm run lint
48
+
49
+ unit:
50
+ name: Unit Tests
51
+ timeout-minutes: 10
52
+ runs-on: ubuntu-24.04
53
+ # Service containers to run with `container-job`
54
+ services:
55
+ # Label used to access the service container
56
+ redis:
57
+ # Docker Hub image
58
+ image: redis
59
+ # Set health checks to wait until redis has started
60
+ options: >-
61
+ --health-cmd "redis-cli ping"
62
+ --health-interval 10s
63
+ --health-timeout 5s
64
+ --health-retries 5
65
+ ports:
66
+ - 6379:6379
67
+ steps:
68
+ - uses: actions/checkout@v6
69
+ - name: Use Node.js 24
70
+ uses: actions/setup-node@v6
71
+ with:
72
+ node-version: 24
73
+ cache: npm
74
+ - name: Setup Redis CLI
75
+ uses: shogo82148/actions-setup-redis@v1
76
+ with:
77
+ redis-version: '7.x'
78
+ auto-start: 'false'
79
+ - run: npm install
80
+ - name: Run unit tests
81
+ run: |
82
+ npm run test:unit
83
+ env:
84
+ NODE_ENV: test
85
+ # account-revoke-on-delete-test.js runs against live Gmail when
86
+ # these are present and skips itself when they are not
87
+ GMAIL_API_PROJECT_ID: ${{ secrets.TEST_GMAIL_API_PROJECT_ID }}
88
+ GMAIL_API_CLIENT_ID: ${{ secrets.TEST_GMAIL_API_CLIENT_ID }}
89
+ GMAIL_API_CLIENT_SECRET: ${{ secrets.TEST_GMAIL_API_CLIENT_SECRET }}
90
+ GMAIL_API_SERVICE_EMAIL: ${{ secrets.TEST_GMAIL_API_SERVICE_EMAIL }}
91
+ GMAIL_API_SERVICE_CLIENT: ${{ secrets.TEST_GMAIL_API_SERVICE_CLIENT }}
92
+ GMAIL_API_SERVICE_KEY: ${{ secrets.TEST_GMAIL_API_SERVICE_KEY }}
93
+ GMAIL_API_ACCOUNT_EMAIL_1: ${{ secrets.TEST_GMAIL_API_ACCOUNT_EMAIL_1 }}
94
+ GMAIL_API_ACCOUNT_REFRESH_1: ${{ secrets.TEST_GMAIL_API_ACCOUNT_REFRESH_1 }}
95
+
96
+ integration:
97
+ name: Integration Tests
98
+ timeout-minutes: 15 # Increased timeout for Gmail API tests
99
+ runs-on: ubuntu-24.04
40
100
  # Service containers to run with `container-job`
41
101
  services:
42
102
  # Label used to access the service container
@@ -53,19 +113,20 @@ jobs:
53
113
  - 6379:6379
54
114
  steps:
55
115
  - uses: actions/checkout@v6
56
- - name: Use Node.js ${{ matrix.node }}
116
+ - name: Use Node.js 24
57
117
  uses: actions/setup-node@v6
58
118
  with:
59
- node-version: ${{ matrix.node }}
119
+ node-version: 24
120
+ cache: npm
60
121
  - name: Setup Redis CLI
61
122
  uses: shogo82148/actions-setup-redis@v1
62
123
  with:
63
124
  redis-version: '7.x'
64
125
  auto-start: 'false'
65
126
  - run: npm install
66
- - name: Run tests
127
+ - name: Run integration tests
67
128
  run: |
68
- npm test
129
+ npm run test:integration
69
130
  env:
70
131
  NODE_ENV: test
71
132
  GMAIL_API_PROJECT_ID: ${{ secrets.TEST_GMAIL_API_PROJECT_ID }}
package/.ncurc.js CHANGED
@@ -1,5 +1,8 @@
1
1
  module.exports = {
2
2
  upgrade: true,
3
+ // Keep joi within the 17.x major (hapi-swagger's peer dependency requires joi 17.x).
4
+ // Using a target function instead of a blanket reject so joi still receives 17.x security patches.
5
+ target: name => (name === 'joi' ? 'minor' : 'latest'),
3
6
  reject: [
4
7
  // Block package upgrades that moved to ESM
5
8
  'nanoid',
@@ -18,9 +21,6 @@ module.exports = {
18
21
  // some kind of CVE in later versions. Only needed for license reference, so the actual version does not matter anyway
19
22
  'startbootstrap-sb-admin-2',
20
23
 
21
- // Keep joi at version 17.x for hapi-swagger compatibility
22
- 'joi',
23
-
24
24
  // @asamuzakjp/css-color >=4.1.2 pulls in @csstools/* v4 which are pure ESM and break pkg bundling
25
25
  '@asamuzakjp/css-color',
26
26
 
package/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.71.0](https://github.com/postalsys/emailengine/compare/v2.70.0...v2.71.0) (2026-06-15)
4
+
5
+
6
+ ### Features
7
+
8
+ * add anonymized feature beacon to license validation ([954d92a](https://github.com/postalsys/emailengine/commit/954d92a9d8e7c5fff6f1b8c711dfd228f556387a))
9
+ * disable deprecated Document Store by default behind a feature gate ([8790f75](https://github.com/postalsys/emailengine/commit/8790f7538ba2ee8d6ec73d1f3bbc221c2f54f0fe))
10
+
11
+
12
+ ### Bug Fixes
13
+
14
+ * defer export worker job consumption until startup recovery completes ([a21b0a5](https://github.com/postalsys/emailengine/commit/a21b0a50fdb98d13a2227a020069129d23fe225b))
15
+ * fail export when a folder cannot be indexed ([36c9c9d](https://github.com/postalsys/emailengine/commit/36c9c9d536540cd5a61b16b69af5e8e84c194ee6))
16
+ * filter transient fetch failures from Sentry and retry DNS errors ([883b9b4](https://github.com/postalsys/emailengine/commit/883b9b487e3580c5b1240d28cd519d190c402b47))
17
+ * retry transient errors in API-account batch export path ([6cba7c7](https://github.com/postalsys/emailengine/commit/6cba7c72658a037f68867cf2464f7d3420a79507))
18
+ * translate OAuth scope error page across 6 languages ([4ae1919](https://github.com/postalsys/emailengine/commit/4ae19199fe628f8a4f7177d53db1f05167077e3e))
19
+ * upgrade joi to 17.13.4 and @postalsys/certs to 1.0.15 (GHSA-q7cg-457f-vx79) ([6ec77e5](https://github.com/postalsys/emailengine/commit/6ec77e50b288b14550cd09b724f5aab59d17ced4))
20
+
3
21
  ## [2.70.0](https://github.com/postalsys/emailengine/compare/v2.69.0...v2.70.0) (2026-06-11)
4
22
 
5
23
 
package/Gruntfile.js CHANGED
@@ -5,22 +5,11 @@ const config = require('@zone-eu/wild-config');
5
5
  module.exports = function (grunt) {
6
6
  // Project configuration.
7
7
  grunt.initConfig({
8
- wait: {
9
- server: {
10
- options: {
11
- delay: 20 * 1000 // Increased from 12s to 20s for Gmail API operations
12
- }
13
- }
14
- },
15
-
16
8
  shell: {
17
9
  eslint: {
18
10
  // Globs are quoted so eslint expands them itself - unquoted, sh (no globstar)
19
11
  // expands lib/**/*.js to depth-2 files only and skips most of lib/
20
- command: "npx eslint 'lib/**/*.js' 'workers/**/*.js' server.js Gruntfile.js",
21
- options: {
22
- async: false
23
- }
12
+ command: "npx eslint 'lib/**/*.js' 'workers/**/*.js' server.js Gruntfile.js"
24
13
  },
25
14
  server: {
26
15
  command: 'node server.js',
@@ -29,16 +18,22 @@ module.exports = function (grunt) {
29
18
  }
30
19
  },
31
20
  flush: {
32
- command: `redis-cli -u "${config.dbs.redis}" flushdb`,
33
- options: {
34
- async: false
35
- }
21
+ command: `redis-cli -u "${config.dbs.redis}" flushdb`
36
22
  },
37
- test: {
38
- command: 'node --test --test-concurrency=1 --test-timeout=180000 test/*.js', // Added 3-minute timeout for tests
39
- options: {
40
- async: false
41
- }
23
+ waitServer: {
24
+ // Polls /health until the server reports ready (all IMAP workers up,
25
+ // Redis responding) instead of sleeping for a fixed delay
26
+ command: 'node test/helpers/wait-for-server.js'
27
+ },
28
+ testUnit: {
29
+ // Self-contained tests - need Redis but not the live server.
30
+ // Run with default --test concurrency; the suite is verified to pass in parallel.
31
+ // The *-test.js pattern keeps helper modules out of the test runner
32
+ command: 'node --test --test-timeout=180000 test/*-test.js'
33
+ },
34
+ testIntegration: {
35
+ // Tests that run against the live server started by shell:server
36
+ command: 'node --test --test-concurrency=1 --test-timeout=180000 test/integration/*-test.js'
42
37
  },
43
38
  options: {
44
39
  stdout: data => console.log(data.toString().trim()),
@@ -50,10 +45,11 @@ module.exports = function (grunt) {
50
45
 
51
46
  // Load the plugin(s)
52
47
  grunt.loadNpmTasks('grunt-shell-spawn');
53
- grunt.loadNpmTasks('grunt-wait');
54
48
 
55
49
  // Tasks
56
- grunt.registerTask('test', ['shell:flush', 'shell:server', 'wait:server', 'shell:test', 'shell:server:kill']);
50
+ grunt.registerTask('test-unit', ['shell:flush', 'shell:testUnit']);
51
+ grunt.registerTask('test-integration', ['shell:flush', 'shell:server', 'shell:waitServer', 'shell:testIntegration', 'shell:server:kill']);
52
+ grunt.registerTask('test', ['test-unit', 'test-integration']);
57
53
 
58
54
  grunt.registerTask('default', ['shell:eslint', 'test']);
59
55
  };
@@ -105,7 +105,14 @@ const GLOBAL_OPTIONS = [
105
105
  { name: '--smtp.host', description: 'SMTP server bind address', type: 'string', default: '127.0.0.1', group: 'SMTP server' },
106
106
  { name: '--smtp.port', description: 'SMTP server port', type: 'number', default: 2525, group: 'SMTP server' },
107
107
  { name: '--smtp.proxy', description: 'Enable HAProxy PROXY protocol', type: 'boolean', default: false, group: 'SMTP server' },
108
- { name: '--smtp.maxMessageSize', description: 'Maximum email size', type: 'number/string', default: '25M', group: 'SMTP server' }
108
+ { name: '--smtp.maxMessageSize', description: 'Maximum email size', type: 'number/string', default: '25M', group: 'SMTP server' },
109
+ {
110
+ name: '--documentStore.enabled',
111
+ description: 'Enable the deprecated Document Store (ElasticSearch) feature',
112
+ type: 'boolean',
113
+ default: false,
114
+ group: 'Document Store (deprecated)'
115
+ }
109
116
  ];
110
117
 
111
118
  // Help formatting functions
@@ -31,6 +31,11 @@ secret = "" # client password, if not set allows any password
31
31
  proxy = false # Set to true if using HAProxy with send-proxy option
32
32
  #maxMessageSize = "25M" # maximum message size accepted by SMTP server (default: 25MB)
33
33
 
34
+ [documentStore]
35
+ # Deprecated Document Store (ElasticSearch) feature. Disabled by default; the worker and
36
+ # all document-store API/UI endpoints are only available when this is enabled.
37
+ enabled = false
38
+
34
39
  [dbs]
35
40
  # redis connection
36
41
  redis = "redis://127.0.0.1:6379/8"
package/config/test.toml CHANGED
@@ -43,3 +43,8 @@ port = 7077
43
43
 
44
44
  [webhooksServer]
45
45
  port = 7078
46
+
47
+ # Keep the deprecated Document Store feature available during tests so the existing
48
+ # document-store API/UI endpoints stay covered. Disabled by default in production.
49
+ [documentStore]
50
+ enabled = true
@@ -1,5 +1,5 @@
1
1
  {
2
- "creationTime": "2026-06-10T14:45:48.000000",
2
+ "creationTime": "2026-06-15T14:45:58.000000",
3
3
  "prefixes": [
4
4
  {
5
5
  "ipv6Prefix": "2001:4860:4801:2008::/64"
package/getswagger.sh CHANGED
@@ -4,6 +4,10 @@ set -e
4
4
 
5
5
  export EENGINE_PORT=5678
6
6
 
7
+ # Keep the deprecated Document Store endpoints in the generated spec; they are only
8
+ # registered when the feature is enabled.
9
+ export EENGINE_DOCUMENT_STORE_ENABLED=true
10
+
7
11
  # refuse to run if something is already listening on the port, otherwise the
8
12
  # polling loop below would silently fetch swagger.json from a stale instance
9
13
  if (exec 3<>"/dev/tcp/127.0.0.1/${EENGINE_PORT}") 2>/dev/null; then
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');
@@ -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({
@@ -1458,7 +1464,7 @@ class Account {
1458
1464
  }
1459
1465
 
1460
1466
  async getText(text, options) {
1461
- if (options.documentStore && (await settings.get('documentStoreEnabled'))) {
1467
+ if (options.documentStore && (await isDocumentStoreEnabled())) {
1462
1468
  await this.loadAccountData(this.account, false);
1463
1469
 
1464
1470
  const { index, client } = this.esClient;
@@ -1516,7 +1522,7 @@ class Account {
1516
1522
  options.preProcessHtml = true;
1517
1523
  }
1518
1524
 
1519
- if (options.documentStore && (await settings.get('documentStoreEnabled'))) {
1525
+ if (options.documentStore && (await isDocumentStoreEnabled())) {
1520
1526
  await this.loadAccountData(this.account, false);
1521
1527
 
1522
1528
  const { index, client } = this.esClient;
@@ -1700,7 +1706,7 @@ class Account {
1700
1706
  }
1701
1707
 
1702
1708
  async listMessages(query) {
1703
- if (query.documentStore && (await settings.get('documentStoreEnabled'))) {
1709
+ if (query.documentStore && (await isDocumentStoreEnabled())) {
1704
1710
  await this.loadAccountData(this.account, false);
1705
1711
 
1706
1712
  const { index, client } = this.esClient;
@@ -1836,7 +1842,7 @@ class Account {
1836
1842
 
1837
1843
  async searchMessages(query, searchOpts) {
1838
1844
  searchOpts = searchOpts || {};
1839
- if (query.documentStore && (await settings.get('documentStoreEnabled'))) {
1845
+ if (query.documentStore && (await isDocumentStoreEnabled())) {
1840
1846
  if (!searchOpts.unified) {
1841
1847
  await this.loadAccountData(this.account, false);
1842
1848
  }
@@ -2401,7 +2407,7 @@ class Account {
2401
2407
  // scan and delete keys
2402
2408
  await redisScanDelete(this.redis, this.logger, `${REDIS_PREFIX}iam:${this.account}:*`);
2403
2409
 
2404
- if (await settings.get('documentStoreEnabled')) {
2410
+ if (await isDocumentStoreEnabled()) {
2405
2411
  // Flush ElasticSearch index for this account
2406
2412
  const { index, client } = this.esClient;
2407
2413
  if (!client) {