@technomoron/mail-magic 1.0.8 → 1.0.11

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 (45) hide show
  1. package/CHANGES +27 -0
  2. package/README.md +9 -3
  3. package/TUTORIAL.MD +1 -0
  4. package/dist/api/assets.js +153 -0
  5. package/dist/api/forms.js +2 -0
  6. package/dist/api/mailer.js +1 -0
  7. package/dist/bin/mail-magic.js +63 -0
  8. package/dist/index.js +61 -2
  9. package/dist/store/envloader.js +3 -3
  10. package/dist/store/store.js +66 -1
  11. package/package.json +17 -3
  12. package/.do-realease.sh +0 -49
  13. package/.editorconfig +0 -9
  14. package/.env-dist +0 -71
  15. package/.prettierrc +0 -14
  16. package/.vscode/extensions.json +0 -3
  17. package/.vscode/settings.json +0 -22
  18. package/config-example/form-template/default.njk +0 -102
  19. package/config-example/forms.config.json +0 -8
  20. package/config-example/init-data.json +0 -33
  21. package/config-example/tx-template/default.njk +0 -107
  22. package/ecosystem.config.cjs +0 -42
  23. package/eslint.config.mjs +0 -196
  24. package/lintconfig.cjs +0 -81
  25. package/src/api/assets.ts +0 -92
  26. package/src/api/forms.ts +0 -237
  27. package/src/api/mailer.ts +0 -269
  28. package/src/index.ts +0 -71
  29. package/src/models/db.ts +0 -112
  30. package/src/models/domain.ts +0 -72
  31. package/src/models/form.ts +0 -209
  32. package/src/models/init.ts +0 -240
  33. package/src/models/txmail.ts +0 -206
  34. package/src/models/user.ts +0 -79
  35. package/src/server.ts +0 -27
  36. package/src/store/envloader.ts +0 -109
  37. package/src/store/store.ts +0 -118
  38. package/src/types.ts +0 -39
  39. package/src/util.ts +0 -137
  40. package/tests/fixtures/certs/test.crt +0 -19
  41. package/tests/fixtures/certs/test.key +0 -28
  42. package/tests/helpers/test-setup.ts +0 -316
  43. package/tests/mail-magic.test.ts +0 -154
  44. package/tsconfig.json +0 -14
  45. package/vitest.config.ts +0 -11
package/src/api/assets.ts DELETED
@@ -1,92 +0,0 @@
1
- import fs from 'fs';
2
- import path from 'path';
3
-
4
- import { ApiError, ApiModule, ApiRoute } from '@technomoron/api-server-base';
5
-
6
- import { mailApiServer } from '../server.js';
7
- import { decodeComponent, sendFileAsync } from '../util.js';
8
-
9
- import type { ApiRequest } from '@technomoron/api-server-base';
10
-
11
- const DOMAIN_PATTERN = /^[a-z0-9][a-z0-9._-]*$/i;
12
- const SEGMENT_PATTERN = /^[a-zA-Z0-9._-]+$/;
13
-
14
- export class AssetAPI extends ApiModule<mailApiServer> {
15
- private async getAsset(apiReq: ApiRequest): Promise<[number, null]> {
16
- const domain = decodeComponent(apiReq.req.params.domain);
17
- if (!domain || !DOMAIN_PATTERN.test(domain)) {
18
- throw new ApiError({ code: 404, message: 'Asset not found' });
19
- }
20
-
21
- const rawPath = apiReq.req.params[0] ?? '';
22
- const segments = rawPath
23
- .split('/')
24
- .filter(Boolean)
25
- .map((segment: string) => decodeComponent(segment));
26
- if (!segments.length || segments.some((segment) => !SEGMENT_PATTERN.test(segment))) {
27
- throw new ApiError({ code: 404, message: 'Asset not found' });
28
- }
29
-
30
- const assetsRoot = path.join(this.server.storage.configpath, domain, 'assets');
31
- if (!fs.existsSync(assetsRoot)) {
32
- throw new ApiError({ code: 404, message: 'Asset not found' });
33
- }
34
- const resolvedRoot = fs.realpathSync(assetsRoot);
35
- const normalizedRoot = resolvedRoot.endsWith(path.sep) ? resolvedRoot : resolvedRoot + path.sep;
36
- const candidate = path.resolve(assetsRoot, path.join(...segments));
37
-
38
- try {
39
- const stats = await fs.promises.stat(candidate);
40
- if (!stats.isFile()) {
41
- throw new ApiError({ code: 404, message: 'Asset not found' });
42
- }
43
- } catch {
44
- throw new ApiError({ code: 404, message: 'Asset not found' });
45
- }
46
-
47
- let realCandidate: string;
48
- try {
49
- realCandidate = await fs.promises.realpath(candidate);
50
- } catch {
51
- throw new ApiError({ code: 404, message: 'Asset not found' });
52
- }
53
- if (!realCandidate.startsWith(normalizedRoot)) {
54
- throw new ApiError({ code: 404, message: 'Asset not found' });
55
- }
56
-
57
- const { res } = apiReq;
58
- const originalStatus = res.status.bind(res);
59
- const originalJson = res.json.bind(res);
60
- res.status = ((code: number) => (res.headersSent ? res : originalStatus(code))) as typeof res.status;
61
- res.json = ((body: unknown) => (res.headersSent ? res : originalJson(body))) as typeof res.json;
62
-
63
- res.type(path.extname(realCandidate));
64
- res.set('Cache-Control', 'public, max-age=300');
65
-
66
- try {
67
- await sendFileAsync(res, realCandidate);
68
- } catch (err) {
69
- this.server.storage.print_debug(
70
- `Failed to serve asset ${domain}/${segments.join('/')}: ${err instanceof Error ? err.message : String(err)}`
71
- );
72
- if (!res.headersSent) {
73
- throw new ApiError({ code: 500, message: 'Failed to stream asset' });
74
- }
75
- }
76
-
77
- return [200, null];
78
- }
79
-
80
- override defineRoutes(): ApiRoute[] {
81
- const route = this.server.storage.env.ASSET_ROUTE;
82
- const normalizedRoute = route.startsWith('/') ? route : `/${route}`;
83
- return [
84
- {
85
- method: 'get',
86
- path: `${normalizedRoute}/:domain/*`,
87
- handler: (apiReq) => this.getAsset(apiReq),
88
- auth: { type: 'none', req: 'any' }
89
- }
90
- ];
91
- }
92
- }
package/src/api/forms.ts DELETED
@@ -1,237 +0,0 @@
1
- import path from 'path';
2
-
3
- import { ApiRoute, ApiRequest, ApiModule, ApiError } from '@technomoron/api-server-base';
4
- import emailAddresses, { ParsedMailbox } from 'email-addresses';
5
- import nunjucks from 'nunjucks';
6
-
7
- import { api_domain } from '../models/domain.js';
8
- import { api_form } from '../models/form.js';
9
- import { api_user } from '../models/user.js';
10
- import { mailApiServer } from '../server.js';
11
- import { buildRequestMeta, normalizeSlug } from '../util.js';
12
-
13
- import type { mailApiRequest, UploadedFile } from '../types.js';
14
-
15
- export class FormAPI extends ApiModule<mailApiServer> {
16
- private validateEmail(email: string): string | undefined {
17
- const parsed = emailAddresses.parseOneAddress(email);
18
- if (parsed) {
19
- return (parsed as ParsedMailbox).address;
20
- }
21
- return undefined;
22
- }
23
-
24
- private async assertDomainAndUser(apireq: mailApiRequest): Promise<void> {
25
- const { domain, locale } = apireq.req.body;
26
-
27
- if (!domain) {
28
- throw new ApiError({ code: 401, message: 'Missing domain' });
29
- }
30
- const user = await api_user.findOne({ where: { token: apireq.token } });
31
- if (!user) {
32
- throw new ApiError({ code: 401, message: `Invalid/Unknown API Key/Token '${apireq.token}'` });
33
- }
34
- const dbdomain = await api_domain.findOne({ where: { name: domain } });
35
- if (!dbdomain) {
36
- throw new ApiError({ code: 401, message: `Unable to look up the domain ${domain}` });
37
- }
38
- if (dbdomain.user_id !== user.user_id) {
39
- throw new ApiError({ code: 403, message: `Domain ${domain} is not owned by this user` });
40
- }
41
- apireq.domain = dbdomain;
42
- apireq.locale = locale || 'en';
43
- apireq.user = user;
44
- }
45
-
46
- private async postFormTemplate(apireq: mailApiRequest): Promise<[number, { Status: string }]> {
47
- await this.assertDomainAndUser(apireq);
48
-
49
- const {
50
- template,
51
- sender = '',
52
- recipient = '',
53
- idname,
54
- subject = '',
55
- locale = '',
56
- secret = ''
57
- } = apireq.req.body;
58
-
59
- if (!template) {
60
- throw new ApiError({ code: 400, message: 'Missing template data' });
61
- }
62
- if (!idname) {
63
- throw new ApiError({ code: 400, message: 'Missing form identifier' });
64
- }
65
- if (!sender) {
66
- throw new ApiError({ code: 400, message: 'Missing sender address' });
67
- }
68
- if (!recipient) {
69
- throw new ApiError({ code: 400, message: 'Missing recipient address' });
70
- }
71
-
72
- const user = apireq.user!;
73
- const domain = apireq.domain!;
74
- const resolvedLocale = locale || apireq.locale || '';
75
- const userSlug = normalizeSlug(user.idname);
76
- const domainSlug = normalizeSlug(domain.name);
77
- const formSlug = normalizeSlug(idname);
78
- const localeSlug = normalizeSlug(resolvedLocale || domain.locale || user.locale || '');
79
- const slug = `${userSlug}-${domainSlug}${localeSlug ? '-' + localeSlug : ''}-${formSlug}`;
80
- const filenameParts = [domainSlug, 'form-template'];
81
- if (localeSlug) {
82
- filenameParts.push(localeSlug);
83
- }
84
- filenameParts.push(formSlug);
85
- let filename = path.join(...filenameParts);
86
- if (!filename.endsWith('.njk')) {
87
- filename += '.njk';
88
- }
89
-
90
- const record = {
91
- user_id: user.user_id,
92
- domain_id: domain.domain_id,
93
- locale: localeSlug,
94
- idname,
95
- sender,
96
- recipient,
97
- subject,
98
- template,
99
- slug,
100
- filename,
101
- secret,
102
- files: []
103
- };
104
-
105
- try {
106
- const [form, created] = await api_form.upsert(record, {
107
- returning: true,
108
- conflictFields: ['user_id', 'domain_id', 'locale', 'idname']
109
- });
110
- this.server.storage.print_debug(`Form template upserted: ${form.idname} (created=${created})`);
111
- } catch (error: unknown) {
112
- throw new ApiError({
113
- code: 500,
114
- message: this.server!.guessExceptionText(error, 'Unknown Sequelize Error on upsert form template')
115
- });
116
- }
117
-
118
- return [200, { Status: 'OK' }];
119
- }
120
-
121
- private async postSendForm(apireq: ApiRequest): Promise<[number, Record<string, unknown>]> {
122
- const { formid, secret, recipient, vars = {}, replyTo, reply_to } = apireq.req.body;
123
-
124
- if (!formid) {
125
- throw new ApiError({ code: 404, message: 'Missing formid field in form' });
126
- }
127
-
128
- const form = await api_form.findOne({ where: { idname: formid } });
129
- if (!form) {
130
- throw new ApiError({ code: 404, message: `No such form: ${formid}` });
131
- }
132
-
133
- if (form.secret && !secret) {
134
- throw new ApiError({ code: 401, message: 'This form requires a secret key' });
135
- }
136
- if (form.secret && form.secret !== secret) {
137
- throw new ApiError({ code: 401, message: 'Bad form secret' });
138
- }
139
- if (recipient && !form.secret) {
140
- throw new ApiError({ code: 401, message: "'recipient' parameterer requires form secret to be set" });
141
- }
142
- let normalizedReplyTo: string | undefined;
143
- let normalizedRecipient: string | undefined;
144
- const replyToValue = (replyTo || reply_to) as string | undefined;
145
- if (replyToValue) {
146
- normalizedReplyTo = this.validateEmail(replyToValue);
147
- if (!normalizedReplyTo) {
148
- throw new ApiError({ code: 400, message: 'Invalid reply-to email address' });
149
- }
150
- }
151
- if (recipient) {
152
- normalizedRecipient = this.validateEmail(String(recipient));
153
- if (!normalizedRecipient) {
154
- throw new ApiError({ code: 400, message: 'Invalid recipient email address' });
155
- }
156
- }
157
-
158
- let parsedVars: unknown = vars ?? {};
159
- if (typeof vars === 'string') {
160
- try {
161
- parsedVars = JSON.parse(vars);
162
- } catch {
163
- throw new ApiError({ code: 400, message: 'Invalid JSON provided in "vars"' });
164
- }
165
- }
166
- const thevars = parsedVars as Record<string, unknown>;
167
-
168
- /*
169
- console.log('Headers:', apireq.req.headers);
170
- console.log('Body:', JSON.stringify(apireq.req.body, null, 2));
171
- console.log('Files:', JSON.stringify(apireq.req.files, null, 2));
172
- */
173
-
174
- const rawFiles = Array.isArray(apireq.req.files) ? (apireq.req.files as UploadedFile[]) : [];
175
- const attachments = rawFiles.map((file) => ({
176
- filename: file.originalname,
177
- path: file.path
178
- }));
179
-
180
- const attachmentMap: Record<string, string> = {};
181
- for (const file of rawFiles) {
182
- attachmentMap[file.fieldname] = file.originalname;
183
- }
184
-
185
- const meta = buildRequestMeta(apireq.req);
186
-
187
- const context = {
188
- ...thevars,
189
- _rcpt_email_: recipient,
190
- _attachments_: attachmentMap,
191
- _vars_: thevars,
192
- _fields_: apireq.req.body,
193
- _files_: rawFiles,
194
- _meta_: meta
195
- };
196
-
197
- nunjucks.configure({ autoescape: true });
198
- const html = nunjucks.renderString(form.template, context);
199
-
200
- const mailOptions = {
201
- from: form.sender,
202
- to: normalizedRecipient || form.recipient,
203
- subject: form.subject,
204
- html,
205
- attachments,
206
- ...(normalizedReplyTo ? { replyTo: normalizedReplyTo } : {})
207
- };
208
-
209
- try {
210
- const info = await this.server.storage.transport!.sendMail(mailOptions);
211
- this.server.storage.print_debug('Email sent: ' + info.response);
212
- } catch (error: unknown) {
213
- const errorMessage = error instanceof Error ? error.message : String(error);
214
- this.server.storage.print_debug('Error sending email: ' + errorMessage);
215
- return [500, { error: `Error sending email: ${errorMessage}` }];
216
- }
217
-
218
- return [200, {}];
219
- }
220
-
221
- override defineRoutes(): ApiRoute[] {
222
- return [
223
- {
224
- method: 'post',
225
- path: '/v1/form/template',
226
- handler: (req) => this.postFormTemplate(req as mailApiRequest),
227
- auth: { type: 'yes', req: 'any' }
228
- },
229
- {
230
- method: 'post',
231
- path: '/v1/form/message',
232
- handler: (req) => this.postSendForm(req),
233
- auth: { type: 'none', req: 'any' }
234
- }
235
- ];
236
- }
237
- }
package/src/api/mailer.ts DELETED
@@ -1,269 +0,0 @@
1
- import { ApiModule, ApiRoute, ApiError } from '@technomoron/api-server-base';
2
- import emailAddresses, { ParsedMailbox } from 'email-addresses';
3
- import { convert } from 'html-to-text';
4
- import nunjucks from 'nunjucks';
5
-
6
- import { api_domain } from '../models/domain.js';
7
- import { api_txmail } from '../models/txmail.js';
8
- import { api_user } from '../models/user.js';
9
- import { mailApiServer } from '../server.js';
10
- import { buildRequestMeta } from '../util.js';
11
-
12
- import type { mailApiRequest, UploadedFile } from '../types.js';
13
-
14
- export class MailerAPI extends ApiModule<mailApiServer> {
15
- //
16
- // Validate and return the parsed email address
17
- //
18
- validateEmail(email: string): string | undefined {
19
- const parsed = emailAddresses.parseOneAddress(email);
20
- if (parsed) {
21
- return (parsed as ParsedMailbox).address;
22
- }
23
- return undefined;
24
- }
25
-
26
- //
27
- // Validate a set of email addresses. Return arrays of invalid
28
- // and valid email addresses.
29
- //
30
-
31
- validateEmails(list: string): { valid: string[]; invalid: string[] } {
32
- const valid = [] as string[],
33
- invalid = [] as string[];
34
-
35
- const emails = list
36
- .split(',')
37
- .map((email) => email.trim())
38
- .filter((email) => email !== '');
39
- emails.forEach((email) => {
40
- const addr = this.validateEmail(email);
41
- if (addr) {
42
- valid.push(addr);
43
- } else {
44
- invalid.push(email);
45
- }
46
- });
47
- return { valid, invalid };
48
- }
49
-
50
- async assert_domain_and_user(apireq: mailApiRequest) {
51
- const { domain, locale } = apireq.req.body;
52
-
53
- if (!domain) {
54
- throw new ApiError({ code: 401, message: 'Missing domain' });
55
- }
56
- const user = await api_user.findOne({ where: { token: apireq.token } });
57
- if (!user) {
58
- throw new ApiError({ code: 401, message: `Invalid/Unknown API Key/Token '${apireq.token}'` });
59
- }
60
- const dbdomain = await api_domain.findOne({ where: { name: domain } });
61
- if (!dbdomain) {
62
- throw new ApiError({ code: 401, message: `Unable to look up the domain ${domain}` });
63
- }
64
- if (dbdomain.user_id !== user.user_id) {
65
- throw new ApiError({ code: 403, message: `Domain ${domain} is not owned by this user` });
66
- }
67
- apireq.domain = dbdomain;
68
- apireq.locale = locale || 'en';
69
- apireq.user = user;
70
- }
71
-
72
- // Store a template in the database
73
-
74
- private async post_template(apireq: mailApiRequest): Promise<[number, { Status: string }]> {
75
- await this.assert_domain_and_user(apireq);
76
-
77
- const { template, sender = '', name, subject = '', locale = '' } = apireq.req.body;
78
-
79
- if (!template) {
80
- throw new ApiError({ code: 400, message: 'Missing template data' });
81
- }
82
- if (!name) {
83
- throw new ApiError({ code: 400, message: 'Missing template name' });
84
- }
85
-
86
- const data = {
87
- user_id: apireq.user!.user_id,
88
- domain_id: apireq.domain!.domain_id,
89
- name,
90
- subject,
91
- locale,
92
- sender,
93
- template
94
- };
95
-
96
- /*
97
- console.log(JSON.stringify({
98
- user: apireq.user,
99
- domain: apireq.domain,
100
- domain_id: apireq.domain.domain_id,
101
- data
102
- }, undefined, 2)); */
103
-
104
- try {
105
- const [templateRecord, created] = await api_txmail.upsert(data, {
106
- returning: true
107
- });
108
- this.server.storage.print_debug(`Template upserted: ${templateRecord.name} (created=${created})`);
109
- } catch (error: unknown) {
110
- throw new ApiError({
111
- code: 500,
112
- message: this.server!.guessExceptionText(error, 'Unknown Sequelize Error on upsert template')
113
- });
114
- }
115
- return [200, { Status: 'OK' }];
116
- }
117
-
118
- // Send a template using posted arguments.
119
-
120
- private async post_send(apireq: mailApiRequest): Promise<[number, Record<string, unknown>]> {
121
- await this.assert_domain_and_user(apireq);
122
-
123
- const { name, rcpt, domain = '', locale = '', vars = {}, replyTo, reply_to, headers } = apireq.req.body;
124
-
125
- if (!name || !rcpt || !domain) {
126
- throw new ApiError({ code: 400, message: 'name/rcpt/domain required' });
127
- }
128
-
129
- let parsedVars: unknown = vars ?? {};
130
- if (typeof vars === 'string') {
131
- try {
132
- parsedVars = JSON.parse(vars);
133
- } catch {
134
- throw new ApiError({ code: 400, message: 'Invalid JSON provided in "vars"' });
135
- }
136
- }
137
- const thevars = parsedVars as Record<string, unknown>;
138
-
139
- // const dbdomain = await api_domain.findOne({ where: { domain } });
140
-
141
- const { valid, invalid } = this.validateEmails(rcpt);
142
- if (invalid.length > 0) {
143
- throw new ApiError({ code: 400, message: 'Invalid email address(es): ' + invalid.join(',') });
144
- }
145
- let template: api_txmail | null = null;
146
- const deflocale = this.server.storage.deflocale || '';
147
- const domain_id = apireq.domain!.domain_id;
148
-
149
- try {
150
- template =
151
- (await api_txmail.findOne({ where: { name, domain_id, locale } })) ||
152
- (await api_txmail.findOne({ where: { name, domain_id, locale: deflocale } })) ||
153
- (await api_txmail.findOne({ where: { name, domain_id } }));
154
- } catch (error: unknown) {
155
- throw new ApiError({
156
- code: 500,
157
- message: this.server!.guessExceptionText(error, 'Unknown Sequelize Error')
158
- });
159
- }
160
- if (!template) {
161
- throw new ApiError({
162
- code: 404,
163
- message: `Template "${name}" not found for any locale in domain "${domain}"`
164
- });
165
- }
166
-
167
- const sender = template.sender || apireq.domain!.sender || apireq.user!.email;
168
- if (!sender) {
169
- throw new ApiError({ code: 500, message: `Unable to locate sender for ${template.name}` });
170
- }
171
-
172
- const rawFiles = Array.isArray(apireq.req.files) ? (apireq.req.files as UploadedFile[]) : [];
173
- const templateAssets = Array.isArray(template.files) ? template.files : [];
174
- const attachments = [
175
- ...templateAssets.map((file) => ({
176
- filename: file.filename,
177
- path: file.path,
178
- cid: file.cid
179
- })),
180
- ...rawFiles.map((file) => ({
181
- filename: file.originalname,
182
- path: file.path
183
- }))
184
- ];
185
-
186
- const attachmentMap: Record<string, string> = {};
187
- for (const file of rawFiles) {
188
- attachmentMap[file.fieldname] = file.originalname;
189
- }
190
- this.server.storage.print_debug(`Template vars: ${JSON.stringify({ vars, thevars }, undefined, 2)}`);
191
-
192
- const meta = buildRequestMeta(apireq.req);
193
- const replyToValue = (replyTo || reply_to) as string | undefined;
194
- let normalizedReplyTo: string | undefined;
195
- if (replyToValue) {
196
- normalizedReplyTo = this.validateEmail(replyToValue);
197
- if (!normalizedReplyTo) {
198
- throw new ApiError({ code: 400, message: 'Invalid reply-to email address' });
199
- }
200
- }
201
-
202
- let normalizedHeaders: Record<string, string> | undefined;
203
- if (headers !== undefined) {
204
- if (!headers || typeof headers !== 'object' || Array.isArray(headers)) {
205
- throw new ApiError({ code: 400, message: 'headers must be a key/value object' });
206
- }
207
- normalizedHeaders = {};
208
- for (const [key, value] of Object.entries(headers as Record<string, unknown>)) {
209
- if (typeof value !== 'string') {
210
- throw new ApiError({ code: 400, message: `headers.${key} must be a string` });
211
- }
212
- normalizedHeaders[key] = value;
213
- }
214
- }
215
-
216
- try {
217
- const env = new nunjucks.Environment(null, { autoescape: false });
218
-
219
- const compiled = nunjucks.compile(template.template, env);
220
-
221
- for (const recipient of valid) {
222
- const fullargs = {
223
- ...thevars,
224
- _rcpt_email_: recipient,
225
- _attachments_: attachmentMap,
226
- _vars_: thevars,
227
- _meta_: meta
228
- };
229
- const html = await compiled.render(fullargs);
230
- const text = convert(html);
231
- const sendargs = {
232
- from: sender,
233
- to: recipient,
234
- subject: template.subject || apireq.req.body.subject || '',
235
- html,
236
- text,
237
- attachments,
238
- ...(normalizedReplyTo ? { replyTo: normalizedReplyTo } : {}),
239
- ...(normalizedHeaders ? { headers: normalizedHeaders } : {})
240
- };
241
- await this.server.storage.transport!.sendMail(sendargs);
242
- }
243
- return [200, { Status: 'OK', Message: 'Emails sent successfully' }];
244
- } catch (error: unknown) {
245
- // console.log(JSON.stringify(e, null, 2));
246
- throw new ApiError({
247
- code: 500,
248
- message: error instanceof Error ? error.message : String(error)
249
- });
250
- }
251
- }
252
-
253
- override defineRoutes(): ApiRoute[] {
254
- return [
255
- {
256
- method: 'post',
257
- path: '/v1/tx/message',
258
- handler: this.post_send.bind(this),
259
- auth: { type: 'yes', req: 'any' }
260
- },
261
- {
262
- method: 'post',
263
- path: '/v1/tx/template',
264
- handler: this.post_template.bind(this),
265
- auth: { type: 'yes', req: 'any' }
266
- }
267
- ];
268
- }
269
- }
package/src/index.ts DELETED
@@ -1,71 +0,0 @@
1
- import { pathToFileURL } from 'node:url';
2
-
3
- import { AssetAPI } from './api/assets.js';
4
- import { FormAPI } from './api/forms.js';
5
- import { MailerAPI } from './api/mailer.js';
6
- import { mailApiServer } from './server.js';
7
- import { mailStore } from './store/store.js';
8
-
9
- import type { ApiServerConf } from '@technomoron/api-server-base';
10
-
11
- export type MailMagicServerOptions = Partial<ApiServerConf>;
12
-
13
- export type MailMagicServerBootstrap = {
14
- server: mailApiServer;
15
- store: mailStore;
16
- env: mailStore['env'];
17
- };
18
-
19
- function buildServerConfig(store: mailStore, overrides: MailMagicServerOptions): MailMagicServerOptions {
20
- const env = store.env;
21
- return {
22
- apiHost: env.API_HOST,
23
- apiPort: env.API_PORT,
24
- uploadPath: env.UPLOAD_PATH,
25
- debug: env.DEBUG,
26
- apiBasePath: '',
27
- swaggerEnabled: env.SWAGGER_ENABLED,
28
- swaggerPath: env.SWAGGER_PATH,
29
- ...overrides
30
- };
31
- }
32
-
33
- export async function createMailMagicServer(overrides: MailMagicServerOptions = {}): Promise<MailMagicServerBootstrap> {
34
- const store = await new mailStore().init();
35
- const config = buildServerConfig(store, overrides);
36
- const server = new mailApiServer(config, store).api(new MailerAPI()).api(new FormAPI()).api(new AssetAPI());
37
-
38
- return { server, store, env: store.env };
39
- }
40
-
41
- export async function startMailMagicServer(overrides: MailMagicServerOptions = {}): Promise<MailMagicServerBootstrap> {
42
- const bootstrap = await createMailMagicServer(overrides);
43
- await bootstrap.server.start();
44
- return bootstrap;
45
- }
46
-
47
- async function bootMailMagic() {
48
- try {
49
- const { env } = await startMailMagicServer();
50
- console.log(`mail-magic server listening on ${env.API_HOST}:${env.API_PORT}`);
51
- } catch (err) {
52
- console.error('Failed to start FormMailer:', err);
53
- process.exit(1);
54
- }
55
- }
56
-
57
- const isDirectExecution = (() => {
58
- if (!process.argv[1]) {
59
- return false;
60
- }
61
-
62
- try {
63
- return import.meta.url === pathToFileURL(process.argv[1]).href;
64
- } catch {
65
- return false;
66
- }
67
- })();
68
-
69
- if (isDirectExecution) {
70
- void bootMailMagic();
71
- }