@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.
Files changed (93) hide show
  1. package/CHANGES +48 -0
  2. package/README.md +7 -2
  3. package/dist/cjs/index.d.ts +1 -0
  4. package/dist/cjs/index.js +1 -1
  5. package/dist/cjs/package.json +1 -1
  6. package/dist/esm/api/assets.d.ts +9 -0
  7. package/dist/esm/api/auth.d.ts +2 -0
  8. package/dist/esm/api/auth.js +17 -8
  9. package/dist/esm/api/forms.d.ts +9 -0
  10. package/dist/esm/api/forms.js +7 -4
  11. package/dist/esm/api/mailer.d.ts +11 -0
  12. package/dist/esm/api/mailer.js +8 -7
  13. package/dist/esm/bin/mail-magic.d.ts +2 -0
  14. package/dist/esm/bin/mail-magic.js +0 -0
  15. package/dist/esm/index.d.ts +12 -0
  16. package/dist/esm/models/db.d.ts +5 -0
  17. package/dist/esm/models/db.js +2 -2
  18. package/dist/esm/models/domain.d.ts +24 -0
  19. package/dist/esm/models/domain.js +0 -2
  20. package/dist/esm/models/form.d.ts +50 -0
  21. package/dist/esm/models/form.js +16 -15
  22. package/dist/esm/models/init.d.ts +12 -0
  23. package/dist/esm/models/init.js +7 -28
  24. package/dist/esm/models/recipient.d.ts +24 -0
  25. package/dist/esm/models/recipient.js +0 -2
  26. package/dist/esm/models/txmail.d.ts +42 -0
  27. package/dist/esm/models/txmail.js +0 -2
  28. package/dist/esm/models/user.d.ts +33 -0
  29. package/dist/esm/models/user.js +0 -2
  30. package/dist/esm/server.d.ts +8 -0
  31. package/dist/esm/store/envloader.d.ts +188 -0
  32. package/dist/esm/store/envloader.js +14 -4
  33. package/dist/esm/store/store.d.ts +37 -0
  34. package/dist/esm/store/store.js +6 -7
  35. package/dist/esm/swagger.d.ts +10 -0
  36. package/dist/esm/types.d.ts +32 -0
  37. package/dist/esm/util/captcha.d.ts +7 -0
  38. package/dist/esm/util/captcha.js +4 -1
  39. package/dist/esm/util/email.d.ts +3 -0
  40. package/dist/esm/util/form-replyto.d.ts +6 -0
  41. package/dist/esm/util/form-submission.d.ts +24 -0
  42. package/dist/esm/util/forms.d.ts +140 -0
  43. package/dist/esm/util/forms.js +18 -32
  44. package/dist/esm/util/paths.d.ts +15 -0
  45. package/dist/esm/util/paths.js +17 -0
  46. package/dist/esm/util/ratelimit.d.ts +16 -0
  47. package/dist/esm/util/ratelimit.js +6 -1
  48. package/dist/esm/util/route.d.ts +1 -0
  49. package/dist/esm/util/shared-template-flatten.d.ts +17 -0
  50. package/dist/esm/util/shared-template-flatten.js +41 -0
  51. package/dist/esm/util/uploads.d.ts +10 -0
  52. package/dist/esm/util/utils.d.ts +27 -0
  53. package/dist/esm/util.d.ts +7 -0
  54. package/docs/swagger/openapi.json +16 -12
  55. package/docs/tutorial.md +2 -2
  56. package/examples/.env-dist +21 -0
  57. package/examples/README.md +74 -0
  58. package/examples/data/example.test/form-template/base.njk +4 -0
  59. package/examples/data/example.test/form-template/en/base.njk +1 -0
  60. package/examples/data/example.test/form-template/en/change-password.njk +5 -0
  61. package/examples/data/example.test/form-template/en/confirm-account.njk +5 -0
  62. package/examples/data/example.test/form-template/en/contact.njk +5 -0
  63. package/examples/data/example.test/form-template/en/partials/fields.njk +5 -0
  64. package/examples/data/example.test/form-template/en/welcome-signup.njk +5 -0
  65. package/examples/data/example.test/form-template/nb/base.njk +1 -0
  66. package/examples/data/example.test/form-template/nb/change-password.njk +5 -0
  67. package/examples/data/example.test/form-template/nb/confirm-account.njk +5 -0
  68. package/examples/data/example.test/form-template/nb/contact.njk +5 -0
  69. package/examples/data/example.test/form-template/nb/partials/fields.njk +5 -0
  70. package/examples/data/example.test/form-template/nb/welcome-signup.njk +5 -0
  71. package/examples/data/example.test/form-template/partials/header.njk +1 -0
  72. package/examples/data/example.test/tx-template/base.njk +16 -0
  73. package/examples/data/example.test/tx-template/en/base.njk +1 -0
  74. package/examples/data/example.test/tx-template/en/change-password.njk +7 -0
  75. package/examples/data/example.test/tx-template/en/confirm.njk +6 -0
  76. package/examples/data/example.test/tx-template/en/invoice.njk +8 -0
  77. package/examples/data/example.test/tx-template/en/partials/header.njk +1 -0
  78. package/examples/data/example.test/tx-template/en/partials/line-items.njk +14 -0
  79. package/examples/data/example.test/tx-template/en/receipt.njk +7 -0
  80. package/examples/data/example.test/tx-template/en/welcome.njk +5 -0
  81. package/examples/data/example.test/tx-template/nb/base.njk +1 -0
  82. package/examples/data/example.test/tx-template/nb/change-password.njk +6 -0
  83. package/examples/data/example.test/tx-template/nb/confirm.njk +6 -0
  84. package/examples/data/example.test/tx-template/nb/invoice.njk +7 -0
  85. package/examples/data/example.test/tx-template/nb/partials/header.njk +1 -0
  86. package/examples/data/example.test/tx-template/nb/receipt.njk +6 -0
  87. package/examples/data/example.test/tx-template/nb/welcome.njk +5 -0
  88. package/examples/data/example.test/tx-template/partials/header.njk +7 -0
  89. package/examples/data/init-data.json +213 -0
  90. package/examples/scripts/mm-api.ts +206 -0
  91. package/examples/scripts/public-form.ts +100 -0
  92. package/examples/scripts/send-messages.ts +114 -0
  93. package/package.json +90 -85
@@ -0,0 +1,33 @@
1
+ import { Sequelize, Model } from 'sequelize';
2
+ import { z } from 'zod';
3
+ export declare const api_user_schema: z.ZodObject<{
4
+ user_id: z.ZodNumber;
5
+ idname: z.ZodString;
6
+ token: z.ZodOptional<z.ZodString>;
7
+ token_hmac: z.ZodOptional<z.ZodString>;
8
+ name: z.ZodString;
9
+ email: z.ZodString;
10
+ domain: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
11
+ locale: z.ZodDefault<z.ZodString>;
12
+ }, z.core.$strip>;
13
+ export type api_user_input = z.input<typeof api_user_schema>;
14
+ export type api_user_type = z.output<typeof api_user_schema>;
15
+ export type api_user_creation_type = Omit<api_user_input, 'user_id'> & {
16
+ user_id?: number;
17
+ };
18
+ export declare class api_user extends Model<api_user_type, api_user_creation_type> {
19
+ user_id: number;
20
+ idname: string;
21
+ token: string | undefined;
22
+ token_hmac: string | undefined;
23
+ name: string;
24
+ email: string;
25
+ domain: number | null | undefined;
26
+ locale: string;
27
+ }
28
+ export declare function apiTokenToHmac(token: string, pepper: string): string;
29
+ export declare function migrateLegacyApiTokens(pepper: string): Promise<{
30
+ migrated: number;
31
+ cleared: number;
32
+ }>;
33
+ export declare function init_api_user(api_db: Sequelize): Promise<typeof api_user>;
@@ -13,8 +13,6 @@ export const api_user_schema = z
13
13
  locale: z.string().default('').describe('Default locale for the user.')
14
14
  })
15
15
  .describe('User account record and API credentials.');
16
- // Sequelize typing pattern: merge the Zod-inferred attribute type onto the model instance type.
17
- // eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
18
16
  export class api_user extends Model {
19
17
  }
20
18
  export function apiTokenToHmac(token, pepper) {
@@ -0,0 +1,8 @@
1
+ import { ApiServerConf, ApiServer } from '@technomoron/api-server-base';
2
+ import { mailStore } from './store/store.js';
3
+ export declare class mailApiServer extends ApiServer {
4
+ private store;
5
+ storage: mailStore;
6
+ constructor(config: Partial<ApiServerConf>, store: mailStore);
7
+ getApiKey<ApiKey>(token: string): Promise<ApiKey | null>;
8
+ }
@@ -0,0 +1,188 @@
1
+ export declare const envOptions: {
2
+ NODE_ENV: {
3
+ description: string;
4
+ options: string[];
5
+ default: string;
6
+ };
7
+ API_PORT: {
8
+ description: string;
9
+ default: string;
10
+ type: "number";
11
+ };
12
+ API_HOST: {
13
+ description: string;
14
+ default: string;
15
+ };
16
+ DB_AUTO_RELOAD: {
17
+ description: string;
18
+ type: "boolean";
19
+ default: false;
20
+ };
21
+ DB_FORCE_SYNC: {
22
+ description: string;
23
+ type: "boolean";
24
+ default: false;
25
+ };
26
+ DB_SYNC_ALTER: {
27
+ description: string;
28
+ type: "boolean";
29
+ default: false;
30
+ };
31
+ API_URL: {
32
+ description: string;
33
+ default: string;
34
+ };
35
+ API_BASE_PATH: {
36
+ description: string;
37
+ default: string;
38
+ };
39
+ ASSET_PUBLIC_BASE: {
40
+ description: string;
41
+ default: string;
42
+ };
43
+ SWAGGER_ENABLED: {
44
+ description: string;
45
+ type: "boolean";
46
+ default: false;
47
+ };
48
+ SWAGGER_PATH: {
49
+ description: string;
50
+ default: string;
51
+ };
52
+ ADMIN_ENABLED: {
53
+ description: string;
54
+ default: false;
55
+ type: "boolean";
56
+ };
57
+ ADMIN_APP_PATH: {
58
+ description: string;
59
+ default: string;
60
+ };
61
+ ASSET_ROUTE: {
62
+ description: string;
63
+ default: string;
64
+ };
65
+ CONFIG_PATH: {
66
+ description: string;
67
+ default: string;
68
+ };
69
+ GEN_ENV_TEMPLATE: {
70
+ description: string;
71
+ default: false;
72
+ type: "boolean";
73
+ };
74
+ DB_USER: {
75
+ description: string;
76
+ };
77
+ DB_PASS: {
78
+ description: string;
79
+ };
80
+ DB_NAME: {
81
+ description: string;
82
+ default: string;
83
+ };
84
+ DB_HOST: {
85
+ description: string;
86
+ default: string;
87
+ };
88
+ DB_TYPE: {
89
+ description: string;
90
+ options: string[];
91
+ default: string;
92
+ };
93
+ DB_LOG: {
94
+ description: string;
95
+ default: string;
96
+ type: "boolean";
97
+ };
98
+ DEBUG: {
99
+ description: string;
100
+ default: false;
101
+ type: "boolean";
102
+ };
103
+ AUTOESCAPE_HTML: {
104
+ description: string;
105
+ default: true;
106
+ type: "boolean";
107
+ };
108
+ SMTP_HOST: {
109
+ description: string;
110
+ default: string;
111
+ };
112
+ SMTP_PORT: {
113
+ description: string;
114
+ default: number;
115
+ type: "number";
116
+ };
117
+ SMTP_SECURE: {
118
+ description: string;
119
+ default: false;
120
+ type: "boolean";
121
+ };
122
+ SMTP_TLS_REJECT: {
123
+ description: string;
124
+ default: true;
125
+ type: "boolean";
126
+ };
127
+ SMTP_REQUIRE_TLS: {
128
+ description: string;
129
+ default: true;
130
+ type: "boolean";
131
+ };
132
+ SMTP_USER: {
133
+ description: string;
134
+ default: string;
135
+ };
136
+ SMTP_PASSWORD: {
137
+ description: string;
138
+ default: string;
139
+ };
140
+ UPLOAD_PATH: {
141
+ description: string;
142
+ default: string;
143
+ };
144
+ UPLOAD_MAX: {
145
+ description: string;
146
+ default: number;
147
+ type: "number";
148
+ };
149
+ FORM_RATE_LIMIT_WINDOW_SEC: {
150
+ description: string;
151
+ default: number;
152
+ type: "number";
153
+ };
154
+ FORM_RATE_LIMIT_MAX: {
155
+ description: string;
156
+ default: number;
157
+ type: "number";
158
+ };
159
+ FORM_MAX_ATTACHMENTS: {
160
+ description: string;
161
+ default: number;
162
+ type: "number";
163
+ };
164
+ FORM_KEEP_UPLOADS: {
165
+ description: string;
166
+ default: true;
167
+ type: "boolean";
168
+ };
169
+ FORM_CAPTCHA_PROVIDER: {
170
+ description: string;
171
+ options: string[];
172
+ default: string;
173
+ };
174
+ FORM_CAPTCHA_SECRET: {
175
+ description: string;
176
+ default: string;
177
+ };
178
+ FORM_CAPTCHA_REQUIRED: {
179
+ description: string;
180
+ default: false;
181
+ type: "boolean";
182
+ };
183
+ API_TOKEN_PEPPER: {
184
+ description: string;
185
+ required: true;
186
+ transform: (raw: string) => string;
187
+ };
188
+ };
@@ -27,7 +27,7 @@ export const envOptions = defineEnvOptions({
27
27
  DB_SYNC_ALTER: {
28
28
  description: 'Alter existing tables on startup to match models (requires write access to the DB)',
29
29
  type: 'boolean',
30
- default: true
30
+ default: false
31
31
  },
32
32
  API_URL: {
33
33
  description: 'Sets the public URL for the API (i.e. https://ml.example.com:3790)',
@@ -67,6 +67,11 @@ export const envOptions = defineEnvOptions({
67
67
  description: 'Path to directory where config files are located',
68
68
  default: './data/'
69
69
  },
70
+ GEN_ENV_TEMPLATE: {
71
+ description: 'Write .env-dist to current working directory on startup',
72
+ default: false,
73
+ type: 'boolean'
74
+ },
70
75
  DB_USER: {
71
76
  description: 'Database username for API database'
72
77
  },
@@ -83,7 +88,7 @@ export const envOptions = defineEnvOptions({
83
88
  },
84
89
  DB_TYPE: {
85
90
  description: 'Database type for the API database',
86
- options: ['sqlite'],
91
+ options: ['sqlite', 'mysql', 'postgres'],
87
92
  default: 'sqlite'
88
93
  },
89
94
  DB_LOG: {
@@ -116,8 +121,13 @@ export const envOptions = defineEnvOptions({
116
121
  type: 'boolean'
117
122
  },
118
123
  SMTP_TLS_REJECT: {
119
- description: 'Reject bad cert/TLS connection to SMTP host',
120
- default: false,
124
+ description: 'Validate the SMTP server TLS certificate. Set false to allow self-signed certificates.',
125
+ default: true,
126
+ type: 'boolean'
127
+ },
128
+ SMTP_REQUIRE_TLS: {
129
+ description: 'Require STARTTLS upgrade on SMTP connection. Set false for servers that do not support STARTTLS (e.g. MailHog).',
130
+ default: true,
121
131
  type: 'boolean'
122
132
  },
123
133
  SMTP_USER: {
@@ -0,0 +1,37 @@
1
+ import { envConfig } from '@technomoron/env-loader';
2
+ import { Transporter } from 'nodemailer';
3
+ import { Sequelize } from 'sequelize';
4
+ import { envOptions } from './envloader.js';
5
+ import type SMTPTransport from 'nodemailer/lib/smtp-transport';
6
+ type UploadedFile = {
7
+ path: string;
8
+ filename?: string;
9
+ destination?: string;
10
+ };
11
+ export type MailStoreVars = envConfig<typeof envOptions>;
12
+ type AutoReloadHandle = {
13
+ close: () => void;
14
+ };
15
+ type AutoReloadContext = {
16
+ vars: Pick<MailStoreVars, 'DB_AUTO_RELOAD'>;
17
+ config_filename: (name: string) => string;
18
+ print_debug: (msg: string) => void;
19
+ };
20
+ export declare function enableInitDataAutoReload(ctx: AutoReloadContext, reload: () => void): AutoReloadHandle | null;
21
+ export declare class mailStore {
22
+ private env;
23
+ vars: MailStoreVars;
24
+ transport?: Transporter<SMTPTransport.SentMessageInfo>;
25
+ api_db: Sequelize | null;
26
+ configpath: string;
27
+ uploadTemplate?: string;
28
+ uploadStagingPath?: string;
29
+ autoReloadHandle: AutoReloadHandle | null;
30
+ print_debug(msg: string): void;
31
+ config_filename(name: string): string;
32
+ resolveUploadPath(domainName?: string): string;
33
+ getUploadStagingPath(): string;
34
+ relocateUploads(domainName: string | null, files: UploadedFile[]): Promise<void>;
35
+ init(overrides?: Partial<MailStoreVars>): Promise<this>;
36
+ }
37
+ export {};
@@ -13,7 +13,7 @@ function create_mail_transport(vars) {
13
13
  tls: {
14
14
  rejectUnauthorized: vars.SMTP_TLS_REJECT
15
15
  },
16
- requireTLS: true,
16
+ requireTLS: vars.SMTP_REQUIRE_TLS,
17
17
  logger: vars.DEBUG,
18
18
  debug: vars.DEBUG
19
19
  };
@@ -22,10 +22,7 @@ function create_mail_transport(vars) {
22
22
  if (user && pass) {
23
23
  args.auth = { user, pass };
24
24
  }
25
- const mailer = createTransport({
26
- ...args
27
- });
28
- return mailer;
25
+ return createTransport(args);
29
26
  }
30
27
  export function enableInitDataAutoReload(ctx, reload) {
31
28
  if (!ctx.vars.DB_AUTO_RELOAD) {
@@ -167,10 +164,12 @@ export class mailStore {
167
164
  if (this.vars.FORM_CAPTCHA_REQUIRED && !String(this.vars.FORM_CAPTCHA_SECRET ?? '').trim()) {
168
165
  throw new Error('FORM_CAPTCHA_SECRET must be set when FORM_CAPTCHA_REQUIRED=true');
169
166
  }
170
- EnvLoader.genTemplate(envOptions, '.env-dist');
167
+ if (this.vars.GEN_ENV_TEMPLATE) {
168
+ EnvLoader.genTemplate(envOptions, '.env-dist');
169
+ }
171
170
  const p = this.vars.CONFIG_PATH;
172
171
  this.configpath = path.isAbsolute(p) ? p : path.resolve(process.cwd(), p);
173
- console.log(`Config path is ${this.configpath}`);
172
+ this.print_debug(`Config path is ${this.configpath}`);
174
173
  if (this.vars.UPLOAD_PATH && this.vars.UPLOAD_PATH.includes('{domain}')) {
175
174
  this.uploadTemplate = this.vars.UPLOAD_PATH;
176
175
  this.uploadStagingPath = path.resolve(this.configpath, '_uploads');
@@ -0,0 +1,10 @@
1
+ import type { mailApiServer } from './server.js';
2
+ type SwaggerInstallOptions = {
3
+ apiBasePath: string;
4
+ assetRoute: string;
5
+ apiUrl: string;
6
+ swaggerEnabled?: boolean;
7
+ swaggerPath?: string;
8
+ };
9
+ export declare function installMailMagicSwagger(server: mailApiServer, opts: SwaggerInstallOptions): void;
10
+ export {};
@@ -0,0 +1,32 @@
1
+ import { ApiRequest } from '@technomoron/api-server-base';
2
+ import { api_domain } from './models/domain.js';
3
+ import { api_user } from './models/user.js';
4
+ export interface mailApiKey {
5
+ uid: number;
6
+ }
7
+ export interface mailApiRequest extends ApiRequest {
8
+ user?: api_user;
9
+ domain?: api_domain;
10
+ locale?: string;
11
+ }
12
+ export interface formType {
13
+ rcpt: string;
14
+ sender: string;
15
+ subject: string;
16
+ template: string;
17
+ }
18
+ export interface StoredFile {
19
+ filename: string;
20
+ path: string;
21
+ cid?: string;
22
+ }
23
+ export interface RequestMeta {
24
+ client_ip: string;
25
+ received_at: string;
26
+ ip_chain: string[];
27
+ }
28
+ export interface UploadedFile {
29
+ originalname: string;
30
+ path: string;
31
+ fieldname: string;
32
+ }
@@ -0,0 +1,7 @@
1
+ export type CaptchaProvider = 'turnstile' | 'hcaptcha' | 'recaptcha';
2
+ export declare function verifyCaptcha(params: {
3
+ provider: CaptchaProvider;
4
+ secret: string;
5
+ token: string;
6
+ remoteip: string | null;
7
+ }): Promise<boolean>;
@@ -4,7 +4,10 @@ export async function verifyCaptcha(params) {
4
4
  hcaptcha: 'https://hcaptcha.com/siteverify',
5
5
  recaptcha: 'https://www.google.com/recaptcha/api/siteverify'
6
6
  };
7
- const endpoint = endpoints[params.provider] ?? endpoints.turnstile;
7
+ const endpoint = endpoints[params.provider];
8
+ if (!endpoint) {
9
+ throw new Error(`Unknown CAPTCHA provider: "${params.provider}"`);
10
+ }
8
11
  const body = new URLSearchParams();
9
12
  body.set('secret', params.secret);
10
13
  body.set('response', params.token);
@@ -0,0 +1,3 @@
1
+ import { ParsedMailbox } from 'email-addresses';
2
+ export declare function validateEmail(email: string): string | undefined;
3
+ export declare function parseMailbox(value: string): ParsedMailbox | undefined;
@@ -0,0 +1,6 @@
1
+ type ReplyToValue = string | {
2
+ name: string;
3
+ address: string;
4
+ };
5
+ export declare function extractReplyToFromSubmission(body: Record<string, unknown>): ReplyToValue | undefined;
6
+ export {};
@@ -0,0 +1,24 @@
1
+ import { z } from 'zod';
2
+ export declare const form_submission_schema: z.ZodObject<{
3
+ _mm_form_key: z.ZodString;
4
+ _mm_locale: z.ZodDefault<z.ZodOptional<z.ZodString>>;
5
+ _mm_recipients: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
6
+ email: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
7
+ name: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
8
+ first_name: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
9
+ last_name: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
10
+ 'cf-turnstile-response': z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
11
+ 'h-captcha-response': z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
12
+ 'g-recaptcha-response': z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
13
+ captcha: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
14
+ }, z.core.$loose>;
15
+ export type ParsedFormSubmission = {
16
+ mm: {
17
+ form_key: string;
18
+ locale: string;
19
+ captcha_token: string;
20
+ recipients_raw: unknown;
21
+ };
22
+ fields: Record<string, unknown>;
23
+ };
24
+ export declare function parseFormSubmissionInput(raw: unknown): ParsedFormSubmission;
@@ -0,0 +1,140 @@
1
+ import { ApiRequest } from '@technomoron/api-server-base';
2
+ import { api_form } from '../models/form.js';
3
+ import { api_recipient } from '../models/recipient.js';
4
+ import { ParsedFormSubmission } from './form-submission.js';
5
+ import type { api_domain } from '../models/domain.js';
6
+ import type { api_user } from '../models/user.js';
7
+ import type { RequestMeta, UploadedFile } from '../types.js';
8
+ export declare function parsePublicSubmissionOrThrow(apireq: ApiRequest): ParsedFormSubmission;
9
+ export declare function enforceAttachmentPolicy(env: {
10
+ FORM_MAX_ATTACHMENTS: number;
11
+ }, rawFiles: UploadedFile[]): void;
12
+ export declare function filterSubmissionFields(rawFields: Record<string, unknown>, allowedFields: unknown): Record<string, unknown>;
13
+ export declare function enforceCaptchaPolicy(params: {
14
+ vars: {
15
+ FORM_CAPTCHA_REQUIRED: boolean;
16
+ FORM_CAPTCHA_SECRET: string;
17
+ FORM_CAPTCHA_PROVIDER: string;
18
+ };
19
+ form: {
20
+ captcha_required: boolean;
21
+ };
22
+ captchaToken: string;
23
+ clientIp: string;
24
+ }): Promise<void>;
25
+ export declare function buildReplyToValue(form: {
26
+ replyto_email: string;
27
+ replyto_from_fields: boolean;
28
+ }, fields: Record<string, unknown>): (string | {
29
+ name: string;
30
+ address: string;
31
+ }) | undefined;
32
+ export declare function parseIdnameList(value: unknown, field: string): string[];
33
+ export type FormRecipientPayload = {
34
+ idnameRaw: string;
35
+ emailRaw: string;
36
+ nameRaw: string;
37
+ formKeyRaw: string;
38
+ formid: string;
39
+ localeRaw: string;
40
+ };
41
+ export declare function parseRecipientPayload(body: Record<string, unknown>): FormRecipientPayload;
42
+ export declare function normalizeRecipientIdname(raw: string): string;
43
+ export declare function normalizeRecipientEmail(raw: string): {
44
+ email: string;
45
+ mailbox: {
46
+ address: string;
47
+ name?: string | null;
48
+ };
49
+ };
50
+ export declare function normalizeRecipientName(raw: string, mailboxName?: string | null): string;
51
+ export declare function resolveFormKeyForRecipient(params: {
52
+ formKeyRaw: string;
53
+ formid: string;
54
+ localeRaw: string;
55
+ user: api_user;
56
+ domain: api_domain;
57
+ }): Promise<string>;
58
+ export declare function parseAllowedFields(raw: unknown): string[];
59
+ export type FormTemplateInput = {
60
+ template: string;
61
+ sender: string;
62
+ recipient: string;
63
+ idname: string;
64
+ subject: string;
65
+ locale: string;
66
+ secret: string;
67
+ replyto_email: string;
68
+ replyto_from_fields: boolean;
69
+ allowed_fields: string[];
70
+ captcha_required: boolean;
71
+ };
72
+ export declare function parseFormTemplatePayload(body: Record<string, unknown>): FormTemplateInput;
73
+ export declare function validateFormTemplatePayload(payload: FormTemplateInput): void;
74
+ export declare function buildFormTemplatePaths(params: {
75
+ user: api_user;
76
+ domain: api_domain;
77
+ idname: string;
78
+ locale: string;
79
+ }): {
80
+ localeSlug: string;
81
+ slug: string;
82
+ filename: string;
83
+ };
84
+ export declare function resolveFormKeyForTemplate(params: {
85
+ user_id: number;
86
+ domain_id: number;
87
+ locale: string;
88
+ idname: string;
89
+ }): Promise<string>;
90
+ export declare function buildFormTemplateRecord(params: {
91
+ form_key: string;
92
+ user_id: number;
93
+ domain_id: number;
94
+ locale: string;
95
+ slug: string;
96
+ filename: string;
97
+ payload: FormTemplateInput;
98
+ }): {
99
+ form_key: string;
100
+ user_id: number;
101
+ domain_id: number;
102
+ locale: string;
103
+ idname: string;
104
+ sender: string;
105
+ recipient: string;
106
+ subject: string;
107
+ template: string;
108
+ slug: string;
109
+ filename: string;
110
+ secret: string;
111
+ replyto_email: string;
112
+ replyto_from_fields: boolean;
113
+ allowed_fields: string[];
114
+ captcha_required: boolean;
115
+ files: never[];
116
+ };
117
+ export declare function resolveRecipients(form: api_form, recipientsRaw: unknown): Promise<api_recipient[]>;
118
+ export declare function buildRecipientTo(form: api_form, recipients: api_recipient[]): string | (string | {
119
+ name: string;
120
+ address: string;
121
+ })[];
122
+ export declare function getPrimaryRecipientInfo(form: api_form, recipients: api_recipient[]): {
123
+ rcptEmail: string;
124
+ rcptName: string;
125
+ rcptIdname: string;
126
+ rcptIdnames: string[];
127
+ };
128
+ export declare function buildSubmissionContext(params: {
129
+ form_key: string;
130
+ localeRaw: string;
131
+ recipients: string[];
132
+ rcptEmail: string;
133
+ rcptName: string;
134
+ rcptIdname: string;
135
+ rcptIdnames: string[];
136
+ attachmentMap: Record<string, string>;
137
+ fields: Record<string, unknown>;
138
+ files: UploadedFile[];
139
+ meta: RequestMeta;
140
+ }): Record<string, unknown>;
@@ -1,4 +1,3 @@
1
- import path from 'path';
2
1
  import { ApiError } from '@technomoron/api-server-base';
3
2
  import { api_form } from '../models/form.js';
4
3
  import { api_recipient } from '../models/recipient.js';
@@ -6,6 +5,7 @@ import { verifyCaptcha } from './captcha.js';
6
5
  import { parseMailbox } from './email.js';
7
6
  import { extractReplyToFromSubmission } from './form-replyto.js';
8
7
  import { parseFormSubmissionInput } from './form-submission.js';
8
+ import { buildFormSlugAndFilename } from './paths.js';
9
9
  import { normalizeBoolean, normalizeSlug } from './utils.js';
10
10
  export function parsePublicSubmissionOrThrow(apireq) {
11
11
  try {
@@ -90,15 +90,13 @@ export async function enforceCaptchaPolicy(params) {
90
90
  }
91
91
  export function buildReplyToValue(form, fields) {
92
92
  const forced = typeof form.replyto_email === 'string' ? form.replyto_email.trim() : '';
93
- const forcedValue = forced ? forced : '';
94
93
  if (form.replyto_from_fields) {
95
94
  const extracted = extractReplyToFromSubmission(fields);
96
95
  if (extracted) {
97
96
  return extracted;
98
97
  }
99
- return forcedValue || undefined;
100
98
  }
101
- return forcedValue || undefined;
99
+ return forced || undefined;
102
100
  }
103
101
  export function parseIdnameList(value, field) {
104
102
  if (value === undefined || value === null || value === '') {
@@ -299,36 +297,24 @@ export function validateFormTemplatePayload(payload) {
299
297
  }
300
298
  }
301
299
  export function buildFormTemplatePaths(params) {
302
- const domainSlug = normalizeSlug(params.domain.name);
303
- const formSlug = normalizeSlug(params.idname);
304
- const localeSlug = normalizeSlug(params.locale || params.domain.locale || params.user.locale || '');
305
- const slug = `${domainSlug}${localeSlug ? '-' + localeSlug : ''}-${formSlug}`;
306
- const filenameParts = [domainSlug, 'form-template'];
307
- if (localeSlug) {
308
- filenameParts.push(localeSlug);
309
- }
310
- filenameParts.push(formSlug);
311
- let filename = path.join(...filenameParts);
312
- if (!filename.endsWith('.njk')) {
313
- filename += '.njk';
314
- }
315
- return { localeSlug, slug, filename };
300
+ return buildFormSlugAndFilename({
301
+ domainName: params.domain.name,
302
+ domainLocale: params.domain.locale,
303
+ userLocale: params.user.locale,
304
+ idname: params.idname,
305
+ locale: params.locale
306
+ });
316
307
  }
317
308
  export async function resolveFormKeyForTemplate(params) {
318
- try {
319
- const existing = await api_form.findOne({
320
- where: {
321
- user_id: params.user_id,
322
- domain_id: params.domain_id,
323
- locale: params.locale,
324
- idname: params.idname
325
- }
326
- });
327
- return existing?.form_key || '';
328
- }
329
- catch {
330
- return '';
331
- }
309
+ const existing = await api_form.findOne({
310
+ where: {
311
+ user_id: params.user_id,
312
+ domain_id: params.domain_id,
313
+ locale: params.locale,
314
+ idname: params.idname
315
+ }
316
+ });
317
+ return existing?.form_key || '';
332
318
  }
333
319
  export function buildFormTemplateRecord(params) {
334
320
  return {