@technomoron/mail-magic 1.0.5 → 1.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.do-realease.sh +40 -1
- package/.env-dist +9 -0
- package/.vscode/extensions.json +1 -13
- package/.vscode/settings.json +13 -114
- package/CHANGES +25 -0
- package/README.md +6 -0
- package/TUTORIAL.MD +57 -55
- package/dist/api/assets.js +77 -0
- package/dist/api/forms.js +31 -4
- package/dist/api/mailer.js +34 -8
- package/dist/index.js +5 -1
- package/dist/models/form.js +11 -1
- package/dist/models/init.js +43 -31
- package/dist/models/txmail.js +13 -4
- package/dist/store/envloader.js +12 -20
- package/dist/store/store.js +1 -0
- package/dist/util.js +23 -0
- package/eslint.config.mjs +133 -41
- package/lintconfig.cjs +81 -0
- package/package.json +30 -20
- package/src/api/assets.ts +92 -0
- package/src/api/forms.ts +33 -5
- package/src/api/mailer.ts +36 -9
- package/src/index.ts +5 -1
- package/src/models/form.ts +12 -1
- package/src/models/init.ts +46 -43
- package/src/models/txmail.ts +14 -6
- package/src/store/envloader.ts +12 -20
- package/src/store/store.ts +2 -0
- package/src/util.ts +26 -0
- package/tests/fixtures/certs/test.crt +19 -0
- package/tests/fixtures/certs/test.key +28 -0
- package/tests/helpers/test-setup.ts +316 -0
- package/tests/mail-magic.test.ts +154 -0
- package/vitest.config.ts +11 -0
package/.do-realease.sh
CHANGED
|
@@ -4,7 +4,46 @@ VERSION=$(node -p "require('./package.json').version")
|
|
|
4
4
|
|
|
5
5
|
echo "Creating release for ${VERSION}"
|
|
6
6
|
|
|
7
|
+
if [ -n "$(git status --porcelain)" ]; then
|
|
8
|
+
echo "Working tree is not clean. Commit or stash changes before release." >&2
|
|
9
|
+
exit 1
|
|
10
|
+
fi
|
|
11
|
+
|
|
12
|
+
UPSTREAM=$(git rev-parse --abbrev-ref --symbolic-full-name @{u} 2>/dev/null || true)
|
|
13
|
+
if [ -z "$UPSTREAM" ]; then
|
|
14
|
+
echo "No upstream configured for $(git rev-parse --abbrev-ref HEAD). Set upstream before release." >&2
|
|
15
|
+
exit 1
|
|
16
|
+
fi
|
|
17
|
+
|
|
18
|
+
if ! git fetch --quiet; then
|
|
19
|
+
echo "Failed to fetch remote updates. Check your network or remote access." >&2
|
|
20
|
+
exit 1
|
|
21
|
+
fi
|
|
22
|
+
|
|
23
|
+
set -- $(git rev-list --left-right --count "${UPSTREAM}...HEAD")
|
|
24
|
+
BEHIND_COUNT=$1
|
|
25
|
+
AHEAD_COUNT=$2
|
|
26
|
+
|
|
27
|
+
if [ "$BEHIND_COUNT" -ne 0 ] || [ "$AHEAD_COUNT" -ne 0 ]; then
|
|
28
|
+
echo "Branch is not in sync with ${UPSTREAM} (behind ${BEHIND_COUNT}, ahead ${AHEAD_COUNT})." >&2
|
|
29
|
+
echo "Pull/push until the branch matches upstream before release." >&2
|
|
30
|
+
exit 1
|
|
31
|
+
fi
|
|
32
|
+
|
|
33
|
+
if git rev-parse -q --verify "refs/tags/v${VERSION}" >/dev/null; then
|
|
34
|
+
echo "Tag v${VERSION} already exists. Aborting." >&2
|
|
35
|
+
exit 1
|
|
36
|
+
fi
|
|
37
|
+
|
|
7
38
|
git tag -a "v${VERSION}" -m "Release version ${VERSION}"
|
|
8
39
|
git push origin "v${VERSION}"
|
|
9
40
|
|
|
10
|
-
|
|
41
|
+
# detect prerelease versions (contains a hyphen)
|
|
42
|
+
if echo "$VERSION" | grep -q "-"; then
|
|
43
|
+
TAG=$(echo "$VERSION" | sed 's/^[0-9.]*-\([a-zA-Z0-9]*\).*/\1/')
|
|
44
|
+
echo "Detected prerelease. Publishing with tag '$TAG'"
|
|
45
|
+
npm publish --tag "$TAG" --access=public
|
|
46
|
+
else
|
|
47
|
+
echo "Stable release. Publishing as latest"
|
|
48
|
+
npm publish --access=public
|
|
49
|
+
fi
|
package/.env-dist
CHANGED
|
@@ -16,6 +16,15 @@ DB_FORCE_SYNC=false
|
|
|
16
16
|
# Sets the public URL for the API (i.e. https://ml.example.com:3790)
|
|
17
17
|
API_URL=http://localhost:3776
|
|
18
18
|
|
|
19
|
+
# Enable the Swagger/OpenAPI endpoint [boolean]
|
|
20
|
+
SWAGGER_ENABLED=false
|
|
21
|
+
|
|
22
|
+
# Path to expose the Swagger/OpenAPI spec (default: /api/swagger when enabled)
|
|
23
|
+
SWAGGER_PATH=
|
|
24
|
+
|
|
25
|
+
# Route prefix exposed for config assets
|
|
26
|
+
ASSET_ROUTE=/asset
|
|
27
|
+
|
|
19
28
|
# Path to directory where config files are located
|
|
20
29
|
CONFIG_PATH=./config/
|
|
21
30
|
|
package/.vscode/extensions.json
CHANGED
|
@@ -1,15 +1,3 @@
|
|
|
1
1
|
{
|
|
2
|
-
"recommendations": [
|
|
3
|
-
"dbaeumer.vscode-eslint",
|
|
4
|
-
"esbenp.prettier-vscode",
|
|
5
|
-
"editorconfig.editorconfig",
|
|
6
|
-
"Vue.volar",
|
|
7
|
-
"wayou.vscode-todo-highlight"
|
|
8
|
-
],
|
|
9
|
-
"unwantedRecommendations": [
|
|
10
|
-
"octref.vetur",
|
|
11
|
-
"hookyqr.beautify",
|
|
12
|
-
"dbaeumer.jshint",
|
|
13
|
-
"ms-vscode.vscode-typescript-tslint-plugin"
|
|
14
|
-
]
|
|
2
|
+
"recommendations": ["dbaeumer.vscode-eslint", "esbenp.prettier-vscode"]
|
|
15
3
|
}
|
package/.vscode/settings.json
CHANGED
|
@@ -1,123 +1,22 @@
|
|
|
1
1
|
{
|
|
2
|
-
"editor.detectIndentation": false,
|
|
3
|
-
"editor.insertSpaces": false,
|
|
4
|
-
"editor.tabSize": 4,
|
|
5
|
-
"editor.bracketPairColorization.enabled": true,
|
|
6
|
-
"editor.guides.bracketPairs": true,
|
|
7
2
|
"editor.formatOnSave": true,
|
|
8
3
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
|
9
|
-
"
|
|
10
|
-
"
|
|
11
|
-
"source.fixAll.eslint": "always"
|
|
4
|
+
"[javascript]": {
|
|
5
|
+
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
|
12
6
|
},
|
|
13
|
-
"[
|
|
7
|
+
"[typescript]": {
|
|
8
|
+
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
|
9
|
+
},
|
|
10
|
+
"[vue]": {
|
|
14
11
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
|
15
12
|
},
|
|
16
|
-
"[
|
|
13
|
+
"[json]": {
|
|
17
14
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
|
18
15
|
},
|
|
19
|
-
"
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
"eslint.
|
|
23
|
-
"eslint.
|
|
24
|
-
"
|
|
25
|
-
"eslint.useFlatConfig": true,
|
|
26
|
-
"eslint.probe": ["javascript", "javascriptreact", "typescript", "typescriptreact", "vue"],
|
|
27
|
-
"eslint.validate": ["javascript", "javascriptreact", "typescript", "typescriptreact", "vue"],
|
|
28
|
-
"explorer.fileNesting.enabled": true,
|
|
29
|
-
"explorer.fileNesting.expand": false,
|
|
30
|
-
"explorer.fileNesting.patterns": {
|
|
31
|
-
".clang-tidy": ".clang-format, .clangd, compile_commands.json",
|
|
32
|
-
".env": "*.env, .env.*, .envrc, env.d.ts",
|
|
33
|
-
".gitignore": ".gitattributes, .gitmodules, .gitmessage, .lfsconfig, .mailmap, .git-blame*",
|
|
34
|
-
".project": ".classpath",
|
|
35
|
-
"+layout.svelte": "+layout.ts,+layout.ts,+layout.js,+layout.server.ts,+layout.server.js,+layout.gql",
|
|
36
|
-
"+page.svelte": "+page.server.ts,+page.server.js,+page.ts,+page.js,+page.gql",
|
|
37
|
-
"ansible.cfg": "ansible.cfg, .ansible-lint, requirements.yml",
|
|
38
|
-
"app.config.*": "*.env, .babelrc*, .codecov, .cssnanorc*, .env.*, .envrc, .htmlnanorc*, .lighthouserc.*, .mocha*, .postcssrc*, .terserrc*, api-extractor.json, ava.config.*, babel.config.*, capacitor.config.*, content.config.*, contentlayer.config.*, cssnano.config.*, cypress.*, env.d.ts, formkit.config.*, formulate.config.*, histoire.config.*, htmlnanorc.*, i18n.config.*, ionic.config.*, jasmine.*, jest.config.*, jsconfig.*, karma*, lighthouserc.*, panda.config.*, playwright.config.*, postcss.config.*, puppeteer.config.*, react-router.config.*, rspack.config.*, sst.config.*, svgo.config.*, tailwind.config.*, tsconfig.*, tsdoc.*, uno.config.*, unocss.config.*, vitest.config.*, vuetify.config.*, webpack.config.*, windi.config.*",
|
|
39
|
-
"application.properties": "*.properties",
|
|
40
|
-
"artisan": "*.env, .babelrc*, .codecov, .cssnanorc*, .env.*, .envrc, .htmlnanorc*, .lighthouserc.*, .mocha*, .postcssrc*, .terserrc*, api-extractor.json, ava.config.*, babel.config.*, capacitor.config.*, content.config.*, contentlayer.config.*, cssnano.config.*, cypress.*, env.d.ts, formkit.config.*, formulate.config.*, histoire.config.*, htmlnanorc.*, i18n.config.*, ionic.config.*, jasmine.*, jest.config.*, jsconfig.*, karma*, lighthouserc.*, panda.config.*, playwright.config.*, postcss.config.*, puppeteer.config.*, react-router.config.*, rspack.config.*, server.php, sst.config.*, svgo.config.*, tailwind.config.*, tsconfig.*, tsdoc.*, uno.config.*, unocss.config.*, vitest.config.*, vuetify.config.*, webpack.config.*, webpack.mix.js, windi.config.*",
|
|
41
|
-
"astro.config.*": "*.env, .babelrc*, .codecov, .cssnanorc*, .env.*, .envrc, .htmlnanorc*, .lighthouserc.*, .mocha*, .postcssrc*, .terserrc*, api-extractor.json, ava.config.*, babel.config.*, capacitor.config.*, content.config.*, contentlayer.config.*, cssnano.config.*, cypress.*, env.d.ts, formkit.config.*, formulate.config.*, histoire.config.*, htmlnanorc.*, i18n.config.*, ionic.config.*, jasmine.*, jest.config.*, jsconfig.*, karma*, lighthouserc.*, panda.config.*, playwright.config.*, postcss.config.*, puppeteer.config.*, react-router.config.*, rspack.config.*, sst.config.*, svgo.config.*, tailwind.config.*, tsconfig.*, tsdoc.*, uno.config.*, unocss.config.*, vitest.config.*, vuetify.config.*, webpack.config.*, windi.config.*",
|
|
42
|
-
"build-wrapper.log": "build-wrapper*.log, build-wrapper-dump*.json, build-wrapper-win*.exe, build-wrapper-linux*, build-wrapper-macosx*",
|
|
43
|
-
"BUILD.bazel": "*.bzl, *.bazel, *.bazelrc, bazel.rc, .bazelignore, .bazelproject, .bazelversion, MODULE.bazel.lock, WORKSPACE",
|
|
44
|
-
"Cargo.toml": ".clippy.toml, .rustfmt.toml, Cargo.Bazel.lock, Cargo.lock, clippy.toml, cross.toml, insta.yaml, rust-toolchain.toml, rustfmt.toml",
|
|
45
|
-
"CMakeLists.txt": "*.cmake, *.cmake.in, .cmake-format.yaml, CMakePresets.json, CMakeCache.txt",
|
|
46
|
-
"composer.json": ".php*.cache, composer.lock, phpunit.xml*, psalm*.xml",
|
|
47
|
-
"default.nix": "shell.nix",
|
|
48
|
-
"deno.json*": "*.env, .env.*, .envrc, api-extractor.json, deno.lock, env.d.ts, import-map.json, import_map.json, jsconfig.*, tsconfig.*, tsdoc.*",
|
|
49
|
-
"Dockerfile": "*.dockerfile, .devcontainer.*, .dockerignore, captain-definition, compose.*, docker-compose.*, dockerfile*",
|
|
50
|
-
"flake.nix": "flake.lock",
|
|
51
|
-
"gatsby-config.*": "*.env, .babelrc*, .codecov, .cssnanorc*, .env.*, .envrc, .htmlnanorc*, .lighthouserc.*, .mocha*, .postcssrc*, .terserrc*, api-extractor.json, ava.config.*, babel.config.*, capacitor.config.*, content.config.*, contentlayer.config.*, cssnano.config.*, cypress.*, env.d.ts, formkit.config.*, formulate.config.*, gatsby-browser.*, gatsby-node.*, gatsby-ssr.*, gatsby-transformer.*, histoire.config.*, htmlnanorc.*, i18n.config.*, ionic.config.*, jasmine.*, jest.config.*, jsconfig.*, karma*, lighthouserc.*, panda.config.*, playwright.config.*, postcss.config.*, puppeteer.config.*, react-router.config.*, rspack.config.*, sst.config.*, svgo.config.*, tailwind.config.*, tsconfig.*, tsdoc.*, uno.config.*, unocss.config.*, vitest.config.*, vuetify.config.*, webpack.config.*, windi.config.*",
|
|
52
|
-
"gemfile": ".ruby-version, gemfile.lock",
|
|
53
|
-
"go.mod": ".air*, go.sum",
|
|
54
|
-
"go.work": "go.work.sum",
|
|
55
|
-
"hatch.toml": ".editorconfig, .flake8, .isort.cfg, .python-version, hatch.toml, requirements*.in, requirements*.pip, requirements*.txt, tox.ini",
|
|
56
|
-
"I*.cs": "$(capture).cs",
|
|
57
|
-
"Makefile": "*.mk",
|
|
58
|
-
"mix.exs": ".credo.exs, .dialyzer_ignore.exs, .formatter.exs, .iex.exs, .tool-versions, mix.lock",
|
|
59
|
-
"next.config.*": "*.env, .babelrc*, .codecov, .cssnanorc*, .env.*, .envrc, .htmlnanorc*, .lighthouserc.*, .mocha*, .postcssrc*, .terserrc*, api-extractor.json, ava.config.*, babel.config.*, capacitor.config.*, content.config.*, contentlayer.config.*, cssnano.config.*, cypress.*, env.d.ts, formkit.config.*, formulate.config.*, histoire.config.*, htmlnanorc.*, i18n.config.*, ionic.config.*, jasmine.*, jest.config.*, jsconfig.*, karma*, lighthouserc.*, next-env.d.ts, next-i18next.config.*, panda.config.*, playwright.config.*, postcss.config.*, puppeteer.config.*, react-router.config.*, rspack.config.*, sst.config.*, svgo.config.*, tailwind.config.*, tsconfig.*, tsdoc.*, uno.config.*, unocss.config.*, vitest.config.*, vuetify.config.*, webpack.config.*, windi.config.*",
|
|
60
|
-
"nuxt.config.*": "*.env, .babelrc*, .codecov, .cssnanorc*, .env.*, .envrc, .htmlnanorc*, .lighthouserc.*, .mocha*, .nuxtignore, .nuxtrc, .postcssrc*, .terserrc*, api-extractor.json, ava.config.*, babel.config.*, capacitor.config.*, content.config.*, contentlayer.config.*, cssnano.config.*, cypress.*, env.d.ts, formkit.config.*, formulate.config.*, histoire.config.*, htmlnanorc.*, i18n.config.*, ionic.config.*, jasmine.*, jest.config.*, jsconfig.*, karma*, lighthouserc.*, panda.config.*, playwright.config.*, postcss.config.*, puppeteer.config.*, react-router.config.*, rspack.config.*, sst.config.*, svgo.config.*, tailwind.config.*, tsconfig.*, tsdoc.*, uno.config.*, unocss.config.*, vitest.config.*, vuetify.config.*, webpack.config.*, windi.config.*",
|
|
61
|
-
"package.json": "*.code-workspace, .browserslist*, .circleci*, .commitlint*, .cspell*, .cursor*, .cz-config.js, .czrc, .dlint.json, .dprint.json*, .editorconfig, .eslint*, .firebase*, .flowconfig, .github*, .gitlab*, .gitmojirc.json, .gitpod*, .huskyrc*, .jslint*, .knip.*, .lintstagedrc*, .ls-lint.yml, .markdownlint*, .node-version, .nodemon*, .npm*, .nvmrc, .pm2*, .pnp.*, .pnpm*, .prettier*, .pylintrc, .release-please*.json, .releaserc*, .ruff.toml, .sentry*, .shellcheckrc, .simple-git-hooks*, .stackblitz*, .styleci*, .stylelint*, .tazerc*, .textlint*, .tool-versions, .travis*, .versionrc*, .vscode*, .watchman*, .windsurfrules, .xo-config*, .yamllint*, .yarnrc*, Procfile, apollo.config.*, appveyor*, azure-pipelines*, biome.json*, bower.json, build.config.*, bun.lock, bun.lockb, bunfig.toml, colada.options.ts, commitlint*, crowdin*, cspell*, dangerfile*, dlint.json, dprint.json*, ec.config.*, electron-builder.*, eslint*, firebase.json, grunt*, gulp*, jenkins*, knip.*, lerna*, lint-staged*, nest-cli.*, netlify*, nixpacks*, nodemon*, npm-shrinkwrap.json, nx.*, package-lock.json, package.nls*.json, phpcs.xml, pm2.*, pnpm*, prettier*, pullapprove*, pyrightconfig.json, release-please*.json, release-tasks.sh, release.config.*, renovate*, rolldown.config.*, rollup.config.*, rspack*, ruff.toml, sentry.*.config.ts, simple-git-hooks*, sonar-project.properties, stylelint*, tsdown.config.*, tslint*, tsup.config.*, turbo*, typedoc*, unlighthouse*, vercel*, vetur.config.*, webpack*, workspace.json, wrangler.*, xo.config.*, yarn*",
|
|
62
|
-
"Pipfile": ".editorconfig, .flake8, .isort.cfg, .python-version, Pipfile, Pipfile.lock, requirements*.in, requirements*.pip, requirements*.txt, tox.ini",
|
|
63
|
-
"pom.xml": "mvnw*",
|
|
64
|
-
"pubspec.yaml": ".metadata, .packages, all_lint_rules.yaml, analysis_options.yaml, build.yaml, pubspec.lock, pubspec_overrides.yaml",
|
|
65
|
-
"pyproject.toml": ".commitlint*, .cspell*, .dlint.json, .dprint.json*, .editorconfig, .eslint*, .flake8, .flowconfig, .isort.cfg, .jslint*, .lintstagedrc*, .ls-lint.yml, .markdownlint*, .pdm-python, .pdm.toml, .prettier*, .pylintrc, .python-version, .ruff.toml, .shellcheckrc, .stylelint*, .textlint*, .xo-config*, .yamllint*, MANIFEST.in, Pipfile, Pipfile.lock, biome.json*, commitlint*, cspell*, dangerfile*, dlint.json, dprint.json*, eslint*, hatch.toml, lint-staged*, pdm.lock, phpcs.xml, poetry.lock, poetry.toml, prettier*, pyproject.toml, pyrightconfig.json, requirements*.in, requirements*.pip, requirements*.txt, ruff.toml, setup.cfg, setup.py, stylelint*, tox.ini, tslint*, uv.lock, uv.toml, xo.config.*",
|
|
66
|
-
"quasar.conf.js": "*.env, .babelrc*, .codecov, .cssnanorc*, .env.*, .envrc, .htmlnanorc*, .lighthouserc.*, .mocha*, .postcssrc*, .terserrc*, api-extractor.json, ava.config.*, babel.config.*, capacitor.config.*, content.config.*, contentlayer.config.*, cssnano.config.*, cypress.*, env.d.ts, formkit.config.*, formulate.config.*, histoire.config.*, htmlnanorc.*, i18n.config.*, ionic.config.*, jasmine.*, jest.config.*, jsconfig.*, karma*, lighthouserc.*, panda.config.*, playwright.config.*, postcss.config.*, puppeteer.config.*, quasar.extensions.json, react-router.config.*, rspack.config.*, sst.config.*, svgo.config.*, tailwind.config.*, tsconfig.*, tsdoc.*, uno.config.*, unocss.config.*, vitest.config.*, vuetify.config.*, webpack.config.*, windi.config.*",
|
|
67
|
-
"readme*": "AUTHORS, Authors, BACKERS*, Backers*, CHANGELOG*, CITATION*, CODEOWNERS, CODE_OF_CONDUCT*, CONTRIBUTING*, CONTRIBUTORS, COPYING*, CREDITS, Changelog*, Citation*, Code_Of_Conduct*, Codeowners, Contributing*, Contributors, Copying*, Credits, GOVERNANCE.MD, Governance.md, HISTORY.MD, History.md, LICENSE*, License*, MAINTAINERS, Maintainers, README-*, README_*, RELEASE_NOTES*, ROADMAP.MD, Readme-*, Readme_*, Release_Notes*, Roadmap.md, SECURITY.MD, SPONSORS*, Security.md, Sponsors*, authors, backers*, changelog*, citation*, code_of_conduct*, codeowners, contributing*, contributors, copying*, credits, governance.md, history.md, license*, maintainers, readme-*, readme_*, release_notes*, roadmap.md, security.md, sponsors*",
|
|
68
|
-
"Readme*": "AUTHORS, Authors, BACKERS*, Backers*, CHANGELOG*, CITATION*, CODEOWNERS, CODE_OF_CONDUCT*, CONTRIBUTING*, CONTRIBUTORS, COPYING*, CREDITS, Changelog*, Citation*, Code_Of_Conduct*, Codeowners, Contributing*, Contributors, Copying*, Credits, GOVERNANCE.MD, Governance.md, HISTORY.MD, History.md, LICENSE*, License*, MAINTAINERS, Maintainers, README-*, README_*, RELEASE_NOTES*, ROADMAP.MD, Readme-*, Readme_*, Release_Notes*, Roadmap.md, SECURITY.MD, SPONSORS*, Security.md, Sponsors*, authors, backers*, changelog*, citation*, code_of_conduct*, codeowners, contributing*, contributors, copying*, credits, governance.md, history.md, license*, maintainers, readme-*, readme_*, release_notes*, roadmap.md, security.md, sponsors*",
|
|
69
|
-
"README*": "AUTHORS, Authors, BACKERS*, Backers*, CHANGELOG*, CITATION*, CODEOWNERS, CODE_OF_CONDUCT*, CONTRIBUTING*, CONTRIBUTORS, COPYING*, CREDITS, Changelog*, Citation*, Code_Of_Conduct*, Codeowners, Contributing*, Contributors, Copying*, Credits, GOVERNANCE.MD, Governance.md, HISTORY.MD, History.md, LICENSE*, License*, MAINTAINERS, Maintainers, README-*, README_*, RELEASE_NOTES*, ROADMAP.MD, Readme-*, Readme_*, Release_Notes*, Roadmap.md, SECURITY.MD, SPONSORS*, Security.md, Sponsors*, authors, backers*, changelog*, citation*, code_of_conduct*, codeowners, contributing*, contributors, copying*, credits, governance.md, history.md, license*, maintainers, readme-*, readme_*, release_notes*, roadmap.md, security.md, sponsors*",
|
|
70
|
-
"remix.config.*": "*.env, .babelrc*, .codecov, .cssnanorc*, .env.*, .envrc, .htmlnanorc*, .lighthouserc.*, .mocha*, .postcssrc*, .terserrc*, api-extractor.json, ava.config.*, babel.config.*, capacitor.config.*, content.config.*, contentlayer.config.*, cssnano.config.*, cypress.*, env.d.ts, formkit.config.*, formulate.config.*, histoire.config.*, htmlnanorc.*, i18n.config.*, ionic.config.*, jasmine.*, jest.config.*, jsconfig.*, karma*, lighthouserc.*, panda.config.*, playwright.config.*, postcss.config.*, puppeteer.config.*, react-router.config.*, remix.*, rspack.config.*, sst.config.*, svgo.config.*, tailwind.config.*, tsconfig.*, tsdoc.*, uno.config.*, unocss.config.*, vitest.config.*, vuetify.config.*, webpack.config.*, windi.config.*",
|
|
71
|
-
"requirements.txt": ".editorconfig, .flake8, .isort.cfg, .python-version, requirements*.in, requirements*.pip, requirements*.txt, tox.ini",
|
|
72
|
-
"rush.json": "*.code-workspace, .browserslist*, .circleci*, .commitlint*, .cspell*, .cursor*, .cz-config.js, .czrc, .dlint.json, .dprint.json*, .editorconfig, .eslint*, .firebase*, .flowconfig, .github*, .gitlab*, .gitmojirc.json, .gitpod*, .huskyrc*, .jslint*, .knip.*, .lintstagedrc*, .ls-lint.yml, .markdownlint*, .node-version, .nodemon*, .npm*, .nvmrc, .pm2*, .pnp.*, .pnpm*, .prettier*, .pylintrc, .release-please*.json, .releaserc*, .ruff.toml, .sentry*, .shellcheckrc, .simple-git-hooks*, .stackblitz*, .styleci*, .stylelint*, .tazerc*, .textlint*, .tool-versions, .travis*, .versionrc*, .vscode*, .watchman*, .windsurfrules, .xo-config*, .yamllint*, .yarnrc*, Procfile, apollo.config.*, appveyor*, azure-pipelines*, biome.json*, bower.json, build.config.*, bun.lock, bun.lockb, bunfig.toml, colada.options.ts, commitlint*, crowdin*, cspell*, dangerfile*, dlint.json, dprint.json*, ec.config.*, electron-builder.*, eslint*, firebase.json, grunt*, gulp*, jenkins*, knip.*, lerna*, lint-staged*, nest-cli.*, netlify*, nixpacks*, nodemon*, npm-shrinkwrap.json, nx.*, package-lock.json, package.nls*.json, phpcs.xml, pm2.*, pnpm*, prettier*, pullapprove*, pyrightconfig.json, release-please*.json, release-tasks.sh, release.config.*, renovate*, rolldown.config.*, rollup.config.*, rspack*, ruff.toml, sentry.*.config.ts, simple-git-hooks*, sonar-project.properties, stylelint*, tsdown.config.*, tslint*, tsup.config.*, turbo*, typedoc*, unlighthouse*, vercel*, vetur.config.*, webpack*, workspace.json, wrangler.*, xo.config.*, yarn*",
|
|
73
|
-
"sanity.config.*": "sanity.cli.*, sanity.types.ts, schema.json",
|
|
74
|
-
"setup.cfg": ".editorconfig, .flake8, .isort.cfg, .python-version, MANIFEST.in, requirements*.in, requirements*.pip, requirements*.txt, setup.cfg, tox.ini",
|
|
75
|
-
"setup.py": ".editorconfig, .flake8, .isort.cfg, .python-version, MANIFEST.in, requirements*.in, requirements*.pip, requirements*.txt, setup.cfg, setup.py, tox.ini",
|
|
76
|
-
"shims.d.ts": "*.d.ts",
|
|
77
|
-
"svelte.config.*": "*.env, .babelrc*, .codecov, .cssnanorc*, .env.*, .envrc, .htmlnanorc*, .lighthouserc.*, .mocha*, .postcssrc*, .terserrc*, api-extractor.json, ava.config.*, babel.config.*, capacitor.config.*, content.config.*, contentlayer.config.*, cssnano.config.*, cypress.*, env.d.ts, formkit.config.*, formulate.config.*, histoire.config.*, houdini.config.*, htmlnanorc.*, i18n.config.*, ionic.config.*, jasmine.*, jest.config.*, jsconfig.*, karma*, lighthouserc.*, mdsvex.config.js, panda.config.*, playwright.config.*, postcss.config.*, puppeteer.config.*, react-router.config.*, rspack.config.*, sst.config.*, svgo.config.*, tailwind.config.*, tsconfig.*, tsdoc.*, uno.config.*, unocss.config.*, vite.config.*, vitest.config.*, vuetify.config.*, webpack.config.*, windi.config.*",
|
|
78
|
-
"vite.config.*": "*.env, .babelrc*, .codecov, .cssnanorc*, .env.*, .envrc, .htmlnanorc*, .lighthouserc.*, .mocha*, .postcssrc*, .terserrc*, api-extractor.json, ava.config.*, babel.config.*, capacitor.config.*, content.config.*, contentlayer.config.*, cssnano.config.*, cypress.*, env.d.ts, formkit.config.*, formulate.config.*, histoire.config.*, htmlnanorc.*, i18n.config.*, ionic.config.*, jasmine.*, jest.config.*, jsconfig.*, karma*, lighthouserc.*, panda.config.*, playwright.config.*, postcss.config.*, puppeteer.config.*, react-router.config.*, rspack.config.*, sst.config.*, svgo.config.*, tailwind.config.*, tsconfig.*, tsdoc.*, uno.config.*, unocss.config.*, vitest.config.*, vuetify.config.*, webpack.config.*, windi.config.*",
|
|
79
|
-
"vue.config.*": "*.env, .babelrc*, .codecov, .cssnanorc*, .env.*, .envrc, .htmlnanorc*, .lighthouserc.*, .mocha*, .postcssrc*, .terserrc*, api-extractor.json, ava.config.*, babel.config.*, capacitor.config.*, content.config.*, contentlayer.config.*, cssnano.config.*, cypress.*, env.d.ts, formkit.config.*, formulate.config.*, histoire.config.*, htmlnanorc.*, i18n.config.*, ionic.config.*, jasmine.*, jest.config.*, jsconfig.*, karma*, lighthouserc.*, panda.config.*, playwright.config.*, postcss.config.*, puppeteer.config.*, react-router.config.*, rspack.config.*, sst.config.*, svgo.config.*, tailwind.config.*, tsconfig.*, tsdoc.*, uno.config.*, unocss.config.*, vitest.config.*, vuetify.config.*, webpack.config.*, windi.config.*",
|
|
80
|
-
"*.asax": "$(capture).*.cs, $(capture).*.vb",
|
|
81
|
-
"*.ascx": "$(capture).*.cs, $(capture).*.vb",
|
|
82
|
-
"*.ashx": "$(capture).*.cs, $(capture).*.vb",
|
|
83
|
-
"*.aspx": "$(capture).*.cs, $(capture).*.vb",
|
|
84
|
-
"*.axaml": "$(capture).axaml.cs",
|
|
85
|
-
"*.bloc.dart": "$(capture).event.dart, $(capture).state.dart",
|
|
86
|
-
"*.c": "$(capture).h",
|
|
87
|
-
"*.cc": "$(capture).hpp, $(capture).h, $(capture).hxx, $(capture).hh",
|
|
88
|
-
"*.cjs": "$(capture).cjs.map, $(capture).*.cjs, $(capture)_*.cjs",
|
|
89
|
-
"*.component.ts": "$(capture).component.html, $(capture).component.spec.ts, $(capture).component.css, $(capture).component.scss, $(capture).component.sass, $(capture).component.less",
|
|
90
|
-
"*.cpp": "$(capture).hpp, $(capture).h, $(capture).hxx, $(capture).hh",
|
|
91
|
-
"*.cs": "$(capture).*.cs",
|
|
92
|
-
"*.cshtml": "$(capture).cshtml.cs, $(capture).cshtml.css",
|
|
93
|
-
"*.csproj": "*.config, *proj.user, appsettings.*, bundleconfig.json",
|
|
94
|
-
"*.css": "$(capture).css.map, $(capture).*.css",
|
|
95
|
-
"*.cxx": "$(capture).hpp, $(capture).h, $(capture).hxx, $(capture).hh",
|
|
96
|
-
"*.dart": "$(capture).freezed.dart, $(capture).g.dart",
|
|
97
|
-
"*.db": "*.db-shm, *.db-wal",
|
|
98
|
-
"*.ex": "$(capture).html.eex, $(capture).html.heex, $(capture).html.leex",
|
|
99
|
-
"*.fs": "$(capture).fs.js, $(capture).fs.js.map, $(capture).fs.jsx, $(capture).fs.ts, $(capture).fs.tsx, $(capture).fs.rs, $(capture).fs.php, $(capture).fs.dart",
|
|
100
|
-
"*.go": "$(capture)_test.go",
|
|
101
|
-
"*.java": "$(capture).class",
|
|
102
|
-
"*.js": "$(capture).js.map, $(capture).*.js, $(capture)_*.js, $(capture).d.ts, $(capture).d.ts.map, $(capture).js.flow",
|
|
103
|
-
"*.jsx": "$(capture).js, $(capture).*.jsx, $(capture)_*.js, $(capture)_*.jsx, $(capture).css, $(capture).module.css, $(capture).less, $(capture).module.less, $(capture).module.less.d.ts, $(capture).scss, $(capture).module.scss, $(capture).module.scss.d.ts",
|
|
104
|
-
"*.master": "$(capture).*.cs, $(capture).*.vb",
|
|
105
|
-
"*.md": "$(capture).*",
|
|
106
|
-
"*.mjs": "$(capture).mjs.map, $(capture).*.mjs, $(capture)_*.mjs",
|
|
107
|
-
"*.module.ts": "$(capture).resolver.ts, $(capture).controller.ts, $(capture).service.ts",
|
|
108
|
-
"*.mts": "$(capture).mts.map, $(capture).*.mts, $(capture)_*.mts",
|
|
109
|
-
"*.proto": "$(capture).pb.go, $(capture).pb.micro.go",
|
|
110
|
-
"*.pubxml": "$(capture).pubxml.user",
|
|
111
|
-
"*.py": "$(capture).pyi",
|
|
112
|
-
"*.razor": "$(capture).razor.cs, $(capture).razor.css, $(capture).razor.scss",
|
|
113
|
-
"*.resx": "$(capture).*.resx, $(capture).designer.cs, $(capture).designer.vb",
|
|
114
|
-
"*.tex": "$(capture).acn, $(capture).acr, $(capture).alg, $(capture).aux, $(capture).bbl, $(capture).bbl-SAVE-ERROR, $(capture).bcf, $(capture).blg, $(capture).fdb_latexmk, $(capture).fls, $(capture).glg, $(capture).glo, $(capture).gls, $(capture).idx, $(capture).ind, $(capture).ist, $(capture).lof, $(capture).log, $(capture).lot, $(capture).nav, $(capture).out, $(capture).run.xml, $(capture).snm, $(capture).synctex.gz, $(capture).toc, $(capture).xdv",
|
|
115
|
-
"*.ts": "$(capture).js, $(capture).d.ts.map, $(capture).*.ts, $(capture)_*.js, $(capture)_*.ts",
|
|
116
|
-
"*.tsx": "$(capture).ts, $(capture).*.tsx, $(capture)_*.ts, $(capture)_*.tsx, $(capture).css, $(capture).module.css, $(capture).less, $(capture).module.less, $(capture).module.less.d.ts, $(capture).scss, $(capture).module.scss, $(capture).module.scss.d.ts, $(capture).css.ts",
|
|
117
|
-
"*.vbproj": "*.config, *proj.user, appsettings.*, bundleconfig.json",
|
|
118
|
-
"*.vue": "$(capture).*.ts, $(capture).*.js, $(capture).story.vue",
|
|
119
|
-
"*.w": "$(capture).*.w, I$(capture).w",
|
|
120
|
-
"*.wat": "$(capture).wasm",
|
|
121
|
-
"*.xaml": "$(capture).xaml.cs"
|
|
122
|
-
}
|
|
16
|
+
"editor.codeActionsOnSave": {
|
|
17
|
+
"source.fixAll.eslint": "explicit"
|
|
18
|
+
},
|
|
19
|
+
"eslint.enable": true,
|
|
20
|
+
"eslint.workingDirectories": ["./"],
|
|
21
|
+
"eslint.validate": ["javascript", "javascriptreact", "typescript", "typescriptreact", "vue"]
|
|
123
22
|
}
|
package/CHANGES
CHANGED
|
@@ -1,3 +1,28 @@
|
|
|
1
|
+
Version 1.0.7 (2026-01-01)
|
|
2
|
+
|
|
3
|
+
- Harden asset serving and template resolution against traversal/symlink escapes by
|
|
4
|
+
resolving real paths and requiring files to exist.
|
|
5
|
+
- Enforce domain ownership for form/template and mailer endpoints, validate
|
|
6
|
+
reply-to/recipient addresses, and allow reply-to plus custom headers on
|
|
7
|
+
transactional sends.
|
|
8
|
+
- Constrain stored template/form filenames to safe relative paths and widen the
|
|
9
|
+
txmail template column to TEXT for larger templates.
|
|
10
|
+
- Added Vitest-based integration tests with a local SMTP harness, plus test
|
|
11
|
+
scripts and dependencies.
|
|
12
|
+
|
|
13
|
+
Version 1.0.6 (2025-11-06)
|
|
14
|
+
|
|
15
|
+
- Added the dedicated asset API module so files stored under `config/<domain>/assets`
|
|
16
|
+
are served directly from `/asset/<domain>/<path>` (configurable via `ASSET_ROUTE`).
|
|
17
|
+
- Updated `@technomoron/api-server-base` to the latest 2.x beta and aligned mailer
|
|
18
|
+
server access with the new API server types.
|
|
19
|
+
- Tightened the template preprocessor to require non-inline assets to live in the
|
|
20
|
+
domain `assets/` folder and automatically rewrite `asset('logo.png')` calls to the
|
|
21
|
+
public route.
|
|
22
|
+
- Clarified inline usage: `asset('logo.png', true)` continues to embed a CID-backed
|
|
23
|
+
attachment, while omitting the second argument keeps the file external and expects
|
|
24
|
+
it under `config/<domain>/assets`.
|
|
25
|
+
|
|
1
26
|
Version 1.0.5 (2025-10-29)
|
|
2
27
|
|
|
3
28
|
- Store compiled templates and assets under domain-rooted directories inside
|
package/README.md
CHANGED
|
@@ -29,6 +29,12 @@ During development you can run `npm run dev` for a watch mode that recompiles on
|
|
|
29
29
|
|
|
30
30
|
When `DB_AUTO_RELOAD` is enabled the service watches `init-data.json` and refreshes templates and forms without a restart.
|
|
31
31
|
|
|
32
|
+
### Template assets and inline resources
|
|
33
|
+
|
|
34
|
+
- Keep any non-inline files (images, attachments, etc.) under `config/<domain>/assets`. Mail Magic rewrites `asset('logo.png')` to the public route `/asset/<domain>/logo.png` (or whatever you set via `ASSET_ROUTE`).
|
|
35
|
+
- Pass `true` as the second argument when you want to embed a file as an inline CID attachment: `asset('logo.png', true)` stores the file in Nodemailer and rewrites the HTML to reference `cid:logo.png`.
|
|
36
|
+
- Avoid mixing template-type folders for assets; everything that should be linked externally belongs in the shared `<domain>/assets` tree so it can be served for both form and transactional templates.
|
|
37
|
+
|
|
32
38
|
## API Overview
|
|
33
39
|
|
|
34
40
|
| Method | Path | Description |
|
package/TUTORIAL.MD
CHANGED
|
@@ -57,6 +57,8 @@ myorg-config/
|
|
|
57
57
|
|
|
58
58
|
> **Tip:** If you want to share partials between templates, keep file names aligned (e.g. identical `header.njk` content under both `form-template/partials/` and `tx-template/partials/`).
|
|
59
59
|
|
|
60
|
+
> **Assets vs inline:** Any file you want to serve as an external URL must live under `myorg.com/assets/`. The template helper `asset('logo.png')` will become `/asset/myorg.com/logo.png`. Use `asset('logo.png', true)` when you need the file embedded as a CID attachment instead.
|
|
61
|
+
|
|
60
62
|
---
|
|
61
63
|
|
|
62
64
|
## 3. Seed users, domains, and templates with `init-data.json`
|
|
@@ -65,49 +67,49 @@ Create `${CONFIG_ROOT}/init-data.json` so the service can bootstrap the MyOrg us
|
|
|
65
67
|
|
|
66
68
|
```json
|
|
67
69
|
{
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
70
|
+
"user": [
|
|
71
|
+
{
|
|
72
|
+
"user_id": 10,
|
|
73
|
+
"idname": "myorg",
|
|
74
|
+
"token": "<generate-a-32-char-hex-token>",
|
|
75
|
+
"name": "MyOrg",
|
|
76
|
+
"email": "notifications@myorg.com"
|
|
77
|
+
}
|
|
78
|
+
],
|
|
79
|
+
"domain": [
|
|
80
|
+
{
|
|
81
|
+
"domain_id": 10,
|
|
82
|
+
"user_id": 10,
|
|
83
|
+
"name": "myorg.com",
|
|
84
|
+
"sender": "MyOrg Mailer <noreply@myorg.com>"
|
|
85
|
+
}
|
|
86
|
+
],
|
|
87
|
+
"template": [
|
|
88
|
+
{
|
|
89
|
+
"template_id": 100,
|
|
90
|
+
"user_id": 10,
|
|
91
|
+
"domain_id": 10,
|
|
92
|
+
"name": "welcome",
|
|
93
|
+
"slug": "welcome",
|
|
94
|
+
"locale": "",
|
|
95
|
+
"filename": "welcome.njk",
|
|
96
|
+
"sender": "support@myorg.com",
|
|
97
|
+
"subject": "Welcome to MyOrg",
|
|
98
|
+
"template": ""
|
|
99
|
+
}
|
|
100
|
+
],
|
|
101
|
+
"form": [
|
|
102
|
+
{
|
|
103
|
+
"form_id": 100,
|
|
104
|
+
"user_id": 10,
|
|
105
|
+
"domain_id": 10,
|
|
106
|
+
"idname": "contact",
|
|
107
|
+
"filename": "contact.njk",
|
|
108
|
+
"sender": "MyOrg Support <support@myorg.com>",
|
|
109
|
+
"recipient": "contact@myorg.com",
|
|
110
|
+
"subject": "New contact form submission"
|
|
111
|
+
}
|
|
112
|
+
]
|
|
111
113
|
}
|
|
112
114
|
```
|
|
113
115
|
|
|
@@ -274,18 +276,18 @@ The inline flag (`true`) in `asset('logo.png', true)` tells Mail Magic to attach
|
|
|
274
276
|
1. Restart `mail-magic` (or run `npm run dev`) so it picks up the new `CONFIG_PATH`.
|
|
275
277
|
2. Confirm the bootstrap worked — the logs should mention importing user `myorg` and domain `myorg.com`.
|
|
276
278
|
3. Trigger a transactional email:
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
279
|
+
```bash
|
|
280
|
+
curl -X POST http://localhost:3000/v1/tx/message \
|
|
281
|
+
-H 'Content-Type: application/json' \
|
|
282
|
+
-H 'X-API-Token: <your 32-char token>' \
|
|
283
|
+
-d '{
|
|
284
|
+
"user": "myorg",
|
|
285
|
+
"domain": "myorg.com",
|
|
286
|
+
"slug": "welcome",
|
|
287
|
+
"to": "new.user@myorg.com",
|
|
288
|
+
"variables": {"first_name": "Kai", "cta_url": "https://myorg.com/confirm"}
|
|
289
|
+
}'
|
|
290
|
+
```
|
|
289
291
|
4. Trigger the contact form template the same way your frontend will post to `/v1/form/message` (Supply `form_id` or `idname` of `contact`). With `DB_AUTO_RELOAD=1`, editing the templates or assets is as simple as saving the file.
|
|
290
292
|
|
|
291
293
|
You now have a clean, self-contained configuration for MyOrg that inherits Mail Magic behaviour while keeping templates, partials, and assets under version control in a dedicated folder.
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { ApiError, ApiModule } from '@technomoron/api-server-base';
|
|
4
|
+
import { decodeComponent, sendFileAsync } from '../util.js';
|
|
5
|
+
const DOMAIN_PATTERN = /^[a-z0-9][a-z0-9._-]*$/i;
|
|
6
|
+
const SEGMENT_PATTERN = /^[a-zA-Z0-9._-]+$/;
|
|
7
|
+
export class AssetAPI extends ApiModule {
|
|
8
|
+
async getAsset(apiReq) {
|
|
9
|
+
const domain = decodeComponent(apiReq.req.params.domain);
|
|
10
|
+
if (!domain || !DOMAIN_PATTERN.test(domain)) {
|
|
11
|
+
throw new ApiError({ code: 404, message: 'Asset not found' });
|
|
12
|
+
}
|
|
13
|
+
const rawPath = apiReq.req.params[0] ?? '';
|
|
14
|
+
const segments = rawPath
|
|
15
|
+
.split('/')
|
|
16
|
+
.filter(Boolean)
|
|
17
|
+
.map((segment) => decodeComponent(segment));
|
|
18
|
+
if (!segments.length || segments.some((segment) => !SEGMENT_PATTERN.test(segment))) {
|
|
19
|
+
throw new ApiError({ code: 404, message: 'Asset not found' });
|
|
20
|
+
}
|
|
21
|
+
const assetsRoot = path.join(this.server.storage.configpath, domain, 'assets');
|
|
22
|
+
if (!fs.existsSync(assetsRoot)) {
|
|
23
|
+
throw new ApiError({ code: 404, message: 'Asset not found' });
|
|
24
|
+
}
|
|
25
|
+
const resolvedRoot = fs.realpathSync(assetsRoot);
|
|
26
|
+
const normalizedRoot = resolvedRoot.endsWith(path.sep) ? resolvedRoot : resolvedRoot + path.sep;
|
|
27
|
+
const candidate = path.resolve(assetsRoot, path.join(...segments));
|
|
28
|
+
try {
|
|
29
|
+
const stats = await fs.promises.stat(candidate);
|
|
30
|
+
if (!stats.isFile()) {
|
|
31
|
+
throw new ApiError({ code: 404, message: 'Asset not found' });
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
throw new ApiError({ code: 404, message: 'Asset not found' });
|
|
36
|
+
}
|
|
37
|
+
let realCandidate;
|
|
38
|
+
try {
|
|
39
|
+
realCandidate = await fs.promises.realpath(candidate);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
throw new ApiError({ code: 404, message: 'Asset not found' });
|
|
43
|
+
}
|
|
44
|
+
if (!realCandidate.startsWith(normalizedRoot)) {
|
|
45
|
+
throw new ApiError({ code: 404, message: 'Asset not found' });
|
|
46
|
+
}
|
|
47
|
+
const { res } = apiReq;
|
|
48
|
+
const originalStatus = res.status.bind(res);
|
|
49
|
+
const originalJson = res.json.bind(res);
|
|
50
|
+
res.status = ((code) => (res.headersSent ? res : originalStatus(code)));
|
|
51
|
+
res.json = ((body) => (res.headersSent ? res : originalJson(body)));
|
|
52
|
+
res.type(path.extname(realCandidate));
|
|
53
|
+
res.set('Cache-Control', 'public, max-age=300');
|
|
54
|
+
try {
|
|
55
|
+
await sendFileAsync(res, realCandidate);
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
this.server.storage.print_debug(`Failed to serve asset ${domain}/${segments.join('/')}: ${err instanceof Error ? err.message : String(err)}`);
|
|
59
|
+
if (!res.headersSent) {
|
|
60
|
+
throw new ApiError({ code: 500, message: 'Failed to stream asset' });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return [200, null];
|
|
64
|
+
}
|
|
65
|
+
defineRoutes() {
|
|
66
|
+
const route = this.server.storage.env.ASSET_ROUTE;
|
|
67
|
+
const normalizedRoute = route.startsWith('/') ? route : `/${route}`;
|
|
68
|
+
return [
|
|
69
|
+
{
|
|
70
|
+
method: 'get',
|
|
71
|
+
path: `${normalizedRoute}/:domain/*`,
|
|
72
|
+
handler: (apiReq) => this.getAsset(apiReq),
|
|
73
|
+
auth: { type: 'none', req: 'any' }
|
|
74
|
+
}
|
|
75
|
+
];
|
|
76
|
+
}
|
|
77
|
+
}
|
package/dist/api/forms.js
CHANGED
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
2
|
import { ApiModule, ApiError } from '@technomoron/api-server-base';
|
|
3
|
+
import emailAddresses from 'email-addresses';
|
|
3
4
|
import nunjucks from 'nunjucks';
|
|
4
5
|
import { api_domain } from '../models/domain.js';
|
|
5
6
|
import { api_form } from '../models/form.js';
|
|
6
7
|
import { api_user } from '../models/user.js';
|
|
7
8
|
import { buildRequestMeta, normalizeSlug } from '../util.js';
|
|
8
9
|
export class FormAPI extends ApiModule {
|
|
10
|
+
validateEmail(email) {
|
|
11
|
+
const parsed = emailAddresses.parseOneAddress(email);
|
|
12
|
+
if (parsed) {
|
|
13
|
+
return parsed.address;
|
|
14
|
+
}
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
9
17
|
async assertDomainAndUser(apireq) {
|
|
10
18
|
const { domain, locale } = apireq.req.body;
|
|
11
19
|
if (!domain) {
|
|
@@ -19,6 +27,9 @@ export class FormAPI extends ApiModule {
|
|
|
19
27
|
if (!dbdomain) {
|
|
20
28
|
throw new ApiError({ code: 401, message: `Unable to look up the domain ${domain}` });
|
|
21
29
|
}
|
|
30
|
+
if (dbdomain.user_id !== user.user_id) {
|
|
31
|
+
throw new ApiError({ code: 403, message: `Domain ${domain} is not owned by this user` });
|
|
32
|
+
}
|
|
22
33
|
apireq.domain = dbdomain;
|
|
23
34
|
apireq.locale = locale || 'en';
|
|
24
35
|
apireq.user = user;
|
|
@@ -85,7 +96,7 @@ export class FormAPI extends ApiModule {
|
|
|
85
96
|
return [200, { Status: 'OK' }];
|
|
86
97
|
}
|
|
87
98
|
async postSendForm(apireq) {
|
|
88
|
-
const { formid, secret, recipient, vars = {} } = apireq.req.body;
|
|
99
|
+
const { formid, secret, recipient, vars = {}, replyTo, reply_to } = apireq.req.body;
|
|
89
100
|
if (!formid) {
|
|
90
101
|
throw new ApiError({ code: 404, message: 'Missing formid field in form' });
|
|
91
102
|
}
|
|
@@ -102,12 +113,27 @@ export class FormAPI extends ApiModule {
|
|
|
102
113
|
if (recipient && !form.secret) {
|
|
103
114
|
throw new ApiError({ code: 401, message: "'recipient' parameterer requires form secret to be set" });
|
|
104
115
|
}
|
|
116
|
+
let normalizedReplyTo;
|
|
117
|
+
let normalizedRecipient;
|
|
118
|
+
const replyToValue = (replyTo || reply_to);
|
|
119
|
+
if (replyToValue) {
|
|
120
|
+
normalizedReplyTo = this.validateEmail(replyToValue);
|
|
121
|
+
if (!normalizedReplyTo) {
|
|
122
|
+
throw new ApiError({ code: 400, message: 'Invalid reply-to email address' });
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (recipient) {
|
|
126
|
+
normalizedRecipient = this.validateEmail(String(recipient));
|
|
127
|
+
if (!normalizedRecipient) {
|
|
128
|
+
throw new ApiError({ code: 400, message: 'Invalid recipient email address' });
|
|
129
|
+
}
|
|
130
|
+
}
|
|
105
131
|
let parsedVars = vars ?? {};
|
|
106
132
|
if (typeof vars === 'string') {
|
|
107
133
|
try {
|
|
108
134
|
parsedVars = JSON.parse(vars);
|
|
109
135
|
}
|
|
110
|
-
catch
|
|
136
|
+
catch {
|
|
111
137
|
throw new ApiError({ code: 400, message: 'Invalid JSON provided in "vars"' });
|
|
112
138
|
}
|
|
113
139
|
}
|
|
@@ -140,10 +166,11 @@ export class FormAPI extends ApiModule {
|
|
|
140
166
|
const html = nunjucks.renderString(form.template, context);
|
|
141
167
|
const mailOptions = {
|
|
142
168
|
from: form.sender,
|
|
143
|
-
to:
|
|
169
|
+
to: normalizedRecipient || form.recipient,
|
|
144
170
|
subject: form.subject,
|
|
145
171
|
html,
|
|
146
|
-
attachments
|
|
172
|
+
attachments,
|
|
173
|
+
...(normalizedReplyTo ? { replyTo: normalizedReplyTo } : {})
|
|
147
174
|
};
|
|
148
175
|
try {
|
|
149
176
|
const info = await this.server.storage.transport.sendMail(mailOptions);
|