@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
|
@@ -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>;
|
package/dist/esm/models/user.js
CHANGED
|
@@ -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:
|
|
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: '
|
|
120
|
-
default:
|
|
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 {};
|
package/dist/esm/store/store.js
CHANGED
|
@@ -13,7 +13,7 @@ function create_mail_transport(vars) {
|
|
|
13
13
|
tls: {
|
|
14
14
|
rejectUnauthorized: vars.SMTP_TLS_REJECT
|
|
15
15
|
},
|
|
16
|
-
requireTLS:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|
package/dist/esm/util/captcha.js
CHANGED
|
@@ -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]
|
|
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,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>;
|
package/dist/esm/util/forms.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
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 {
|