@technomoron/mail-magic 1.0.6 → 1.0.9
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 +45 -1
- package/.env-dist +12 -3
- package/.vscode/extensions.json +1 -13
- package/.vscode/settings.json +13 -114
- package/CHANGES +22 -0
- package/README.md +4 -3
- package/TUTORIAL.MD +1 -0
- package/dist/api/assets.js +19 -6
- package/dist/api/forms.js +31 -4
- package/dist/api/mailer.js +34 -8
- package/dist/index.js +2 -0
- package/dist/models/form.js +11 -1
- package/dist/models/init.js +10 -7
- package/dist/models/txmail.js +13 -4
- package/dist/store/envloader.js +9 -0
- package/dist/store/store.js +1 -0
- package/eslint.config.mjs +133 -41
- package/lintconfig.cjs +81 -0
- package/package.json +30 -20
- package/src/api/assets.ts +19 -6
- package/src/api/forms.ts +35 -5
- package/src/api/mailer.ts +37 -9
- package/src/index.ts +3 -1
- package/src/models/form.ts +12 -1
- package/src/models/init.ts +10 -7
- package/src/models/txmail.ts +14 -6
- package/src/store/envloader.ts +12 -3
- package/src/store/store.ts +80 -1
- package/tests/fixtures/certs/test.crt +19 -0
- package/tests/fixtures/certs/test.key +28 -0
- package/tests/helpers/test-setup.ts +317 -0
- package/tests/mail-magic.test.ts +171 -0
- package/vitest.config.ts +11 -0
package/.do-realease.sh
CHANGED
|
@@ -4,7 +4,51 @@ 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 ! npm whoami >/dev/null 2>&1; then
|
|
34
|
+
echo "Not logged into npm. Run 'npm login' before release." >&2
|
|
35
|
+
exit 1
|
|
36
|
+
fi
|
|
37
|
+
|
|
38
|
+
if git rev-parse -q --verify "refs/tags/v${VERSION}" >/dev/null; then
|
|
39
|
+
echo "Tag v${VERSION} already exists. Aborting." >&2
|
|
40
|
+
exit 1
|
|
41
|
+
fi
|
|
42
|
+
|
|
7
43
|
git tag -a "v${VERSION}" -m "Release version ${VERSION}"
|
|
8
44
|
git push origin "v${VERSION}"
|
|
9
45
|
|
|
10
|
-
|
|
46
|
+
# detect prerelease versions (contains a hyphen)
|
|
47
|
+
if echo "$VERSION" | grep -q "-"; then
|
|
48
|
+
TAG=$(echo "$VERSION" | sed 's/^[0-9.]*-\([a-zA-Z0-9]*\).*/\1/')
|
|
49
|
+
echo "Detected prerelease. Publishing with tag '$TAG'"
|
|
50
|
+
npm publish --tag "$TAG" --access=public
|
|
51
|
+
else
|
|
52
|
+
echo "Stable release. Publishing as latest"
|
|
53
|
+
npm publish --access=public
|
|
54
|
+
fi
|
package/.env-dist
CHANGED
|
@@ -16,8 +16,17 @@ 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
|
-
CONFIG_PATH=./
|
|
29
|
+
CONFIG_PATH=./data/
|
|
21
30
|
|
|
22
31
|
# Database username for API database
|
|
23
32
|
DB_USER=
|
|
@@ -58,5 +67,5 @@ SMTP_USER=
|
|
|
58
67
|
# Password for SMTP host
|
|
59
68
|
SMTP_PASSWORD=
|
|
60
69
|
|
|
61
|
-
# Path for attached files
|
|
62
|
-
UPLOAD_PATH=./uploads
|
|
70
|
+
# Path for attached files. Use {domain} to scope per domain.
|
|
71
|
+
UPLOAD_PATH=./{domain}/uploads
|
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,7 +1,29 @@
|
|
|
1
|
+
Version 1.0.9 (2026-01-02)
|
|
2
|
+
|
|
3
|
+
- Default CONFIG_PATH to ./data and document the data-rooted layout, including
|
|
4
|
+
per-domain uploads.
|
|
5
|
+
- Allow UPLOAD_PATH to include {domain}, staging incoming uploads under
|
|
6
|
+
<CONFIG_PATH>/_uploads before relocating to <CONFIG_PATH>/<domain>/uploads
|
|
7
|
+
for transactional and form submissions.
|
|
8
|
+
|
|
9
|
+
Version 1.0.7 (2026-01-01)
|
|
10
|
+
|
|
11
|
+
- Harden asset serving and template resolution against traversal/symlink escapes by
|
|
12
|
+
resolving real paths and requiring files to exist.
|
|
13
|
+
- Enforce domain ownership for form/template and mailer endpoints, validate
|
|
14
|
+
reply-to/recipient addresses, and allow reply-to plus custom headers on
|
|
15
|
+
transactional sends.
|
|
16
|
+
- Constrain stored template/form filenames to safe relative paths and widen the
|
|
17
|
+
txmail template column to TEXT for larger templates.
|
|
18
|
+
- Added Vitest-based integration tests with a local SMTP harness, plus test
|
|
19
|
+
scripts and dependencies.
|
|
20
|
+
|
|
1
21
|
Version 1.0.6 (2025-11-06)
|
|
2
22
|
|
|
3
23
|
- Added the dedicated asset API module so files stored under `config/<domain>/assets`
|
|
4
24
|
are served directly from `/asset/<domain>/<path>` (configurable via `ASSET_ROUTE`).
|
|
25
|
+
- Updated `@technomoron/api-server-base` to the latest 2.x beta and aligned mailer
|
|
26
|
+
server access with the new API server types.
|
|
5
27
|
- Tightened the template preprocessor to require non-inline assets to live in the
|
|
6
28
|
domain `assets/` folder and automatically rewrite `asset('logo.png')` calls to the
|
|
7
29
|
public route.
|
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@ Mail Magic is a TypeScript service for managing, templating, and delivering tran
|
|
|
15
15
|
1. Clone the repository: `git clone git@github.com:technomoron/mail-magic.git`
|
|
16
16
|
2. Install dependencies: `npm install`
|
|
17
17
|
3. Create your environment file: copy `.env-dist` to `.env` and adjust values
|
|
18
|
-
4. Populate the config directory (see `config-example/` for a reference layout)
|
|
18
|
+
4. Populate the config directory (defaults to `./data/`; see `config-example/` for a reference layout). You can point `CONFIG_PATH` at `./config` to use the bundled sample data.
|
|
19
19
|
5. Build the project: `npm run build`
|
|
20
20
|
6. Start the API server: `npm run start`
|
|
21
21
|
|
|
@@ -24,14 +24,15 @@ During development you can run `npm run dev` for a watch mode that recompiles on
|
|
|
24
24
|
## Configuration
|
|
25
25
|
|
|
26
26
|
- **Environment variables** are defined in `src/store/envloader.ts`. Important settings include SMTP credentials, API host/port, the config directory path, and database options.
|
|
27
|
-
- **Config directory** (`CONFIG_PATH`) contains JSON seed data (`init-data.json`), optional API key files, and template assets. Each domain now lives directly under the config root (for example `
|
|
27
|
+
- **Config directory** (`CONFIG_PATH`) contains JSON seed data (`init-data.json`), optional API key files, and template assets. Each domain now lives directly under the config root (for example `data/example.com/form-template/…`). Use an absolute path or a relative one like `../data` when you want the config outside the repo. Review `config-example/` for the recommended layout, in particular the `form-template/` and `tx-template/` folders used for compiled Nunjucks templates.
|
|
28
28
|
- **Database** defaults to SQLite (`maildata.db`). You can switch dialects by updating the environment options if your deployment requires another database.
|
|
29
|
+
- **Uploads** default to `<CONFIG_PATH>/<domain>/uploads` via `UPLOAD_PATH=./{domain}/uploads`. Set a fixed path if you prefer a shared upload directory.
|
|
29
30
|
|
|
30
31
|
When `DB_AUTO_RELOAD` is enabled the service watches `init-data.json` and refreshes templates and forms without a restart.
|
|
31
32
|
|
|
32
33
|
### Template assets and inline resources
|
|
33
34
|
|
|
34
|
-
- Keep any non-inline files (images, attachments, etc.) under
|
|
35
|
+
- Keep any non-inline files (images, attachments, etc.) under `<CONFIG_PATH>/<domain>/assets`. Mail Magic rewrites `asset('logo.png')` to the public route `/asset/<domain>/logo.png` (or whatever you set via `ASSET_ROUTE`).
|
|
35
36
|
- 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
37
|
- 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
38
|
|
package/TUTORIAL.MD
CHANGED
|
@@ -19,6 +19,7 @@ Update your `.env` (or runtime environment) to point at the new workspace:
|
|
|
19
19
|
```
|
|
20
20
|
CONFIG_PATH=${CONFIG_ROOT}
|
|
21
21
|
DB_AUTO_RELOAD=1 # optional: hot‑reload init-data and templates
|
|
22
|
+
UPLOAD_PATH=./{domain}/uploads
|
|
22
23
|
```
|
|
23
24
|
|
|
24
25
|
From now on the tutorial assumes `${CONFIG_ROOT}` is the root of the custom config tree.
|
package/dist/api/assets.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
|
-
import {
|
|
3
|
+
import { ApiError, ApiModule } from '@technomoron/api-server-base';
|
|
4
4
|
import { decodeComponent, sendFileAsync } from '../util.js';
|
|
5
5
|
const DOMAIN_PATTERN = /^[a-z0-9][a-z0-9._-]*$/i;
|
|
6
6
|
const SEGMENT_PATTERN = /^[a-zA-Z0-9._-]+$/;
|
|
@@ -19,27 +19,40 @@ export class AssetAPI extends ApiModule {
|
|
|
19
19
|
throw new ApiError({ code: 404, message: 'Asset not found' });
|
|
20
20
|
}
|
|
21
21
|
const assetsRoot = path.join(this.server.storage.configpath, domain, 'assets');
|
|
22
|
-
|
|
22
|
+
if (!fs.existsSync(assetsRoot)) {
|
|
23
|
+
throw new ApiError({ code: 404, message: 'Asset not found' });
|
|
24
|
+
}
|
|
25
|
+
const resolvedRoot = fs.realpathSync(assetsRoot);
|
|
23
26
|
const normalizedRoot = resolvedRoot.endsWith(path.sep) ? resolvedRoot : resolvedRoot + path.sep;
|
|
24
27
|
const candidate = path.resolve(assetsRoot, path.join(...segments));
|
|
25
|
-
|
|
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 {
|
|
26
35
|
throw new ApiError({ code: 404, message: 'Asset not found' });
|
|
27
36
|
}
|
|
37
|
+
let realCandidate;
|
|
28
38
|
try {
|
|
29
|
-
await fs.promises.
|
|
39
|
+
realCandidate = await fs.promises.realpath(candidate);
|
|
30
40
|
}
|
|
31
41
|
catch {
|
|
32
42
|
throw new ApiError({ code: 404, message: 'Asset not found' });
|
|
33
43
|
}
|
|
44
|
+
if (!realCandidate.startsWith(normalizedRoot)) {
|
|
45
|
+
throw new ApiError({ code: 404, message: 'Asset not found' });
|
|
46
|
+
}
|
|
34
47
|
const { res } = apiReq;
|
|
35
48
|
const originalStatus = res.status.bind(res);
|
|
36
49
|
const originalJson = res.json.bind(res);
|
|
37
50
|
res.status = ((code) => (res.headersSent ? res : originalStatus(code)));
|
|
38
51
|
res.json = ((body) => (res.headersSent ? res : originalJson(body)));
|
|
39
|
-
res.type(path.extname(
|
|
52
|
+
res.type(path.extname(realCandidate));
|
|
40
53
|
res.set('Cache-Control', 'public, max-age=300');
|
|
41
54
|
try {
|
|
42
|
-
await sendFileAsync(res,
|
|
55
|
+
await sendFileAsync(res, realCandidate);
|
|
43
56
|
}
|
|
44
57
|
catch (err) {
|
|
45
58
|
this.server.storage.print_debug(`Failed to serve asset ${domain}/${segments.join('/')}: ${err instanceof Error ? err.message : String(err)}`);
|
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);
|
package/dist/api/mailer.js
CHANGED
|
@@ -15,7 +15,7 @@ export class MailerAPI extends ApiModule {
|
|
|
15
15
|
if (parsed) {
|
|
16
16
|
return parsed.address;
|
|
17
17
|
}
|
|
18
|
-
return
|
|
18
|
+
return undefined;
|
|
19
19
|
}
|
|
20
20
|
//
|
|
21
21
|
// Validate a set of email addresses. Return arrays of invalid
|
|
@@ -51,6 +51,9 @@ export class MailerAPI extends ApiModule {
|
|
|
51
51
|
if (!dbdomain) {
|
|
52
52
|
throw new ApiError({ code: 401, message: `Unable to look up the domain ${domain}` });
|
|
53
53
|
}
|
|
54
|
+
if (dbdomain.user_id !== user.user_id) {
|
|
55
|
+
throw new ApiError({ code: 403, message: `Domain ${domain} is not owned by this user` });
|
|
56
|
+
}
|
|
54
57
|
apireq.domain = dbdomain;
|
|
55
58
|
apireq.locale = locale || 'en';
|
|
56
59
|
apireq.user = user;
|
|
@@ -85,7 +88,7 @@ export class MailerAPI extends ApiModule {
|
|
|
85
88
|
const [templateRecord, created] = await api_txmail.upsert(data, {
|
|
86
89
|
returning: true
|
|
87
90
|
});
|
|
88
|
-
|
|
91
|
+
this.server.storage.print_debug(`Template upserted: ${templateRecord.name} (created=${created})`);
|
|
89
92
|
}
|
|
90
93
|
catch (error) {
|
|
91
94
|
throw new ApiError({
|
|
@@ -98,7 +101,7 @@ export class MailerAPI extends ApiModule {
|
|
|
98
101
|
// Send a template using posted arguments.
|
|
99
102
|
async post_send(apireq) {
|
|
100
103
|
await this.assert_domain_and_user(apireq);
|
|
101
|
-
const { name, rcpt, domain = '', locale = '', vars = {} } = apireq.req.body;
|
|
104
|
+
const { name, rcpt, domain = '', locale = '', vars = {}, replyTo, reply_to, headers } = apireq.req.body;
|
|
102
105
|
if (!name || !rcpt || !domain) {
|
|
103
106
|
throw new ApiError({ code: 400, message: 'name/rcpt/domain required' });
|
|
104
107
|
}
|
|
@@ -107,7 +110,7 @@ export class MailerAPI extends ApiModule {
|
|
|
107
110
|
try {
|
|
108
111
|
parsedVars = JSON.parse(vars);
|
|
109
112
|
}
|
|
110
|
-
catch
|
|
113
|
+
catch {
|
|
111
114
|
throw new ApiError({ code: 400, message: 'Invalid JSON provided in "vars"' });
|
|
112
115
|
}
|
|
113
116
|
}
|
|
@@ -118,7 +121,7 @@ export class MailerAPI extends ApiModule {
|
|
|
118
121
|
throw new ApiError({ code: 400, message: 'Invalid email address(es): ' + invalid.join(',') });
|
|
119
122
|
}
|
|
120
123
|
let template = null;
|
|
121
|
-
const deflocale =
|
|
124
|
+
const deflocale = this.server.storage.deflocale || '';
|
|
122
125
|
const domain_id = apireq.domain.domain_id;
|
|
123
126
|
try {
|
|
124
127
|
template =
|
|
@@ -159,8 +162,29 @@ export class MailerAPI extends ApiModule {
|
|
|
159
162
|
for (const file of rawFiles) {
|
|
160
163
|
attachmentMap[file.fieldname] = file.originalname;
|
|
161
164
|
}
|
|
162
|
-
|
|
165
|
+
this.server.storage.print_debug(`Template vars: ${JSON.stringify({ vars, thevars }, undefined, 2)}`);
|
|
163
166
|
const meta = buildRequestMeta(apireq.req);
|
|
167
|
+
const replyToValue = (replyTo || reply_to);
|
|
168
|
+
let normalizedReplyTo;
|
|
169
|
+
if (replyToValue) {
|
|
170
|
+
normalizedReplyTo = this.validateEmail(replyToValue);
|
|
171
|
+
if (!normalizedReplyTo) {
|
|
172
|
+
throw new ApiError({ code: 400, message: 'Invalid reply-to email address' });
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
let normalizedHeaders;
|
|
176
|
+
if (headers !== undefined) {
|
|
177
|
+
if (!headers || typeof headers !== 'object' || Array.isArray(headers)) {
|
|
178
|
+
throw new ApiError({ code: 400, message: 'headers must be a key/value object' });
|
|
179
|
+
}
|
|
180
|
+
normalizedHeaders = {};
|
|
181
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
182
|
+
if (typeof value !== 'string') {
|
|
183
|
+
throw new ApiError({ code: 400, message: `headers.${key} must be a string` });
|
|
184
|
+
}
|
|
185
|
+
normalizedHeaders[key] = value;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
164
188
|
try {
|
|
165
189
|
const env = new nunjucks.Environment(null, { autoescape: false });
|
|
166
190
|
const compiled = nunjucks.compile(template.template, env);
|
|
@@ -180,9 +204,11 @@ export class MailerAPI extends ApiModule {
|
|
|
180
204
|
subject: template.subject || apireq.req.body.subject || '',
|
|
181
205
|
html,
|
|
182
206
|
text,
|
|
183
|
-
attachments
|
|
207
|
+
attachments,
|
|
208
|
+
...(normalizedReplyTo ? { replyTo: normalizedReplyTo } : {}),
|
|
209
|
+
...(normalizedHeaders ? { headers: normalizedHeaders } : {})
|
|
184
210
|
};
|
|
185
|
-
await
|
|
211
|
+
await this.server.storage.transport.sendMail(sendargs);
|
|
186
212
|
}
|
|
187
213
|
return [200, { Status: 'OK', Message: 'Emails sent successfully' }];
|
|
188
214
|
}
|
package/dist/index.js
CHANGED
package/dist/models/form.js
CHANGED
|
@@ -128,6 +128,16 @@ export async function init_api_form(api_db) {
|
|
|
128
128
|
});
|
|
129
129
|
return api_form;
|
|
130
130
|
}
|
|
131
|
+
function assertSafeRelativePath(filename, label) {
|
|
132
|
+
const normalized = path.normalize(filename);
|
|
133
|
+
if (path.isAbsolute(normalized)) {
|
|
134
|
+
throw new Error(`${label} path must be relative`);
|
|
135
|
+
}
|
|
136
|
+
if (normalized.split(path.sep).includes('..')) {
|
|
137
|
+
throw new Error(`${label} path cannot include '..' segments`);
|
|
138
|
+
}
|
|
139
|
+
return normalized;
|
|
140
|
+
}
|
|
131
141
|
export async function upsert_form(record) {
|
|
132
142
|
const { user, domain } = await user_and_domain(record.domain_id);
|
|
133
143
|
const idname = normalizeSlug(user.idname);
|
|
@@ -150,7 +160,7 @@ export async function upsert_form(record) {
|
|
|
150
160
|
if (!record.filename.endsWith('.njk')) {
|
|
151
161
|
record.filename += '.njk';
|
|
152
162
|
}
|
|
153
|
-
record.filename =
|
|
163
|
+
record.filename = assertSafeRelativePath(record.filename, 'Form filename');
|
|
154
164
|
let instance = null;
|
|
155
165
|
instance = await api_form.findByPk(record.form_id);
|
|
156
166
|
if (instance) {
|
package/dist/models/init.js
CHANGED
|
@@ -21,16 +21,17 @@ function resolveAsset(basePath, domainName, assetName) {
|
|
|
21
21
|
if (!fs.existsSync(assetsRoot)) {
|
|
22
22
|
return null;
|
|
23
23
|
}
|
|
24
|
-
const resolvedRoot =
|
|
24
|
+
const resolvedRoot = fs.realpathSync(assetsRoot);
|
|
25
25
|
const normalizedRoot = resolvedRoot.endsWith(path.sep) ? resolvedRoot : resolvedRoot + path.sep;
|
|
26
26
|
const candidate = path.resolve(assetsRoot, assetName);
|
|
27
|
-
if (!candidate.
|
|
27
|
+
if (!fs.existsSync(candidate) || !fs.statSync(candidate).isFile()) {
|
|
28
28
|
return null;
|
|
29
29
|
}
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
const realCandidate = fs.realpathSync(candidate);
|
|
31
|
+
if (!realCandidate.startsWith(normalizedRoot)) {
|
|
32
|
+
return null;
|
|
32
33
|
}
|
|
33
|
-
return
|
|
34
|
+
return realCandidate;
|
|
34
35
|
}
|
|
35
36
|
function buildAssetUrl(baseUrl, route, domainName, assetPath) {
|
|
36
37
|
const trimmedBase = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
|
|
@@ -80,8 +81,10 @@ async function _load_template(store, filename, pathname, user, domain, locale, t
|
|
|
80
81
|
if (filename.startsWith(prefix)) {
|
|
81
82
|
relFile = filename.slice(prefix.length);
|
|
82
83
|
}
|
|
83
|
-
const
|
|
84
|
-
|
|
84
|
+
const resolvedRoot = path.resolve(rootDir);
|
|
85
|
+
const normalizedRoot = resolvedRoot.endsWith(path.sep) ? resolvedRoot : resolvedRoot + path.sep;
|
|
86
|
+
const absPath = path.resolve(resolvedRoot, pathname || '', relFile);
|
|
87
|
+
if (!absPath.startsWith(normalizedRoot)) {
|
|
85
88
|
throw new Error(`Invalid template path "${filename}"`);
|
|
86
89
|
}
|
|
87
90
|
if (!fs.existsSync(absPath)) {
|