emailengine-app 2.68.1 → 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.
- package/.github/workflows/deploy.yml +8 -3
- package/.github/workflows/release.yaml +6 -0
- package/CHANGELOG.md +59 -0
- package/Gruntfile.js +3 -1
- package/config/default.toml +2 -0
- package/data/google-crawlers.json +7 -1
- package/getswagger.sh +40 -4
- package/gettext-extract.js +163 -0
- package/lib/account.js +135 -72
- package/lib/api-routes/account-routes.js +684 -106
- package/lib/api-routes/blocklist-routes.js +344 -0
- package/lib/api-routes/chat-routes.js +32 -14
- package/lib/api-routes/delivery-test-routes.js +346 -0
- package/lib/api-routes/export-routes.js +28 -14
- package/lib/api-routes/gateway-routes.js +427 -0
- package/lib/api-routes/license-routes.js +156 -0
- package/lib/api-routes/mailbox-routes.js +344 -0
- package/lib/api-routes/message-routes.js +221 -187
- package/lib/api-routes/oauth2-app-routes.js +697 -0
- package/lib/api-routes/outbox-routes.js +185 -0
- package/lib/api-routes/pubsub-routes.js +102 -0
- package/lib/api-routes/route-helpers.js +58 -0
- package/lib/api-routes/settings-routes.js +357 -0
- package/lib/api-routes/stats-routes.js +111 -0
- package/lib/api-routes/submit-routes.js +461 -0
- package/lib/api-routes/template-routes.js +60 -75
- package/lib/api-routes/token-routes.js +297 -0
- package/lib/api-routes/webhook-route-routes.js +181 -0
- package/lib/autodetect-imap-settings.js +0 -2
- package/lib/consts.js +5 -0
- package/lib/email-client/base-client.js +28 -6
- package/lib/email-client/gmail-client.js +133 -112
- package/lib/email-client/imap/mailbox.js +34 -11
- package/lib/email-client/imap/subconnection.js +20 -13
- package/lib/email-client/imap/sync-operations.js +131 -3
- package/lib/email-client/imap-client.js +152 -75
- package/lib/email-client/notification-handler.js +1 -4
- package/lib/email-client/outlook-client.js +134 -75
- package/lib/export.js +97 -20
- package/lib/feature-flags.js +2 -2
- package/lib/gateway.js +4 -9
- package/lib/get-raw-email.js +5 -5
- package/lib/imapproxy/imap-core/lib/commands/starttls.js +18 -0
- package/lib/imapproxy/imap-core/lib/imap-command.js +6 -1
- package/lib/imapproxy/imap-core/lib/imap-connection.js +106 -24
- package/lib/imapproxy/imap-core/lib/imap-server.js +24 -0
- package/lib/imapproxy/imap-core/lib/imap-stream.js +26 -0
- package/lib/logger.js +24 -21
- package/lib/message-port-stream.js +113 -16
- package/lib/metrics-collector.js +0 -2
- package/lib/oauth2-apps.js +13 -4
- package/lib/outbox.js +24 -40
- package/lib/redis-operations.js +1 -1
- package/lib/reject-worker-calls.js +42 -0
- package/lib/routes-ui.js +37 -8778
- package/lib/schemas.js +429 -84
- package/lib/sentry.js +139 -0
- package/lib/settings.js +9 -3
- package/lib/stream-encrypt.js +1 -1
- package/lib/templates.js +1 -1
- package/lib/tokens.js +5 -3
- package/lib/tools.js +70 -4
- package/lib/ui-routes/account-routes.js +45 -212
- package/lib/ui-routes/admin-config-routes.js +928 -489
- package/lib/ui-routes/admin-entities-routes.js +1 -0
- package/lib/ui-routes/auth-routes.js +1339 -0
- package/lib/ui-routes/dashboard-routes.js +188 -0
- package/lib/ui-routes/document-store-routes.js +800 -0
- package/lib/ui-routes/export-routes.js +217 -0
- package/lib/ui-routes/internals-routes.js +354 -0
- package/lib/ui-routes/network-config-routes.js +759 -0
- package/lib/ui-routes/{oauth-routes.js → oauth-config-routes.js} +369 -91
- package/lib/ui-routes/route-helpers.js +314 -0
- package/lib/ui-routes/smtp-test-routes.js +236 -0
- package/lib/ui-routes/unsubscribe-routes.js +232 -0
- package/lib/webhook-request.js +36 -0
- package/lib/webhooks.js +8 -4
- package/package.json +13 -12
- package/sbom.json +1 -1
- package/server.js +222 -39
- package/static/licenses.html +160 -300
- package/translations/messages.pot +112 -132
- package/update-info.sh +19 -1
- package/views/config/logging.hbs +48 -0
- package/views/dashboard.hbs +7 -26
- package/views/internals/index.hbs +15 -0
- package/views/tokens/index.hbs +9 -0
- package/workers/api.js +200 -4424
- package/workers/documents.js +2 -22
- package/workers/export.js +103 -104
- package/workers/imap-proxy.js +3 -23
- package/workers/imap.js +32 -36
- package/workers/smtp.js +2 -22
- package/workers/submit.js +26 -35
- package/workers/webhooks.js +9 -43
|
@@ -16,6 +16,7 @@ jobs:
|
|
|
16
16
|
deploy:
|
|
17
17
|
name: Deploy Demo App
|
|
18
18
|
runs-on: ubuntu-24.04
|
|
19
|
+
timeout-minutes: 15
|
|
19
20
|
|
|
20
21
|
steps:
|
|
21
22
|
- name: Checkout
|
|
@@ -40,7 +41,7 @@ jobs:
|
|
|
40
41
|
id: deploy
|
|
41
42
|
run: |
|
|
42
43
|
echo $GITHUB_SHA > commit.txt
|
|
43
|
-
./update-info.sh
|
|
44
|
+
EE_COMMIT_HASH=$GITHUB_SHA ./update-info.sh
|
|
44
45
|
npm install --omit=dev
|
|
45
46
|
tar czf /tmp/${SERVICE_NAME}.tar.gz --exclude .git .
|
|
46
47
|
scp -o "UserKnownHostsFile=/dev/null" -o "StrictHostKeyChecking=no" /tmp/${SERVICE_NAME}.tar.gz deploy@${TARGET_HOST_02}:
|
|
@@ -54,7 +55,7 @@ jobs:
|
|
|
54
55
|
id: deploy-demo
|
|
55
56
|
run: |
|
|
56
57
|
echo $GITHUB_SHA > commit.txt
|
|
57
|
-
./update-info.sh
|
|
58
|
+
EE_COMMIT_HASH=$GITHUB_SHA ./update-info.sh
|
|
58
59
|
npm install --omit=dev
|
|
59
60
|
tar czf /tmp/${SERVICE_NAME}.tar.gz --exclude .git .
|
|
60
61
|
scp -o "UserKnownHostsFile=/dev/null" -o "StrictHostKeyChecking=no" /tmp/${SERVICE_NAME}.tar.gz deploy@${TARGET_HOST_02}:
|
|
@@ -63,6 +64,7 @@ jobs:
|
|
|
63
64
|
docker:
|
|
64
65
|
name: Build Docker Image
|
|
65
66
|
runs-on: ubuntu-24.04
|
|
67
|
+
timeout-minutes: 30
|
|
66
68
|
permissions:
|
|
67
69
|
contents: read
|
|
68
70
|
packages: write
|
|
@@ -102,8 +104,11 @@ jobs:
|
|
|
102
104
|
images: |
|
|
103
105
|
${{ github.repository }}
|
|
104
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
|
|
105
110
|
tags: |
|
|
106
|
-
type=raw,value=
|
|
111
|
+
type=raw,value=edge,enable=true
|
|
107
112
|
|
|
108
113
|
- name: Build and push
|
|
109
114
|
uses: docker/build-push-action@v7
|
|
@@ -63,6 +63,10 @@ jobs:
|
|
|
63
63
|
runs-on: ubuntu-latest
|
|
64
64
|
needs: release_please
|
|
65
65
|
if: ${{needs.release_please.outputs.release_created}}
|
|
66
|
+
# Multi-arch builds run the arm64 leg under QEMU emulation, which can
|
|
67
|
+
# hang indefinitely on a misbehaving step. A normal build finishes in
|
|
68
|
+
# ~4 minutes, so fail fast instead of burning the 6-hour default limit.
|
|
69
|
+
timeout-minutes: 30
|
|
66
70
|
steps:
|
|
67
71
|
- run: echo version v${{needs.release_please.outputs.major}}.${{needs.release_please.outputs.minor}}.${{needs.release_please.outputs.patch}}
|
|
68
72
|
|
|
@@ -99,9 +103,11 @@ jobs:
|
|
|
99
103
|
platforms: ${{ steps.buildx.outputs.platforms }}
|
|
100
104
|
push: true
|
|
101
105
|
tags: |
|
|
106
|
+
${{ github.repository }}:latest
|
|
102
107
|
${{ github.repository }}:v${{needs.release_please.outputs.major}}.${{needs.release_please.outputs.minor}}.${{needs.release_please.outputs.patch}}
|
|
103
108
|
${{ github.repository }}:v${{needs.release_please.outputs.major}}.${{needs.release_please.outputs.minor}}
|
|
104
109
|
${{ github.repository }}:v${{needs.release_please.outputs.major}}
|
|
110
|
+
ghcr.io/${{ github.repository }}:latest
|
|
105
111
|
ghcr.io/${{ github.repository }}:v${{needs.release_please.outputs.major}}.${{needs.release_please.outputs.minor}}.${{needs.release_please.outputs.patch}}
|
|
106
112
|
ghcr.io/${{ github.repository }}:v${{needs.release_please.outputs.major}}.${{needs.release_please.outputs.minor}}
|
|
107
113
|
ghcr.io/${{ github.repository }}:v${{needs.release_please.outputs.major}}
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,64 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [2.70.0](https://github.com/postalsys/emailengine/compare/v2.69.0...v2.70.0) (2026-06-11)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* allow enabling Sentry error reporting from the admin UI ([a4076a4](https://github.com/postalsys/emailengine/commit/a4076a4fa6658ccc998f9bfe35dddf015cd12b09))
|
|
9
|
+
* replace Bugsnag with self-hosted Sentry for error tracking ([62de831](https://github.com/postalsys/emailengine/commit/62de831a2911da9b649fe693547ba09d70e2e481))
|
|
10
|
+
* tag Sentry error reports with instance id and license key ([2e9683f](https://github.com/postalsys/emailengine/commit/2e9683f27d54ff8fa7ba3f6f4453866fd135f173))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* align API response schemas with actual responses and fix uncovered API bugs ([abacbf6](https://github.com/postalsys/emailengine/commit/abacbf66f048a77d00d180a831a97594380d8ed9))
|
|
16
|
+
* align remaining API response schemas with actual responses ([06136ab](https://github.com/postalsys/emailengine/commit/06136abd20a97c45b25aac7f93653387eb55928a))
|
|
17
|
+
* assign unassigned accounts after license activation ([5677977](https://github.com/postalsys/emailengine/commit/56779778eba27f2d1604ff06f17e5736a5ddee61))
|
|
18
|
+
* 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))
|
|
19
|
+
* report correct nextAttempt time for queued messages ([d5ebdfb](https://github.com/postalsys/emailengine/commit/d5ebdfb029b3ed331a8239ace7426f4f0704ae2e))
|
|
20
|
+
* resolve code review findings in Gmail bulk ops, exports, webhooks, and schemas ([f0ddb46](https://github.com/postalsys/emailengine/commit/f0ddb4631fb924ddb93e0a99d0e20bc9f0cc8ee2))
|
|
21
|
+
|
|
22
|
+
## [2.69.0](https://github.com/postalsys/emailengine/compare/v2.68.1...v2.69.0) (2026-06-09)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
### Features
|
|
26
|
+
|
|
27
|
+
* add label/category filtering to message search ([c171c4e](https://github.com/postalsys/emailengine/commit/c171c4e1efe582f7e532b69108110a1c48aa3ede))
|
|
28
|
+
* detect unsafe Redis eviction config in dashboard warnings ([712d5d6](https://github.com/postalsys/emailengine/commit/712d5d69b99cc45c6f3e123a084ef054236e0110))
|
|
29
|
+
* rebuild lost IMAP sync state without replaying messageNew ([9fe391e](https://github.com/postalsys/emailengine/commit/9fe391e9d6350485aded780709305d698a4c65fb))
|
|
30
|
+
* surface token hash for identification in API, UI, and logs ([8e2a5a9](https://github.com/postalsys/emailengine/commit/8e2a5a9f2bb5194e92575c34c0006a8c4e21888d))
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
### Bug Fixes
|
|
34
|
+
|
|
35
|
+
* avoid 500 on GET /admin/totp without a partial-auth session ([b6f2394](https://github.com/postalsys/emailengine/commit/b6f2394182379d94a05f95c84f5ce04ae6f87a05))
|
|
36
|
+
* bound inbound line length in the IMAP proxy parser ([9fb8a5e](https://github.com/postalsys/emailengine/commit/9fb8a5e301564dee46b48ca32a5b84e4832d96e8))
|
|
37
|
+
* close leaks and a worker crash found in the connection hardening review ([e97950e](https://github.com/postalsys/emailengine/commit/e97950e7e907ac5b5549302b8865c5d2bfb0d6e2))
|
|
38
|
+
* count undecodable export queue entries as skipped ([72e2498](https://github.com/postalsys/emailengine/commit/72e2498390f17ad24d5f7b1577540674d7e1fe09))
|
|
39
|
+
* distinguish SO_REUSEPORT fallback cause in logs and admin UI ([99ad9f3](https://github.com/postalsys/emailengine/commit/99ad9f3359eab5bafc4726535584874d4823f7bd))
|
|
40
|
+
* drain webhook response body on every delivery path ([2cc3e7a](https://github.com/postalsys/emailengine/commit/2cc3e7aef114b5b913dea5c363278e1a883d5a48))
|
|
41
|
+
* extract gettext strings from the decomposed ui-routes modules ([227e60f](https://github.com/postalsys/emailengine/commit/227e60ff4f354c5d0f55534877c8ab6fbdd92c7d))
|
|
42
|
+
* guard cross-thread download streams against early producer errors ([191ec62](https://github.com/postalsys/emailengine/commit/191ec6204887c3abe78da7a9944e3165b19c0b87))
|
|
43
|
+
* guard IMAP notification handlers against malformed events ([0248953](https://github.com/postalsys/emailengine/commit/0248953a5dcccba5d46704bf7113a70fb5ac3150))
|
|
44
|
+
* guard webhook notifications in BullMQ failure handlers ([e90048e](https://github.com/postalsys/emailengine/commit/e90048e7b7bd92916219602f92305342234df4a3))
|
|
45
|
+
* harden IMAP proxy STARTTLS against command injection ([3661ddd](https://github.com/postalsys/emailengine/commit/3661dddbf216b29ec8f074d3f6422dd4db5d9e7b))
|
|
46
|
+
* harden inter-thread IPC (worker-death call rejection, webhook guards, stream cleanup) ([0111a8e](https://github.com/postalsys/emailengine/commit/0111a8e2bf139c3412cc37e5e933fd5316cee8fc))
|
|
47
|
+
* harden message export against data loss and corruption ([540d867](https://github.com/postalsys/emailengine/commit/540d8678ec25f62e8123fc78fa929f4e4713b5d4))
|
|
48
|
+
* harden multiple-API-worker startup and centralize proxy-agent reload ([7f2db9e](https://github.com/postalsys/emailengine/commit/7f2db9e557d960aa1b7523b7d8678598683c2f29))
|
|
49
|
+
* list Outlook folders with slashes in the name ([dd35e7b](https://github.com/postalsys/emailengine/commit/dd35e7b262a47f9395811d75c4bcc1d9ab4c6ec0))
|
|
50
|
+
* only reseed lost IMAP sync state when no stored state exists ([78e0141](https://github.com/postalsys/emailengine/commit/78e0141e9f3478d2a2a1206c805258c6807a45d8))
|
|
51
|
+
* prevent IMAP worker crash on download stream errors ([d9d5396](https://github.com/postalsys/emailengine/commit/d9d539632cc245a8e2a14585fc7918186bee00b6))
|
|
52
|
+
* prevent post-BYE command dispatch in IMAP proxy teardown ([d51e92d](https://github.com/postalsys/emailengine/commit/d51e92d298e9373a6dc5c2eb0919885e161b45c7))
|
|
53
|
+
* re-arm a generous idle timeout for proxied IMAP connections ([627c6d4](https://github.com/postalsys/emailengine/commit/627c6d472e50f6fc3e958e4774226557a526018d))
|
|
54
|
+
* reject in-flight worker calls when a worker thread terminates ([08e7b01](https://github.com/postalsys/emailengine/commit/08e7b0128a4b8e9f9e5a96fefe49110bd032bd2c))
|
|
55
|
+
* release cross-thread download streams on abort and error ([6514249](https://github.com/postalsys/emailengine/commit/65142499d095bbcb5f0bb49755a2e24f6e52fa62))
|
|
56
|
+
* release MessagePort streams when message/attachment downloads fail ([787f4f5](https://github.com/postalsys/emailengine/commit/787f4f5ef6364ec1389c15269c8f7f8eaaa2142a))
|
|
57
|
+
* return 422 for unsupported label search and surface Outlook categories ([d981359](https://github.com/postalsys/emailengine/commit/d981359b78b61d2c8a03e4e35086a4c9674ce53a))
|
|
58
|
+
* return the append destination folder from uploadMessage ([96aed6b](https://github.com/postalsys/emailengine/commit/96aed6b1144e1d3ee9da79b3a1fb03606245f83c))
|
|
59
|
+
* send correctly typed SMTP and IMAP proxy state change notifications ([3fbd27f](https://github.com/postalsys/emailengine/commit/3fbd27fc08bb74f9738a3fc95d4177fa4978c263))
|
|
60
|
+
* tear down subconnections when deleting an IMAP account ([bd81a33](https://github.com/postalsys/emailengine/commit/bd81a33caf9400cbee2781a9180f1a901c1d6e48))
|
|
61
|
+
|
|
3
62
|
## [2.68.1](https://github.com/postalsys/emailengine/compare/v2.68.0...v2.68.1) (2026-06-01)
|
|
4
63
|
|
|
5
64
|
|
package/Gruntfile.js
CHANGED
|
@@ -15,7 +15,9 @@ module.exports = function (grunt) {
|
|
|
15
15
|
|
|
16
16
|
shell: {
|
|
17
17
|
eslint: {
|
|
18
|
-
|
|
18
|
+
// Globs are quoted so eslint expands them itself - unquoted, sh (no globstar)
|
|
19
|
+
// 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",
|
|
19
21
|
options: {
|
|
20
22
|
async: false
|
|
21
23
|
}
|
package/config/default.toml
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"creationTime": "2026-
|
|
2
|
+
"creationTime": "2026-06-10T14:45:48.000000",
|
|
3
3
|
"prefixes": [
|
|
4
4
|
{
|
|
5
5
|
"ipv6Prefix": "2001:4860:4801:2008::/64"
|
|
@@ -310,6 +310,9 @@
|
|
|
310
310
|
{
|
|
311
311
|
"ipv6Prefix": "2001:4860:4801:207e::/64"
|
|
312
312
|
},
|
|
313
|
+
{
|
|
314
|
+
"ipv6Prefix": "2001:4860:4801:207f::/64"
|
|
315
|
+
},
|
|
313
316
|
{
|
|
314
317
|
"ipv6Prefix": "2001:4860:4801:2080::/64"
|
|
315
318
|
},
|
|
@@ -796,6 +799,9 @@
|
|
|
796
799
|
{
|
|
797
800
|
"ipv4Prefix": "74.125.219.192/27"
|
|
798
801
|
},
|
|
802
|
+
{
|
|
803
|
+
"ipv4Prefix": "74.125.219.224/27"
|
|
804
|
+
},
|
|
799
805
|
{
|
|
800
806
|
"ipv4Prefix": "74.125.219.32/27"
|
|
801
807
|
},
|
package/getswagger.sh
CHANGED
|
@@ -1,8 +1,44 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
|
|
3
|
+
set -e
|
|
4
|
+
|
|
3
5
|
export EENGINE_PORT=5678
|
|
4
6
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
# refuse to run if something is already listening on the port, otherwise the
|
|
8
|
+
# polling loop below would silently fetch swagger.json from a stale instance
|
|
9
|
+
if (exec 3<>"/dev/tcp/127.0.0.1/${EENGINE_PORT}") 2>/dev/null; then
|
|
10
|
+
echo "Port ${EENGINE_PORT} is already in use" >&2
|
|
11
|
+
exit 1
|
|
12
|
+
fi
|
|
13
|
+
|
|
14
|
+
node server.js > /dev/null 2>&1 &
|
|
15
|
+
SERVER_PID=$!
|
|
16
|
+
|
|
17
|
+
cleanup() {
|
|
18
|
+
kill "$SERVER_PID" 2>/dev/null || true
|
|
19
|
+
wait "$SERVER_PID" 2>/dev/null || true
|
|
20
|
+
}
|
|
21
|
+
trap cleanup EXIT
|
|
22
|
+
|
|
23
|
+
# poll until the API is up instead of relying on a fixed sleep
|
|
24
|
+
rm -f swagger.json.tmp
|
|
25
|
+
for i in $(seq 1 60); do
|
|
26
|
+
if curl -fs --max-time 5 "http://127.0.0.1:${EENGINE_PORT}/swagger.json" -o swagger.json.tmp; then
|
|
27
|
+
break
|
|
28
|
+
fi
|
|
29
|
+
if ! kill -0 "$SERVER_PID" 2>/dev/null; then
|
|
30
|
+
echo "Server exited before swagger.json could be fetched" >&2
|
|
31
|
+
exit 1
|
|
32
|
+
fi
|
|
33
|
+
sleep 1
|
|
34
|
+
done
|
|
35
|
+
|
|
36
|
+
if [ ! -s swagger.json.tmp ]; then
|
|
37
|
+
echo "Timed out waiting for swagger.json" >&2
|
|
38
|
+
exit 1
|
|
39
|
+
fi
|
|
40
|
+
|
|
41
|
+
# only replace swagger.json if the download parses as JSON
|
|
42
|
+
node -e 'JSON.parse(require("fs").readFileSync("swagger.json.tmp", "utf-8"))'
|
|
43
|
+
|
|
44
|
+
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 };
|