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.
- package/.github/workflows/test.yml +73 -12
- package/.ncurc.js +3 -3
- package/CHANGELOG.md +18 -0
- package/Gruntfile.js +19 -23
- package/bin/emailengine.js +8 -1
- package/config/default.toml +5 -0
- package/config/test.toml +5 -0
- package/data/google-crawlers.json +1 -1
- package/getswagger.sh +4 -0
- package/lib/account.js +31 -25
- package/lib/api-routes/message-routes.js +125 -121
- package/lib/document-store.js +22 -1
- package/lib/email-client/base-client.js +3 -2
- package/lib/email-client/imap/mailbox.js +2 -2
- package/lib/email-client/notification-handler.js +2 -2
- package/lib/export.js +12 -0
- package/lib/feature-flags.js +6 -0
- package/lib/license-beacon.js +367 -0
- package/lib/logger.js +11 -1
- package/lib/routes-ui.js +2 -1
- package/lib/tools.js +26 -2
- package/lib/ui-routes/admin-config-routes.js +4 -3
- package/lib/ui-routes/document-store-routes.js +7 -1
- package/package.json +19 -16
- package/sbom.json +1 -1
- package/server.js +30 -8
- package/static/licenses.html +43 -123
- package/translations/de.mo +0 -0
- package/translations/de.po +154 -142
- package/translations/et.mo +0 -0
- package/translations/et.po +129 -131
- package/translations/fr.mo +0 -0
- package/translations/fr.po +133 -136
- package/translations/ja.mo +0 -0
- package/translations/ja.po +126 -129
- package/translations/messages.pot +37 -37
- package/translations/nl.mo +0 -0
- package/translations/nl.po +128 -130
- package/translations/pl.mo +0 -0
- package/translations/pl.po +125 -128
- package/views/dashboard.hbs +22 -0
- package/workers/api.js +22 -5
- 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
|
-
|
|
33
|
-
name:
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
|
116
|
+
- name: Use Node.js 24
|
|
57
117
|
uses: actions/setup-node@v6
|
|
58
118
|
with:
|
|
59
|
-
node-version:
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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:
|
|
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
|
};
|
package/bin/emailengine.js
CHANGED
|
@@ -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
|
package/config/default.toml
CHANGED
|
@@ -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
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
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
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
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1060
|
+
let payload = {
|
|
1061
|
+
serviceUrl,
|
|
1062
|
+
account: this.account,
|
|
1063
|
+
date: new Date().toISOString(),
|
|
1064
|
+
event: ACCOUNT_DELETED_NOTIFY
|
|
1065
|
+
};
|
|
1061
1066
|
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
2410
|
+
if (await isDocumentStoreEnabled()) {
|
|
2405
2411
|
// Flush ElasticSearch index for this account
|
|
2406
2412
|
const { index, client } = this.esClient;
|
|
2407
2413
|
if (!client) {
|