@technomoron/mail-magic 1.0.40 → 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 +23 -2
- package/README.md +5 -0
- package/dist/cjs/index.d.ts +1 -0
- package/dist/cjs/index.js +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 +4 -4
- package/dist/esm/bin/mail-magic.d.ts +2 -0
- package/dist/esm/index.d.ts +12 -0
- package/dist/esm/models/db.d.ts +5 -0
- package/dist/esm/models/domain.d.ts +24 -0
- package/dist/esm/models/form.d.ts +50 -0
- package/dist/esm/models/form.js +16 -13
- package/dist/esm/models/init.d.ts +12 -0
- package/dist/esm/models/recipient.d.ts +24 -0
- package/dist/esm/models/txmail.d.ts +42 -0
- package/dist/esm/models/user.d.ts +33 -0
- package/dist/esm/server.d.ts +8 -0
- package/dist/esm/store/envloader.d.ts +188 -0
- package/dist/esm/store/envloader.js +9 -4
- package/dist/esm/store/store.d.ts +37 -0
- package/dist/esm/store/store.js +1 -1
- 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 +17 -29
- 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/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/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 +6 -4
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
{
|
|
2
|
+
"user": [
|
|
3
|
+
{
|
|
4
|
+
"user_id": 1,
|
|
5
|
+
"idname": "example",
|
|
6
|
+
"token": "example-token",
|
|
7
|
+
"name": "Example User",
|
|
8
|
+
"email": "noreply@example.test",
|
|
9
|
+
"domain": 1,
|
|
10
|
+
"locale": "en"
|
|
11
|
+
}
|
|
12
|
+
],
|
|
13
|
+
"domain": [
|
|
14
|
+
{
|
|
15
|
+
"domain_id": 1,
|
|
16
|
+
"user_id": 1,
|
|
17
|
+
"name": "example.test",
|
|
18
|
+
"sender": "Example <noreply@example.test>",
|
|
19
|
+
"locale": "en",
|
|
20
|
+
"is_default": true
|
|
21
|
+
}
|
|
22
|
+
],
|
|
23
|
+
"template": [
|
|
24
|
+
{
|
|
25
|
+
"template_id": 1,
|
|
26
|
+
"user_id": 1,
|
|
27
|
+
"domain_id": 1,
|
|
28
|
+
"name": "welcome",
|
|
29
|
+
"locale": "en",
|
|
30
|
+
"sender": "Example <noreply@example.test>",
|
|
31
|
+
"subject": "Welcome"
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"template_id": 2,
|
|
35
|
+
"user_id": 1,
|
|
36
|
+
"domain_id": 1,
|
|
37
|
+
"name": "confirm",
|
|
38
|
+
"locale": "en",
|
|
39
|
+
"sender": "Example <noreply@example.test>",
|
|
40
|
+
"subject": "Confirm your account"
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"template_id": 3,
|
|
44
|
+
"user_id": 1,
|
|
45
|
+
"domain_id": 1,
|
|
46
|
+
"name": "change-password",
|
|
47
|
+
"locale": "en",
|
|
48
|
+
"sender": "Example <noreply@example.test>",
|
|
49
|
+
"subject": "Change password"
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"template_id": 4,
|
|
53
|
+
"user_id": 1,
|
|
54
|
+
"domain_id": 1,
|
|
55
|
+
"name": "receipt",
|
|
56
|
+
"locale": "en",
|
|
57
|
+
"sender": "Example <noreply@example.test>",
|
|
58
|
+
"subject": "Your receipt"
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"template_id": 5,
|
|
62
|
+
"user_id": 1,
|
|
63
|
+
"domain_id": 1,
|
|
64
|
+
"name": "invoice",
|
|
65
|
+
"locale": "en",
|
|
66
|
+
"sender": "Example <noreply@example.test>",
|
|
67
|
+
"subject": "Your invoice"
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"template_id": 6,
|
|
71
|
+
"user_id": 1,
|
|
72
|
+
"domain_id": 1,
|
|
73
|
+
"name": "welcome",
|
|
74
|
+
"locale": "nb",
|
|
75
|
+
"sender": "Example <noreply@example.test>",
|
|
76
|
+
"subject": "Velkommen"
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
"template_id": 7,
|
|
80
|
+
"user_id": 1,
|
|
81
|
+
"domain_id": 1,
|
|
82
|
+
"name": "confirm",
|
|
83
|
+
"locale": "nb",
|
|
84
|
+
"sender": "Example <noreply@example.test>",
|
|
85
|
+
"subject": "Bekreft konto"
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
"template_id": 8,
|
|
89
|
+
"user_id": 1,
|
|
90
|
+
"domain_id": 1,
|
|
91
|
+
"name": "change-password",
|
|
92
|
+
"locale": "nb",
|
|
93
|
+
"sender": "Example <noreply@example.test>",
|
|
94
|
+
"subject": "Endre passord"
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
"template_id": 9,
|
|
98
|
+
"user_id": 1,
|
|
99
|
+
"domain_id": 1,
|
|
100
|
+
"name": "receipt",
|
|
101
|
+
"locale": "nb",
|
|
102
|
+
"sender": "Example <noreply@example.test>",
|
|
103
|
+
"subject": "Kvittering"
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
"template_id": 10,
|
|
107
|
+
"user_id": 1,
|
|
108
|
+
"domain_id": 1,
|
|
109
|
+
"name": "invoice",
|
|
110
|
+
"locale": "nb",
|
|
111
|
+
"sender": "Example <noreply@example.test>",
|
|
112
|
+
"subject": "Faktura"
|
|
113
|
+
}
|
|
114
|
+
],
|
|
115
|
+
"form": [
|
|
116
|
+
{
|
|
117
|
+
"form_id": 1,
|
|
118
|
+
"form_key": "example-contact-en",
|
|
119
|
+
"user_id": 1,
|
|
120
|
+
"domain_id": 1,
|
|
121
|
+
"idname": "contact",
|
|
122
|
+
"locale": "en",
|
|
123
|
+
"sender": "Example Forms <forms@example.test>",
|
|
124
|
+
"recipient": "owner@example.test",
|
|
125
|
+
"subject": "Contact form",
|
|
126
|
+
"secret": "form-secret"
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
"form_id": 2,
|
|
130
|
+
"form_key": "example-welcome-signup-en",
|
|
131
|
+
"user_id": 1,
|
|
132
|
+
"domain_id": 1,
|
|
133
|
+
"idname": "welcome-signup",
|
|
134
|
+
"locale": "en",
|
|
135
|
+
"sender": "Example Forms <forms@example.test>",
|
|
136
|
+
"recipient": "owner@example.test",
|
|
137
|
+
"subject": "Welcome signup",
|
|
138
|
+
"secret": "form-secret"
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
"form_id": 3,
|
|
142
|
+
"form_key": "example-confirm-account-en",
|
|
143
|
+
"user_id": 1,
|
|
144
|
+
"domain_id": 1,
|
|
145
|
+
"idname": "confirm-account",
|
|
146
|
+
"locale": "en",
|
|
147
|
+
"sender": "Example Forms <forms@example.test>",
|
|
148
|
+
"recipient": "owner@example.test",
|
|
149
|
+
"subject": "Confirm account form",
|
|
150
|
+
"secret": "form-secret"
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
"form_id": 4,
|
|
154
|
+
"form_key": "example-change-password-en",
|
|
155
|
+
"user_id": 1,
|
|
156
|
+
"domain_id": 1,
|
|
157
|
+
"idname": "change-password",
|
|
158
|
+
"locale": "en",
|
|
159
|
+
"sender": "Example Forms <forms@example.test>",
|
|
160
|
+
"recipient": "owner@example.test",
|
|
161
|
+
"subject": "Change password form",
|
|
162
|
+
"secret": "form-secret"
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
"form_id": 5,
|
|
166
|
+
"form_key": "example-contact-nb",
|
|
167
|
+
"user_id": 1,
|
|
168
|
+
"domain_id": 1,
|
|
169
|
+
"idname": "contact",
|
|
170
|
+
"locale": "nb",
|
|
171
|
+
"sender": "Example Forms <forms@example.test>",
|
|
172
|
+
"recipient": "owner@example.test",
|
|
173
|
+
"subject": "Kontakt",
|
|
174
|
+
"secret": "form-secret"
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
"form_id": 6,
|
|
178
|
+
"form_key": "example-welcome-signup-nb",
|
|
179
|
+
"user_id": 1,
|
|
180
|
+
"domain_id": 1,
|
|
181
|
+
"idname": "welcome-signup",
|
|
182
|
+
"locale": "nb",
|
|
183
|
+
"sender": "Example Forms <forms@example.test>",
|
|
184
|
+
"recipient": "owner@example.test",
|
|
185
|
+
"subject": "Registrering",
|
|
186
|
+
"secret": "form-secret"
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
"form_id": 7,
|
|
190
|
+
"form_key": "example-confirm-account-nb",
|
|
191
|
+
"user_id": 1,
|
|
192
|
+
"domain_id": 1,
|
|
193
|
+
"idname": "confirm-account",
|
|
194
|
+
"locale": "nb",
|
|
195
|
+
"sender": "Example Forms <forms@example.test>",
|
|
196
|
+
"recipient": "owner@example.test",
|
|
197
|
+
"subject": "Bekreft konto form",
|
|
198
|
+
"secret": "form-secret"
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
"form_id": 8,
|
|
202
|
+
"form_key": "example-change-password-nb",
|
|
203
|
+
"user_id": 1,
|
|
204
|
+
"domain_id": 1,
|
|
205
|
+
"idname": "change-password",
|
|
206
|
+
"locale": "nb",
|
|
207
|
+
"sender": "Example Forms <forms@example.test>",
|
|
208
|
+
"recipient": "owner@example.test",
|
|
209
|
+
"subject": "Endre passord form",
|
|
210
|
+
"secret": "form-secret"
|
|
211
|
+
}
|
|
212
|
+
]
|
|
213
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import TemplateClient from '../../client/src/mail-magic-client.ts';
|
|
5
|
+
|
|
6
|
+
type Command = 'template' | 'asset' | 'path';
|
|
7
|
+
|
|
8
|
+
type Options = {
|
|
9
|
+
apiUrl?: string;
|
|
10
|
+
token?: string;
|
|
11
|
+
files: string[];
|
|
12
|
+
name?: string;
|
|
13
|
+
domain?: string;
|
|
14
|
+
sender?: string;
|
|
15
|
+
subject?: string;
|
|
16
|
+
locale?: string;
|
|
17
|
+
path?: string;
|
|
18
|
+
templateType?: 'tx' | 'form';
|
|
19
|
+
template?: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const DEFAULT_BASE_URL = 'http://127.0.0.1:3776';
|
|
23
|
+
|
|
24
|
+
function usage(): void {
|
|
25
|
+
const text = `Usage:
|
|
26
|
+
mm-api.ts template --file <template.njk> --name <name> --domain <domain> [--sender "Name <email>"] [--subject <subject>] [--locale <locale>]
|
|
27
|
+
mm-api.ts asset --file <file> --domain <domain> [--path <subdir>] [--template-type tx|form] [--template <name>] [--locale <locale>]
|
|
28
|
+
mm-api.ts path --file <file> --domain <domain> --path <subdir> [--template-type tx|form] [--template <name>] [--locale <locale>]
|
|
29
|
+
|
|
30
|
+
Environment:
|
|
31
|
+
MM_BASE_URL Base API URL (default: http://127.0.0.1:3776)
|
|
32
|
+
MM_API_URL Alternate API URL (if set, /api is stripped)
|
|
33
|
+
MM_TOKEN API token (default: example-token)
|
|
34
|
+
MM_DOMAIN Default domain
|
|
35
|
+
`;
|
|
36
|
+
console.log(text);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function normalizeBaseUrl(value: string): string {
|
|
40
|
+
const trimmed = value.replace(/\/+$/, '');
|
|
41
|
+
if (trimmed.endsWith('/api')) {
|
|
42
|
+
return trimmed.slice(0, -4);
|
|
43
|
+
}
|
|
44
|
+
return trimmed;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function parseArgs(argv: string[]): { command: Command; options: Options } {
|
|
48
|
+
if (argv.length < 3) {
|
|
49
|
+
usage();
|
|
50
|
+
throw new Error('Missing command');
|
|
51
|
+
}
|
|
52
|
+
const command = argv[2] as Command;
|
|
53
|
+
if (command !== 'template' && command !== 'asset' && command !== 'path') {
|
|
54
|
+
usage();
|
|
55
|
+
throw new Error(`Unknown command: ${command}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const options: Options = { files: [] };
|
|
59
|
+
let index = 3;
|
|
60
|
+
while (index < argv.length) {
|
|
61
|
+
const key = argv[index];
|
|
62
|
+
const next = argv[index + 1];
|
|
63
|
+
if (!key.startsWith('--')) {
|
|
64
|
+
usage();
|
|
65
|
+
throw new Error(`Unknown argument: ${key}`);
|
|
66
|
+
}
|
|
67
|
+
switch (key) {
|
|
68
|
+
case '--file':
|
|
69
|
+
if (!next) {
|
|
70
|
+
throw new Error('Missing value for --file');
|
|
71
|
+
}
|
|
72
|
+
options.files.push(next);
|
|
73
|
+
index += 2;
|
|
74
|
+
break;
|
|
75
|
+
case '--name':
|
|
76
|
+
options.name = next;
|
|
77
|
+
index += 2;
|
|
78
|
+
break;
|
|
79
|
+
case '--domain':
|
|
80
|
+
options.domain = next;
|
|
81
|
+
index += 2;
|
|
82
|
+
break;
|
|
83
|
+
case '--sender':
|
|
84
|
+
options.sender = next;
|
|
85
|
+
index += 2;
|
|
86
|
+
break;
|
|
87
|
+
case '--subject':
|
|
88
|
+
options.subject = next;
|
|
89
|
+
index += 2;
|
|
90
|
+
break;
|
|
91
|
+
case '--locale':
|
|
92
|
+
options.locale = next;
|
|
93
|
+
index += 2;
|
|
94
|
+
break;
|
|
95
|
+
case '--path':
|
|
96
|
+
options.path = next;
|
|
97
|
+
index += 2;
|
|
98
|
+
break;
|
|
99
|
+
case '--template-type':
|
|
100
|
+
if (next !== 'tx' && next !== 'form') {
|
|
101
|
+
throw new Error('template-type must be tx or form');
|
|
102
|
+
}
|
|
103
|
+
options.templateType = next;
|
|
104
|
+
index += 2;
|
|
105
|
+
break;
|
|
106
|
+
case '--template':
|
|
107
|
+
options.template = next;
|
|
108
|
+
index += 2;
|
|
109
|
+
break;
|
|
110
|
+
case '--api':
|
|
111
|
+
options.apiUrl = next;
|
|
112
|
+
index += 2;
|
|
113
|
+
break;
|
|
114
|
+
case '--token':
|
|
115
|
+
options.token = next;
|
|
116
|
+
index += 2;
|
|
117
|
+
break;
|
|
118
|
+
case '--help':
|
|
119
|
+
usage();
|
|
120
|
+
process.exit(0);
|
|
121
|
+
break;
|
|
122
|
+
default:
|
|
123
|
+
usage();
|
|
124
|
+
throw new Error(`Unknown argument: ${key}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return { command, options };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function resolveFileList(files: string[]): string[] {
|
|
132
|
+
if (!files.length) {
|
|
133
|
+
throw new Error('At least one --file is required');
|
|
134
|
+
}
|
|
135
|
+
const resolved: string[] = [];
|
|
136
|
+
for (const file of files) {
|
|
137
|
+
const fullPath = path.resolve(file);
|
|
138
|
+
if (!fs.existsSync(fullPath)) {
|
|
139
|
+
throw new Error(`File not found: ${file}`);
|
|
140
|
+
}
|
|
141
|
+
resolved.push(fullPath);
|
|
142
|
+
}
|
|
143
|
+
return resolved;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function runTemplate(client: TemplateClient, options: Options): Promise<void> {
|
|
147
|
+
if (!options.domain || !options.name) {
|
|
148
|
+
throw new Error('template requires --domain and --name');
|
|
149
|
+
}
|
|
150
|
+
const [file] = resolveFileList(options.files);
|
|
151
|
+
const template = fs.readFileSync(file, 'utf8');
|
|
152
|
+
await client.storeTxTemplate({
|
|
153
|
+
domain: options.domain,
|
|
154
|
+
name: options.name,
|
|
155
|
+
sender: options.sender,
|
|
156
|
+
subject: options.subject,
|
|
157
|
+
locale: options.locale,
|
|
158
|
+
template
|
|
159
|
+
});
|
|
160
|
+
console.log(`Stored template '${options.name}' for ${options.domain}`);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async function runAsset(client: TemplateClient, options: Options): Promise<void> {
|
|
164
|
+
if (!options.domain) {
|
|
165
|
+
throw new Error('asset requires --domain');
|
|
166
|
+
}
|
|
167
|
+
const files = resolveFileList(options.files);
|
|
168
|
+
await client.uploadAssets({
|
|
169
|
+
domain: options.domain,
|
|
170
|
+
files,
|
|
171
|
+
templateType: options.templateType,
|
|
172
|
+
template: options.template,
|
|
173
|
+
locale: options.locale,
|
|
174
|
+
path: options.path
|
|
175
|
+
});
|
|
176
|
+
console.log(`Uploaded ${files.length} asset(s) for ${options.domain}`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async function main(): Promise<void> {
|
|
180
|
+
const { command, options } = parseArgs(process.argv);
|
|
181
|
+
const baseUrlRaw = options.apiUrl || DEFAULT_BASE_URL;
|
|
182
|
+
const baseUrl = normalizeBaseUrl(baseUrlRaw);
|
|
183
|
+
const token = options.token || 'example-token';
|
|
184
|
+
const domain = options.domain || 'example.test';
|
|
185
|
+
|
|
186
|
+
const client = new TemplateClient(baseUrl, token);
|
|
187
|
+
const mergedOptions: Options = { ...options, domain };
|
|
188
|
+
|
|
189
|
+
if (command === 'template') {
|
|
190
|
+
await runTemplate(client, mergedOptions);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (command === 'asset' || command === 'path') {
|
|
195
|
+
if (command === 'path' && !mergedOptions.path) {
|
|
196
|
+
throw new Error('path requires --path');
|
|
197
|
+
}
|
|
198
|
+
await runAsset(client, mergedOptions);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
main().catch((err) => {
|
|
204
|
+
console.error('Error:', err instanceof Error ? err.message : String(err));
|
|
205
|
+
process.exit(1);
|
|
206
|
+
});
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import TemplateClient from '../../client/src/mail-magic-client.ts';
|
|
2
|
+
|
|
3
|
+
const baseUrl = 'http://127.0.0.1:3776';
|
|
4
|
+
const token = 'example-token';
|
|
5
|
+
const domain = 'example.test';
|
|
6
|
+
|
|
7
|
+
type ApiEnvelope<T> = {
|
|
8
|
+
success: boolean;
|
|
9
|
+
code: number;
|
|
10
|
+
message?: string;
|
|
11
|
+
data: T;
|
|
12
|
+
errors?: Record<string, unknown>;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
type FormTemplateUpsertData = {
|
|
16
|
+
Status: string;
|
|
17
|
+
created: boolean;
|
|
18
|
+
form_key: string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
function extractApiMessage(payload: unknown): string | undefined {
|
|
22
|
+
if (!payload || typeof payload !== 'object') {
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
const message = (payload as { message?: unknown }).message;
|
|
26
|
+
if (typeof message === 'string' && message.trim()) {
|
|
27
|
+
return message.trim();
|
|
28
|
+
}
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function postJson<T>(
|
|
33
|
+
url: string,
|
|
34
|
+
body: Record<string, unknown>,
|
|
35
|
+
headers: Record<string, string> = {}
|
|
36
|
+
): Promise<T> {
|
|
37
|
+
const res = await fetch(url, {
|
|
38
|
+
method: 'POST',
|
|
39
|
+
headers: {
|
|
40
|
+
'content-type': 'application/json',
|
|
41
|
+
...headers
|
|
42
|
+
},
|
|
43
|
+
body: JSON.stringify(body)
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const json = (await res.json().catch(() => null)) as unknown;
|
|
47
|
+
if (!res.ok) {
|
|
48
|
+
const message = extractApiMessage(json) || res.statusText;
|
|
49
|
+
throw new Error(`Request failed: ${res.status} ${message}`);
|
|
50
|
+
}
|
|
51
|
+
return json as T;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function main(): Promise<void> {
|
|
55
|
+
const client = new TemplateClient(baseUrl, token);
|
|
56
|
+
|
|
57
|
+
// Store a form template (authenticated). Capture `form_key` for public submissions.
|
|
58
|
+
const stored = (await client.storeFormTemplate({
|
|
59
|
+
domain,
|
|
60
|
+
idname: 'journalist-contact',
|
|
61
|
+
sender: 'Example Forms <forms@example.test>',
|
|
62
|
+
recipient: 'default@example.test',
|
|
63
|
+
subject: 'Journalist Contact',
|
|
64
|
+
template: '<p>Hello {{ _fields_.msg }}</p>'
|
|
65
|
+
})) as ApiEnvelope<FormTemplateUpsertData>;
|
|
66
|
+
|
|
67
|
+
const form_key = String(stored?.data?.form_key ?? '');
|
|
68
|
+
if (!form_key) {
|
|
69
|
+
throw new Error('Expected data.form_key in the form template response');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Configure recipient allowlist (authenticated).
|
|
73
|
+
await postJson(
|
|
74
|
+
`${baseUrl}/api/v1/form/recipient`,
|
|
75
|
+
{
|
|
76
|
+
domain,
|
|
77
|
+
form_key,
|
|
78
|
+
idname: 'desk',
|
|
79
|
+
email: 'News Desk <desk@example.test>'
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
Authorization: `Bearer apikey-${token}`
|
|
83
|
+
}
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
// Submit the form publicly (no auth required).
|
|
87
|
+
await postJson(`${baseUrl}/api/v1/form/message`, {
|
|
88
|
+
_mm_form_key: form_key,
|
|
89
|
+
_mm_recipients: ['desk'],
|
|
90
|
+
msg: 'Hello from the public form example'
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
console.log('Created/updated form: journalist-contact');
|
|
94
|
+
console.log(`Public form_key: ${form_key}`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
main().catch((err: unknown) => {
|
|
98
|
+
console.error('Error:', err instanceof Error ? err.message : String(err));
|
|
99
|
+
process.exit(1);
|
|
100
|
+
});
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import TemplateClient from '../../client/src/mail-magic-client.ts';
|
|
2
|
+
|
|
3
|
+
type Mode = 'tx' | 'form' | 'both';
|
|
4
|
+
|
|
5
|
+
const baseUrl = 'http://127.0.0.1:3776';
|
|
6
|
+
const token = 'example-token';
|
|
7
|
+
const domain = 'example.test';
|
|
8
|
+
|
|
9
|
+
const txName = 'welcome';
|
|
10
|
+
const formId = 'contact';
|
|
11
|
+
const locale = 'en';
|
|
12
|
+
|
|
13
|
+
const rcpt = 'user@example.test';
|
|
14
|
+
const sender = 'Example <noreply@example.test>';
|
|
15
|
+
const formSender = 'Example Forms <forms@example.test>';
|
|
16
|
+
const formRecipient = 'owner@example.test';
|
|
17
|
+
const formSecret = 'form-secret';
|
|
18
|
+
|
|
19
|
+
type ApiEnvelope<T> = {
|
|
20
|
+
success: boolean;
|
|
21
|
+
code: number;
|
|
22
|
+
message?: string;
|
|
23
|
+
data: T;
|
|
24
|
+
errors?: Record<string, unknown>;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
type FormTemplateUpsertData = {
|
|
28
|
+
Status: string;
|
|
29
|
+
created: boolean;
|
|
30
|
+
form_key: string;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
async function postJson(url: string, body: Record<string, unknown>): Promise<void> {
|
|
34
|
+
const res = await fetch(url, {
|
|
35
|
+
method: 'POST',
|
|
36
|
+
headers: { 'content-type': 'application/json' },
|
|
37
|
+
body: JSON.stringify(body)
|
|
38
|
+
});
|
|
39
|
+
if (!res.ok) {
|
|
40
|
+
const msg = (await res.text().catch(() => res.statusText)) || res.statusText;
|
|
41
|
+
throw new Error(`Request failed: ${res.status} ${msg}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const modeArg = (process.argv[2] || 'both') as Mode;
|
|
46
|
+
const mode: Mode = modeArg === 'tx' || modeArg === 'form' ? modeArg : 'both';
|
|
47
|
+
|
|
48
|
+
async function sendTx(client: TemplateClient): Promise<void> {
|
|
49
|
+
await client.storeTxTemplate({
|
|
50
|
+
domain,
|
|
51
|
+
name: txName,
|
|
52
|
+
sender,
|
|
53
|
+
subject: 'Welcome from mail-magic',
|
|
54
|
+
locale,
|
|
55
|
+
template: '<p>Hello {{ name }}!</p>'
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
await client.sendTxMessage({
|
|
59
|
+
domain,
|
|
60
|
+
name: txName,
|
|
61
|
+
locale,
|
|
62
|
+
rcpt,
|
|
63
|
+
vars: { name: 'Example' }
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
console.log(`Sent tx template '${txName}' to ${rcpt}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function sendForm(client: TemplateClient): Promise<void> {
|
|
70
|
+
const stored = (await client.storeFormTemplate({
|
|
71
|
+
domain,
|
|
72
|
+
idname: formId,
|
|
73
|
+
sender: formSender,
|
|
74
|
+
recipient: formRecipient,
|
|
75
|
+
subject: 'Example form submission',
|
|
76
|
+
locale,
|
|
77
|
+
secret: formSecret,
|
|
78
|
+
template: '<p>Contact from {{ _fields_.name }} ({{ _fields_.email }})</p>'
|
|
79
|
+
})) as ApiEnvelope<FormTemplateUpsertData>;
|
|
80
|
+
|
|
81
|
+
const form_key = String(stored?.data?.form_key ?? '').trim();
|
|
82
|
+
if (!form_key) {
|
|
83
|
+
throw new Error('Expected data.form_key in the form template response');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Public form submission endpoint (no auth): requires `_mm_form_key`.
|
|
87
|
+
await postJson(`${baseUrl}/api/v1/form/message`, {
|
|
88
|
+
_mm_form_key: form_key,
|
|
89
|
+
name: 'Example User',
|
|
90
|
+
email: rcpt,
|
|
91
|
+
message: 'Hello from the example script.'
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
console.log(`Sent form message '${formId}' to ${formRecipient}`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function main(): Promise<void> {
|
|
98
|
+
const client = new TemplateClient(baseUrl, token);
|
|
99
|
+
if (mode === 'tx') {
|
|
100
|
+
await sendTx(client);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (mode === 'form') {
|
|
104
|
+
await sendForm(client);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
await sendTx(client);
|
|
108
|
+
await sendForm(client);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
main().catch((err: unknown) => {
|
|
112
|
+
console.error('Error:', err instanceof Error ? err.message : String(err));
|
|
113
|
+
process.exit(1);
|
|
114
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@technomoron/mail-magic",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.41",
|
|
4
4
|
"main": "dist/cjs/index.js",
|
|
5
5
|
"module": "dist/esm/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
},
|
|
10
10
|
"exports": {
|
|
11
11
|
".": {
|
|
12
|
+
"types": "./dist/cjs/index.d.ts",
|
|
12
13
|
"import": "./dist/esm/index.js",
|
|
13
14
|
"require": "./dist/cjs/index.js"
|
|
14
15
|
}
|
|
@@ -16,6 +17,7 @@
|
|
|
16
17
|
"files": [
|
|
17
18
|
"dist",
|
|
18
19
|
"docs",
|
|
20
|
+
"examples",
|
|
19
21
|
"README.md",
|
|
20
22
|
"CHANGES"
|
|
21
23
|
],
|
|
@@ -66,7 +68,7 @@
|
|
|
66
68
|
"scripts": {
|
|
67
69
|
"start": "node dist/esm/index.js",
|
|
68
70
|
"dev": "NODE_ENV=development nodemon --watch 'src/**/*.ts' --watch 'config/**/*.*' --watch '.env' --exec 'tsx' src/index.ts",
|
|
69
|
-
"run": "NODE_ENV=production
|
|
71
|
+
"run": "NODE_ENV=production run-s start",
|
|
70
72
|
"sync:shared": "node ../../scripts/sync-shared-code.cjs >/dev/null",
|
|
71
73
|
"build:esm": "tsc --project tsconfig/tsconfig.esm.json",
|
|
72
74
|
"build:cjs": "node scripts/add-shebang.cjs --cjs-only",
|
|
@@ -76,8 +78,8 @@
|
|
|
76
78
|
"test": "run-s --silent sync:shared test:unit",
|
|
77
79
|
"test:watch": "vitest",
|
|
78
80
|
"scrub": "rimraf ./node_modules/ ./dist/ pnpm-lock.yaml package-lock.json yarn.lock",
|
|
79
|
-
"lint": "node ../../node_modules/eslint/bin/eslint.js --config ../../eslint.config.mjs --no-error-on-unmatched-pattern
|
|
80
|
-
"lintfix": "node ../../node_modules/eslint/bin/eslint.js --config ../../eslint.config.mjs --fix --no-error-on-unmatched-pattern
|
|
81
|
+
"lint": "node ../../node_modules/eslint/bin/eslint.js --config ../../eslint.config.mjs --no-error-on-unmatched-pattern ./",
|
|
82
|
+
"lintfix": "node ../../node_modules/eslint/bin/eslint.js --config ../../eslint.config.mjs --fix --no-error-on-unmatched-pattern ./",
|
|
81
83
|
"pretty": "node ../../node_modules/prettier/bin/prettier.cjs --config ../../.prettierrc --write \"**/*.{js,jsx,cjs,mjs,ts,tsx,mts,vue,json,md}\"",
|
|
82
84
|
"format": "run-s lintfix pretty",
|
|
83
85
|
"cleanbuild": "run-s clean:dist format build",
|