From abb5847186ed5627795c5b51b2a1a41956df1b4b Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Mon, 10 Aug 2026 16:03:15 +0200 Subject: [PATCH 01/14] rename allowed emails to valid email domains --- examples/defaults.env | 2 +- src/routes/data.js | 4 ++-- src/utils/config.js | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/defaults.env b/examples/defaults.env index 4a84cbc..d02f606 100644 --- a/examples/defaults.env +++ b/examples/defaults.env @@ -11,4 +11,4 @@ PREFIX=/ # Supports glob-like patterns separated by a comma. # (See https://github.com/micromatch/micromatch#matching-features) # Example: domain.com,example.*,*.com -ALLOWED_EMAIL_DOMAINS=* \ No newline at end of file +VALID_EMAIL_DOMAINS=* \ No newline at end of file diff --git a/src/routes/data.js b/src/routes/data.js index f7dec0e..d27c947 100644 --- a/src/routes/data.js +++ b/src/routes/data.js @@ -26,7 +26,7 @@ function IsValideEmail(email) { function IsEmailAllowed(email) { const [, domain] = email.split("@") - return micromatch.isMatch(domain, config.ALLOWED_EMAIL_DOMAINS) + return micromatch.isMatch(domain, config.VALID_EMAIL_DOMAINS) } function ValidateEmail(email) { @@ -44,7 +44,7 @@ function ValidateEmail(email) { function EmailAllowed(email) { const [, domain] = email.split("@") - if (!micromatch.isMatch(domain, config.ALLOWED_EMAIL_DOMAINS)) { + if (!micromatch.isMatch(domain, config.VALID_EMAIL_DOMAINS)) { throw new HttpError(400, "Email domain not allowed") } } diff --git a/src/utils/config.js b/src/utils/config.js index 39f7995..3d647fd 100644 --- a/src/utils/config.js +++ b/src/utils/config.js @@ -61,7 +61,7 @@ define("APP_CLIENT_ID", { required: true }) define("APP_CLIENT_SECRET", { required: true }) define("APP_SCOPE", { default: "openid profile email" }) -define("ALLOWED_EMAIL_DOMAINS", { type: "array", default: ["*"] }) +define("VALID_EMAIL_DOMAINS", { type: "array", default: ["*"] }) // Mail From a8ea9581a382abbfc8ab53c974bda25b620a3ea3 Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Mon, 10 Aug 2026 17:35:10 +0200 Subject: [PATCH 02/14] migrated to esm --- examples/defaults.env | 3 + package-lock.json | 315 ++++++++++++++++++++++++++--- package.json | 13 +- src/app.js | 38 ++-- src/docker.js | 14 +- src/router.js | 29 ++- src/routes/data.js | 45 ++--- src/routes/{routes.js => index.js} | 13 +- src/routes/oauth-application.js | 16 +- src/routes/oauth-mail.js | 93 +++++---- src/server.js | 16 +- src/services/index.js | 4 + src/services/mailboxes.js | 20 ++ src/services/users.js | 0 src/types/errors.js | 2 +- src/utils/config.js | 104 +++++++++- src/utils/db.js | 69 +++---- src/utils/logger.js | 93 +++++---- src/utils/token.js | 26 +-- 19 files changed, 638 insertions(+), 275 deletions(-) rename src/routes/{routes.js => index.js} (56%) create mode 100644 src/services/index.js create mode 100644 src/services/mailboxes.js create mode 100644 src/services/users.js diff --git a/examples/defaults.env b/examples/defaults.env index 004a5ed..c2f75e3 100644 --- a/examples/defaults.env +++ b/examples/defaults.env @@ -14,3 +14,6 @@ PREFIX=/ # (See https://github.com/micromatch/micromatch#matching-features) # Example: domain.com,example.*,*.com VALID_EMAIL_DOMAINS=* + +# Allow non-admins to create mailboxes +ALLOW_USER_MAILBOX_CREATION=true \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 92478ac..9aedfb4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,8 @@ "mongo": "^0.1.0", "passport": "^0.7.0", "passport-openidconnect": "^0.1.2", + "pino": "^10.3.1", + "pino-pretty": "^13.1.3", "redis": "^5.1.1", "tldts": "^7.0.8" } @@ -32,6 +34,12 @@ "sparse-bitfield": "^3.0.3" } }, + "node_modules/@pinojs/redact": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", + "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==", + "license": "MIT" + }, "node_modules/@redis/bloom": { "version": "5.12.1", "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-5.12.1.tgz", @@ -156,6 +164,15 @@ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "license": "MIT" }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/axios": { "version": "1.19.0", "resolved": "https://registry.npmjs.org/axios/-/axios-1.19.0.tgz", @@ -168,6 +185,12 @@ "proxy-from-env": "^2.1.0" } }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, "node_modules/body-parser": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", @@ -205,6 +228,15 @@ "url": "https://opencollective.com/express" } }, + "node_modules/brace-expansion": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", + "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, "node_modules/braces": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", @@ -279,6 +311,12 @@ "node": ">=0.10.0" } }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "license": "MIT" + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -344,6 +382,15 @@ "node": ">=6.6.0" } }, + "node_modules/dateformat": { + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz", + "integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -432,6 +479,15 @@ "node": ">= 0.8" } }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -579,6 +635,18 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, + "node_modules/fast-copy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-4.0.4.tgz", + "integrity": "sha512-eVAiWVNPSEGIzDl5yPuLrx8fNMogScXvD9xp1Kzd41FjRIz2I3sSIcxsFeM5EzFfHAfobdvs8ZySffUopljvIA==", + "license": "MIT" + }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "license": "MIT" + }, "node_modules/filelist": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.6.tgz", @@ -588,33 +656,6 @@ "minimatch": "^5.0.1" } }, - "node_modules/filelist/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "license": "MIT" - }, - "node_modules/filelist/node_modules/brace-expansion": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", - "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/filelist/node_modules/minimatch": { - "version": "5.1.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", - "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -820,6 +861,12 @@ "node": ">= 0.4" } }, + "node_modules/help-me": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz", + "integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==", + "license": "MIT" + }, "node_modules/http-errors": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", @@ -916,6 +963,15 @@ "node": ">=10" } }, + "node_modules/joycon": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", + "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/jsonwebtoken": { "version": "9.0.3", "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", @@ -1079,6 +1135,27 @@ "url": "https://opencollective.com/express" } }, + "node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/mongo": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/mongo/-/mongo-0.1.0.tgz", @@ -1182,6 +1259,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -1297,6 +1383,83 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pino": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/pino/-/pino-10.3.1.tgz", + "integrity": "sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==", + "license": "MIT", + "dependencies": { + "@pinojs/redact": "^0.4.0", + "atomic-sleep": "^1.0.0", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^3.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^4.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-3.0.0.tgz", + "integrity": "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==", + "license": "MIT", + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/pino-pretty": { + "version": "13.1.3", + "resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-13.1.3.tgz", + "integrity": "sha512-ttXRkkOz6WWC95KeY9+xxWL6AtImwbyMHrL1mSwqwW9u+vLp/WIElvHvCSDg0xO/Dzrggz1zv3rN5ovTRVowKg==", + "license": "MIT", + "dependencies": { + "colorette": "^2.0.7", + "dateformat": "^4.6.3", + "fast-copy": "^4.0.0", + "fast-safe-stringify": "^2.1.1", + "help-me": "^5.0.0", + "joycon": "^3.1.1", + "minimist": "^1.2.6", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^3.0.0", + "pump": "^3.0.0", + "secure-json-parse": "^4.0.0", + "sonic-boom": "^4.0.1", + "strip-json-comments": "^5.0.2" + }, + "bin": { + "pino-pretty": "bin.js" + } + }, + "node_modules/pino-std-serializers": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz", + "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==", + "license": "MIT" + }, + "node_modules/process-warning": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.1.0.tgz", + "integrity": "sha512-jQSaVHsPgtyw60e1rQ/A+/ArPEj/S8pS/vFnyGa/gYFXrKk/6RuDkoqVDQ5NI5MmS01698ltlAk0NoDBNLujRw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -1319,6 +1482,16 @@ "node": ">=10" } }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -1344,6 +1517,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", + "license": "MIT" + }, "node_modules/random-bytes": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", @@ -1381,6 +1560,15 @@ "node": ">= 0.10" } }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, "node_modules/redis": { "version": "5.12.1", "resolved": "https://registry.npmjs.org/redis/-/redis-5.12.1.tgz", @@ -1433,12 +1621,37 @@ ], "license": "MIT" }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/secure-json-parse": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-4.1.0.tgz", + "integrity": "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/semver": { "version": "7.8.5", "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", @@ -1574,6 +1787,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/sonic-boom": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", + "integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, "node_modules/sparse-bitfield": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", @@ -1583,6 +1805,15 @@ "memory-pager": "^1.0.2" } }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, "node_modules/statuses": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", @@ -1592,6 +1823,36 @@ "node": ">= 0.8" } }, + "node_modules/strip-json-comments": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-5.0.3.tgz", + "integrity": "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==", + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/thread-stream": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.2.0.tgz", + "integrity": "sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ==", + "license": "MIT", + "dependencies": { + "real-require": "^1.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/thread-stream/node_modules/real-require": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-1.0.0.tgz", + "integrity": "sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g==", + "license": "MIT" + }, "node_modules/tldts": { "version": "7.4.10", "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.10.tgz", diff --git a/package.json b/package.json index 8781b6f..655fd25 100644 --- a/package.json +++ b/package.json @@ -12,12 +12,21 @@ }, "license": "MIT", "author": "codeshelldev", - "type": "commonjs", + "type": "module", "main": "src/server.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "start": "node ./src/server.js" }, + "imports": { + "#services/*": "./src/services/*.js", + "#services": "./src/services/index.js", + "#utils/*": "./src/utils/*.js", + "#types/*": "./src/types/*.js", + "#router": "./src/router.js", + "#routes/*": "./src/routes/*.js", + "#routes": "./src/routes/index.js" + }, "dependencies": { "axios": "^1.9.0", "connect-redis": "^10.0.0", @@ -29,6 +38,8 @@ "mongo": "^0.1.0", "passport": "^0.7.0", "passport-openidconnect": "^0.1.2", + "pino": "^10.3.1", + "pino-pretty": "^13.1.3", "redis": "^5.1.1", "tldts": "^7.0.8" } diff --git a/src/app.js b/src/app.js index e5850c9..b68afbd 100644 --- a/src/app.js +++ b/src/app.js @@ -1,28 +1,28 @@ -const express = require("express") -const session = require("express-session") -const passport = require("passport") -const { RedisStore } = require("connect-redis") -const { HttpError } = require("./types/errors") +import express, { static as staticServe, urlencoded, json } from "express" +import session from "express-session" +import passport from "passport" +import { RedisStore } from "connect-redis" +import { HttpError } from "#types/errors" -const { GetRedis } = require("./utils/db") +import { GetRedis } from "#utils/db" -const logger = require("./utils/logger") -const config = require("./utils/config") +import logger from "#utils/logger" +import config from "#utils/config" -const { router: rootRouter } = require("./router") +import { router as rootRouter } from "#router" -const routes = require("./routes/routes") -const dataRoutes = require("./routes/data") -const oauthMailRoutes = require("./routes/oauth-mail") -const oauthApplicationRoutes = require("./routes/oauth-application") +import routes from "#routes" +import dataRoutes from "#routes/data" +import oauthMailRoutes from "#routes/oauth-mail" +import oauthApplicationRoutes from "#routes/oauth-application" function CreateApp() { const app = express() - app.use(config.PREFIX, express.static("public")) + app.use(config.PREFIX, staticServe("public")) - app.use(express.urlencoded({ extended: true })) - app.use(express.json()) + app.use(urlencoded({ extended: true })) + app.use(json()) app.set("view engine", "ejs") @@ -48,7 +48,7 @@ function CreateApp() { app.use(passport.session()) app.use((req, res, next) => { - logger.log(`New ${req.method} Request from ${req.ip} [${req.path}]`) + logger.info(`New ${req.method} Request from ${req.ip} [${req.path}]`) next() }) @@ -87,7 +87,7 @@ function CreateApp() { return res.status(err.status).json({ error: err.message }) } - logger.err(err) + logger.error("", err) res.status(500).json({ error: "Internal server error" }) }) @@ -95,4 +95,4 @@ function CreateApp() { return app } -module.exports = CreateApp +export default CreateApp diff --git a/src/docker.js b/src/docker.js index 1948e9d..d26b5e5 100644 --- a/src/docker.js +++ b/src/docker.js @@ -1,19 +1,19 @@ -const logger = require("./utils/logger") -const db = require("./utils/db") +import logger from "#utils/logger" +import { Close } from "#utils/db" -module.exports = () => { +export default () => { process.on("SIGTERM", async () => { - logger.log("Received SIGTERM, shutting down gracefully...") + logger.info("Received SIGTERM, shutting down gracefully...") - await db.Close() + await Close() process.exit(0) }) process.on("SIGINT", async () => { - logger.log("Received SIGINT, shutting down gracefully...") + logger.info("Received SIGINT, shutting down gracefully...") - await db.Close() + await Close() process.exit(0) }) diff --git a/src/router.js b/src/router.js index 1fa4870..b1092f9 100644 --- a/src/router.js +++ b/src/router.js @@ -1,20 +1,22 @@ -const express = require("express") -const router = express.Router() -const { HttpError } = require("./types/errors") -const db = require("./utils/db") +import { Router } from "express" + +import { HttpError } from "#types/errors" +import { GetUserByID } from "#utils/db" + +const router = Router() // App OIDC: full trust, user logged into mailbaux directly -function GetAppUserID(req) { +export function GetAppUserID(req) { return req.isAuthenticated?.() ? req.user?.id : null } // Mail OIDC: id from a validated id_token during the mailcow -> mailbaux -> authentik flow // Only allowed to read/select existing mailboxes -function GetMailFlowUserID(req) { +export function GetMailFlowUserID(req) { return req.session?.mail?.id || null } -function RequireAppAuth(req, res, next) { +export function RequireAppAuth(req, res, next) { if (!res.locals.context.isApp) { throw new HttpError(401, "App authentication required") } @@ -26,7 +28,7 @@ function RequireAppAuth(req, res, next) { next() } -function RequireMailAuth(req, res, next) { +export function RequireMailAuth(req, res, next) { if (!res.locals.context.isMail) { throw new HttpError(401, "Mail authentication required") } @@ -38,7 +40,7 @@ function RequireMailAuth(req, res, next) { next() } -function RequireMailOrAppAuth(req, res, next) { +export function RequireMailOrAppAuth(req, res, next) { if (!res.locals.context.isMail && !res.locals.context.isApp) { throw new HttpError(401, "Mail or App authentication required") } @@ -64,15 +66,10 @@ router.use(async (req, res, next) => { if (id) { res.locals.id = id - res.locals.user = await db.GetUserByID(id) + res.locals.user = await GetUserByID(id) } next() }) -module.exports.router = router -module.exports.GetAppUserID = GetAppUserID -module.exports.GetMailFlowUserID = GetMailFlowUserID -module.exports.RequireAppAuth = RequireAppAuth -module.exports.RequireMailAuth = RequireMailAuth -module.exports.RequireMailOrAppAuth = RequireMailOrAppAuth +export { router } diff --git a/src/routes/data.js b/src/routes/data.js index d27c947..f27b5b8 100644 --- a/src/routes/data.js +++ b/src/routes/data.js @@ -1,17 +1,16 @@ -const express = require("express") -const router = express.Router() -const { HttpError } = require("../types/errors") -const micromatch = require("micromatch") +import { Router } from "express" +import { HttpError } from "#types/errors" +import micromatch from "micromatch" -const logger = require("../utils/logger") -const config = require("../utils/config") -const db = require("../utils/db") +import logger from "#utils/logger" +import config from "#utils/config" +import { FindBy, UpdateBy, DeleteFromArrayBy, AddToArray } from "#utils/db" -const { - RequireAppAuth, - RequireMailAuth, - RequireMailOrAppAuth, -} = require("../router") +import services from "#services" + +import { RequireAppAuth, RequireMailAuth, RequireMailOrAppAuth } from "#router" + +const router = Router() function IsValideEmail(email) { if (!email || typeof email !== "string") { @@ -44,7 +43,7 @@ function ValidateEmail(email) { function EmailAllowed(email) { const [, domain] = email.split("@") - if (!micromatch.isMatch(domain, config.VALID_EMAIL_DOMAINS)) { + if (!isMatch(domain, config.VALID_EMAIL_DOMAINS)) { throw new HttpError(400, "Email domain not allowed") } } @@ -58,7 +57,7 @@ async function ValidateMailboxAsync(email, name) { throw new HttpError(400, "Invalid name") } - const existing = await db.FindBy({ "mailboxes.email": email }) + const existing = await FindBy({ "mailboxes.email": email }) if (existing) { throw new HttpError(409, "Mailbox already claimed") @@ -68,7 +67,7 @@ async function ValidateMailboxAsync(email, name) { async function EnsureNotMailboxAsync(email) { ValidateEmail(email) - const mailbox = await db.FindBy({ "mailboxes.email": email }) + const mailbox = await FindBy({ "mailboxes.email": email }) if (mailbox) { throw new HttpError(409, "Mailbox already claimed") @@ -78,7 +77,7 @@ async function EnsureNotMailboxAsync(email) { async function EnsureMailboxAsync(email) { ValidateEmail(email) - const mailbox = await db.FindBy({ "mailboxes.email": email }) + const mailbox = await FindBy({ "mailboxes.email": email }) if (!mailbox) { throw new HttpError(404, "Mailbox does not exist") @@ -118,10 +117,7 @@ router.post("/mailbox/edit", RequireAppAuth, async (req, res, next) => { await EnsureMailboxOwnershipAsync(user, email) - await db.UpdateBy( - { id: res.locals.id, "mailboxes.email": email }, - { "mailboxes.$.name": req.body.name }, - ) + await services.mailboxes.EditMailbox(res.locals.id, email, { name }) return res.sendStatus(200) }) @@ -132,10 +128,7 @@ router.post("/mailbox/delete", RequireAppAuth, async (req, res, next) => { await EnsureMailboxOwnershipAsync(user, email) - await db.DeleteFromArrayBy( - { id: res.locals.id }, - { mailboxes: { email: email } }, - ) + await services.mailboxes.DeleteMailbox(res.locals.id, email) return res.sendStatus(200) }) @@ -166,7 +159,7 @@ router.post("/mailbox/create", RequireAppAuth, async (req, res, next) => { await EnsureNotMailboxAsync(email) - await db.AddToArray({ id: res.locals.id }, { mailboxes: { email, name } }) + await services.mailboxes.CreateMailbox(res.locals.id, { email, name }) return res.sendStatus(200) }) @@ -182,4 +175,4 @@ router.post("/mailbox/check", async (req, res, next) => { return res.sendStatus(200) }) -module.exports = router +export default router diff --git a/src/routes/routes.js b/src/routes/index.js similarity index 56% rename from src/routes/routes.js rename to src/routes/index.js index 0031a7f..bb99434 100644 --- a/src/routes/routes.js +++ b/src/routes/index.js @@ -1,9 +1,10 @@ -const express = require("express") -const router = express.Router() +import { Router } from "express" -const { HttpError } = require("../types/errors") -const { RequireMailAuth } = require("../router") -const logger = require("../utils/logger") +import { HttpError } from "#types/errors" +import { RequireMailAuth } from "#router" +import logger from "#utils/logger" + +const router = Router() router.get("/", (req, res, next) => { if (!req.isAuthenticated()) { @@ -21,4 +22,4 @@ router.get("/select", RequireMailAuth, async (req, res, next) => { }) }) -module.exports = router +export default router diff --git a/src/routes/oauth-application.js b/src/routes/oauth-application.js index 8323b58..8b51c47 100644 --- a/src/routes/oauth-application.js +++ b/src/routes/oauth-application.js @@ -1,10 +1,12 @@ -const router = require("express").Router() +import { Router } from "express" -const config = require("../utils/config") -const logger = require("../utils/logger") +import config from "#utils/config" +import logger from "#utils/logger" -const passport = require("passport") -const OpenIDConnectStrategy = require("passport-openidconnect").Strategy +import passport from "passport" +import { Strategy as OpenIDConnectStrategy } from "passport-openidconnect" + +const router = Router() passport.use( "oidc", @@ -24,7 +26,7 @@ passport.use( return done(new Error("OIDC profile missing id")) } - logger.debug("Profile: ", profile) + logger.dev("Profile: ", profile) return done(null, profile) }, @@ -53,4 +55,4 @@ router.get( }, ) -module.exports = router +export default router diff --git a/src/routes/oauth-mail.js b/src/routes/oauth-mail.js index 38fa55a..5d9ee86 100644 --- a/src/routes/oauth-mail.js +++ b/src/routes/oauth-mail.js @@ -1,18 +1,25 @@ -const express = require("express") -const router = express.Router() -const crypto = require("crypto") +import { Router } from "express" -const { DecodeToken, SignToken } = require("../utils/token") +import crypto from "crypto" -const logger = require("../utils/logger") -const config = require("../utils/config") -const db = require("../utils/db") +import { DecodeToken, SignToken } from "#utils/token" -const { parse: ParseUrl } = require("tldts") +import logger from "#utils/logger" +import config from "#utils/config" +import { + GetUserByID, + WriteToCache, + GetFromCache, + DeleteFromCache, +} from "#utils/db" -const qs = require("querystring") +import tldts from "tldts" -const axios = require("axios") +import querystring from "querystring" + +import axios from "axios" + +const router = Router() async function TokenExchange(endpoint, code, id, secret, redirectURI) { const data = { @@ -23,7 +30,7 @@ async function TokenExchange(endpoint, code, id, secret, redirectURI) { client_secret: secret, } - const formData = qs.stringify(data) + const formData = querystring.stringify(data) const response = await axios.post(endpoint, formData, { headers: { @@ -31,7 +38,7 @@ async function TokenExchange(endpoint, code, id, secret, redirectURI) { }, }) - logger.debug(`Token Exchange:\n`, response.data) + logger.dev("Token Exchange", response.data) return response.data } @@ -43,7 +50,7 @@ async function GetUserInfo(endpoint, token) { }, }) - logger.debug(`Userinfo:\n`, response.data) + logger.dev("Userinfo:", response.data) return response.data } @@ -51,7 +58,7 @@ async function GetUserInfo(endpoint, token) { async function IsOwnedByUser(id, email) { if (!id || !email) return false - const user = await db.GetUserByID(id) + const user = await GetUserByID(id) if (!user) return false @@ -68,17 +75,17 @@ function GetBaseUrl(req, overwriteHost = null) { function GetMatchingRedirectUri(req, redirectUris, host = null) { const baseUrl = GetBaseUrl(req, host) - const rootDomain = ParseUrl(baseUrl).domain + const rootDomain = tldts.parse(baseUrl).domain let candidates = redirectUris.filter( - (uri) => ParseUrl(uri).domain === rootDomain, + (uri) => tldts.parse(uri).domain === rootDomain, ) if (candidates.length > 1) { - const subdomain = ParseUrl(baseUrl).subdomain + const subdomain = tldts.parse(baseUrl).subdomain const match = candidates.find( - (uri) => ParseUrl(uri).subdomain === subdomain, + (uri) => tldts.parse(uri).subdomain === subdomain, ) if (match) candidates = [match] @@ -124,14 +131,14 @@ router.get("/authorize", async (req, res, next) => { // Generate our own random nonce instead const nonce = crypto.randomBytes(24).toString("hex") - await db.WriteToCache(`state:${nonce}`, { + await WriteToCache(`state:${nonce}`, { host: originalHost, origState: req.query.state, }) if ( matchedUri && - ParseUrl(GetBaseUrl(req)).domain !== ParseUrl(matchedUri).domain + tldts.parse(GetBaseUrl(req)).domain !== tldts.parse(matchedUri).domain ) { const authorizeUrl = matchedUri.replace("callback", "authorize") @@ -144,11 +151,11 @@ router.get("/authorize", async (req, res, next) => { const forwardedQuery = new URLSearchParams(req.query) forwardedQuery.set("state", nonce) - const newUrl = `${config.MAIL_AUTHORIZATION_ENDPOINT}?${forwardedQuery.toString()}` + const newUrl = `${MAIL_AUTHORIZATION_ENDPOINT}?${forwardedQuery.toString()}` return res.redirect(newUrl) } catch (err) { - logger.err("Error in /authorize:", err) + logger.error("Error in /authorize:", err) return res.status(500).send("Authorization failed") } }) @@ -161,13 +168,13 @@ router.get("/callback", async (req, res, next) => { return res.status(400).send("Missing state") } - const stateData = await db.GetFromCache(`state:${nonce}`) + const stateData = await GetFromCache(`state:${nonce}`) if (!stateData?.host) { return res.status(400).send("Invalid or expired state") } - await db.DeleteFromCache(`state:${nonce}`) + await DeleteFromCache(`state:${nonce}`) const redirectUri = GetMatchingRedirectUri( req, @@ -193,11 +200,11 @@ router.get("/callback", async (req, res, next) => { const codeHandle = crypto.randomBytes(24).toString("hex") - await db.WriteToCache(`code:${codeHandle}`, tokenRes) + await WriteToCache(`code:${codeHandle}`, tokenRes) const idToken = DecodeToken(tokenRes.id_token) - await db.WriteToCache(`access:${tokenRes.access_token}`, idToken.sub) + await WriteToCache(`access:${tokenRes.access_token}`, idToken.sub) req.session.mail = { code: codeHandle, @@ -207,7 +214,7 @@ router.get("/callback", async (req, res, next) => { return res.redirect("/select") } catch (err) { - logger.err("Error in /callback:", err) + logger.error("Error in /callback:", err) return res.status(502).send("Callback failed") } }) @@ -220,9 +227,9 @@ router.get("/mailbox", async (req, res, next) => { return res.status(400).send("No pending mail session") } - const originalHost = await db.GetFromCache(`state:${mailData.state}`) + const originalHost = await GetFromCache(`state:${mailData.state}`) - const tokenRes = await db.GetFromCache(`code:${mailData.code}`) + const tokenRes = await GetFromCache(`code:${mailData.code}`) if (!tokenRes?.id_token) { return res.status(400).send("Session expired, please restart") @@ -230,7 +237,7 @@ router.get("/mailbox", async (req, res, next) => { const idToken = DecodeToken(tokenRes.id_token) - await db.WriteToCache(`id:${idToken.sub}`, mailData.selected_mailbox) + await WriteToCache(`id:${idToken.sub}`, mailData.selected_mailbox) req.session.mail = {} @@ -248,7 +255,7 @@ router.get("/mailbox", async (req, res, next) => { `${redirectUri}?code=${mailData.code}&state=${mailData.state}`, ) } catch (err) { - logger.err("Error in /mailbox:", err) + logger.error("Error in /mailbox:", err) return res.status(500).send("Mailbox selection failed") } }) @@ -259,13 +266,13 @@ router.post("/token", async (req, res, next) => { return res.status(400).json({ error: "invalid_request" }) } - const tokenRes = await db.GetFromCache(`code:${req.body.code}`) + const tokenRes = await GetFromCache(`code:${req.body.code}`) if (!tokenRes?.id_token) { return res.status(400).json({ error: "invalid_grant" }) } - await db.DeleteFromCache(`code:${req.body.code}`) + await DeleteFromCache(`code:${req.body.code}`) const idToken = tokenRes.id_token const accessToken = tokenRes.access_token @@ -274,8 +281,8 @@ router.post("/token", async (req, res, next) => { let newPayload = decoded - const id = await db.GetFromCache(`access:${accessToken}`) - const mailbox = id ? await db.GetFromCache(`id:${id}`) : null + const id = await GetFromCache(`access:${accessToken}`) + const mailbox = id ? await GetFromCache(`id:${id}`) : null if (!(await IsOwnedByUser(id, mailbox))) { return res.status(403).json({ error: "mailbox_not_selected" }) @@ -287,7 +294,7 @@ router.post("/token", async (req, res, next) => { const newIdToken = SignToken(newPayload) // Keep access-token -> id mapping alive only as long as the token itself is valid - await db.WriteToCache(`access:${accessToken}`, id, 300) + await WriteToCache(`access:${accessToken}`, id, 300) const responseBody = { access_token: accessToken, @@ -299,7 +306,7 @@ router.post("/token", async (req, res, next) => { return res.status(200).json(responseBody) } catch (err) { - logger.err("Error in /token:", err) + logger.error("Error in /token:", err) return res.status(500).json({ error: "server_error" }) } }) @@ -323,8 +330,8 @@ router.get("/userinfo", async (req, res, next) => { return res.status(502).json({ error: "userinfo_unavailable" }) } - const id = await db.GetFromCache(`access:${accessToken}`) - const mailbox = id ? await db.GetFromCache(`id:${id}`) : null + const id = await GetFromCache(`access:${accessToken}`) + const mailbox = id ? await GetFromCache(`id:${id}`) : null if (!(await IsOwnedByUser(id, mailbox))) { return res.status(403).json({ error: "mailbox_not_selected" }) @@ -332,14 +339,14 @@ router.get("/userinfo", async (req, res, next) => { userinfo.email = mailbox - await db.DeleteFromCache(`access:${accessToken}`) - await db.DeleteFromCache(`id:${id}`) + await DeleteFromCache(`access:${accessToken}`) + await DeleteFromCache(`id:${id}`) return res.status(200).json(userinfo) } catch (err) { - logger.err("Error in /userinfo:", err) + _err("Error in /userinfo:", err) return res.status(502).json({ error: "server_error" }) } }) -module.exports = router +export default router diff --git a/src/server.js b/src/server.js index a5b9980..a93e975 100644 --- a/src/server.js +++ b/src/server.js @@ -1,23 +1,23 @@ -const logger = require("./utils/logger") -const db = require("./utils/db") -const docker = require("./docker") -const CreateApp = require("./app") +import logger from "#utils/logger" +import { Init } from "#utils/db" +import docker from "./docker.js" +import app from "./app.js" const PORT = process.env.PORT || 8070 async function start() { try { - await db.Init() + await Init() - const app = CreateApp() + const app = app() await docker() app.listen(PORT, () => { - logger.log(`Server running on http://localhost:${PORT}`) + logger.info(`Server running on http://localhost:${PORT}`) }) } catch (err) { - console.error(err) + logger.error(err) process.exit(1) } } diff --git a/src/services/index.js b/src/services/index.js new file mode 100644 index 0000000..4ceafeb --- /dev/null +++ b/src/services/index.js @@ -0,0 +1,4 @@ +import * as mailboxes from "./mailboxes.js" +import * as users from "./users.js" + +export default { mailboxes, users } diff --git a/src/services/mailboxes.js b/src/services/mailboxes.js new file mode 100644 index 0000000..4e5c2f9 --- /dev/null +++ b/src/services/mailboxes.js @@ -0,0 +1,20 @@ +async function EditMailbox(id, email, { name }) { + await db.UpdateBy( + { id: id, "mailboxes.email": email }, + { "mailboxes.$.name": name }, + ) +} + +async function CreateMailbox(id, { email, name }) { + await db.AddToArray({ id: id }, { mailboxes: { email, name } }) +} + +async function DeleteMailbox(id, email) { + await db.DeleteFromArrayBy({ id: id }, { mailboxes: { email: email } }) +} + +module.exports = { + EditMailbox: EditMailbox, + CreateMailbox: CreateMailbox, + DeleteMailbox: DeleteMailbox, +} diff --git a/src/services/users.js b/src/services/users.js new file mode 100644 index 0000000..e69de29 diff --git a/src/types/errors.js b/src/types/errors.js index b7df544..ce4d6a5 100644 --- a/src/types/errors.js +++ b/src/types/errors.js @@ -5,4 +5,4 @@ class HttpError extends Error { } } -module.exports.HttpError = HttpError +export { HttpError } diff --git a/src/utils/config.js b/src/utils/config.js index cb11283..5e9cb61 100644 --- a/src/utils/config.js +++ b/src/utils/config.js @@ -1,7 +1,95 @@ -const logger = require("./logger") - +import logger from "#utils/logger" + +/** + * @typedef {Object} Config + * + * @property {number} LOG_LEVEL + * @property {string} HOST + * @property {string} PREFIX + * @property {string} SESSION_SECRET + * @property {string} JWT_KEY_PATH + * + * @property {string} APP_ISSUER + * @property {string} APP_AUTHORIZATION_ENDPOINT + * @property {string} APP_TOKEN_ENDPOINT + * @property {string} APP_USERINFO_ENDPOINT + * @property {string} APP_REDIRECT_PATH + * @property {string} APP_CLIENT_ID + * @property {string} APP_CLIENT_SECRET + * @property {string} APP_SCOPE + * + * @property {string[]} VALID_EMAIL_DOMAINS + * @property {boolean} ALLOW_USER_MAILBOX_CREATION + * + * @property {string} MAIL_ISSUER + * @property {string} MAIL_AUTHORIZATION_ENDPOINT + * @property {string} MAIL_TOKEN_ENDPOINT + * @property {string} MAIL_USERINFO_ENDPOINT + * @property {string[]} MAIL_REDIRECT_URIS + * @property {string[]} MAIL_CALLBACK_URIS + * @property {string} MAIL_CLIENT_ID + * @property {string} MAIL_CLIENT_SECRET + * + * @property {string} DB_HOST + * @property {string} DB_NAME + * @property {string} DB_USER + * @property {string} DB_PASSWORD + * @property {string} DB_URI + * + * @property {string} REDIS_HOST + * @property {string} REDIS_PASSWORD + * @property {string} REDIS_URI + */ + +/** @type {Config} */ const config = {} +/** + * @typedef {Object} DefineOptions + * @property {"string"|"number"|"array"|"bool"} [type] + * @property {boolean} [required] + */ + +/** + * @overload + * @param {string} name + * @param {DefineOptions & { type: "string", default?: string | (() => string) }} options + * @returns {string} + */ + +/** + * @overload + * @param {string} name + * @param {DefineOptions & { type: "number", default?: number | (() => number) }} options + * @returns {number} + */ + +/** + * @overload + * @param {string} name + * @param {DefineOptions & { type: "array", default?: string[] | (() => string[]) }} options + * @returns {string[]} + */ + +/** + * @overload + * @param {string} name + * @param {DefineOptions & { type: "bool", default?: boolean | (() => boolean) }} options + * @returns {boolean} + */ + +/** + * @overload + * @param {string} name + * @param {DefineOptions & { type?: undefined, default?: string | (() => string) }} options + * @returns {string} + */ + +/** + * @param {string} name + * @param {DefineOptions & { default?: unknown }} [options={}] + * @returns {string|number|string[]|boolean|undefined} + */ function define( name, { type = "string", default: def, required = false } = {}, @@ -13,14 +101,14 @@ function define( if (def !== undefined) { value = typeof def === "function" ? def() : def } else if (required) { - logger.env(`${name} is required but not set`) + logger.error(`${name} is required but not set`) process.exit(1) } } else if (type === "number") { value = Number(raw) if (Number.isNaN(value)) { - logger.env(`${name} must be a number, got "${raw}"`) + env(`${name} must be a number, got "${raw}"`) process.exit(1) } } else if (type === "array") { @@ -33,7 +121,7 @@ function define( } if (value === undefined) { - logger.env(`${name} is not set`) + logger.warn(`${name} is not set`) } else { logger.debug(`${name} = ${value}`) } @@ -42,7 +130,7 @@ function define( return value } -define("LOG_LEVEL", { type: "number", default: 1 }) +define("LOG_LEVEL", { type: "string", default: "info" }) const HOST = define("HOST", { required: true }) define("PREFIX", { default: "/" }) @@ -63,6 +151,8 @@ define("APP_SCOPE", { default: "openid profile email" }) define("VALID_EMAIL_DOMAINS", { type: "array", default: ["*"] }) +define("ALLOW_USER_MAILBOX_CREATION", { type: "bool", default: true }) + // Mail define("MAIL_ISSUER", { default: HOST }) @@ -95,4 +185,4 @@ define("REDIS_PASSWORD", { required: true }) config.REDIS_URI = `redis://default:${encodeURIComponent(config.REDIS_PASSWORD)}@${config.REDIS_HOST}` -module.exports = config +export default config diff --git a/src/utils/db.js b/src/utils/db.js index 370ba1a..7e14b81 100644 --- a/src/utils/db.js +++ b/src/utils/db.js @@ -1,44 +1,44 @@ -const logger = require("./logger") -const config = require("./config") +import logger from "#utils/logger" +import config from "#utils/config" -const { MongoClient } = require("mongodb") -const redis = require("redis") +import { MongoClient } from "mongodb" +import { createClient } from "redis" let mongoClient, mongoSession let redisClient -async function Init() { +export async function Init() { mongoClient = new MongoClient(config.DB_URI) await mongoClient.connect() - logger.db("Connected to MongoDB") + _db("Connected to MongoDB") mongoSession = mongoClient.startSession() - logger.db("Started MongoDB Session") + _db("Started MongoDB Session") - redisClient = redis.createClient({ url: config.REDIS_URI }) + redisClient = createClient({ url: config.REDIS_URI }) await redisClient.connect() - logger.db("Connected to Redis") + _db("Connected to Redis") } -async function Close() { - logger.db("Closing MongoDB Connection") +export async function Close() { + _db("Closing MongoDB Connection") await mongoSession.endSession() await mongoClient.close() } -function Connect() { +export function Connect() { return mongoClient.db(config.DB_NAME) } // Mongo -async function InsertUser(user) { +export async function InsertUser(user) { const db = await Connect() const collection = db.collection("users") @@ -48,7 +48,7 @@ async function InsertUser(user) { return result } -async function GetUserByID(id) { +export async function GetUserByID(id) { const db = await Connect() const collection = db.collection("users") @@ -56,19 +56,19 @@ async function GetUserByID(id) { return await collection.findOne({ id: id }) } -async function DeleteUserByID(id) { +export async function DeleteUserByID(id) { const db = await Connect() const collection = db.collection("users") const result = await collection.deleteOne({ id: id }) - logger.db("Deleted a User") + _db("Deleted a User") return result } -async function FindBy(query) { +export async function FindBy(query) { const db = await Connect() const collection = db.collection("users") @@ -78,7 +78,7 @@ async function FindBy(query) { return result } -async function AddToArray(query, update) { +export async function AddToArray(query, update) { const db = await Connect() const collection = db.collection("users") @@ -92,7 +92,7 @@ async function AddToArray(query, update) { return result } -async function DeleteFromArrayBy(query, update) { +export async function DeleteFromArrayBy(query, update) { const db = await Connect() const collection = db.collection("users") @@ -102,7 +102,7 @@ async function DeleteFromArrayBy(query, update) { return result } -async function UpdateBy(query, update) { +export async function UpdateBy(query, update) { const db = await Connect() const collection = db.collection("users") @@ -118,7 +118,7 @@ async function UpdateBy(query, update) { // REDIS -async function GetFromCache(key) { +export async function GetFromCache(key) { const value = await redisClient.get(key) if (value === null) { @@ -132,7 +132,7 @@ async function GetFromCache(key) { } } -async function WriteToCache(key, value, ttl = 3600) { +export async function WriteToCache(key, value, ttl = 3600) { if (typeof value !== "string") { value = JSON.stringify(value) } @@ -141,33 +141,14 @@ async function WriteToCache(key, value, ttl = 3600) { await redisClient.expire(key, ttl) } -async function DeleteFromCache(key) { +export async function DeleteFromCache(key) { await redisClient.del(key) } -function GetMongoDB() { +export function GetMongoDB() { return mongoClient } -function GetRedis() { +export function GetRedis() { return redisClient } - -exports.GetMongoDB = GetMongoDB - -exports.GetRedis = GetRedis - -exports.DeleteUserByID = DeleteUserByID -exports.GetUserByID = GetUserByID -exports.InsertUser = InsertUser -exports.Init = Init -exports.Close = Close - -exports.AddToArray = AddToArray -exports.FindBy = FindBy -exports.UpdateBy = UpdateBy -exports.DeleteFromArrayBy = DeleteFromArrayBy - -exports.GetFromCache = GetFromCache -exports.WriteToCache = WriteToCache -exports.DeleteFromCache = DeleteFromCache diff --git a/src/utils/logger.js b/src/utils/logger.js index 2e3f571..520ac36 100644 --- a/src/utils/logger.js +++ b/src/utils/logger.js @@ -1,59 +1,56 @@ -const util = require("util") - -const LOG_LEVEL = parseInt(process.env.LOG_LEVEL) || 1 - -const LEVEL = { - ENV: { value: "ENV_", id: 1 }, - DB: { value: "DB__", id: 1 }, - INFO: { value: "INFO", id: 1 }, - WARN: { value: "WARN", id: 1 }, - EROR: { value: "EROR", id: 1 }, - DEBG: { value: "DEBG", id: 10 }, -} - -function _log(level, ...args) { - if (LOG_LEVEL < level.id) { - return +import pino from "pino" + +const _logger = pino({ + level: process.env.LOG_LEVEL ?? "info", + customLevels: { + dev: 15, + }, + transport: { + target: "pino-pretty", + options: { + colorize: true, + translateTime: "dd.mm.yy HH:MM:ss", + ignore: "pid,hostname", + }, + }, +}) + +function normalize(obj) { + if (obj instanceof Error) { + return { err: obj } } - const formattedArgs = args.map((arg) => { - if (typeof arg === "object") { - return util.inspect(arg, { showHidden: false, depth: null, colors: true }) - } - return arg - }) - console.log(`[${level.value}]`, ...formattedArgs) + return obj } -function log(...args) { - _log(LEVEL.INFO, ...args) -} +const logger = { + trace(msg, obj) { + _logger.trace(normalize(obj), msg) + }, -function warn(...args) { - _log(LEVEL.WARN, ...args) -} + dev(msg, obj) { + _logger.dev(normalize(obj), msg) + }, -function err(...args) { - _log(LEVEL.EROR, ...args) -} + debug(msg, obj) { + _logger.debug(normalize(obj), msg) + }, -function env(...args) { - _log(LEVEL.ENV, ...args) -} + info(msg, obj) { + _logger.info(normalize(obj), msg) + }, -function db(...args) { - _log(LEVEL.DB, ...args) -} + warn(msg, obj) { + _logger.warn(normalize(obj), msg) + }, -function debug(...args) { - _log(LEVEL.DEBG, ...args) -} + error(msg, obj) { + _logger.error(normalize(obj), msg) + }, -exports.log = log -exports.warn = warn -exports.err = err -exports.env = env -exports.db = db -exports.debug = debug + fatal(msg, obj) { + _logger.fatal(normalize(obj), msg) + }, +} -exports.LEVEL = LEVEL +export default logger diff --git a/src/utils/token.js b/src/utils/token.js index 23b27aa..8f3815f 100644 --- a/src/utils/token.js +++ b/src/utils/token.js @@ -1,26 +1,26 @@ -const jwt = require("jsonwebtoken") -const fs = require("fs") -const path = require("path") +import jwt from "jsonwebtoken" +import crypto from "crypto" -const config = require("./config") +import fs from "fs" +import path from "path" -const logger = require("./logger") +import config from "#utils/config" -const { generateKeyPairSync } = require("crypto") +import logger from "#utils/logger" let privateKey -function CheckForKey() { +export function CheckForKey() { const privateKeyPath = path.join(config.JWT_KEY_PATH, "private_key.pem") const publicKeyPath = path.join(config.JWT_KEY_PATH, "public_key.pem") if (fs.existsSync(privateKeyPath) && fs.existsSync(publicKeyPath)) { privateKey = fs.readFileSync(privateKeyPath, "utf8") - logger.log("Loaded existing RSA private key") + logger.info("Loaded existing RSA private key") } else { const { privateKey: genPrivKey, publicKey: genPubKey } = - generateKeyPairSync("rsa", { + crypto.generateKeyPairSync("rsa", { modulusLength: 2048, publicKeyEncoding: { type: "spki", @@ -43,7 +43,7 @@ function CheckForKey() { } } -function SignToken(payload) { +export function SignToken(payload) { const options = { algorithm: "RS256", keyid: "middleware-key-1", @@ -52,12 +52,8 @@ function SignToken(payload) { return jwt.sign(payload, privateKey, options) } -function DecodeToken(idToken) { +export function DecodeToken(idToken) { return jwt.decode(idToken) } CheckForKey() - -exports.SignToken = SignToken -exports.DecodeToken = DecodeToken -exports.CheckForKey = CheckForKey From c1ff9d99ed6f595baf2665a5920f31ea8193a723 Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Mon, 10 Aug 2026 17:50:23 +0200 Subject: [PATCH 03/14] fix missing esm converison in mailboxes.js --- src/services/mailboxes.js | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/src/services/mailboxes.js b/src/services/mailboxes.js index 4e5c2f9..8bf9c28 100644 --- a/src/services/mailboxes.js +++ b/src/services/mailboxes.js @@ -1,20 +1,14 @@ -async function EditMailbox(id, email, { name }) { +export async function EditMailbox(id, email, { name }) { await db.UpdateBy( { id: id, "mailboxes.email": email }, { "mailboxes.$.name": name }, ) } -async function CreateMailbox(id, { email, name }) { +export async function CreateMailbox(id, { email, name }) { await db.AddToArray({ id: id }, { mailboxes: { email, name } }) } -async function DeleteMailbox(id, email) { +export async function DeleteMailbox(id, email) { await db.DeleteFromArrayBy({ id: id }, { mailboxes: { email: email } }) } - -module.exports = { - EditMailbox: EditMailbox, - CreateMailbox: CreateMailbox, - DeleteMailbox: DeleteMailbox, -} From 8430e799d2439ce983cb75d131cee0e0b15249ba Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Mon, 10 Aug 2026 18:16:23 +0200 Subject: [PATCH 04/14] fix logger --- src/utils/logger.js | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/src/utils/logger.js b/src/utils/logger.js index 520ac36..43a1715 100644 --- a/src/utils/logger.js +++ b/src/utils/logger.js @@ -13,8 +13,52 @@ const _logger = pino({ ignore: "pid,hostname", }, }, + mixin() { + return { caller: getCaller() } + }, }) +function getCaller() { + const stack = new Error().stack?.split("\n") ?? [] + + return parseCaller(stack[6]) +} + +function parseCaller( + frame, + { showFunction = false, showLine = false, showFile = true } = {}, +) { + if (!frame) return "unknown" + + let match = frame.match(/^\s*at\s+(.+?)\s+\((.+):(\d+):(\d+)\)$/) + + if (match) { + const [, functionName, file, line] = match + + let res = `` + + if (showFile) res += cleanPath(file) + if (showLine) res += `:${line}` + if (showFunction) res += ` ${functionName}()` + + return res + } + + match = frame.match(/^\s*at\s+(.+):(\d+):(\d+)$/) + + if (match) { + const [, file, line] = match + + return `${cleanPath(file)}` + } + + return frame.trim().replace(/^at\s+/, "") +} + +function cleanPath(file) { + return file.replace(/^file:\/\//, "").replace(/^.*\/src\//, "") +} + function normalize(obj) { if (obj instanceof Error) { return { err: obj } From 45c768a479c5b97c30950242569bf5c0c851f1f1 Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Mon, 10 Aug 2026 18:18:36 +0200 Subject: [PATCH 05/14] . --- src/server.js | 2 +- src/utils/logger.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/server.js b/src/server.js index a93e975..e275a96 100644 --- a/src/server.js +++ b/src/server.js @@ -17,7 +17,7 @@ async function start() { logger.info(`Server running on http://localhost:${PORT}`) }) } catch (err) { - logger.error(err) + logger.error("", err) process.exit(1) } } diff --git a/src/utils/logger.js b/src/utils/logger.js index 43a1715..9a89463 100644 --- a/src/utils/logger.js +++ b/src/utils/logger.js @@ -26,7 +26,7 @@ function getCaller() { function parseCaller( frame, - { showFunction = false, showLine = false, showFile = true } = {}, + { showFunction = false, showLine = true, showFile = true } = {}, ) { if (!frame) return "unknown" From c1863e958a429f7caa96f2548512bee911bd0ae3 Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Mon, 10 Aug 2026 18:21:38 +0200 Subject: [PATCH 06/14] fix db.js --- src/utils/db.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/utils/db.js b/src/utils/db.js index 7e14b81..66fd52b 100644 --- a/src/utils/db.js +++ b/src/utils/db.js @@ -12,21 +12,21 @@ export async function Init() { await mongoClient.connect() - _db("Connected to MongoDB") + logger.debug("Connected to MongoDB") mongoSession = mongoClient.startSession() - _db("Started MongoDB Session") + logger.debug("Started MongoDB Session") redisClient = createClient({ url: config.REDIS_URI }) await redisClient.connect() - _db("Connected to Redis") + logger.debug("Connected to Redis") } export async function Close() { - _db("Closing MongoDB Connection") + logger.debug("Closing MongoDB Connection") await mongoSession.endSession() await mongoClient.close() @@ -63,7 +63,7 @@ export async function DeleteUserByID(id) { const result = await collection.deleteOne({ id: id }) - _db("Deleted a User") + logger.warn("Deleted a User") return result } From daf1d51137fa342751e9010d942393ccc24cebef Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Mon, 10 Aug 2026 18:34:53 +0200 Subject: [PATCH 07/14] skip callers --- src/server.js | 2 +- src/utils/config.js | 12 ++++++---- src/utils/logger.js | 53 ++++++++++++++++++++++++++++----------------- 3 files changed, 42 insertions(+), 25 deletions(-) diff --git a/src/server.js b/src/server.js index e275a96..5b147cc 100644 --- a/src/server.js +++ b/src/server.js @@ -17,7 +17,7 @@ async function start() { logger.info(`Server running on http://localhost:${PORT}`) }) } catch (err) { - logger.error("", err) + logger.error("", err, { skipCaller: true }) process.exit(1) } } diff --git a/src/utils/config.js b/src/utils/config.js index 5e9cb61..a9a92b0 100644 --- a/src/utils/config.js +++ b/src/utils/config.js @@ -101,14 +101,18 @@ function define( if (def !== undefined) { value = typeof def === "function" ? def() : def } else if (required) { - logger.error(`${name} is required but not set`) + logger.error(`${name} is required but not set`, null, { + skipCaller: true, + }) process.exit(1) } } else if (type === "number") { value = Number(raw) if (Number.isNaN(value)) { - env(`${name} must be a number, got "${raw}"`) + logger.error(`${name} must be a number, got "${raw}"`, null, { + skipCaller: true, + }) process.exit(1) } } else if (type === "array") { @@ -121,9 +125,9 @@ function define( } if (value === undefined) { - logger.warn(`${name} is not set`) + logger.warn(`${name} is not set`, null, { skipCaller: true }) } else { - logger.debug(`${name} = ${value}`) + logger.debug(`${name} = ${value}`, null, { skipCaller: true }) } config[name] = value diff --git a/src/utils/logger.js b/src/utils/logger.js index 9a89463..13da89e 100644 --- a/src/utils/logger.js +++ b/src/utils/logger.js @@ -13,15 +13,12 @@ const _logger = pino({ ignore: "pid,hostname", }, }, - mixin() { - return { caller: getCaller() } - }, }) -function getCaller() { +function getCaller(skips) { const stack = new Error().stack?.split("\n") ?? [] - return parseCaller(stack[6]) + return parseCaller(stack[4 + skips]) } function parseCaller( @@ -48,8 +45,12 @@ function parseCaller( if (match) { const [, file, line] = match + let res = `` + + if (showFile) res += cleanPath(file) + if (showLine) res += `:${line}` - return `${cleanPath(file)}` + return res } return frame.trim().replace(/^at\s+/, "") @@ -67,33 +68,45 @@ function normalize(obj) { return obj } +function log(level, msg, obj, options = {}) { + const caller = getCaller(options.skipCaller ?? 0) + + if (obj !== undefined && typeof obj === "object" && obj !== null) { + obj.caller = caller + + _logger[level](normalize(obj), msg) + } else { + _logger[level]({ caller }, msg) + } +} + const logger = { - trace(msg, obj) { - _logger.trace(normalize(obj), msg) + trace(msg, obj, options = {}) { + log("trace", msg, obj, options) }, - dev(msg, obj) { - _logger.dev(normalize(obj), msg) + dev(msg, obj, options = {}) { + log("dev", msg, obj, options) }, - debug(msg, obj) { - _logger.debug(normalize(obj), msg) + debug(msg, obj, options = {}) { + log("debug", msg, obj, options) }, - info(msg, obj) { - _logger.info(normalize(obj), msg) + info(msg, obj, options = {}) { + log("info", msg, obj, options) }, - warn(msg, obj) { - _logger.warn(normalize(obj), msg) + warn(msg, obj, options = {}) { + log("warn", msg, obj, options) }, - error(msg, obj) { - _logger.error(normalize(obj), msg) + error(msg, obj, options = {}) { + log("error", msg, obj, options) }, - fatal(msg, obj) { - _logger.fatal(normalize(obj), msg) + fatal(msg, obj, options = {}) { + log("fatal", msg, obj, options) }, } From fba7f8a35189facd85d9c7cf8e1326a2ee3aeb4f Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Mon, 10 Aug 2026 18:36:56 +0200 Subject: [PATCH 08/14] . --- src/server.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/server.js b/src/server.js index 5b147cc..647cc9a 100644 --- a/src/server.js +++ b/src/server.js @@ -1,7 +1,7 @@ import logger from "#utils/logger" import { Init } from "#utils/db" import docker from "./docker.js" -import app from "./app.js" +import createApp from "./app.js" const PORT = process.env.PORT || 8070 @@ -9,7 +9,7 @@ async function start() { try { await Init() - const app = app() + const app = createApp() await docker() From 6c5e1b19cb32e3a637d5ba9a757b9f7fa3d3400d Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Mon, 10 Aug 2026 18:40:49 +0200 Subject: [PATCH 09/14] fix name --- src/routes/data.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/routes/data.js b/src/routes/data.js index f27b5b8..4d80c6c 100644 --- a/src/routes/data.js +++ b/src/routes/data.js @@ -117,7 +117,9 @@ router.post("/mailbox/edit", RequireAppAuth, async (req, res, next) => { await EnsureMailboxOwnershipAsync(user, email) - await services.mailboxes.EditMailbox(res.locals.id, email, { name }) + await services.mailboxes.EditMailbox(res.locals.id, email, { + name: req.body.name, + }) return res.sendStatus(200) }) From 9e172c06c476d20716468b1ea2db610e2231753e Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Mon, 10 Aug 2026 18:41:05 +0200 Subject: [PATCH 10/14] . --- src/routes/data.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/routes/data.js b/src/routes/data.js index 4d80c6c..58e70cc 100644 --- a/src/routes/data.js +++ b/src/routes/data.js @@ -114,11 +114,12 @@ router.get("/mailbox", RequireMailOrAppAuth, async (req, res, next) => { router.post("/mailbox/edit", RequireAppAuth, async (req, res, next) => { const user = res.locals.user const email = req.body.email + const name = req.body.name await EnsureMailboxOwnershipAsync(user, email) await services.mailboxes.EditMailbox(res.locals.id, email, { - name: req.body.name, + name, }) return res.sendStatus(200) From 7a67d54c7744cd51824354c9fbf73b69beef905d Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Mon, 10 Aug 2026 18:43:18 +0200 Subject: [PATCH 11/14] add missing import --- src/services/mailboxes.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/services/mailboxes.js b/src/services/mailboxes.js index 8bf9c28..2f43a70 100644 --- a/src/services/mailboxes.js +++ b/src/services/mailboxes.js @@ -1,3 +1,5 @@ +import db from "#utils/db" + export async function EditMailbox(id, email, { name }) { await db.UpdateBy( { id: id, "mailboxes.email": email }, From de2ab05b63da9fea7d69bab9dadd47c8ff34ce30 Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Mon, 10 Aug 2026 18:47:07 +0200 Subject: [PATCH 12/14] fix db import --- src/services/mailboxes.js | 8 ++++---- src/utils/db.js | 12 ------------ 2 files changed, 4 insertions(+), 16 deletions(-) diff --git a/src/services/mailboxes.js b/src/services/mailboxes.js index 2f43a70..4679012 100644 --- a/src/services/mailboxes.js +++ b/src/services/mailboxes.js @@ -1,16 +1,16 @@ -import db from "#utils/db" +import { AddToArray, DeleteFromArrayBy, UpdateBy } from "#utils/db" export async function EditMailbox(id, email, { name }) { - await db.UpdateBy( + await UpdateBy( { id: id, "mailboxes.email": email }, { "mailboxes.$.name": name }, ) } export async function CreateMailbox(id, { email, name }) { - await db.AddToArray({ id: id }, { mailboxes: { email, name } }) + await AddToArray({ id: id }, { mailboxes: { email, name } }) } export async function DeleteMailbox(id, email) { - await db.DeleteFromArrayBy({ id: id }, { mailboxes: { email: email } }) + await DeleteFromArrayBy({ id: id }, { mailboxes: { email: email } }) } diff --git a/src/utils/db.js b/src/utils/db.js index 66fd52b..3045bad 100644 --- a/src/utils/db.js +++ b/src/utils/db.js @@ -140,15 +140,3 @@ export async function WriteToCache(key, value, ttl = 3600) { await redisClient.set(key, value) await redisClient.expire(key, ttl) } - -export async function DeleteFromCache(key) { - await redisClient.del(key) -} - -export function GetMongoDB() { - return mongoClient -} - -export function GetRedis() { - return redisClient -} From 0ec3cc2cdb53b40e921f52560c4056ca11e97ecb Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Mon, 10 Aug 2026 18:49:12 +0200 Subject: [PATCH 13/14] oops... --- src/utils/db.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/utils/db.js b/src/utils/db.js index 3045bad..66fd52b 100644 --- a/src/utils/db.js +++ b/src/utils/db.js @@ -140,3 +140,15 @@ export async function WriteToCache(key, value, ttl = 3600) { await redisClient.set(key, value) await redisClient.expire(key, ttl) } + +export async function DeleteFromCache(key) { + await redisClient.del(key) +} + +export function GetMongoDB() { + return mongoClient +} + +export function GetRedis() { + return redisClient +} From 00d95a4e7a3157f16b945c5ee3792b52c5dcd4e8 Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Mon, 10 Aug 2026 18:52:15 +0200 Subject: [PATCH 14/14] fix broken import usage --- src/routes/data.js | 2 +- src/server.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/routes/data.js b/src/routes/data.js index 58e70cc..9b4b34f 100644 --- a/src/routes/data.js +++ b/src/routes/data.js @@ -43,7 +43,7 @@ function ValidateEmail(email) { function EmailAllowed(email) { const [, domain] = email.split("@") - if (!isMatch(domain, config.VALID_EMAIL_DOMAINS)) { + if (!micromatch.isMatch(domain, config.VALID_EMAIL_DOMAINS)) { throw new HttpError(400, "Email domain not allowed") } } diff --git a/src/server.js b/src/server.js index 647cc9a..f7f6035 100644 --- a/src/server.js +++ b/src/server.js @@ -17,7 +17,7 @@ async function start() { logger.info(`Server running on http://localhost:${PORT}`) }) } catch (err) { - logger.error("", err, { skipCaller: true }) + logger.error("Encountered error", err, { skipCaller: true }) process.exit(1) } }