@technomoron/mail-magic 1.0.4
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/.do-realease.sh +10 -0
- package/.editorconfig +9 -0
- package/.env-dist +62 -0
- package/.prettierrc +14 -0
- package/.vscode/extensions.json +15 -0
- package/.vscode/settings.json +123 -0
- package/CHANGES +25 -0
- package/README.md +63 -0
- package/config-example/form-template/default.njk +102 -0
- package/config-example/forms.config.json +8 -0
- package/config-example/init-data.json +33 -0
- package/config-example/tx-template/default.njk +107 -0
- package/dist/api/forms.js +175 -0
- package/dist/api/mailer.js +213 -0
- package/dist/index.js +50 -0
- package/dist/models/db.js +99 -0
- package/dist/models/domain.js +58 -0
- package/dist/models/form.js +168 -0
- package/dist/models/init.js +176 -0
- package/dist/models/txmail.js +167 -0
- package/dist/models/user.js +65 -0
- package/dist/server.js +22 -0
- package/dist/store/envloader.js +116 -0
- package/dist/store/store.js +85 -0
- package/dist/types.js +1 -0
- package/dist/util.js +94 -0
- package/ecosystem.config.cjs +42 -0
- package/eslint.config.mjs +104 -0
- package/package.json +67 -0
- package/src/api/forms.ts +209 -0
- package/src/api/mailer.ts +242 -0
- package/src/index.ts +67 -0
- package/src/models/db.ts +112 -0
- package/src/models/domain.ts +72 -0
- package/src/models/form.ts +198 -0
- package/src/models/init.ts +237 -0
- package/src/models/txmail.ts +199 -0
- package/src/models/user.ts +79 -0
- package/src/server.ts +27 -0
- package/src/store/envloader.ts +117 -0
- package/src/store/store.ts +116 -0
- package/src/types.ts +39 -0
- package/src/util.ts +111 -0
- package/test1.sh +13 -0
- package/tsconfig.json +14 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { ApiModule, ApiError } from '@technomoron/api-server-base';
|
|
3
|
+
import nunjucks from 'nunjucks';
|
|
4
|
+
import { api_domain } from '../models/domain.js';
|
|
5
|
+
import { api_form } from '../models/form.js';
|
|
6
|
+
import { api_user } from '../models/user.js';
|
|
7
|
+
import { buildRequestMeta, normalizeSlug } from '../util.js';
|
|
8
|
+
export class FormAPI extends ApiModule {
|
|
9
|
+
async assertDomainAndUser(apireq) {
|
|
10
|
+
const { domain, locale } = apireq.req.body;
|
|
11
|
+
if (!domain) {
|
|
12
|
+
throw new ApiError({ code: 401, message: 'Missing domain' });
|
|
13
|
+
}
|
|
14
|
+
const user = await api_user.findOne({ where: { token: apireq.token } });
|
|
15
|
+
if (!user) {
|
|
16
|
+
throw new ApiError({ code: 401, message: `Invalid/Unknown API Key/Token '${apireq.token}'` });
|
|
17
|
+
}
|
|
18
|
+
const dbdomain = await api_domain.findOne({ where: { name: domain } });
|
|
19
|
+
if (!dbdomain) {
|
|
20
|
+
throw new ApiError({ code: 401, message: `Unable to look up the domain ${domain}` });
|
|
21
|
+
}
|
|
22
|
+
apireq.domain = dbdomain;
|
|
23
|
+
apireq.locale = locale || 'en';
|
|
24
|
+
apireq.user = user;
|
|
25
|
+
}
|
|
26
|
+
async postFormTemplate(apireq) {
|
|
27
|
+
await this.assertDomainAndUser(apireq);
|
|
28
|
+
const { template, sender = '', recipient = '', idname, subject = '', locale = '', secret = '' } = apireq.req.body;
|
|
29
|
+
if (!template) {
|
|
30
|
+
throw new ApiError({ code: 400, message: 'Missing template data' });
|
|
31
|
+
}
|
|
32
|
+
if (!idname) {
|
|
33
|
+
throw new ApiError({ code: 400, message: 'Missing form identifier' });
|
|
34
|
+
}
|
|
35
|
+
if (!sender) {
|
|
36
|
+
throw new ApiError({ code: 400, message: 'Missing sender address' });
|
|
37
|
+
}
|
|
38
|
+
if (!recipient) {
|
|
39
|
+
throw new ApiError({ code: 400, message: 'Missing recipient address' });
|
|
40
|
+
}
|
|
41
|
+
const user = apireq.user;
|
|
42
|
+
const domain = apireq.domain;
|
|
43
|
+
const resolvedLocale = locale || apireq.locale || '';
|
|
44
|
+
const userSlug = normalizeSlug(user.idname);
|
|
45
|
+
const domainSlug = normalizeSlug(domain.name);
|
|
46
|
+
const formSlug = normalizeSlug(idname);
|
|
47
|
+
const localeSlug = normalizeSlug(resolvedLocale || domain.locale || user.locale || '');
|
|
48
|
+
const slug = `${userSlug}-${domainSlug}${localeSlug ? '-' + localeSlug : ''}-${formSlug}`;
|
|
49
|
+
const filenameParts = [userSlug, domainSlug, 'form-template'];
|
|
50
|
+
if (localeSlug) {
|
|
51
|
+
filenameParts.push(localeSlug);
|
|
52
|
+
}
|
|
53
|
+
filenameParts.push(formSlug);
|
|
54
|
+
let filename = path.join(...filenameParts);
|
|
55
|
+
if (!filename.endsWith('.njk')) {
|
|
56
|
+
filename += '.njk';
|
|
57
|
+
}
|
|
58
|
+
const record = {
|
|
59
|
+
user_id: user.user_id,
|
|
60
|
+
domain_id: domain.domain_id,
|
|
61
|
+
locale: localeSlug,
|
|
62
|
+
idname,
|
|
63
|
+
sender,
|
|
64
|
+
recipient,
|
|
65
|
+
subject,
|
|
66
|
+
template,
|
|
67
|
+
slug,
|
|
68
|
+
filename,
|
|
69
|
+
secret,
|
|
70
|
+
files: []
|
|
71
|
+
};
|
|
72
|
+
try {
|
|
73
|
+
const [form, created] = await api_form.upsert(record, {
|
|
74
|
+
returning: true,
|
|
75
|
+
conflictFields: ['user_id', 'domain_id', 'locale', 'idname']
|
|
76
|
+
});
|
|
77
|
+
this.server.storage.print_debug(`Form template upserted: ${form.idname} (created=${created})`);
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
throw new ApiError({
|
|
81
|
+
code: 500,
|
|
82
|
+
message: this.server.guessExceptionText(error, 'Unknown Sequelize Error on upsert form template')
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
return [200, { Status: 'OK' }];
|
|
86
|
+
}
|
|
87
|
+
async postSendForm(apireq) {
|
|
88
|
+
const { formid, secret, recipient, vars = {} } = apireq.req.body;
|
|
89
|
+
if (!formid) {
|
|
90
|
+
throw new ApiError({ code: 404, message: 'Missing formid field in form' });
|
|
91
|
+
}
|
|
92
|
+
const form = await api_form.findOne({ where: { idname: formid } });
|
|
93
|
+
if (!form) {
|
|
94
|
+
throw new ApiError({ code: 404, message: `No such form: ${formid}` });
|
|
95
|
+
}
|
|
96
|
+
if (form.secret && !secret) {
|
|
97
|
+
throw new ApiError({ code: 401, message: 'This form requires a secret key' });
|
|
98
|
+
}
|
|
99
|
+
if (form.secret && form.secret !== secret) {
|
|
100
|
+
throw new ApiError({ code: 401, message: 'Bad form secret' });
|
|
101
|
+
}
|
|
102
|
+
if (recipient && !form.secret) {
|
|
103
|
+
throw new ApiError({ code: 401, message: "'recipient' parameterer requires form secret to be set" });
|
|
104
|
+
}
|
|
105
|
+
let parsedVars = vars ?? {};
|
|
106
|
+
if (typeof vars === 'string') {
|
|
107
|
+
try {
|
|
108
|
+
parsedVars = JSON.parse(vars);
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
throw new ApiError({ code: 400, message: 'Invalid JSON provided in "vars"' });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
const thevars = parsedVars;
|
|
115
|
+
/*
|
|
116
|
+
console.log('Headers:', apireq.req.headers);
|
|
117
|
+
console.log('Body:', JSON.stringify(apireq.req.body, null, 2));
|
|
118
|
+
console.log('Files:', JSON.stringify(apireq.req.files, null, 2));
|
|
119
|
+
*/
|
|
120
|
+
const rawFiles = Array.isArray(apireq.req.files) ? apireq.req.files : [];
|
|
121
|
+
const attachments = rawFiles.map((file) => ({
|
|
122
|
+
filename: file.originalname,
|
|
123
|
+
path: file.path
|
|
124
|
+
}));
|
|
125
|
+
const attachmentMap = {};
|
|
126
|
+
for (const file of rawFiles) {
|
|
127
|
+
attachmentMap[file.fieldname] = file.originalname;
|
|
128
|
+
}
|
|
129
|
+
const meta = buildRequestMeta(apireq.req);
|
|
130
|
+
const context = {
|
|
131
|
+
...thevars,
|
|
132
|
+
_rcpt_email_: recipient,
|
|
133
|
+
_attachments_: attachmentMap,
|
|
134
|
+
_vars_: thevars,
|
|
135
|
+
_fields_: apireq.req.body,
|
|
136
|
+
_files_: rawFiles,
|
|
137
|
+
_meta_: meta
|
|
138
|
+
};
|
|
139
|
+
nunjucks.configure({ autoescape: true });
|
|
140
|
+
const html = nunjucks.renderString(form.template, context);
|
|
141
|
+
const mailOptions = {
|
|
142
|
+
from: form.sender,
|
|
143
|
+
to: recipient || form.recipient,
|
|
144
|
+
subject: form.subject,
|
|
145
|
+
html,
|
|
146
|
+
attachments
|
|
147
|
+
};
|
|
148
|
+
try {
|
|
149
|
+
const info = await this.server.storage.transport.sendMail(mailOptions);
|
|
150
|
+
this.server.storage.print_debug('Email sent: ' + info.response);
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
154
|
+
this.server.storage.print_debug('Error sending email: ' + errorMessage);
|
|
155
|
+
return [500, { error: `Error sending email: ${errorMessage}` }];
|
|
156
|
+
}
|
|
157
|
+
return [200, {}];
|
|
158
|
+
}
|
|
159
|
+
defineRoutes() {
|
|
160
|
+
return [
|
|
161
|
+
{
|
|
162
|
+
method: 'post',
|
|
163
|
+
path: '/v1/form/template',
|
|
164
|
+
handler: (req) => this.postFormTemplate(req),
|
|
165
|
+
auth: { type: 'yes', req: 'any' }
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
method: 'post',
|
|
169
|
+
path: '/v1/form/message',
|
|
170
|
+
handler: (req) => this.postSendForm(req),
|
|
171
|
+
auth: { type: 'none', req: 'any' }
|
|
172
|
+
}
|
|
173
|
+
];
|
|
174
|
+
}
|
|
175
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { ApiModule, ApiError } from '@technomoron/api-server-base';
|
|
2
|
+
import emailAddresses from 'email-addresses';
|
|
3
|
+
import { convert } from 'html-to-text';
|
|
4
|
+
import nunjucks from 'nunjucks';
|
|
5
|
+
import { api_domain } from '../models/domain.js';
|
|
6
|
+
import { api_txmail } from '../models/txmail.js';
|
|
7
|
+
import { api_user } from '../models/user.js';
|
|
8
|
+
import { buildRequestMeta } from '../util.js';
|
|
9
|
+
export class MailerAPI extends ApiModule {
|
|
10
|
+
//
|
|
11
|
+
// Validate and return the parsed email address
|
|
12
|
+
//
|
|
13
|
+
validateEmail(email) {
|
|
14
|
+
const parsed = emailAddresses.parseOneAddress(email);
|
|
15
|
+
if (parsed) {
|
|
16
|
+
return parsed.address;
|
|
17
|
+
}
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
//
|
|
21
|
+
// Validate a set of email addresses. Return arrays of invalid
|
|
22
|
+
// and valid email addresses.
|
|
23
|
+
//
|
|
24
|
+
validateEmails(list) {
|
|
25
|
+
const valid = [], invalid = [];
|
|
26
|
+
const emails = list
|
|
27
|
+
.split(',')
|
|
28
|
+
.map((email) => email.trim())
|
|
29
|
+
.filter((email) => email !== '');
|
|
30
|
+
emails.forEach((email) => {
|
|
31
|
+
const addr = this.validateEmail(email);
|
|
32
|
+
if (addr) {
|
|
33
|
+
valid.push(addr);
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
invalid.push(email);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
return { valid, invalid };
|
|
40
|
+
}
|
|
41
|
+
async assert_domain_and_user(apireq) {
|
|
42
|
+
const { domain, locale } = apireq.req.body;
|
|
43
|
+
if (!domain) {
|
|
44
|
+
throw new ApiError({ code: 401, message: 'Missing domain' });
|
|
45
|
+
}
|
|
46
|
+
const user = await api_user.findOne({ where: { token: apireq.token } });
|
|
47
|
+
if (!user) {
|
|
48
|
+
throw new ApiError({ code: 401, message: `Invalid/Unknown API Key/Token '${apireq.token}'` });
|
|
49
|
+
}
|
|
50
|
+
const dbdomain = await api_domain.findOne({ where: { name: domain } });
|
|
51
|
+
if (!dbdomain) {
|
|
52
|
+
throw new ApiError({ code: 401, message: `Unable to look up the domain ${domain}` });
|
|
53
|
+
}
|
|
54
|
+
apireq.domain = dbdomain;
|
|
55
|
+
apireq.locale = locale || 'en';
|
|
56
|
+
apireq.user = user;
|
|
57
|
+
}
|
|
58
|
+
// Store a template in the database
|
|
59
|
+
async post_template(apireq) {
|
|
60
|
+
await this.assert_domain_and_user(apireq);
|
|
61
|
+
const { template, sender = '', name, subject = '', locale = '' } = apireq.req.body;
|
|
62
|
+
if (!template) {
|
|
63
|
+
throw new ApiError({ code: 400, message: 'Missing template data' });
|
|
64
|
+
}
|
|
65
|
+
if (!name) {
|
|
66
|
+
throw new ApiError({ code: 400, message: 'Missing template name' });
|
|
67
|
+
}
|
|
68
|
+
const data = {
|
|
69
|
+
user_id: apireq.user.user_id,
|
|
70
|
+
domain_id: apireq.domain.domain_id,
|
|
71
|
+
name,
|
|
72
|
+
subject,
|
|
73
|
+
locale,
|
|
74
|
+
sender,
|
|
75
|
+
template
|
|
76
|
+
};
|
|
77
|
+
/*
|
|
78
|
+
console.log(JSON.stringify({
|
|
79
|
+
user: apireq.user,
|
|
80
|
+
domain: apireq.domain,
|
|
81
|
+
domain_id: apireq.domain.domain_id,
|
|
82
|
+
data
|
|
83
|
+
}, undefined, 2)); */
|
|
84
|
+
try {
|
|
85
|
+
const [templateRecord, created] = await api_txmail.upsert(data, {
|
|
86
|
+
returning: true
|
|
87
|
+
});
|
|
88
|
+
console.log('Template upserted:', templateRecord.name, 'Created:', created);
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
throw new ApiError({
|
|
92
|
+
code: 500,
|
|
93
|
+
message: this.server.guessExceptionText(error, 'Unknown Sequelize Error on upsert template')
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
return [200, { Status: 'OK' }];
|
|
97
|
+
}
|
|
98
|
+
// Send a template using posted arguments.
|
|
99
|
+
async post_send(apireq) {
|
|
100
|
+
await this.assert_domain_and_user(apireq);
|
|
101
|
+
const { name, rcpt, domain = '', locale = '', vars = {} } = apireq.req.body;
|
|
102
|
+
if (!name || !rcpt || !domain) {
|
|
103
|
+
throw new ApiError({ code: 400, message: 'name/rcpt/domain required' });
|
|
104
|
+
}
|
|
105
|
+
let parsedVars = vars ?? {};
|
|
106
|
+
if (typeof vars === 'string') {
|
|
107
|
+
try {
|
|
108
|
+
parsedVars = JSON.parse(vars);
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
throw new ApiError({ code: 400, message: 'Invalid JSON provided in "vars"' });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
const thevars = parsedVars;
|
|
115
|
+
// const dbdomain = await api_domain.findOne({ where: { domain } });
|
|
116
|
+
const { valid, invalid } = this.validateEmails(rcpt);
|
|
117
|
+
if (invalid.length > 0) {
|
|
118
|
+
throw new ApiError({ code: 400, message: 'Invalid email address(es): ' + invalid.join(',') });
|
|
119
|
+
}
|
|
120
|
+
let template = null;
|
|
121
|
+
const deflocale = apireq.server.store.deflocale || '';
|
|
122
|
+
const domain_id = apireq.domain.domain_id;
|
|
123
|
+
try {
|
|
124
|
+
template =
|
|
125
|
+
(await api_txmail.findOne({ where: { name, domain_id, locale } })) ||
|
|
126
|
+
(await api_txmail.findOne({ where: { name, domain_id, locale: deflocale } })) ||
|
|
127
|
+
(await api_txmail.findOne({ where: { name, domain_id } }));
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
throw new ApiError({
|
|
131
|
+
code: 500,
|
|
132
|
+
message: this.server.guessExceptionText(error, 'Unknown Sequelize Error')
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
if (!template) {
|
|
136
|
+
throw new ApiError({
|
|
137
|
+
code: 404,
|
|
138
|
+
message: `Template "${name}" not found for any locale in domain "${domain}"`
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
const sender = template.sender || apireq.domain.sender || apireq.user.email;
|
|
142
|
+
if (!sender) {
|
|
143
|
+
throw new ApiError({ code: 500, message: `Unable to locate sender for ${template.name}` });
|
|
144
|
+
}
|
|
145
|
+
const rawFiles = Array.isArray(apireq.req.files) ? apireq.req.files : [];
|
|
146
|
+
const templateAssets = Array.isArray(template.files) ? template.files : [];
|
|
147
|
+
const attachments = [
|
|
148
|
+
...templateAssets.map((file) => ({
|
|
149
|
+
filename: file.filename,
|
|
150
|
+
path: file.path,
|
|
151
|
+
cid: file.cid
|
|
152
|
+
})),
|
|
153
|
+
...rawFiles.map((file) => ({
|
|
154
|
+
filename: file.originalname,
|
|
155
|
+
path: file.path
|
|
156
|
+
}))
|
|
157
|
+
];
|
|
158
|
+
const attachmentMap = {};
|
|
159
|
+
for (const file of rawFiles) {
|
|
160
|
+
attachmentMap[file.fieldname] = file.originalname;
|
|
161
|
+
}
|
|
162
|
+
console.log(JSON.stringify({ vars, thevars }, undefined, 2));
|
|
163
|
+
const meta = buildRequestMeta(apireq.req);
|
|
164
|
+
try {
|
|
165
|
+
const env = new nunjucks.Environment(null, { autoescape: false });
|
|
166
|
+
const compiled = nunjucks.compile(template.template, env);
|
|
167
|
+
for (const recipient of valid) {
|
|
168
|
+
const fullargs = {
|
|
169
|
+
...thevars,
|
|
170
|
+
_rcpt_email_: recipient,
|
|
171
|
+
_attachments_: attachmentMap,
|
|
172
|
+
_vars_: thevars,
|
|
173
|
+
_meta_: meta
|
|
174
|
+
};
|
|
175
|
+
const html = await compiled.render(fullargs);
|
|
176
|
+
const text = convert(html);
|
|
177
|
+
const sendargs = {
|
|
178
|
+
from: sender,
|
|
179
|
+
to: recipient,
|
|
180
|
+
subject: template.subject || apireq.req.body.subject || '',
|
|
181
|
+
html,
|
|
182
|
+
text,
|
|
183
|
+
attachments
|
|
184
|
+
};
|
|
185
|
+
await apireq.server.storage.transport.sendMail(sendargs);
|
|
186
|
+
}
|
|
187
|
+
return [200, { Status: 'OK', Message: 'Emails sent successfully' }];
|
|
188
|
+
}
|
|
189
|
+
catch (error) {
|
|
190
|
+
// console.log(JSON.stringify(e, null, 2));
|
|
191
|
+
throw new ApiError({
|
|
192
|
+
code: 500,
|
|
193
|
+
message: error instanceof Error ? error.message : String(error)
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
defineRoutes() {
|
|
198
|
+
return [
|
|
199
|
+
{
|
|
200
|
+
method: 'post',
|
|
201
|
+
path: '/v1/tx/message',
|
|
202
|
+
handler: this.post_send.bind(this),
|
|
203
|
+
auth: { type: 'yes', req: 'any' }
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
method: 'post',
|
|
207
|
+
path: '/v1/tx/template',
|
|
208
|
+
handler: this.post_template.bind(this),
|
|
209
|
+
auth: { type: 'yes', req: 'any' }
|
|
210
|
+
}
|
|
211
|
+
];
|
|
212
|
+
}
|
|
213
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { pathToFileURL } from 'node:url';
|
|
2
|
+
import { FormAPI } from './api/forms.js';
|
|
3
|
+
import { MailerAPI } from './api/mailer.js';
|
|
4
|
+
import { mailApiServer } from './server.js';
|
|
5
|
+
import { mailStore } from './store/store.js';
|
|
6
|
+
function buildServerConfig(store, overrides) {
|
|
7
|
+
const env = store.env;
|
|
8
|
+
return {
|
|
9
|
+
apiHost: env.API_HOST,
|
|
10
|
+
apiPort: env.API_PORT,
|
|
11
|
+
uploadPath: env.UPLOAD_PATH,
|
|
12
|
+
debug: env.DEBUG,
|
|
13
|
+
...overrides
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
export async function createMailMagicServer(overrides = {}) {
|
|
17
|
+
const store = await new mailStore().init();
|
|
18
|
+
const config = buildServerConfig(store, overrides);
|
|
19
|
+
const server = new mailApiServer(config, store).api(new MailerAPI()).api(new FormAPI());
|
|
20
|
+
return { server, store, env: store.env };
|
|
21
|
+
}
|
|
22
|
+
export async function startMailMagicServer(overrides = {}) {
|
|
23
|
+
const bootstrap = await createMailMagicServer(overrides);
|
|
24
|
+
await bootstrap.server.start();
|
|
25
|
+
return bootstrap;
|
|
26
|
+
}
|
|
27
|
+
async function bootMailMagic() {
|
|
28
|
+
try {
|
|
29
|
+
const { env } = await startMailMagicServer();
|
|
30
|
+
console.log(`mail-magic server listening on ${env.API_HOST}:${env.API_PORT}`);
|
|
31
|
+
}
|
|
32
|
+
catch (err) {
|
|
33
|
+
console.error('Failed to start FormMailer:', err);
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const isDirectExecution = (() => {
|
|
38
|
+
if (!process.argv[1]) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
return import.meta.url === pathToFileURL(process.argv[1]).href;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
})();
|
|
48
|
+
if (isDirectExecution) {
|
|
49
|
+
void bootMailMagic();
|
|
50
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { Sequelize } from 'sequelize';
|
|
2
|
+
import { init_api_domain, api_domain } from './domain.js';
|
|
3
|
+
import { init_api_form, api_form } from './form.js';
|
|
4
|
+
import { importData } from './init.js';
|
|
5
|
+
import { init_api_txmail, api_txmail } from './txmail.js';
|
|
6
|
+
import { init_api_user, api_user } from './user.js';
|
|
7
|
+
export async function init_api_db(db, store) {
|
|
8
|
+
await init_api_user(db);
|
|
9
|
+
await init_api_domain(db);
|
|
10
|
+
await init_api_txmail(db);
|
|
11
|
+
await init_api_form(db);
|
|
12
|
+
// User ↔ Domain
|
|
13
|
+
api_user.hasMany(api_domain, {
|
|
14
|
+
foreignKey: 'user_id',
|
|
15
|
+
as: 'domains'
|
|
16
|
+
});
|
|
17
|
+
api_domain.belongsTo(api_user, {
|
|
18
|
+
foreignKey: 'user_id',
|
|
19
|
+
as: 'user'
|
|
20
|
+
});
|
|
21
|
+
// User ↔ Template
|
|
22
|
+
api_user.hasMany(api_txmail, {
|
|
23
|
+
foreignKey: 'user_id',
|
|
24
|
+
as: 'txmail'
|
|
25
|
+
});
|
|
26
|
+
api_txmail.belongsTo(api_user, {
|
|
27
|
+
foreignKey: 'user_id',
|
|
28
|
+
as: 'user'
|
|
29
|
+
});
|
|
30
|
+
// Domain ↔ Template
|
|
31
|
+
api_domain.hasMany(api_txmail, {
|
|
32
|
+
foreignKey: 'domain_id',
|
|
33
|
+
as: 'txmail'
|
|
34
|
+
});
|
|
35
|
+
api_txmail.belongsTo(api_domain, {
|
|
36
|
+
foreignKey: 'domain_id',
|
|
37
|
+
as: 'domain'
|
|
38
|
+
});
|
|
39
|
+
api_user.belongsTo(api_domain, {
|
|
40
|
+
foreignKey: 'domain',
|
|
41
|
+
as: 'defaultDomain'
|
|
42
|
+
});
|
|
43
|
+
api_domain.hasMany(api_user, {
|
|
44
|
+
foreignKey: 'domain',
|
|
45
|
+
as: 'usersWithDefault'
|
|
46
|
+
});
|
|
47
|
+
api_user.hasMany(api_form, {
|
|
48
|
+
foreignKey: 'user_id',
|
|
49
|
+
as: 'forms'
|
|
50
|
+
});
|
|
51
|
+
api_form.belongsTo(api_user, {
|
|
52
|
+
foreignKey: 'user_id',
|
|
53
|
+
as: 'user'
|
|
54
|
+
});
|
|
55
|
+
api_domain.hasMany(api_form, {
|
|
56
|
+
foreignKey: 'domain_id',
|
|
57
|
+
as: 'forms'
|
|
58
|
+
});
|
|
59
|
+
api_form.belongsTo(api_domain, {
|
|
60
|
+
foreignKey: 'domain_id',
|
|
61
|
+
as: 'domain'
|
|
62
|
+
});
|
|
63
|
+
await db.query('PRAGMA foreign_keys = OFF');
|
|
64
|
+
store.print_debug(`Force alter tables: ${store.env.DB_FORCE_SYNC}`);
|
|
65
|
+
await db.sync({ alter: true, force: store.env.DB_FORCE_SYNC });
|
|
66
|
+
await db.query('PRAGMA foreign_keys = ON');
|
|
67
|
+
await importData(store);
|
|
68
|
+
store.print_debug('API Database Initialized...');
|
|
69
|
+
}
|
|
70
|
+
export async function connect_api_db(store) {
|
|
71
|
+
console.log('DB INIT');
|
|
72
|
+
const env = store.env;
|
|
73
|
+
const dbparams = {
|
|
74
|
+
logging: false, // env.DB_LOG ? console.log : false,
|
|
75
|
+
dialect: env.DB_TYPE,
|
|
76
|
+
dialectOptions: {
|
|
77
|
+
charset: 'utf8mb4'
|
|
78
|
+
},
|
|
79
|
+
define: {
|
|
80
|
+
charset: 'utf8mb4',
|
|
81
|
+
collate: 'utf8mb4_unicode_ci'
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
if (env.DB_TYPE === 'sqlite') {
|
|
85
|
+
dbparams.storage = env.DB_NAME.endsWith('.db') ? env.DB_NAME : `${env.DB_NAME}.db`;
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
dbparams.host = env.DB_HOST;
|
|
89
|
+
dbparams.database = env.DB_NAME;
|
|
90
|
+
dbparams.username = env.DB_USER;
|
|
91
|
+
dbparams.password = env.DB_PASS;
|
|
92
|
+
}
|
|
93
|
+
store.print_debug(`Database params are:\n${JSON.stringify(dbparams, undefined, 2)}`);
|
|
94
|
+
const db = new Sequelize(dbparams);
|
|
95
|
+
await db.authenticate();
|
|
96
|
+
store.print_debug('API Database Connected');
|
|
97
|
+
await init_api_db(db, store);
|
|
98
|
+
return db;
|
|
99
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { Model, DataTypes } from 'sequelize';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
export const api_domain_schema = z.object({
|
|
4
|
+
domain_id: z.number().int().nonnegative(),
|
|
5
|
+
user_id: z.number().int().nonnegative(),
|
|
6
|
+
name: z.string().min(1),
|
|
7
|
+
sender: z.string().default(''),
|
|
8
|
+
locale: z.string().default(''),
|
|
9
|
+
is_default: z.boolean().default(false)
|
|
10
|
+
});
|
|
11
|
+
export class api_domain extends Model {
|
|
12
|
+
}
|
|
13
|
+
export async function init_api_domain(api_db) {
|
|
14
|
+
api_domain.init({
|
|
15
|
+
domain_id: {
|
|
16
|
+
type: DataTypes.INTEGER,
|
|
17
|
+
autoIncrement: true,
|
|
18
|
+
allowNull: false,
|
|
19
|
+
primaryKey: true
|
|
20
|
+
},
|
|
21
|
+
user_id: {
|
|
22
|
+
type: DataTypes.INTEGER,
|
|
23
|
+
allowNull: false,
|
|
24
|
+
references: {
|
|
25
|
+
model: 'user',
|
|
26
|
+
key: 'user_id'
|
|
27
|
+
},
|
|
28
|
+
onDelete: 'CASCADE',
|
|
29
|
+
onUpdate: 'CASCADE'
|
|
30
|
+
},
|
|
31
|
+
name: {
|
|
32
|
+
type: DataTypes.STRING,
|
|
33
|
+
allowNull: false,
|
|
34
|
+
defaultValue: ''
|
|
35
|
+
},
|
|
36
|
+
sender: {
|
|
37
|
+
type: DataTypes.STRING,
|
|
38
|
+
allowNull: false,
|
|
39
|
+
defaultValue: ''
|
|
40
|
+
},
|
|
41
|
+
locale: {
|
|
42
|
+
type: DataTypes.STRING,
|
|
43
|
+
allowNull: false,
|
|
44
|
+
defaultValue: ''
|
|
45
|
+
},
|
|
46
|
+
is_default: {
|
|
47
|
+
type: DataTypes.BOOLEAN,
|
|
48
|
+
allowNull: false,
|
|
49
|
+
defaultValue: false
|
|
50
|
+
}
|
|
51
|
+
}, {
|
|
52
|
+
sequelize: api_db,
|
|
53
|
+
tableName: 'domain',
|
|
54
|
+
charset: 'utf8mb4',
|
|
55
|
+
collate: 'utf8mb4_unicode_ci'
|
|
56
|
+
});
|
|
57
|
+
return api_domain;
|
|
58
|
+
}
|