tina4-nodejs 3.0.0-rc.2 → 3.1.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/BENCHMARK_REPORT.md +248 -86
- package/CARBONAH.md +4 -4
- package/CLAUDE.md +16 -1
- package/COMPARISON.md +58 -46
- package/README.md +60 -6
- package/package.json +2 -1
- package/packages/cli/src/bin.ts +8 -0
- package/packages/cli/src/commands/generate.ts +237 -0
- package/packages/core/gallery/queue/meta.json +1 -1
- package/packages/core/gallery/queue/src/lib/queueDb.ts +32 -0
- package/packages/core/gallery/queue/src/routes/api/gallery/queue/consume/post.ts +28 -0
- package/packages/core/gallery/queue/src/routes/api/gallery/queue/fail/post.ts +28 -0
- package/packages/core/gallery/queue/src/routes/api/gallery/queue/produce/post.ts +20 -10
- package/packages/core/gallery/queue/src/routes/api/gallery/queue/retry/post.ts +25 -0
- package/packages/core/gallery/queue/src/routes/api/gallery/queue/status/get.ts +36 -6
- package/packages/core/gallery/queue/src/routes/gallery/queue/get.ts +160 -0
- package/packages/core/src/cache.ts +402 -10
- package/packages/core/src/index.ts +5 -2
- package/packages/core/src/messenger.ts +118 -36
- package/packages/core/src/queue.ts +172 -92
- package/packages/core/src/response.ts +46 -0
- package/packages/core/src/router.ts +94 -1
- package/packages/core/src/server.ts +66 -7
- package/packages/core/src/types.ts +20 -1
- package/packages/core/src/websocketConnection.ts +16 -0
- package/packages/frond/src/engine.ts +184 -6
- package/packages/orm/src/baseModel.ts +274 -20
- package/packages/orm/src/cachedDatabase.ts +180 -0
- package/packages/orm/src/index.ts +4 -0
- package/packages/orm/src/model.ts +1 -0
- package/packages/orm/src/types.ts +75 -0
|
@@ -4,10 +4,24 @@
|
|
|
4
4
|
* Sends email via raw SMTP socket communication (net/tls).
|
|
5
5
|
* No nodemailer, no external dependencies.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
7
|
+
* Unified .env-driven configuration with constructor override.
|
|
8
|
+
* Priority: constructor params > .env (TINA4_MAIL_* with SMTP_* fallback) > sensible defaults
|
|
8
9
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
10
|
+
* // .env
|
|
11
|
+
* // TINA4_MAIL_HOST=smtp.gmail.com
|
|
12
|
+
* // TINA4_MAIL_PORT=587
|
|
13
|
+
* // TINA4_MAIL_USERNAME=user@gmail.com
|
|
14
|
+
* // TINA4_MAIL_PASSWORD=app-password
|
|
15
|
+
* // TINA4_MAIL_FROM=noreply@myapp.com
|
|
16
|
+
* // TINA4_MAIL_ENCRYPTION=tls
|
|
17
|
+
* // TINA4_MAIL_IMAP_HOST=imap.gmail.com
|
|
18
|
+
* // TINA4_MAIL_IMAP_PORT=993
|
|
19
|
+
*
|
|
20
|
+
* import { Messenger } from "@tina4/core";
|
|
21
|
+
*
|
|
22
|
+
* const mail = new Messenger(); // reads from .env
|
|
23
|
+
* const mail = new Messenger({ host: "smtp.office365.com", port: 587 }); // override
|
|
24
|
+
* await mail.send({ to: "user@test.com", subject: "Welcome", body: "<h1>Hello!</h1>", html: true, text: "Hello!" });
|
|
11
25
|
*/
|
|
12
26
|
import net from "node:net";
|
|
13
27
|
import tls from "node:tls";
|
|
@@ -46,6 +60,8 @@ interface MessengerOptions {
|
|
|
46
60
|
password?: string;
|
|
47
61
|
fromAddress?: string;
|
|
48
62
|
fromName?: string;
|
|
63
|
+
encryption?: string;
|
|
64
|
+
/** @deprecated Use encryption instead */
|
|
49
65
|
useTls?: boolean;
|
|
50
66
|
imapHost?: string;
|
|
51
67
|
imapPort?: number;
|
|
@@ -80,8 +96,9 @@ interface SendOptions {
|
|
|
80
96
|
subject: string;
|
|
81
97
|
body: string;
|
|
82
98
|
html?: boolean;
|
|
83
|
-
|
|
84
|
-
|
|
99
|
+
text?: string;
|
|
100
|
+
cc?: string | string[];
|
|
101
|
+
bcc?: string | string[];
|
|
85
102
|
replyTo?: string;
|
|
86
103
|
attachments?: string[];
|
|
87
104
|
headers?: Record<string, string>;
|
|
@@ -152,13 +169,16 @@ function buildMimeMessage(options: {
|
|
|
152
169
|
subject: string;
|
|
153
170
|
body: string;
|
|
154
171
|
html: boolean;
|
|
172
|
+
text?: string;
|
|
155
173
|
replyTo?: string;
|
|
156
174
|
attachments?: string[];
|
|
157
175
|
headers?: Record<string, string>;
|
|
158
176
|
messageId: string;
|
|
159
177
|
}): string {
|
|
160
178
|
const boundary = `----=_Tina4_${Date.now()}_${Math.random().toString(36).substring(2)}`;
|
|
179
|
+
const altBoundary = `----=_Tina4Alt_${Date.now()}_${Math.random().toString(36).substring(2)}`;
|
|
161
180
|
const hasAttachments = options.attachments && options.attachments.length > 0;
|
|
181
|
+
const hasTextAlt = options.text !== undefined && options.html;
|
|
162
182
|
const lines: string[] = [];
|
|
163
183
|
|
|
164
184
|
// Headers
|
|
@@ -190,34 +210,34 @@ function buildMimeMessage(options: {
|
|
|
190
210
|
lines.push(`Content-Type: multipart/mixed; boundary="${boundary}"`);
|
|
191
211
|
lines.push("");
|
|
192
212
|
lines.push(`--${boundary}`);
|
|
193
|
-
}
|
|
194
213
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
lines.push("
|
|
214
|
+
// Body part (with optional text alternative)
|
|
215
|
+
if (hasTextAlt) {
|
|
216
|
+
lines.push(`Content-Type: multipart/alternative; boundary="${altBoundary}"`);
|
|
217
|
+
lines.push("");
|
|
218
|
+
lines.push(`--${altBoundary}`);
|
|
219
|
+
lines.push("Content-Type: text/plain; charset=UTF-8");
|
|
199
220
|
lines.push("Content-Transfer-Encoding: 7bit");
|
|
200
221
|
lines.push("");
|
|
201
|
-
|
|
202
|
-
lines.push("Content-Type: text/html; charset=UTF-8");
|
|
222
|
+
lines.push(options.text!);
|
|
203
223
|
lines.push("");
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
if (hasAttachments) {
|
|
207
|
-
lines.push("Content-Type: text/plain; charset=UTF-8");
|
|
224
|
+
lines.push(`--${altBoundary}`);
|
|
225
|
+
lines.push("Content-Type: text/html; charset=UTF-8");
|
|
208
226
|
lines.push("Content-Transfer-Encoding: 7bit");
|
|
209
227
|
lines.push("");
|
|
228
|
+
lines.push(options.body);
|
|
229
|
+
lines.push("");
|
|
230
|
+
lines.push(`--${altBoundary}--`);
|
|
210
231
|
} else {
|
|
211
|
-
|
|
232
|
+
const contentType = options.html ? "text/html" : "text/plain";
|
|
233
|
+
lines.push(`Content-Type: ${contentType}; charset=UTF-8`);
|
|
234
|
+
lines.push("Content-Transfer-Encoding: 7bit");
|
|
212
235
|
lines.push("");
|
|
236
|
+
lines.push(options.body);
|
|
213
237
|
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
lines.push(options.body);
|
|
217
238
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
for (const filePath of options.attachments) {
|
|
239
|
+
// Attachments
|
|
240
|
+
for (const filePath of options.attachments!) {
|
|
221
241
|
const fileName = basename(filePath);
|
|
222
242
|
const fileData = readFileSync(filePath);
|
|
223
243
|
const base64Data = fileData.toString("base64");
|
|
@@ -237,6 +257,29 @@ function buildMimeMessage(options: {
|
|
|
237
257
|
|
|
238
258
|
lines.push("");
|
|
239
259
|
lines.push(`--${boundary}--`);
|
|
260
|
+
} else if (hasTextAlt) {
|
|
261
|
+
// Text alternative without attachments
|
|
262
|
+
lines.push(`Content-Type: multipart/alternative; boundary="${altBoundary}"`);
|
|
263
|
+
lines.push("");
|
|
264
|
+
lines.push(`--${altBoundary}`);
|
|
265
|
+
lines.push("Content-Type: text/plain; charset=UTF-8");
|
|
266
|
+
lines.push("Content-Transfer-Encoding: 7bit");
|
|
267
|
+
lines.push("");
|
|
268
|
+
lines.push(options.text!);
|
|
269
|
+
lines.push("");
|
|
270
|
+
lines.push(`--${altBoundary}`);
|
|
271
|
+
lines.push("Content-Type: text/html; charset=UTF-8");
|
|
272
|
+
lines.push("Content-Transfer-Encoding: 7bit");
|
|
273
|
+
lines.push("");
|
|
274
|
+
lines.push(options.body);
|
|
275
|
+
lines.push("");
|
|
276
|
+
lines.push(`--${altBoundary}--`);
|
|
277
|
+
} else {
|
|
278
|
+
// Simple message
|
|
279
|
+
const contentType = options.html ? "text/html" : "text/plain";
|
|
280
|
+
lines.push(`Content-Type: ${contentType}; charset=UTF-8`);
|
|
281
|
+
lines.push("");
|
|
282
|
+
lines.push(options.body);
|
|
240
283
|
}
|
|
241
284
|
|
|
242
285
|
return lines.join("\r\n");
|
|
@@ -251,6 +294,7 @@ export class Messenger {
|
|
|
251
294
|
private password: string;
|
|
252
295
|
private fromAddress: string;
|
|
253
296
|
private fromName: string;
|
|
297
|
+
private encryption: string;
|
|
254
298
|
private useTls: boolean;
|
|
255
299
|
private imapHost: string;
|
|
256
300
|
private imapPort: number;
|
|
@@ -258,17 +302,54 @@ export class Messenger {
|
|
|
258
302
|
private imapPass: string;
|
|
259
303
|
|
|
260
304
|
constructor(options?: MessengerOptions) {
|
|
261
|
-
|
|
262
|
-
this.
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
this.
|
|
267
|
-
|
|
268
|
-
this.
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
305
|
+
// Priority: constructor > TINA4_MAIL_* > SMTP_* > sensible default
|
|
306
|
+
this.host = options?.host
|
|
307
|
+
?? process.env.TINA4_MAIL_HOST
|
|
308
|
+
?? process.env.SMTP_HOST
|
|
309
|
+
?? "localhost";
|
|
310
|
+
this.port = options?.port
|
|
311
|
+
?? parseInt(process.env.TINA4_MAIL_PORT ?? process.env.SMTP_PORT ?? "587", 10);
|
|
312
|
+
this.username = options?.username
|
|
313
|
+
?? process.env.TINA4_MAIL_USERNAME
|
|
314
|
+
?? process.env.SMTP_USERNAME
|
|
315
|
+
?? "";
|
|
316
|
+
this.password = options?.password
|
|
317
|
+
?? process.env.TINA4_MAIL_PASSWORD
|
|
318
|
+
?? process.env.SMTP_PASSWORD
|
|
319
|
+
?? "";
|
|
320
|
+
this.fromAddress = options?.fromAddress
|
|
321
|
+
?? process.env.TINA4_MAIL_FROM
|
|
322
|
+
?? process.env.SMTP_FROM
|
|
323
|
+
?? (this.username || "noreply@localhost");
|
|
324
|
+
this.fromName = options?.fromName
|
|
325
|
+
?? process.env.TINA4_MAIL_FROM_NAME
|
|
326
|
+
?? process.env.SMTP_FROM_NAME
|
|
327
|
+
?? "";
|
|
328
|
+
|
|
329
|
+
// Encryption: constructor > .env > backward-compat useTls > default "tls"
|
|
330
|
+
const envEncryption = options?.encryption
|
|
331
|
+
?? process.env.TINA4_MAIL_ENCRYPTION;
|
|
332
|
+
if (envEncryption) {
|
|
333
|
+
this.encryption = envEncryption.toLowerCase();
|
|
334
|
+
} else if (options?.useTls !== undefined) {
|
|
335
|
+
this.encryption = options.useTls ? "tls" : "none";
|
|
336
|
+
} else {
|
|
337
|
+
this.encryption = "tls";
|
|
338
|
+
}
|
|
339
|
+
this.useTls = ["tls", "starttls"].includes(this.encryption);
|
|
340
|
+
|
|
341
|
+
this.imapHost = options?.imapHost
|
|
342
|
+
?? process.env.TINA4_MAIL_IMAP_HOST
|
|
343
|
+
?? process.env.IMAP_HOST
|
|
344
|
+
?? "";
|
|
345
|
+
this.imapPort = options?.imapPort
|
|
346
|
+
?? parseInt(process.env.TINA4_MAIL_IMAP_PORT ?? process.env.IMAP_PORT ?? "993", 10);
|
|
347
|
+
this.imapUser = options?.imapUser
|
|
348
|
+
?? process.env.IMAP_USER
|
|
349
|
+
?? this.username;
|
|
350
|
+
this.imapPass = options?.imapPass
|
|
351
|
+
?? process.env.IMAP_PASS
|
|
352
|
+
?? this.password;
|
|
272
353
|
}
|
|
273
354
|
|
|
274
355
|
/**
|
|
@@ -276,8 +357,8 @@ export class Messenger {
|
|
|
276
357
|
*/
|
|
277
358
|
async send(options: SendOptions): Promise<SendResult> {
|
|
278
359
|
const toList = Array.isArray(options.to) ? options.to : [options.to];
|
|
279
|
-
const ccList = options.cc
|
|
280
|
-
const bccList = options.bcc
|
|
360
|
+
const ccList = Array.isArray(options.cc) ? options.cc : (options.cc ? [options.cc] : []);
|
|
361
|
+
const bccList = Array.isArray(options.bcc) ? options.bcc : (options.bcc ? [options.bcc] : []);
|
|
281
362
|
const allRecipients = [...toList, ...ccList, ...bccList];
|
|
282
363
|
const messageId = `${randomUUID()}@${this.host}`;
|
|
283
364
|
|
|
@@ -401,6 +482,7 @@ export class Messenger {
|
|
|
401
482
|
subject: options.subject,
|
|
402
483
|
body: options.body,
|
|
403
484
|
html: options.html ?? false,
|
|
485
|
+
text: options.text,
|
|
404
486
|
replyTo: options.replyTo,
|
|
405
487
|
attachments: options.attachments,
|
|
406
488
|
headers: options.headers,
|