@technomoron/mail-magic 1.0.38 → 1.0.41
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/CHANGES +48 -0
- package/README.md +7 -2
- package/dist/cjs/index.d.ts +1 -0
- package/dist/cjs/index.js +1 -1
- package/dist/cjs/package.json +1 -1
- package/dist/esm/api/assets.d.ts +9 -0
- package/dist/esm/api/auth.d.ts +2 -0
- package/dist/esm/api/auth.js +17 -8
- package/dist/esm/api/forms.d.ts +9 -0
- package/dist/esm/api/forms.js +7 -4
- package/dist/esm/api/mailer.d.ts +11 -0
- package/dist/esm/api/mailer.js +8 -7
- package/dist/esm/bin/mail-magic.d.ts +2 -0
- package/dist/esm/bin/mail-magic.js +0 -0
- package/dist/esm/index.d.ts +12 -0
- package/dist/esm/models/db.d.ts +5 -0
- package/dist/esm/models/db.js +2 -2
- package/dist/esm/models/domain.d.ts +24 -0
- package/dist/esm/models/domain.js +0 -2
- package/dist/esm/models/form.d.ts +50 -0
- package/dist/esm/models/form.js +16 -15
- package/dist/esm/models/init.d.ts +12 -0
- package/dist/esm/models/init.js +7 -28
- package/dist/esm/models/recipient.d.ts +24 -0
- package/dist/esm/models/recipient.js +0 -2
- package/dist/esm/models/txmail.d.ts +42 -0
- package/dist/esm/models/txmail.js +0 -2
- package/dist/esm/models/user.d.ts +33 -0
- package/dist/esm/models/user.js +0 -2
- package/dist/esm/server.d.ts +8 -0
- package/dist/esm/store/envloader.d.ts +188 -0
- package/dist/esm/store/envloader.js +14 -4
- package/dist/esm/store/store.d.ts +37 -0
- package/dist/esm/store/store.js +6 -7
- package/dist/esm/swagger.d.ts +10 -0
- package/dist/esm/types.d.ts +32 -0
- package/dist/esm/util/captcha.d.ts +7 -0
- package/dist/esm/util/captcha.js +4 -1
- package/dist/esm/util/email.d.ts +3 -0
- package/dist/esm/util/form-replyto.d.ts +6 -0
- package/dist/esm/util/form-submission.d.ts +24 -0
- package/dist/esm/util/forms.d.ts +140 -0
- package/dist/esm/util/forms.js +18 -32
- package/dist/esm/util/paths.d.ts +15 -0
- package/dist/esm/util/paths.js +17 -0
- package/dist/esm/util/ratelimit.d.ts +16 -0
- package/dist/esm/util/ratelimit.js +6 -1
- package/dist/esm/util/route.d.ts +1 -0
- package/dist/esm/util/shared-template-flatten.d.ts +17 -0
- package/dist/esm/util/shared-template-flatten.js +41 -0
- package/dist/esm/util/uploads.d.ts +10 -0
- package/dist/esm/util/utils.d.ts +27 -0
- package/dist/esm/util.d.ts +7 -0
- package/docs/swagger/openapi.json +16 -12
- package/docs/tutorial.md +2 -2
- package/examples/.env-dist +21 -0
- package/examples/README.md +74 -0
- package/examples/data/example.test/form-template/base.njk +4 -0
- package/examples/data/example.test/form-template/en/base.njk +1 -0
- package/examples/data/example.test/form-template/en/change-password.njk +5 -0
- package/examples/data/example.test/form-template/en/confirm-account.njk +5 -0
- package/examples/data/example.test/form-template/en/contact.njk +5 -0
- package/examples/data/example.test/form-template/en/partials/fields.njk +5 -0
- package/examples/data/example.test/form-template/en/welcome-signup.njk +5 -0
- package/examples/data/example.test/form-template/nb/base.njk +1 -0
- package/examples/data/example.test/form-template/nb/change-password.njk +5 -0
- package/examples/data/example.test/form-template/nb/confirm-account.njk +5 -0
- package/examples/data/example.test/form-template/nb/contact.njk +5 -0
- package/examples/data/example.test/form-template/nb/partials/fields.njk +5 -0
- package/examples/data/example.test/form-template/nb/welcome-signup.njk +5 -0
- package/examples/data/example.test/form-template/partials/header.njk +1 -0
- package/examples/data/example.test/tx-template/base.njk +16 -0
- package/examples/data/example.test/tx-template/en/base.njk +1 -0
- package/examples/data/example.test/tx-template/en/change-password.njk +7 -0
- package/examples/data/example.test/tx-template/en/confirm.njk +6 -0
- package/examples/data/example.test/tx-template/en/invoice.njk +8 -0
- package/examples/data/example.test/tx-template/en/partials/header.njk +1 -0
- package/examples/data/example.test/tx-template/en/partials/line-items.njk +14 -0
- package/examples/data/example.test/tx-template/en/receipt.njk +7 -0
- package/examples/data/example.test/tx-template/en/welcome.njk +5 -0
- package/examples/data/example.test/tx-template/nb/base.njk +1 -0
- package/examples/data/example.test/tx-template/nb/change-password.njk +6 -0
- package/examples/data/example.test/tx-template/nb/confirm.njk +6 -0
- package/examples/data/example.test/tx-template/nb/invoice.njk +7 -0
- package/examples/data/example.test/tx-template/nb/partials/header.njk +1 -0
- package/examples/data/example.test/tx-template/nb/receipt.njk +6 -0
- package/examples/data/example.test/tx-template/nb/welcome.njk +5 -0
- package/examples/data/example.test/tx-template/partials/header.njk +7 -0
- package/examples/data/init-data.json +213 -0
- package/examples/scripts/mm-api.ts +206 -0
- package/examples/scripts/public-form.ts +100 -0
- package/examples/scripts/send-messages.ts +114 -0
- package/package.json +90 -85
package/CHANGES
CHANGED
|
@@ -1,3 +1,51 @@
|
|
|
1
|
+
CHANGES
|
|
2
|
+
=======
|
|
3
|
+
|
|
4
|
+
Version 1.0.41 (2026-02-22)
|
|
5
|
+
|
|
6
|
+
- chore(release): add package-level `release:check` script and wire `release` to shared publish script.
|
|
7
|
+
- chore(scripts): replace `rm -rf` cleanup scripts with `rimraf`.
|
|
8
|
+
- chore(examples): move example data, scripts, and `.env-dist` from standalone `packages/examples/` workspace into `packages/server/examples/`; add `examples` to the npm `files` list so they are shipped with the package.
|
|
9
|
+
- chore(repo): rename admin package directory from `mail-magic-admin` to `admin` (npm package name unchanged).
|
|
10
|
+
- test(logging): suppress noisy SQL/startup stdout in automated tests by default (`DB_LOG=false` in test setup; startup logs debug-only).
|
|
11
|
+
- test(logging): quiet package test output by default (silent Vitest + silent sync step) and remove Node `DEP0170` warning in schema-drift test DB setup.
|
|
12
|
+
- fix(forms): replace global `nunjucks.configure()` call in `postSendForm` with a scoped `nunjucks.Environment` instance, matching the pattern already used in `post_send`.
|
|
13
|
+
- fix(forms): add plain-text email part to form submissions via `html-to-text` (was HTML-only, harming deliverability).
|
|
14
|
+
- fix(forms): derive `recipients` template variable from already-resolved DB records instead of re-parsing the raw input with a second `parseIdnameList` call.
|
|
15
|
+
- fix(forms): remove silent `catch` in `resolveFormKeyForTemplate` so DB errors propagate instead of being silently treated as "no existing form".
|
|
16
|
+
- fix(forms): extract `buildFormSlugAndFilename` into `util/paths.ts` to eliminate duplicated slug/filename generation between the API upload path and the DB upsert path.
|
|
17
|
+
- fix(models): filter `allowed_fields` JSON getter to string elements only; previously returned non-string values (numbers, nulls) typed as `string[]`.
|
|
18
|
+
- fix(smtp): make `requireTLS` configurable via `SMTP_REQUIRE_TLS` env var (default `true`, preserving existing behaviour); allows disabling STARTTLS for servers such as MailHog.
|
|
19
|
+
- fix(smtp): change `SMTP_TLS_REJECT` default from `false` to `true` so TLS certificates are validated in production by default.
|
|
20
|
+
- fix(smtp): correct `SMTP_TLS_REJECT` description (previously described the inverse of what the option does).
|
|
21
|
+
- fix(config): change `DB_SYNC_ALTER` default from `true` to `false` to prevent automatic DDL on production startups.
|
|
22
|
+
- fix(config): add `mysql` and `postgres` to the `DB_TYPE` allowed-values list to match what the connection code already supports.
|
|
23
|
+
- fix(ratelimit): skip rate limiting when the client IP cannot be resolved, rather than bucketing all unresolvable IPs under a shared `unknown` key.
|
|
24
|
+
- fix(captcha): throw an explicit error for unknown CAPTCHA providers instead of silently falling back to Turnstile.
|
|
25
|
+
- fix(auth): make `domain` optional in `assert_domain_and_user`; when absent from the request body, fall back to the authenticated user's default domain (`api_user.domain`).
|
|
26
|
+
- fix(build): remove `noImplicitAny: false` override from `tsconfig/tsconfig.esm.json`; the root tsconfig already enables `strict`, which includes `noImplicitAny`.
|
|
27
|
+
- fix(build): add `declaration: true` to `tsconfig/tsconfig.esm.json` to emit `.d.ts` files; generate `dist/cjs/index.d.ts` re-exporting ESM types; add `types` condition to package exports; extract `STARTUP_ERROR_MESSAGE` from compiled ESM output instead of hardcoding it in the CJS shim script.
|
|
28
|
+
- fix(scripts): remove `--ext` flags from all `lint`/`lintfix` npm scripts — ESLint v9 flat config silently ignores these flags; file coverage is already provided by the glob patterns in `eslint.config.mjs`.
|
|
29
|
+
- docs(swagger): remove `domain` from the `required` array in all authenticated request schemas (`TxTemplateUpsertRequest`, `TxSendRequest`, `TxSendMultipartRequest`, `FormRecipientUpsertRequest`, `FormTemplateUpsertRequest`); mark property descriptions as optional; bump spec version to 1.0.41.
|
|
30
|
+
- docs(readme): add `SMTP_REQUIRE_TLS` to quick-start `.env` example and configuration reference.
|
|
31
|
+
- (Changes generated/assisted by Claude Code (profile: anthropic-claude-sonnet-4-6/high).)
|
|
32
|
+
|
|
33
|
+
Version 1.0.40 (2026-02-22)
|
|
34
|
+
|
|
35
|
+
- chore(changes): normalize this package changelog to required CHANGES format.
|
|
36
|
+
- (Changes generated/assisted by Codex (profile: chatgpt-5.3-codex/medium).)
|
|
37
|
+
|
|
38
|
+
Version 1.0.39 (2026-02-19)
|
|
39
|
+
|
|
40
|
+
- Code clarity pass: restore `DB_LOG` env-driven logging config (was commented
|
|
41
|
+
out), normalize import path in `db.ts`.
|
|
42
|
+
- Simplify `create_mail_transport` by removing unnecessary spread and
|
|
43
|
+
intermediate variable.
|
|
44
|
+
- Simplify `buildReplyToValue` by removing redundant variable and duplicate
|
|
45
|
+
return path in `forms.ts`.
|
|
46
|
+
- Replace `forEach` with `for...of` and use separate `const` declarations in
|
|
47
|
+
`validateEmails` (`mailer.ts`).
|
|
48
|
+
|
|
1
49
|
Version 1.0.38 (2026-02-17)
|
|
2
50
|
|
|
3
51
|
- Prefer `fs.watch` for init-data auto-reload with automatic fallback to
|
package/README.md
CHANGED
|
@@ -60,6 +60,7 @@ API_URL=http://127.0.0.1:3776
|
|
|
60
60
|
SMTP_HOST=127.0.0.1
|
|
61
61
|
SMTP_PORT=1025
|
|
62
62
|
SMTP_SECURE=false
|
|
63
|
+
SMTP_REQUIRE_TLS=false
|
|
63
64
|
SMTP_TLS_REJECT=false
|
|
64
65
|
```
|
|
65
66
|
|
|
@@ -144,6 +145,10 @@ Commonly used variables:
|
|
|
144
145
|
- `FORM_RATE_LIMIT_WINDOW_SEC`, `FORM_RATE_LIMIT_MAX`
|
|
145
146
|
- `FORM_MAX_ATTACHMENTS`, `FORM_KEEP_UPLOADS`
|
|
146
147
|
- `FORM_CAPTCHA_PROVIDER`, `FORM_CAPTCHA_SECRET`, `FORM_CAPTCHA_REQUIRED`
|
|
148
|
+
- SMTP:
|
|
149
|
+
- `SMTP_HOST`, `SMTP_PORT`, `SMTP_SECURE`
|
|
150
|
+
- `SMTP_REQUIRE_TLS` (default `true`; set to `false` for local dev servers like MailHog that don't support STARTTLS)
|
|
151
|
+
- `SMTP_TLS_REJECT` (default `true`; set to `false` to accept self-signed certificates)
|
|
147
152
|
- Swagger/OpenAPI:
|
|
148
153
|
- `SWAGGER_ENABLED`, `SWAGGER_PATH`
|
|
149
154
|
|
|
@@ -308,5 +313,5 @@ pnpm -w --filter @technomoron/mail-magic cleanbuild
|
|
|
308
313
|
|
|
309
314
|
Documentation:
|
|
310
315
|
|
|
311
|
-
- `packages/
|
|
312
|
-
- `packages/
|
|
316
|
+
- `packages/server/docs/tutorial.md` is a hands-on config walkthrough.
|
|
317
|
+
- `packages/server/docs/form-security.md` covers the public form endpoint contract and recommended mitigations.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from '../esm/index.js';
|
package/dist/cjs/index.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
const load = () => import('../esm/index.js');
|
|
4
4
|
|
|
5
5
|
module.exports = {
|
|
6
|
-
STARTUP_ERROR_MESSAGE:
|
|
6
|
+
STARTUP_ERROR_MESSAGE: "Failed to start mail-magic:",
|
|
7
7
|
createMailMagicServer: async (...args) => (await load()).createMailMagicServer(...args),
|
|
8
8
|
startMailMagicServer: async (...args) => (await load()).startMailMagicServer(...args)
|
|
9
9
|
};
|
package/dist/cjs/package.json
CHANGED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { ApiModule, ApiRoute } from '@technomoron/api-server-base';
|
|
2
|
+
import { mailApiServer } from '../server.js';
|
|
3
|
+
import type { NextFunction, Request, Response } from 'express';
|
|
4
|
+
export declare class AssetAPI extends ApiModule<mailApiServer> {
|
|
5
|
+
private resolveTemplateDir;
|
|
6
|
+
private postAssets;
|
|
7
|
+
defineRoutes(): ApiRoute[];
|
|
8
|
+
}
|
|
9
|
+
export declare function createAssetHandler(server: mailApiServer): (req: Request, res: Response, next?: NextFunction) => Promise<void>;
|
package/dist/esm/api/auth.js
CHANGED
|
@@ -4,11 +4,8 @@ import { api_user } from '../models/user.js';
|
|
|
4
4
|
import { getBodyValue } from '../util/utils.js';
|
|
5
5
|
export async function assert_domain_and_user(apireq) {
|
|
6
6
|
const body = apireq.req.body ?? {};
|
|
7
|
-
const
|
|
7
|
+
const domainRaw = getBodyValue(body, 'domain');
|
|
8
8
|
const locale = getBodyValue(body, 'locale');
|
|
9
|
-
if (!domain) {
|
|
10
|
-
throw new ApiError({ code: 401, message: 'Missing domain' });
|
|
11
|
-
}
|
|
12
9
|
const rawUid = apireq.getRealUid();
|
|
13
10
|
const uid = rawUid === null ? null : Number(rawUid);
|
|
14
11
|
if (!uid || Number.isNaN(uid)) {
|
|
@@ -18,12 +15,24 @@ export async function assert_domain_and_user(apireq) {
|
|
|
18
15
|
if (!user) {
|
|
19
16
|
throw new ApiError({ code: 401, message: 'Invalid/Unknown API Key/Token' });
|
|
20
17
|
}
|
|
21
|
-
|
|
22
|
-
if (
|
|
23
|
-
|
|
18
|
+
let dbdomain;
|
|
19
|
+
if (domainRaw) {
|
|
20
|
+
dbdomain = await api_domain.findOne({ where: { name: domainRaw } });
|
|
21
|
+
if (!dbdomain) {
|
|
22
|
+
throw new ApiError({ code: 401, message: `Unable to look up the domain ${domainRaw}` });
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
else if (user.domain != null) {
|
|
26
|
+
dbdomain = await api_domain.findByPk(user.domain);
|
|
27
|
+
if (!dbdomain) {
|
|
28
|
+
throw new ApiError({ code: 401, message: 'Unable to resolve default domain for this user' });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
throw new ApiError({ code: 401, message: 'Missing domain' });
|
|
24
33
|
}
|
|
25
34
|
if (dbdomain.user_id !== user.user_id) {
|
|
26
|
-
throw new ApiError({ code: 403, message: `Domain ${
|
|
35
|
+
throw new ApiError({ code: 403, message: `Domain ${dbdomain.name} is not owned by this user` });
|
|
27
36
|
}
|
|
28
37
|
apireq.domain = dbdomain;
|
|
29
38
|
apireq.user = user;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { ApiRoute, ApiModule } from '@technomoron/api-server-base';
|
|
2
|
+
import { mailApiServer } from '../server.js';
|
|
3
|
+
export declare class FormAPI extends ApiModule<mailApiServer> {
|
|
4
|
+
private readonly rateLimiter;
|
|
5
|
+
private postFormRecipient;
|
|
6
|
+
private postFormTemplate;
|
|
7
|
+
private postSendForm;
|
|
8
|
+
defineRoutes(): ApiRoute[];
|
|
9
|
+
}
|
package/dist/esm/api/forms.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { ApiModule, ApiError } from '@technomoron/api-server-base';
|
|
2
|
+
import { convert } from 'html-to-text';
|
|
2
3
|
import { nanoid } from 'nanoid';
|
|
3
4
|
import nunjucks from 'nunjucks';
|
|
4
5
|
import { UniqueConstraintError } from 'sequelize';
|
|
5
6
|
import { api_domain } from '../models/domain.js';
|
|
6
7
|
import { api_form } from '../models/form.js';
|
|
7
8
|
import { api_recipient } from '../models/recipient.js';
|
|
8
|
-
import { buildFormTemplateRecord, buildFormTemplatePaths, buildRecipientTo, buildReplyToValue, buildSubmissionContext, enforceAttachmentPolicy, enforceCaptchaPolicy, filterSubmissionFields, getPrimaryRecipientInfo, normalizeRecipientEmail, normalizeRecipientIdname, normalizeRecipientName,
|
|
9
|
+
import { buildFormTemplateRecord, buildFormTemplatePaths, buildRecipientTo, buildReplyToValue, buildSubmissionContext, enforceAttachmentPolicy, enforceCaptchaPolicy, filterSubmissionFields, getPrimaryRecipientInfo, normalizeRecipientEmail, normalizeRecipientIdname, normalizeRecipientName, parseFormTemplatePayload, parseRecipientPayload, parsePublicSubmissionOrThrow, resolveFormKeyForTemplate, resolveFormKeyForRecipient, resolveRecipients, validateFormTemplatePayload } from '../util/forms.js';
|
|
9
10
|
import { FixedWindowRateLimiter, enforceFormRateLimit } from '../util/ratelimit.js';
|
|
10
11
|
import { buildAttachments, cleanupUploadedFiles } from '../util/uploads.js';
|
|
11
12
|
import { getBodyValue } from '../util/utils.js';
|
|
@@ -139,7 +140,7 @@ export class FormAPI extends ApiModule {
|
|
|
139
140
|
const clientIp = apireq.getClientIp() ?? '';
|
|
140
141
|
await enforceCaptchaPolicy({ vars: env, form, captchaToken, clientIp });
|
|
141
142
|
const resolvedRecipients = await resolveRecipients(form, recipientsRaw);
|
|
142
|
-
const recipients =
|
|
143
|
+
const recipients = resolvedRecipients.map((r) => r.idname ?? '').filter(Boolean);
|
|
143
144
|
const { rcptEmail, rcptName, rcptIdname, rcptIdnames } = getPrimaryRecipientInfo(form, resolvedRecipients);
|
|
144
145
|
const domainRecord = await api_domain.findOne({ where: { domain_id: form.domain_id } });
|
|
145
146
|
await this.server.storage.relocateUploads(domainRecord?.name ?? null, rawFiles);
|
|
@@ -171,13 +172,15 @@ export class FormAPI extends ApiModule {
|
|
|
171
172
|
files: rawFiles,
|
|
172
173
|
meta
|
|
173
174
|
});
|
|
174
|
-
nunjucks.
|
|
175
|
-
const html =
|
|
175
|
+
const njkEnv = new nunjucks.Environment(null, { autoescape: this.server.storage.vars.AUTOESCAPE_HTML });
|
|
176
|
+
const html = njkEnv.renderString(form.template, context);
|
|
177
|
+
const text = convert(html);
|
|
176
178
|
const mailOptions = {
|
|
177
179
|
from: form.sender,
|
|
178
180
|
to,
|
|
179
181
|
subject: form.subject,
|
|
180
182
|
html,
|
|
183
|
+
text,
|
|
181
184
|
attachments: allAttachments,
|
|
182
185
|
...(replyToValue ? { replyTo: replyToValue } : {})
|
|
183
186
|
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { ApiModule, ApiRoute } from '@technomoron/api-server-base';
|
|
2
|
+
import { mailApiServer } from '../server.js';
|
|
3
|
+
export declare class MailerAPI extends ApiModule<mailApiServer> {
|
|
4
|
+
validateEmails(list: string): {
|
|
5
|
+
valid: string[];
|
|
6
|
+
invalid: string[];
|
|
7
|
+
};
|
|
8
|
+
private post_template;
|
|
9
|
+
private post_send;
|
|
10
|
+
defineRoutes(): ApiRoute[];
|
|
11
|
+
}
|
package/dist/esm/api/mailer.js
CHANGED
|
@@ -11,12 +11,13 @@ export class MailerAPI extends ApiModule {
|
|
|
11
11
|
// and valid email addresses.
|
|
12
12
|
//
|
|
13
13
|
validateEmails(list) {
|
|
14
|
-
const valid = []
|
|
14
|
+
const valid = [];
|
|
15
|
+
const invalid = [];
|
|
15
16
|
const emails = list
|
|
16
17
|
.split(',')
|
|
17
18
|
.map((email) => email.trim())
|
|
18
19
|
.filter((email) => email !== '');
|
|
19
|
-
|
|
20
|
+
for (const email of emails) {
|
|
20
21
|
const addr = validateEmail(email);
|
|
21
22
|
if (addr) {
|
|
22
23
|
valid.push(addr);
|
|
@@ -24,7 +25,7 @@ export class MailerAPI extends ApiModule {
|
|
|
24
25
|
else {
|
|
25
26
|
invalid.push(email);
|
|
26
27
|
}
|
|
27
|
-
}
|
|
28
|
+
}
|
|
28
29
|
return { valid, invalid };
|
|
29
30
|
}
|
|
30
31
|
// Store a template in the database
|
|
@@ -62,10 +63,10 @@ export class MailerAPI extends ApiModule {
|
|
|
62
63
|
}
|
|
63
64
|
// Send a template using posted arguments.
|
|
64
65
|
async post_send(apireq) {
|
|
65
|
-
const { name, rcpt,
|
|
66
|
+
const { name, rcpt, locale = '', vars = {}, replyTo, reply_to, headers } = apireq.req.body;
|
|
66
67
|
await assert_domain_and_user(apireq);
|
|
67
|
-
if (!name || !rcpt
|
|
68
|
-
throw new ApiError({ code: 400, message: 'name/rcpt
|
|
68
|
+
if (!name || !rcpt) {
|
|
69
|
+
throw new ApiError({ code: 400, message: 'name/rcpt required' });
|
|
69
70
|
}
|
|
70
71
|
let parsedVars = vars ?? {};
|
|
71
72
|
if (typeof vars === 'string') {
|
|
@@ -97,7 +98,7 @@ export class MailerAPI extends ApiModule {
|
|
|
97
98
|
if (!template) {
|
|
98
99
|
throw new ApiError({
|
|
99
100
|
code: 404,
|
|
100
|
-
message: `Template "${name}" not found for any locale in domain "${domain}"`
|
|
101
|
+
message: `Template "${name}" not found for any locale in domain "${apireq.domain.name}"`
|
|
101
102
|
});
|
|
102
103
|
}
|
|
103
104
|
const sender = template.sender || apireq.domain.sender || apireq.user.email;
|
|
File without changes
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { mailApiServer } from './server.js';
|
|
2
|
+
import { MailStoreVars, mailStore } from './store/store.js';
|
|
3
|
+
import type { ApiServerConf } from '@technomoron/api-server-base';
|
|
4
|
+
export type MailMagicServerOptions = Partial<ApiServerConf>;
|
|
5
|
+
export type MailMagicServerBootstrap = {
|
|
6
|
+
server: mailApiServer;
|
|
7
|
+
store: mailStore;
|
|
8
|
+
vars: MailStoreVars;
|
|
9
|
+
};
|
|
10
|
+
export declare const STARTUP_ERROR_MESSAGE = "Failed to start mail-magic:";
|
|
11
|
+
export declare function createMailMagicServer(overrides?: MailMagicServerOptions, envOverrides?: Partial<MailStoreVars>): Promise<MailMagicServerBootstrap>;
|
|
12
|
+
export declare function startMailMagicServer(overrides?: MailMagicServerOptions, envOverrides?: Partial<MailStoreVars>): Promise<MailMagicServerBootstrap>;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { Sequelize } from 'sequelize';
|
|
2
|
+
import { mailStore } from '../store/store.js';
|
|
3
|
+
export declare function usesSqlitePragmas(db: Pick<Sequelize, 'getDialect'>): boolean;
|
|
4
|
+
export declare function init_api_db(db: Sequelize, store: mailStore): Promise<void>;
|
|
5
|
+
export declare function connect_api_db(store: mailStore): Promise<Sequelize>;
|
package/dist/esm/models/db.js
CHANGED
|
@@ -97,10 +97,10 @@ export async function init_api_db(db, store) {
|
|
|
97
97
|
store.print_debug('API Database Initialized...');
|
|
98
98
|
}
|
|
99
99
|
export async function connect_api_db(store) {
|
|
100
|
-
|
|
100
|
+
store.print_debug('DB INIT');
|
|
101
101
|
const env = store.vars;
|
|
102
102
|
const dbparams = {
|
|
103
|
-
logging:
|
|
103
|
+
logging: env.DB_LOG ? console.log : false,
|
|
104
104
|
dialect: env.DB_TYPE,
|
|
105
105
|
dialectOptions: {
|
|
106
106
|
charset: 'utf8mb4'
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Sequelize, Model } from 'sequelize';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
export declare const api_domain_schema: z.ZodObject<{
|
|
4
|
+
domain_id: z.ZodNumber;
|
|
5
|
+
user_id: z.ZodNumber;
|
|
6
|
+
name: z.ZodString;
|
|
7
|
+
sender: z.ZodDefault<z.ZodString>;
|
|
8
|
+
locale: z.ZodDefault<z.ZodString>;
|
|
9
|
+
is_default: z.ZodDefault<z.ZodBoolean>;
|
|
10
|
+
}, z.core.$strip>;
|
|
11
|
+
export type api_domain_input = z.input<typeof api_domain_schema>;
|
|
12
|
+
export type api_domain_type = z.output<typeof api_domain_schema>;
|
|
13
|
+
export type api_domain_creation_type = Omit<api_domain_input, 'domain_id'> & {
|
|
14
|
+
domain_id?: number;
|
|
15
|
+
};
|
|
16
|
+
export declare class api_domain extends Model<api_domain_type, api_domain_creation_type> {
|
|
17
|
+
domain_id: number;
|
|
18
|
+
user_id: number;
|
|
19
|
+
name: string;
|
|
20
|
+
sender: string;
|
|
21
|
+
locale: string;
|
|
22
|
+
is_default: boolean;
|
|
23
|
+
}
|
|
24
|
+
export declare function init_api_domain(api_db: Sequelize): Promise<typeof api_domain>;
|
|
@@ -15,8 +15,6 @@ export const api_domain_schema = z
|
|
|
15
15
|
is_default: z.boolean().default(false).describe('If true, this is the default domain for the user.')
|
|
16
16
|
})
|
|
17
17
|
.describe('Domain configuration record.');
|
|
18
|
-
// Sequelize typing pattern: merge the Zod-inferred attribute type onto the model instance type.
|
|
19
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
|
|
20
18
|
export class api_domain extends Model {
|
|
21
19
|
}
|
|
22
20
|
export async function init_api_domain(api_db) {
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { Sequelize, Model } from 'sequelize';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { StoredFile } from '../types.js';
|
|
4
|
+
export declare const api_form_schema: z.ZodObject<{
|
|
5
|
+
form_id: z.ZodNumber;
|
|
6
|
+
form_key: z.ZodDefault<z.ZodString>;
|
|
7
|
+
user_id: z.ZodNumber;
|
|
8
|
+
domain_id: z.ZodNumber;
|
|
9
|
+
locale: z.ZodDefault<z.ZodString>;
|
|
10
|
+
idname: z.ZodString;
|
|
11
|
+
sender: z.ZodString;
|
|
12
|
+
recipient: z.ZodString;
|
|
13
|
+
subject: z.ZodString;
|
|
14
|
+
template: z.ZodDefault<z.ZodString>;
|
|
15
|
+
filename: z.ZodDefault<z.ZodString>;
|
|
16
|
+
slug: z.ZodDefault<z.ZodString>;
|
|
17
|
+
secret: z.ZodDefault<z.ZodString>;
|
|
18
|
+
replyto_email: z.ZodDefault<z.ZodString>;
|
|
19
|
+
replyto_from_fields: z.ZodDefault<z.ZodBoolean>;
|
|
20
|
+
allowed_fields: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
21
|
+
captcha_required: z.ZodDefault<z.ZodBoolean>;
|
|
22
|
+
files: z.ZodDefault<z.ZodArray<z.ZodType<StoredFile, unknown, z.core.$ZodTypeInternals<StoredFile, unknown>>>>;
|
|
23
|
+
}, z.core.$strip>;
|
|
24
|
+
export type api_form_input = z.input<typeof api_form_schema>;
|
|
25
|
+
export type api_form_type = z.output<typeof api_form_schema>;
|
|
26
|
+
export type api_form_creation_type = Omit<api_form_input, 'form_id'> & {
|
|
27
|
+
form_id?: number;
|
|
28
|
+
};
|
|
29
|
+
export declare class api_form extends Model<api_form_type, api_form_creation_type> {
|
|
30
|
+
form_id: number;
|
|
31
|
+
form_key: string;
|
|
32
|
+
user_id: number;
|
|
33
|
+
domain_id: number;
|
|
34
|
+
locale: string;
|
|
35
|
+
idname: string;
|
|
36
|
+
sender: string;
|
|
37
|
+
recipient: string;
|
|
38
|
+
subject: string;
|
|
39
|
+
template: string;
|
|
40
|
+
filename: string;
|
|
41
|
+
slug: string;
|
|
42
|
+
secret: string;
|
|
43
|
+
replyto_email: string;
|
|
44
|
+
replyto_from_fields: boolean;
|
|
45
|
+
allowed_fields: string[];
|
|
46
|
+
captcha_required: boolean;
|
|
47
|
+
files: StoredFile[];
|
|
48
|
+
}
|
|
49
|
+
export declare function init_api_form(api_db: Sequelize): Promise<typeof api_form>;
|
|
50
|
+
export declare function upsert_form(record: api_form_type): Promise<api_form>;
|
package/dist/esm/models/form.js
CHANGED
|
@@ -2,7 +2,7 @@ import path from 'path';
|
|
|
2
2
|
import { nanoid } from 'nanoid';
|
|
3
3
|
import { Model, DataTypes, UniqueConstraintError } from 'sequelize';
|
|
4
4
|
import { z } from 'zod';
|
|
5
|
-
import { assertSafeRelativePath } from '../util/paths.js';
|
|
5
|
+
import { assertSafeRelativePath, buildFormSlugAndFilename } from '../util/paths.js';
|
|
6
6
|
import { user_and_domain, normalizeSlug } from '../util.js';
|
|
7
7
|
const stored_file_schema = z
|
|
8
8
|
.object({
|
|
@@ -68,8 +68,6 @@ export const api_form_schema = z
|
|
|
68
68
|
.describe('Derived list of template-referenced assets (inline cids and external links) resolved during preprocessing/import.')
|
|
69
69
|
})
|
|
70
70
|
.describe('Form configuration and template used by the public form submission endpoint.');
|
|
71
|
-
// Sequelize typing pattern: merge the Zod-inferred attribute type onto the model instance type.
|
|
72
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
|
|
73
71
|
export class api_form extends Model {
|
|
74
72
|
}
|
|
75
73
|
export async function init_api_form(api_db) {
|
|
@@ -175,7 +173,9 @@ export async function init_api_form(api_db) {
|
|
|
175
173
|
}
|
|
176
174
|
try {
|
|
177
175
|
const parsed = JSON.parse(raw);
|
|
178
|
-
return Array.isArray(parsed)
|
|
176
|
+
return Array.isArray(parsed)
|
|
177
|
+
? parsed.filter((item) => typeof item === 'string')
|
|
178
|
+
: [];
|
|
179
179
|
}
|
|
180
180
|
catch {
|
|
181
181
|
return [];
|
|
@@ -233,23 +233,24 @@ export async function init_api_form(api_db) {
|
|
|
233
233
|
export async function upsert_form(record) {
|
|
234
234
|
const { user, domain } = await user_and_domain(record.domain_id);
|
|
235
235
|
const dname = normalizeSlug(domain.name);
|
|
236
|
-
const
|
|
237
|
-
|
|
236
|
+
const { slug, filename: generatedFilename } = buildFormSlugAndFilename({
|
|
237
|
+
domainName: domain.name,
|
|
238
|
+
domainLocale: domain.locale,
|
|
239
|
+
userLocale: user.locale,
|
|
240
|
+
idname: record.idname,
|
|
241
|
+
locale: record.locale
|
|
242
|
+
});
|
|
238
243
|
if (!record.slug) {
|
|
239
|
-
record.slug =
|
|
244
|
+
record.slug = slug;
|
|
240
245
|
}
|
|
241
246
|
if (!record.filename) {
|
|
242
|
-
|
|
243
|
-
if (locale)
|
|
244
|
-
parts.push(locale);
|
|
245
|
-
parts.push(name);
|
|
246
|
-
record.filename = path.join(...parts);
|
|
247
|
+
record.filename = generatedFilename;
|
|
247
248
|
}
|
|
248
249
|
else {
|
|
249
250
|
record.filename = path.join(dname, 'form-template', record.filename);
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
251
|
+
if (!record.filename.endsWith('.njk')) {
|
|
252
|
+
record.filename += '.njk';
|
|
253
|
+
}
|
|
253
254
|
}
|
|
254
255
|
record.filename = assertSafeRelativePath(record.filename, 'Form filename');
|
|
255
256
|
let instance = null;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { mailStore } from '../store/store.js';
|
|
2
|
+
import { StoredFile } from '../types.js';
|
|
3
|
+
import { api_form_type } from './form.js';
|
|
4
|
+
import { api_txmail_type } from './txmail.js';
|
|
5
|
+
interface LoadedTemplate {
|
|
6
|
+
html: string;
|
|
7
|
+
assets: StoredFile[];
|
|
8
|
+
}
|
|
9
|
+
export declare function loadFormTemplate(store: mailStore, form: api_form_type): Promise<LoadedTemplate>;
|
|
10
|
+
export declare function loadTxTemplate(store: mailStore, template: api_txmail_type): Promise<LoadedTemplate>;
|
|
11
|
+
export declare function importData(store: mailStore): Promise<void>;
|
|
12
|
+
export {};
|
package/dist/esm/models/init.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
|
-
import { Unyuck } from '@technomoron/unyuck';
|
|
4
3
|
import { z } from 'zod';
|
|
5
4
|
import { buildAssetUrl } from '../util/paths.js';
|
|
5
|
+
import { flattenTemplateWithAssets } from '../util/shared-template-flatten.js';
|
|
6
6
|
import { user_and_domain } from '../util.js';
|
|
7
7
|
import { api_domain, api_domain_schema } from './domain.js';
|
|
8
8
|
import { api_form_schema, upsert_form } from './form.js';
|
|
@@ -52,35 +52,14 @@ async function _load_template(store, filename, pathname, user, domain, locale, t
|
|
|
52
52
|
}
|
|
53
53
|
const assetBaseUrl = store.vars.ASSET_PUBLIC_BASE?.trim() ? store.vars.ASSET_PUBLIC_BASE : store.vars.API_URL;
|
|
54
54
|
const assetRoute = store.vars.ASSET_ROUTE;
|
|
55
|
-
const
|
|
56
|
-
|
|
55
|
+
const { html, assets } = flattenTemplateWithAssets({
|
|
56
|
+
domainRoot,
|
|
57
|
+
templateKey,
|
|
57
58
|
baseUrl: assetBaseUrl,
|
|
58
|
-
|
|
59
|
-
|
|
59
|
+
assetFormatter: (urlPath) => buildAssetUrl(assetBaseUrl, assetRoute, domain.name, urlPath),
|
|
60
|
+
normalizeInlineCid: buildInlineAssetCid
|
|
60
61
|
});
|
|
61
|
-
|
|
62
|
-
let html = mergedHtml;
|
|
63
|
-
const mappedAssets = assets.map((asset) => {
|
|
64
|
-
const rel = asset.filename.replace(/\\/g, '/');
|
|
65
|
-
const urlPath = rel.startsWith('assets/') ? rel.slice('assets/'.length) : rel;
|
|
66
|
-
return {
|
|
67
|
-
filename: urlPath,
|
|
68
|
-
path: asset.path,
|
|
69
|
-
cid: asset.cid ? buildInlineAssetCid(urlPath) : undefined
|
|
70
|
-
};
|
|
71
|
-
});
|
|
72
|
-
for (const asset of assets) {
|
|
73
|
-
if (!asset.cid) {
|
|
74
|
-
continue;
|
|
75
|
-
}
|
|
76
|
-
const rel = asset.filename.replace(/\\/g, '/');
|
|
77
|
-
const urlPath = rel.startsWith('assets/') ? rel.slice('assets/'.length) : rel;
|
|
78
|
-
const desiredCid = buildInlineAssetCid(urlPath);
|
|
79
|
-
if (asset.cid !== desiredCid) {
|
|
80
|
-
html = html.replaceAll(`cid:${asset.cid}`, `cid:${desiredCid}`);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
return { html, assets: mappedAssets };
|
|
62
|
+
return { html, assets: assets };
|
|
84
63
|
}
|
|
85
64
|
catch (err) {
|
|
86
65
|
throw new Error(`Template "${absPath}" failed to preprocess: ${err.message}`);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Sequelize, Model } from 'sequelize';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
export declare const api_recipient_schema: z.ZodObject<{
|
|
4
|
+
recipient_id: z.ZodNumber;
|
|
5
|
+
domain_id: z.ZodNumber;
|
|
6
|
+
form_key: z.ZodDefault<z.ZodString>;
|
|
7
|
+
idname: z.ZodString;
|
|
8
|
+
email: z.ZodString;
|
|
9
|
+
name: z.ZodDefault<z.ZodString>;
|
|
10
|
+
}, z.core.$strip>;
|
|
11
|
+
export type api_recipient_input = z.input<typeof api_recipient_schema>;
|
|
12
|
+
export type api_recipient_type = z.output<typeof api_recipient_schema>;
|
|
13
|
+
export type api_recipient_creation_type = Omit<api_recipient_input, 'recipient_id'> & {
|
|
14
|
+
recipient_id?: number;
|
|
15
|
+
};
|
|
16
|
+
export declare class api_recipient extends Model<api_recipient_type, api_recipient_creation_type> {
|
|
17
|
+
recipient_id: number;
|
|
18
|
+
domain_id: number;
|
|
19
|
+
form_key: string;
|
|
20
|
+
idname: string;
|
|
21
|
+
email: string;
|
|
22
|
+
name: string;
|
|
23
|
+
}
|
|
24
|
+
export declare function init_api_recipient(api_db: Sequelize): Promise<typeof api_recipient>;
|
|
@@ -11,8 +11,6 @@ export const api_recipient_schema = z
|
|
|
11
11
|
name: z.string().default('').describe('Optional recipient display name.')
|
|
12
12
|
})
|
|
13
13
|
.describe('Recipient routing record for form submissions.');
|
|
14
|
-
// Sequelize typing pattern: merge the Zod-inferred attribute type onto the model instance type.
|
|
15
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
|
|
16
14
|
export class api_recipient extends Model {
|
|
17
15
|
}
|
|
18
16
|
export async function init_api_recipient(api_db) {
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Sequelize, Model } from 'sequelize';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { StoredFile } from '../types.js';
|
|
4
|
+
export declare const api_txmail_schema: z.ZodObject<{
|
|
5
|
+
template_id: z.ZodNumber;
|
|
6
|
+
user_id: z.ZodNumber;
|
|
7
|
+
domain_id: z.ZodNumber;
|
|
8
|
+
name: z.ZodString;
|
|
9
|
+
locale: z.ZodDefault<z.ZodString>;
|
|
10
|
+
template: z.ZodDefault<z.ZodString>;
|
|
11
|
+
filename: z.ZodDefault<z.ZodString>;
|
|
12
|
+
sender: z.ZodString;
|
|
13
|
+
subject: z.ZodString;
|
|
14
|
+
slug: z.ZodDefault<z.ZodString>;
|
|
15
|
+
part: z.ZodDefault<z.ZodBoolean>;
|
|
16
|
+
files: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
17
|
+
filename: z.ZodString;
|
|
18
|
+
path: z.ZodString;
|
|
19
|
+
cid: z.ZodOptional<z.ZodString>;
|
|
20
|
+
}, z.core.$strip>>>;
|
|
21
|
+
}, z.core.$strip>;
|
|
22
|
+
export type api_txmail_input = z.input<typeof api_txmail_schema>;
|
|
23
|
+
export type api_txmail_type = z.output<typeof api_txmail_schema>;
|
|
24
|
+
export type api_txmail_creation_type = Omit<api_txmail_input, 'template_id'> & {
|
|
25
|
+
template_id?: number;
|
|
26
|
+
};
|
|
27
|
+
export declare class api_txmail extends Model<api_txmail_type, api_txmail_creation_type> {
|
|
28
|
+
template_id: number;
|
|
29
|
+
user_id: number;
|
|
30
|
+
domain_id: number;
|
|
31
|
+
name: string;
|
|
32
|
+
locale: string;
|
|
33
|
+
template: string;
|
|
34
|
+
filename: string;
|
|
35
|
+
sender: string;
|
|
36
|
+
subject: string;
|
|
37
|
+
slug: string;
|
|
38
|
+
part: boolean;
|
|
39
|
+
files: StoredFile[];
|
|
40
|
+
}
|
|
41
|
+
export declare function upsert_txmail(record: api_txmail_type): Promise<api_txmail>;
|
|
42
|
+
export declare function init_api_txmail(api_db: Sequelize): Promise<typeof api_txmail>;
|
|
@@ -26,8 +26,6 @@ export const api_txmail_schema = z
|
|
|
26
26
|
.describe('Derived list of template-referenced assets resolved during preprocessing/import.')
|
|
27
27
|
})
|
|
28
28
|
.describe('Transactional email template configuration.');
|
|
29
|
-
// Sequelize typing pattern: merge the Zod-inferred attribute type onto the model instance type.
|
|
30
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
|
|
31
29
|
export class api_txmail extends Model {
|
|
32
30
|
}
|
|
33
31
|
export async function upsert_txmail(record) {
|