@technomoron/mail-magic 1.0.40 → 1.0.42

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 (87) hide show
  1. package/CHANGES +45 -2
  2. package/README.md +5 -0
  3. package/dist/cjs/index.d.ts +1 -0
  4. package/dist/cjs/index.js +1 -1
  5. package/dist/esm/api/assets.d.ts +11 -0
  6. package/dist/esm/api/assets.js +48 -18
  7. package/dist/esm/api/auth.d.ts +2 -0
  8. package/dist/esm/api/auth.js +18 -9
  9. package/dist/esm/api/forms.d.ts +9 -0
  10. package/dist/esm/api/forms.js +42 -7
  11. package/dist/esm/api/mailer.d.ts +11 -0
  12. package/dist/esm/api/mailer.js +37 -8
  13. package/dist/esm/bin/mail-magic.d.ts +2 -0
  14. package/dist/esm/index.d.ts +12 -0
  15. package/dist/esm/index.js +5 -4
  16. package/dist/esm/models/db.d.ts +5 -0
  17. package/dist/esm/models/domain.d.ts +24 -0
  18. package/dist/esm/models/form.d.ts +50 -0
  19. package/dist/esm/models/form.js +16 -13
  20. package/dist/esm/models/init.d.ts +12 -0
  21. package/dist/esm/models/recipient.d.ts +24 -0
  22. package/dist/esm/models/txmail.d.ts +42 -0
  23. package/dist/esm/models/user.d.ts +33 -0
  24. package/dist/esm/server.d.ts +8 -0
  25. package/dist/esm/store/envloader.d.ts +188 -0
  26. package/dist/esm/store/envloader.js +9 -4
  27. package/dist/esm/store/store.d.ts +38 -0
  28. package/dist/esm/store/store.js +20 -16
  29. package/dist/esm/swagger.d.ts +10 -0
  30. package/dist/esm/types.d.ts +36 -0
  31. package/dist/esm/util/captcha.d.ts +7 -0
  32. package/dist/esm/util/captcha.js +4 -1
  33. package/dist/esm/util/email.d.ts +3 -0
  34. package/dist/esm/util/form-replyto.d.ts +6 -0
  35. package/dist/esm/util/form-submission.d.ts +24 -0
  36. package/dist/esm/util/forms.d.ts +140 -0
  37. package/dist/esm/util/forms.js +42 -39
  38. package/dist/esm/util/paths.d.ts +15 -0
  39. package/dist/esm/util/paths.js +17 -0
  40. package/dist/esm/util/ratelimit.d.ts +7 -0
  41. package/dist/esm/util/ratelimit.js +10 -41
  42. package/dist/esm/util/route.d.ts +1 -0
  43. package/dist/esm/util/shared-template-flatten.d.ts +17 -0
  44. package/dist/esm/util/uploads.d.ts +11 -0
  45. package/dist/esm/util/uploads.js +16 -11
  46. package/dist/esm/util/utils.d.ts +25 -0
  47. package/dist/esm/util/utils.js +0 -18
  48. package/dist/esm/util.d.ts +7 -0
  49. package/docs/swagger/openapi.json +16 -12
  50. package/examples/.env-dist +21 -0
  51. package/examples/README.md +74 -0
  52. package/examples/data/example.test/form-template/base.njk +4 -0
  53. package/examples/data/example.test/form-template/en/base.njk +1 -0
  54. package/examples/data/example.test/form-template/en/change-password.njk +5 -0
  55. package/examples/data/example.test/form-template/en/confirm-account.njk +5 -0
  56. package/examples/data/example.test/form-template/en/contact.njk +5 -0
  57. package/examples/data/example.test/form-template/en/partials/fields.njk +5 -0
  58. package/examples/data/example.test/form-template/en/welcome-signup.njk +5 -0
  59. package/examples/data/example.test/form-template/nb/base.njk +1 -0
  60. package/examples/data/example.test/form-template/nb/change-password.njk +5 -0
  61. package/examples/data/example.test/form-template/nb/confirm-account.njk +5 -0
  62. package/examples/data/example.test/form-template/nb/contact.njk +5 -0
  63. package/examples/data/example.test/form-template/nb/partials/fields.njk +5 -0
  64. package/examples/data/example.test/form-template/nb/welcome-signup.njk +5 -0
  65. package/examples/data/example.test/form-template/partials/header.njk +1 -0
  66. package/examples/data/example.test/tx-template/base.njk +16 -0
  67. package/examples/data/example.test/tx-template/en/base.njk +1 -0
  68. package/examples/data/example.test/tx-template/en/change-password.njk +7 -0
  69. package/examples/data/example.test/tx-template/en/confirm.njk +6 -0
  70. package/examples/data/example.test/tx-template/en/invoice.njk +8 -0
  71. package/examples/data/example.test/tx-template/en/partials/header.njk +1 -0
  72. package/examples/data/example.test/tx-template/en/partials/line-items.njk +14 -0
  73. package/examples/data/example.test/tx-template/en/receipt.njk +7 -0
  74. package/examples/data/example.test/tx-template/en/welcome.njk +5 -0
  75. package/examples/data/example.test/tx-template/nb/base.njk +1 -0
  76. package/examples/data/example.test/tx-template/nb/change-password.njk +6 -0
  77. package/examples/data/example.test/tx-template/nb/confirm.njk +6 -0
  78. package/examples/data/example.test/tx-template/nb/invoice.njk +7 -0
  79. package/examples/data/example.test/tx-template/nb/partials/header.njk +1 -0
  80. package/examples/data/example.test/tx-template/nb/receipt.njk +6 -0
  81. package/examples/data/example.test/tx-template/nb/welcome.njk +5 -0
  82. package/examples/data/example.test/tx-template/partials/header.njk +7 -0
  83. package/examples/data/init-data.json +213 -0
  84. package/examples/scripts/mm-api.ts +206 -0
  85. package/examples/scripts/public-form.ts +100 -0
  86. package/examples/scripts/send-messages.ts +114 -0
  87. package/package.json +7 -5
@@ -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({
@@ -173,7 +173,9 @@ export async function init_api_form(api_db) {
173
173
  }
174
174
  try {
175
175
  const parsed = JSON.parse(raw);
176
- return Array.isArray(parsed) ? parsed : [];
176
+ return Array.isArray(parsed)
177
+ ? parsed.filter((item) => typeof item === 'string')
178
+ : [];
177
179
  }
178
180
  catch {
179
181
  return [];
@@ -231,23 +233,24 @@ export async function init_api_form(api_db) {
231
233
  export async function upsert_form(record) {
232
234
  const { user, domain } = await user_and_domain(record.domain_id);
233
235
  const dname = normalizeSlug(domain.name);
234
- const name = normalizeSlug(record.idname);
235
- const locale = normalizeSlug(record.locale || domain.locale || user.locale || '');
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
+ });
236
243
  if (!record.slug) {
237
- record.slug = `${dname}${locale ? '-' + locale : ''}-${name}`;
244
+ record.slug = slug;
238
245
  }
239
246
  if (!record.filename) {
240
- const parts = [dname, 'form-template'];
241
- if (locale)
242
- parts.push(locale);
243
- parts.push(name);
244
- record.filename = path.join(...parts);
247
+ record.filename = generatedFilename;
245
248
  }
246
249
  else {
247
250
  record.filename = path.join(dname, 'form-template', record.filename);
248
- }
249
- if (!record.filename.endsWith('.njk')) {
250
- record.filename += '.njk';
251
+ if (!record.filename.endsWith('.njk')) {
252
+ record.filename += '.njk';
253
+ }
251
254
  }
252
255
  record.filename = assertSafeRelativePath(record.filename, 'Form filename');
253
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 {};
@@ -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>;
@@ -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>;
@@ -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>;
@@ -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)',
@@ -88,7 +88,7 @@ export const envOptions = defineEnvOptions({
88
88
  },
89
89
  DB_TYPE: {
90
90
  description: 'Database type for the API database',
91
- options: ['sqlite'],
91
+ options: ['sqlite', 'mysql', 'postgres'],
92
92
  default: 'sqlite'
93
93
  },
94
94
  DB_LOG: {
@@ -121,8 +121,13 @@ export const envOptions = defineEnvOptions({
121
121
  type: 'boolean'
122
122
  },
123
123
  SMTP_TLS_REJECT: {
124
- description: 'Reject bad cert/TLS connection to SMTP host',
125
- 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,
126
131
  type: 'boolean'
127
132
  },
128
133
  SMTP_USER: {
@@ -0,0 +1,38 @@
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
+ fieldname?: string;
8
+ originalname?: string;
9
+ filepath?: string;
10
+ buffer?: Buffer;
11
+ };
12
+ export type MailStoreVars = envConfig<typeof envOptions>;
13
+ type AutoReloadHandle = {
14
+ close: () => void;
15
+ };
16
+ type AutoReloadContext = {
17
+ vars: Pick<MailStoreVars, 'DB_AUTO_RELOAD'>;
18
+ config_filename: (name: string) => string;
19
+ print_debug: (msg: string) => void;
20
+ };
21
+ export declare function enableInitDataAutoReload(ctx: AutoReloadContext, reload: () => void): AutoReloadHandle | null;
22
+ export declare class mailStore {
23
+ private env;
24
+ vars: MailStoreVars;
25
+ transport?: Transporter<SMTPTransport.SentMessageInfo>;
26
+ api_db: Sequelize | null;
27
+ configpath: string;
28
+ uploadTemplate?: string;
29
+ uploadStagingPath?: string;
30
+ autoReloadHandle: AutoReloadHandle | null;
31
+ print_debug(msg: string): void;
32
+ config_filename(name: string): string;
33
+ resolveUploadPath(domainName?: string): string;
34
+ getUploadStagingPath(): string;
35
+ relocateUploads(domainName: string | null, files: UploadedFile[]): Promise<void>;
36
+ init(overrides?: Partial<MailStoreVars>): Promise<this>;
37
+ }
38
+ 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
  };
@@ -102,24 +102,28 @@ export class mailStore {
102
102
  }
103
103
  await fs.promises.mkdir(targetDir, { recursive: true });
104
104
  await Promise.all(files.map(async (file) => {
105
- if (!file?.path) {
105
+ const name = (file.originalname ?? file.filepath) ? path.basename(file.filepath ?? file.originalname ?? '') : '';
106
+ if (!name) {
106
107
  return;
107
108
  }
108
- const basename = path.basename(file.path);
109
- const destination = path.join(targetDir, basename);
110
- if (destination === file.path) {
111
- return;
112
- }
113
- try {
114
- await fs.promises.rename(file.path, destination);
115
- }
116
- catch {
117
- await fs.promises.copyFile(file.path, destination);
118
- await fs.promises.unlink(file.path);
109
+ const destination = path.join(targetDir, name);
110
+ if (file.buffer) {
111
+ await fs.promises.writeFile(destination, file.buffer);
112
+ file.filepath = destination;
113
+ file.buffer = undefined;
119
114
  }
120
- file.path = destination;
121
- if (file.destination !== undefined) {
122
- file.destination = targetDir;
115
+ else if (file.filepath) {
116
+ if (destination === file.filepath) {
117
+ return;
118
+ }
119
+ try {
120
+ await fs.promises.rename(file.filepath, destination);
121
+ }
122
+ catch {
123
+ await fs.promises.copyFile(file.filepath, destination);
124
+ await fs.promises.unlink(file.filepath);
125
+ }
126
+ file.filepath = destination;
123
127
  }
124
128
  }));
125
129
  }
@@ -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,36 @@
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
+ fieldname: string;
30
+ originalname: string;
31
+ encoding?: string;
32
+ mimetype?: string;
33
+ size?: number;
34
+ buffer?: Buffer;
35
+ filepath?: string;
36
+ }
@@ -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;