From 31c54083a9aa65b5e42e93e0c5d974fbe53cf5cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thea=20Sch=C3=B6bl?= Date: Wed, 26 Jun 2024 20:40:00 +0200 Subject: [PATCH] feat: email client prototype --- backend/backend/package.json | 4 +- backend/mail-plugin/app.js | 2 + backend/mail-plugin/package.json | 68 ++ backend/mail-plugin/src/cli.ts | 123 ++++ backend/mail-plugin/src/types.d.ts | 9 + backend/mail-plugin/tsconfig.json | 4 + frontend/app/.gitignore | 1 + frontend/app/package.json | 9 + frontend/app/package.json.orig | 214 ++++++ frontend/app/src/app/app.module.ts | 24 +- .../app/modules/mail/mail-adapter.service.ts | 237 +++++++ .../app/modules/mail/mail-detail.component.ts | 58 ++ .../app/src/app/modules/mail/mail-detail.html | 93 +++ .../app/src/app/modules/mail/mail-detail.scss | 97 +++ .../app/modules/mail/mail-login.component.ts | 65 ++ .../app/src/app/modules/mail/mail-login.html | 67 ++ .../app/src/app/modules/mail/mail-login.scss | 17 + .../app/modules/mail/mail-page.component.ts | 60 ++ .../app/src/app/modules/mail/mail-page.html | 45 ++ .../app/src/app/modules/mail/mail-page.scss | 26 + .../app/modules/mail/mail-storage.provider.ts | 243 +++++++ .../app/src/app/modules/mail/mail.module.ts | 60 ++ .../app/src/app/modules/mail/mail.pipe.ts | 17 + .../app/src/app/modules/mail/mail.service.ts | 84 +++ .../modules/mail/mailbox-page.component.ts | 54 ++ .../src/app/modules/mail/mailbox-page.html | 54 ++ .../src/app/modules/mail/mailbox-page.scss | 44 ++ frontend/app/src/app/modules/mail/schema.ts | 128 ++++ .../profile/page/profile-page-section.html | 5 + .../profile/page/profile-page-section.scss | 7 + frontend/app/src/app/translation/i18n.spec.ts | 1 + frontend/app/src/app/util/data-size.pipe.ts | 25 + frontend/app/src/app/util/lazy-load.pipe.ts | 36 + frontend/app/src/app/util/lazy.component.ts | 46 ++ frontend/app/src/app/util/lazy.html | 3 + frontend/app/src/app/util/lazy.scss | 0 frontend/app/src/app/util/rxjs/from-cursor.ts | 22 + .../app/src/app/util/shadow-html.directive.ts | 18 + frontend/app/src/assets/i18n/de.json | 30 + frontend/app/src/assets/i18n/en.json | 30 + .../app/src/config/profile-page-sections.ts | 13 + frontend/app/src/index.html | 2 +- packages/logger/package.json | 4 +- pnpm-lock.yaml | 644 +++++++++++++----- 44 files changed, 2597 insertions(+), 196 deletions(-) create mode 100644 backend/mail-plugin/app.js create mode 100644 backend/mail-plugin/package.json create mode 100644 backend/mail-plugin/src/cli.ts create mode 100644 backend/mail-plugin/src/types.d.ts create mode 100644 backend/mail-plugin/tsconfig.json create mode 100644 frontend/app/package.json.orig create mode 100644 frontend/app/src/app/modules/mail/mail-adapter.service.ts create mode 100644 frontend/app/src/app/modules/mail/mail-detail.component.ts create mode 100644 frontend/app/src/app/modules/mail/mail-detail.html create mode 100644 frontend/app/src/app/modules/mail/mail-detail.scss create mode 100644 frontend/app/src/app/modules/mail/mail-login.component.ts create mode 100644 frontend/app/src/app/modules/mail/mail-login.html create mode 100644 frontend/app/src/app/modules/mail/mail-login.scss create mode 100644 frontend/app/src/app/modules/mail/mail-page.component.ts create mode 100644 frontend/app/src/app/modules/mail/mail-page.html create mode 100644 frontend/app/src/app/modules/mail/mail-page.scss create mode 100644 frontend/app/src/app/modules/mail/mail-storage.provider.ts create mode 100644 frontend/app/src/app/modules/mail/mail.module.ts create mode 100644 frontend/app/src/app/modules/mail/mail.pipe.ts create mode 100644 frontend/app/src/app/modules/mail/mail.service.ts create mode 100644 frontend/app/src/app/modules/mail/mailbox-page.component.ts create mode 100644 frontend/app/src/app/modules/mail/mailbox-page.html create mode 100644 frontend/app/src/app/modules/mail/mailbox-page.scss create mode 100644 frontend/app/src/app/modules/mail/schema.ts create mode 100644 frontend/app/src/app/util/data-size.pipe.ts create mode 100644 frontend/app/src/app/util/lazy-load.pipe.ts create mode 100644 frontend/app/src/app/util/lazy.component.ts create mode 100644 frontend/app/src/app/util/lazy.html create mode 100644 frontend/app/src/app/util/lazy.scss create mode 100644 frontend/app/src/app/util/rxjs/from-cursor.ts create mode 100644 frontend/app/src/app/util/shadow-html.directive.ts diff --git a/backend/backend/package.json b/backend/backend/package.json index b4caf35d..116c3c76 100644 --- a/backend/backend/package.json +++ b/backend/backend/package.json @@ -53,7 +53,7 @@ "@types/geojson": "1.0.6", "@types/node": "18.15.3", "@types/node-cron": "3.0.7", - "@types/nodemailer": "6.4.7", + "@types/nodemailer": "6.4.15", "@types/promise-queue": "2.2.0", "@types/uuid": "8.3.4", "body-parser": "1.20.2", @@ -69,7 +69,7 @@ "nock": "13.3.1", "node-cache": "5.1.2", "node-cron": "3.0.2", - "nodemailer": "6.9.1", + "nodemailer": "6.9.14", "prom-client": "14.1.1", "promise-queue": "2.2.5", "uuid": "8.3.2" diff --git a/backend/mail-plugin/app.js b/backend/mail-plugin/app.js new file mode 100644 index 00000000..1bda7715 --- /dev/null +++ b/backend/mail-plugin/app.js @@ -0,0 +1,2 @@ +#!/usr/bin/env node +import './lib/cli.js'; diff --git a/backend/mail-plugin/package.json b/backend/mail-plugin/package.json new file mode 100644 index 00000000..641aa020 --- /dev/null +++ b/backend/mail-plugin/package.json @@ -0,0 +1,68 @@ +{ + "name": "@openstapps/mail-plugin", + "description": "Mail Plugin", + "version": "3.2.0", + "private": true, + "type": "module", + "license": "GPL-3.0-only", + "author": "Thea Schöbl", + "bin": "app.js", + "files": [ + "app.js", + "lib", + "README.md", + "CHANGELOG.md", + "Dockerfile" + ], + "scripts": { + "build": "tsup-node --dts", + "deploy": "pnpm --prod --filter=@openstapps/minimal-plugin deploy ../../.deploy/minimal-plugin", + "dev": "tsup-node --watch --onSuccess \"pnpm run start\"", + "format": "prettier . -c --ignore-path ../../.gitignore", + "format:fix": "prettier --write . --ignore-path ../../.gitignore", + "lint": "eslint --ext .ts src/", + "lint:fix": "eslint --fix --ext .ts src/", + "start": "node app.js" + }, + "dependencies": { + "@openstapps/core": "workspace:*", + "@openstapps/core-tools": "workspace:*", + "@openstapps/logger": "workspace:*", + "commander": "10.0.0", + "dotenv": "16.4.5", + "express": "4.18.2", + "imapflow": "1.0.162", + "mailparser": "3.7.1", + "node-forge": "1.3.1", + "nodemailer": "6.9.14", + "ts-node": "10.9.2" + }, + "devDependencies": { + "@openstapps/eslint-config": "workspace:*", + "@openstapps/prettier-config": "workspace:*", + "@openstapps/tsconfig": "workspace:*", + "@types/express": "4.17.17", + "@types/imapflow": "1.0.18", + "@types/mailparser": "3.4.4", + "@types/node": "18.15.3", + "@types/node-forge": "1.3.11", + "@types/nodemailer": "6.4.15", + "tsup": "6.7.0", + "typescript": "5.4.2" + }, + "tsup": { + "entry": [ + "src/cli.ts" + ], + "sourcemap": true, + "clean": true, + "format": "esm", + "outDir": "lib" + }, + "prettier": "@openstapps/prettier-config", + "eslintConfig": { + "extends": [ + "@openstapps" + ] + } +} diff --git a/backend/mail-plugin/src/cli.ts b/backend/mail-plugin/src/cli.ts new file mode 100644 index 00000000..16ddef18 --- /dev/null +++ b/backend/mail-plugin/src/cli.ts @@ -0,0 +1,123 @@ +import {config} from 'dotenv'; +import {ImapFlow} from 'imapflow'; +import express from 'express'; + +config({path: '.env.local'}); + +const app = express(); +const port = process.env.PORT || 4000; + +app.use(async (request, response, next) => { + try { + const [user, pass] = Buffer.from(request.headers['authorization']!.replace(/^Basic /, ''), 'base64') + .toString('utf8') + .split(':'); + + const client = new ImapFlow({ + host: 'imap.server.uni-frankfurt.de', + port: 993, + secure: true, + emitLogs: false, + auth: {user, pass}, + }); + response.locals.client = client; + + await client.connect(); + response.on('finish', async () => { + await client.logout(); + client.close(); + }); + + next(); + } catch { + response.status(401).send(); + } +}); + +app.get('/', async (_request, response) => { + const result = await response.locals.client.listTree(); + response.json(result); +}); + +app.get('/:mailbox', async (request, response) => { + try { + await response.locals.client.mailboxOpen(request.params.mailbox); + const since = Number(request.query.since) || undefined; + const preData = await response.locals.client.status(request.params.mailbox, {messages: true}); + if (preData.messages === 0) { + response.json([]); + return; + } + + const data = response.locals.client.fetch( + '1:*', + {}, + { + // caution, BigInt can throw + changedSince: typeof since === 'string' ? BigInt(since) : undefined, + }, + ); + + const messages = []; + for await (const message of data) { + messages.push(message.seq.toString()); + } + response.json(messages); + } catch (error) { + console.error(error); + response.status(404).send(); + } +}); + +app.get('/:mailbox/:id', async (request, response) => { + try { + await response.locals.client.mailboxOpen(request.params.mailbox); + const message = await response.locals.client.fetchOne(request.params.id, { + envelope: true, + labels: true, + flags: true, + bodyStructure: true, + }); + response.json({ + bodyStructure: message.bodyStructure, + labels: [...(message.labels ?? [])], + flags: [...(message.flags ?? [])], + envelope: message.envelope, + seq: message.seq, + }); + } catch (error) { + console.error(error); + response.status(404).send(); + } +}); + +app.get('/:mailbox/:id/:part', async (request, response) => { + try { + await response.locals.client.mailboxOpen(request.params.mailbox, {readOnly: true}); + if (request.query.raw) { + const message = await response.locals.client.fetchOne(request.params.id, { + bodyParts: [`${request.params.part}.mime`, request.params.part], + }); + + response.write(message.bodyParts.get(`${request.params.part}.mime`)); + response.write(message.bodyParts.get(request.params.part)); + + response.end(); + } else { + const message = await response.locals.client.download(request.params.id, request.params.part); + message.content.on('data', chunk => { + response.write(chunk); + }); + message.content.on('end', () => { + response.end(); + }); + } + } catch (error) { + console.error(error); + response.status(404).send(); + } +}); + +app.listen(port, () => { + console.log(`Server listening on port ${port}`); +}); diff --git a/backend/mail-plugin/src/types.d.ts b/backend/mail-plugin/src/types.d.ts new file mode 100644 index 00000000..6badff66 --- /dev/null +++ b/backend/mail-plugin/src/types.d.ts @@ -0,0 +1,9 @@ +import {ImapFlow} from 'imapflow'; + +declare global { + namespace Express { + interface Locals { + client: ImapFlow; + } + } +} diff --git a/backend/mail-plugin/tsconfig.json b/backend/mail-plugin/tsconfig.json new file mode 100644 index 00000000..988e304d --- /dev/null +++ b/backend/mail-plugin/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "@openstapps/tsconfig", + "exclude": ["lib", "app.js"] +} diff --git a/frontend/app/.gitignore b/frontend/app/.gitignore index 47da5244..a140ba7c 100644 --- a/frontend/app/.gitignore +++ b/frontend/app/.gitignore @@ -32,6 +32,7 @@ platforms/ /plugins/ $RECYCLE.BIN/ dist/ +.nx/ # ignore generated resources resources/*/icon/ resources/*/splash/ diff --git a/frontend/app/package.json b/frontend/app/package.json index 26f7c7e2..9dfbc596 100644 --- a/frontend/app/package.json +++ b/frontend/app/package.json @@ -90,6 +90,7 @@ "@openstapps/core": "workspace:*", "@transistorsoft/capacitor-background-fetch": "5.2.0", "@types/dom-view-transitions": "1.0.4", + "asn1js": "3.0.5", "capacitor-secure-storage-plugin": "0.9.0", "cordova-plugin-calendar": "5.1.6", "date-fns": "3.6.0", @@ -98,6 +99,8 @@ "geojson": "0.5.0", "ionic-appauth": "0.9.0", "jsonpath-plus": "10.0.6", + "libbase64": "1.3.0", + "libqp": "2.1.0", "maplibre-gl": "4.0.2", "material-symbols": "0.17.1", "moment": "2.30.1", @@ -106,11 +109,14 @@ "ngx-markdown": "17.1.1", "ngx-moment": "6.0.2", "opening_hours": "3.8.0", + "pkijs": "3.1.0", "pmtiles": "3.0.3", + "postal-mime": "2.2.5", "rxjs": "7.8.1", "semver": "7.6.0", "swiper": "8.4.5", "tslib": "2.6.2", + "zod": "3.23.8", "zone.js": "0.14.4" }, "devDependencies": { @@ -139,9 +145,11 @@ "@ionic/cli": "7.2.0", "@openstapps/prettier-config": "workspace:*", "@openstapps/tsconfig": "workspace:*", + "@types/dompurify": "3.0.5", "@types/fontkit": "2.0.7", "@types/geojson": "1.0.6", "@types/glob": "8.1.0", + "@types/imapflow": "1.0.18", "@types/jasmine": "5.1.4", "@types/jasminewd2": "2.0.13", "@types/jsonpath": "0.2.0", @@ -154,6 +162,7 @@ "@typescript-eslint/parser": "7.2.0", "cordova-res": "0.15.4", "cypress": "13.7.0", + "dompurify": "3.1.6", "eslint": "8.57.0", "eslint-plugin-jsdoc": "48.2.1", "eslint-plugin-prettier": "5.1.3", diff --git a/frontend/app/package.json.orig b/frontend/app/package.json.orig new file mode 100644 index 00000000..58b38432 --- /dev/null +++ b/frontend/app/package.json.orig @@ -0,0 +1,214 @@ +{ + "name": "@openstapps/app", + "description": "The generic app tailored to fulfill needs of German universities, written using Ionic Framework.", + "version": "3.3.2", + "private": true, + "license": "GPL-3.0-only", + "author": "Karl-Philipp Wulfert ", + "contributors": [ + "Frank Nagel ", + "Jovan Krunić ", + "Michel Jonathan Schmitz ", + "Rainer Killinger ", + "Sebastian Lange ", + "Thea Schöbl " + ], + "scripts": { + "analyze": "webpack-bundle-analyzer www/stats.json", + "build": "pnpm check-icons && ng build --configuration=production --stats-json && webpack-bundle-analyzer www/stats.json --mode static --report www/bundle-info.html --no-open", + "build:analyze": "npm run build:stats && npm run analyze", + "build:android": "ionic capacitor build android --no-open && cd android && ./gradlew clean assemble && cd ..", + "build:prod": "ng build --configuration=production", + "build:stats": "ng build --configuration=production --stats-json", + "changelog": "conventional-changelog -p angular -i src/assets/about/CHANGELOG.md -s -r 0", + "check-icons": "node scripts/check-icon-correctness.mjs", + "chromium:no-cors": "chromium --disable-web-security --user-data-dir=\".browser-data/chromium\"", + "chromium:virtual-host": "chromium --host-resolver-rules=\"MAP mobile.app.uni-frankfurt.de:* localhost:8100\" --ignore-certificate-errors", + "cypress:open": "cypress open", + "cypress:run": "cypress run", + "docker:build": "sudo docker run -p 8100:8100 -p 35729:35729 -p 53703:53703 -v $PWD:/app -it registry.gitlab.com/openstapps/app bash -c \"npm install && npm run build\"", + "docker:build:android": "sudo docker run -p 8100:8100 -p 35729:35729 -p 53703:53703 -v $PWD:/app -it registry.gitlab.com/openstapps/app bash -c \"npm run build:android\"", + "docker:enter": "sudo docker run -p 8100:8100 -p 35729:35729 -p 53703:53703 -v $PWD:/app -it registry.gitlab.com/openstapps/app bash", + "docker:pull": "sudo docker pull registry.gitlab.com/openstapps/app", + "docker:run:android": "sudo docker run -v $PWD:/app --privileged -v /dev/bus/usb:/dev/bus/usb --net=host -it registry.gitlab.com/openstapps/app bash -c \"npm run run:android\"", + "docker:serve": "sudo docker run -p 8100:8100 -p 35729:35729 -p 53703:53703 -v $PWD:/app -it registry.gitlab.com/openstapps/app bash -c \"npm run start:external\"", + "e2e": "ng e2e", + "format": "prettier . -c", + "format:fix": "prettier --write .", + "licenses": "license-checker --json > src/assets/about/licenses.json && node ./scripts/accumulate-licenses.mjs && git add src/assets/about/licenses.json", + "lint": "ng lint && stylelint \"**/*.scss\"", + "lint:fix": "eslint --fix -c .eslintrc.json --ignore-path .eslintignore --ext .ts,.html src/ && stylelint --fix \"**/*.scss\"", + "minify-icons": "node scripts/minify-icon-font.mjs", + "postinstall": "jetify && echo \"skipping jetify in production mode\"", + "preview": "http-server www --p 8101 -o", + "push": "git push && git push origin \"v$npm_package_version\"", + "resources:ios": "capacitor-assets generate --ios --iconBackgroundColor $(grep -oE \"^@include ion-color\\(primary, #[a-fA-F0-9]{3,6}\" src/theme/colors.scss | grep -oE \"#[a-fA-F0-9]{3,6}\") --splashBackgroundColor $(grep -oE \"^@include ion-color\\(primary, #[a-fA-F0-9]{3,6}\" src/theme/colors.scss | grep -oE \"#[a-fA-F0-9]{3,6}\")", + "run:android": "ionic capacitor run android --livereload --external", + "start": "ionic serve", + "start:external": "ionic serve --external", + "start:prod": "ionic serve --prod", + "start:virtual-host": "ionic serve --public-host=mobile.app.uni-frankfurt.de --ssl=true --open=false", + "test": "ng test --code-coverage", + "test:integration": "sh integration-test.sh" + }, + "dependencies": { + "@angular/animations": "17.3.0", + "@angular/cdk": "17.3.0", + "@angular/common": "17.3.0", + "@angular/core": "17.3.0", + "@angular/forms": "17.3.0", + "@angular/platform-browser": "17.3.0", + "@angular/router": "17.3.0", + "@awesome-cordova-plugins/calendar": "6.6.0", + "@awesome-cordova-plugins/core": "6.6.0", + "@capacitor-community/screen-brightness": "6.0.0", + "@capacitor/app": "6.0.0", + "@capacitor/browser": "6.0.1", + "@capacitor/clipboard": "6.0.0", + "@capacitor/core": "6.1.1", + "@capacitor/device": "6.0.0", + "@capacitor/dialog": "6.0.0", + "@capacitor/filesystem": "6.0.0", + "@capacitor/geolocation": "6.0.0", + "@capacitor/haptics": "6.0.0", + "@capacitor/keyboard": "6.0.1", + "@capacitor/local-notifications": "6.0.0", + "@capacitor/network": "6.0.1", + "@capacitor/preferences": "6.0.1", + "@capacitor/screen-orientation": "6.0.1", + "@capacitor/share": "6.0.1", + "@capacitor/splash-screen": "6.0.1", + "@ionic-native/core": "5.36.0", + "@ionic/angular": "7.8.0", + "@ionic/storage-angular": "4.0.0", + "@maplibre/ngx-maplibre-gl": "17.4.1", + "@ngx-translate/core": "15.0.0", + "@ngx-translate/http-loader": "8.0.0", + "@openid/appauth": "1.3.1", + "@openstapps/api": "workspace:*", + "@openstapps/collection-utils": "workspace:*", + "@openstapps/core": "workspace:*", + "@transistorsoft/capacitor-background-fetch": "5.2.0", + "@types/dom-view-transitions": "1.0.4", + "asn1js": "3.0.5", + "capacitor-secure-storage-plugin": "0.9.0", + "cordova-plugin-calendar": "5.1.6", + "date-fns": "3.6.0", + "deepmerge": "4.3.1", + "form-data": "4.0.0", + "geojson": "0.5.0", + "ionic-appauth": "0.9.0", +<<<<<<< HEAD + "jsonpath-plus": "10.0.6", + "libbase64": "1.3.0", + "libqp": "2.1.0", +||||||| parent of cff9b026 (fix: pipeline) + "jsonpath-plus": "6.0.1", + "libbase64": "^1.3.0", + "libqp": "^2.1.0", +======= + "jsonpath-plus": "6.0.1", + "libbase64": "1.3.0", + "libqp": "2.1.0", +>>>>>>> cff9b026 (fix: pipeline) + "maplibre-gl": "4.0.2", + "material-symbols": "0.17.1", + "moment": "2.30.1", + "ngx-date-fns": "11.0.0", + "ngx-logger": "5.0.12", + "ngx-markdown": "17.1.1", + "ngx-moment": "6.0.2", + "opening_hours": "3.8.0", + "pkijs": "3.1.0", + "pmtiles": "3.0.3", + "postal-mime": "2.2.5", + "rxjs": "7.8.1", + "semver": "7.6.0", + "swiper": "8.4.5", + "tslib": "2.6.2", + "zod": "3.23.8", + "zone.js": "0.14.4" + }, + "devDependencies": { + "@angular-devkit/architect": "0.1703.0", + "@angular-devkit/build-angular": "17.3.0", + "@angular-devkit/core": "17.3.0", + "@angular-devkit/schematics": "17.3.0", + "@angular-eslint/builder": "17.3.0", + "@angular-eslint/eslint-plugin": "17.3.0", + "@angular-eslint/eslint-plugin-template": "17.3.0", + "@angular-eslint/schematics": "17.3.0", + "@angular-eslint/template-parser": "17.3.0", + "@angular/cli": "17.3.0", + "@angular/compiler": "17.3.0", + "@angular/compiler-cli": "17.3.0", + "@angular/language-server": "17.3.0", + "@angular/language-service": "17.3.0", + "@angular/platform-browser-dynamic": "17.3.0", + "@capacitor/android": "6.1.1", + "@capacitor/assets": "3.0.4", + "@capacitor/cli": "6.1.1", + "@capacitor/ios": "6.1.1", + "@compodoc/compodoc": "1.1.23", + "@cypress/schematic": "2.5.1", + "@ionic/angular-toolkit": "11.0.1", + "@ionic/cli": "7.2.0", + "@openstapps/prettier-config": "workspace:*", + "@openstapps/tsconfig": "workspace:*", + "@types/dompurify": "3.0.5", + "@types/fontkit": "2.0.7", + "@types/geojson": "1.0.6", + "@types/glob": "8.1.0", + "@types/imapflow": "1.0.18", + "@types/jasmine": "5.1.4", + "@types/jasminewd2": "2.0.13", + "@types/jsonpath": "0.2.0", + "@types/karma": "6.3.8", + "@types/karma-coverage": "2.0.3", + "@types/karma-jasmine": "4.0.5", + "@types/node": "18.15.3", + "@types/semver": "7.5.8", + "@typescript-eslint/eslint-plugin": "7.2.0", + "@typescript-eslint/parser": "7.2.0", + "cordova-res": "0.15.4", + "cypress": "13.7.0", + "dompurify": "3.1.6", + "eslint": "8.57.0", + "eslint-plugin-jsdoc": "48.2.1", + "eslint-plugin-prettier": "5.1.3", + "eslint-plugin-unicorn": "51.0.1", + "fast-deep-equal": "3.1.3", + "fontkit": "2.0.2", + "glob": "10.3.10", + "http-server": "14.1.1", + "is-docker": "2.2.1", + "jasmine-core": "5.1.2", + "jasmine-spec-reporter": "7.0.0", + "jetifier": "2.0.0", + "junit-report-merger": "6.0.3", + "karma": "6.4.3", + "karma-chrome-launcher": "3.2.0", + "karma-coverage": "2.2.1", + "karma-jasmine": "5.1.0", + "karma-junit-reporter": "2.0.1", + "karma-mocha-reporter": "2.2.5", + "license-checker": "25.0.1", + "stylelint": "16.3.1", + "stylelint-config-clean-order": "5.4.1", + "stylelint-config-prettier-scss": "1.0.0", + "stylelint-config-recommended-scss": "14.0.0", + "stylelint-config-standard-scss": "13.0.0", + "surge": "0.23.1", + "ts-node": "10.9.2", + "typescript": "5.4.2", + "webpack-bundle-analyzer": "4.10.1" + }, + "cordova": { + "plugins": {}, + "platforms": [ + "ios", + "browser", + "android" + ] + } +} diff --git a/frontend/app/src/app/app.module.ts b/frontend/app/src/app/app.module.ts index 80c0fcd6..f6bba3ab 100644 --- a/frontend/app/src/app/app.module.ts +++ b/frontend/app/src/app/app.module.ts @@ -15,12 +15,11 @@ import {CommonModule, LocationStrategy, PathLocationStrategy, registerLocaleData} from '@angular/common'; import {HTTP_INTERCEPTORS, HttpClient, HttpClientModule} from '@angular/common/http'; import localeDe from '@angular/common/locales/de'; -import {APP_INITIALIZER, NgModule} from '@angular/core'; +import {APP_INITIALIZER, Injectable, NgModule} from '@angular/core'; import {BrowserModule} from '@angular/platform-browser'; import {RouteReuseStrategy} from '@angular/router'; import {IonicModule, IonicRouteStrategy, Platform} from '@ionic/angular'; import {TranslateLoader, TranslateModule, TranslateService} from '@ngx-translate/core'; -import {TranslateHttpLoader} from '@ngx-translate/http-loader'; import moment from 'moment'; import 'moment/min/locales'; import {LoggerModule, NGXLogger, NgxLoggerLevel} from 'ngx-logger'; @@ -71,6 +70,8 @@ import {Capacitor} from '@capacitor/core'; import {SplashScreen} from '@capacitor/splash-screen'; import maplibregl from 'maplibre-gl'; import {Protocol} from 'pmtiles'; +import {MailModule} from './modules/mail/mail.module'; +import {Observable, from} from 'rxjs'; registerLocaleData(localeDe); @@ -129,12 +130,16 @@ export function initializerFactory( }; } -/** - * TODO - * @param http TODO - */ -export function createTranslateLoader(http: HttpClient) { - return new TranslateHttpLoader(http, './assets/i18n/', '.json'); +@Injectable({providedIn: 'root'}) +export class ImportTranslateLoader { + static translations: Record Promise<{default: object}>> = { + de: () => import('../assets/i18n/de.json'), + en: () => import('../assets/i18n/en.json'), + }; + + getTranslation(lang: string): Observable { + return from(ImportTranslateLoader.translations[lang]().then(it => it.default)); + } } /** @@ -165,6 +170,7 @@ export function createTranslateLoader(http: HttpClient) { ProfilePageModule, FeedbackModule, MapModule, + MailModule, MenuModule, NavigationModule, NewsModule, @@ -177,7 +183,7 @@ export function createTranslateLoader(http: HttpClient) { loader: { deps: [HttpClient], provide: TranslateLoader, - useFactory: createTranslateLoader, + useClass: ImportTranslateLoader, }, }), UtilModule, diff --git a/frontend/app/src/app/modules/mail/mail-adapter.service.ts b/frontend/app/src/app/modules/mail/mail-adapter.service.ts new file mode 100644 index 00000000..6b93b46f --- /dev/null +++ b/frontend/app/src/app/modules/mail/mail-adapter.service.ts @@ -0,0 +1,237 @@ +import {HttpClient} from '@angular/common/http'; +import {Injectable, inject} from '@angular/core'; +import {Observable, map, catchError, tap, mergeMap, forkJoin, of} from 'rxjs'; +import {Email, MailboxTreeRoot, RawEmail, RawEmailBodyStructure, Signature, SignedValue} from './schema'; +import {ContentInfo, SignedData} from 'pkijs'; +import PostalMime from 'postal-mime'; +import {z} from 'zod'; + +function value(value: undefined): undefined; +function value(value: T): SignedValue; +/** + * + */ +function value(value: T | undefined): SignedValue | undefined { + return value === undefined ? undefined : {value}; +} + +@Injectable({providedIn: 'root'}) +export class MailAdapterService { + httpClient = inject(HttpClient); + + request(options: { + method?: string; + path?: string[]; + options?: Record; + responseType?: 'json' | 'arraybuffer'; + credentials?: string; + }): Observable { + return this.httpClient.request( + options.method ?? 'GET', + `https://cumulet.rz.uni-frankfurt.de/${options.path?.map(encodeURIComponent).join('/') ?? ''}${ + options.options + ? `?${Object.entries(options.options).map(item => item.map(encodeURIComponent).join('='))}` + : '' + }`, + { + responseType: options.responseType as 'json', + headers: options.credentials ? {authorization: `Basic ${options.credentials}`} : undefined, + }, + ); + } + + checkCredentials(credentials: string): Observable { + return this.request({ + path: [], + options: {}, + responseType: 'json', + credentials, + }).pipe( + map(() => true), + catchError(error => { + if (error.status === 401) { + return of(false); + } else { + throw error; + } + }), + ); + } + + listMailboxes(credentials: string): Observable { + return this.request({credentials}).pipe(mergeMap(it => MailboxTreeRoot.parseAsync(it))); + } + + listRawEmails(credentials: string, mailbox: string, since?: string): Observable { + return this.request({ + credentials, + path: [mailbox], + options: since === undefined ? undefined : {since}, + }).pipe( + mergeMap(it => z.array(z.string()).parseAsync(it)), + tap(console.log), + ); + } + + private getRawEmail(credentials: string, mailbox: string, id: string): Observable { + return this.request({ + credentials, + path: [mailbox, id], + options: {raw: 'true'}, + }).pipe(mergeMap(it => RawEmail.parseAsync(it))); + } + + private getPart(credentials: string, mailbox: string, id: string, part: string): Observable { + return this.request({path: [mailbox, id, part], credentials, responseType: 'arraybuffer'}); + } + + private getRawPart( + credentials: string, + mailbox: string, + id: string, + part: string, + ): Observable { + return this.request({ + path: [mailbox, id, part], + options: {raw: 'true'}, + responseType: 'arraybuffer', + credentials, + }); + } + + private resolveRawEmail(credentials: string, mailbox: string, email: RawEmail): Observable { + console.log(email); + + if ( + email.bodyStructure.type === 'application/x-pkcs7-mime' || + email.bodyStructure.type === 'application/pkcs7-mime' + ) { + return this.getRawPart(credentials, mailbox, email.seq, email.bodyStructure.part ?? 'TEXT').pipe( + mergeMap(async buffer => { + const info = ContentInfo.fromBER(buffer); + const signedData = new SignedData({schema: info.content}); + const valid = await signedData + .verify({signer: 0, data: signedData.encapContentInfo.eContent?.valueBeforeDecodeView}) + .catch(() => false); + const content = new TextDecoder().decode( + signedData.encapContentInfo.eContent?.valueBeforeDecodeView, + ); + const signedEmail = await PostalMime.parse(content); + + function signed(value: undefined): undefined; + function signed(value: T): SignedValue; + /** + * + */ + function signed(value: T | undefined): SignedValue | undefined { + return value === undefined + ? undefined + : { + value, + signature: { + type: 'pkcs7', + valid, + }, + }; + } + + const result: Email = { + id: email.seq, + mailbox, + subject: signedEmail.subject ? signed(signedEmail.subject) : value(email.envelope.subject), + flags: new Set(), //TODO + from: signed({ + name: signedEmail.from.name || undefined, + address: signedEmail.from.address || undefined, + }), + to: signedEmail.to?.map(({name, address}) => + signed({ + name, + address, + }), + ), + date: signedEmail.date ? signed(new Date(signedEmail.date)) : value(email.envelope.date), + html: signedEmail.html ? signed(signedEmail.html) : undefined, + text: signedEmail.text ? signed(signedEmail.text) : undefined, + attachments: [], + }; + + return result; + }), + ); + } + + const traverse = ( + item: RawEmailBodyStructure, + result: Pick, + signature?: Signature, + ): Observable> => { + // https://datatracker.ietf.org/doc/html/rfc1847#section-2.1 + if (item.type === 'multipart/signed' && item.parameters?.protocol === 'application/pkcs7-signature') { + return forkJoin({ + data: this.getPart(credentials, mailbox, email.seq, item.childNodes![0].part!), + signature: this.getRawPart(credentials, mailbox, email.seq, item.childNodes![1].part!), + }).pipe( + mergeMap(({data, signature}) => { + const info = ContentInfo.fromBER(signature); + const signedData = new SignedData({schema: info.content}); + return signedData.verify({signer: 0, data}); + }), + catchError(error => { + console.log(error); + return of(false); + }), + mergeMap(valid => traverse(item.childNodes![0], result, {type: 'pkcs7', valid})), + ); + } else if (item.type.startsWith('multipart/')) { + return forkJoin(item.childNodes!.map(child => traverse(child, result, signature))).pipe( + map(children => children[0]), + ); + } else if (item.type === 'text/plain') { + return this.getPart(credentials, mailbox, email.seq, item.part ?? 'TEXT').pipe( + map(text => { + result.html = {value: new TextDecoder().decode(text), signature}; + return result; + }), + ); + } else if (item.type === 'text/html') { + return this.getPart(credentials, mailbox, email.seq, item.part ?? 'TEXT').pipe( + map(html => { + result.html = {value: new TextDecoder().decode(html), signature}; + return result; + }), + ); + } else if (item.part === undefined) { + return of(result); + } else { + result.attachments.push({value: {part: item.part, size: item.size ?? Number.NaN, filename: ''}}); + return of(result); + } + }; + + return traverse(email.bodyStructure, {attachments: []}).pipe( + map( + partial => + ({ + ...partial, + id: email.seq, + mailbox, + flags: new Set(email.flags), + subject: value(email.envelope.subject), + from: value({ + name: email.envelope.from[0]?.name || undefined, + address: email.envelope.from[0]?.address || undefined, + }), + date: value(email.envelope.date), + }) satisfies Email, + ), + tap(console.log), + ); + } + + getEmail(credentials: string, mailbox: string, id: string): Observable { + return this.getRawEmail(credentials, mailbox, id).pipe( + mergeMap(it => this.resolveRawEmail(credentials, mailbox, it)), + ); + } +} diff --git a/frontend/app/src/app/modules/mail/mail-detail.component.ts b/frontend/app/src/app/modules/mail/mail-detail.component.ts new file mode 100644 index 00000000..0f97446c --- /dev/null +++ b/frontend/app/src/app/modules/mail/mail-detail.component.ts @@ -0,0 +1,58 @@ +import {ChangeDetectionStrategy, Component, inject, signal} from '@angular/core'; +import {AsyncPipe, TitleCasePipe} from '@angular/common'; +import {IonicModule} from '@ionic/angular'; +import {DataModule} from '../data/data.module'; +import {IonIconModule} from 'src/app/util/ion-icon/ion-icon.module'; +import {UtilModule} from 'src/app/util/util.module'; +import {FormatPurePipeModule, ParseIsoPipeModule} from 'ngx-date-fns'; +import {ActivatedRoute, RouterModule} from '@angular/router'; +import {map, mergeMap} from 'rxjs'; +import {DomSanitizer} from '@angular/platform-browser'; +import {materialFade} from 'src/app/animation/material-motion'; +import {TranslateModule} from '@ngx-translate/core'; +import {ShadowHtmlDirective} from 'src/app/util/shadow-html.directive'; +import {MailStorageProvider} from './mail-storage.provider'; + +@Component({ + selector: 'stapps-mail-detail', + templateUrl: 'mail-detail.html', + styleUrl: 'mail-detail.scss', + animations: [materialFade], + standalone: true, + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [ + AsyncPipe, + IonicModule, + DataModule, + IonIconModule, + UtilModule, + FormatPurePipeModule, + ParseIsoPipeModule, + RouterModule, + ShadowHtmlDirective, + TranslateModule, + TitleCasePipe, + ], +}) +export class MailDetailComponent { + readonly activatedRoute = inject(ActivatedRoute); + + readonly mailStorage = inject(MailStorageProvider); + + readonly sanitizer = inject(DomSanitizer); + + parameters = this.activatedRoute.paramMap.pipe( + map(parameters => ({ + mailbox: parameters.get('mailbox')!, + id: parameters.get('id')!, + })), + ); + + mail = this.parameters.pipe(mergeMap(({mailbox, id}) => this.mailStorage.getEmail(mailbox, id))); + + collapse = signal(false); + + todo() { + alert('TODO'); + } +} diff --git a/frontend/app/src/app/modules/mail/mail-detail.html b/frontend/app/src/app/modules/mail/mail-detail.html new file mode 100644 index 00000000..079cc993 --- /dev/null +++ b/frontend/app/src/app/modules/mail/mail-detail.html @@ -0,0 +1,93 @@ + + + + + + + @if (mail | async; as mail) { + {{ mail.subject?.value }} + } @else { + + } + + + + + + + + + + + + + + @if (mail | async; as mail) { +

{{ mail.subject?.value }}

+ + @if (mail.html) { +
+
+
+ } @else if (mail.text) { +
+
{{ mail.text.value }}
+
+ } + + @for (attachment of mail.attachments; track attachment) { + + + {{ attachment.value.filename }} + {{ attachment.value.size }} + + + + + + + + } + +
+
+ + @if (mail.date) { + + + + + } +
{{ 'mail.DATE' | translate | titlecase }} + +
+
+
+ } +
diff --git a/frontend/app/src/app/modules/mail/mail-detail.scss b/frontend/app/src/app/modules/mail/mail-detail.scss new file mode 100644 index 00000000..0f19be5d --- /dev/null +++ b/frontend/app/src/app/modules/mail/mail-detail.scss @@ -0,0 +1,97 @@ +@import '../../../theme/util/mixins'; + +ion-item { + margin-block-end: var(--spacing-xl); +} + +ion-title { + transition: + opacity 0.2s ease, + translate 0.2s ease; +} + +h1 { + margin: var(--spacing-sm) var(--spacing-md); + font-weight: var(--font-weight-bold); + color: var(--ion-color-primary-contrast); +} + +aside { + display: flex; + flex-direction: column; + margin: var(--spacing-xs) var(--spacing-md); + color: var(--ion-color-primary-contrast); +} + +.to { + display: flex; + gap: var(--spacing-xs); + align-items: center; + justify-content: flex-start; + + > span:has(+ span)::after { + content: ','; + } +} + +.from { + display: flex; + gap: var(--spacing-xs); + align-items: center; + justify-content: flex-start; +} + +main { + @include border-radius-in-parallax(var(--border-radius-default)); + + margin: var(--spacing-md); + padding: var(--spacing-md); + background: var(--ion-item-background); +} + +.html { + overflow-x: auto; +} + +pre { + font-family: inherit; + word-wrap: break-word; + white-space: pre-wrap; +} + +footer { + // css hack to make the element stick to the bottom of the scroll container even + // when the content is not filling it + position: sticky; + top: 100vh; + + > div { + margin: var(--spacing-lg); + opacity: 0.8; + } + + td { + padding-inline-start: var(--spacing-md); + word-break: break-word; + } + + code { + font-size: inherit; + } +} + +.attachment { + display: flex; + align-items: center; + justify-content: space-between; + + margin: var(--spacing-md) 0; + padding: var(--spacing-md); + + border: 1px solid var(--ion-border-color); + border-radius: var(--border-radius-default); +} + +ion-content::part(background) { + background: none; +} diff --git a/frontend/app/src/app/modules/mail/mail-login.component.ts b/frontend/app/src/app/modules/mail/mail-login.component.ts new file mode 100644 index 00000000..ecb5adc3 --- /dev/null +++ b/frontend/app/src/app/modules/mail/mail-login.component.ts @@ -0,0 +1,65 @@ +import {AsyncPipe, TitleCasePipe} from '@angular/common'; +import {ChangeDetectionStrategy, Component, WritableSignal, computed, inject, signal} from '@angular/core'; +import {FormsModule} from '@angular/forms'; +import {IonicModule} from '@ionic/angular'; +import {TranslateModule} from '@ngx-translate/core'; +import {IonIconModule} from 'src/app/util/ion-icon/ion-icon.module'; +import {UtilModule} from 'src/app/util/util.module'; +import {MailService} from './mail.service'; +import {Observable, of, map, catchError, startWith, shareReplay, tap, take} from 'rxjs'; +import {ActivatedRoute, Router} from '@angular/router'; + +@Component({ + selector: 'stapps-mail-login', + templateUrl: 'mail-login.html', + styleUrl: 'mail-login.scss', + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, + imports: [IonicModule, UtilModule, TranslateModule, TitleCasePipe, IonIconModule, FormsModule, AsyncPipe], +}) +export class MailLoginComponent { + showPassword = signal(false); + + email = signal(''); + + password = signal(''); + + // eslint-disable-next-line unicorn/no-useless-undefined + error: WritableSignal> = signal(of(undefined)); + + loading = computed(() => + this.error().pipe( + map(() => false), + startWith(true), + ), + ); + + mailService = inject(MailService); + + router = inject(Router); + + activatedRoute = inject(ActivatedRoute); + + submit(event: SubmitEvent) { + event.preventDefault(); + const form = event.target as HTMLFormElement; + if (form.checkValidity()) { + this.error.set( + this.mailService.login(this.email(), this.password()).pipe( + tap(success => { + if (success) { + this.activatedRoute.data.pipe(take(1)).subscribe(data => { + this.router.navigate(data.redirectTo); + }); + } + }), + map(success => (success ? undefined : 'mail.login.error.INVALID_CREDENTIALS')), + catchError(error => of(error.message)), + shareReplay(1), + ), + ); + } else { + form.reportValidity(); + } + } +} diff --git a/frontend/app/src/app/modules/mail/mail-login.html b/frontend/app/src/app/modules/mail/mail-login.html new file mode 100644 index 00000000..9541dffa --- /dev/null +++ b/frontend/app/src/app/modules/mail/mail-login.html @@ -0,0 +1,67 @@ + + + + + + + + + +

{{ 'mail.login.TITLE' | translate | titlecase }}

+
+ + + + + + + + + + @if (error() | async; as error) { + {{ error | translate | titlecase }} + } + + + {{ 'mail.login.LOGIN' | translate | titlecase }} + +
+
diff --git a/frontend/app/src/app/modules/mail/mail-login.scss b/frontend/app/src/app/modules/mail/mail-login.scss new file mode 100644 index 00000000..16b401e9 --- /dev/null +++ b/frontend/app/src/app/modules/mail/mail-login.scss @@ -0,0 +1,17 @@ +h1 { + margin-inline: auto; + color: var(--ion-color-primary-contrast); +} + +form { + display: flex; + flex-direction: column; + align-items: flex-end; + + max-width: 30em; + margin: var(--spacing-xxl) auto; + padding: var(--spacing-xl); + + background: var(--ion-item-background); + border-radius: var(--border-radius-default); +} diff --git a/frontend/app/src/app/modules/mail/mail-page.component.ts b/frontend/app/src/app/modules/mail/mail-page.component.ts new file mode 100644 index 00000000..2e53b0e8 --- /dev/null +++ b/frontend/app/src/app/modules/mail/mail-page.component.ts @@ -0,0 +1,60 @@ +import {ChangeDetectionStrategy, Component, inject} from '@angular/core'; +import {MailService} from './mail.service'; +import {AsyncPipe, TitleCasePipe} from '@angular/common'; +import {IonicModule} from '@ionic/angular'; +import {DataModule} from '../data/data.module'; +import {IonIconModule} from 'src/app/util/ion-icon/ion-icon.module'; +import {UtilModule} from 'src/app/util/util.module'; +import {FormatPurePipeModule, IsTodayPipeModule} from 'ngx-date-fns'; +import {ActivatedRoute, RouterModule} from '@angular/router'; +import {map, mergeMap} from 'rxjs'; +import {MailStorageProvider} from './mail-storage.provider'; +import {MailboxTreeItem} from './schema'; +import {SCIcon} from 'src/app/util/ion-icon/icon'; +import {TranslateModule} from '@ngx-translate/core'; + +@Component({ + selector: 'stapps-mail-page', + templateUrl: 'mail-page.html', + styleUrl: 'mail-page.scss', + standalone: true, + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [ + AsyncPipe, + IonicModule, + DataModule, + IonIconModule, + UtilModule, + FormatPurePipeModule, + IsTodayPipeModule, + RouterModule, + TranslateModule, + TitleCasePipe, + ], +}) +export class MailPageComponent { + readonly activatedRoute = inject(ActivatedRoute); + + readonly mailService = inject(MailService); + + readonly mailStorage = inject(MailStorageProvider); + + mailIcons: Record, keyof typeof SCIcon> = { + '\\Inbox': SCIcon.inbox, + '\\All': SCIcon.all_inbox, + '\\Archive': SCIcon.archive, + '\\Drafts': SCIcon.drafts, + '\\Flagged': SCIcon.flag, + '\\Junk': SCIcon.folder, + '\\Sent': SCIcon.send, + '\\Trash': SCIcon.delete, + }; + + mailbox = this.activatedRoute.paramMap.pipe( + mergeMap(parameters => + this.mailStorage.mailboxes.pipe( + map(mailboxes => mailboxes.folders.find(it => it.path === parameters.get('mailbox')!)!), + ), + ), + ); +} diff --git a/frontend/app/src/app/modules/mail/mail-page.html b/frontend/app/src/app/modules/mail/mail-page.html new file mode 100644 index 00000000..992fa438 --- /dev/null +++ b/frontend/app/src/app/modules/mail/mail-page.html @@ -0,0 +1,45 @@ + + + + + + + + Mail + + + + + + + @if (mailStorage.mailboxes | async; as mailboxes) { + + @for (folder of mailboxes.folders; track folder) { + + @if (folder.specialUse) { + + {{ 'mail.mailboxes.' + folder.specialUse | translate | titlecase }} + } @else { + + {{ folder.name }} + } + + } + + } + + + + + diff --git a/frontend/app/src/app/modules/mail/mail-page.scss b/frontend/app/src/app/modules/mail/mail-page.scss new file mode 100644 index 00000000..8052908e --- /dev/null +++ b/frontend/app/src/app/modules/mail/mail-page.scss @@ -0,0 +1,26 @@ +ion-list { + margin: var(--spacing-md); + border-radius: var(--border-radius-default); +} + +ion-split-pane { + --border: none; +} + +ion-menu, +ion-menu::part(container), +ion-menu::part(backdrop) { + background: none; +} + +ion-menu:not(.menu-pane-visible) { + &::part(container) { + box-shadow: none; + } + + > ion-list { + height: 100%; + margin-inline-end: var(--spacing-xl); + box-shadow: rgba(0 0 0 / 18%) 4px 0 16px; + } +} diff --git a/frontend/app/src/app/modules/mail/mail-storage.provider.ts b/frontend/app/src/app/modules/mail/mail-storage.provider.ts new file mode 100644 index 00000000..896901c9 --- /dev/null +++ b/frontend/app/src/app/modules/mail/mail-storage.provider.ts @@ -0,0 +1,243 @@ +/* eslint-disable unicorn/no-useless-undefined */ +import {Injectable, inject} from '@angular/core'; +import {Capacitor} from '@capacitor/core'; +import {SecureStoragePlugin} from 'capacitor-secure-storage-plugin'; +import { + BehaviorSubject, + Observable, + catchError, + defer, + distinctUntilChanged, + filter, + from, + fromEvent, + map, + merge, + mergeMap, + of, + shareReplay, + startWith, + take, + firstValueFrom, + Subject, +} from 'rxjs'; +import {Email, EmailMeta, MailboxTreeRoot} from './schema'; +import equal from 'fast-deep-equal'; +import {MailAdapterService} from './mail-adapter.service'; +import {StorageProvider} from '../storage/storage.provider'; + +@Injectable({providedIn: 'root'}) +export class MailStorageProvider { + static readonly DB_NAME = 'mail'; + + static readonly MAILBOX_STORE_NAME = 'mailboxes'; + + static readonly EMAIL_STORE_NAME = 'emails'; + + static readonly CREDENTIALS_KEY = 'email-credentials'; + + static readonly EMAIL_MAILBOX_INDEX = 'email'; + + static readonly EMAIL_FLAGS_INDEX = 'flags'; + + static readonly EMAIL_DATE_INDEX = 'date'; + + storageProvider = inject(StorageProvider); + + mailAdapter = inject(MailAdapterService); + + database = defer(() => { + const request = indexedDB.open(MailStorageProvider.DB_NAME, 1); + return merge( + fromEvent(request, 'upgradeneeded').pipe( + map(event => { + const database = (event.target as IDBOpenDBRequest).result; + const mailStore = database.createObjectStore(MailStorageProvider.EMAIL_STORE_NAME, { + keyPath: ['id', 'mailbox'], + }); + mailStore.createIndex(MailStorageProvider.EMAIL_MAILBOX_INDEX, 'mailbox', {unique: false}); + mailStore.createIndex(MailStorageProvider.EMAIL_FLAGS_INDEX, 'flags', { + unique: false, + multiEntry: true, + }); + mailStore.createIndex(MailStorageProvider.EMAIL_DATE_INDEX, 'date', {unique: false}); + return database; + }), + ), + fromEvent(request, 'success').pipe( + take(1), + map(event => (event.target as IDBOpenDBRequest).result), + ), + fromEvent(request, 'error').pipe( + take(1), + map(event => { + throw (event.target as IDBOpenDBRequest).error; + }), + ), + fromEvent(request, 'blocked').pipe( + take(1), + map(() => { + throw new Error('Database blocked'); + }), + ), + ); + }).pipe(shareReplay(1)); + + private mailboxesChanged = new BehaviorSubject(undefined); + + mailboxes: Observable = this.mailboxesChanged.pipe( + mergeMap(() => + this.storageProvider + .get(MailStorageProvider.MAILBOX_STORE_NAME) + .catch(() => undefined!), + ), + filter(it => it !== undefined), + distinctUntilChanged((a, b) => equal(a, b)), + shareReplay(1), + ); + + async setMailboxes(root: MailboxTreeRoot | undefined): Promise { + await this.storageProvider.put(MailStorageProvider.MAILBOX_STORE_NAME, root); + this.mailboxesChanged.next(); + } + + private credentialsChanged = new BehaviorSubject(undefined); + + credentials: Observable = this.credentialsChanged.pipe( + mergeMap(() => { + return Capacitor.isNativePlatform() + ? from(SecureStoragePlugin.get({key: MailStorageProvider.CREDENTIALS_KEY})).pipe( + map(({value}) => value), + catchError(() => of(undefined)), + ) + : of(localStorage.getItem(MailStorageProvider.CREDENTIALS_KEY) ?? undefined); + }), + ); + + async setCredentials(credentials: string | undefined): Promise { + if (Capacitor.isNativePlatform()) { + await (credentials === undefined + ? SecureStoragePlugin.remove({key: MailStorageProvider.CREDENTIALS_KEY}) + : SecureStoragePlugin.set({key: MailStorageProvider.CREDENTIALS_KEY, value: credentials})); + } else { + if (credentials === undefined) { + localStorage.removeItem(MailStorageProvider.CREDENTIALS_KEY); + } else { + localStorage.setItem(MailStorageProvider.CREDENTIALS_KEY, credentials); + } + } + this.credentialsChanged.next(); + } + + private emailChanged = new Subject>(); + + private mailboxContentChanged = new Subject>(); + + getEmails(mailbox: string): Observable { + return this.mailboxContentChanged.pipe( + filter(it => it.has(mailbox)), + startWith(undefined), + mergeMap(() => this.database), + mergeMap(database => { + return defer(() => { + const transaction = database.transaction([MailStorageProvider.EMAIL_STORE_NAME], 'readonly'); + const store = transaction.objectStore(MailStorageProvider.EMAIL_STORE_NAME); + + const index = store.index(MailStorageProvider.EMAIL_MAILBOX_INDEX); + const request = index.getAll(IDBKeyRange.only(mailbox)); + + return merge( + fromEvent(request, 'success').pipe(map(() => request.result as Array)), + fromEvent(request, 'error').pipe( + map(event => { + throw (event.target as IDBRequest).error; + }), + ), + ).pipe(take(1)); + }); + }), + map, EmailMeta[]>(emails => + emails + .map(email => ({id: email.id, mailbox: email.mailbox, incomplete: true}) satisfies EmailMeta) + .sort((a, b) => Number(b.id) - Number(a.id)), + ), + shareReplay(1), + ); + } + + getEmail(mailbox: string, id: string): Observable { + return this.emailChanged.pipe( + filter(it => it.has(JSON.stringify([id, mailbox]))), + startWith(undefined), + mergeMap(() => this.database), + mergeMap(database => { + return defer(() => { + const transaction = database.transaction([MailStorageProvider.EMAIL_STORE_NAME], 'readonly'); + const store = transaction.objectStore(MailStorageProvider.EMAIL_STORE_NAME); + + const request = store.get([id, mailbox]); + + return merge( + fromEvent(request, 'success').pipe(map(() => request.result as EmailMeta | Email)), + fromEvent(request, 'error').pipe( + map(event => { + throw (event.target as IDBRequest).error; + }), + ), + ).pipe(take(1)); + }); + }), + mergeMap(email => + 'incomplete' in email + ? this.credentials.pipe( + filter(it => it !== undefined), + take(1), + mergeMap(credentials => + this.mailAdapter.getEmail(credentials!, mailbox, id).pipe( + mergeMap(async email => { + console.log('fetiching'); + await this.setEmail(email, true); + console.log('done'); + return email; + }), + ), + ), + ) + : of(email), + ), + shareReplay(1), + ); + } + + async setEmail(email: Email | EmailMeta | Array, quiet = false): Promise { + const database = await firstValueFrom(this.database); + const transaction = database.transaction([MailStorageProvider.EMAIL_STORE_NAME], 'readwrite'); + const store = transaction.objectStore(MailStorageProvider.EMAIL_STORE_NAME); + + const mailboxesAffected = new Set(); + const emailsAffected = new Set(); + + for (const it of Array.isArray(email) ? email : [email]) { + mailboxesAffected.add(it.mailbox); + emailsAffected.add(JSON.stringify([it.id, it.mailbox])); + store.put(it); + } + + await firstValueFrom( + merge( + fromEvent(transaction, 'complete').pipe( + map(() => { + if (!quiet) { + this.emailChanged.next(emailsAffected); + } + }), + ), + fromEvent(transaction, 'error').pipe( + map(event => { + throw (event.target as IDBRequest).error; + }), + ), + ), + ); + } +} diff --git a/frontend/app/src/app/modules/mail/mail.module.ts b/frontend/app/src/app/modules/mail/mail.module.ts new file mode 100644 index 00000000..9d9d991a --- /dev/null +++ b/frontend/app/src/app/modules/mail/mail.module.ts @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2024 StApps + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ +import {Router, RouterModule} from '@angular/router'; +import {NgModule, inject} from '@angular/core'; +import {MailService} from './mail.service'; +import {map, take} from 'rxjs'; + +function mailLoginGuard() { + const router = inject(Router); + return inject(MailService).isLoggedIn.pipe( + map(isLoggedIn => (isLoggedIn ? true : router.createUrlTree(['/mail-login']))), + take(1), + ); +} + +@NgModule({ + imports: [ + RouterModule.forChild([ + { + path: 'mail-login', + data: {redirectTo: ['/mail']}, + loadComponent: () => import('./mail-login.component').then(m => m.MailLoginComponent), + }, + { + path: 'mail', + loadComponent: () => import('./mail-page.component').then(m => m.MailPageComponent), + canActivate: [mailLoginGuard], + canActivateChild: [mailLoginGuard], + children: [ + { + path: '', + redirectTo: 'INBOX', + pathMatch: 'full', + }, + { + path: ':mailbox', + loadComponent: () => import('./mailbox-page.component').then(m => m.MailboxPageComponent), + }, + { + path: ':mailbox/:id', + loadComponent: () => import('./mail-detail.component').then(m => m.MailDetailComponent), + }, + ], + }, + ]), + ], +}) +export class MailModule {} diff --git a/frontend/app/src/app/modules/mail/mail.pipe.ts b/frontend/app/src/app/modules/mail/mail.pipe.ts new file mode 100644 index 00000000..465bf766 --- /dev/null +++ b/frontend/app/src/app/modules/mail/mail.pipe.ts @@ -0,0 +1,17 @@ +import {Pipe, PipeTransform, inject} from '@angular/core'; +import {Observable} from 'rxjs'; +import {MailStorageProvider} from './mail-storage.provider'; +import {Email} from './schema'; + +@Pipe({ + name: 'mail', + pure: true, + standalone: true, +}) +export class MailPipe implements PipeTransform { + mailStorage = inject(MailStorageProvider); + + transform(value: {mailbox: string; id: string}): Observable { + return this.mailStorage.getEmail(value.mailbox, value.id); + } +} diff --git a/frontend/app/src/app/modules/mail/mail.service.ts b/frontend/app/src/app/modules/mail/mail.service.ts new file mode 100644 index 00000000..67c38f65 --- /dev/null +++ b/frontend/app/src/app/modules/mail/mail.service.ts @@ -0,0 +1,84 @@ +import {Injectable} from '@angular/core'; +import { + Observable, + map, + mergeMap, + filter, + from, + combineLatest, + tap, + of, + catchError, + BehaviorSubject, +} from 'rxjs'; +import {MailStorageProvider} from './mail-storage.provider'; +import {MailAdapterService} from './mail-adapter.service'; +import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; +import {EmailMeta} from './schema'; + +@Injectable({providedIn: 'root'}) +export class MailService { + isLoggedIn = this.mailStorage.credentials.pipe(map(it => it !== undefined)); + + manualSync = new BehaviorSubject(undefined); + + constructor( + private mailStorage: MailStorageProvider, + private mailAdapter: MailAdapterService, + ) { + this.mailStorage.credentials + .pipe( + takeUntilDestroyed(), + mergeMap(credentials => this.manualSync.pipe(map(() => credentials))), + mergeMap(credentials => { + return credentials === undefined + ? of() + : this.mailAdapter + .listMailboxes(credentials) + .pipe(mergeMap(mailboxes => this.mailStorage.setMailboxes(mailboxes))); + }), + ) + .subscribe(() => {}); + combineLatest([ + this.mailStorage.credentials.pipe(filter(it => it !== undefined)), + this.mailStorage.mailboxes, + ]) + .pipe( + takeUntilDestroyed(), + mergeMap(([credentials, mailboxes]) => + from(mailboxes.folders).pipe( + mergeMap(async mailbox => { + return this.mailAdapter.listRawEmails(credentials!, mailbox.path).pipe( + map(emails => + emails.map(it => ({id: it, mailbox: mailbox.path, incomplete: true}) satisfies EmailMeta), + ), + catchError(error => { + console.error(error); + return of(); + }), + ); + }), + mergeMap(emails => emails), + ), + ), + mergeMap(emails => this.mailStorage.setEmail(emails)), + ) + .subscribe(() => {}); + } + + login(username: string, password: string): Observable { + const credentials = btoa(`${username}:${password}`); + return this.mailAdapter.checkCredentials(credentials).pipe( + tap(success => { + if (success) { + this.mailStorage.setCredentials(credentials); + } + }), + ); + } + + logout() { + this.mailStorage.setCredentials(undefined); + this.mailStorage.setMailboxes(undefined); + } +} diff --git a/frontend/app/src/app/modules/mail/mailbox-page.component.ts b/frontend/app/src/app/modules/mail/mailbox-page.component.ts new file mode 100644 index 00000000..4c988066 --- /dev/null +++ b/frontend/app/src/app/modules/mail/mailbox-page.component.ts @@ -0,0 +1,54 @@ +import {ChangeDetectionStrategy, Component, inject} from '@angular/core'; +import {ActivatedRoute, RouterModule} from '@angular/router'; +import {MailService} from './mail.service'; +import {MailStorageProvider} from './mail-storage.provider'; +import {mergeMap, map, filter} from 'rxjs'; +import {IonicModule} from '@ionic/angular'; +import {IonIconModule} from 'src/app/util/ion-icon/ion-icon.module'; +import {TranslateModule} from '@ngx-translate/core'; +import {AsyncPipe, TitleCasePipe} from '@angular/common'; +import {LazyComponent} from 'src/app/util/lazy.component'; +import {MailPipe} from './mail.pipe'; +import {FormatPurePipeModule, IsTodayPipeModule} from 'ngx-date-fns'; +import {LazyLoadPipe} from 'src/app/util/lazy-load.pipe'; + +@Component({ + selector: 'stapps-mailbox-page', + templateUrl: 'mailbox-page.html', + styleUrl: 'mailbox-page.scss', + standalone: true, + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [ + IonicModule, + IonIconModule, + TranslateModule, + AsyncPipe, + LazyComponent, + MailPipe, + IsTodayPipeModule, + FormatPurePipeModule, + RouterModule, + LazyLoadPipe, + TitleCasePipe, + ], +}) +export class MailboxPageComponent { + readonly activatedRoute = inject(ActivatedRoute); + + readonly mailService = inject(MailService); + + readonly mailStorage = inject(MailStorageProvider); + + mailbox = this.activatedRoute.paramMap.pipe( + mergeMap(parameters => + this.mailStorage.mailboxes.pipe( + map(mailboxes => mailboxes.folders.find(it => it.path === parameters.get('mailbox')!)!), + ), + ), + ); + + mails = this.mailbox.pipe( + filter(mailbox => mailbox !== undefined), + mergeMap(mailbox => this.mailStorage.getEmails(mailbox.path)), + ); +} diff --git a/frontend/app/src/app/modules/mail/mailbox-page.html b/frontend/app/src/app/modules/mail/mailbox-page.html new file mode 100644 index 00000000..726fbbcb --- /dev/null +++ b/frontend/app/src/app/modules/mail/mailbox-page.html @@ -0,0 +1,54 @@ + +

+ @if (mailbox | async; as mailbox) { + @if (mailbox.specialUse) { + {{ 'mail.mailboxes.' + mailbox.specialUse | translate | titlecase }} + } @else { + {{ mailbox.name }} + } + } @else { + + } +

+ + @if (mails | async; as mails) { + + @for (mail of mails; track mail.id) { + + @if (mail | mail | lazyLoad: item | async; as mail) { +
+ @if (mail.from; as from) { +
+ {{ (from.value.name || from.value.address)?.charAt(0)?.toUpperCase() }} +
+ } +
+ +

+ {{ mail.from.value.name || mail.from.value.address }} +

+ @if (mail.subject) { +

{{ mail.subject.value }}

+ } +
+ + @if (mail.date.value | dfnsIsToday) { + {{ mail.date.value | dfnsFormatPure: 'p' }} + } @else { + {{ mail.date.value | dfnsFormatPure: 'P' }} + } + + } @else { +
+ +
+ +

+

+
+ } +
+ } +
+ } +
diff --git a/frontend/app/src/app/modules/mail/mailbox-page.scss b/frontend/app/src/app/modules/mail/mailbox-page.scss new file mode 100644 index 00000000..f9513ac7 --- /dev/null +++ b/frontend/app/src/app/modules/mail/mailbox-page.scss @@ -0,0 +1,44 @@ +.avatar { + display: flex; + align-items: center; + justify-content: center; + + width: 2em; + height: 2em; + + color: var(--ion-color-light-contrast); + + background: var(--ion-color-light); + border-radius: 50%; +} + +h1 { + margin-inline: var(--spacing-md); + color: var(--ion-color-primary-contrast); +} + +h2 > ion-skeleton-text { + width: min(60%, 20em); +} + +p > ion-skeleton-text { + width: min(80%, 20em); +} + +.avatar > ion-skeleton-text { + border-radius: 50%; +} + +ion-item.unread h2 { + font-weight: bold; +} + +ion-item p { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +ion-content::part(background) { + background: none; +} diff --git a/frontend/app/src/app/modules/mail/schema.ts b/frontend/app/src/app/modules/mail/schema.ts new file mode 100644 index 00000000..93d9e714 --- /dev/null +++ b/frontend/app/src/app/modules/mail/schema.ts @@ -0,0 +1,128 @@ +import {z} from 'zod'; + +export const RawEmailAddress = z.object({ + name: z.optional(z.string()), + address: z.optional(z.string()), +}); + +export type RawEmailAddress = z.infer; + +export const RawEmailEnvelope = z.object({ + date: z.coerce.date(), + subject: z.string(), + messageId: z.string(), + inReplyTo: z.optional(z.string()), + from: z.array(RawEmailAddress), + sender: z.array(RawEmailAddress), + replyTo: z.array(RawEmailAddress), + to: z.array(RawEmailAddress), + cc: z.optional(z.array(RawEmailAddress)), + bcc: z.optional(z.array(RawEmailAddress)), +}); + +export type RawEmailEnvelope = z.infer; + +const RawEmailBodyStructureBase = z.object({ + part: z.optional(z.string()), + type: z.string(), + parameters: z.optional(z.record(z.string(), z.string())), + encoding: z.optional(z.enum(['7bit', '8bit', 'binary', 'base64', 'quoted-printable'])), + size: z.optional(z.number()), + envelope: z.optional(RawEmailEnvelope), + disposition: z.optional(z.string()), + dispositionParameters: z.optional(z.record(z.string(), z.string())), +}); + +export type RawEmailBodyStructure = z.infer & { + childNodes?: RawEmailBodyStructure[]; +}; + +export const RawEmailBodyStructure: z.ZodType = RawEmailBodyStructureBase.extend({ + childNodes: z.optional(z.lazy(() => z.array(RawEmailBodyStructure))), +}); + +export const RawEmail = z.object({ + bodyStructure: RawEmailBodyStructure, + labels: z.array(z.string()).transform(it => new Set(it)), + flags: z.array(z.string()).transform(it => new Set(it)), + envelope: RawEmailEnvelope, + seq: z.coerce.string(), +}); + +export type RawEmail = z.infer; + +const MailboxTreeItemBase = z.object({ + path: z.string(), + name: z.string(), + delimiter: z.string(), + flags: z.array(z.string()).or(z.object({})), + specialUse: z.optional( + z.enum(['\\All', '\\Archive', '\\Drafts', '\\Flagged', '\\Junk', '\\Sent', '\\Trash', '\\Inbox']), + ), + listed: z.boolean(), + subscribed: z.boolean(), + disabled: z.boolean().optional(), +}); + +export type MailboxTreeItem = z.infer & { + folders?: MailboxTreeItem[]; +}; + +export const MailboxTreeItem: z.ZodType = MailboxTreeItemBase.extend({ + folders: z.optional(z.lazy(() => z.array(MailboxTreeItem))), +}); + +export const MailboxTreeRoot = z.object({ + path: z.literal('').default(''), + root: z.literal(true), + folders: z.array(MailboxTreeItem), +}); + +export type MailboxTreeRoot = z.infer; + +export interface Signature { + type: 'pkcs7'; + valid: boolean; +} + +export interface SignedValue { + value: T; + signature?: Signature; +} + +export interface EmailAddress { + name?: string; + address?: string; +} + +export interface EmailAttachmentBase { + filename: string; + size: number; +} + +export interface EmailAttachmentRemote extends EmailAttachmentBase { + part: string; +} + +export interface EmailAttachmentLocal extends EmailAttachmentBase { + content: ArrayBuffer; +} + +export type EmailAttachment = EmailAttachmentRemote | EmailAttachmentLocal; + +export interface Email { + id: string; + mailbox: string; + flags: Set; + subject?: SignedValue; + date: SignedValue; + from: SignedValue; + to?: SignedValue[]; + cc?: SignedValue[]; + bcc?: SignedValue[]; + html?: SignedValue; + text?: SignedValue; + attachments: SignedValue[]; +} + +export type EmailMeta = Pick & {incomplete: true}; diff --git a/frontend/app/src/app/modules/profile/page/profile-page-section.html b/frontend/app/src/app/modules/profile/page/profile-page-section.html index 308b92ad..cff4822e 100644 --- a/frontend/app/src/app/modules/profile/page/profile-page-section.html +++ b/frontend/app/src/app/modules/profile/page/profile-page-section.html @@ -39,6 +39,11 @@ } {{ 'name' | translateSimple: link }} + @if (link.beta) { + + {{ 'beta' | translate }} + + } } diff --git a/frontend/app/src/app/modules/profile/page/profile-page-section.scss b/frontend/app/src/app/modules/profile/page/profile-page-section.scss index bde1c548..c8b85bc1 100644 --- a/frontend/app/src/app/modules/profile/page/profile-page-section.scss +++ b/frontend/app/src/app/modules/profile/page/profile-page-section.scss @@ -50,6 +50,13 @@ ion-item { } } +ion-note { + position: absolute; + top: 0; + right: 0; + padding: var(--spacing-xs); +} + simple-swiper { --swiper-slide-width: #{$width}; diff --git a/frontend/app/src/app/translation/i18n.spec.ts b/frontend/app/src/app/translation/i18n.spec.ts index 4672a619..5ed4cd84 100644 --- a/frontend/app/src/app/translation/i18n.spec.ts +++ b/frontend/app/src/app/translation/i18n.spec.ts @@ -19,6 +19,7 @@ import german from '../../assets/i18n/de.json'; const exceptions = new Set( [ + 'ID', 'login', 'ok', 'protein', diff --git a/frontend/app/src/app/util/data-size.pipe.ts b/frontend/app/src/app/util/data-size.pipe.ts new file mode 100644 index 00000000..40e1df7b --- /dev/null +++ b/frontend/app/src/app/util/data-size.pipe.ts @@ -0,0 +1,25 @@ +import {Pipe, PipeTransform} from '@angular/core'; + +/** + * Format a data size in bytes to a human readable string + */ +export function formatDataSize(value: number, precision = 2): string { + const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; + let unit = 0; + while (value >= 1024 && unit < units.length - 1) { + value /= 1024; + unit++; + } + return `${value.toFixed(precision)} ${units[unit]}`; +} + +@Pipe({ + name: 'dataSize', + pure: true, + standalone: true, +}) +export class DataSizePipe implements PipeTransform { + transform(value: number, precision = 2): string { + return formatDataSize(value, precision); + } +} diff --git a/frontend/app/src/app/util/lazy-load.pipe.ts b/frontend/app/src/app/util/lazy-load.pipe.ts new file mode 100644 index 00000000..11c67691 --- /dev/null +++ b/frontend/app/src/app/util/lazy-load.pipe.ts @@ -0,0 +1,36 @@ +import {OnDestroy, Pipe, PipeTransform} from '@angular/core'; +import {Observable, Subject, mergeMap} from 'rxjs'; + +@Pipe({ + name: 'lazyLoad', + pure: true, + standalone: true, +}) +export class LazyLoadPipe implements PipeTransform, OnDestroy { + intersectionObserver?: IntersectionObserver; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + transform(value: Observable, element: any): Observable { + if (element.el instanceof Element) { + const isInView = new Subject(); + this.intersectionObserver?.disconnect(); + this.intersectionObserver = new IntersectionObserver(entries => { + if (entries.some(it => it.isIntersecting)) { + this.intersectionObserver?.disconnect(); + delete this.intersectionObserver; + isInView.next(); + isInView.complete(); + } + }); + this.intersectionObserver.observe(element.el); + return isInView.pipe(mergeMap(() => value)); + } else { + console.error('LazyLoadPipe: not an element', element); + return value; + } + } + + ngOnDestroy() { + this.intersectionObserver?.disconnect(); + } +} diff --git a/frontend/app/src/app/util/lazy.component.ts b/frontend/app/src/app/util/lazy.component.ts new file mode 100644 index 00000000..27db65a4 --- /dev/null +++ b/frontend/app/src/app/util/lazy.component.ts @@ -0,0 +1,46 @@ +import {NgTemplateOutlet} from '@angular/common'; +import { + ChangeDetectionStrategy, + Component, + ContentChild, + ElementRef, + OnDestroy, + OnInit, + Renderer2, + TemplateRef, + signal, +} from '@angular/core'; + +@Component({ + selector: 'stapps-lazy', + templateUrl: 'lazy.html', + styleUrl: 'lazy.scss', + standalone: true, + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [NgTemplateOutlet], +}) +export class LazyComponent implements OnInit, OnDestroy { + @ContentChild(TemplateRef) template!: TemplateRef; + + isIntersecting = signal(false); + + intersectionObserver = new IntersectionObserver(entries => { + this.isIntersecting.set(this.isIntersecting() || entries.some(entry => entry.isIntersecting)); + if (this.isIntersecting()) { + this.intersectionObserver.disconnect(); + } + }); + + constructor( + readonly element: ElementRef, + readonly renderer: Renderer2, + ) {} + + ngOnInit() { + this.intersectionObserver.observe(this.element.nativeElement); + } + + ngOnDestroy() { + this.intersectionObserver.disconnect(); + } +} diff --git a/frontend/app/src/app/util/lazy.html b/frontend/app/src/app/util/lazy.html new file mode 100644 index 00000000..892ca534 --- /dev/null +++ b/frontend/app/src/app/util/lazy.html @@ -0,0 +1,3 @@ +@if (isIntersecting()) { + +} diff --git a/frontend/app/src/app/util/lazy.scss b/frontend/app/src/app/util/lazy.scss new file mode 100644 index 00000000..e69de29b diff --git a/frontend/app/src/app/util/rxjs/from-cursor.ts b/frontend/app/src/app/util/rxjs/from-cursor.ts new file mode 100644 index 00000000..8afef255 --- /dev/null +++ b/frontend/app/src/app/util/rxjs/from-cursor.ts @@ -0,0 +1,22 @@ +import {EMPTY, Observable, concat, defer, of} from 'rxjs'; + +/** + * Turns an IDBCursorWithValue into an Observable of values. + * + * Values are emitted lazily, i.e. the next value is only emitted when the + * previous one has been consumed. + * @example fromCursor(cursor).pipe(take(10)).subscribe(console.log); + */ +export function fromCursor(cursor: IDBCursorWithValue): Observable { + if (cursor.key === null) { + return EMPTY; + } + + const value = cursor.value as T; + cursor.continue(); + + return concat( + of(value), + defer(() => fromCursor(cursor)), + ); +} diff --git a/frontend/app/src/app/util/shadow-html.directive.ts b/frontend/app/src/app/util/shadow-html.directive.ts new file mode 100644 index 00000000..78a8e708 --- /dev/null +++ b/frontend/app/src/app/util/shadow-html.directive.ts @@ -0,0 +1,18 @@ +import {Directive, ElementRef, Host, Input} from '@angular/core'; +import {sanitize} from 'dompurify'; + +@Directive({ + selector: '[shadowHTML]', + standalone: true, +}) +export class ShadowHtmlDirective { + @Input({required: true}) + set shadowHTML(content: string) { + this.shadowRoot.innerHTML = ''; + this.shadowRoot.append(sanitize(content, {RETURN_DOM_FRAGMENT: true, USE_PROFILES: {html: true}})); + } + + shadowRoot = (this.elementRef.nativeElement as HTMLElement).attachShadow({mode: 'open'}); + + constructor(@Host() readonly elementRef: ElementRef) {} +} diff --git a/frontend/app/src/assets/i18n/de.json b/frontend/app/src/assets/i18n/de.json index 4c758acb..ad7cd3d9 100644 --- a/frontend/app/src/assets/i18n/de.json +++ b/frontend/app/src/assets/i18n/de.json @@ -8,6 +8,7 @@ "export": "Exportieren", "share": "Teilen", "timeSuffix": "Uhr", + "beta": "Beta", "ratings": { "thank_you": "Vielen Dank für die Bewertung!" }, @@ -389,6 +390,35 @@ } } }, + "mail": { + "SIGNATURE_VALID": "Signatur gültig", + "SIGNATURE_INVALID": "Signatur ungültig", + "SIGNATURE_UNSUPPORTED": "Signatur nicht unterstützt", + "ID": "ID", + "FROM": "von", + "SENDER": "Absender", + "TO": "an", + "DATE": "Datum", + "login": { + "TITLE": "E-Mail Login", + "LOGIN": "Login", + "PLACEHOLDER_USERNAME": "Nutzername", + "PLACEHOLDER_PASSWORD": "Passwort", + "error": { + "INVALID_CREDENTIALS": "ungültige Zugangsdaten" + } + }, + "mailboxes": { + "\\Inbox": "Posteingang", + "\\All": "Alle", + "\\Archive": "Archiv", + "\\Drafts": "Entwürfe", + "\\Flagged": "Markiert", + "\\Junk": "Spam", + "\\Sent": "Gesendet", + "\\Trash": "Papierkorb" + } + }, "menu": { "context": { "title": "Kontext Menü", diff --git a/frontend/app/src/assets/i18n/en.json b/frontend/app/src/assets/i18n/en.json index 41617bd5..9c891202 100644 --- a/frontend/app/src/assets/i18n/en.json +++ b/frontend/app/src/assets/i18n/en.json @@ -7,6 +7,7 @@ "back": "back", "export": "Export", "share": "Share", + "beta": "beta", "timeSuffix": "", "ratings": { "thank_you": "Thank you for your feedback!" @@ -389,6 +390,35 @@ } } }, + "mail": { + "SIGNATURE_VALID": "signature valid", + "SIGNATURE_INVALID": "signature invalid", + "SIGNATURE_UNSUPPORTED": "signature unsupported", + "ID": "ID", + "FROM": "from", + "SENDER": "sender", + "TO": "to", + "DATE": "date", + "login": { + "TITLE": "email login", + "LOGIN": "login", + "PLACEHOLDER_USERNAME": "username", + "PLACEHOLDER_PASSWORD": "password", + "error": { + "INVALID_CREDENTIALS": "invalid credentials" + } + }, + "mailboxes": { + "\\Inbox": "inbox", + "\\All": "all inboxes", + "\\Archive": "archive", + "\\Drafts": "drafts", + "\\Flagged": "flagged", + "\\Junk": "spam", + "\\Sent": "sent", + "\\Trash": "trash" + } + }, "menu": { "context": { "title": "context menu", diff --git a/frontend/app/src/config/profile-page-sections.ts b/frontend/app/src/config/profile-page-sections.ts index 6b5f9845..efba4fa8 100644 --- a/frontend/app/src/config/profile-page-sections.ts +++ b/frontend/app/src/config/profile-page-sections.ts @@ -49,6 +49,7 @@ export interface SCSectionLink extends SCThing { link: string[]; needsAuth?: true; icon?: string; + beta?: true; } export interface SCSection extends SCThing { @@ -150,6 +151,18 @@ export const profilePageSections: SCSection[] = [ }, ...SCSectionLinkConstantValues, }, + { + name: 'Mail', + icon: SCIcon.mail, + link: ['/mail'], + beta: true, + translations: { + de: { + name: 'Email', + }, + }, + ...SCSectionLinkConstantValues, + }, ], translations: { de: { diff --git a/frontend/app/src/index.html b/frontend/app/src/index.html index cf3ded06..15648190 100644 --- a/frontend/app/src/index.html +++ b/frontend/app/src/index.html @@ -18,7 +18,7 @@ - + diff --git a/packages/logger/package.json b/packages/logger/package.json index a845349f..14223e23 100644 --- a/packages/logger/package.json +++ b/packages/logger/package.json @@ -28,10 +28,10 @@ "test": "c8 mocha" }, "dependencies": { - "@types/nodemailer": "6.4.7", + "@types/nodemailer": "6.4.15", "chalk": "5.2.0", "flatted": "3.2.7", - "nodemailer": "6.9.1" + "nodemailer": "6.9.14" }, "devDependencies": { "@openstapps/eslint-config": "workspace:*", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 12b59703..87f103c6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -84,8 +84,8 @@ importers: specifier: 3.0.7 version: 3.0.7 '@types/nodemailer': - specifier: 6.4.7 - version: 6.4.7 + specifier: 6.4.15 + version: 6.4.15 '@types/promise-queue': specifier: 2.2.0 version: 2.2.0 @@ -132,8 +132,8 @@ importers: specifier: 3.0.2 version: 3.0.2 nodemailer: - specifier: 6.9.1 - version: 6.9.1 + specifier: 6.9.14 + version: 6.9.14 prom-client: specifier: 14.1.1 version: 14.1.1 @@ -246,6 +246,97 @@ importers: backend/database: {} + backend/mail-plugin: + dependencies: + '@openstapps/core': + specifier: workspace:* + version: link:../../packages/core + '@openstapps/core-tools': + specifier: workspace:* + version: link:../../packages/core-tools + '@openstapps/logger': + specifier: workspace:* + version: link:../../packages/logger + commander: + specifier: 10.0.0 + version: 10.0.0 + dotenv: + specifier: 16.4.5 + version: 16.4.5 + express: + specifier: 4.18.2 + version: 4.18.2 + imapflow: + specifier: 1.0.162 + version: 1.0.162 + mailparser: + specifier: 3.7.1 + version: 3.7.1 + node-forge: + specifier: 1.3.1 + version: 1.3.1 + nodemailer: + specifier: 6.9.14 + version: 6.9.14 + ts-node: + specifier: 10.9.2 + version: 10.9.2(@types/node@18.15.3)(typescript@5.4.2) + devDependencies: + '@openstapps/eslint-config': + specifier: workspace:* + version: link:../../configuration/eslint-config + '@openstapps/prettier-config': + specifier: workspace:* + version: link:../../configuration/prettier-config + '@openstapps/tsconfig': + specifier: workspace:* + version: link:../../configuration/tsconfig + '@types/express': + specifier: 4.17.17 + version: 4.17.17 + '@types/imapflow': + specifier: 1.0.18 + version: 1.0.18 + '@types/mailparser': + specifier: 3.4.4 + version: 3.4.4 + '@types/node': + specifier: 18.15.3 + version: 18.15.3 + '@types/node-forge': + specifier: 1.3.11 + version: 1.3.11 + '@types/nodemailer': + specifier: 6.4.15 + version: 6.4.15 + '@typescript-eslint/eslint-plugin': + specifier: 7.2.0 + version: 7.2.0(@typescript-eslint/parser@7.2.0)(eslint@8.57.0)(typescript@5.4.2) + '@typescript-eslint/parser': + specifier: 7.2.0 + version: 7.2.0(eslint@8.57.0)(typescript@5.4.2) + eslint: + specifier: 8.57.0 + version: 8.57.0 + eslint-config-prettier: + specifier: 9.1.0 + version: 9.1.0(eslint@8.57.0) + eslint-plugin-jsdoc: + specifier: 48.2.1 + version: 48.2.1(eslint@8.57.0) + eslint-plugin-unicorn: + specifier: 51.0.1 + version: 51.0.1(eslint@8.57.0) + prettier: + specifier: 3.1.1 + version: 3.1.1 + tsup: + specifier: 6.7.0 + version: 6.7.0(ts-node@10.9.2)(typescript@5.4.2) + typescript: + specifier: 5.4.2 + version: 5.4.2 + backend/proxy: dependencies: '@openstapps/logger': @@ -815,6 +906,9 @@ importers: '@types/dom-view-transitions': specifier: 1.0.4 version: 1.0.4 + asn1js: + specifier: 3.0.5 + version: 3.0.5 capacitor-secure-storage-plugin: specifier: 0.9.0 version: 0.9.0(@capacitor/core@6.1.1) @@ -839,6 +933,12 @@ importers: jsonpath-plus: specifier: 10.0.6 version: 10.0.6 + libbase64: + specifier: 1.3.0 + version: 1.3.0 + libqp: + specifier: 2.1.0 + version: 2.1.0 maplibre-gl: specifier: 4.0.2 version: 4.0.2 @@ -863,9 +963,15 @@ importers: opening_hours: specifier: 3.8.0 version: 3.8.0 + pkijs: + specifier: 3.1.0 + version: 3.1.0 pmtiles: specifier: 3.0.3 version: 3.0.3 + postal-mime: + specifier: 2.2.5 + version: 2.2.5 rxjs: specifier: 7.8.1 version: 7.8.1 @@ -878,6 +984,9 @@ importers: tslib: specifier: 2.6.2 version: 2.6.2 + zod: + specifier: 3.23.8 + version: 3.23.8 zone.js: specifier: 0.14.4 version: 0.14.4 @@ -957,6 +1066,9 @@ importers: '@openstapps/tsconfig': specifier: workspace:* version: link:../../configuration/tsconfig + '@types/dompurify': + specifier: 3.0.5 + version: 3.0.5 '@types/fontkit': specifier: 2.0.7 version: 2.0.7 @@ -966,6 +1078,9 @@ importers: '@types/glob': specifier: 8.1.0 version: 8.1.0 + '@types/imapflow': + specifier: 1.0.18 + version: 1.0.18 '@types/jasmine': specifier: 5.1.4 version: 5.1.4 @@ -1002,6 +1117,9 @@ importers: cypress: specifier: 13.7.0 version: 13.7.0 + dompurify: + specifier: 3.1.6 + version: 3.1.6 eslint: specifier: 8.57.0 version: 8.57.0 @@ -1964,8 +2082,8 @@ importers: packages/logger: dependencies: '@types/nodemailer': - specifier: 6.4.7 - version: 6.4.7 + specifier: 6.4.15 + version: 6.4.15 chalk: specifier: 5.2.0 version: 5.2.0 @@ -1973,8 +2091,8 @@ importers: specifier: 3.2.7 version: 3.2.7 nodemailer: - specifier: 6.9.1 - version: 6.9.1 + specifier: 6.9.14 + version: 6.9.14 devDependencies: '@openstapps/eslint-config': specifier: workspace:* @@ -2144,7 +2262,7 @@ packages: css-loader: 6.10.0(webpack@5.90.3) esbuild-wasm: 0.20.1 fast-glob: 3.3.2 - http-proxy-middleware: 2.0.6(@types/express@4.17.21) + http-proxy-middleware: 2.0.6(@types/express@4.17.17) https-proxy-agent: 7.0.4 inquirer: 9.2.15 jsonc-parser: 3.2.1 @@ -5084,7 +5202,7 @@ packages: '@ionic/utils-subprocess': 2.1.14 '@ionic/utils-terminal': 2.3.5 commander: 9.5.0 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.6(supports-color@8.1.1) env-paths: 2.2.1 kleur: 4.1.5 native-run: 2.0.1 @@ -5275,7 +5393,7 @@ packages: fs-extra: 7.0.1 lodash.startcase: 4.4.0 outdent: 0.5.0 - prettier: 2.8.6 + prettier: 2.8.8 resolve-from: 5.0.0 semver: 7.6.0 dev: true @@ -5443,7 +5561,7 @@ packages: '@changesets/types': 5.2.1 fs-extra: 7.0.1 human-id: 1.0.2 - prettier: 2.8.6 + prettier: 2.8.8 dev: true /@colors/colors@1.5.0: @@ -6619,7 +6737,7 @@ packages: resolution: {integrity: sha512-0JZ1Zkp3wURnv8oq6Qt7fMPo5MpjbLoUoa9Bu2Q4PJuSDWM8H8gwF3dQO7VTeUj3/0o1IB1wGkFWZZYgUXZMUg==} engines: {node: '>=16.0.0'} dependencies: - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.6(supports-color@8.1.1) tslib: 2.6.2 transitivePeerDependencies: - supports-color @@ -6642,7 +6760,7 @@ packages: engines: {node: '>=16.0.0'} dependencies: '@types/fs-extra': 8.1.5 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.6(supports-color@8.1.1) fs-extra: 9.1.0 tslib: 2.6.2 transitivePeerDependencies: @@ -7588,6 +7706,13 @@ packages: - chokidar dev: true + /@selderee/plugin-htmlparser2@0.11.0: + resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==} + dependencies: + domhandler: 5.0.3 + selderee: 0.11.0 + dev: false + /@sideway/address@4.1.4: resolution: {integrity: sha512-7vwq+rOHVWjyXxVlR76Agnvhy8I9rpzjosTESvmhNeXOXdZZB15Fl+TI9x1SiHZH5Jv2wTGduSxFDIaq0m3DUw==} dependencies: @@ -7881,13 +8006,6 @@ packages: '@types/connect': 3.4.35 '@types/node': 18.15.3 - /@types/body-parser@1.19.5: - resolution: {integrity: sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==} - dependencies: - '@types/connect': 3.4.38 - '@types/node': 18.15.3 - dev: true - /@types/bonjour@3.5.13: resolution: {integrity: sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ==} dependencies: @@ -7932,12 +8050,6 @@ packages: dependencies: '@types/node': 18.15.3 - /@types/connect@3.4.38: - resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} - dependencies: - '@types/node': 18.15.3 - dev: true - /@types/cookie@0.4.1: resolution: {integrity: sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==} dev: true @@ -8008,6 +8120,12 @@ packages: resolution: {integrity: sha512-oDuagM6G+xPLrLU4KeCKlr1oalMF5mJqV5pDPMDVIEaa8AkUW00i6u+5P02XCjdEEUQJC9dpnxqSLsZeAciSLQ==} dev: false + /@types/dompurify@3.0.5: + resolution: {integrity: sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg==} + dependencies: + '@types/trusted-types': 2.0.7 + dev: true + /@types/eslint-scope@3.7.7: resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==} dependencies: @@ -8026,14 +8144,6 @@ packages: resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} dev: true - /@types/express-serve-static-core@4.17.35: - resolution: {integrity: sha512-wALWQwrgiB2AWTT91CB62b6Yt0sNHpznUXeZEcnPU3DRdlDIz74x8Qg1UUYKSVFi+va5vKOLYRBI1bRKiLLKIg==} - dependencies: - '@types/node': 18.15.3 - '@types/qs': 6.9.7 - '@types/range-parser': 1.2.4 - '@types/send': 0.17.1 - /@types/express-serve-static-core@4.19.5: resolution: {integrity: sha512-y6W03tvrACO72aijJ5uF02FRq5cgDR9lUxddQ8vyF+GvmjJQqbzDcJngEjURc+ZsG31VI3hODNZJ2URj86pzmg==} dependencies: @@ -8041,24 +8151,14 @@ packages: '@types/qs': 6.9.15 '@types/range-parser': 1.2.7 '@types/send': 0.17.4 - dev: true /@types/express@4.17.17: resolution: {integrity: sha512-Q4FmmuLGBG58btUnfS1c1r/NQdlp3DMfGDGig8WhfpA2YRUtEkxAjkZb0yvplJGYdF1fsQ81iMDcH24sSCNC/Q==} dependencies: '@types/body-parser': 1.19.2 - '@types/express-serve-static-core': 4.17.35 - '@types/qs': 6.9.7 - '@types/serve-static': 1.15.2 - - /@types/express@4.17.21: - resolution: {integrity: sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==} - dependencies: - '@types/body-parser': 1.19.5 '@types/express-serve-static-core': 4.19.5 '@types/qs': 6.9.15 '@types/serve-static': 1.15.7 - dev: true /@types/fontkit@2.0.7: resolution: {integrity: sha512-f5BjGam6y3FrfEY2JxXwba66SYzqP+FREZh4UuBN1WDePl8EhTKjba3ZZQ2iORUufkrFt/c/UIugj0Uv/HEdRg==} @@ -8101,12 +8201,8 @@ packages: resolution: {integrity: sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==} dev: false - /@types/http-errors@2.0.1: - resolution: {integrity: sha512-/K3ds8TRAfBvi5vfjuz8y6+GiAYBZ0x4tXv1Av6CWBWn0IlADc+ZX9pMq7oU0fNQPnBwIZl3rmeLp6SBApbxSQ==} - /@types/http-errors@2.0.4: resolution: {integrity: sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==} - dev: true /@types/http-proxy@1.17.14: resolution: {integrity: sha512-SSrD0c1OQzlFX7pGu1eXxSEjemej64aaNPRhhVYUGqXh0BtldAAx37MG8btcumvpgKyZp1F5Gn3JkktdxiFv6w==} @@ -8114,6 +8210,12 @@ packages: '@types/node': 18.15.3 dev: true + /@types/imapflow@1.0.18: + resolution: {integrity: sha512-BoWZUoMktji2YJmkRY8z0KsjvyDNpBzeC/rLVMFKcHkPxaKp+SHBFfx/kj7ltKh3l010Lc9RZqnJs8KUMNhf6Q==} + dependencies: + '@types/node': 18.15.3 + dev: true + /@types/is-ci@3.0.0: resolution: {integrity: sha512-Q0Op0hdWbYd1iahB+IFNQcWXFq4O0Q5MwQP7uN0souuQ4rPg1vEYcnIOfr1gY+M+6rc8FGoRaBO1mOOvL29sEQ==} dependencies: @@ -8201,6 +8303,13 @@ packages: '@types/geojson': 1.0.6 dev: false + /@types/mailparser@3.4.4: + resolution: {integrity: sha512-C6Znp2QVS25JqtuPyxj38Qh+QoFcLycdxsvcc6IZCGekhaMBzbdTXzwGzhGoYb3TfKu8IRCNV0sV1o3Od97cEQ==} + dependencies: + '@types/node': 18.15.3 + iconv-lite: 0.6.3 + dev: true + /@types/mapbox__point-geometry@0.1.4: resolution: {integrity: sha512-mUWlSxAmYLfwnRBmgYV86tgYmMIICX4kza8YnE/eIlywGe2XoOxlpVnXWwir92xRLjwyarqwpu2EJKD2pk0IUA==} dev: false @@ -8221,15 +8330,8 @@ packages: dev: false optional: true - /@types/mime@1.3.2: - resolution: {integrity: sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==} - /@types/mime@1.3.5: resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} - dev: true - - /@types/mime@3.0.1: - resolution: {integrity: sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==} /@types/minimatch@5.1.2: resolution: {integrity: sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==} @@ -8278,11 +8380,10 @@ packages: /@types/node@18.15.3: resolution: {integrity: sha512-p6ua9zBxz5otCmbpb5D3U4B5Nanw6Pk3PPyX05xnxbB/fRv71N7CPmORg7uAD5P70T0xmx1pzAx/FUfa5X+3cw==} - /@types/nodemailer@6.4.7: - resolution: {integrity: sha512-f5qCBGAn/f0qtRcd4SEn88c8Fp3Swct1731X4ryPKqS61/A3LmmzN8zaEz7hneJvpjFbUUgY7lru/B/7ODTazg==} + /@types/nodemailer@6.4.15: + resolution: {integrity: sha512-0EBJxawVNjPkng1zm2vopRctuWVCxk34JcIlRuXSf54habUWdz1FB7wHDqOqvDa8Mtpt0Q3LTXQkAs2LNyK5jQ==} dependencies: '@types/node': 18.15.3 - dev: false /@types/normalize-package-data@2.4.1: resolution: {integrity: sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==} @@ -8301,17 +8402,9 @@ packages: /@types/qs@6.9.15: resolution: {integrity: sha512-uXHQKES6DQKKCLh441Xv/dwxOq1TVS3JPUMlEqoEglvlhR6Mxnlew/Xq/LRVHpLyk7iK3zODe1qYHIMltO7XGg==} - dev: true - - /@types/qs@6.9.7: - resolution: {integrity: sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==} - - /@types/range-parser@1.2.4: - resolution: {integrity: sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==} /@types/range-parser@1.2.7: resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} - dev: true /@types/retry@0.12.0: resolution: {integrity: sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==} @@ -8330,39 +8423,24 @@ packages: /@types/semver@7.5.8: resolution: {integrity: sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==} - /@types/send@0.17.1: - resolution: {integrity: sha512-Cwo8LE/0rnvX7kIIa3QHCkcuF21c05Ayb0ZfxPiv0W8VRiZiNW/WuRupHKpqqGVGf7SUA44QSOUKaEd9lIrd/Q==} - dependencies: - '@types/mime': 1.3.2 - '@types/node': 18.15.3 - /@types/send@0.17.4: resolution: {integrity: sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==} dependencies: '@types/mime': 1.3.5 '@types/node': 18.15.3 - dev: true /@types/serve-index@1.9.4: resolution: {integrity: sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug==} dependencies: - '@types/express': 4.17.21 + '@types/express': 4.17.17 dev: true - /@types/serve-static@1.15.2: - resolution: {integrity: sha512-J2LqtvFYCzaj8pVYKw8klQXrLLk7TBZmQ4ShlcdkELFKGwGMfevMLneMMRkMgZxotOD9wg497LpC7O8PcvAmfw==} - dependencies: - '@types/http-errors': 2.0.1 - '@types/mime': 3.0.1 - '@types/node': 18.15.3 - /@types/serve-static@1.15.7: resolution: {integrity: sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==} dependencies: '@types/http-errors': 2.0.4 '@types/node': 18.15.3 '@types/send': 0.17.4 - dev: true /@types/sha1@1.1.3: resolution: {integrity: sha512-bXfx/6xrPu1l6pLItGRMPX00lhnJavpj2qiQeLHflXvL2Ix97aC8FTF2/pQoqukRzcCwKyN3csZvOLzamIoaSA==} @@ -8444,6 +8522,10 @@ packages: resolution: {integrity: sha512-RBz2uRZVCXuMg93WD//aTS5B120QlT4lR/gL+935QtGsKHLS6sCtZBaKfWjIfk7ZXv/r8mtGbwjVIee6/3XTow==} dev: true + /@types/trusted-types@2.0.7: + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + dev: true + /@types/unist@2.0.10: resolution: {integrity: sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==} requiresBuild: true @@ -8886,6 +8968,13 @@ packages: engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} dev: true + /abort-controller@3.0.0: + resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} + engines: {node: '>=6.5'} + dependencies: + event-target-shim: 5.0.1 + dev: false + /accepts@1.3.8: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} engines: {node: '>= 0.6'} @@ -8999,17 +9088,6 @@ packages: ajv: 8.12.0 dev: true - /ajv-formats@2.1.1(ajv@8.17.1): - resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} - peerDependencies: - ajv: ^8.0.0 - peerDependenciesMeta: - ajv: - optional: true - dependencies: - ajv: 8.17.1 - dev: true - /ajv-keywords@3.5.2(ajv@6.12.6): resolution: {integrity: sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==} peerDependencies: @@ -9018,12 +9096,12 @@ packages: ajv: 6.12.6 dev: true - /ajv-keywords@5.1.0(ajv@8.17.1): + /ajv-keywords@5.1.0(ajv@8.12.0): resolution: {integrity: sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==} peerDependencies: ajv: ^8.8.2 dependencies: - ajv: 8.17.1 + ajv: 8.12.0 fast-deep-equal: 3.1.3 dev: true @@ -9268,6 +9346,15 @@ packages: dependencies: safer-buffer: 2.1.2 + /asn1js@3.0.5: + resolution: {integrity: sha512-FVnvrKJwpt9LP2lAMl8qZswRNm3T4q9CON+bxldk2iwk3FFpuwhx2FfinyitizWHsVYyaY+y5JzDR0rCMV5yTQ==} + engines: {node: '>=12.0.0'} + dependencies: + pvtsutils: 1.3.5 + pvutils: 1.1.3 + tslib: 2.6.2 + dev: false + /assert-plus@1.0.0: resolution: {integrity: sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==} engines: {node: '>=0.8'} @@ -9311,6 +9398,11 @@ packages: resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==} engines: {node: '>= 4.0.0'} + /atomic-sleep@1.0.0: + resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} + engines: {node: '>=8.0.0'} + dev: false + /autoprefixer@10.4.18(postcss@8.4.35): resolution: {integrity: sha512-1DKbDfsr6KUElM6wg+0zRNkB/Q7WcKYAaK+pzXn+Xqmszm/5Xa9coeNdtP88Vi+dPzZnMjhge8GIV49ZQkDa+g==} engines: {node: ^10 || ^12 || >=14} @@ -9652,7 +9744,6 @@ packages: unpipe: 1.0.0 transitivePeerDependencies: - supports-color - dev: false /body-parser@1.20.2: resolution: {integrity: sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==} @@ -9776,6 +9867,13 @@ packages: base64-js: 1.5.1 ieee754: 1.2.1 + /buffer@6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + dev: false + /buildcheck@0.0.6: resolution: {integrity: sha512-8f9ZJCUXyT1M35Jx7MkBgmBMo3oHTTBIPLiY9xyL0pl3T5RwcPEY8cUHr5LBNfu/fk6c2T4DJZuVM/8ZZT2D2A==} engines: {node: '>=10.0.0'} @@ -9813,6 +9911,11 @@ packages: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} + /bytestreamjs@2.0.1: + resolution: {integrity: sha512-U1Z/ob71V/bXfVABvNr/Kumf5VyeQRBEm6Txb0PQ6S7V5GpBM3w4Cbqz/xPDicR5tN0uvDifng8C+5qECeGwyQ==} + engines: {node: '>=6.0.0'} + dev: false + /bytewise-core@1.2.3: resolution: {integrity: sha512-nZD//kc78OOxeYtRlVk8/zXqTB4gf/nlguL1ggWA8FuchMyOxcyHR4QPQZMUmA7czC+YnaBrPUCubqAWe50DaA==} dependencies: @@ -10662,12 +10765,6 @@ packages: /cookie@0.5.0: resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==} engines: {node: '>= 0.6'} - dev: false - - /cookie@0.6.0: - resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==} - engines: {node: '>= 0.6'} - dev: true /cookiejar@2.1.4: resolution: {integrity: sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==} @@ -11820,7 +11917,6 @@ packages: domelementtype: 2.3.0 domhandler: 5.0.3 entities: 4.5.0 - dev: true /dom7@4.0.6: resolution: {integrity: sha512-emjdpPLhpNubapLFdjNL9tP06Sr+GZkrIHEXLWvOGsytACUrkbeIdjO5g77m00BrHTznnlcNqgmn7pCN192TBA==} @@ -11830,7 +11926,6 @@ packages: /domelementtype@2.3.0: resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} - dev: true /domhandler@4.3.1: resolution: {integrity: sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==} @@ -11844,13 +11939,10 @@ packages: engines: {node: '>= 4'} dependencies: domelementtype: 2.3.0 - dev: true /dompurify@3.1.6: resolution: {integrity: sha512-cTOAhc36AalkjtBpfG6O8JimdTMWNXjiePT2xQH/ppBGi/4uIpmj8eKyIkMJErXWARyINV/sB38yf8JCLF5pbQ==} requiresBuild: true - dev: false - optional: true /domutils@2.8.0: resolution: {integrity: sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==} @@ -11866,7 +11958,6 @@ packages: dom-serializer: 2.0.0 domelementtype: 2.3.0 domhandler: 5.0.3 - dev: true /dot-prop@5.3.0: resolution: {integrity: sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==} @@ -11884,7 +11975,7 @@ packages: hasBin: true dependencies: cross-spawn: 7.0.3 - dotenv: 16.3.1 + dotenv: 16.4.5 dotenv-expand: 10.0.0 minimist: 1.2.8 dev: true @@ -11894,16 +11985,15 @@ packages: engines: {node: '>=12'} dev: true - /dotenv@16.3.1: - resolution: {integrity: sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==} - engines: {node: '>=12'} - dev: true - /dotenv@16.3.2: resolution: {integrity: sha512-HTlk5nmhkm8F6JcdXvHIzaorzCoziNQT9mGxLPVXW8wJF1TiGSL60ZGB4gHWabHOaMmWmhvk2/lPHfnBiT78AQ==} engines: {node: '>=12'} dev: true + /dotenv@16.4.5: + resolution: {integrity: sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==} + engines: {node: '>=12'} + /duplexer2@0.1.4: resolution: {integrity: sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==} dependencies: @@ -11989,6 +12079,16 @@ packages: engines: {node: '>= 0.8'} dev: true + /encoding-japanese@2.0.0: + resolution: {integrity: sha512-++P0RhebUC8MJAwJOsT93dT+5oc5oPImp1HubZpAuCZ5kTLnhuuBhKHj2jJeO/Gj93idPBWmIuQ9QWMe5rX3pQ==} + engines: {node: '>=8.10.0'} + dev: false + + /encoding-japanese@2.1.0: + resolution: {integrity: sha512-58XySVxUgVlBikBTbQ8WdDxBDHIdXucB16LO5PBHR8t75D54wQrNo4cg+58+R1CtJfKnsVsvt9XlteRaR8xw1w==} + engines: {node: '>=8.10.0'} + dev: false + /encoding@0.1.13: resolution: {integrity: sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==} requiresBuild: true @@ -12580,6 +12680,11 @@ packages: through: 2.3.8 dev: true + /event-target-shim@5.0.1: + resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} + engines: {node: '>=6'} + dev: false + /eventemitter2@6.4.7: resolution: {integrity: sha512-tYUSVOGeQPKt/eC1ABfhHy5Xd96N3oIijJvN3O9+TsC28T5V9yX9oEfEK5faP0EFSNVOG97qtAS68GBrQB2hDg==} dev: true @@ -12594,7 +12699,6 @@ packages: /events@3.3.0: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} - dev: true /execa@4.1.0: resolution: {integrity: sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==} @@ -12706,46 +12810,6 @@ packages: vary: 1.1.2 transitivePeerDependencies: - supports-color - dev: false - - /express@4.19.2: - resolution: {integrity: sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==} - engines: {node: '>= 0.10.0'} - dependencies: - accepts: 1.3.8 - array-flatten: 1.1.1 - body-parser: 1.20.2 - content-disposition: 0.5.4 - content-type: 1.0.5 - cookie: 0.6.0 - cookie-signature: 1.0.6 - debug: 2.6.9 - depd: 2.0.0 - encodeurl: 1.0.2 - escape-html: 1.0.3 - etag: 1.8.1 - finalhandler: 1.2.0 - fresh: 0.5.2 - http-errors: 2.0.0 - merge-descriptors: 1.0.1 - methods: 1.1.2 - on-finished: 2.4.1 - parseurl: 1.3.3 - path-to-regexp: 0.1.7 - proxy-addr: 2.0.7 - qs: 6.11.0 - range-parser: 1.2.1 - safe-buffer: 5.2.1 - send: 0.18.0 - serve-static: 1.15.0 - setprototypeof: 1.2.0 - statuses: 2.0.1 - type-is: 1.6.18 - utils-merge: 1.0.1 - vary: 1.1.2 - transitivePeerDependencies: - - supports-color - dev: true /extend-shallow@2.0.1: resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==} @@ -12850,6 +12914,11 @@ packages: /fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + /fast-redact@3.5.0: + resolution: {integrity: sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==} + engines: {node: '>=6'} + dev: false + /fast-safe-stringify@2.1.1: resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} dev: true @@ -13800,7 +13869,6 @@ packages: /he@1.2.0: resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} hasBin: true - dev: true /hexoid@1.0.0: resolution: {integrity: sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g==} @@ -13879,6 +13947,17 @@ packages: engines: {node: '>=8'} dev: true + /html-to-text@9.0.5: + resolution: {integrity: sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==} + engines: {node: '>=14'} + dependencies: + '@selderee/plugin-htmlparser2': 0.11.0 + deepmerge: 4.3.1 + dom-serializer: 2.0.0 + htmlparser2: 8.0.2 + selderee: 0.11.0 + dev: false + /htmlparser2@8.0.2: resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==} dependencies: @@ -13886,7 +13965,6 @@ packages: domhandler: 5.0.3 domutils: 3.1.0 entities: 4.5.0 - dev: true /http-auth-connect@1.0.6: resolution: {integrity: sha512-yaO0QSCPqGCjPrl3qEEHjJP+lwZ6gMpXLuCBE06eWwcXomkI5TARtu0kxf9teFuBj6iaV3Ybr15jaWUvbzNzHw==} @@ -13955,7 +14033,7 @@ packages: - supports-color dev: true - /http-proxy-middleware@2.0.6(@types/express@4.17.21): + /http-proxy-middleware@2.0.6(@types/express@4.17.17): resolution: {integrity: sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw==} engines: {node: '>=12.0.0'} peerDependencies: @@ -13964,7 +14042,7 @@ packages: '@types/express': optional: true dependencies: - '@types/express': 4.17.21 + '@types/express': 4.17.17 '@types/http-proxy': 1.17.14 http-proxy: 1.18.1 is-glob: 4.0.3 @@ -14164,6 +14242,20 @@ packages: dev: true optional: true + /imapflow@1.0.162: + resolution: {integrity: sha512-pfx45n2gEIC9MeXAadcfehu5MboUzXqgQiZviKbnIxI6a/QkonOSAMXvBBkWbXQ5FXc9M5IpziJs6TP7jikBrg==} + dependencies: + encoding-japanese: 2.1.0 + iconv-lite: 0.6.3 + libbase64: 1.3.0 + libmime: 5.3.5 + libqp: 2.1.0 + mailsplit: 5.4.0 + nodemailer: 6.9.13 + pino: 9.0.0 + socks: 2.8.3 + dev: false + /immediate@3.0.6: resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} requiresBuild: true @@ -14355,7 +14447,6 @@ packages: dependencies: jsbn: 1.1.0 sprintf-js: 1.1.3 - dev: true /ip-regex@4.3.0: resolution: {integrity: sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q==} @@ -14937,7 +15028,6 @@ packages: /jsbn@1.1.0: resolution: {integrity: sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==} - dev: true /jsdoc-type-pratt-parser@4.0.0: resolution: {integrity: sha512-YtOli5Cmzy3q4dP26GraSOeAhqecewG04hoO8DY56CH4KJ9Fvv5qKWUCCo3HZob7esJQHCv6/+bnTy72xZZaVQ==} @@ -15270,6 +15360,10 @@ packages: engines: {node: '> 0.8'} dev: true + /leac@0.6.0: + resolution: {integrity: sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==} + dev: false + /leek@0.0.24: resolution: {integrity: sha512-6PVFIYXxlYF0o6hrAsHtGpTmi06otkwNrMcmQ0K96SeSRHPREPa9J3nJZ1frliVH7XT0XFswoJFQoXsDukzGNQ==} dependencies: @@ -15322,6 +15416,40 @@ packages: prelude-ls: 1.2.1 type-check: 0.4.0 + /libbase64@1.2.1: + resolution: {integrity: sha512-l+nePcPbIG1fNlqMzrh68MLkX/gTxk/+vdvAb388Ssi7UuUN31MI44w4Yf33mM3Cm4xDfw48mdf3rkdHszLNew==} + dev: false + + /libbase64@1.3.0: + resolution: {integrity: sha512-GgOXd0Eo6phYgh0DJtjQ2tO8dc0IVINtZJeARPeiIJqge+HdsWSuaDTe8ztQ7j/cONByDZ3zeB325AHiv5O0dg==} + dev: false + + /libmime@5.2.0: + resolution: {integrity: sha512-X2U5Wx0YmK0rXFbk67ASMeqYIkZ6E5vY7pNWRKtnNzqjvdYYG8xtPDpCnuUEnPU9vlgNev+JoSrcaKSUaNvfsw==} + dependencies: + encoding-japanese: 2.0.0 + iconv-lite: 0.6.3 + libbase64: 1.2.1 + libqp: 2.0.1 + dev: false + + /libmime@5.3.5: + resolution: {integrity: sha512-nSlR1yRZ43L3cZCiWEw7ali3jY29Hz9CQQ96Oy+sSspYnIP5N54ucOPHqooBsXzwrX1pwn13VUE05q4WmzfaLg==} + dependencies: + encoding-japanese: 2.1.0 + iconv-lite: 0.6.3 + libbase64: 1.3.0 + libqp: 2.1.0 + dev: false + + /libqp@2.0.1: + resolution: {integrity: sha512-Ka0eC5LkF3IPNQHJmYBWljJsw0UvM6j+QdKRbWyCdTmYwvIDE6a7bCm0UkTAL/K+3KXK5qXT/ClcInU01OpdLg==} + dev: false + + /libqp@2.1.0: + resolution: {integrity: sha512-O6O6/fsG5jiUVbvdgT7YX3xY3uIadR6wEZ7+vy9u7PKHAlSEB6blvC1o5pHBjgsi95Uo0aiBBdkyFecj6jtb7A==} + dev: false + /license-checker@25.0.1: resolution: {integrity: sha512-mET5AIwl7MR2IAKYYoVBBpV0OnkKQ1xGj2IMMeEFIs42QAkEVjRtFZGWmQ28WeU7MP779iAgOaOy93Mn44mn6g==} hasBin: true @@ -15372,6 +15500,12 @@ packages: engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} dev: true + /linkify-it@5.0.0: + resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==} + dependencies: + uc.micro: 2.1.0 + dev: false + /listr2@3.14.0(enquirer@2.4.1): resolution: {integrity: sha512-TyWI8G99GX9GjE54cJ+RrNMcIFBfwMPxc3XTFiAYGN4s10hWROGtOg7+O6u6LE3mNkyld7RSLE6nrKBvTfcs3g==} engines: {node: '>=10.0.0'} @@ -15683,6 +15817,29 @@ packages: '@jridgewell/sourcemap-codec': 1.5.0 dev: true + /mailparser@3.7.1: + resolution: {integrity: sha512-RCnBhy5q8XtB3mXzxcAfT1huNqN93HTYYyL6XawlIKycfxM/rXPg9tXoZ7D46+SgCS1zxKzw+BayDQSvncSTTw==} + dependencies: + encoding-japanese: 2.1.0 + he: 1.2.0 + html-to-text: 9.0.5 + iconv-lite: 0.6.3 + libmime: 5.3.5 + linkify-it: 5.0.0 + mailsplit: 5.4.0 + nodemailer: 6.9.13 + punycode.js: 2.3.1 + tlds: 1.252.0 + dev: false + + /mailsplit@5.4.0: + resolution: {integrity: sha512-wnYxX5D5qymGIPYLwnp6h8n1+6P6vz/MJn5AzGjZ8pwICWssL+CCQjWBIToOVHASmATot4ktvlLo6CyLfOXWYA==} + dependencies: + libbase64: 1.2.1 + libmime: 5.2.0 + libqp: 2.0.1 + dev: false + /make-dir@2.1.0: resolution: {integrity: sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==} engines: {node: '>=6'} @@ -16815,7 +16972,6 @@ packages: /node-forge@1.3.1: resolution: {integrity: sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==} engines: {node: '>= 6.13.0'} - dev: true /node-gyp-build@4.8.1: resolution: {integrity: sha512-OSs33Z9yWr148JZcbZd5WiAXhh/n9z8TxQcdMhIOlpN9AhWpLfvVFO73+m77bBABQMaY9XSvIa+qk0jlI7Gcaw==} @@ -16877,8 +17033,13 @@ packages: /node-releases@2.0.18: resolution: {integrity: sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==} - /nodemailer@6.9.1: - resolution: {integrity: sha512-qHw7dOiU5UKNnQpXktdgQ1d3OFgRAekuvbJLcdG5dnEo/GtcTHRYM7+UfJARdOFU9WUQO8OiIamgWPmiSFHYAA==} + /nodemailer@6.9.13: + resolution: {integrity: sha512-7o38Yogx6krdoBf3jCAqnIN4oSQFx+fMa0I7dK1D+me9kBxx12D+/33wSb+fhOCtIxvYJ+4x4IMEhmhCKfAiOA==} + engines: {node: '>=6.0.0'} + dev: false + + /nodemailer@6.9.14: + resolution: {integrity: sha512-Dobp/ebDKBvz91sbtRKhcznLThrKxKt97GI2FAlAyy+fk19j73Uz3sBXolVtmcXjaorivqsbbbjDY+Jkt4/bQA==} engines: {node: '>=6.0.0'} dev: false @@ -17182,6 +17343,11 @@ packages: resolution: {integrity: sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==} dev: true + /on-exit-leak-free@2.1.2: + resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} + engines: {node: '>=14.0.0'} + dev: false + /on-finished@2.3.0: resolution: {integrity: sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==} engines: {node: '>= 0.8'} @@ -17560,6 +17726,13 @@ packages: dependencies: entities: 4.5.0 + /parseley@0.12.1: + resolution: {integrity: sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==} + dependencies: + leac: 0.6.0 + peberminta: 0.9.0 + dev: false + /parseurl@1.3.3: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} @@ -17665,6 +17838,10 @@ packages: xmldoc: 1.3.0 dev: true + /peberminta@0.9.0: + resolution: {integrity: sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==} + dev: false + /pend@1.2.0: resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} dev: true @@ -17701,6 +17878,34 @@ packages: requiresBuild: true dev: true + /pino-abstract-transport@1.2.0: + resolution: {integrity: sha512-Guhh8EZfPCfH+PMXAb6rKOjGQEoy0xlAIn+irODG5kgfYV+BQ0rGYYWTIel3P5mmyXqkYkPmdIkywsn6QKUR1Q==} + dependencies: + readable-stream: 4.5.2 + split2: 4.2.0 + dev: false + + /pino-std-serializers@6.2.2: + resolution: {integrity: sha512-cHjPPsE+vhj/tnhCy/wiMh3M3z3h/j15zHQX+S9GkTBgqJuTuJzYJ4gUyACLhDaJ7kk9ba9iRDmbH2tJU03OiA==} + dev: false + + /pino@9.0.0: + resolution: {integrity: sha512-uI1ThkzTShNSwvsUM6b4ND8ANzWURk9zTELMztFkmnCQeR/4wkomJ+echHee5GMWGovoSfjwdeu80DsFIt7mbA==} + hasBin: true + dependencies: + atomic-sleep: 1.0.0 + fast-redact: 3.5.0 + on-exit-leak-free: 2.1.2 + pino-abstract-transport: 1.2.0 + pino-std-serializers: 6.2.2 + process-warning: 3.0.0 + quick-format-unescaped: 4.0.4 + real-require: 0.2.0 + safe-stable-stringify: 2.4.3 + sonic-boom: 3.8.1 + thread-stream: 2.7.0 + dev: false + /pirates@4.0.6: resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} engines: {node: '>= 6'} @@ -17726,6 +17931,17 @@ packages: find-up: 6.3.0 dev: true + /pkijs@3.1.0: + resolution: {integrity: sha512-N+OCWUp6xrg7OkG+4DIiZUOsp3qMztjq8RGCc1hSY92dsUG8cTlAo7pEkfRGjcdyBv2c1Y9bjAzqdTJAlctuNg==} + engines: {node: '>=12.0.0'} + dependencies: + asn1js: 3.0.5 + bytestreamjs: 2.0.1 + pvtsutils: 1.3.5 + pvutils: 1.1.3 + tslib: 2.6.2 + dev: false + /plantuml-encoder@1.4.0: resolution: {integrity: sha512-sxMwpDw/ySY1WB2CE3+IdMuEcWibJ72DDOsXLkSmEaSzwEUaYBT6DWgOfBiHGCux4q433X6+OEFWjlVqp7gL6g==} dev: false @@ -17770,6 +17986,10 @@ packages: engines: {node: '>= 0.4'} dev: true + /postal-mime@2.2.5: + resolution: {integrity: sha512-6eTJf+B47JMdDuLF/4MBiGpTinxl0W8bA9CzrSoiQrNVRqK8Vhe59VrS6sXh2lG/lgo0bxpZFcWOF4Dv1FaSfg==} + dev: false + /postcss-load-config@3.1.4(ts-node@10.9.2): resolution: {integrity: sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==} engines: {node: '>= 10'} @@ -17958,12 +18178,6 @@ packages: fast-diff: 1.3.0 dev: true - /prettier@2.8.6: - resolution: {integrity: sha512-mtuzdiBbHwPEgl7NxWlqOkithPyp4VN93V7VeHVWBF+ad3I5avc0RVDT4oImXQy9H/AqxA2NSQH8pSxHW6FYbQ==} - engines: {node: '>=10.13.0'} - hasBin: true - dev: true - /prettier@2.8.8: resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} engines: {node: '>=10.13.0'} @@ -18009,10 +18223,13 @@ packages: resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} dev: true + /process-warning@3.0.0: + resolution: {integrity: sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==} + dev: false + /process@0.11.10: resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} engines: {node: '>= 0.6.0'} - dev: true /progress@1.1.8: resolution: {integrity: sha512-UdA8mJ4weIkUBO224tIarHzuHs4HuYiJvsuGT7j/SPQiUJVjYvNDBIPa0hAorduOfjGohB/qHWRa/lrrWX/mXw==} @@ -18134,6 +18351,11 @@ packages: end-of-stream: 1.4.4 once: 1.4.0 + /punycode.js@2.3.1: + resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} + engines: {node: '>=6'} + dev: false + /punycode@1.4.1: resolution: {integrity: sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==} dev: true @@ -18151,6 +18373,17 @@ packages: resolution: {integrity: sha512-LA0Y9kxMYv47GIPJy6MI84fqTd2HmYZI83W/kM/SkKfDlajnZYfmXFTxkbY+xSBPkLJxltMa9hIkmdc29eguMA==} dev: true + /pvtsutils@1.3.5: + resolution: {integrity: sha512-ARvb14YB9Nm2Xi6nBq1ZX6dAM0FsJnuk+31aUp4TrcZEdKUlSqOqsxJHUPJDNE3qiIp+iUPEIeR6Je/tgV7zsA==} + dependencies: + tslib: 2.6.2 + dev: false + + /pvutils@1.1.3: + resolution: {integrity: sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ==} + engines: {node: '>=6.0.0'} + dev: false + /q@1.5.1: resolution: {integrity: sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==} engines: {node: '>=0.6.0', teleport: '>=0.2.0'} @@ -18204,6 +18437,10 @@ packages: resolution: {integrity: sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==} dev: true + /quick-format-unescaped@4.0.4: + resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} + dev: false + /quick-lru@4.0.1: resolution: {integrity: sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==} engines: {node: '>=8'} @@ -18240,7 +18477,6 @@ packages: http-errors: 2.0.0 iconv-lite: 0.4.24 unpipe: 1.0.0 - dev: false /raw-body@2.5.2: resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} @@ -18398,6 +18634,17 @@ packages: string_decoder: 1.3.0 util-deprecate: 1.0.2 + /readable-stream@4.5.2: + resolution: {integrity: sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + abort-controller: 3.0.0 + buffer: 6.0.3 + events: 3.3.0 + process: 0.11.10 + string_decoder: 1.3.0 + dev: false + /readdir-scoped-modules@1.1.0: resolution: {integrity: sha512-asaikDeqAQg7JifRsZn1NJZXo9E+VwlyCfbkZhwyISinqk5zNS6266HS5kah6P0SaQKGF6SkNnZVHUzHFYxYDw==} deprecated: This functionality has been moved to @npmcli/fs @@ -18415,6 +18662,11 @@ packages: picomatch: 2.3.1 dev: true + /real-require@0.2.0: + resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} + engines: {node: '>= 12.13.0'} + dev: false + /rechoir@0.6.2: resolution: {integrity: sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==} engines: {node: '>= 0.10'} @@ -18882,9 +19134,9 @@ packages: engines: {node: '>= 12.13.0'} dependencies: '@types/json-schema': 7.0.15 - ajv: 8.17.1 - ajv-formats: 2.1.1(ajv@8.17.1) - ajv-keywords: 5.1.0(ajv@8.17.1) + ajv: 8.12.0 + ajv-formats: 2.1.1(ajv@8.12.0) + ajv-keywords: 5.1.0(ajv@8.12.0) dev: true /secure-compare@3.0.1: @@ -18895,6 +19147,12 @@ packages: resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==} dev: false + /selderee@0.11.0: + resolution: {integrity: sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==} + dependencies: + parseley: 0.12.1 + dev: false + /select-hose@2.0.0: resolution: {integrity: sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==} dev: true @@ -19386,7 +19644,12 @@ packages: dependencies: ip-address: 9.0.5 smart-buffer: 4.2.0 - dev: true + + /sonic-boom@3.8.1: + resolution: {integrity: sha512-y4Z8LCDBuum+PBP3lSV7RHrXscqksve/bi0as7mhwVnBW+/wUqKT/2Kb7um8yqcFy0duYbbPxzt89Zy2nOCaxg==} + dependencies: + atomic-sleep: 1.0.0 + dev: false /sort-asc@0.2.0: resolution: {integrity: sha512-umMGhjPeHAI6YjABoSTrFp2zaBtXBej1a0yKkuMUyjjqu6FJsTF+JYwCswWDg+zJfk/5npWUUbd33HH/WLzpaA==} @@ -19551,7 +19814,6 @@ packages: /split2@4.2.0: resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} engines: {node: '>= 10.x'} - dev: true /split@0.3.1: resolution: {integrity: sha512-hCHXkQDs1HFKRsrT9EutGT1hmjS1FW1Aei8dk/CxrT7mslcMtAxbiv8LYA/AYDvjB6h9rSXgW8zAZwg20tKMTw==} @@ -19571,7 +19833,6 @@ packages: /sprintf-js@1.1.3: resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==} - dev: true /ssh-config@1.1.6: resolution: {integrity: sha512-ZPO9rECxzs5JIQ6G/2EfL1I9ho/BVZkx9HRKn8+0af7QgwAmumQ7XBFP1ggMyPMo+/tUbmv0HFdv4qifdO/9JA==} @@ -20398,6 +20659,12 @@ packages: any-promise: 1.3.0 dev: true + /thread-stream@2.7.0: + resolution: {integrity: sha512-qQiRWsU/wvNolI6tbbCKd9iKaTnCXsTwVxhhKM6nctPdujTyztjlbUkUTUymidWcMnZ5pWR0ej4a0tjsW021vw==} + dependencies: + real-require: 0.2.0 + dev: false + /throttleit@1.0.1: resolution: {integrity: sha512-vDZpf9Chs9mAdfY046mcPt8fg5QSZr37hEH4TXYBnDF+izxgrbRGUAAaBvIk/fJm9aOFCGFd1EsNg5AZCbnQCQ==} dev: true @@ -20442,6 +20709,11 @@ packages: resolution: {integrity: sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA==} dev: false + /tlds@1.252.0: + resolution: {integrity: sha512-GA16+8HXvqtfEnw/DTcwB0UU354QE1n3+wh08oFjr6Znl7ZLAeUgYzCcK+/CCrOyE0vnHR8/pu3XXG3vDijXpQ==} + hasBin: true + dev: false + /tmp@0.0.33: resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} engines: {node: '>=0.6.0'} @@ -21021,6 +21293,10 @@ packages: resolution: {integrity: sha512-fYmIy7fKTSFAhG3fuPlubeGaMoAd6r0rSnfEsO5nEY55i26KSLt9EH7PLQiiqPUhNqYIJvSkTy1oArIcXAbPbA==} dev: true + /uc.micro@2.1.0: + resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} + dev: false + /uglify-js@3.19.1: resolution: {integrity: sha512-y/2wiW+ceTYR2TSSptAhfnEtpLaQ4Ups5zrjB2d3kuVxHj16j/QJwPl5PvuGy9uARb39J0+iKxcRPvtpsx4A4A==} engines: {node: '>=0.8.0'} @@ -21526,7 +21802,7 @@ packages: dependencies: '@types/bonjour': 3.5.13 '@types/connect-history-api-fallback': 1.5.4 - '@types/express': 4.17.21 + '@types/express': 4.17.17 '@types/serve-index': 1.9.4 '@types/serve-static': 1.15.7 '@types/sockjs': 0.3.36 @@ -21538,10 +21814,10 @@ packages: compression: 1.7.4 connect-history-api-fallback: 2.0.0 default-gateway: 6.0.3 - express: 4.19.2 + express: 4.18.2 graceful-fs: 4.2.11 html-entities: 2.5.2 - http-proxy-middleware: 2.0.6(@types/express@4.17.21) + http-proxy-middleware: 2.0.6(@types/express@4.17.17) ipaddr.js: 2.2.0 launch-editor: 2.8.0 open: 8.4.2 @@ -22050,6 +22326,10 @@ packages: resolution: {integrity: sha512-C1x6lfvBICFTQIMgbt3JqMOno3VOtkWat/xEakLTOurskYIHPmzJrzd1e8BnmtdDVJlGuk5D+FxyCA8MPmkIyA==} dev: true + /zod@3.23.8: + resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==} + dev: false + /zone.js@0.14.4: resolution: {integrity: sha512-NtTUvIlNELez7Q1DzKVIFZBzNb646boQMgpATo9z3Ftuu/gWvzxCW7jdjcUDoRGxRikrhVHB/zLXh1hxeJawvw==} dependencies: