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
@@ -41,7 +41,7 @@ jobs:
41
41
  id: deploy
42
42
  run: |
43
43
  echo $GITHUB_SHA > commit.txt
44
- ./update-info.sh
44
+ EE_COMMIT_HASH=$GITHUB_SHA ./update-info.sh
45
45
  npm install --omit=dev
46
46
  tar czf /tmp/${SERVICE_NAME}.tar.gz --exclude .git .
47
47
  scp -o "UserKnownHostsFile=/dev/null" -o "StrictHostKeyChecking=no" /tmp/${SERVICE_NAME}.tar.gz deploy@${TARGET_HOST_02}:
@@ -55,7 +55,7 @@ jobs:
55
55
  id: deploy-demo
56
56
  run: |
57
57
  echo $GITHUB_SHA > commit.txt
58
- ./update-info.sh
58
+ EE_COMMIT_HASH=$GITHUB_SHA ./update-info.sh
59
59
  npm install --omit=dev
60
60
  tar czf /tmp/${SERVICE_NAME}.tar.gz --exclude .git .
61
61
  scp -o "UserKnownHostsFile=/dev/null" -o "StrictHostKeyChecking=no" /tmp/${SERVICE_NAME}.tar.gz deploy@${TARGET_HOST_02}:
@@ -104,8 +104,11 @@ jobs:
104
104
  images: |
105
105
  ${{ github.repository }}
106
106
  ghcr.io/${{ github.repository }}
107
+ # :latest is owned by the release workflow (release.yaml) and
108
+ # always points at the newest release; per-push dev builds from
109
+ # master are published as :edge instead
107
110
  tags: |
108
- type=raw,value=latest,enable=true
111
+ type=raw,value=edge,enable=true
109
112
 
110
113
  - name: Build and push
111
114
  uses: docker/build-push-action@v7
@@ -103,9 +103,11 @@ jobs:
103
103
  platforms: ${{ steps.buildx.outputs.platforms }}
104
104
  push: true
105
105
  tags: |
106
+ ${{ github.repository }}:latest
106
107
  ${{ github.repository }}:v${{needs.release_please.outputs.major}}.${{needs.release_please.outputs.minor}}.${{needs.release_please.outputs.patch}}
107
108
  ${{ github.repository }}:v${{needs.release_please.outputs.major}}.${{needs.release_please.outputs.minor}}
108
109
  ${{ github.repository }}:v${{needs.release_please.outputs.major}}
110
+ ghcr.io/${{ github.repository }}:latest
109
111
  ghcr.io/${{ github.repository }}:v${{needs.release_please.outputs.major}}.${{needs.release_please.outputs.minor}}.${{needs.release_please.outputs.patch}}
110
112
  ghcr.io/${{ github.repository }}:v${{needs.release_please.outputs.major}}.${{needs.release_please.outputs.minor}}
111
113
  ghcr.io/${{ github.repository }}:v${{needs.release_please.outputs.major}}
@@ -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,42 @@
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
+
21
+ ## [2.70.0](https://github.com/postalsys/emailengine/compare/v2.69.0...v2.70.0) (2026-06-11)
22
+
23
+
24
+ ### Features
25
+
26
+ * allow enabling Sentry error reporting from the admin UI ([a4076a4](https://github.com/postalsys/emailengine/commit/a4076a4fa6658ccc998f9bfe35dddf015cd12b09))
27
+ * replace Bugsnag with self-hosted Sentry for error tracking ([62de831](https://github.com/postalsys/emailengine/commit/62de831a2911da9b649fe693547ba09d70e2e481))
28
+ * tag Sentry error reports with instance id and license key ([2e9683f](https://github.com/postalsys/emailengine/commit/2e9683f27d54ff8fa7ba3f6f4453866fd135f173))
29
+
30
+
31
+ ### Bug Fixes
32
+
33
+ * align API response schemas with actual responses and fix uncovered API bugs ([abacbf6](https://github.com/postalsys/emailengine/commit/abacbf66f048a77d00d180a831a97594380d8ed9))
34
+ * align remaining API response schemas with actual responses ([06136ab](https://github.com/postalsys/emailengine/commit/06136abd20a97c45b25aac7f93653387eb55928a))
35
+ * assign unassigned accounts after license activation ([5677977](https://github.com/postalsys/emailengine/commit/56779778eba27f2d1604ff06f17e5736a5ddee61))
36
+ * do not crash at startup when a feature flag env variable is set to a falsy value ([98ad979](https://github.com/postalsys/emailengine/commit/98ad97988e752fad9e2ae11eff63f35ffcf84976))
37
+ * report correct nextAttempt time for queued messages ([d5ebdfb](https://github.com/postalsys/emailengine/commit/d5ebdfb029b3ed331a8239ace7426f4f0704ae2e))
38
+ * resolve code review findings in Gmail bulk ops, exports, webhooks, and schemas ([f0ddb46](https://github.com/postalsys/emailengine/commit/f0ddb4631fb924ddb93e0a99d0e20bc9f0cc8ee2))
39
+
3
40
  ## [2.69.0](https://github.com/postalsys/emailengine/compare/v2.68.1...v2.69.0) (2026-06-09)
4
41
 
5
42
 
package/Gruntfile.js CHANGED
@@ -5,20 +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
- command: 'npx eslint lib/**/*.js workers/**/*.js server.js Gruntfile.js',
19
- options: {
20
- async: false
21
- }
10
+ // Globs are quoted so eslint expands them itself - unquoted, sh (no globstar)
11
+ // expands lib/**/*.js to depth-2 files only and skips most of lib/
12
+ command: "npx eslint 'lib/**/*.js' 'workers/**/*.js' server.js Gruntfile.js"
22
13
  },
23
14
  server: {
24
15
  command: 'node server.js',
@@ -27,16 +18,22 @@ module.exports = function (grunt) {
27
18
  }
28
19
  },
29
20
  flush: {
30
- command: `redis-cli -u "${config.dbs.redis}" flushdb`,
31
- options: {
32
- async: false
33
- }
21
+ command: `redis-cli -u "${config.dbs.redis}" flushdb`
34
22
  },
35
- test: {
36
- command: 'node --test --test-concurrency=1 --test-timeout=180000 test/*.js', // Added 3-minute timeout for tests
37
- options: {
38
- async: false
39
- }
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'
40
37
  },
41
38
  options: {
42
39
  stdout: data => console.log(data.toString().trim()),
@@ -48,10 +45,11 @@ module.exports = function (grunt) {
48
45
 
49
46
  // Load the plugin(s)
50
47
  grunt.loadNpmTasks('grunt-shell-spawn');
51
- grunt.loadNpmTasks('grunt-wait');
52
48
 
53
49
  // Tasks
54
- 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']);
55
53
 
56
54
  grunt.registerTask('default', ['shell:eslint', 'test']);
57
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-09T14:45:50.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
@@ -1,8 +1,48 @@
1
1
  #!/bin/bash
2
2
 
3
+ set -e
4
+
3
5
  export EENGINE_PORT=5678
4
6
 
5
- npm start > /dev/null 2>&1 &
6
- sleep 5
7
- curl -s "http://127.0.0.1:${EENGINE_PORT}/swagger.json" > swagger.json
8
- pkill emailengine
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
+
11
+ # refuse to run if something is already listening on the port, otherwise the
12
+ # polling loop below would silently fetch swagger.json from a stale instance
13
+ if (exec 3<>"/dev/tcp/127.0.0.1/${EENGINE_PORT}") 2>/dev/null; then
14
+ echo "Port ${EENGINE_PORT} is already in use" >&2
15
+ exit 1
16
+ fi
17
+
18
+ node server.js > /dev/null 2>&1 &
19
+ SERVER_PID=$!
20
+
21
+ cleanup() {
22
+ kill "$SERVER_PID" 2>/dev/null || true
23
+ wait "$SERVER_PID" 2>/dev/null || true
24
+ }
25
+ trap cleanup EXIT
26
+
27
+ # poll until the API is up instead of relying on a fixed sleep
28
+ rm -f swagger.json.tmp
29
+ for i in $(seq 1 60); do
30
+ if curl -fs --max-time 5 "http://127.0.0.1:${EENGINE_PORT}/swagger.json" -o swagger.json.tmp; then
31
+ break
32
+ fi
33
+ if ! kill -0 "$SERVER_PID" 2>/dev/null; then
34
+ echo "Server exited before swagger.json could be fetched" >&2
35
+ exit 1
36
+ fi
37
+ sleep 1
38
+ done
39
+
40
+ if [ ! -s swagger.json.tmp ]; then
41
+ echo "Timed out waiting for swagger.json" >&2
42
+ exit 1
43
+ fi
44
+
45
+ # only replace swagger.json if the download parses as JSON
46
+ node -e 'JSON.parse(require("fs").readFileSync("swagger.json.tmp", "utf-8"))'
47
+
48
+ mv swagger.json.tmp swagger.json
@@ -0,0 +1,163 @@
1
+ 'use strict';
2
+
3
+ // Extracts translatable strings from JS sources into translations/messages.pot.
4
+ // Run via `npm run gettext` after xgettext-template has generated the POT from the
5
+ // Handlebars views - this script joins the strings found in JS files into that file.
6
+ //
7
+ // Replaces jsxgettext, which bundled acorn 5 and crashed on post-ES2018 syntax
8
+ // (optional chaining, nullish coalescing, etc.). Files are discovered dynamically,
9
+ // so new modules with gettext()/ngettext() calls are picked up automatically.
10
+
11
+ const { parse } = require('acorn');
12
+ const { full: walkFull } = require('acorn-walk');
13
+ const { po } = require('gettext-parser');
14
+ const fs = require('fs');
15
+ const Path = require('path');
16
+
17
+ const ROOT_DIR = __dirname;
18
+ const POT_PATH = Path.join(ROOT_DIR, 'translations', 'messages.pot');
19
+
20
+ // All server-side code that may contain gettext()/ngettext() calls
21
+ const SCAN_DIRS = ['bin', 'lib', 'workers'];
22
+ const SCAN_FILES = ['server.js'];
23
+
24
+ function listJsFiles(dir) {
25
+ return fs
26
+ .readdirSync(dir, { recursive: true })
27
+ .filter(entryPath => entryPath.endsWith('.js'))
28
+ .map(entryPath => Path.join(dir, entryPath));
29
+ }
30
+
31
+ // The full sorted scan set (sorted to keep POT reference output deterministic)
32
+ function listScanFiles() {
33
+ let files = SCAN_FILES.map(file => Path.join(ROOT_DIR, file));
34
+ for (let dir of SCAN_DIRS) {
35
+ files = files.concat(listJsFiles(Path.join(ROOT_DIR, dir)));
36
+ }
37
+ return files.sort();
38
+ }
39
+
40
+ function parseFile(filePath) {
41
+ const source = fs.readFileSync(filePath, 'utf-8');
42
+ try {
43
+ return parse(source, { ecmaVersion: 'latest', sourceType: 'script', locations: true, allowHashBang: true });
44
+ } catch (err) {
45
+ err.message = `Failed to parse ${filePath}: ${err.message}`;
46
+ throw err;
47
+ }
48
+ }
49
+
50
+ // Resolves a call argument into a string value. Handles string literals and
51
+ // concatenation of string literals ('foo' + 'bar'). Returns false for anything
52
+ // dynamic (identifiers, template literals with expressions, etc.) - such calls
53
+ // forward runtime values and do not define new translatable strings.
54
+ function resolveString(node) {
55
+ switch (node.type) {
56
+ case 'Literal':
57
+ return typeof node.value === 'string' ? node.value : false;
58
+ case 'TemplateLiteral':
59
+ return node.expressions.length === 0 ? node.quasis[0].value.cooked : false;
60
+ case 'BinaryExpression': {
61
+ if (node.operator !== '+') {
62
+ return false;
63
+ }
64
+ let left = resolveString(node.left);
65
+ let right = resolveString(node.right);
66
+ return left !== false && right !== false ? left + right : false;
67
+ }
68
+ default:
69
+ return false;
70
+ }
71
+ }
72
+
73
+ function calleeName(callee) {
74
+ if (callee.type === 'Identifier') {
75
+ return callee.name;
76
+ }
77
+ if (callee.type === 'MemberExpression' && !callee.computed && callee.property.type === 'Identifier') {
78
+ return callee.property.name;
79
+ }
80
+ return false;
81
+ }
82
+
83
+ function extractFromFile(filePath) {
84
+ const ast = parseFile(filePath);
85
+
86
+ let entries = [];
87
+ let reference = node => `${Path.relative(ROOT_DIR, filePath)}:${node.loc.start.line}`;
88
+
89
+ walkFull(ast, node => {
90
+ if (node.type !== 'CallExpression') {
91
+ return;
92
+ }
93
+
94
+ let name = calleeName(node.callee);
95
+
96
+ // An empty msgid is skipped (like xgettext does): translations[''] is the POT header
97
+ // entry in gettext-parser, so an empty key would corrupt the header block
98
+ if (name === 'gettext' && node.arguments.length >= 1) {
99
+ let msgid = resolveString(node.arguments[0]);
100
+ if (msgid !== false && msgid !== '') {
101
+ entries.push({ msgid, reference: reference(node) });
102
+ }
103
+ }
104
+
105
+ if (name === 'ngettext' && node.arguments.length >= 2) {
106
+ let msgid = resolveString(node.arguments[0]);
107
+ let msgidPlural = resolveString(node.arguments[1]);
108
+ if (msgid !== false && msgid !== '' && msgidPlural !== false) {
109
+ entries.push({ msgid, msgidPlural, reference: reference(node) });
110
+ }
111
+ }
112
+ });
113
+
114
+ return entries;
115
+ }
116
+
117
+ function main() {
118
+ let files = listScanFiles();
119
+
120
+ let extracted = [];
121
+ for (let filePath of files) {
122
+ extracted = extracted.concat(extractFromFile(filePath));
123
+ }
124
+
125
+ let pot = po.parse(fs.readFileSync(POT_PATH));
126
+ let translations = (pot.translations[''] = pot.translations[''] || {});
127
+
128
+ // xgettext-template does not stamp a creation date, so set it here (jsxgettext used to)
129
+ pot.headers = pot.headers || {};
130
+ pot.headers['POT-Creation-Date'] = new Date()
131
+ .toISOString()
132
+ .replace(/T/, ' ')
133
+ .replace(/:\d+\.\d+Z$/, '+0000');
134
+
135
+ for (let { msgid, msgidPlural, reference } of extracted) {
136
+ let entry = translations[msgid];
137
+ if (!entry) {
138
+ entry = translations[msgid] = { msgid, msgstr: [''] };
139
+ }
140
+
141
+ if (msgidPlural && !entry.msgid_plural) {
142
+ entry.msgid_plural = msgidPlural;
143
+ entry.msgstr = ['', ''];
144
+ }
145
+
146
+ entry.comments = entry.comments || {};
147
+ let references = entry.comments.reference ? entry.comments.reference.split('\n') : [];
148
+ if (!references.includes(reference)) {
149
+ references.push(reference);
150
+ }
151
+ entry.comments.reference = references.join('\n');
152
+ }
153
+
154
+ fs.writeFileSync(POT_PATH, po.compile(pot));
155
+ console.log(`Extracted ${extracted.length} gettext strings from ${files.length} JS files into ${Path.relative(ROOT_DIR, POT_PATH)}`);
156
+ }
157
+
158
+ if (require.main === module) {
159
+ main();
160
+ }
161
+
162
+ // Exported for the gettext coverage test, which walks the same scan set with the same helpers
163
+ module.exports = { listScanFiles, parseFile, calleeName, extractFromFile };