odac 1.0.1 → 1.2.0
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/.agent/rules/coding.md +27 -0
- package/.agent/rules/memory.md +33 -0
- package/.agent/rules/project.md +30 -0
- package/.agent/rules/workflow.md +16 -0
- package/.github/workflows/auto-pr-description.yml +3 -1
- package/.github/workflows/release.yml +42 -1
- package/.github/workflows/test-coverage.yml +6 -5
- package/.github/workflows/test-publish.yml +36 -0
- package/.husky/pre-commit +10 -0
- package/.husky/pre-push +13 -0
- package/.releaserc.js +3 -3
- package/CHANGELOG.md +184 -0
- package/README.md +53 -34
- package/bin/odac.js +181 -49
- package/client/odac.js +878 -995
- package/docs/backend/01-overview/03-development-server.md +39 -46
- package/docs/backend/02-structure/01-typical-project-layout.md +59 -25
- package/docs/backend/03-config/00-configuration-overview.md +15 -6
- package/docs/backend/03-config/01-database-connection.md +3 -3
- package/docs/backend/03-config/02-static-route-mapping-optional.md +1 -1
- package/docs/backend/03-config/03-request-timeout.md +1 -1
- package/docs/backend/03-config/04-environment-variables.md +4 -4
- package/docs/backend/03-config/05-early-hints.md +2 -2
- package/docs/backend/04-routing/02-controller-less-view-routes.md +9 -3
- package/docs/backend/04-routing/03-api-and-data-routes.md +18 -0
- package/docs/backend/04-routing/07-cron-jobs.md +17 -1
- package/docs/backend/04-routing/09-websocket.md +29 -0
- package/docs/backend/05-controllers/01-how-to-build-a-controller.md +48 -3
- package/docs/backend/05-controllers/02-your-trusty-odac-assistant.md +2 -0
- package/docs/backend/05-controllers/03-controller-classes.md +61 -55
- package/docs/backend/05-forms/01-custom-forms.md +103 -95
- package/docs/backend/05-forms/02-automatic-database-insert.md +21 -21
- package/docs/backend/06-request-and-response/01-the-request-object-what-is-the-user-asking-for.md +17 -0
- package/docs/backend/07-views/02-rendering-a-view.md +1 -1
- package/docs/backend/07-views/03-variables.md +5 -5
- package/docs/backend/07-views/04-request-data.md +1 -1
- package/docs/backend/07-views/08-backend-javascript.md +1 -1
- package/docs/backend/07-views/10-styling-and-tailwind.md +93 -0
- package/docs/backend/08-database/01-getting-started.md +100 -0
- package/docs/backend/08-database/02-basics.md +136 -0
- package/docs/backend/08-database/03-advanced.md +84 -0
- package/docs/backend/08-database/04-migrations.md +48 -0
- package/docs/backend/09-validation/01-the-validator-service.md +1 -0
- package/docs/backend/10-authentication/03-register.md +9 -2
- package/docs/backend/10-authentication/04-odac-register-forms.md +48 -48
- package/docs/backend/10-authentication/05-session-management.md +16 -2
- package/docs/backend/10-authentication/06-odac-login-forms.md +50 -50
- package/docs/backend/10-authentication/07-magic-links.md +134 -0
- package/docs/backend/11-mail/01-the-mail-service.md +118 -28
- package/docs/backend/12-streaming/01-streaming-overview.md +2 -2
- package/docs/backend/13-utilities/01-odac-var.md +7 -7
- package/docs/backend/13-utilities/02-ipc.md +73 -0
- package/docs/frontend/01-overview/01-introduction.md +5 -1
- package/docs/frontend/02-ajax-navigation/01-quick-start.md +1 -1
- package/docs/index.json +21 -125
- package/eslint.config.mjs +5 -47
- package/jest.config.js +1 -1
- package/package.json +16 -7
- package/src/Auth.js +414 -121
- package/src/Config.js +12 -7
- package/src/Database.js +188 -0
- package/src/Env.js +3 -1
- package/src/Ipc.js +337 -0
- package/src/Lang.js +9 -2
- package/src/Mail.js +408 -37
- package/src/Odac.js +105 -40
- package/src/Request.js +71 -49
- package/src/Route/Cron.js +62 -18
- package/src/Route/Internal.js +215 -12
- package/src/Route/Middleware.js +7 -2
- package/src/Route.js +372 -109
- package/src/Server.js +118 -12
- package/src/Storage.js +169 -0
- package/src/Token.js +6 -4
- package/src/Validator.js +95 -3
- package/src/Var.js +22 -6
- package/src/View/EarlyHints.js +43 -33
- package/src/View/Form.js +210 -28
- package/src/View.js +108 -7
- package/src/WebSocket.js +18 -3
- package/template/odac.json +5 -0
- package/template/package.json +3 -1
- package/template/route/www.js +12 -10
- package/template/view/content/home.html +3 -3
- package/template/view/head/main.html +2 -2
- package/test/Client.test.js +168 -0
- package/test/Config.test.js +112 -0
- package/test/Lang.test.js +92 -0
- package/test/Odac.test.js +86 -0
- package/test/{framework/middleware.test.js → Route/Middleware.test.js} +2 -2
- package/test/{framework/Route.test.js → Route.test.js} +1 -1
- package/test/{framework/View → View}/EarlyHints.test.js +1 -1
- package/test/{framework/WebSocket.test.js → WebSocket.test.js} +2 -2
- package/test/scripts/check-coverage.js +4 -4
- package/docs/backend/08-database/01-database-connection.md +0 -99
- package/docs/backend/08-database/02-using-mysql.md +0 -322
- package/src/Mysql.js +0 -575
- package/template/config.json +0 -5
- package/test/cli/Cli.test.js +0 -36
- package/test/core/Candy.test.js +0 -234
- package/test/core/Commands.test.js +0 -538
- package/test/core/Config.test.js +0 -1432
- package/test/core/Lang.test.js +0 -250
- package/test/core/Process.test.js +0 -156
- package/test/server/Api.test.js +0 -647
- package/test/server/DNS.test.js +0 -2050
- package/test/server/DNS.test.js.bak +0 -2084
- package/test/server/Hub.test.js +0 -497
- package/test/server/Log.test.js +0 -73
- package/test/server/Mail.account.test_.js +0 -460
- package/test/server/Mail.init.test_.js +0 -411
- package/test/server/Mail.test_.js +0 -1340
- package/test/server/SSL.test_.js +0 -1491
- package/test/server/Server.test.js +0 -765
- package/test/server/Service.test_.js +0 -1127
- package/test/server/Subdomain.test.js +0 -440
- package/test/server/Web/Firewall.test.js +0 -175
- package/test/server/Web/Proxy.test.js +0 -397
- package/test/server/Web.test.js +0 -1494
- package/test/server/__mocks__/acme-client.js +0 -17
- package/test/server/__mocks__/bcrypt.js +0 -50
- package/test/server/__mocks__/child_process.js +0 -389
- package/test/server/__mocks__/crypto.js +0 -432
- package/test/server/__mocks__/fs.js +0 -450
- package/test/server/__mocks__/globalOdac.js +0 -227
- package/test/server/__mocks__/http.js +0 -575
- package/test/server/__mocks__/https.js +0 -272
- package/test/server/__mocks__/index.js +0 -249
- package/test/server/__mocks__/mail/server.js +0 -100
- package/test/server/__mocks__/mail/smtp.js +0 -31
- package/test/server/__mocks__/mailparser.js +0 -81
- package/test/server/__mocks__/net.js +0 -369
- package/test/server/__mocks__/node-forge.js +0 -328
- package/test/server/__mocks__/os.js +0 -320
- package/test/server/__mocks__/path.js +0 -291
- package/test/server/__mocks__/selfsigned.js +0 -8
- package/test/server/__mocks__/server/src/mail/server.js +0 -100
- package/test/server/__mocks__/server/src/mail/smtp.js +0 -31
- package/test/server/__mocks__/smtp-server.js +0 -106
- package/test/server/__mocks__/sqlite3.js +0 -394
- package/test/server/__mocks__/testFactories.js +0 -299
- package/test/server/__mocks__/testHelpers.js +0 -363
- package/test/server/__mocks__/tls.js +0 -229
package/src/Mail.js
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
const
|
|
1
|
+
const net = require('net')
|
|
2
|
+
const nodeCrypto = require('crypto')
|
|
2
3
|
const fs = require('fs')
|
|
4
|
+
// const Form = require('./View/Form')
|
|
5
|
+
|
|
6
|
+
const CACHE_DIR = './storage/.cache'
|
|
3
7
|
|
|
4
8
|
class Mail {
|
|
5
9
|
#header = {}
|
|
@@ -7,11 +11,273 @@ class Mail {
|
|
|
7
11
|
#subject = ''
|
|
8
12
|
#template
|
|
9
13
|
#to
|
|
14
|
+
#htmlContent
|
|
15
|
+
#textContent
|
|
10
16
|
|
|
11
17
|
constructor(template) {
|
|
12
18
|
this.#template = template
|
|
13
19
|
}
|
|
14
20
|
|
|
21
|
+
#functions = {
|
|
22
|
+
'{!!': {
|
|
23
|
+
function: '${await ',
|
|
24
|
+
close: '!!}',
|
|
25
|
+
end: '}'
|
|
26
|
+
},
|
|
27
|
+
'{{--': {
|
|
28
|
+
function: '`; /*',
|
|
29
|
+
close: '--}}',
|
|
30
|
+
end: '*/ html += `'
|
|
31
|
+
},
|
|
32
|
+
'{{': {
|
|
33
|
+
function: '${Odac.Var(await ',
|
|
34
|
+
close: '}}',
|
|
35
|
+
end: ').html().replace(/\\n/g, "<br>")}'
|
|
36
|
+
},
|
|
37
|
+
break: {
|
|
38
|
+
function: 'break;',
|
|
39
|
+
arguments: {}
|
|
40
|
+
},
|
|
41
|
+
component: {},
|
|
42
|
+
continue: {
|
|
43
|
+
function: 'continue;',
|
|
44
|
+
arguments: {}
|
|
45
|
+
},
|
|
46
|
+
mysql: {},
|
|
47
|
+
elseif: {
|
|
48
|
+
function: '} else if(await ($condition)){',
|
|
49
|
+
arguments: {
|
|
50
|
+
condition: true
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
else: {
|
|
54
|
+
function: '} else {'
|
|
55
|
+
},
|
|
56
|
+
fetch: {},
|
|
57
|
+
for: {
|
|
58
|
+
function: '{ let _arr = $constructor; for(let $key in _arr){ let $value = _arr[$key];',
|
|
59
|
+
end: '}}',
|
|
60
|
+
arguments: {
|
|
61
|
+
var: null,
|
|
62
|
+
get: null,
|
|
63
|
+
key: 'key',
|
|
64
|
+
value: 'value'
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
if: {
|
|
68
|
+
function: 'if(await ($condition)){',
|
|
69
|
+
arguments: {
|
|
70
|
+
condition: true
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
'<odac:js>': {
|
|
74
|
+
end: ' html += `',
|
|
75
|
+
function: '`; ',
|
|
76
|
+
close: '</odac:js>'
|
|
77
|
+
},
|
|
78
|
+
lazy: {},
|
|
79
|
+
list: {
|
|
80
|
+
arguments: {
|
|
81
|
+
var: null,
|
|
82
|
+
get: null,
|
|
83
|
+
key: 'key',
|
|
84
|
+
value: 'value'
|
|
85
|
+
},
|
|
86
|
+
end: '}}',
|
|
87
|
+
function: '{ let _arr = $constructor; for(let $key in _arr){ let $value = _arr[$key];',
|
|
88
|
+
replace: 'ul'
|
|
89
|
+
},
|
|
90
|
+
while: {
|
|
91
|
+
function: 'while(await ($condition)){',
|
|
92
|
+
arguments: {
|
|
93
|
+
condition: true
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
#parseOdacTag(content) {
|
|
99
|
+
// Parse backend comments
|
|
100
|
+
content = content.replace(/<!--odac([\s\S]*?)(?:odac-->|-->)/g, () => '')
|
|
101
|
+
|
|
102
|
+
// Parse <script:odac> tags
|
|
103
|
+
content = content.replace(/<script:odac([^>]*)>([\s\S]*?)<\/script:odac>/g, (fullMatch, attributes, jsContent) => {
|
|
104
|
+
return `<odac:js>${jsContent}</odac:js>`
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
content = content.replace(/<odac:else\s*\/>/g, '<odac:else>')
|
|
108
|
+
content = content.replace(/<odac:elseif\s+([^>]*?)\/>/g, '<odac:elseif $1>')
|
|
109
|
+
|
|
110
|
+
content = content.replace(/<odac([^>]*?)\/>/g, (fullMatch, attributes) => {
|
|
111
|
+
attributes = attributes.trim()
|
|
112
|
+
const attrs = {}
|
|
113
|
+
const attrRegex = /(\w+)(?:=(["'])((?:(?!\2).)*)\2|=([^\s>]+))?/g
|
|
114
|
+
let match
|
|
115
|
+
while ((match = attrRegex.exec(attributes))) {
|
|
116
|
+
const key = match[1]
|
|
117
|
+
const value = match[3] !== undefined ? match[3] : match[4] !== undefined ? match[4] : true
|
|
118
|
+
attrs[key] = value
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (attrs.get) return `{{ get('${attrs.get}') || '' }}`
|
|
122
|
+
else if (attrs.var) return attrs.raw ? `{!! ${attrs.var} !!}` : `{{ ${attrs.var} }}`
|
|
123
|
+
|
|
124
|
+
return fullMatch
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
let depth = 0
|
|
128
|
+
let maxDepth = 10
|
|
129
|
+
while (depth < maxDepth && content.includes('<odac')) {
|
|
130
|
+
const before = content
|
|
131
|
+
content = content.replace(/<odac([^>]*)>((?:(?!<odac)[\s\S])*?)<\/odac>/g, (fullMatch, attributes, innerContent) => {
|
|
132
|
+
attributes = attributes.trim()
|
|
133
|
+
innerContent = innerContent.trim()
|
|
134
|
+
|
|
135
|
+
const attrs = {}
|
|
136
|
+
const attrRegex = /(\w+)(?:=(["'])((?:(?!\2).)*)\2|=([^\s>]+))?/g
|
|
137
|
+
let match
|
|
138
|
+
while ((match = attrRegex.exec(attributes))) {
|
|
139
|
+
const key = match[1]
|
|
140
|
+
const value = match[3] !== undefined ? match[3] : match[4] !== undefined ? match[4] : true
|
|
141
|
+
attrs[key] = value
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (attrs.get) return `{{ get('${attrs.get}') || '' }}`
|
|
145
|
+
else if (attrs.var) return attrs.raw ? `{!! ${attrs.var} !!}` : `{{ ${attrs.var} }}`
|
|
146
|
+
else if (attrs.t || attrs.translate)
|
|
147
|
+
return `{{ '${innerContent}' }}` // Simple fallback for mail
|
|
148
|
+
else return `{{ '${innerContent}' }}`
|
|
149
|
+
})
|
|
150
|
+
if (before === content) break
|
|
151
|
+
depth++
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return content
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async #render(file, data) {
|
|
158
|
+
const fd = fs.openSync(file, 'r')
|
|
159
|
+
let mtime, content
|
|
160
|
+
try {
|
|
161
|
+
mtime = fs.fstatSync(fd).mtimeMs
|
|
162
|
+
content = fs.readFileSync(fd, 'utf8')
|
|
163
|
+
} finally {
|
|
164
|
+
fs.closeSync(fd)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Since mail doesn't have a persistent Odac instance access like View cache, we manage a simple cache or just re-compile.
|
|
168
|
+
// For performance in emails (usually background), re-compiling is okay, but caching is better.
|
|
169
|
+
// Let's use global Odac.View.cache if available or local.
|
|
170
|
+
if (!Odac.View) Odac.View = {}
|
|
171
|
+
if (!Odac.View.cache) Odac.View.cache = {}
|
|
172
|
+
|
|
173
|
+
if (Odac.View.cache[file]?.mtime !== mtime) {
|
|
174
|
+
// No Form options needed normally for simplified email templates, but keeping Form.parse for consistency if needed
|
|
175
|
+
// content = Form.parse(content, {Request: {}, ...Odac}) // Partially mock if Form needs it, but Form usually needs full Request.
|
|
176
|
+
// Skipping Form.parse for mail for now unless requested, as it relies on Session/Request heavily.
|
|
177
|
+
// User asked for "View file rendering", usually meaning logic tags.
|
|
178
|
+
|
|
179
|
+
const jsBlocks = []
|
|
180
|
+
content = content.replace(/<script:odac([^>]*)>([\s\S]*?)<\/script:odac>/g, (match, attrs, jsContent) => {
|
|
181
|
+
const placeholder = `___ODAC_JS_BLOCK_${jsBlocks.length}___`
|
|
182
|
+
jsBlocks.push(jsContent)
|
|
183
|
+
return `<script:odac${attrs}>${placeholder}</script:odac>`
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
content = this.#parseOdacTag(content)
|
|
187
|
+
content = content.replace(/`/g, '\\\\`').replace(/\$\{/g, '\\\\${')
|
|
188
|
+
|
|
189
|
+
jsBlocks.forEach((jsContent, index) => {
|
|
190
|
+
content = content.replace(`___ODAC_JS_BLOCK_${index}___`, jsContent)
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
let result = 'html += `\n' + content + '\n`'
|
|
194
|
+
content = content.split('\n')
|
|
195
|
+
|
|
196
|
+
for (let key in this.#functions) {
|
|
197
|
+
let att = ''
|
|
198
|
+
let func = this.#functions[key]
|
|
199
|
+
let matches = func.close
|
|
200
|
+
? result.match(new RegExp(`${key}[\\s\\S]*?${func.close}`, 'g'))
|
|
201
|
+
: result.match(new RegExp(`<odac:${key}(?:\\s+[^>]*?(?:"[^"]*"|'[^']*'|[^"'>])*)?>`, 'g'))
|
|
202
|
+
if (!matches) continue
|
|
203
|
+
for (let match of matches) {
|
|
204
|
+
let matchForParsing = match
|
|
205
|
+
if (!func.close) matchForParsing = matchForParsing.replace(/^<odac:/, '').replace(/>$/, '')
|
|
206
|
+
const attrRegex = /(\w+)(?:=(["'])((?:(?!\2).)*)\2|=([^\s>]+))?/g
|
|
207
|
+
let attrMatch
|
|
208
|
+
const args = []
|
|
209
|
+
while ((attrMatch = attrRegex.exec(matchForParsing))) {
|
|
210
|
+
args.push(attrMatch[0])
|
|
211
|
+
}
|
|
212
|
+
let vars = {}
|
|
213
|
+
if (func.arguments)
|
|
214
|
+
for (let arg of args) {
|
|
215
|
+
const argRegex = /(\w+)(?:=(["'])((?:(?!\2).)*)\2|=([^\s>]+))?/
|
|
216
|
+
const argMatch = argRegex.exec(arg)
|
|
217
|
+
if (!argMatch) continue
|
|
218
|
+
const argKey = argMatch[1]
|
|
219
|
+
const value = argMatch[3] !== undefined ? argMatch[3] : argMatch[4] !== undefined ? argMatch[4] : true
|
|
220
|
+
if (func.arguments[argKey] === undefined) {
|
|
221
|
+
att += `${argKey}="${value}"`
|
|
222
|
+
continue
|
|
223
|
+
}
|
|
224
|
+
vars[argKey] = value
|
|
225
|
+
}
|
|
226
|
+
if (!func.function) continue
|
|
227
|
+
let fun = func.function
|
|
228
|
+
|
|
229
|
+
// Simplified logic for loop
|
|
230
|
+
if (key === 'for' || key === 'list') {
|
|
231
|
+
let constructor
|
|
232
|
+
if (vars.var) {
|
|
233
|
+
constructor = `await ${vars.var}`
|
|
234
|
+
delete vars.var
|
|
235
|
+
}
|
|
236
|
+
fun = fun.replace(/\$constructor/g, constructor)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
for (let argKey in func.arguments) {
|
|
240
|
+
if (argKey === 'var' || argKey === 'get') continue
|
|
241
|
+
if (vars[argKey] === undefined) vars[argKey] = func.arguments[argKey]
|
|
242
|
+
fun = fun.replace(new RegExp(`\\$${argKey}`, 'g'), vars[argKey])
|
|
243
|
+
}
|
|
244
|
+
if (func.close) {
|
|
245
|
+
result = result.replace(match, fun + match.substring(key.length, match.length - func.close.length) + func.end)
|
|
246
|
+
} else {
|
|
247
|
+
result = result.replace(match, (func.replace ? `<${[func.replace, att].join(' ')}>` : '') + '`; ' + fun + ' html += `')
|
|
248
|
+
result = result.replace(`</odac:${key}>`, '`; ' + (func.end ?? '}') + ' html += `' + (func.replace ? `</${func.replace}>` : ''))
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
let cache = `${nodeCrypto.createHash('md5').update(file).digest('hex')}`
|
|
254
|
+
if (!fs.existsSync(CACHE_DIR)) fs.mkdirSync(CACHE_DIR, {recursive: true})
|
|
255
|
+
fs.writeFileSync(
|
|
256
|
+
`${CACHE_DIR}/${cache}`,
|
|
257
|
+
`module.exports = async (Odac, data, get, __) => {\n
|
|
258
|
+
// Destructure data keys into local scope variables
|
|
259
|
+
${Object.keys(data)
|
|
260
|
+
.map(k => `let ${k} = data['${k}'];`)
|
|
261
|
+
.join('\n')}
|
|
262
|
+
let html = '';\n${result}\nreturn html.trim()\n}`
|
|
263
|
+
)
|
|
264
|
+
delete require.cache[require.resolve(`${__dir}/${CACHE_DIR}/${cache}`)]
|
|
265
|
+
Odac.View.cache[file] = {mtime: mtime, cache: cache}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
try {
|
|
269
|
+
return await require(`${__dir}/${CACHE_DIR}/${Odac.View.cache[file].cache}`)(
|
|
270
|
+
Odac,
|
|
271
|
+
data,
|
|
272
|
+
key => data[key],
|
|
273
|
+
(...args) => (Odac.Lang ? Odac.Lang.get(...args) : args[0])
|
|
274
|
+
)
|
|
275
|
+
} catch (e) {
|
|
276
|
+
console.error(e)
|
|
277
|
+
return ''
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
15
281
|
header(header) {
|
|
16
282
|
this.#header = header
|
|
17
283
|
return this
|
|
@@ -27,34 +293,108 @@ class Mail {
|
|
|
27
293
|
return this
|
|
28
294
|
}
|
|
29
295
|
|
|
30
|
-
to(email) {
|
|
31
|
-
|
|
32
|
-
this
|
|
296
|
+
to(email, name = '') {
|
|
297
|
+
this.#to = {value: [{address: email, name: name}]}
|
|
298
|
+
return this
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
html(content) {
|
|
302
|
+
this.#htmlContent = content
|
|
33
303
|
return this
|
|
34
304
|
}
|
|
35
305
|
|
|
36
|
-
|
|
306
|
+
text(content) {
|
|
307
|
+
this.#textContent = content
|
|
308
|
+
return this
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
#encode(text) {
|
|
312
|
+
if (!text) return ''
|
|
313
|
+
// eslint-disable-next-line
|
|
314
|
+
if (/^[\x00-\x7F]*$/.test(text)) return text
|
|
315
|
+
return '=?UTF-8?B?' + Buffer.from(text).toString('base64') + '?='
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
#stripHtml(html) {
|
|
319
|
+
if (!html) return ''
|
|
320
|
+
|
|
321
|
+
let text = html
|
|
322
|
+
// Recursively remove script and style tags to handle nested injections
|
|
323
|
+
// Single-pass removal for plain text generation.
|
|
324
|
+
// Recursive removal (do-while) is dangerous (ReDoS) and unnecessary for text/plain output.
|
|
325
|
+
text = text.replace(/<(script|style)\b[^>]*>[\s\S]*?<\/\1>/gim, '')
|
|
326
|
+
text = text.replace(/<[^>]+>/g, '')
|
|
327
|
+
|
|
328
|
+
return text.replace(/\s+/g, ' ').trim()
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
send(data = {}) {
|
|
37
332
|
return new Promise(resolve => {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
'
|
|
57
|
-
|
|
333
|
+
;(async () => {
|
|
334
|
+
try {
|
|
335
|
+
if (!this.#from || !this.#subject || !this.#to) {
|
|
336
|
+
console.error('[Mail] Missing required fields: From, Subject, or To')
|
|
337
|
+
return resolve(false)
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (!Odac.Var(this.#from.email).is('email')) {
|
|
341
|
+
console.error('[Mail] From field is not a valid e-mail address')
|
|
342
|
+
return resolve(false)
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (!Odac.Var(this.#to.value[0].address).is('email')) {
|
|
346
|
+
console.error('[Mail] To field is not a valid e-mail address')
|
|
347
|
+
return resolve(false)
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
let htmlContent = ''
|
|
351
|
+
let textContent = ''
|
|
352
|
+
|
|
353
|
+
if (this.#template) {
|
|
354
|
+
if (!fs.existsSync(__dir + '/view/mail/' + this.#template + '.html')) {
|
|
355
|
+
console.error(`[Mail] Template not found: ${__dir}/view/mail/${this.#template}.html`)
|
|
356
|
+
return resolve(false)
|
|
357
|
+
}
|
|
358
|
+
htmlContent = await this.#render(__dir + '/view/mail/' + this.#template + '.html', data)
|
|
359
|
+
textContent = this.#stripHtml(htmlContent)
|
|
360
|
+
} else {
|
|
361
|
+
if (this.#htmlContent) htmlContent = this.#htmlContent
|
|
362
|
+
if (this.#textContent) textContent = this.#textContent
|
|
363
|
+
|
|
364
|
+
if (!htmlContent && !textContent) {
|
|
365
|
+
console.error('[Mail] No content provided (Template, HTML, or Text)')
|
|
366
|
+
return resolve(false)
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// If only HTML is provided, auto-generate text
|
|
370
|
+
if (htmlContent && !textContent) {
|
|
371
|
+
textContent = this.#stripHtml(htmlContent)
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (!this.#header['From']) this.#header['From'] = `${this.#encode(this.#from.name)} <${this.#from.email}>`
|
|
376
|
+
if (!this.#header['To']) {
|
|
377
|
+
const t = this.#to.value[0]
|
|
378
|
+
this.#header['To'] = t.name ? `${this.#encode(t.name)} <${t.address}>` : t.address
|
|
379
|
+
}
|
|
380
|
+
if (!this.#header['Subject']) this.#header['Subject'] = this.#encode(this.#subject)
|
|
381
|
+
if (!this.#header['Message-ID']) this.#header['Message-ID'] = `<${nodeCrypto.randomBytes(16).toString('hex')}-${Date.now()}@odac>`
|
|
382
|
+
|
|
383
|
+
if (!this.#header['Date']) this.#header['Date'] = new Date().toUTCString()
|
|
384
|
+
if (!this.#header['Content-Type']) {
|
|
385
|
+
if (htmlContent) {
|
|
386
|
+
this.#header['Content-Type'] =
|
|
387
|
+
'multipart/alternative; charset=UTF-8; boundary="----=' + nodeCrypto.randomBytes(32).toString('hex') + '"'
|
|
388
|
+
} else {
|
|
389
|
+
this.#header['Content-Type'] = 'text/plain; charset=UTF-8'
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
if (!this.#header['X-Mailer']) this.#header['X-Mailer'] = 'ODAC'
|
|
393
|
+
if (!this.#header['MIME-Version']) this.#header['MIME-Version'] = '1.0'
|
|
394
|
+
|
|
395
|
+
const client = new net.Socket()
|
|
396
|
+
const payload = {
|
|
397
|
+
auth: process.env.ODAC_API_KEY,
|
|
58
398
|
action: 'mail.send',
|
|
59
399
|
data: [
|
|
60
400
|
{
|
|
@@ -62,22 +402,53 @@ class Mail {
|
|
|
62
402
|
from: {value: [{address: this.#from.email, name: this.#from.name}]},
|
|
63
403
|
to: this.#to,
|
|
64
404
|
header: this.#header,
|
|
65
|
-
html:
|
|
66
|
-
text:
|
|
405
|
+
html: htmlContent,
|
|
406
|
+
text: textContent,
|
|
407
|
+
attachments: []
|
|
67
408
|
}
|
|
68
409
|
]
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const socketPath = process.env.ODAC_API_SOCKET || '/var/run/odac.sock'
|
|
413
|
+
|
|
414
|
+
if (Odac.Config.debug) console.log(`[Mail] Connecting to Odac Core via Unix Socket: ${socketPath}...`)
|
|
415
|
+
|
|
416
|
+
client.connect(socketPath, () => {
|
|
417
|
+
if (Odac.Config.debug) console.log('[Mail] Connected to Odac Core. Sending payload...')
|
|
418
|
+
client.write(JSON.stringify(payload))
|
|
419
|
+
})
|
|
420
|
+
|
|
421
|
+
client.on('data', data => {
|
|
422
|
+
if (Odac.Config.debug) console.log('[Mail] Received data from server:', data.toString())
|
|
423
|
+
try {
|
|
424
|
+
const response = JSON.parse(data.toString())
|
|
425
|
+
resolve(response)
|
|
426
|
+
} catch (error) {
|
|
427
|
+
console.error('[Mail] Error parsing response:', error)
|
|
428
|
+
resolve(false)
|
|
429
|
+
}
|
|
430
|
+
client.destroy()
|
|
431
|
+
})
|
|
432
|
+
|
|
433
|
+
client.on('error', error => {
|
|
434
|
+
console.error('[Mail] Socket Error:', error)
|
|
435
|
+
resolve(false)
|
|
436
|
+
})
|
|
437
|
+
|
|
438
|
+
client.on('close', () => {
|
|
439
|
+
if (Odac.Config.debug) console.log('[Mail] Connection closed')
|
|
440
|
+
})
|
|
441
|
+
} catch (error) {
|
|
442
|
+
console.error('[Mail] Unexpected error:', error)
|
|
77
443
|
resolve(false)
|
|
78
|
-
}
|
|
444
|
+
}
|
|
445
|
+
})()
|
|
79
446
|
})
|
|
80
447
|
}
|
|
81
448
|
}
|
|
82
449
|
|
|
83
|
-
module.exports = Mail
|
|
450
|
+
module.exports = new Proxy(Mail, {
|
|
451
|
+
apply(target, thisArg, args) {
|
|
452
|
+
return new target(...args)
|
|
453
|
+
}
|
|
454
|
+
})
|
package/src/Odac.js
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
module.exports = {
|
|
2
2
|
init: async function () {
|
|
3
3
|
global.Odac = this.instance()
|
|
4
|
+
global.Odac.Storage = require('./Storage.js')
|
|
5
|
+
global.Odac.Storage.init()
|
|
6
|
+
|
|
4
7
|
await global.Odac.Env.init()
|
|
5
8
|
await global.Odac.Config.init()
|
|
6
|
-
await global.Odac.
|
|
9
|
+
await global.Odac.Database.init()
|
|
10
|
+
|
|
11
|
+
global.Odac.Ipc = require('./Ipc.js')
|
|
12
|
+
await global.Odac.Ipc.init()
|
|
13
|
+
|
|
7
14
|
await global.Odac.Route.init()
|
|
8
15
|
await global.Odac.Server.init()
|
|
9
16
|
global.Odac.instance = this.instance
|
|
@@ -18,15 +25,50 @@ module.exports = {
|
|
|
18
25
|
_odac.Config = require('./Config.js')
|
|
19
26
|
_odac.Env = require('./Env.js')
|
|
20
27
|
_odac.Mail = (...args) => new (require('./Mail.js'))(...args)
|
|
21
|
-
_odac.
|
|
28
|
+
_odac.Database = require('./Database.js')
|
|
29
|
+
_odac.DB = _odac.Database
|
|
22
30
|
_odac.Route = global.Odac?.Route ?? new (require('./Route.js'))()
|
|
31
|
+
|
|
32
|
+
_odac._ipcSubs = []
|
|
33
|
+
const ipcSingleton = require('./Ipc.js')
|
|
34
|
+
|
|
35
|
+
_odac.Ipc = new Proxy(ipcSingleton, {
|
|
36
|
+
get(target, prop) {
|
|
37
|
+
if (prop === 'subscribe') {
|
|
38
|
+
return async (channel, callback) => {
|
|
39
|
+
const res = await target.subscribe(channel, callback)
|
|
40
|
+
_odac._ipcSubs.push({channel, callback})
|
|
41
|
+
return res
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (prop === 'unsubscribe') {
|
|
45
|
+
return async (channel, callback) => {
|
|
46
|
+
const res = await target.unsubscribe(channel, callback)
|
|
47
|
+
const index = _odac._ipcSubs.findIndex(s => s.channel === channel && s.callback === callback)
|
|
48
|
+
if (index > -1) _odac._ipcSubs.splice(index, 1)
|
|
49
|
+
return res
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const value = target[prop]
|
|
53
|
+
if (typeof value === 'function') return value.bind(target)
|
|
54
|
+
return value
|
|
55
|
+
},
|
|
56
|
+
set(target, prop, value) {
|
|
57
|
+
target[prop] = value
|
|
58
|
+
return true
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
|
|
23
62
|
_odac.Server = require('./Server.js')
|
|
63
|
+
_odac.Storage = require('./Storage.js')
|
|
24
64
|
_odac.Var = (...args) => new (require('./Var.js'))(...args)
|
|
25
65
|
|
|
26
66
|
if (req) {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
67
|
+
if (typeof req === 'object') {
|
|
68
|
+
_odac.Request = new (require('./Request.js'))(id, req, res, _odac)
|
|
69
|
+
_odac.Auth = new (require('./Auth.js'))(_odac.Request)
|
|
70
|
+
_odac.Token = new (require('./Token.js'))(_odac.Request)
|
|
71
|
+
}
|
|
30
72
|
_odac.Lang = new (require('./Lang.js'))(_odac)
|
|
31
73
|
if (res) {
|
|
32
74
|
_odac.View = new (require('./View.js'))(_odac)
|
|
@@ -57,53 +99,76 @@ module.exports = {
|
|
|
57
99
|
_odac.cleanup = function () {
|
|
58
100
|
for (const id of _odac._intervals) clearInterval(id)
|
|
59
101
|
for (const id of _odac._timeouts) clearTimeout(id)
|
|
102
|
+
if (_odac._ipcSubs) {
|
|
103
|
+
for (const sub of _odac._ipcSubs) {
|
|
104
|
+
ipcSingleton.unsubscribe(sub.channel, sub.callback).catch(console.error)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
60
107
|
_odac._intervals = []
|
|
61
108
|
_odac._timeouts = []
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
if (global.Odac?.Route?.class) {
|
|
65
|
-
for (const name in global.Odac.Route.class) {
|
|
66
|
-
const Module = global.Odac.Route.class[name].module
|
|
67
|
-
_odac[name] = typeof Module === 'function' ? new Module(_odac) : Module
|
|
68
|
-
}
|
|
109
|
+
_odac._ipcSubs = []
|
|
69
110
|
}
|
|
70
111
|
|
|
71
112
|
_odac.__ = function (...args) {
|
|
72
113
|
return _odac.Lang.get(...args)
|
|
73
114
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
115
|
+
if (typeof req === 'object') {
|
|
116
|
+
_odac.abort = function (code) {
|
|
117
|
+
return _odac.Request.abort(code)
|
|
118
|
+
}
|
|
119
|
+
_odac.cookie = function (key, value, options) {
|
|
120
|
+
return _odac.Request.cookie(key, value, options)
|
|
121
|
+
}
|
|
122
|
+
_odac.direct = function (url) {
|
|
123
|
+
return _odac.Request.redirect(url)
|
|
124
|
+
}
|
|
82
125
|
}
|
|
83
126
|
_odac.env = function (key, defaultValue) {
|
|
84
127
|
return _odac.Env.get(key, defaultValue)
|
|
85
128
|
}
|
|
86
|
-
|
|
87
|
-
return
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
129
|
+
if (typeof req === 'object') {
|
|
130
|
+
_odac.return = function (data) {
|
|
131
|
+
return _odac.Request.end(data)
|
|
132
|
+
}
|
|
133
|
+
_odac.request = function (key) {
|
|
134
|
+
return _odac.Request.request(key)
|
|
135
|
+
}
|
|
136
|
+
_odac.set = function (key, value) {
|
|
137
|
+
return _odac.Request.set(key, value)
|
|
138
|
+
}
|
|
139
|
+
_odac.session = function (key, value) {
|
|
140
|
+
return _odac.Request.session(key, value)
|
|
141
|
+
}
|
|
142
|
+
_odac.share = function (key, value) {
|
|
143
|
+
return _odac.Request.share(key, value)
|
|
144
|
+
}
|
|
145
|
+
_odac.token = function (hash) {
|
|
146
|
+
return hash ? _odac.Token.check(hash) : _odac.Token.generate()
|
|
147
|
+
}
|
|
148
|
+
_odac.validator = function () {
|
|
149
|
+
return new (require('./Validator.js'))(_odac.Request)
|
|
150
|
+
}
|
|
151
|
+
_odac.write = function (value) {
|
|
152
|
+
return _odac.Request.write(value)
|
|
153
|
+
}
|
|
154
|
+
_odac.stream = function (input) {
|
|
155
|
+
_odac.Request.clearTimeout()
|
|
156
|
+
return new (require('./Stream'))(_odac.Request.req, _odac.Request.res, input, _odac)
|
|
157
|
+
}
|
|
103
158
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
159
|
+
|
|
160
|
+
if (global.Odac?.Route?.class) {
|
|
161
|
+
_odac.App = {}
|
|
162
|
+
for (const name in global.Odac.Route.class) {
|
|
163
|
+
const Module = global.Odac.Route.class[name].module
|
|
164
|
+
const instance = typeof Module === 'function' ? new Module(_odac) : Module
|
|
165
|
+
|
|
166
|
+
if (_odac[name]) {
|
|
167
|
+
_odac.App[name] = instance
|
|
168
|
+
} else {
|
|
169
|
+
_odac[name] = instance
|
|
170
|
+
}
|
|
171
|
+
}
|
|
107
172
|
}
|
|
108
173
|
}
|
|
109
174
|
|