tina4-nodejs 3.13.91 → 3.13.94
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/CLAUDE.md +16 -3
- package/README.md +1 -1
- package/package.json +12 -9
- package/packages/cli/dist/bin.js +1312 -987
- package/packages/core/dist/index.js +1312 -987
- package/packages/core/src/auth.ts +141 -21
- package/packages/core/src/devMailbox.ts +20 -44
- package/packages/core/src/index.ts +2 -2
- package/packages/core/src/messenger.ts +72 -0
- package/packages/core/src/queueBackends/kafkaBackend.ts +108 -12
- package/packages/core/src/sessionHandlers/childError.ts +72 -0
- package/packages/core/src/sessionHandlers/mongoClient.ts +9 -3
- package/packages/core/src/sessionHandlers/redisHandler.ts +18 -5
- package/packages/core/src/sessionHandlers/respClient.ts +5 -1
- package/packages/frond/dist/index.js +74 -31
- package/packages/frond/src/engine.ts +99 -33
- package/packages/orm/dist/index.js +3102 -2777
- package/packages/orm/src/adapters/sqlite.ts +4 -1
- package/packages/orm/src/database.ts +108 -8
- package/types/cli/src/bin.d.ts +92 -0
- package/types/cli/src/commands/build.d.ts +2 -0
- package/types/cli/src/commands/generate.d.ts +47 -0
- package/types/cli/src/commands/init.d.ts +1 -0
- package/types/cli/src/commands/metrics.d.ts +6 -0
- package/types/cli/src/commands/migrate.d.ts +1 -0
- package/types/cli/src/commands/migrateCreate.d.ts +1 -0
- package/types/cli/src/commands/migrateRollback.d.ts +1 -0
- package/types/cli/src/commands/migrateStatus.d.ts +1 -0
- package/types/cli/src/commands/queue.d.ts +20 -0
- package/types/cli/src/commands/routes.d.ts +1 -0
- package/types/cli/src/commands/seed.d.ts +1 -0
- package/types/cli/src/commands/serve.d.ts +6 -0
- package/types/cli/src/commands/test.d.ts +1 -0
- package/types/core/src/ai.d.ts +64 -0
- package/types/core/src/api.d.ts +262 -0
- package/types/core/src/auth.d.ts +154 -0
- package/types/core/src/authGate.d.ts +20 -0
- package/types/core/src/background.d.ts +34 -0
- package/types/core/src/cache.d.ts +160 -0
- package/types/core/src/constants.d.ts +38 -0
- package/types/core/src/container.d.ts +44 -0
- package/types/core/src/context/chunker.d.ts +31 -0
- package/types/core/src/context/index.d.ts +93 -0
- package/types/core/src/devAdmin.d.ts +179 -0
- package/types/core/src/devMailbox.d.ts +54 -0
- package/types/core/src/docs.d.ts +141 -0
- package/types/core/src/docsAutoDiscovery.d.ts +6 -0
- package/types/core/src/dotenv.d.ts +65 -0
- package/types/core/src/env.d.ts +28 -0
- package/types/core/src/errorOverlay.d.ts +36 -0
- package/types/core/src/events.d.ts +75 -0
- package/types/core/src/fakeData.d.ts +55 -0
- package/types/core/src/feedback.d.ts +90 -0
- package/types/core/src/graphql.d.ts +207 -0
- package/types/core/src/health.d.ts +22 -0
- package/types/core/src/htmlElement.d.ts +75 -0
- package/types/core/src/i18n.d.ts +37 -0
- package/types/core/src/index.d.ts +93 -0
- package/types/core/src/job.d.ts +39 -0
- package/types/core/src/logger.d.ts +123 -0
- package/types/core/src/mcp.d.ts +248 -0
- package/types/core/src/messenger.d.ts +191 -0
- package/types/core/src/metrics.d.ts +77 -0
- package/types/core/src/middleware.d.ts +207 -0
- package/types/core/src/mqtt.d.ts +257 -0
- package/types/core/src/mqttMessage.d.ts +67 -0
- package/types/core/src/plan.d.ts +96 -0
- package/types/core/src/projectIndex.d.ts +56 -0
- package/types/core/src/queue.d.ts +219 -0
- package/types/core/src/queueBackends/kafkaBackend.d.ts +117 -0
- package/types/core/src/queueBackends/liteBackend.d.ts +119 -0
- package/types/core/src/queueBackends/mongoBackend.d.ts +97 -0
- package/types/core/src/queueBackends/rabbitmqBackend.d.ts +55 -0
- package/types/core/src/rateLimiter.d.ts +49 -0
- package/types/core/src/request.d.ts +25 -0
- package/types/core/src/response.d.ts +28 -0
- package/types/core/src/routeDiscovery.d.ts +12 -0
- package/types/core/src/router.d.ts +355 -0
- package/types/core/src/scss.d.ts +19 -0
- package/types/core/src/server.d.ts +131 -0
- package/types/core/src/service.d.ts +115 -0
- package/types/core/src/session.d.ts +256 -0
- package/types/core/src/sessionHandlers/childError.d.ts +34 -0
- package/types/core/src/sessionHandlers/databaseHandler.d.ts +42 -0
- package/types/core/src/sessionHandlers/mongoClient.d.ts +24 -0
- package/types/core/src/sessionHandlers/mongoHandler.d.ts +61 -0
- package/types/core/src/sessionHandlers/redisHandler.d.ts +60 -0
- package/types/core/src/sessionHandlers/respClient.d.ts +22 -0
- package/types/core/src/sessionHandlers/valkeyHandler.d.ts +65 -0
- package/types/core/src/static.d.ts +2 -0
- package/types/core/src/test.d.ts +94 -0
- package/types/core/src/testClient.d.ts +36 -0
- package/types/core/src/testing.d.ts +58 -0
- package/types/core/src/types.d.ts +219 -0
- package/types/core/src/validator.d.ts +52 -0
- package/types/core/src/websocket.d.ts +376 -0
- package/types/core/src/websocketBackplane.d.ts +166 -0
- package/types/core/src/websocketConnection.d.ts +54 -0
- package/types/core/src/wsdl.d.ts +101 -0
- package/types/frond/src/engine.d.ts +263 -0
- package/types/frond/src/index.d.ts +2 -0
- package/types/orm/src/adapters/firebird.d.ts +138 -0
- package/types/orm/src/adapters/mongodb.d.ts +81 -0
- package/types/orm/src/adapters/mssql.d.ts +70 -0
- package/types/orm/src/adapters/mysql.d.ts +66 -0
- package/types/orm/src/adapters/odbc.d.ts +97 -0
- package/types/orm/src/adapters/postgres.d.ts +85 -0
- package/types/orm/src/adapters/sqlite.d.ts +56 -0
- package/types/orm/src/autoCrud.d.ts +73 -0
- package/types/orm/src/baseModel.d.ts +391 -0
- package/types/orm/src/cachedDatabase.d.ts +177 -0
- package/types/orm/src/database.d.ts +609 -0
- package/types/orm/src/databaseResult.d.ts +85 -0
- package/types/orm/src/docstore.d.ts +182 -0
- package/types/orm/src/fakeData.d.ts +22 -0
- package/types/orm/src/index.d.ts +40 -0
- package/types/orm/src/migration.d.ts +275 -0
- package/types/orm/src/model.d.ts +7 -0
- package/types/orm/src/query.d.ts +14 -0
- package/types/orm/src/queryBuilder.d.ts +173 -0
- package/types/orm/src/realtime/index.d.ts +7 -0
- package/types/orm/src/realtime/models/attachment.d.ts +43 -0
- package/types/orm/src/realtime/models/channel.d.ts +32 -0
- package/types/orm/src/realtime/models/channelMember.d.ts +32 -0
- package/types/orm/src/realtime/models/message.d.ts +36 -0
- package/types/orm/src/realtime/models/workspace.d.ts +26 -0
- package/types/orm/src/realtime/realtime.d.ts +24 -0
- package/types/orm/src/realtime/storage.d.ts +61 -0
- package/types/orm/src/seeder.d.ts +118 -0
- package/types/orm/src/sqlTranslator.d.ts +134 -0
- package/types/orm/src/types.d.ts +138 -0
- package/types/orm/src/validation.d.ts +6 -0
- package/types/swagger/src/generator.d.ts +46 -0
- package/types/swagger/src/index.d.ts +2 -0
- package/types/swagger/src/ui.d.ts +11 -0
package/packages/cli/dist/bin.js
CHANGED
|
@@ -1298,11 +1298,14 @@ var init_sqlite = __esm({
|
|
|
1298
1298
|
const pragma = schema && isIdentifier(schema) && isIdentifier(tbl) ? `PRAGMA ${schema}.table_info("${tbl}")` : `PRAGMA table_info("${table2}")`;
|
|
1299
1299
|
const rows = this.db.prepare(pragma).all();
|
|
1300
1300
|
return rows.map((r) => ({
|
|
1301
|
+
// PRAGMA table_info reports `pk` as the 1-BASED POSITION within the primary
|
|
1302
|
+
// key, not a boolean: a composite key gives pk=1, pk=2, ... Testing `=== 1`
|
|
1303
|
+
// reported only the first column of a composite key.
|
|
1301
1304
|
name: r.name,
|
|
1302
1305
|
type: r.type,
|
|
1303
1306
|
nullable: r.notnull === 0,
|
|
1304
1307
|
default: r.dflt_value,
|
|
1305
|
-
primaryKey: r.pk
|
|
1308
|
+
primaryKey: Number(r.pk) > 0
|
|
1306
1309
|
}));
|
|
1307
1310
|
}
|
|
1308
1311
|
lastInsertId() {
|
|
@@ -4298,6 +4301,8 @@ var init_database = __esm({
|
|
|
4298
4301
|
poolIndex = 0;
|
|
4299
4302
|
/** Factory for creating new adapters (used by pool) */
|
|
4300
4303
|
adapterFactory = null;
|
|
4304
|
+
/** table -> primary-key column name (or null), introspected once */
|
|
4305
|
+
_pkCache = /* @__PURE__ */ new Map();
|
|
4301
4306
|
/**
|
|
4302
4307
|
* Whether a standalone write auto-commits. ON by default — a write made
|
|
4303
4308
|
* outside an explicit transaction commits on its own connection before
|
|
@@ -4556,29 +4561,112 @@ var init_database = __esm({
|
|
|
4556
4561
|
}
|
|
4557
4562
|
return result;
|
|
4558
4563
|
}
|
|
4559
|
-
/**
|
|
4564
|
+
/**
|
|
4565
|
+
* The table's primary-key column, introspected once and cached.
|
|
4566
|
+
*
|
|
4567
|
+
* Uses the cross-engine getColumns() contract (v3.13.14, #48), which reports
|
|
4568
|
+
* primaryKey per column on every adapter. Resolves to null when the table has
|
|
4569
|
+
* no primary key or cannot be introspected.
|
|
4570
|
+
*/
|
|
4571
|
+
async primaryKey(table2) {
|
|
4572
|
+
if (!this._pkCache.has(table2)) {
|
|
4573
|
+
let pk = [];
|
|
4574
|
+
try {
|
|
4575
|
+
const columns = await this.getColumns(table2);
|
|
4576
|
+
pk = columns.filter((c) => c.primaryKey).map((c) => c.name);
|
|
4577
|
+
} catch {
|
|
4578
|
+
pk = [];
|
|
4579
|
+
}
|
|
4580
|
+
this._pkCache.set(table2, pk);
|
|
4581
|
+
}
|
|
4582
|
+
return this._pkCache.get(table2) ?? [];
|
|
4583
|
+
}
|
|
4584
|
+
/**
|
|
4585
|
+
* A failed write must be loud.
|
|
4586
|
+
*
|
|
4587
|
+
* The adapters catch a SQL error and return { success: false, affectedRows: 0 },
|
|
4588
|
+
* so a filterless update produced invalid SQL ("... WHERE ") and reported
|
|
4589
|
+
* nothing rather than raising. A caller who does not inspect the result
|
|
4590
|
+
* believes the write landed (audit feature 4, P1).
|
|
4591
|
+
*/
|
|
4592
|
+
static assertWrote(result, verb, table2) {
|
|
4593
|
+
if (result && result.success === false) {
|
|
4594
|
+
throw new Error(
|
|
4595
|
+
`${verb} failed on ${table2}: ${result.error ?? "unknown error"}`
|
|
4596
|
+
);
|
|
4597
|
+
}
|
|
4598
|
+
return result;
|
|
4599
|
+
}
|
|
4600
|
+
/**
|
|
4601
|
+
* Update rows. A write with no filter is an error, not a full-table write.
|
|
4602
|
+
*
|
|
4603
|
+
* With no explicit filter the primary key is taken out of `data` and used as
|
|
4604
|
+
* the WHERE clause. With neither a filter nor a primary key in `data` this
|
|
4605
|
+
* throws rather than silently changing nothing (audit feature 4, P1).
|
|
4606
|
+
*/
|
|
4560
4607
|
async update(table2, data, filter, params) {
|
|
4608
|
+
let effectiveFilter = filter ?? {};
|
|
4609
|
+
let effectiveData = data;
|
|
4610
|
+
if (Object.keys(effectiveFilter).length === 0) {
|
|
4611
|
+
const pkColumns = await this.primaryKey(table2);
|
|
4612
|
+
const missing = pkColumns.filter((c) => !(c in data));
|
|
4613
|
+
if (pkColumns.length === 0 || missing.length > 0) {
|
|
4614
|
+
throw new Error(
|
|
4615
|
+
`update requires a filter or the complete primary key in the data; pass filter explicitly to update multiple rows (table=${table2}, primary key=[${pkColumns.join(", ")}], missing from data=[${missing.join(", ")}]). To empty a table use truncate(${table2}).`
|
|
4616
|
+
);
|
|
4617
|
+
}
|
|
4618
|
+
effectiveData = { ...data };
|
|
4619
|
+
const keyed = {};
|
|
4620
|
+
for (const col of pkColumns) {
|
|
4621
|
+
keyed[col] = effectiveData[col];
|
|
4622
|
+
delete effectiveData[col];
|
|
4623
|
+
}
|
|
4624
|
+
if (Object.keys(effectiveData).length === 0) {
|
|
4625
|
+
throw new Error(
|
|
4626
|
+
`update was given only the primary key [${pkColumns.join(", ")}] and no columns to set (table=${table2})`
|
|
4627
|
+
);
|
|
4628
|
+
}
|
|
4629
|
+
effectiveFilter = keyed;
|
|
4630
|
+
}
|
|
4561
4631
|
const adapter = this.getNextAdapter();
|
|
4562
|
-
const result = adapter.updateAsync ? await adapter.updateAsync(table2,
|
|
4632
|
+
const result = adapter.updateAsync ? await adapter.updateAsync(table2, effectiveData, effectiveFilter, params) : adapter.update(table2, effectiveData, effectiveFilter, params);
|
|
4563
4633
|
if (this.autoCommit && !this.inExplicitTransaction()) {
|
|
4564
4634
|
try {
|
|
4565
4635
|
await adapterCommit(adapter);
|
|
4566
4636
|
} catch {
|
|
4567
4637
|
}
|
|
4568
4638
|
}
|
|
4569
|
-
return result;
|
|
4639
|
+
return _Database.assertWrote(result, "update", table2);
|
|
4570
4640
|
}
|
|
4571
|
-
/** Delete rows
|
|
4641
|
+
/** Delete rows. A filterless delete throws; use truncate() to empty a table. */
|
|
4572
4642
|
async delete(table2, filter, params) {
|
|
4643
|
+
const effectiveFilter = filter ?? {};
|
|
4644
|
+
if (!Array.isArray(effectiveFilter) && typeof effectiveFilter !== "string" && Object.keys(effectiveFilter).length === 0) {
|
|
4645
|
+
throw new Error(
|
|
4646
|
+
`delete requires a filter (table=${table2}). To remove every row use truncate(${table2}).`
|
|
4647
|
+
);
|
|
4648
|
+
}
|
|
4573
4649
|
const adapter = this.getNextAdapter();
|
|
4574
|
-
const result = adapter.deleteAsync ? await adapter.deleteAsync(table2,
|
|
4650
|
+
const result = adapter.deleteAsync ? await adapter.deleteAsync(table2, effectiveFilter, params) : adapter.delete(table2, effectiveFilter, params);
|
|
4575
4651
|
if (this.autoCommit && !this.inExplicitTransaction()) {
|
|
4576
4652
|
try {
|
|
4577
4653
|
await adapterCommit(adapter);
|
|
4578
4654
|
} catch {
|
|
4579
4655
|
}
|
|
4580
4656
|
}
|
|
4581
|
-
return result;
|
|
4657
|
+
return _Database.assertWrote(result, "delete", table2);
|
|
4658
|
+
}
|
|
4659
|
+
/** Remove every row. The explicit spelling of a whole-table delete. */
|
|
4660
|
+
async truncate(table2) {
|
|
4661
|
+
const adapter = this.getNextAdapter();
|
|
4662
|
+
const result = adapter.deleteAsync ? await adapter.deleteAsync(table2, "1 = 1", []) : adapter.delete(table2, "1 = 1", []);
|
|
4663
|
+
if (this.autoCommit && !this.inExplicitTransaction()) {
|
|
4664
|
+
try {
|
|
4665
|
+
await adapterCommit(adapter);
|
|
4666
|
+
} catch {
|
|
4667
|
+
}
|
|
4668
|
+
}
|
|
4669
|
+
return _Database.assertWrote(result, "truncate", table2);
|
|
4582
4670
|
}
|
|
4583
4671
|
/** Close all database connections (pool or single). */
|
|
4584
4672
|
close() {
|
|
@@ -12122,6 +12210,7 @@ var init_logger = __esm({
|
|
|
12122
12210
|
var auth_exports = {};
|
|
12123
12211
|
__export(auth_exports, {
|
|
12124
12212
|
Auth: () => Auth,
|
|
12213
|
+
JWT_LEEWAY_SECONDS: () => JWT_LEEWAY_SECONDS,
|
|
12125
12214
|
authMiddleware: () => authMiddleware,
|
|
12126
12215
|
authenticateRequest: () => authenticateRequest,
|
|
12127
12216
|
checkPassword: () => checkPassword,
|
|
@@ -12130,6 +12219,7 @@ __export(auth_exports, {
|
|
|
12130
12219
|
getToken: () => getToken,
|
|
12131
12220
|
hashPassword: () => hashPassword,
|
|
12132
12221
|
refreshToken: () => refreshToken,
|
|
12222
|
+
resolveAlgorithm: () => resolveAlgorithm,
|
|
12133
12223
|
validToken: () => validToken,
|
|
12134
12224
|
validateApiKey: () => validateApiKey
|
|
12135
12225
|
});
|
|
@@ -12190,6 +12280,18 @@ function ensureDevSecret(cwd) {
|
|
|
12190
12280
|
}
|
|
12191
12281
|
return newSecret;
|
|
12192
12282
|
}
|
|
12283
|
+
function unsupportedAlgorithmError(algorithm) {
|
|
12284
|
+
return new Error(
|
|
12285
|
+
`Unsupported JWT algorithm "${algorithm}". Tina4 signs with ${SUPPORTED_ALGORITHMS.join(", ")} (HMAC via node:crypto; RS256 needs a PEM key pair). Set TINA4_JWT_ALGORITHM to one of those.`
|
|
12286
|
+
);
|
|
12287
|
+
}
|
|
12288
|
+
function resolveAlgorithm(algorithm) {
|
|
12289
|
+
const chosen = (algorithm || process.env.TINA4_JWT_ALGORITHM || "HS256").trim();
|
|
12290
|
+
if (!HMAC_DIGESTS.has(chosen) && !RSA_SIGN_ALGORITHMS.has(chosen)) {
|
|
12291
|
+
throw unsupportedAlgorithmError(chosen);
|
|
12292
|
+
}
|
|
12293
|
+
return chosen;
|
|
12294
|
+
}
|
|
12193
12295
|
function base64urlEncode(data) {
|
|
12194
12296
|
return data.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
12195
12297
|
}
|
|
@@ -12212,7 +12314,7 @@ function getToken(payload, secretOrExpiresIn, expiresIn = 60, algorithm) {
|
|
|
12212
12314
|
if (!resolvedSecret) {
|
|
12213
12315
|
_warnBlankSecret();
|
|
12214
12316
|
}
|
|
12215
|
-
const resolvedAlgorithm = algorithm
|
|
12317
|
+
const resolvedAlgorithm = resolveAlgorithm(algorithm);
|
|
12216
12318
|
const header = { alg: resolvedAlgorithm, typ: "JWT" };
|
|
12217
12319
|
const now = Math.floor(Date.now() / 1e3);
|
|
12218
12320
|
const claims = { ...payload, iat: now };
|
|
@@ -12230,19 +12332,27 @@ function validToken(token, secret, algorithm) {
|
|
|
12230
12332
|
if (!resolvedSecret) {
|
|
12231
12333
|
_warnBlankSecret();
|
|
12232
12334
|
}
|
|
12233
|
-
const resolvedAlgorithm = algorithm
|
|
12335
|
+
const resolvedAlgorithm = resolveAlgorithm(algorithm);
|
|
12234
12336
|
try {
|
|
12235
12337
|
const parts = token.split(".");
|
|
12236
12338
|
if (parts.length !== 3) return null;
|
|
12237
12339
|
const [h, p, sig] = parts;
|
|
12340
|
+
const header = JSON.parse(base64urlDecode(h).toString());
|
|
12341
|
+
if (header.alg !== resolvedAlgorithm) return null;
|
|
12238
12342
|
const signingInput = `${h}.${p}`;
|
|
12239
12343
|
if (!verifySignature(signingInput, sig, resolvedSecret, resolvedAlgorithm)) {
|
|
12240
12344
|
return null;
|
|
12241
12345
|
}
|
|
12242
12346
|
const payload = JSON.parse(base64urlDecode(p).toString());
|
|
12243
|
-
|
|
12347
|
+
const now = Date.now() / 1e3;
|
|
12348
|
+
if (typeof payload.exp === "number" && now > payload.exp) {
|
|
12244
12349
|
return null;
|
|
12245
12350
|
}
|
|
12351
|
+
if (Object.hasOwn(payload, "nbf")) {
|
|
12352
|
+
const notBefore = payload.nbf;
|
|
12353
|
+
if (typeof notBefore !== "number" || !Number.isFinite(notBefore)) return null;
|
|
12354
|
+
if (now + JWT_LEEWAY_SECONDS < notBefore) return null;
|
|
12355
|
+
}
|
|
12246
12356
|
return payload;
|
|
12247
12357
|
} catch {
|
|
12248
12358
|
return null;
|
|
@@ -12258,32 +12368,33 @@ function getPayload(token) {
|
|
|
12258
12368
|
}
|
|
12259
12369
|
}
|
|
12260
12370
|
function sign(input, secret, algorithm) {
|
|
12261
|
-
|
|
12262
|
-
|
|
12263
|
-
return base64urlEncode(
|
|
12371
|
+
const digest = HMAC_DIGESTS.get(algorithm);
|
|
12372
|
+
if (digest) {
|
|
12373
|
+
return base64urlEncode(createHmac2(digest, secret).update(input).digest());
|
|
12264
12374
|
}
|
|
12265
|
-
|
|
12266
|
-
|
|
12375
|
+
const rsaAlgorithm = RSA_SIGN_ALGORITHMS.get(algorithm);
|
|
12376
|
+
if (rsaAlgorithm) {
|
|
12377
|
+
const signer = createSign(rsaAlgorithm);
|
|
12267
12378
|
signer.update(input);
|
|
12268
|
-
|
|
12269
|
-
return base64urlEncode(sig);
|
|
12379
|
+
return base64urlEncode(signer.sign(secret));
|
|
12270
12380
|
}
|
|
12271
|
-
throw
|
|
12381
|
+
throw unsupportedAlgorithmError(algorithm);
|
|
12272
12382
|
}
|
|
12273
12383
|
function verifySignature(input, sig, secret, algorithm) {
|
|
12274
|
-
if (algorithm
|
|
12384
|
+
if (HMAC_DIGESTS.has(algorithm)) {
|
|
12275
12385
|
const expected = sign(input, secret, algorithm);
|
|
12276
12386
|
const a = Buffer.from(sig);
|
|
12277
12387
|
const b = Buffer.from(expected);
|
|
12278
12388
|
if (a.length !== b.length) return false;
|
|
12279
12389
|
return timingSafeEqual(a, b);
|
|
12280
12390
|
}
|
|
12281
|
-
|
|
12282
|
-
|
|
12391
|
+
const rsaAlgorithm = RSA_SIGN_ALGORITHMS.get(algorithm);
|
|
12392
|
+
if (rsaAlgorithm) {
|
|
12393
|
+
const verifier = createVerify(rsaAlgorithm);
|
|
12283
12394
|
verifier.update(input);
|
|
12284
12395
|
return verifier.verify(secret, base64urlDecode(sig));
|
|
12285
12396
|
}
|
|
12286
|
-
throw
|
|
12397
|
+
throw unsupportedAlgorithmError(algorithm);
|
|
12287
12398
|
}
|
|
12288
12399
|
function hashPassword(password, salt, iterations = 26e4) {
|
|
12289
12400
|
const actualSalt = salt ?? randomBytes3(16).toString("hex");
|
|
@@ -12308,7 +12419,7 @@ function checkPassword(password, hash) {
|
|
|
12308
12419
|
return false;
|
|
12309
12420
|
}
|
|
12310
12421
|
}
|
|
12311
|
-
function authMiddleware(secret, algorithm
|
|
12422
|
+
function authMiddleware(secret, algorithm) {
|
|
12312
12423
|
return (req2, res, next) => {
|
|
12313
12424
|
const authHeader = req2.headers.authorization ?? "";
|
|
12314
12425
|
if (!authHeader.startsWith("Bearer ")) {
|
|
@@ -12331,11 +12442,11 @@ function refreshToken(token, expiresIn = 60) {
|
|
|
12331
12442
|
const { iat: _iat, exp: _exp, ...claims } = payload;
|
|
12332
12443
|
return getToken(claims, expiresIn);
|
|
12333
12444
|
}
|
|
12334
|
-
function authenticateRequest(headers, secret, algorithm
|
|
12445
|
+
function authenticateRequest(headers, secret, algorithm) {
|
|
12335
12446
|
const authHeader = headers.authorization ?? headers.Authorization ?? "";
|
|
12336
12447
|
if (!authHeader.startsWith("Bearer ")) return null;
|
|
12337
12448
|
const token = authHeader.slice(7);
|
|
12338
|
-
if (validToken(token)) return getPayload(token);
|
|
12449
|
+
if (validToken(token, secret, algorithm)) return getPayload(token);
|
|
12339
12450
|
if (validateApiKey(token)) {
|
|
12340
12451
|
return { _auth: "api_key" };
|
|
12341
12452
|
}
|
|
@@ -12349,12 +12460,23 @@ function validateApiKey(provided, expected) {
|
|
|
12349
12460
|
if (a.length !== b.length) return false;
|
|
12350
12461
|
return timingSafeEqual(a, b);
|
|
12351
12462
|
}
|
|
12352
|
-
var BLANK_SECRET_WARNING, Auth;
|
|
12463
|
+
var BLANK_SECRET_WARNING, HMAC_DIGESTS, RSA_SIGN_ALGORITHMS, SUPPORTED_ALGORITHMS, JWT_LEEWAY_SECONDS, Auth;
|
|
12353
12464
|
var init_auth = __esm({
|
|
12354
12465
|
"../core/src/auth.ts"() {
|
|
12355
12466
|
"use strict";
|
|
12356
12467
|
init_dotenv();
|
|
12357
12468
|
BLANK_SECRET_WARNING = "Auth: TINA4_SECRET is not set \u2014 JWT signing is insecure. Set TINA4_SECRET to a random value (e.g. `openssl rand -hex 32`) in your environment or .env before serving traffic. For LOCAL DEV, set TINA4_DEBUG=true and a per-machine secret is generated automatically into .env.local (gitignored). Seeing this warning means the run was NOT detected as dev \u2014 typically a container or CI without TINA4_DEBUG set, or TINA4_ENV=production.";
|
|
12469
|
+
HMAC_DIGESTS = /* @__PURE__ */ new Map([
|
|
12470
|
+
["HS256", "sha256"],
|
|
12471
|
+
["HS384", "sha384"],
|
|
12472
|
+
["HS512", "sha512"]
|
|
12473
|
+
]);
|
|
12474
|
+
RSA_SIGN_ALGORITHMS = /* @__PURE__ */ new Map([["RS256", "RSA-SHA256"]]);
|
|
12475
|
+
SUPPORTED_ALGORITHMS = [
|
|
12476
|
+
...HMAC_DIGESTS.keys(),
|
|
12477
|
+
...RSA_SIGN_ALGORITHMS.keys()
|
|
12478
|
+
];
|
|
12479
|
+
JWT_LEEWAY_SECONDS = 60;
|
|
12358
12480
|
Auth = class {
|
|
12359
12481
|
static getToken = getToken;
|
|
12360
12482
|
static validToken = validToken;
|
|
@@ -13793,7 +13915,7 @@ function _generateFormToken(descriptor = "") {
|
|
|
13793
13915
|
function _generateFormTokenValue(descriptor = "") {
|
|
13794
13916
|
return new SafeString(_buildFormTokenJwt(descriptor));
|
|
13795
13917
|
}
|
|
13796
|
-
var SafeString, KNOWN_TAGS, TERMINATOR_TAGS, JSON_UNSAFE_RE, JSON_UNSAFE_MAP, NUMERIC_RE, METHOD_CALL_RE, FN_CALL_RE, IS_NOT_RE, IS_RE, NOT_IN_RE, IN_RE, DIVISIBLE_BY_RE, FILTER_WITH_ARGS_RE, FILTER_COMPARISON_RE, TITLE_WORD_RE, STRIP_TAGS_RE, FORMAT_RE, LEADING_WS_RE, TRAILING_WS_RE, THOUSANDS_RE, LIVE_RE, LIVE_WS_RE, LIVE_SRC_RE, filterChainCache, pathParseCache, TEMPLATE_CACHE_MAX, TOKEN_RE, RAW_BLOCK_RE, VarRef, BUILTIN_FILTERS, _formTokenSessionId, Frond;
|
|
13918
|
+
var SafeString, KNOWN_TAGS, TERMINATOR_TAGS, GATEABLE_TAGS, BLOCK_TAG_ENDS, JSON_UNSAFE_RE, JSON_UNSAFE_MAP, NUMERIC_RE, METHOD_CALL_RE, FN_CALL_RE, IS_NOT_RE, IS_RE, NOT_IN_RE, IN_RE, DIVISIBLE_BY_RE, FILTER_WITH_ARGS_RE, FILTER_COMPARISON_RE, TITLE_WORD_RE, STRIP_TAGS_RE, FORMAT_RE, LEADING_WS_RE, TRAILING_WS_RE, THOUSANDS_RE, LIVE_RE, LIVE_WS_RE, LIVE_SRC_RE, filterChainCache, pathParseCache, TEMPLATE_CACHE_MAX, TOKEN_RE, RAW_BLOCK_RE, VarRef, BUILTIN_FILTERS, _formTokenSessionId, Frond;
|
|
13797
13919
|
var init_engine = __esm({
|
|
13798
13920
|
"../frond/src/engine.ts"() {
|
|
13799
13921
|
"use strict";
|
|
@@ -13836,6 +13958,29 @@ var init_engine = __esm({
|
|
|
13836
13958
|
"endset",
|
|
13837
13959
|
"endspaceless"
|
|
13838
13960
|
]);
|
|
13961
|
+
GATEABLE_TAGS = /* @__PURE__ */ new Set([
|
|
13962
|
+
"autoescape",
|
|
13963
|
+
"cache",
|
|
13964
|
+
"for",
|
|
13965
|
+
"from",
|
|
13966
|
+
"if",
|
|
13967
|
+
"import",
|
|
13968
|
+
"include",
|
|
13969
|
+
"live",
|
|
13970
|
+
"macro",
|
|
13971
|
+
"set",
|
|
13972
|
+
"spaceless"
|
|
13973
|
+
]);
|
|
13974
|
+
BLOCK_TAG_ENDS = {
|
|
13975
|
+
autoescape: "endautoescape",
|
|
13976
|
+
cache: "endcache",
|
|
13977
|
+
for: "endfor",
|
|
13978
|
+
if: "endif",
|
|
13979
|
+
live: "endlive",
|
|
13980
|
+
macro: "endmacro",
|
|
13981
|
+
set: "endset",
|
|
13982
|
+
spaceless: "endspaceless"
|
|
13983
|
+
};
|
|
13839
13984
|
JSON_UNSAFE_RE = /[<>&'\u2028\u2029]/g;
|
|
13840
13985
|
JSON_UNSAFE_MAP = {
|
|
13841
13986
|
"<": "\\u003c",
|
|
@@ -14471,42 +14616,27 @@ var init_engine = __esm({
|
|
|
14471
14616
|
if (stripA && i + 1 < tokens.length && tokens[i + 1][0] === "TEXT") {
|
|
14472
14617
|
tokens[i + 1] = ["TEXT", tokens[i + 1][1].replace(LEADING_WS_RE, "")];
|
|
14473
14618
|
}
|
|
14474
|
-
if (tag
|
|
14475
|
-
|
|
14476
|
-
|
|
14477
|
-
|
|
14478
|
-
|
|
14479
|
-
|
|
14480
|
-
output.push(result);
|
|
14481
|
-
i = skip;
|
|
14482
|
-
}
|
|
14619
|
+
if (!this.tagPermitted(tag)) {
|
|
14620
|
+
i = this.skipDeniedTag(tokens, i, tag, content);
|
|
14621
|
+
} else if (tag === "if") {
|
|
14622
|
+
const [result, skip] = this.handleIf(tokens, i, context);
|
|
14623
|
+
output.push(result);
|
|
14624
|
+
i = skip;
|
|
14483
14625
|
} else if (tag === "for") {
|
|
14484
|
-
|
|
14485
|
-
|
|
14486
|
-
|
|
14487
|
-
} else {
|
|
14488
|
-
const [result, skip] = this.handleFor(tokens, i, context);
|
|
14489
|
-
output.push(result);
|
|
14490
|
-
i = skip;
|
|
14491
|
-
}
|
|
14626
|
+
const [result, skip] = this.handleFor(tokens, i, context);
|
|
14627
|
+
output.push(result);
|
|
14628
|
+
i = skip;
|
|
14492
14629
|
} else if (tag === "set") {
|
|
14493
|
-
|
|
14494
|
-
if (this._sandbox && this._allowedTags !== null && !this._allowedTags.has("set")) {
|
|
14495
|
-
i = isBlockSet ? this.skipBlock(tokens, i, "set", "endset") : i + 1;
|
|
14496
|
-
} else if (isBlockSet) {
|
|
14630
|
+
if (!content.includes("=")) {
|
|
14497
14631
|
i = this.handleSetBlock(tokens, i, context);
|
|
14498
14632
|
} else {
|
|
14499
14633
|
this.handleSet(content, context);
|
|
14500
14634
|
i++;
|
|
14501
14635
|
}
|
|
14502
14636
|
} else if (tag === "include") {
|
|
14503
|
-
|
|
14504
|
-
|
|
14505
|
-
|
|
14506
|
-
const result = this.handleInclude(content, context);
|
|
14507
|
-
output.push(result);
|
|
14508
|
-
i++;
|
|
14509
|
-
}
|
|
14637
|
+
const result = this.handleInclude(content, context);
|
|
14638
|
+
output.push(result);
|
|
14639
|
+
i++;
|
|
14510
14640
|
} else if (tag === "macro") {
|
|
14511
14641
|
const skip = this.handleMacro(tokens, i, context);
|
|
14512
14642
|
i = skip;
|
|
@@ -14551,6 +14681,41 @@ var init_engine = __esm({
|
|
|
14551
14681
|
}
|
|
14552
14682
|
return output.join("");
|
|
14553
14683
|
}
|
|
14684
|
+
/**
|
|
14685
|
+
* May this filter RUN under the current sandbox?
|
|
14686
|
+
*
|
|
14687
|
+
* The escaping decision has to ask this rather than read the filter name out of
|
|
14688
|
+
* the source. Node carries safety as a FLAG rather than as a value-level marker
|
|
14689
|
+
* (Python and Ruby return a SafeString, PHP prepends a RAW_MARKER -- all three
|
|
14690
|
+
* produced only by actually running the filter), so here the name alone was
|
|
14691
|
+
* enough to suppress auto-escaping even when the filter was denied and skipped.
|
|
14692
|
+
*/
|
|
14693
|
+
filterPermitted(name) {
|
|
14694
|
+
if (!this._sandbox || this._allowedFilters === null) return true;
|
|
14695
|
+
return this._allowedFilters.has(name);
|
|
14696
|
+
}
|
|
14697
|
+
/**
|
|
14698
|
+
* May this tag run under the current sandbox?
|
|
14699
|
+
*
|
|
14700
|
+
* One gate for every tag, so the allow-list governs the whole tag vocabulary
|
|
14701
|
+
* instead of the four names that happened to be checked individually.
|
|
14702
|
+
*/
|
|
14703
|
+
tagPermitted(tag) {
|
|
14704
|
+
if (!this._sandbox || this._allowedTags === null) return true;
|
|
14705
|
+
if (!GATEABLE_TAGS.has(tag)) return true;
|
|
14706
|
+
return this._allowedTags.has(tag);
|
|
14707
|
+
}
|
|
14708
|
+
/**
|
|
14709
|
+
* Consume a denied tag WITHOUT running it, returning the index past its body.
|
|
14710
|
+
*
|
|
14711
|
+
* Advancing a single token past a body-owning tag would leave the body's tokens
|
|
14712
|
+
* to render at the TOP level, leaking exactly the content the sandbox denied.
|
|
14713
|
+
*/
|
|
14714
|
+
skipDeniedTag(tokens, start2, tag, content) {
|
|
14715
|
+
const closeTag = BLOCK_TAG_ENDS[tag];
|
|
14716
|
+
if (closeTag === void 0 || tag === "set" && content.includes("=")) return start2 + 1;
|
|
14717
|
+
return this.skipBlock(tokens, start2, tag, closeTag);
|
|
14718
|
+
}
|
|
14554
14719
|
skipBlock(tokens, start2, openTag, closeTag) {
|
|
14555
14720
|
let depth = 0;
|
|
14556
14721
|
let i = start2 + 1;
|
|
@@ -14558,7 +14723,7 @@ var init_engine = __esm({
|
|
|
14558
14723
|
if (tokens[i][0] === "BLOCK") {
|
|
14559
14724
|
const [content] = stripTag(tokens[i][1]);
|
|
14560
14725
|
const tag = content.split(/\s+/)[0] || "";
|
|
14561
|
-
if (tag === openTag) depth++;
|
|
14726
|
+
if (tag === openTag && !(openTag === "set" && content.includes("="))) depth++;
|
|
14562
14727
|
else if (tag === closeTag) {
|
|
14563
14728
|
if (depth === 0) return i + 1;
|
|
14564
14729
|
depth--;
|
|
@@ -14743,11 +14908,11 @@ var init_engine = __esm({
|
|
|
14743
14908
|
for (const [fname, rawArgs] of filters) {
|
|
14744
14909
|
const args = rawArgs.map((a) => a instanceof VarRef ? evalExpr(a.name, context) : a);
|
|
14745
14910
|
if (fname === "raw" || fname === "safe") {
|
|
14746
|
-
isSafe = true;
|
|
14911
|
+
if (this.filterPermitted(fname)) isSafe = true;
|
|
14747
14912
|
continue;
|
|
14748
14913
|
}
|
|
14749
14914
|
if (fname === "escape" || fname === "e") {
|
|
14750
|
-
isSafe = true;
|
|
14915
|
+
if (this.filterPermitted(fname)) isSafe = true;
|
|
14751
14916
|
}
|
|
14752
14917
|
if (this._sandbox && this._allowedFilters !== null) {
|
|
14753
14918
|
if (!this._allowedFilters.has(fname)) {
|
|
@@ -16558,858 +16723,128 @@ var init_rateLimiter = __esm({
|
|
|
16558
16723
|
}
|
|
16559
16724
|
});
|
|
16560
16725
|
|
|
16561
|
-
// ../core/src/
|
|
16562
|
-
import
|
|
16563
|
-
import
|
|
16564
|
-
import { readFileSync as readFileSync8 } from "node:fs";
|
|
16565
|
-
import { basename as basename3 } from "node:path";
|
|
16726
|
+
// ../core/src/devMailbox.ts
|
|
16727
|
+
import { mkdirSync as mkdirSync9, readdirSync as readdirSync6, readFileSync as readFileSync8, writeFileSync as writeFileSync7, unlinkSync as unlinkSync4, existsSync as existsSync11 } from "node:fs";
|
|
16728
|
+
import { join as join13 } from "node:path";
|
|
16566
16729
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
16567
|
-
|
|
16568
|
-
|
|
16569
|
-
|
|
16570
|
-
|
|
16571
|
-
|
|
16572
|
-
|
|
16573
|
-
|
|
16574
|
-
|
|
16575
|
-
const lines = buffer.split("\r\n");
|
|
16576
|
-
for (let i = 0; i < lines.length; i++) {
|
|
16577
|
-
const line = lines[i];
|
|
16578
|
-
if (line.length < 3) continue;
|
|
16579
|
-
const code = parseInt(line.substring(0, 3), 10);
|
|
16580
|
-
if (line.length >= 4 && line[3] === " ") {
|
|
16581
|
-
socket.removeListener("data", onData);
|
|
16582
|
-
socket.removeListener("error", onError);
|
|
16583
|
-
resolve31({ code, text: buffer.trim() });
|
|
16584
|
-
return;
|
|
16585
|
-
}
|
|
16586
|
-
}
|
|
16587
|
-
};
|
|
16588
|
-
const onError = (err) => {
|
|
16589
|
-
socket.removeListener("data", onData);
|
|
16590
|
-
reject(err);
|
|
16591
|
-
};
|
|
16592
|
-
socket.on("data", onData);
|
|
16593
|
-
socket.on("error", onError);
|
|
16594
|
-
});
|
|
16595
|
-
}
|
|
16596
|
-
function sendCommand(socket, command) {
|
|
16597
|
-
return new Promise((resolve31, reject) => {
|
|
16598
|
-
socket.write(command + "\r\n", "utf-8", (err) => {
|
|
16599
|
-
if (err) return reject(err);
|
|
16600
|
-
readResponse(socket).then(resolve31, reject);
|
|
16601
|
-
});
|
|
16602
|
-
});
|
|
16603
|
-
}
|
|
16604
|
-
function buildMimeMessage(options) {
|
|
16605
|
-
const boundary = `----=_Tina4_${Date.now()}_${Math.random().toString(36).substring(2)}`;
|
|
16606
|
-
const altBoundary = `----=_Tina4Alt_${Date.now()}_${Math.random().toString(36).substring(2)}`;
|
|
16607
|
-
const hasAttachments = options.attachments && options.attachments.length > 0;
|
|
16608
|
-
const hasTextAlt = options.text !== void 0 && options.html;
|
|
16609
|
-
const lines = [];
|
|
16610
|
-
const fromHeader = options.fromName ? `"${options.fromName}" <${options.from}>` : options.from;
|
|
16611
|
-
lines.push(`From: ${fromHeader}`);
|
|
16612
|
-
lines.push(`To: ${options.to.join(", ")}`);
|
|
16613
|
-
if (options.cc.length > 0) {
|
|
16614
|
-
lines.push(`Cc: ${options.cc.join(", ")}`);
|
|
16615
|
-
}
|
|
16616
|
-
lines.push(`Subject: ${options.subject}`);
|
|
16617
|
-
lines.push(`Date: ${(/* @__PURE__ */ new Date()).toUTCString()}`);
|
|
16618
|
-
lines.push(`Message-ID: <${options.messageId}>`);
|
|
16619
|
-
lines.push("MIME-Version: 1.0");
|
|
16620
|
-
if (options.replyTo) {
|
|
16621
|
-
lines.push(`Reply-To: ${options.replyTo}`);
|
|
16622
|
-
}
|
|
16623
|
-
if (options.headers) {
|
|
16624
|
-
for (const [key, value] of Object.entries(options.headers)) {
|
|
16625
|
-
lines.push(`${key}: ${value}`);
|
|
16626
|
-
}
|
|
16627
|
-
}
|
|
16628
|
-
if (hasAttachments) {
|
|
16629
|
-
lines.push(`Content-Type: multipart/mixed; boundary="${boundary}"`);
|
|
16630
|
-
lines.push("");
|
|
16631
|
-
lines.push(`--${boundary}`);
|
|
16632
|
-
if (hasTextAlt) {
|
|
16633
|
-
lines.push(`Content-Type: multipart/alternative; boundary="${altBoundary}"`);
|
|
16634
|
-
lines.push("");
|
|
16635
|
-
lines.push(`--${altBoundary}`);
|
|
16636
|
-
lines.push("Content-Type: text/plain; charset=UTF-8");
|
|
16637
|
-
lines.push("Content-Transfer-Encoding: 7bit");
|
|
16638
|
-
lines.push("");
|
|
16639
|
-
lines.push(options.text);
|
|
16640
|
-
lines.push("");
|
|
16641
|
-
lines.push(`--${altBoundary}`);
|
|
16642
|
-
lines.push("Content-Type: text/html; charset=UTF-8");
|
|
16643
|
-
lines.push("Content-Transfer-Encoding: 7bit");
|
|
16644
|
-
lines.push("");
|
|
16645
|
-
lines.push(options.body);
|
|
16646
|
-
lines.push("");
|
|
16647
|
-
lines.push(`--${altBoundary}--`);
|
|
16648
|
-
} else {
|
|
16649
|
-
const contentType = options.html ? "text/html" : "text/plain";
|
|
16650
|
-
lines.push(`Content-Type: ${contentType}; charset=UTF-8`);
|
|
16651
|
-
lines.push("Content-Transfer-Encoding: 7bit");
|
|
16652
|
-
lines.push("");
|
|
16653
|
-
lines.push(options.body);
|
|
16654
|
-
}
|
|
16655
|
-
for (const filePath of options.attachments) {
|
|
16656
|
-
const fileName = basename3(filePath);
|
|
16657
|
-
const fileData = readFileSync8(filePath);
|
|
16658
|
-
const base64Data = fileData.toString("base64");
|
|
16659
|
-
lines.push("");
|
|
16660
|
-
lines.push(`--${boundary}`);
|
|
16661
|
-
lines.push(`Content-Type: application/octet-stream; name="${fileName}"`);
|
|
16662
|
-
lines.push("Content-Transfer-Encoding: base64");
|
|
16663
|
-
lines.push(`Content-Disposition: attachment; filename="${fileName}"`);
|
|
16664
|
-
lines.push("");
|
|
16665
|
-
for (let i = 0; i < base64Data.length; i += 76) {
|
|
16666
|
-
lines.push(base64Data.substring(i, i + 76));
|
|
16667
|
-
}
|
|
16668
|
-
}
|
|
16669
|
-
lines.push("");
|
|
16670
|
-
lines.push(`--${boundary}--`);
|
|
16671
|
-
} else if (hasTextAlt) {
|
|
16672
|
-
lines.push(`Content-Type: multipart/alternative; boundary="${altBoundary}"`);
|
|
16673
|
-
lines.push("");
|
|
16674
|
-
lines.push(`--${altBoundary}`);
|
|
16675
|
-
lines.push("Content-Type: text/plain; charset=UTF-8");
|
|
16676
|
-
lines.push("Content-Transfer-Encoding: 7bit");
|
|
16677
|
-
lines.push("");
|
|
16678
|
-
lines.push(options.text);
|
|
16679
|
-
lines.push("");
|
|
16680
|
-
lines.push(`--${altBoundary}`);
|
|
16681
|
-
lines.push("Content-Type: text/html; charset=UTF-8");
|
|
16682
|
-
lines.push("Content-Transfer-Encoding: 7bit");
|
|
16683
|
-
lines.push("");
|
|
16684
|
-
lines.push(options.body);
|
|
16685
|
-
lines.push("");
|
|
16686
|
-
lines.push(`--${altBoundary}--`);
|
|
16687
|
-
} else {
|
|
16688
|
-
const contentType = options.html ? "text/html" : "text/plain";
|
|
16689
|
-
lines.push(`Content-Type: ${contentType}; charset=UTF-8`);
|
|
16690
|
-
lines.push("");
|
|
16691
|
-
lines.push(options.body);
|
|
16692
|
-
}
|
|
16693
|
-
return lines.join("\r\n");
|
|
16694
|
-
}
|
|
16695
|
-
function imapQuote(s) {
|
|
16696
|
-
if (/^[a-zA-Z0-9_./-]+$/.test(s)) return s;
|
|
16697
|
-
return '"' + s.replace(/\\/g, "\\\\").replace(/"/g, '\\"') + '"';
|
|
16698
|
-
}
|
|
16699
|
-
function imapReadLine(socket) {
|
|
16700
|
-
return new Promise((resolve31, reject) => {
|
|
16701
|
-
let buffer = "";
|
|
16702
|
-
const onData = (chunk) => {
|
|
16703
|
-
buffer += chunk.toString("utf-8");
|
|
16704
|
-
const nlIndex = buffer.indexOf("\r\n");
|
|
16705
|
-
if (nlIndex !== -1) {
|
|
16706
|
-
socket.removeListener("data", onData);
|
|
16707
|
-
socket.removeListener("error", onError);
|
|
16708
|
-
resolve31(buffer);
|
|
16730
|
+
var DevMailbox;
|
|
16731
|
+
var init_devMailbox = __esm({
|
|
16732
|
+
"../core/src/devMailbox.ts"() {
|
|
16733
|
+
"use strict";
|
|
16734
|
+
DevMailbox = class {
|
|
16735
|
+
mailboxDir;
|
|
16736
|
+
constructor(mailboxDir) {
|
|
16737
|
+
this.mailboxDir = mailboxDir ?? process.env.TINA4_MAILBOX_DIR ?? "data/mailbox";
|
|
16709
16738
|
}
|
|
16710
|
-
|
|
16711
|
-
|
|
16712
|
-
|
|
16713
|
-
|
|
16714
|
-
|
|
16715
|
-
|
|
16716
|
-
|
|
16717
|
-
});
|
|
16718
|
-
}
|
|
16719
|
-
function imapCommand(socket, command) {
|
|
16720
|
-
return new Promise((resolve31, reject) => {
|
|
16721
|
-
imapTagCounter++;
|
|
16722
|
-
const tag = `T${imapTagCounter}`;
|
|
16723
|
-
const fullCommand = `${tag} ${command}\r
|
|
16724
|
-
`;
|
|
16725
|
-
let buffer = "";
|
|
16726
|
-
const onData = (chunk) => {
|
|
16727
|
-
buffer += chunk.toString("utf-8");
|
|
16728
|
-
if (buffer.includes(`${tag} OK`)) {
|
|
16729
|
-
socket.removeListener("data", onData);
|
|
16730
|
-
socket.removeListener("error", onError);
|
|
16731
|
-
resolve31(buffer);
|
|
16732
|
-
return;
|
|
16739
|
+
/**
|
|
16740
|
+
* Ensure a folder directory exists.
|
|
16741
|
+
*/
|
|
16742
|
+
ensureFolder(folder) {
|
|
16743
|
+
const dir = join13(this.mailboxDir, folder);
|
|
16744
|
+
mkdirSync9(dir, { recursive: true });
|
|
16745
|
+
return dir;
|
|
16733
16746
|
}
|
|
16734
|
-
|
|
16735
|
-
|
|
16736
|
-
|
|
16737
|
-
|
|
16747
|
+
/**
|
|
16748
|
+
* Capture an email to the dev mailbox instead of sending it.
|
|
16749
|
+
*
|
|
16750
|
+
* The parameter order MATCHES Messenger.send() on purpose. It did not before:
|
|
16751
|
+
* send()'s 5th positional was `text` and capture()'s was `cc`, so the same call
|
|
16752
|
+
* meant different things depending on which door it came through -- that mismatch
|
|
16753
|
+
* IS nodejs#42.
|
|
16754
|
+
*
|
|
16755
|
+
* BREAKING: `text` is now the 5th positional. A caller passing cc positionally
|
|
16756
|
+
* must move it. Aligning the two signatures is the fix; leaving them apart would
|
|
16757
|
+
* preserve the bug.
|
|
16758
|
+
*/
|
|
16759
|
+
capture(to, subject, body, html = false, text, cc = [], bcc = [], replyTo, attachments = [], from) {
|
|
16760
|
+
const id = randomUUID2();
|
|
16761
|
+
const toList = Array.isArray(to) ? to : [to];
|
|
16762
|
+
const ccList = Array.isArray(cc) ? cc : cc ? [cc] : [];
|
|
16763
|
+
const bccList = Array.isArray(bcc) ? bcc : bcc ? [bcc] : [];
|
|
16764
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
16765
|
+
const message = {
|
|
16766
|
+
id,
|
|
16767
|
+
type: "outbox",
|
|
16768
|
+
from: from ?? process.env.TINA4_MAIL_FROM ?? "dev@localhost",
|
|
16769
|
+
to: toList,
|
|
16770
|
+
cc: ccList,
|
|
16771
|
+
bcc: bccList,
|
|
16772
|
+
reply_to: replyTo,
|
|
16773
|
+
subject,
|
|
16774
|
+
body,
|
|
16775
|
+
text,
|
|
16776
|
+
html,
|
|
16777
|
+
attachments,
|
|
16778
|
+
date: now,
|
|
16779
|
+
read: false
|
|
16780
|
+
};
|
|
16781
|
+
const outboxDir = this.ensureFolder("outbox");
|
|
16782
|
+
writeFileSync7(join13(outboxDir, `${id}.json`), JSON.stringify(message, null, 2));
|
|
16783
|
+
const inboxDir = this.ensureFolder("inbox");
|
|
16784
|
+
const inboxMessage = { ...message, type: "inbox" };
|
|
16785
|
+
writeFileSync7(join13(inboxDir, `${id}.json`), JSON.stringify(inboxMessage, null, 2));
|
|
16786
|
+
return { success: true, message: "Email captured to dev mailbox", id };
|
|
16738
16787
|
}
|
|
16739
|
-
|
|
16740
|
-
|
|
16741
|
-
|
|
16742
|
-
|
|
16743
|
-
|
|
16744
|
-
|
|
16745
|
-
|
|
16746
|
-
|
|
16747
|
-
|
|
16748
|
-
}
|
|
16749
|
-
|
|
16750
|
-
const e = err instanceof Error ? err : new Error(String(err));
|
|
16751
|
-
Log.error(`Messenger IMAP ${method}() failed: ${e.name}: ${e.message}`);
|
|
16752
|
-
if (e instanceof MessengerConnectionError) return e;
|
|
16753
|
-
return new MessengerConnectionError(`IMAP ${method} failed: ${e.message}`);
|
|
16754
|
-
}
|
|
16755
|
-
function parseSearchResponse(response) {
|
|
16756
|
-
const match = response.match(/\* SEARCH (.+)/);
|
|
16757
|
-
if (!match) return [];
|
|
16758
|
-
return match[1].trim().split(/\s+/).filter((s) => /^\d+$/.test(s));
|
|
16759
|
-
}
|
|
16760
|
-
function parseHeaderResponse(uid, response) {
|
|
16761
|
-
const headers = {};
|
|
16762
|
-
const headerBlock = response.match(/\r\n([\s\S]*?)\r\n\)/);
|
|
16763
|
-
if (headerBlock) {
|
|
16764
|
-
const lines = headerBlock[1].split(/\r\n/);
|
|
16765
|
-
let currentKey = "";
|
|
16766
|
-
for (const line of lines) {
|
|
16767
|
-
if (/^\s/.test(line) && currentKey) {
|
|
16768
|
-
headers[currentKey] += " " + line.trim();
|
|
16769
|
-
} else {
|
|
16770
|
-
const colonIdx = line.indexOf(":");
|
|
16771
|
-
if (colonIdx > 0) {
|
|
16772
|
-
currentKey = line.substring(0, colonIdx).trim().toLowerCase();
|
|
16773
|
-
headers[currentKey] = line.substring(colonIdx + 1).trim();
|
|
16788
|
+
/**
|
|
16789
|
+
* List messages from a folder (default: inbox).
|
|
16790
|
+
*/
|
|
16791
|
+
inbox(limit = 50, offset = 0, folder = "inbox") {
|
|
16792
|
+
const dir = this.ensureFolder(folder);
|
|
16793
|
+
const results = [];
|
|
16794
|
+
let files;
|
|
16795
|
+
try {
|
|
16796
|
+
files = readdirSync6(dir).filter((f) => f.endsWith(".json")).sort().reverse();
|
|
16797
|
+
} catch {
|
|
16798
|
+
return [];
|
|
16774
16799
|
}
|
|
16775
|
-
|
|
16776
|
-
|
|
16777
|
-
|
|
16778
|
-
|
|
16779
|
-
|
|
16780
|
-
|
|
16781
|
-
|
|
16782
|
-
from: headers["from"] ?? "",
|
|
16783
|
-
to: headers["to"] ?? "",
|
|
16784
|
-
date: headers["date"] ?? "",
|
|
16785
|
-
snippet: "",
|
|
16786
|
-
seen
|
|
16787
|
-
};
|
|
16788
|
-
}
|
|
16789
|
-
function emptyFullMessage(uid) {
|
|
16790
|
-
return { uid, subject: "", from: "", to: "", cc: "", date: "", bodyText: "", bodyHtml: "", headers: {} };
|
|
16791
|
-
}
|
|
16792
|
-
function parseFullMessage(uid, response) {
|
|
16793
|
-
const bodyMatch = response.match(/\{(\d+)\}\r\n([\s\S]*)/);
|
|
16794
|
-
const rawMessage = bodyMatch ? bodyMatch[2] : response;
|
|
16795
|
-
const headerEnd = rawMessage.indexOf("\r\n\r\n");
|
|
16796
|
-
const headerSection = headerEnd > 0 ? rawMessage.substring(0, headerEnd) : rawMessage;
|
|
16797
|
-
const bodySection = headerEnd > 0 ? rawMessage.substring(headerEnd + 4) : "";
|
|
16798
|
-
const headers = {};
|
|
16799
|
-
const headerLines = headerSection.split(/\r\n/);
|
|
16800
|
-
let currentKey = "";
|
|
16801
|
-
for (const line of headerLines) {
|
|
16802
|
-
if (/^\s/.test(line) && currentKey) {
|
|
16803
|
-
headers[currentKey] += " " + line.trim();
|
|
16804
|
-
} else {
|
|
16805
|
-
const colonIdx = line.indexOf(":");
|
|
16806
|
-
if (colonIdx > 0) {
|
|
16807
|
-
currentKey = line.substring(0, colonIdx).trim().toLowerCase();
|
|
16808
|
-
headers[currentKey] = line.substring(colonIdx + 1).trim();
|
|
16809
|
-
}
|
|
16810
|
-
}
|
|
16811
|
-
}
|
|
16812
|
-
const contentType = headers["content-type"] ?? "text/plain";
|
|
16813
|
-
let bodyText = "";
|
|
16814
|
-
let bodyHtml = "";
|
|
16815
|
-
if (contentType.includes("multipart")) {
|
|
16816
|
-
const boundaryMatch = contentType.match(/boundary="?([^";\s]+)"?/);
|
|
16817
|
-
if (boundaryMatch) {
|
|
16818
|
-
const boundary = boundaryMatch[1];
|
|
16819
|
-
const parts = bodySection.split("--" + boundary);
|
|
16820
|
-
for (const part of parts) {
|
|
16821
|
-
if (part.trim() === "" || part.trim() === "--") continue;
|
|
16822
|
-
const partHeaderEnd = part.indexOf("\r\n\r\n");
|
|
16823
|
-
const partHeaders = partHeaderEnd > 0 ? part.substring(0, partHeaderEnd).toLowerCase() : "";
|
|
16824
|
-
const partBody = partHeaderEnd > 0 ? part.substring(partHeaderEnd + 4).trim() : "";
|
|
16825
|
-
if (partHeaders.includes("text/html")) {
|
|
16826
|
-
bodyHtml = partBody;
|
|
16827
|
-
} else if (partHeaders.includes("text/plain")) {
|
|
16828
|
-
bodyText = partBody;
|
|
16800
|
+
const sliced = files.slice(offset, offset + limit);
|
|
16801
|
+
for (const file of sliced) {
|
|
16802
|
+
try {
|
|
16803
|
+
const msg = JSON.parse(readFileSync8(join13(dir, file), "utf-8"));
|
|
16804
|
+
results.push(msg);
|
|
16805
|
+
} catch {
|
|
16806
|
+
}
|
|
16829
16807
|
}
|
|
16808
|
+
return results;
|
|
16830
16809
|
}
|
|
16831
|
-
|
|
16832
|
-
|
|
16833
|
-
|
|
16834
|
-
|
|
16835
|
-
|
|
16836
|
-
|
|
16837
|
-
|
|
16838
|
-
|
|
16839
|
-
|
|
16840
|
-
|
|
16841
|
-
|
|
16842
|
-
|
|
16843
|
-
|
|
16844
|
-
|
|
16845
|
-
|
|
16846
|
-
|
|
16847
|
-
|
|
16848
|
-
headers
|
|
16849
|
-
};
|
|
16850
|
-
}
|
|
16851
|
-
var MessengerConnectionError, Messenger, imapTagCounter;
|
|
16852
|
-
var init_messenger = __esm({
|
|
16853
|
-
"../core/src/messenger.ts"() {
|
|
16854
|
-
"use strict";
|
|
16855
|
-
init_dotenv();
|
|
16856
|
-
init_logger();
|
|
16857
|
-
MessengerConnectionError = class extends Error {
|
|
16858
|
-
constructor(message) {
|
|
16859
|
-
super(message);
|
|
16860
|
-
this.name = "MessengerConnectionError";
|
|
16861
|
-
}
|
|
16862
|
-
};
|
|
16863
|
-
Messenger = class {
|
|
16864
|
-
host;
|
|
16865
|
-
port;
|
|
16866
|
-
username;
|
|
16867
|
-
password;
|
|
16868
|
-
fromAddress;
|
|
16869
|
-
fromName;
|
|
16870
|
-
encryption;
|
|
16871
|
-
useTls;
|
|
16872
|
-
imapHost;
|
|
16873
|
-
imapPort;
|
|
16874
|
-
imapUser;
|
|
16875
|
-
imapPass;
|
|
16876
|
-
imapEncryption;
|
|
16877
|
-
constructor(options) {
|
|
16878
|
-
this.host = options?.host ?? process.env.TINA4_MAIL_HOST ?? "localhost";
|
|
16879
|
-
this.port = options?.port ?? parseInt(process.env.TINA4_MAIL_PORT ?? "587", 10);
|
|
16880
|
-
this.username = options?.username ?? process.env.TINA4_MAIL_USERNAME ?? "";
|
|
16881
|
-
this.password = options?.password ?? process.env.TINA4_MAIL_PASSWORD ?? "";
|
|
16882
|
-
this.fromAddress = options?.fromAddress ?? process.env.TINA4_MAIL_FROM ?? (this.username || "noreply@localhost");
|
|
16883
|
-
this.fromName = options?.fromName ?? process.env.TINA4_MAIL_FROM_NAME ?? "";
|
|
16884
|
-
const envEncryption = options?.encryption ?? process.env.TINA4_MAIL_ENCRYPTION;
|
|
16885
|
-
if (envEncryption) {
|
|
16886
|
-
this.encryption = envEncryption.toLowerCase();
|
|
16887
|
-
} else if (options?.useTls !== void 0) {
|
|
16888
|
-
this.encryption = options.useTls ? "tls" : "none";
|
|
16889
|
-
} else {
|
|
16890
|
-
this.encryption = "tls";
|
|
16810
|
+
/**
|
|
16811
|
+
* Read a single message by ID. Searches all folders.
|
|
16812
|
+
*/
|
|
16813
|
+
read(msgId) {
|
|
16814
|
+
const folders = ["inbox", "outbox"];
|
|
16815
|
+
for (const folder of folders) {
|
|
16816
|
+
const filePath = join13(this.mailboxDir, folder, `${msgId}.json`);
|
|
16817
|
+
if (existsSync11(filePath)) {
|
|
16818
|
+
try {
|
|
16819
|
+
const msg = JSON.parse(readFileSync8(filePath, "utf-8"));
|
|
16820
|
+
msg.read = true;
|
|
16821
|
+
writeFileSync7(filePath, JSON.stringify(msg, null, 2));
|
|
16822
|
+
return msg;
|
|
16823
|
+
} catch {
|
|
16824
|
+
return null;
|
|
16825
|
+
}
|
|
16826
|
+
}
|
|
16891
16827
|
}
|
|
16892
|
-
|
|
16893
|
-
this.imapHost = options?.imapHost ?? process.env.TINA4_MAIL_IMAP_HOST ?? "";
|
|
16894
|
-
this.imapPort = options?.imapPort ?? parseInt(process.env.TINA4_MAIL_IMAP_PORT ?? "993", 10);
|
|
16895
|
-
this.imapUser = options?.imapUser ?? process.env.TINA4_MAIL_IMAP_USERNAME ?? this.username;
|
|
16896
|
-
this.imapPass = options?.imapPass ?? process.env.TINA4_MAIL_IMAP_PASSWORD ?? this.password;
|
|
16897
|
-
this.imapEncryption = (options?.imapEncryption ?? process.env.TINA4_MAIL_IMAP_ENCRYPTION ?? "tls").toLowerCase();
|
|
16828
|
+
return null;
|
|
16898
16829
|
}
|
|
16899
16830
|
/**
|
|
16900
|
-
*
|
|
16901
|
-
* Returns one of "tls", "starttls", "none", "ssl".
|
|
16831
|
+
* Count unread messages in the inbox.
|
|
16902
16832
|
*/
|
|
16903
|
-
|
|
16904
|
-
|
|
16905
|
-
|
|
16906
|
-
|
|
16907
|
-
|
|
16908
|
-
|
|
16909
|
-
|
|
16910
|
-
|
|
16911
|
-
|
|
16912
|
-
|
|
16913
|
-
|
|
16914
|
-
|
|
16915
|
-
|
|
16916
|
-
|
|
16917
|
-
|
|
16918
|
-
}
|
|
16919
|
-
if (!this.fromAddress) {
|
|
16920
|
-
return { success: false, message: "No from address configured" };
|
|
16921
|
-
}
|
|
16922
|
-
try {
|
|
16923
|
-
let socket;
|
|
16924
|
-
if (this.port === 465) {
|
|
16925
|
-
socket = tls.connect({ host: this.host, port: this.port, rejectUnauthorized: tlsRejectUnauthorized() });
|
|
16926
|
-
await new Promise((resolve31, reject) => {
|
|
16927
|
-
socket.once("secureConnect", resolve31);
|
|
16928
|
-
socket.once("error", reject);
|
|
16929
|
-
});
|
|
16930
|
-
} else {
|
|
16931
|
-
socket = net2.createConnection({ host: this.host, port: this.port });
|
|
16932
|
-
await new Promise((resolve31, reject) => {
|
|
16933
|
-
socket.once("connect", resolve31);
|
|
16934
|
-
socket.once("error", reject);
|
|
16935
|
-
});
|
|
16936
|
-
}
|
|
16937
|
-
const greeting = await readResponse(socket);
|
|
16938
|
-
if (greeting.code !== 220) {
|
|
16939
|
-
socket.destroy();
|
|
16940
|
-
return { success: false, message: `SMTP greeting failed: ${greeting.text}` };
|
|
16941
|
-
}
|
|
16942
|
-
const ehlo = await sendCommand(socket, `EHLO ${this.host}`);
|
|
16943
|
-
if (ehlo.code !== 250) {
|
|
16944
|
-
socket.destroy();
|
|
16945
|
-
return { success: false, message: `EHLO failed: ${ehlo.text}` };
|
|
16946
|
-
}
|
|
16947
|
-
if (this.useTls && this.port !== 465 && ehlo.text.includes("STARTTLS")) {
|
|
16948
|
-
const starttls = await sendCommand(socket, "STARTTLS");
|
|
16949
|
-
if (starttls.code !== 220) {
|
|
16950
|
-
socket.destroy();
|
|
16951
|
-
return { success: false, message: `STARTTLS failed: ${starttls.text}` };
|
|
16952
|
-
}
|
|
16953
|
-
const plainSocket = socket;
|
|
16954
|
-
socket = tls.connect(
|
|
16955
|
-
{ socket: plainSocket, host: this.host, rejectUnauthorized: tlsRejectUnauthorized() }
|
|
16956
|
-
);
|
|
16957
|
-
await new Promise((resolve31, reject) => {
|
|
16958
|
-
socket.once("secureConnect", resolve31);
|
|
16959
|
-
socket.once("error", reject);
|
|
16960
|
-
});
|
|
16961
|
-
const ehlo2 = await sendCommand(socket, `EHLO ${this.host}`);
|
|
16962
|
-
if (ehlo2.code !== 250) {
|
|
16963
|
-
socket.destroy();
|
|
16964
|
-
return { success: false, message: `EHLO after STARTTLS failed: ${ehlo2.text}` };
|
|
16965
|
-
}
|
|
16966
|
-
}
|
|
16967
|
-
if (this.username && this.password) {
|
|
16968
|
-
const auth = await sendCommand(socket, "AUTH LOGIN");
|
|
16969
|
-
if (auth.code !== 334) {
|
|
16970
|
-
socket.destroy();
|
|
16971
|
-
return { success: false, message: `AUTH LOGIN failed: ${auth.text}` };
|
|
16972
|
-
}
|
|
16973
|
-
const userResp = await sendCommand(socket, Buffer.from(this.username).toString("base64"));
|
|
16974
|
-
if (userResp.code !== 334) {
|
|
16975
|
-
socket.destroy();
|
|
16976
|
-
return { success: false, message: `AUTH username failed: ${userResp.text}` };
|
|
16977
|
-
}
|
|
16978
|
-
const passResp = await sendCommand(socket, Buffer.from(this.password).toString("base64"));
|
|
16979
|
-
if (passResp.code !== 235) {
|
|
16980
|
-
socket.destroy();
|
|
16981
|
-
return { success: false, message: `AUTH password failed: ${passResp.text}` };
|
|
16982
|
-
}
|
|
16983
|
-
}
|
|
16984
|
-
const mailFrom = await sendCommand(socket, `MAIL FROM:<${this.fromAddress}>`);
|
|
16985
|
-
if (mailFrom.code !== 250) {
|
|
16986
|
-
socket.destroy();
|
|
16987
|
-
return { success: false, message: `MAIL FROM failed: ${mailFrom.text}` };
|
|
16988
|
-
}
|
|
16989
|
-
for (const recipient of allRecipients) {
|
|
16990
|
-
const rcpt = await sendCommand(socket, `RCPT TO:<${recipient}>`);
|
|
16991
|
-
if (rcpt.code !== 250 && rcpt.code !== 251) {
|
|
16992
|
-
socket.destroy();
|
|
16993
|
-
return { success: false, message: `RCPT TO <${recipient}> failed: ${rcpt.text}` };
|
|
16994
|
-
}
|
|
16995
|
-
}
|
|
16996
|
-
const dataCmd = await sendCommand(socket, "DATA");
|
|
16997
|
-
if (dataCmd.code !== 354) {
|
|
16998
|
-
socket.destroy();
|
|
16999
|
-
return { success: false, message: `DATA failed: ${dataCmd.text}` };
|
|
17000
|
-
}
|
|
17001
|
-
const mimeMessage = buildMimeMessage({
|
|
17002
|
-
from: this.fromAddress,
|
|
17003
|
-
fromName: this.fromName,
|
|
17004
|
-
to: toList,
|
|
17005
|
-
cc: ccList,
|
|
17006
|
-
subject: options.subject,
|
|
17007
|
-
body: options.body,
|
|
17008
|
-
html: options.html ?? false,
|
|
17009
|
-
text: options.text,
|
|
17010
|
-
replyTo: options.replyTo,
|
|
17011
|
-
attachments: options.attachments,
|
|
17012
|
-
headers: options.headers,
|
|
17013
|
-
messageId
|
|
17014
|
-
});
|
|
17015
|
-
const endData = await sendCommand(socket, mimeMessage + "\r\n.");
|
|
17016
|
-
if (endData.code !== 250) {
|
|
17017
|
-
socket.destroy();
|
|
17018
|
-
return { success: false, message: `Message delivery failed: ${endData.text}` };
|
|
17019
|
-
}
|
|
17020
|
-
await sendCommand(socket, "QUIT");
|
|
17021
|
-
socket.destroy();
|
|
17022
|
-
return { success: true, message: "Email sent successfully", id: messageId };
|
|
17023
|
-
} catch (err) {
|
|
17024
|
-
const errMsg = err instanceof Error ? err.message : String(err);
|
|
17025
|
-
return { success: false, message: `SMTP error: ${errMsg}` };
|
|
17026
|
-
}
|
|
17027
|
-
}
|
|
17028
|
-
/**
|
|
17029
|
-
* Test the SMTP connection without sending an email.
|
|
17030
|
-
*/
|
|
17031
|
-
async testConnection() {
|
|
17032
|
-
try {
|
|
17033
|
-
let socket;
|
|
17034
|
-
if (this.port === 465) {
|
|
17035
|
-
socket = tls.connect({ host: this.host, port: this.port, rejectUnauthorized: tlsRejectUnauthorized() });
|
|
17036
|
-
await new Promise((resolve31, reject) => {
|
|
17037
|
-
socket.once("secureConnect", resolve31);
|
|
17038
|
-
socket.once("error", reject);
|
|
17039
|
-
});
|
|
17040
|
-
} else {
|
|
17041
|
-
socket = net2.createConnection({ host: this.host, port: this.port });
|
|
17042
|
-
await new Promise((resolve31, reject) => {
|
|
17043
|
-
socket.once("connect", resolve31);
|
|
17044
|
-
socket.once("error", reject);
|
|
17045
|
-
});
|
|
17046
|
-
}
|
|
17047
|
-
const greeting = await readResponse(socket);
|
|
17048
|
-
if (greeting.code !== 220) {
|
|
17049
|
-
socket.destroy();
|
|
17050
|
-
return { success: false, message: `SMTP greeting failed: ${greeting.text}` };
|
|
17051
|
-
}
|
|
17052
|
-
const ehlo = await sendCommand(socket, `EHLO ${this.host}`);
|
|
17053
|
-
if (ehlo.code !== 250) {
|
|
17054
|
-
socket.destroy();
|
|
17055
|
-
return { success: false, message: `EHLO failed: ${ehlo.text}` };
|
|
17056
|
-
}
|
|
17057
|
-
await sendCommand(socket, "QUIT");
|
|
17058
|
-
socket.destroy();
|
|
17059
|
-
return { success: true, message: `Connected to ${this.host}:${this.port}` };
|
|
17060
|
-
} catch (err) {
|
|
17061
|
-
const errMsg = err instanceof Error ? err.message : String(err);
|
|
17062
|
-
return { success: false, message: `Connection failed: ${errMsg}` };
|
|
17063
|
-
}
|
|
17064
|
-
}
|
|
17065
|
-
// ── IMAP (Read) ────────────────────────────────────────────
|
|
17066
|
-
/**
|
|
17067
|
-
* Connect to the IMAP server via raw TCP/TLS.
|
|
17068
|
-
* Returns the socket and reads the greeting.
|
|
17069
|
-
*/
|
|
17070
|
-
async imapConnect() {
|
|
17071
|
-
if (!this.imapHost) {
|
|
17072
|
-
throw new Error("IMAP host not configured (set imapHost or IMAP_HOST env)");
|
|
17073
|
-
}
|
|
17074
|
-
let socket;
|
|
17075
|
-
const useTls = this.imapEncryption === "tls" || this.imapEncryption === "ssl" || this.imapEncryption === "" && this.imapPort === 993;
|
|
17076
|
-
if (useTls) {
|
|
17077
|
-
socket = tls.connect({ host: this.imapHost, port: this.imapPort, rejectUnauthorized: tlsRejectUnauthorized() });
|
|
17078
|
-
await new Promise((resolve31, reject) => {
|
|
17079
|
-
socket.once("secureConnect", resolve31);
|
|
17080
|
-
socket.once("error", reject);
|
|
17081
|
-
});
|
|
17082
|
-
} else {
|
|
17083
|
-
socket = net2.createConnection({ host: this.imapHost, port: this.imapPort });
|
|
17084
|
-
await new Promise((resolve31, reject) => {
|
|
17085
|
-
socket.once("connect", resolve31);
|
|
17086
|
-
socket.once("error", reject);
|
|
17087
|
-
});
|
|
17088
|
-
}
|
|
17089
|
-
await imapReadLine(socket);
|
|
17090
|
-
if (this.imapUser && this.imapPass) {
|
|
17091
|
-
const loginResp = await imapCommand(socket, `LOGIN ${imapQuote(this.imapUser)} ${imapQuote(this.imapPass)}`);
|
|
17092
|
-
if (!loginResp.includes("OK")) {
|
|
17093
|
-
socket.destroy();
|
|
17094
|
-
throw new Error(`IMAP login failed: ${loginResp}`);
|
|
17095
|
-
}
|
|
17096
|
-
}
|
|
17097
|
-
return socket;
|
|
17098
|
-
}
|
|
17099
|
-
/**
|
|
17100
|
-
* Disconnect from IMAP cleanly.
|
|
17101
|
-
*/
|
|
17102
|
-
async imapDisconnect(socket) {
|
|
17103
|
-
try {
|
|
17104
|
-
await imapCommand(socket, "LOGOUT");
|
|
17105
|
-
} catch {
|
|
17106
|
-
}
|
|
17107
|
-
socket.destroy();
|
|
17108
|
-
}
|
|
17109
|
-
/**
|
|
17110
|
-
* Fetch latest messages from a folder.
|
|
17111
|
-
* Returns list of message summaries.
|
|
17112
|
-
*/
|
|
17113
|
-
async inbox(limit = 20, offset = 0, folder = "INBOX") {
|
|
17114
|
-
let socket;
|
|
17115
|
-
try {
|
|
17116
|
-
socket = await this.imapConnect();
|
|
17117
|
-
} catch (err) {
|
|
17118
|
-
throw imapFail("inbox", err);
|
|
17119
|
-
}
|
|
17120
|
-
try {
|
|
17121
|
-
await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
|
|
17122
|
-
const searchResp = await imapCommand(socket, "SEARCH ALL");
|
|
17123
|
-
const uids = parseSearchResponse(searchResp);
|
|
17124
|
-
if (uids.length === 0) return [];
|
|
17125
|
-
uids.reverse();
|
|
17126
|
-
const selected = uids.slice(offset, offset + limit);
|
|
17127
|
-
if (selected.length === 0) return [];
|
|
17128
|
-
const messages = [];
|
|
17129
|
-
for (const uid of selected) {
|
|
17130
|
-
const fetchResp = await imapCommand(socket, `FETCH ${uid} (FLAGS BODY.PEEK[HEADER.FIELDS (FROM TO SUBJECT DATE)])`);
|
|
17131
|
-
messages.push(parseHeaderResponse(uid, fetchResp));
|
|
17132
|
-
}
|
|
17133
|
-
return messages;
|
|
17134
|
-
} catch (err) {
|
|
17135
|
-
throw imapFail("inbox", err);
|
|
17136
|
-
} finally {
|
|
17137
|
-
await this.imapDisconnect(socket);
|
|
17138
|
-
}
|
|
17139
|
-
}
|
|
17140
|
-
/**
|
|
17141
|
-
* Read a single message by sequence number or UID.
|
|
17142
|
-
*/
|
|
17143
|
-
async read(uid, folder = "INBOX") {
|
|
17144
|
-
let socket;
|
|
17145
|
-
try {
|
|
17146
|
-
socket = await this.imapConnect();
|
|
17147
|
-
} catch (err) {
|
|
17148
|
-
throw imapFail("read", err);
|
|
17149
|
-
}
|
|
17150
|
-
try {
|
|
17151
|
-
await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
|
|
17152
|
-
const fetchResp = await imapCommand(socket, `FETCH ${uid} (FLAGS BODY[])`);
|
|
17153
|
-
if (!/\{\d+\}/.test(fetchResp)) {
|
|
17154
|
-
return emptyFullMessage(uid);
|
|
17155
|
-
}
|
|
17156
|
-
await imapCommand(socket, `STORE ${uid} +FLAGS (\\Seen)`);
|
|
17157
|
-
return parseFullMessage(uid, fetchResp);
|
|
17158
|
-
} catch (err) {
|
|
17159
|
-
throw imapFail("read", err);
|
|
17160
|
-
} finally {
|
|
17161
|
-
await this.imapDisconnect(socket);
|
|
17162
|
-
}
|
|
17163
|
-
}
|
|
17164
|
-
/**
|
|
17165
|
-
* Search messages using IMAP search criteria.
|
|
17166
|
-
*/
|
|
17167
|
-
async search(folder = "INBOX", subject, sender, since, before, unseenOnly = false, limit = 50) {
|
|
17168
|
-
const criteria = ["ALL"];
|
|
17169
|
-
if (subject) criteria.push(`SUBJECT "${subject}"`);
|
|
17170
|
-
if (sender) criteria.push(`FROM "${sender}"`);
|
|
17171
|
-
if (since) criteria.push(`SINCE ${since}`);
|
|
17172
|
-
if (before) criteria.push(`BEFORE ${before}`);
|
|
17173
|
-
if (unseenOnly) criteria.push("UNSEEN");
|
|
17174
|
-
const query = criteria.join(" ");
|
|
17175
|
-
let socket;
|
|
17176
|
-
try {
|
|
17177
|
-
socket = await this.imapConnect();
|
|
17178
|
-
} catch (err) {
|
|
17179
|
-
throw imapFail("search", err);
|
|
17180
|
-
}
|
|
17181
|
-
try {
|
|
17182
|
-
await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
|
|
17183
|
-
const searchResp = await imapCommand(socket, `SEARCH ${query}`);
|
|
17184
|
-
const uids = parseSearchResponse(searchResp);
|
|
17185
|
-
if (uids.length === 0) return [];
|
|
17186
|
-
uids.reverse();
|
|
17187
|
-
const messages = [];
|
|
17188
|
-
for (const uid of uids.slice(0, limit)) {
|
|
17189
|
-
const fetchResp = await imapCommand(socket, `FETCH ${uid} (FLAGS BODY.PEEK[HEADER.FIELDS (FROM TO SUBJECT DATE)])`);
|
|
17190
|
-
messages.push(parseHeaderResponse(uid, fetchResp));
|
|
17191
|
-
}
|
|
17192
|
-
return messages;
|
|
17193
|
-
} catch (err) {
|
|
17194
|
-
throw imapFail("search", err);
|
|
17195
|
-
} finally {
|
|
17196
|
-
await this.imapDisconnect(socket);
|
|
17197
|
-
}
|
|
17198
|
-
}
|
|
17199
|
-
/**
|
|
17200
|
-
* Delete a message by UID.
|
|
17201
|
-
*/
|
|
17202
|
-
async deleteMessage(uid, folder = "INBOX") {
|
|
17203
|
-
const socket = await this.imapConnect();
|
|
17204
|
-
try {
|
|
17205
|
-
await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
|
|
17206
|
-
await imapCommand(socket, `STORE ${uid} +FLAGS (\\Deleted)`);
|
|
17207
|
-
await imapCommand(socket, "EXPUNGE");
|
|
17208
|
-
} finally {
|
|
17209
|
-
await this.imapDisconnect(socket);
|
|
17210
|
-
}
|
|
17211
|
-
}
|
|
17212
|
-
/**
|
|
17213
|
-
* Mark a message as read.
|
|
17214
|
-
*/
|
|
17215
|
-
async markRead(uid, folder = "INBOX") {
|
|
17216
|
-
const socket = await this.imapConnect();
|
|
17217
|
-
try {
|
|
17218
|
-
await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
|
|
17219
|
-
await imapCommand(socket, `STORE ${uid} +FLAGS (\\Seen)`);
|
|
17220
|
-
} finally {
|
|
17221
|
-
await this.imapDisconnect(socket);
|
|
17222
|
-
}
|
|
17223
|
-
}
|
|
17224
|
-
/**
|
|
17225
|
-
* Count unseen messages in a folder.
|
|
17226
|
-
*/
|
|
17227
|
-
async unread(folder = "INBOX") {
|
|
17228
|
-
let socket;
|
|
17229
|
-
try {
|
|
17230
|
-
socket = await this.imapConnect();
|
|
17231
|
-
} catch (err) {
|
|
17232
|
-
throw imapFail("unread", err);
|
|
17233
|
-
}
|
|
17234
|
-
try {
|
|
17235
|
-
await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
|
|
17236
|
-
const searchResp = await imapCommand(socket, "SEARCH UNSEEN");
|
|
17237
|
-
return parseSearchResponse(searchResp).length;
|
|
17238
|
-
} catch (err) {
|
|
17239
|
-
throw imapFail("unread", err);
|
|
17240
|
-
} finally {
|
|
17241
|
-
await this.imapDisconnect(socket);
|
|
17242
|
-
}
|
|
17243
|
-
}
|
|
17244
|
-
/**
|
|
17245
|
-
* List available IMAP folders/mailboxes.
|
|
17246
|
-
*/
|
|
17247
|
-
async folders() {
|
|
17248
|
-
let socket;
|
|
17249
|
-
try {
|
|
17250
|
-
socket = await this.imapConnect();
|
|
17251
|
-
} catch (err) {
|
|
17252
|
-
throw imapFail("folders", err);
|
|
17253
|
-
}
|
|
17254
|
-
try {
|
|
17255
|
-
const resp = await imapCommand(socket, 'LIST "" "*"');
|
|
17256
|
-
const result = [];
|
|
17257
|
-
for (const line of resp.split("\r\n")) {
|
|
17258
|
-
const m = line.match(/\* LIST \([^)]*\) "[^"]*" "?([^"\r\n]+)"?/i);
|
|
17259
|
-
if (m) result.push(m[1]);
|
|
17260
|
-
}
|
|
17261
|
-
return result;
|
|
17262
|
-
} catch (err) {
|
|
17263
|
-
throw imapFail("folders", err);
|
|
17264
|
-
} finally {
|
|
17265
|
-
await this.imapDisconnect(socket);
|
|
17266
|
-
}
|
|
17267
|
-
}
|
|
17268
|
-
/**
|
|
17269
|
-
* Test IMAP connectivity without reading.
|
|
17270
|
-
*/
|
|
17271
|
-
async testImapConnection() {
|
|
17272
|
-
try {
|
|
17273
|
-
const socket = await this.imapConnect();
|
|
17274
|
-
await this.imapDisconnect(socket);
|
|
17275
|
-
return { success: true, message: `Connected to ${this.imapHost}:${this.imapPort}` };
|
|
17276
|
-
} catch (err) {
|
|
17277
|
-
const errMsg = err instanceof Error ? err.message : String(err);
|
|
17278
|
-
return { success: false, message: `IMAP connection failed: ${errMsg}` };
|
|
17279
|
-
}
|
|
17280
|
-
}
|
|
17281
|
-
};
|
|
17282
|
-
imapTagCounter = 0;
|
|
17283
|
-
}
|
|
17284
|
-
});
|
|
17285
|
-
|
|
17286
|
-
// ../core/src/devMailbox.ts
|
|
17287
|
-
import { mkdirSync as mkdirSync9, readdirSync as readdirSync6, readFileSync as readFileSync9, writeFileSync as writeFileSync7, unlinkSync as unlinkSync4, existsSync as existsSync11 } from "node:fs";
|
|
17288
|
-
import { join as join13 } from "node:path";
|
|
17289
|
-
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
17290
|
-
function createMessenger() {
|
|
17291
|
-
const debug = process.env.TINA4_DEBUG;
|
|
17292
|
-
const smtpHost = process.env.TINA4_MAIL_HOST;
|
|
17293
|
-
const isProd = !isTruthy(debug) && process.env.NODE_ENV === "production";
|
|
17294
|
-
if (isTruthy(debug)) {
|
|
17295
|
-
return new DevMailbox();
|
|
17296
|
-
}
|
|
17297
|
-
if (!smtpHost) {
|
|
17298
|
-
return new DevMailbox();
|
|
17299
|
-
}
|
|
17300
|
-
if (!isProd) {
|
|
17301
|
-
return new DevMailbox();
|
|
17302
|
-
}
|
|
17303
|
-
return new Messenger();
|
|
17304
|
-
}
|
|
17305
|
-
var DevMailbox;
|
|
17306
|
-
var init_devMailbox = __esm({
|
|
17307
|
-
"../core/src/devMailbox.ts"() {
|
|
17308
|
-
"use strict";
|
|
17309
|
-
init_messenger();
|
|
17310
|
-
init_dotenv();
|
|
17311
|
-
DevMailbox = class {
|
|
17312
|
-
mailboxDir;
|
|
17313
|
-
constructor(mailboxDir) {
|
|
17314
|
-
this.mailboxDir = mailboxDir ?? process.env.TINA4_MAILBOX_DIR ?? "data/mailbox";
|
|
17315
|
-
}
|
|
17316
|
-
/**
|
|
17317
|
-
* Ensure a folder directory exists.
|
|
17318
|
-
*/
|
|
17319
|
-
ensureFolder(folder) {
|
|
17320
|
-
const dir = join13(this.mailboxDir, folder);
|
|
17321
|
-
mkdirSync9(dir, { recursive: true });
|
|
17322
|
-
return dir;
|
|
17323
|
-
}
|
|
17324
|
-
/**
|
|
17325
|
-
* Capture an email to the dev mailbox instead of sending it.
|
|
17326
|
-
*/
|
|
17327
|
-
capture(to, subject, body, html = false, cc = [], bcc = [], replyTo, attachments = [], from) {
|
|
17328
|
-
const id = randomUUID3();
|
|
17329
|
-
const toList = Array.isArray(to) ? to : [to];
|
|
17330
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
17331
|
-
const message = {
|
|
17332
|
-
id,
|
|
17333
|
-
type: "outbox",
|
|
17334
|
-
from: from ?? process.env.TINA4_MAIL_FROM ?? "dev@localhost",
|
|
17335
|
-
to: toList,
|
|
17336
|
-
cc,
|
|
17337
|
-
bcc,
|
|
17338
|
-
reply_to: replyTo,
|
|
17339
|
-
subject,
|
|
17340
|
-
body,
|
|
17341
|
-
html,
|
|
17342
|
-
attachments,
|
|
17343
|
-
date: now,
|
|
17344
|
-
read: false
|
|
17345
|
-
};
|
|
17346
|
-
const outboxDir = this.ensureFolder("outbox");
|
|
17347
|
-
writeFileSync7(join13(outboxDir, `${id}.json`), JSON.stringify(message, null, 2));
|
|
17348
|
-
const inboxDir = this.ensureFolder("inbox");
|
|
17349
|
-
const inboxMessage = { ...message, type: "inbox" };
|
|
17350
|
-
writeFileSync7(join13(inboxDir, `${id}.json`), JSON.stringify(inboxMessage, null, 2));
|
|
17351
|
-
return { success: true, message: "Email captured to dev mailbox", id };
|
|
17352
|
-
}
|
|
17353
|
-
/**
|
|
17354
|
-
* List messages from a folder (default: inbox).
|
|
17355
|
-
*/
|
|
17356
|
-
inbox(limit = 50, offset = 0, folder = "inbox") {
|
|
17357
|
-
const dir = this.ensureFolder(folder);
|
|
17358
|
-
const results = [];
|
|
17359
|
-
let files;
|
|
17360
|
-
try {
|
|
17361
|
-
files = readdirSync6(dir).filter((f) => f.endsWith(".json")).sort().reverse();
|
|
17362
|
-
} catch {
|
|
17363
|
-
return [];
|
|
17364
|
-
}
|
|
17365
|
-
const sliced = files.slice(offset, offset + limit);
|
|
17366
|
-
for (const file of sliced) {
|
|
17367
|
-
try {
|
|
17368
|
-
const msg = JSON.parse(readFileSync9(join13(dir, file), "utf-8"));
|
|
17369
|
-
results.push(msg);
|
|
17370
|
-
} catch {
|
|
17371
|
-
}
|
|
17372
|
-
}
|
|
17373
|
-
return results;
|
|
17374
|
-
}
|
|
17375
|
-
/**
|
|
17376
|
-
* Read a single message by ID. Searches all folders.
|
|
17377
|
-
*/
|
|
17378
|
-
read(msgId) {
|
|
17379
|
-
const folders = ["inbox", "outbox"];
|
|
17380
|
-
for (const folder of folders) {
|
|
17381
|
-
const filePath = join13(this.mailboxDir, folder, `${msgId}.json`);
|
|
17382
|
-
if (existsSync11(filePath)) {
|
|
17383
|
-
try {
|
|
17384
|
-
const msg = JSON.parse(readFileSync9(filePath, "utf-8"));
|
|
17385
|
-
msg.read = true;
|
|
17386
|
-
writeFileSync7(filePath, JSON.stringify(msg, null, 2));
|
|
17387
|
-
return msg;
|
|
17388
|
-
} catch {
|
|
17389
|
-
return null;
|
|
17390
|
-
}
|
|
17391
|
-
}
|
|
17392
|
-
}
|
|
17393
|
-
return null;
|
|
17394
|
-
}
|
|
17395
|
-
/**
|
|
17396
|
-
* Count unread messages in the inbox.
|
|
17397
|
-
*/
|
|
17398
|
-
unreadCount() {
|
|
17399
|
-
const dir = this.ensureFolder("inbox");
|
|
17400
|
-
let count = 0;
|
|
17401
|
-
try {
|
|
17402
|
-
const files = readdirSync6(dir).filter((f) => f.endsWith(".json"));
|
|
17403
|
-
for (const file of files) {
|
|
17404
|
-
try {
|
|
17405
|
-
const msg = JSON.parse(readFileSync9(join13(dir, file), "utf-8"));
|
|
17406
|
-
if (!msg.read) count++;
|
|
17407
|
-
} catch {
|
|
17408
|
-
}
|
|
17409
|
-
}
|
|
17410
|
-
} catch {
|
|
17411
|
-
}
|
|
17412
|
-
return count;
|
|
16833
|
+
unreadCount() {
|
|
16834
|
+
const dir = this.ensureFolder("inbox");
|
|
16835
|
+
let count = 0;
|
|
16836
|
+
try {
|
|
16837
|
+
const files = readdirSync6(dir).filter((f) => f.endsWith(".json"));
|
|
16838
|
+
for (const file of files) {
|
|
16839
|
+
try {
|
|
16840
|
+
const msg = JSON.parse(readFileSync8(join13(dir, file), "utf-8"));
|
|
16841
|
+
if (!msg.read) count++;
|
|
16842
|
+
} catch {
|
|
16843
|
+
}
|
|
16844
|
+
}
|
|
16845
|
+
} catch {
|
|
16846
|
+
}
|
|
16847
|
+
return count;
|
|
17413
16848
|
}
|
|
17414
16849
|
/**
|
|
17415
16850
|
* Delete a message by ID. Removes from all folders.
|
|
@@ -17474,7 +16909,7 @@ var init_devMailbox = __esm({
|
|
|
17474
16909
|
const subject = subjects[i % subjects.length];
|
|
17475
16910
|
const from = senders[i % senders.length];
|
|
17476
16911
|
const date = new Date(Date.now() - i * 36e5).toISOString();
|
|
17477
|
-
const id =
|
|
16912
|
+
const id = randomUUID2();
|
|
17478
16913
|
const message = {
|
|
17479
16914
|
id,
|
|
17480
16915
|
type: "inbox",
|
|
@@ -18595,7 +18030,7 @@ var init_metrics = __esm({
|
|
|
18595
18030
|
});
|
|
18596
18031
|
|
|
18597
18032
|
// ../core/src/feedback.ts
|
|
18598
|
-
import { readFileSync as
|
|
18033
|
+
import { readFileSync as readFileSync10, existsSync as existsSync13 } from "node:fs";
|
|
18599
18034
|
import { dirname as dirname6, join as join15, resolve as resolve9 } from "node:path";
|
|
18600
18035
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
18601
18036
|
function feedbackEnabled() {
|
|
@@ -18736,7 +18171,7 @@ var init_feedback = __esm({
|
|
|
18736
18171
|
handleFeedbackWidgetJs = (_req, res) => {
|
|
18737
18172
|
let body;
|
|
18738
18173
|
if (existsSync13(WIDGET_BUNDLE_PATH)) {
|
|
18739
|
-
body =
|
|
18174
|
+
body = readFileSync10(WIDGET_BUNDLE_PATH);
|
|
18740
18175
|
} else {
|
|
18741
18176
|
body = "console.warn('tina4-feedback-widget bundle not built yet');";
|
|
18742
18177
|
}
|
|
@@ -20444,7 +19879,7 @@ __export(errorOverlay_exports, {
|
|
|
20444
19879
|
renderErrorOverlay: () => renderErrorOverlay,
|
|
20445
19880
|
renderProductionError: () => renderProductionError
|
|
20446
19881
|
});
|
|
20447
|
-
import { readFileSync as
|
|
19882
|
+
import { readFileSync as readFileSync12, statSync as statSync10 } from "node:fs";
|
|
20448
19883
|
import { resolve as resolve11 } from "node:path";
|
|
20449
19884
|
function esc(text) {
|
|
20450
19885
|
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
@@ -20468,7 +19903,7 @@ function parseStack(stack) {
|
|
|
20468
19903
|
function readSourceLines(filename, lineno) {
|
|
20469
19904
|
try {
|
|
20470
19905
|
const absPath = resolve11(filename);
|
|
20471
|
-
const content =
|
|
19906
|
+
const content = readFileSync12(absPath, "utf-8");
|
|
20472
19907
|
const allLines = content.split("\n");
|
|
20473
19908
|
const start2 = Math.max(0, lineno - CONTEXT_LINES - 1);
|
|
20474
19909
|
const end = Math.min(allLines.length, lineno + CONTEXT_LINES);
|
|
@@ -21732,8 +21167,8 @@ __export(context_exports, {
|
|
|
21732
21167
|
fts5Supported: () => fts5Supported
|
|
21733
21168
|
});
|
|
21734
21169
|
import { DatabaseSync as DatabaseSync3 } from "node:sqlite";
|
|
21735
|
-
import { existsSync as existsSync15, mkdirSync as mkdirSync11, readFileSync as
|
|
21736
|
-
import { basename as
|
|
21170
|
+
import { existsSync as existsSync15, mkdirSync as mkdirSync11, readFileSync as readFileSync13, readdirSync as readdirSync9, realpathSync as realpathSync2 } from "node:fs";
|
|
21171
|
+
import { basename as basename4, dirname as dirname8, extname as extname6, isAbsolute as isAbsolute5, join as join17, relative as relative4, resolve as resolve12 } from "node:path";
|
|
21737
21172
|
function fts5Supported() {
|
|
21738
21173
|
try {
|
|
21739
21174
|
const conn = new DatabaseSync3(":memory:");
|
|
@@ -21756,7 +21191,7 @@ function realResolve(abs) {
|
|
|
21756
21191
|
} catch {
|
|
21757
21192
|
}
|
|
21758
21193
|
try {
|
|
21759
|
-
return join17(realpathSync2(dirname8(abs)),
|
|
21194
|
+
return join17(realpathSync2(dirname8(abs)), basename4(abs));
|
|
21760
21195
|
} catch {
|
|
21761
21196
|
return abs;
|
|
21762
21197
|
}
|
|
@@ -21869,7 +21304,7 @@ var init_context = __esm({
|
|
|
21869
21304
|
// ── indexing ───────────────────────────────────────────────
|
|
21870
21305
|
static chunksFor(label, text) {
|
|
21871
21306
|
const ext = extname6(label).toLowerCase();
|
|
21872
|
-
const special = SPECIAL_FILES.has(
|
|
21307
|
+
const special = SPECIAL_FILES.has(basename4(label).toLowerCase());
|
|
21873
21308
|
if (CODE_EXTS.has(ext) || CONFIG_EXTS.has(ext) || special) {
|
|
21874
21309
|
return chunkCode(text, label);
|
|
21875
21310
|
}
|
|
@@ -21886,7 +21321,7 @@ var init_context = __esm({
|
|
|
21886
21321
|
const stored = label != null ? String(label) : String(file);
|
|
21887
21322
|
let text;
|
|
21888
21323
|
try {
|
|
21889
|
-
text =
|
|
21324
|
+
text = readFileSync13(file, "utf-8");
|
|
21890
21325
|
} catch {
|
|
21891
21326
|
return 0;
|
|
21892
21327
|
}
|
|
@@ -21966,7 +21401,7 @@ var init_context = __esm({
|
|
|
21966
21401
|
if (parts.some((seg) => SKIP_DIRS.has(seg)) || dirParts.some((seg) => seg.startsWith("."))) {
|
|
21967
21402
|
return -1;
|
|
21968
21403
|
}
|
|
21969
|
-
if (!_Context.eligible(
|
|
21404
|
+
if (!_Context.eligible(basename4(rel))) return -1;
|
|
21970
21405
|
const stored = rel;
|
|
21971
21406
|
if (!existsSync15(abs)) {
|
|
21972
21407
|
this.conn.prepare("DELETE FROM chunks WHERE path = ?").run(stored);
|
|
@@ -22058,7 +21493,7 @@ var init_context = __esm({
|
|
|
22058
21493
|
});
|
|
22059
21494
|
|
|
22060
21495
|
// ../core/src/websocketBackplane.ts
|
|
22061
|
-
import { randomUUID as
|
|
21496
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
22062
21497
|
function createBackplane(url) {
|
|
22063
21498
|
const backend = (process.env.TINA4_WS_BACKPLANE ?? "").trim().toLowerCase();
|
|
22064
21499
|
switch (backend) {
|
|
@@ -22088,7 +21523,7 @@ function buildEnvelope(src, kind, message, opts = {}) {
|
|
|
22088
21523
|
return envelope;
|
|
22089
21524
|
}
|
|
22090
21525
|
function randomInstanceId() {
|
|
22091
|
-
return
|
|
21526
|
+
return randomUUID3().replace(/-/g, "").slice(0, 16);
|
|
22092
21527
|
}
|
|
22093
21528
|
var RedisBackplane, NATSBackplane, WS_BACKPLANE_CHANNEL, WsBackplaneManager;
|
|
22094
21529
|
var init_websocketBackplane = __esm({
|
|
@@ -22316,7 +21751,7 @@ __export(websocket_exports, {
|
|
|
22316
21751
|
});
|
|
22317
21752
|
import { createServer } from "node:http";
|
|
22318
21753
|
import { createHash as createHash4 } from "node:crypto";
|
|
22319
|
-
import { randomUUID as
|
|
21754
|
+
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
22320
21755
|
function computeAcceptKey(key) {
|
|
22321
21756
|
return createHash4("sha1").update(key + MAGIC_STRING).digest("base64");
|
|
22322
21757
|
}
|
|
@@ -22444,7 +21879,7 @@ function parseFrame(data) {
|
|
|
22444
21879
|
return { fin: !!fin, opcode, payload: Buffer.from(payload), bytesConsumed: offset + payloadLen };
|
|
22445
21880
|
}
|
|
22446
21881
|
function createRouteConnection(socket, path8, headers, params, auth) {
|
|
22447
|
-
const id =
|
|
21882
|
+
const id = randomUUID4().slice(0, 8);
|
|
22448
21883
|
const send = (message) => {
|
|
22449
21884
|
try {
|
|
22450
21885
|
socket.write(buildFrame(OP_TEXT, Buffer.from(message, "utf-8")));
|
|
@@ -23001,7 +22436,7 @@ var init_websocket = __esm({
|
|
|
23001
22436
|
}
|
|
23002
22437
|
responseLines.push("", "");
|
|
23003
22438
|
socket.write(responseLines.join("\r\n"));
|
|
23004
|
-
const clientId =
|
|
22439
|
+
const clientId = randomUUID4().slice(0, 8);
|
|
23005
22440
|
const client = {
|
|
23006
22441
|
id: clientId,
|
|
23007
22442
|
socket,
|
|
@@ -23289,7 +22724,7 @@ var init_websocket = __esm({
|
|
|
23289
22724
|
|
|
23290
22725
|
// ../core/src/queueBackends/rabbitmqBackend.ts
|
|
23291
22726
|
import { execFileSync } from "node:child_process";
|
|
23292
|
-
import { randomUUID as
|
|
22727
|
+
import { randomUUID as randomUUID5 } from "node:crypto";
|
|
23293
22728
|
function parseAmqpUrl(url) {
|
|
23294
22729
|
const config = {};
|
|
23295
22730
|
let rest = url.replace(/^amqps:\/\//, "").replace(/^amqp:\/\//, "");
|
|
@@ -23733,7 +23168,7 @@ var init_rabbitmqBackend = __esm({
|
|
|
23733
23168
|
}
|
|
23734
23169
|
}
|
|
23735
23170
|
push(queue, payload, _delay) {
|
|
23736
|
-
const id =
|
|
23171
|
+
const id = randomUUID5();
|
|
23737
23172
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
23738
23173
|
const job = {
|
|
23739
23174
|
id,
|
|
@@ -23772,7 +23207,7 @@ var init_rabbitmqBackend = __esm({
|
|
|
23772
23207
|
|
|
23773
23208
|
// ../core/src/queueBackends/kafkaBackend.ts
|
|
23774
23209
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
23775
|
-
import { randomUUID as
|
|
23210
|
+
import { randomUUID as randomUUID6 } from "node:crypto";
|
|
23776
23211
|
function kafkaSecurityConfig(env = process.env) {
|
|
23777
23212
|
const mapping = [
|
|
23778
23213
|
["security.protocol", "SECURITY_PROTOCOL"],
|
|
@@ -23796,7 +23231,7 @@ var init_kafkaBackend = __esm({
|
|
|
23796
23231
|
"use strict";
|
|
23797
23232
|
API_PRODUCE = 0;
|
|
23798
23233
|
API_FETCH = 1;
|
|
23799
|
-
KafkaBackend = class {
|
|
23234
|
+
KafkaBackend = class _KafkaBackend {
|
|
23800
23235
|
brokers;
|
|
23801
23236
|
groupId;
|
|
23802
23237
|
constructor(config) {
|
|
@@ -24158,12 +23593,15 @@ var init_kafkaBackend = __esm({
|
|
|
24158
23593
|
if (errCode === 0) {
|
|
24159
23594
|
finish("__PUBLISHED__", 0);
|
|
24160
23595
|
} else {
|
|
23596
|
+
// Report the CODE, not just "it failed" \u2014 the caller decides
|
|
23597
|
+
// whether it is retriable (3/5, the async topic-creation race)
|
|
23598
|
+
// or fatal (e.g. 29 TOPIC_AUTHORIZATION_FAILED).
|
|
24161
23599
|
process.stderr.write("Produce error code " + errCode);
|
|
24162
|
-
finish("
|
|
23600
|
+
finish("__PRODUCEERROR__" + errCode, 0);
|
|
24163
23601
|
}
|
|
24164
23602
|
} catch (e) {
|
|
24165
23603
|
process.stderr.write("produce parse: " + e.message);
|
|
24166
|
-
finish("
|
|
23604
|
+
finish("__PARSEERROR__produce: " + e.message, 0);
|
|
24167
23605
|
}
|
|
24168
23606
|
return;
|
|
24169
23607
|
} else if (operation === "get") {
|
|
@@ -24177,6 +23615,7 @@ var init_kafkaBackend = __esm({
|
|
|
24177
23615
|
pos += 4; // throttleTimeMs (v1+)
|
|
24178
23616
|
const topicCount = buffer.readInt32BE(pos); pos += 4;
|
|
24179
23617
|
let out = "__EMPTY__";
|
|
23618
|
+
let fatalCode = 0;
|
|
24180
23619
|
for (let t = 0; t < topicCount; t++) {
|
|
24181
23620
|
const tl = buffer.readInt16BE(pos); pos += 2 + tl;
|
|
24182
23621
|
const pc = buffer.readInt32BE(pos); pos += 4;
|
|
@@ -24188,6 +23627,14 @@ var init_kafkaBackend = __esm({
|
|
|
24188
23627
|
const abortedCount = buffer.readInt32BE(pos); pos += 4;
|
|
24189
23628
|
if (abortedCount > 0) pos += abortedCount * 16; // (-1 => none, skip)
|
|
24190
23629
|
const recSetSize = buffer.readInt32BE(pos); pos += 4;
|
|
23630
|
+
// 3 = UNKNOWN_TOPIC_OR_PARTITION, 5 = LEADER_NOT_AVAILABLE:
|
|
23631
|
+
// "nothing to read here yet", which a consumer that starts
|
|
23632
|
+
// before its producer hits on every cold start. Any OTHER code
|
|
23633
|
+
// (29 TOPIC_AUTHORIZATION_FAILED, 13 STALE_CONTROLLER_EPOCH, \u2026)
|
|
23634
|
+
// is a real failure and must NOT be reported as an empty queue.
|
|
23635
|
+
if (errCode !== 0 && errCode !== 3 && errCode !== 5) {
|
|
23636
|
+
fatalCode = errCode;
|
|
23637
|
+
}
|
|
24191
23638
|
if (errCode === 0 && recSetSize > 0) {
|
|
24192
23639
|
const val = firstRecordValue(buffer, pos, pos + recSetSize);
|
|
24193
23640
|
if (val !== null) out = val;
|
|
@@ -24195,21 +23642,33 @@ var init_kafkaBackend = __esm({
|
|
|
24195
23642
|
pos += recSetSize > 0 ? recSetSize : 0;
|
|
24196
23643
|
}
|
|
24197
23644
|
}
|
|
23645
|
+
if (fatalCode !== 0) {
|
|
23646
|
+
process.stderr.write("Fetch error code " + fatalCode);
|
|
23647
|
+
finish("__FETCHERROR__" + fatalCode, 0);
|
|
23648
|
+
return;
|
|
23649
|
+
}
|
|
24198
23650
|
finish(out, 0);
|
|
24199
23651
|
} catch (e) {
|
|
23652
|
+
// A parse failure is NOT an empty queue either \u2014 say so.
|
|
24200
23653
|
process.stderr.write("fetch parse: " + e.message);
|
|
24201
|
-
finish("
|
|
23654
|
+
finish("__PARSEERROR__fetch: " + e.message, 0);
|
|
24202
23655
|
}
|
|
24203
23656
|
return;
|
|
24204
23657
|
}
|
|
24205
23658
|
});
|
|
24206
23659
|
|
|
23660
|
+
// Report the reason on STDOUT and exit 0. Writing it to stderr and
|
|
23661
|
+
// exiting non-zero LOST it: stderr to a pipe is an async write and
|
|
23662
|
+
// process.exit() truncates it, so the parent saw an empty stderr and fell
|
|
23663
|
+
// back to execFileSync's message -- which embeds this entire script.
|
|
23664
|
+
// stdout is flushed by finish()'s write callback, so it survives.
|
|
24207
23665
|
sock.on("error", (err) => {
|
|
24208
|
-
|
|
24209
|
-
finish("", 1);
|
|
23666
|
+
finish("__TRANSPORTERROR__" + err.message, 0);
|
|
24210
23667
|
});
|
|
24211
23668
|
|
|
24212
|
-
var timer = setTimeout(() => {
|
|
23669
|
+
var timer = setTimeout(() => {
|
|
23670
|
+
finish("__TRANSPORTERROR__timed out after 10s talking to " + host + ":" + port, 0);
|
|
23671
|
+
}, 10000);
|
|
24213
23672
|
`;
|
|
24214
23673
|
try {
|
|
24215
23674
|
const result = execFileSync2(process.execPath, ["-e", script], {
|
|
@@ -24218,12 +23677,51 @@ var init_kafkaBackend = __esm({
|
|
|
24218
23677
|
stdio: ["pipe", "pipe", "pipe"]
|
|
24219
23678
|
});
|
|
24220
23679
|
return result;
|
|
24221
|
-
} catch {
|
|
24222
|
-
|
|
23680
|
+
} catch (err) {
|
|
23681
|
+
const e = err;
|
|
23682
|
+
const reason = String(e.stderr ?? "").trim() || e.message || "unknown error";
|
|
23683
|
+
const firstLine2 = reason.split("\n", 1)[0].slice(0, 200);
|
|
23684
|
+
return "__TRANSPORTERROR__" + firstLine2;
|
|
23685
|
+
}
|
|
23686
|
+
}
|
|
23687
|
+
/**
|
|
23688
|
+
* Sleep synchronously between produce retries.
|
|
23689
|
+
*
|
|
23690
|
+
* `push()` is synchronous (the whole backend drives its socket through a child
|
|
23691
|
+
* process), so there is no event loop to await on. `Atomics.wait` on a
|
|
23692
|
+
* SharedArrayBuffer is the stdlib way to block a thread for a fixed time --
|
|
23693
|
+
* no dependency, no busy-wait burning CPU.
|
|
23694
|
+
*/
|
|
23695
|
+
static sleepSync(ms) {
|
|
23696
|
+
const shared = new Int32Array(new SharedArrayBuffer(4));
|
|
23697
|
+
Atomics.wait(shared, 0, 0, ms);
|
|
23698
|
+
}
|
|
23699
|
+
/**
|
|
23700
|
+
* Turn a sentinel from the protocol child into a thrown error, or return.
|
|
23701
|
+
*
|
|
23702
|
+
* The wording matches the Python and PHP backends exactly -- the parity rule
|
|
23703
|
+
* covers user-visible error messages, not just behaviour.
|
|
23704
|
+
*/
|
|
23705
|
+
static assertNoError(result, operation, topic) {
|
|
23706
|
+
const fatal = /^__(PRODUCEERROR|FETCHERROR)__(\d+)/.exec(result);
|
|
23707
|
+
if (fatal) {
|
|
23708
|
+
throw new Error(
|
|
23709
|
+
`Kafka rejected the ${operation} for topic ${topic}: error code ${fatal[2]}`
|
|
23710
|
+
);
|
|
23711
|
+
}
|
|
23712
|
+
if (result.startsWith("__TRANSPORTERROR__")) {
|
|
23713
|
+
throw new Error(
|
|
23714
|
+
`Kafka ${operation} for topic ${topic} failed: ` + result.slice("__TRANSPORTERROR__".length)
|
|
23715
|
+
);
|
|
23716
|
+
}
|
|
23717
|
+
if (result.startsWith("__PARSEERROR__")) {
|
|
23718
|
+
throw new Error(
|
|
23719
|
+
`Kafka ${operation} for topic ${topic} returned an unreadable response: ` + result.slice("__PARSEERROR__".length)
|
|
23720
|
+
);
|
|
24223
23721
|
}
|
|
24224
23722
|
}
|
|
24225
23723
|
push(queue, payload, _delay) {
|
|
24226
|
-
const id =
|
|
23724
|
+
const id = randomUUID6();
|
|
24227
23725
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
24228
23726
|
const job = {
|
|
24229
23727
|
id,
|
|
@@ -24233,14 +23731,25 @@ var init_kafkaBackend = __esm({
|
|
|
24233
23731
|
attempts: 0,
|
|
24234
23732
|
delayUntil: null
|
|
24235
23733
|
};
|
|
24236
|
-
const
|
|
24237
|
-
|
|
24238
|
-
|
|
23734
|
+
const body = JSON.stringify(job);
|
|
23735
|
+
let result = "";
|
|
23736
|
+
for (let attempt = 1; attempt <= 10; attempt++) {
|
|
23737
|
+
result = this.execSync("publish", queue, body);
|
|
23738
|
+
if (result.includes("__PUBLISHED__")) {
|
|
23739
|
+
return id;
|
|
23740
|
+
}
|
|
23741
|
+
const retriable = /^__PRODUCEERROR__(3|5)\b/.test(result);
|
|
23742
|
+
if (!retriable || attempt === 10) {
|
|
23743
|
+
break;
|
|
23744
|
+
}
|
|
23745
|
+
_KafkaBackend.sleepSync(200);
|
|
24239
23746
|
}
|
|
24240
|
-
|
|
23747
|
+
_KafkaBackend.assertNoError(result, "produce", queue);
|
|
23748
|
+
throw new Error(`Kafka publish failed for topic ${queue}: ${result || "no response"}`);
|
|
24241
23749
|
}
|
|
24242
23750
|
pop(queue) {
|
|
24243
23751
|
const result = this.execSync("get", queue);
|
|
23752
|
+
_KafkaBackend.assertNoError(result, "fetch", queue);
|
|
24244
23753
|
if (!result || result === "__EMPTY__" || result === "__UNSUPPORTED__") return null;
|
|
24245
23754
|
try {
|
|
24246
23755
|
return JSON.parse(result);
|
|
@@ -24258,7 +23767,7 @@ var init_kafkaBackend = __esm({
|
|
|
24258
23767
|
});
|
|
24259
23768
|
|
|
24260
23769
|
// ../core/src/queueBackends/mongoBackend.ts
|
|
24261
|
-
import { randomUUID as
|
|
23770
|
+
import { randomUUID as randomUUID7 } from "node:crypto";
|
|
24262
23771
|
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
24263
23772
|
var MongoBackend2;
|
|
24264
23773
|
var init_mongoBackend = __esm({
|
|
@@ -24571,7 +24080,7 @@ var init_mongoBackend = __esm({
|
|
|
24571
24080
|
}
|
|
24572
24081
|
}
|
|
24573
24082
|
push(queue, payload, delay) {
|
|
24574
|
-
const id =
|
|
24083
|
+
const id = randomUUID7();
|
|
24575
24084
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
24576
24085
|
const job = {
|
|
24577
24086
|
id,
|
|
@@ -24706,9 +24215,9 @@ var init_job = __esm({
|
|
|
24706
24215
|
});
|
|
24707
24216
|
|
|
24708
24217
|
// ../core/src/queueBackends/liteBackend.ts
|
|
24709
|
-
import { mkdirSync as mkdirSync12, readdirSync as readdirSync10, readFileSync as
|
|
24218
|
+
import { mkdirSync as mkdirSync12, readdirSync as readdirSync10, readFileSync as readFileSync14, writeFileSync as writeFileSync9, unlinkSync as unlinkSync5, existsSync as existsSync16 } from "node:fs";
|
|
24710
24219
|
import { join as join18 } from "node:path";
|
|
24711
|
-
import { randomUUID as
|
|
24220
|
+
import { randomUUID as randomUUID8 } from "node:crypto";
|
|
24712
24221
|
var LiteBackend;
|
|
24713
24222
|
var init_liteBackend = __esm({
|
|
24714
24223
|
"../core/src/queueBackends/liteBackend.ts"() {
|
|
@@ -24760,7 +24269,7 @@ var init_liteBackend = __esm({
|
|
|
24760
24269
|
}
|
|
24761
24270
|
push(queue, payload, delay, priority) {
|
|
24762
24271
|
const dir = this.ensureDir(queue);
|
|
24763
|
-
const id =
|
|
24272
|
+
const id = randomUUID8();
|
|
24764
24273
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
24765
24274
|
const job = {
|
|
24766
24275
|
id,
|
|
@@ -24796,7 +24305,7 @@ var init_liteBackend = __esm({
|
|
|
24796
24305
|
const filePath = join18(dir, filename);
|
|
24797
24306
|
let job;
|
|
24798
24307
|
try {
|
|
24799
|
-
job = JSON.parse(
|
|
24308
|
+
job = JSON.parse(readFileSync14(filePath, "utf-8"));
|
|
24800
24309
|
} catch {
|
|
24801
24310
|
continue;
|
|
24802
24311
|
}
|
|
@@ -24860,7 +24369,7 @@ var init_liteBackend = __esm({
|
|
|
24860
24369
|
const filePath = join18(reservedDir, filename);
|
|
24861
24370
|
let record;
|
|
24862
24371
|
try {
|
|
24863
|
-
record = JSON.parse(
|
|
24372
|
+
record = JSON.parse(readFileSync14(filePath, "utf-8"));
|
|
24864
24373
|
} catch {
|
|
24865
24374
|
continue;
|
|
24866
24375
|
}
|
|
@@ -24973,7 +24482,7 @@ var init_liteBackend = __esm({
|
|
|
24973
24482
|
let count = 0;
|
|
24974
24483
|
for (const file of files) {
|
|
24975
24484
|
try {
|
|
24976
|
-
const job = JSON.parse(
|
|
24485
|
+
const job = JSON.parse(readFileSync14(join18(scanDir, file), "utf-8"));
|
|
24977
24486
|
if (job.status === status2) count++;
|
|
24978
24487
|
} catch {
|
|
24979
24488
|
}
|
|
@@ -25030,7 +24539,7 @@ var init_liteBackend = __esm({
|
|
|
25030
24539
|
const files = readdirSync10(dir).filter((f) => f.endsWith(".queue-data")).sort();
|
|
25031
24540
|
for (const file of files) {
|
|
25032
24541
|
try {
|
|
25033
|
-
const job = JSON.parse(
|
|
24542
|
+
const job = JSON.parse(readFileSync14(join18(dir, file), "utf-8"));
|
|
25034
24543
|
const attempts = job.attempts || 0;
|
|
25035
24544
|
if (attempts > 0 && attempts < maxRetries) {
|
|
25036
24545
|
results.push(job);
|
|
@@ -25056,7 +24565,7 @@ var init_liteBackend = __esm({
|
|
|
25056
24565
|
const failedDir = join18(this.basePath, q, "failed");
|
|
25057
24566
|
const filePath = join18(failedDir, `${jobId}.queue-data`);
|
|
25058
24567
|
if (existsSync16(filePath)) {
|
|
25059
|
-
const job = JSON.parse(
|
|
24568
|
+
const job = JSON.parse(readFileSync14(filePath, "utf-8"));
|
|
25060
24569
|
job.status = "pending";
|
|
25061
24570
|
job.attempts = (job.attempts || 0) + 1;
|
|
25062
24571
|
job.error = void 0;
|
|
@@ -25080,7 +24589,7 @@ var init_liteBackend = __esm({
|
|
|
25080
24589
|
const files = readdirSync10(failedDir).filter((f) => f.endsWith(".queue-data")).sort();
|
|
25081
24590
|
for (const file of files) {
|
|
25082
24591
|
try {
|
|
25083
|
-
const job = JSON.parse(
|
|
24592
|
+
const job = JSON.parse(readFileSync14(join18(failedDir, file), "utf-8"));
|
|
25084
24593
|
if ((job.attempts || 0) >= maxRetries) {
|
|
25085
24594
|
job.status = "dead";
|
|
25086
24595
|
results.push(job);
|
|
@@ -25114,7 +24623,7 @@ var init_liteBackend = __esm({
|
|
|
25114
24623
|
const files = readdirSync10(dir).filter((f) => f.endsWith(".queue-data"));
|
|
25115
24624
|
for (const file of files) {
|
|
25116
24625
|
try {
|
|
25117
|
-
const job = JSON.parse(
|
|
24626
|
+
const job = JSON.parse(readFileSync14(join18(dir, file), "utf-8"));
|
|
25118
24627
|
if (job.status === status2) {
|
|
25119
24628
|
unlinkSync5(join18(dir, file));
|
|
25120
24629
|
count++;
|
|
@@ -25141,7 +24650,7 @@ var init_liteBackend = __esm({
|
|
|
25141
24650
|
for (const file of files) {
|
|
25142
24651
|
try {
|
|
25143
24652
|
const filePath = join18(failedDir, file);
|
|
25144
|
-
const job = JSON.parse(
|
|
24653
|
+
const job = JSON.parse(readFileSync14(filePath, "utf-8"));
|
|
25145
24654
|
if ((job.attempts || 0) >= maxRetries) {
|
|
25146
24655
|
continue;
|
|
25147
24656
|
}
|
|
@@ -25173,7 +24682,7 @@ var init_liteBackend = __esm({
|
|
|
25173
24682
|
const filePath = join18(dir, file);
|
|
25174
24683
|
let job;
|
|
25175
24684
|
try {
|
|
25176
|
-
job = JSON.parse(
|
|
24685
|
+
job = JSON.parse(readFileSync14(filePath, "utf-8"));
|
|
25177
24686
|
} catch {
|
|
25178
24687
|
continue;
|
|
25179
24688
|
}
|
|
@@ -27663,7 +27172,7 @@ ${end}
|
|
|
27663
27172
|
|
|
27664
27173
|
// ../core/src/devAdmin.ts
|
|
27665
27174
|
import { cpus as osCpus } from "node:os";
|
|
27666
|
-
import { readFileSync as
|
|
27175
|
+
import { readFileSync as readFileSync18, writeFileSync as writeFileSync13, existsSync as existsSync20, readdirSync as readdirSync14, mkdirSync as mkdirSync15, copyFileSync as copyFileSync2, statSync as statSync15 } from "node:fs";
|
|
27667
27176
|
import { join as join22, dirname as dirname10, resolve as resolve16, relative as relative8 } from "node:path";
|
|
27668
27177
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
27669
27178
|
import { timingSafeEqual as timingSafeEqual2 } from "node:crypto";
|
|
@@ -27849,7 +27358,7 @@ function resolveDevEnvVar(key) {
|
|
|
27849
27358
|
if (live !== void 0 && live !== "") return live;
|
|
27850
27359
|
const envPath = join22(process.cwd(), ".env");
|
|
27851
27360
|
if (!existsSync20(envPath)) return "";
|
|
27852
|
-
for (const line of
|
|
27361
|
+
for (const line of readFileSync18(envPath, "utf-8").split("\n")) {
|
|
27853
27362
|
const t = line.trim();
|
|
27854
27363
|
if (!t || t.startsWith("#") || !t.includes("=")) continue;
|
|
27855
27364
|
const eq = t.indexOf("=");
|
|
@@ -27859,7 +27368,7 @@ function resolveDevEnvVar(key) {
|
|
|
27859
27368
|
}
|
|
27860
27369
|
function upsertDevEnvVar(key, value) {
|
|
27861
27370
|
const envPath = join22(process.cwd(), ".env");
|
|
27862
|
-
const lines = existsSync20(envPath) ?
|
|
27371
|
+
const lines = existsSync20(envPath) ? readFileSync18(envPath, "utf-8").split("\n") : [];
|
|
27863
27372
|
let found = false;
|
|
27864
27373
|
const out = [];
|
|
27865
27374
|
for (const line of lines) {
|
|
@@ -27892,7 +27401,7 @@ function parseEnvFile() {
|
|
|
27892
27401
|
const envPath = join22(process.cwd(), ".env");
|
|
27893
27402
|
const result = {};
|
|
27894
27403
|
if (!existsSync20(envPath)) return result;
|
|
27895
|
-
const lines =
|
|
27404
|
+
const lines = readFileSync18(envPath, "utf-8").split("\n");
|
|
27896
27405
|
for (const line of lines) {
|
|
27897
27406
|
const trimmed = line.trim();
|
|
27898
27407
|
if (!trimmed || trimmed.startsWith("#") || !trimmed.includes("=")) continue;
|
|
@@ -28249,7 +27758,7 @@ var init_devAdmin = __esm({
|
|
|
28249
27758
|
for (const rel of ["../../../package.json", "../../package.json"]) {
|
|
28250
27759
|
const p = resolve16(__dirname2, rel);
|
|
28251
27760
|
if (existsSync20(p)) {
|
|
28252
|
-
const pkg = JSON.parse(
|
|
27761
|
+
const pkg = JSON.parse(readFileSync18(p, "utf-8"));
|
|
28253
27762
|
if (pkg.version) return pkg.version;
|
|
28254
27763
|
}
|
|
28255
27764
|
}
|
|
@@ -28907,7 +28416,7 @@ var init_devAdmin = __esm({
|
|
|
28907
28416
|
for (const filename of readdirSync14(queueDir).sort()) {
|
|
28908
28417
|
if (!filename.endsWith(".queue-data")) continue;
|
|
28909
28418
|
try {
|
|
28910
|
-
const job = JSON.parse(
|
|
28419
|
+
const job = JSON.parse(readFileSync18(join22(queueDir, filename), "utf-8"));
|
|
28911
28420
|
jobs.push(mapQueueJob(job, topic, "pending"));
|
|
28912
28421
|
} catch {
|
|
28913
28422
|
}
|
|
@@ -29393,7 +28902,7 @@ var init_devAdmin = __esm({
|
|
|
29393
28902
|
}
|
|
29394
28903
|
try {
|
|
29395
28904
|
const envPath = join22(process.cwd(), ".env");
|
|
29396
|
-
const lines = existsSync20(envPath) ?
|
|
28905
|
+
const lines = existsSync20(envPath) ? readFileSync18(envPath, "utf-8").split("\n") : [];
|
|
29397
28906
|
const keysFound = { TINA4_DATABASE_URL: false, TINA4_DATABASE_USERNAME: false, TINA4_DATABASE_PASSWORD: false };
|
|
29398
28907
|
const newLines = [];
|
|
29399
28908
|
for (const line of lines) {
|
|
@@ -29439,7 +28948,7 @@ var init_devAdmin = __esm({
|
|
|
29439
28948
|
const metaFile = join22(entryPath, "meta.json");
|
|
29440
28949
|
if (statSync15(entryPath).isDirectory() && existsSync20(metaFile)) {
|
|
29441
28950
|
try {
|
|
29442
|
-
const meta = JSON.parse(
|
|
28951
|
+
const meta = JSON.parse(readFileSync18(metaFile, "utf-8"));
|
|
29443
28952
|
meta.id = entry;
|
|
29444
28953
|
const srcDir = join22(entryPath, "src");
|
|
29445
28954
|
if (existsSync20(srcDir)) {
|
|
@@ -29654,7 +29163,7 @@ var init_devAdmin = __esm({
|
|
|
29654
29163
|
return;
|
|
29655
29164
|
}
|
|
29656
29165
|
try {
|
|
29657
|
-
const content =
|
|
29166
|
+
const content = readFileSync18(target, "utf-8");
|
|
29658
29167
|
const path8 = relative8(root, target);
|
|
29659
29168
|
res.json({ path: path8, content, language: devAdminLanguage(path8), bytes: Buffer.byteLength(content, "utf-8") });
|
|
29660
29169
|
} catch (e) {
|
|
@@ -29696,7 +29205,7 @@ var init_devAdmin = __esm({
|
|
|
29696
29205
|
return;
|
|
29697
29206
|
}
|
|
29698
29207
|
try {
|
|
29699
|
-
const buf =
|
|
29208
|
+
const buf = readFileSync18(target);
|
|
29700
29209
|
const ext = target.slice(target.lastIndexOf(".") + 1).toLowerCase();
|
|
29701
29210
|
const mime = {
|
|
29702
29211
|
js: "application/javascript",
|
|
@@ -30124,7 +29633,7 @@ var init_devAdmin = __esm({
|
|
|
30124
29633
|
});
|
|
30125
29634
|
|
|
30126
29635
|
// ../core/src/i18n.ts
|
|
30127
|
-
import { readFileSync as
|
|
29636
|
+
import { readFileSync as readFileSync19, readdirSync as readdirSync15, existsSync as existsSync21 } from "node:fs";
|
|
30128
29637
|
import { join as join23, resolve as resolve17 } from "node:path";
|
|
30129
29638
|
var I18n;
|
|
30130
29639
|
var init_i18n = __esm({
|
|
@@ -30221,7 +29730,7 @@ var init_i18n = __esm({
|
|
|
30221
29730
|
const filePath = join23(this._localeDir, `${locale}.json`);
|
|
30222
29731
|
if (existsSync21(filePath)) {
|
|
30223
29732
|
try {
|
|
30224
|
-
const raw =
|
|
29733
|
+
const raw = readFileSync19(filePath, "utf-8");
|
|
30225
29734
|
const data = JSON.parse(raw);
|
|
30226
29735
|
this._translations.set(locale, _I18n._flatten(data));
|
|
30227
29736
|
return;
|
|
@@ -30234,7 +29743,7 @@ var init_i18n = __esm({
|
|
|
30234
29743
|
const yamlPath = join23(this._localeDir, `${locale}${ext}`);
|
|
30235
29744
|
if (existsSync21(yamlPath)) {
|
|
30236
29745
|
try {
|
|
30237
|
-
const raw =
|
|
29746
|
+
const raw = readFileSync19(yamlPath, "utf-8");
|
|
30238
29747
|
const data = _I18n._parseSimpleYaml(raw);
|
|
30239
29748
|
this._translations.set(locale, _I18n._flatten(data));
|
|
30240
29749
|
return;
|
|
@@ -30902,6 +30411,36 @@ var init_docsAutoDiscovery = __esm({
|
|
|
30902
30411
|
}
|
|
30903
30412
|
});
|
|
30904
30413
|
|
|
30414
|
+
// ../core/src/sessionHandlers/childError.ts
|
|
30415
|
+
function childFailureReason(err) {
|
|
30416
|
+
const e = err ?? {};
|
|
30417
|
+
const stderr = String(e.stderr ?? "").trim();
|
|
30418
|
+
if (stderr !== "") {
|
|
30419
|
+
return firstLine(stderr);
|
|
30420
|
+
}
|
|
30421
|
+
if (e.code === "ETIMEDOUT" || e.signal) {
|
|
30422
|
+
return `timed out or was killed (${e.code ?? e.signal})`;
|
|
30423
|
+
}
|
|
30424
|
+
if (typeof e.status === "number" && e.status !== 0) {
|
|
30425
|
+
return `child exited with code ${e.status} and no output`;
|
|
30426
|
+
}
|
|
30427
|
+
return firstLine(String(e.message ?? "unknown error"));
|
|
30428
|
+
}
|
|
30429
|
+
function firstLine(text) {
|
|
30430
|
+
const line = text.split("\n", 1)[0] ?? "";
|
|
30431
|
+
return line.length > MAX_FALLBACK ? `${line.slice(0, MAX_FALLBACK)}...` : line;
|
|
30432
|
+
}
|
|
30433
|
+
function childFailureError(label, err) {
|
|
30434
|
+
return new Error(`${label} command failed: ${childFailureReason(err)}`);
|
|
30435
|
+
}
|
|
30436
|
+
var MAX_FALLBACK;
|
|
30437
|
+
var init_childError = __esm({
|
|
30438
|
+
"../core/src/sessionHandlers/childError.ts"() {
|
|
30439
|
+
"use strict";
|
|
30440
|
+
MAX_FALLBACK = 200;
|
|
30441
|
+
}
|
|
30442
|
+
});
|
|
30443
|
+
|
|
30905
30444
|
// ../core/src/sessionHandlers/respClient.ts
|
|
30906
30445
|
import { execFileSync as execFileSync4 } from "node:child_process";
|
|
30907
30446
|
function respCommandSync(target, args, label = "Redis") {
|
|
@@ -31021,7 +30560,7 @@ function respCommandSync(target, args, label = "Redis") {
|
|
|
31021
30560
|
stdio: ["pipe", "pipe", "pipe"]
|
|
31022
30561
|
});
|
|
31023
30562
|
} catch (err) {
|
|
31024
|
-
throw
|
|
30563
|
+
throw childFailureError(label, err);
|
|
31025
30564
|
}
|
|
31026
30565
|
if (result === "__NULL__") return "";
|
|
31027
30566
|
if (result.startsWith("__ERR__")) {
|
|
@@ -31032,6 +30571,7 @@ function respCommandSync(target, args, label = "Redis") {
|
|
|
31032
30571
|
var init_respClient = __esm({
|
|
31033
30572
|
"../core/src/sessionHandlers/respClient.ts"() {
|
|
31034
30573
|
"use strict";
|
|
30574
|
+
init_childError();
|
|
31035
30575
|
}
|
|
31036
30576
|
});
|
|
31037
30577
|
|
|
@@ -31050,6 +30590,7 @@ var moduleRequire, RedisNpmSessionHandler;
|
|
|
31050
30590
|
var init_redisHandler = __esm({
|
|
31051
30591
|
"../core/src/sessionHandlers/redisHandler.ts"() {
|
|
31052
30592
|
"use strict";
|
|
30593
|
+
init_childError();
|
|
31053
30594
|
init_respClient();
|
|
31054
30595
|
moduleRequire = createRequire8(import.meta.url);
|
|
31055
30596
|
RedisNpmSessionHandler = class {
|
|
@@ -31110,9 +30651,17 @@ var init_redisHandler = __esm({
|
|
|
31110
30651
|
(async () => {
|
|
31111
30652
|
try {
|
|
31112
30653
|
const redis = require("redis");
|
|
30654
|
+
// reconnectStrategy: false \u2014 this child runs ONE command and exits, so
|
|
30655
|
+
// retrying inside it is pointless: the handler is called again on the
|
|
30656
|
+
// next request anyway. With the driver's default strategy a refused
|
|
30657
|
+
// connection never rejects, the child hangs until execFileSync's 5s
|
|
30658
|
+
// timeout kills it, and the caller is told "timed out" when the truth
|
|
30659
|
+
// is "connection refused". Off, connect() rejects in ~5ms with the real
|
|
30660
|
+
// reason -- a better message AND no 5s stall per request when Redis is
|
|
30661
|
+
// down.
|
|
31113
30662
|
const clientOpts = useUrl
|
|
31114
|
-
? { url }
|
|
31115
|
-
: { socket: { host, port }, password: password || undefined, database: db };
|
|
30663
|
+
? { url, socket: { reconnectStrategy: false } }
|
|
30664
|
+
: { socket: { host, port, reconnectStrategy: false }, password: password || undefined, database: db };
|
|
31116
30665
|
const client = redis.createClient(clientOpts);
|
|
31117
30666
|
client.on("error", () => {});
|
|
31118
30667
|
await client.connect();
|
|
@@ -31126,8 +30675,10 @@ var init_redisHandler = __esm({
|
|
|
31126
30675
|
const out = (result === null || result === undefined) ? "__NULL__" : String(result);
|
|
31127
30676
|
process.stdout.write(out, () => process.exit(0));
|
|
31128
30677
|
} catch (err) {
|
|
31129
|
-
|
|
31130
|
-
process.exit(
|
|
30678
|
+
// Exit from the write CALLBACK: stderr to a pipe is an async write and
|
|
30679
|
+
// a bare process.exit() truncates it, which left the parent with an
|
|
30680
|
+
// empty stderr and nothing but execFileSync's script-dump message.
|
|
30681
|
+
process.stderr.write(String((err && err.message) || err), () => process.exit(1));
|
|
31131
30682
|
}
|
|
31132
30683
|
})();
|
|
31133
30684
|
`;
|
|
@@ -31139,7 +30690,7 @@ var init_redisHandler = __esm({
|
|
|
31139
30690
|
stdio: ["pipe", "pipe", "pipe"]
|
|
31140
30691
|
});
|
|
31141
30692
|
} catch (err) {
|
|
31142
|
-
throw
|
|
30693
|
+
throw childFailureError("Redis", err);
|
|
31143
30694
|
}
|
|
31144
30695
|
if (result === "__NULL__") return "";
|
|
31145
30696
|
return result;
|
|
@@ -31271,8 +30822,10 @@ function mongoCommandSync(target, command, args, label = "MongoDB") {
|
|
|
31271
30822
|
process.stdout.write(out, () => process.exit(0));
|
|
31272
30823
|
} catch (err) {
|
|
31273
30824
|
try { if (client) await client.close(); } catch (e) {}
|
|
31274
|
-
|
|
31275
|
-
process.exit(
|
|
30825
|
+
// Exit from the write CALLBACK: stderr to a pipe is an async write and
|
|
30826
|
+
// a bare process.exit() truncates it, which left the parent with an
|
|
30827
|
+
// empty stderr and nothing but execFileSync's script-dump message.
|
|
30828
|
+
process.stderr.write(String((err && err.message) || err), () => process.exit(1));
|
|
31276
30829
|
}
|
|
31277
30830
|
})();
|
|
31278
30831
|
} else {
|
|
@@ -31413,12 +30966,13 @@ function mongoCommandSync(target, command, args, label = "MongoDB") {
|
|
|
31413
30966
|
stdio: ["pipe", "pipe", "pipe"]
|
|
31414
30967
|
});
|
|
31415
30968
|
} catch (err) {
|
|
31416
|
-
throw
|
|
30969
|
+
throw childFailureError(label, err);
|
|
31417
30970
|
}
|
|
31418
30971
|
}
|
|
31419
30972
|
var init_mongoClient = __esm({
|
|
31420
30973
|
"../core/src/sessionHandlers/mongoClient.ts"() {
|
|
31421
30974
|
"use strict";
|
|
30975
|
+
init_childError();
|
|
31422
30976
|
}
|
|
31423
30977
|
});
|
|
31424
30978
|
|
|
@@ -31571,7 +31125,7 @@ __export(session_exports, {
|
|
|
31571
31125
|
sessionCookieName: () => sessionCookieName
|
|
31572
31126
|
});
|
|
31573
31127
|
import { randomBytes as randomBytes6 } from "node:crypto";
|
|
31574
|
-
import { existsSync as existsSync23, mkdirSync as mkdirSync18, readFileSync as
|
|
31128
|
+
import { existsSync as existsSync23, mkdirSync as mkdirSync18, readFileSync as readFileSync21, writeFileSync as writeFileSync16, unlinkSync as unlinkSync7, readdirSync as readdirSync16 } from "node:fs";
|
|
31575
31129
|
import { join as join25 } from "node:path";
|
|
31576
31130
|
function isSecureScheme(forwardedProto, socketEncrypted) {
|
|
31577
31131
|
const forwarded = (forwardedProto ?? "").trim();
|
|
@@ -31624,7 +31178,7 @@ var init_session = __esm({
|
|
|
31624
31178
|
const filePath = this.filePath(sessionId);
|
|
31625
31179
|
try {
|
|
31626
31180
|
if (!existsSync23(filePath)) return null;
|
|
31627
|
-
const raw =
|
|
31181
|
+
const raw = readFileSync21(filePath, "utf-8");
|
|
31628
31182
|
const wrapper = JSON.parse(raw);
|
|
31629
31183
|
if (wrapper._expires && wrapper._expires > 0 && Date.now() / 1e3 > wrapper._expires) {
|
|
31630
31184
|
try {
|
|
@@ -31660,7 +31214,7 @@ var init_session = __esm({
|
|
|
31660
31214
|
if (!file.endsWith(".json")) continue;
|
|
31661
31215
|
const fullPath = join25(this.storagePath, file);
|
|
31662
31216
|
try {
|
|
31663
|
-
const raw =
|
|
31217
|
+
const raw = readFileSync21(fullPath, "utf-8");
|
|
31664
31218
|
const wrapper = JSON.parse(raw);
|
|
31665
31219
|
if (wrapper._expires && wrapper._expires > 0 && now > wrapper._expires) {
|
|
31666
31220
|
unlinkSync7(fullPath);
|
|
@@ -32177,7 +31731,7 @@ var init_events = __esm({
|
|
|
32177
31731
|
// ../core/src/server.ts
|
|
32178
31732
|
import { createServer as createServer2 } from "node:http";
|
|
32179
31733
|
import { resolve as resolve19, dirname as dirname11, join as join26, relative as relative9 } from "node:path";
|
|
32180
|
-
import { existsSync as existsSync24, readdirSync as readdirSync17, readFileSync as
|
|
31734
|
+
import { existsSync as existsSync24, readdirSync as readdirSync17, readFileSync as readFileSync22, statSync as statSync16 } from "node:fs";
|
|
32181
31735
|
import { isatty } from "node:tty";
|
|
32182
31736
|
import { fileURLToPath as fileURLToPath5 } from "node:url";
|
|
32183
31737
|
import { execFileSync as execFileSync7, exec } from "node:child_process";
|
|
@@ -32239,7 +31793,7 @@ async function autoMigrateOnStartup(migrationDir = "migrations", base = process.
|
|
|
32239
31793
|
function readPackageVersion() {
|
|
32240
31794
|
try {
|
|
32241
31795
|
const pkgPath = resolve19(dirname11(fileURLToPath5(import.meta.url)), "..", "..", "..", "package.json");
|
|
32242
|
-
const pkg = JSON.parse(
|
|
31796
|
+
const pkg = JSON.parse(readFileSync22(pkgPath, "utf-8"));
|
|
32243
31797
|
return pkg.version ?? "0.0.0";
|
|
32244
31798
|
} catch {
|
|
32245
31799
|
return "0.0.0";
|
|
@@ -33069,7 +32623,7 @@ ${reset2}
|
|
|
33069
32623
|
res.raw.writeHead(200, void 0, { "Content-Type": "text/html; charset=utf-8" });
|
|
33070
32624
|
res.raw.end(html);
|
|
33071
32625
|
} else {
|
|
33072
|
-
const html =
|
|
32626
|
+
const html = readFileSync22(resolve19(templatesDir, tplFile), "utf-8");
|
|
33073
32627
|
res.raw.writeHead(200, void 0, { "Content-Type": "text/html; charset=utf-8" });
|
|
33074
32628
|
res.raw.end(html);
|
|
33075
32629
|
}
|
|
@@ -33460,7 +33014,7 @@ var init_constants = __esm({
|
|
|
33460
33014
|
});
|
|
33461
33015
|
|
|
33462
33016
|
// ../core/src/scss.ts
|
|
33463
|
-
import { readFileSync as
|
|
33017
|
+
import { readFileSync as readFileSync23, writeFileSync as writeFileSync17, existsSync as existsSync25, mkdirSync as mkdirSync19, readdirSync as readdirSync18 } from "node:fs";
|
|
33464
33018
|
import { join as join27, resolve as resolve20, dirname as dirname12 } from "node:path";
|
|
33465
33019
|
function compileString(scss, importPaths, variables) {
|
|
33466
33020
|
const imported = /* @__PURE__ */ new Set();
|
|
@@ -33491,7 +33045,7 @@ function resolveImports(content, paths, imported) {
|
|
|
33491
33045
|
for (const candidate of candidates) {
|
|
33492
33046
|
if (existsSync25(candidate) && !imported.has(candidate)) {
|
|
33493
33047
|
imported.add(candidate);
|
|
33494
|
-
const fileContent =
|
|
33048
|
+
const fileContent = readFileSync23(candidate, "utf-8");
|
|
33495
33049
|
return resolveImports(fileContent, [dirname12(candidate), ...paths], imported);
|
|
33496
33050
|
}
|
|
33497
33051
|
}
|
|
@@ -33832,7 +33386,7 @@ var init_scss = __esm({
|
|
|
33832
33386
|
/** Compile an SCSS file to CSS. */
|
|
33833
33387
|
compileFile(filePath) {
|
|
33834
33388
|
const absPath = resolve20(filePath);
|
|
33835
|
-
const content =
|
|
33389
|
+
const content = readFileSync23(absPath, "utf-8");
|
|
33836
33390
|
const paths = [dirname12(absPath), ...this._importPaths];
|
|
33837
33391
|
return compileString(content, paths, { ...this._variables });
|
|
33838
33392
|
}
|
|
@@ -33855,7 +33409,7 @@ var init_scss = __esm({
|
|
|
33855
33409
|
const imported = /* @__PURE__ */ new Set();
|
|
33856
33410
|
let merged = "";
|
|
33857
33411
|
for (const file of files) {
|
|
33858
|
-
const content =
|
|
33412
|
+
const content = readFileSync23(file, "utf-8");
|
|
33859
33413
|
imported.add(file);
|
|
33860
33414
|
merged += resolveImports(content, paths, imported) + "\n";
|
|
33861
33415
|
}
|
|
@@ -33872,7 +33426,7 @@ var init_scss = __esm({
|
|
|
33872
33426
|
if (!existsSync25(outDir)) mkdirSync19(outDir, { recursive: true });
|
|
33873
33427
|
let existing = null;
|
|
33874
33428
|
try {
|
|
33875
|
-
existing = existsSync25(absOutput) ?
|
|
33429
|
+
existing = existsSync25(absOutput) ? readFileSync23(absOutput, "utf-8") : null;
|
|
33876
33430
|
} catch {
|
|
33877
33431
|
existing = null;
|
|
33878
33432
|
}
|
|
@@ -33956,10 +33510,10 @@ var init_mqttMessage = __esm({
|
|
|
33956
33510
|
});
|
|
33957
33511
|
|
|
33958
33512
|
// ../core/src/mqtt.ts
|
|
33959
|
-
import
|
|
33960
|
-
import
|
|
33513
|
+
import net2 from "node:net";
|
|
33514
|
+
import tls from "node:tls";
|
|
33961
33515
|
import { randomBytes as randomBytes7 } from "node:crypto";
|
|
33962
|
-
import { existsSync as existsSync26, readFileSync as
|
|
33516
|
+
import { existsSync as existsSync26, readFileSync as readFileSync24 } from "node:fs";
|
|
33963
33517
|
var MqttError, MqttTimeoutError, CONNECT, CONNACK, PUBLISH, PUBACK, SUBSCRIBE, SUBACK, PINGREQ, PINGRESP, DISCONNECT, PROTOCOL_LEVEL, DEFAULT_PORT, DEFAULT_TLS_PORT, DEFAULT_URL, DEFAULT_KEEPALIVE, SUBSCRIPTION_REFUSED, MAX_REMAINING_LENGTH, QOS2_REFUSED_MESSAGE, CONNACK_RETURN_CODES, Mqtt;
|
|
33964
33518
|
var init_mqtt = __esm({
|
|
33965
33519
|
"../core/src/mqtt.ts"() {
|
|
@@ -34424,10 +33978,10 @@ var init_mqtt = __esm({
|
|
|
34424
33978
|
servername: this.host,
|
|
34425
33979
|
rejectUnauthorized: this.tlsVerify
|
|
34426
33980
|
};
|
|
34427
|
-
if (this.tlsVerify && this.caFile) opts.ca =
|
|
34428
|
-
sock =
|
|
33981
|
+
if (this.tlsVerify && this.caFile) opts.ca = readFileSync24(this.caFile);
|
|
33982
|
+
sock = tls.connect(opts, () => settle(() => resolve31(sock)));
|
|
34429
33983
|
} else {
|
|
34430
|
-
sock =
|
|
33984
|
+
sock = net2.createConnection({ host: this.host, port: this.port }, () => settle(() => resolve31(sock)));
|
|
34431
33985
|
}
|
|
34432
33986
|
sock.once("error", (err) => {
|
|
34433
33987
|
settle(() => {
|
|
@@ -34978,7 +34532,7 @@ import https from "node:https";
|
|
|
34978
34532
|
import { URL as URL2 } from "node:url";
|
|
34979
34533
|
import { randomBytes as randomBytes8 } from "node:crypto";
|
|
34980
34534
|
import { promises as fsp, createWriteStream } from "node:fs";
|
|
34981
|
-
import { basename as
|
|
34535
|
+
import { basename as basename6 } from "node:path";
|
|
34982
34536
|
import { pipeline } from "node:stream/promises";
|
|
34983
34537
|
function sameOrigin(urlA, urlB) {
|
|
34984
34538
|
try {
|
|
@@ -35268,7 +34822,7 @@ var init_api = __esm({
|
|
|
35268
34822
|
error: err instanceof Error ? err.message : String(err)
|
|
35269
34823
|
};
|
|
35270
34824
|
}
|
|
35271
|
-
uploadName = filename ||
|
|
34825
|
+
uploadName = filename || basename6(filePath);
|
|
35272
34826
|
} else {
|
|
35273
34827
|
return { http_code: null, body: null, headers: {}, error: "upload requires filePath or fileBytes" };
|
|
35274
34828
|
}
|
|
@@ -35624,6 +35178,777 @@ var init_api = __esm({
|
|
|
35624
35178
|
}
|
|
35625
35179
|
});
|
|
35626
35180
|
|
|
35181
|
+
// ../core/src/messenger.ts
|
|
35182
|
+
import net3 from "node:net";
|
|
35183
|
+
import tls2 from "node:tls";
|
|
35184
|
+
import { readFileSync as readFileSync25 } from "node:fs";
|
|
35185
|
+
import { basename as basename7 } from "node:path";
|
|
35186
|
+
import { randomUUID as randomUUID9 } from "node:crypto";
|
|
35187
|
+
function tlsRejectUnauthorized() {
|
|
35188
|
+
return !isTruthy(process.env.TINA4_MAIL_TLS_INSECURE);
|
|
35189
|
+
}
|
|
35190
|
+
function readResponse(socket) {
|
|
35191
|
+
return new Promise((resolve31, reject) => {
|
|
35192
|
+
let buffer = "";
|
|
35193
|
+
const onData = (chunk) => {
|
|
35194
|
+
buffer += chunk.toString("utf-8");
|
|
35195
|
+
const lines = buffer.split("\r\n");
|
|
35196
|
+
for (let i = 0; i < lines.length; i++) {
|
|
35197
|
+
const line = lines[i];
|
|
35198
|
+
if (line.length < 3) continue;
|
|
35199
|
+
const code = parseInt(line.substring(0, 3), 10);
|
|
35200
|
+
if (line.length >= 4 && line[3] === " ") {
|
|
35201
|
+
socket.removeListener("data", onData);
|
|
35202
|
+
socket.removeListener("error", onError);
|
|
35203
|
+
resolve31({ code, text: buffer.trim() });
|
|
35204
|
+
return;
|
|
35205
|
+
}
|
|
35206
|
+
}
|
|
35207
|
+
};
|
|
35208
|
+
const onError = (err) => {
|
|
35209
|
+
socket.removeListener("data", onData);
|
|
35210
|
+
reject(err);
|
|
35211
|
+
};
|
|
35212
|
+
socket.on("data", onData);
|
|
35213
|
+
socket.on("error", onError);
|
|
35214
|
+
});
|
|
35215
|
+
}
|
|
35216
|
+
function sendCommand(socket, command) {
|
|
35217
|
+
return new Promise((resolve31, reject) => {
|
|
35218
|
+
socket.write(command + "\r\n", "utf-8", (err) => {
|
|
35219
|
+
if (err) return reject(err);
|
|
35220
|
+
readResponse(socket).then(resolve31, reject);
|
|
35221
|
+
});
|
|
35222
|
+
});
|
|
35223
|
+
}
|
|
35224
|
+
function buildMimeMessage(options) {
|
|
35225
|
+
const boundary = `----=_Tina4_${Date.now()}_${Math.random().toString(36).substring(2)}`;
|
|
35226
|
+
const altBoundary = `----=_Tina4Alt_${Date.now()}_${Math.random().toString(36).substring(2)}`;
|
|
35227
|
+
const hasAttachments = options.attachments && options.attachments.length > 0;
|
|
35228
|
+
const hasTextAlt = options.text !== void 0 && options.html;
|
|
35229
|
+
const lines = [];
|
|
35230
|
+
const fromHeader = options.fromName ? `"${options.fromName}" <${options.from}>` : options.from;
|
|
35231
|
+
lines.push(`From: ${fromHeader}`);
|
|
35232
|
+
lines.push(`To: ${options.to.join(", ")}`);
|
|
35233
|
+
if (options.cc.length > 0) {
|
|
35234
|
+
lines.push(`Cc: ${options.cc.join(", ")}`);
|
|
35235
|
+
}
|
|
35236
|
+
lines.push(`Subject: ${options.subject}`);
|
|
35237
|
+
lines.push(`Date: ${(/* @__PURE__ */ new Date()).toUTCString()}`);
|
|
35238
|
+
lines.push(`Message-ID: <${options.messageId}>`);
|
|
35239
|
+
lines.push("MIME-Version: 1.0");
|
|
35240
|
+
if (options.replyTo) {
|
|
35241
|
+
lines.push(`Reply-To: ${options.replyTo}`);
|
|
35242
|
+
}
|
|
35243
|
+
if (options.headers) {
|
|
35244
|
+
for (const [key, value] of Object.entries(options.headers)) {
|
|
35245
|
+
lines.push(`${key}: ${value}`);
|
|
35246
|
+
}
|
|
35247
|
+
}
|
|
35248
|
+
if (hasAttachments) {
|
|
35249
|
+
lines.push(`Content-Type: multipart/mixed; boundary="${boundary}"`);
|
|
35250
|
+
lines.push("");
|
|
35251
|
+
lines.push(`--${boundary}`);
|
|
35252
|
+
if (hasTextAlt) {
|
|
35253
|
+
lines.push(`Content-Type: multipart/alternative; boundary="${altBoundary}"`);
|
|
35254
|
+
lines.push("");
|
|
35255
|
+
lines.push(`--${altBoundary}`);
|
|
35256
|
+
lines.push("Content-Type: text/plain; charset=UTF-8");
|
|
35257
|
+
lines.push("Content-Transfer-Encoding: 7bit");
|
|
35258
|
+
lines.push("");
|
|
35259
|
+
lines.push(options.text);
|
|
35260
|
+
lines.push("");
|
|
35261
|
+
lines.push(`--${altBoundary}`);
|
|
35262
|
+
lines.push("Content-Type: text/html; charset=UTF-8");
|
|
35263
|
+
lines.push("Content-Transfer-Encoding: 7bit");
|
|
35264
|
+
lines.push("");
|
|
35265
|
+
lines.push(options.body);
|
|
35266
|
+
lines.push("");
|
|
35267
|
+
lines.push(`--${altBoundary}--`);
|
|
35268
|
+
} else {
|
|
35269
|
+
const contentType = options.html ? "text/html" : "text/plain";
|
|
35270
|
+
lines.push(`Content-Type: ${contentType}; charset=UTF-8`);
|
|
35271
|
+
lines.push("Content-Transfer-Encoding: 7bit");
|
|
35272
|
+
lines.push("");
|
|
35273
|
+
lines.push(options.body);
|
|
35274
|
+
}
|
|
35275
|
+
for (const filePath of options.attachments) {
|
|
35276
|
+
const fileName = basename7(filePath);
|
|
35277
|
+
const fileData = readFileSync25(filePath);
|
|
35278
|
+
const base64Data = fileData.toString("base64");
|
|
35279
|
+
lines.push("");
|
|
35280
|
+
lines.push(`--${boundary}`);
|
|
35281
|
+
lines.push(`Content-Type: application/octet-stream; name="${fileName}"`);
|
|
35282
|
+
lines.push("Content-Transfer-Encoding: base64");
|
|
35283
|
+
lines.push(`Content-Disposition: attachment; filename="${fileName}"`);
|
|
35284
|
+
lines.push("");
|
|
35285
|
+
for (let i = 0; i < base64Data.length; i += 76) {
|
|
35286
|
+
lines.push(base64Data.substring(i, i + 76));
|
|
35287
|
+
}
|
|
35288
|
+
}
|
|
35289
|
+
lines.push("");
|
|
35290
|
+
lines.push(`--${boundary}--`);
|
|
35291
|
+
} else if (hasTextAlt) {
|
|
35292
|
+
lines.push(`Content-Type: multipart/alternative; boundary="${altBoundary}"`);
|
|
35293
|
+
lines.push("");
|
|
35294
|
+
lines.push(`--${altBoundary}`);
|
|
35295
|
+
lines.push("Content-Type: text/plain; charset=UTF-8");
|
|
35296
|
+
lines.push("Content-Transfer-Encoding: 7bit");
|
|
35297
|
+
lines.push("");
|
|
35298
|
+
lines.push(options.text);
|
|
35299
|
+
lines.push("");
|
|
35300
|
+
lines.push(`--${altBoundary}`);
|
|
35301
|
+
lines.push("Content-Type: text/html; charset=UTF-8");
|
|
35302
|
+
lines.push("Content-Transfer-Encoding: 7bit");
|
|
35303
|
+
lines.push("");
|
|
35304
|
+
lines.push(options.body);
|
|
35305
|
+
lines.push("");
|
|
35306
|
+
lines.push(`--${altBoundary}--`);
|
|
35307
|
+
} else {
|
|
35308
|
+
const contentType = options.html ? "text/html" : "text/plain";
|
|
35309
|
+
lines.push(`Content-Type: ${contentType}; charset=UTF-8`);
|
|
35310
|
+
lines.push("");
|
|
35311
|
+
lines.push(options.body);
|
|
35312
|
+
}
|
|
35313
|
+
return lines.join("\r\n");
|
|
35314
|
+
}
|
|
35315
|
+
function imapQuote(s) {
|
|
35316
|
+
if (/^[a-zA-Z0-9_./-]+$/.test(s)) return s;
|
|
35317
|
+
return '"' + s.replace(/\\/g, "\\\\").replace(/"/g, '\\"') + '"';
|
|
35318
|
+
}
|
|
35319
|
+
function imapReadLine(socket) {
|
|
35320
|
+
return new Promise((resolve31, reject) => {
|
|
35321
|
+
let buffer = "";
|
|
35322
|
+
const onData = (chunk) => {
|
|
35323
|
+
buffer += chunk.toString("utf-8");
|
|
35324
|
+
const nlIndex = buffer.indexOf("\r\n");
|
|
35325
|
+
if (nlIndex !== -1) {
|
|
35326
|
+
socket.removeListener("data", onData);
|
|
35327
|
+
socket.removeListener("error", onError);
|
|
35328
|
+
resolve31(buffer);
|
|
35329
|
+
}
|
|
35330
|
+
};
|
|
35331
|
+
const onError = (err) => {
|
|
35332
|
+
socket.removeListener("data", onData);
|
|
35333
|
+
reject(err);
|
|
35334
|
+
};
|
|
35335
|
+
socket.on("data", onData);
|
|
35336
|
+
socket.on("error", onError);
|
|
35337
|
+
});
|
|
35338
|
+
}
|
|
35339
|
+
function imapCommand(socket, command) {
|
|
35340
|
+
return new Promise((resolve31, reject) => {
|
|
35341
|
+
imapTagCounter++;
|
|
35342
|
+
const tag = `T${imapTagCounter}`;
|
|
35343
|
+
const fullCommand = `${tag} ${command}\r
|
|
35344
|
+
`;
|
|
35345
|
+
let buffer = "";
|
|
35346
|
+
const onData = (chunk) => {
|
|
35347
|
+
buffer += chunk.toString("utf-8");
|
|
35348
|
+
if (buffer.includes(`${tag} OK`)) {
|
|
35349
|
+
socket.removeListener("data", onData);
|
|
35350
|
+
socket.removeListener("error", onError);
|
|
35351
|
+
resolve31(buffer);
|
|
35352
|
+
return;
|
|
35353
|
+
}
|
|
35354
|
+
if (buffer.includes(`${tag} NO`) || buffer.includes(`${tag} BAD`)) {
|
|
35355
|
+
socket.removeListener("data", onData);
|
|
35356
|
+
socket.removeListener("error", onError);
|
|
35357
|
+
reject(new MessengerConnectionError(`IMAP command failed: ${command.split(" ")[0]} \u2192 ${buffer.trim()}`));
|
|
35358
|
+
}
|
|
35359
|
+
};
|
|
35360
|
+
const onError = (err) => {
|
|
35361
|
+
socket.removeListener("data", onData);
|
|
35362
|
+
reject(err);
|
|
35363
|
+
};
|
|
35364
|
+
socket.on("data", onData);
|
|
35365
|
+
socket.on("error", onError);
|
|
35366
|
+
socket.write(fullCommand, "utf-8");
|
|
35367
|
+
});
|
|
35368
|
+
}
|
|
35369
|
+
function imapFail(method, err) {
|
|
35370
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
35371
|
+
Log.error(`Messenger IMAP ${method}() failed: ${e.name}: ${e.message}`);
|
|
35372
|
+
if (e instanceof MessengerConnectionError) return e;
|
|
35373
|
+
return new MessengerConnectionError(`IMAP ${method} failed: ${e.message}`);
|
|
35374
|
+
}
|
|
35375
|
+
function parseSearchResponse(response) {
|
|
35376
|
+
const match = response.match(/\* SEARCH (.+)/);
|
|
35377
|
+
if (!match) return [];
|
|
35378
|
+
return match[1].trim().split(/\s+/).filter((s) => /^\d+$/.test(s));
|
|
35379
|
+
}
|
|
35380
|
+
function parseHeaderResponse(uid, response) {
|
|
35381
|
+
const headers = {};
|
|
35382
|
+
const headerBlock = response.match(/\r\n([\s\S]*?)\r\n\)/);
|
|
35383
|
+
if (headerBlock) {
|
|
35384
|
+
const lines = headerBlock[1].split(/\r\n/);
|
|
35385
|
+
let currentKey = "";
|
|
35386
|
+
for (const line of lines) {
|
|
35387
|
+
if (/^\s/.test(line) && currentKey) {
|
|
35388
|
+
headers[currentKey] += " " + line.trim();
|
|
35389
|
+
} else {
|
|
35390
|
+
const colonIdx = line.indexOf(":");
|
|
35391
|
+
if (colonIdx > 0) {
|
|
35392
|
+
currentKey = line.substring(0, colonIdx).trim().toLowerCase();
|
|
35393
|
+
headers[currentKey] = line.substring(colonIdx + 1).trim();
|
|
35394
|
+
}
|
|
35395
|
+
}
|
|
35396
|
+
}
|
|
35397
|
+
}
|
|
35398
|
+
const seen = /\\Seen/i.test(response);
|
|
35399
|
+
return {
|
|
35400
|
+
uid,
|
|
35401
|
+
subject: headers["subject"] ?? "",
|
|
35402
|
+
from: headers["from"] ?? "",
|
|
35403
|
+
to: headers["to"] ?? "",
|
|
35404
|
+
date: headers["date"] ?? "",
|
|
35405
|
+
snippet: "",
|
|
35406
|
+
seen
|
|
35407
|
+
};
|
|
35408
|
+
}
|
|
35409
|
+
function emptyFullMessage(uid) {
|
|
35410
|
+
return { uid, subject: "", from: "", to: "", cc: "", date: "", bodyText: "", bodyHtml: "", headers: {} };
|
|
35411
|
+
}
|
|
35412
|
+
function parseFullMessage(uid, response) {
|
|
35413
|
+
const bodyMatch = response.match(/\{(\d+)\}\r\n([\s\S]*)/);
|
|
35414
|
+
const rawMessage = bodyMatch ? bodyMatch[2] : response;
|
|
35415
|
+
const headerEnd = rawMessage.indexOf("\r\n\r\n");
|
|
35416
|
+
const headerSection = headerEnd > 0 ? rawMessage.substring(0, headerEnd) : rawMessage;
|
|
35417
|
+
const bodySection = headerEnd > 0 ? rawMessage.substring(headerEnd + 4) : "";
|
|
35418
|
+
const headers = {};
|
|
35419
|
+
const headerLines = headerSection.split(/\r\n/);
|
|
35420
|
+
let currentKey = "";
|
|
35421
|
+
for (const line of headerLines) {
|
|
35422
|
+
if (/^\s/.test(line) && currentKey) {
|
|
35423
|
+
headers[currentKey] += " " + line.trim();
|
|
35424
|
+
} else {
|
|
35425
|
+
const colonIdx = line.indexOf(":");
|
|
35426
|
+
if (colonIdx > 0) {
|
|
35427
|
+
currentKey = line.substring(0, colonIdx).trim().toLowerCase();
|
|
35428
|
+
headers[currentKey] = line.substring(colonIdx + 1).trim();
|
|
35429
|
+
}
|
|
35430
|
+
}
|
|
35431
|
+
}
|
|
35432
|
+
const contentType = headers["content-type"] ?? "text/plain";
|
|
35433
|
+
let bodyText = "";
|
|
35434
|
+
let bodyHtml = "";
|
|
35435
|
+
if (contentType.includes("multipart")) {
|
|
35436
|
+
const boundaryMatch = contentType.match(/boundary="?([^";\s]+)"?/);
|
|
35437
|
+
if (boundaryMatch) {
|
|
35438
|
+
const boundary = boundaryMatch[1];
|
|
35439
|
+
const parts = bodySection.split("--" + boundary);
|
|
35440
|
+
for (const part of parts) {
|
|
35441
|
+
if (part.trim() === "" || part.trim() === "--") continue;
|
|
35442
|
+
const partHeaderEnd = part.indexOf("\r\n\r\n");
|
|
35443
|
+
const partHeaders = partHeaderEnd > 0 ? part.substring(0, partHeaderEnd).toLowerCase() : "";
|
|
35444
|
+
const partBody = partHeaderEnd > 0 ? part.substring(partHeaderEnd + 4).trim() : "";
|
|
35445
|
+
if (partHeaders.includes("text/html")) {
|
|
35446
|
+
bodyHtml = partBody;
|
|
35447
|
+
} else if (partHeaders.includes("text/plain")) {
|
|
35448
|
+
bodyText = partBody;
|
|
35449
|
+
}
|
|
35450
|
+
}
|
|
35451
|
+
}
|
|
35452
|
+
} else if (contentType.includes("text/html")) {
|
|
35453
|
+
bodyHtml = bodySection;
|
|
35454
|
+
} else {
|
|
35455
|
+
bodyText = bodySection;
|
|
35456
|
+
}
|
|
35457
|
+
bodyText = bodyText.replace(/\)\r\n[A-Z]\d+ OK.*$/s, "").trim();
|
|
35458
|
+
bodyHtml = bodyHtml.replace(/\)\r\n[A-Z]\d+ OK.*$/s, "").trim();
|
|
35459
|
+
return {
|
|
35460
|
+
uid,
|
|
35461
|
+
subject: headers["subject"] ?? "",
|
|
35462
|
+
from: headers["from"] ?? "",
|
|
35463
|
+
to: headers["to"] ?? "",
|
|
35464
|
+
cc: headers["cc"] ?? "",
|
|
35465
|
+
date: headers["date"] ?? "",
|
|
35466
|
+
bodyText,
|
|
35467
|
+
bodyHtml,
|
|
35468
|
+
headers
|
|
35469
|
+
};
|
|
35470
|
+
}
|
|
35471
|
+
function createMessenger() {
|
|
35472
|
+
return new Messenger();
|
|
35473
|
+
}
|
|
35474
|
+
var MessengerConnectionError, Messenger, imapTagCounter;
|
|
35475
|
+
var init_messenger = __esm({
|
|
35476
|
+
"../core/src/messenger.ts"() {
|
|
35477
|
+
"use strict";
|
|
35478
|
+
init_dotenv();
|
|
35479
|
+
init_devMailbox();
|
|
35480
|
+
init_logger();
|
|
35481
|
+
MessengerConnectionError = class extends Error {
|
|
35482
|
+
constructor(message) {
|
|
35483
|
+
super(message);
|
|
35484
|
+
this.name = "MessengerConnectionError";
|
|
35485
|
+
}
|
|
35486
|
+
};
|
|
35487
|
+
Messenger = class {
|
|
35488
|
+
host;
|
|
35489
|
+
port;
|
|
35490
|
+
username;
|
|
35491
|
+
password;
|
|
35492
|
+
fromAddress;
|
|
35493
|
+
fromName;
|
|
35494
|
+
encryption;
|
|
35495
|
+
useTls;
|
|
35496
|
+
/** Whether an SMTP host was actually configured (see the constructor). */
|
|
35497
|
+
smtpConfigured = false;
|
|
35498
|
+
/** The local mailbox, present only when this messenger captures. */
|
|
35499
|
+
devMailbox = null;
|
|
35500
|
+
imapHost;
|
|
35501
|
+
imapPort;
|
|
35502
|
+
imapUser;
|
|
35503
|
+
imapPass;
|
|
35504
|
+
imapEncryption;
|
|
35505
|
+
constructor(options) {
|
|
35506
|
+
this.smtpConfigured = Boolean(options?.host ?? process.env.TINA4_MAIL_HOST);
|
|
35507
|
+
this.host = options?.host ?? process.env.TINA4_MAIL_HOST ?? "localhost";
|
|
35508
|
+
this.port = options?.port ?? parseInt(process.env.TINA4_MAIL_PORT ?? "587", 10);
|
|
35509
|
+
this.username = options?.username ?? process.env.TINA4_MAIL_USERNAME ?? "";
|
|
35510
|
+
this.password = options?.password ?? process.env.TINA4_MAIL_PASSWORD ?? "";
|
|
35511
|
+
this.fromAddress = options?.fromAddress ?? process.env.TINA4_MAIL_FROM ?? (this.username || "noreply@localhost");
|
|
35512
|
+
this.fromName = options?.fromName ?? process.env.TINA4_MAIL_FROM_NAME ?? "";
|
|
35513
|
+
const envEncryption = options?.encryption ?? process.env.TINA4_MAIL_ENCRYPTION;
|
|
35514
|
+
if (envEncryption) {
|
|
35515
|
+
this.encryption = envEncryption.toLowerCase();
|
|
35516
|
+
} else if (options?.useTls !== void 0) {
|
|
35517
|
+
this.encryption = options.useTls ? "tls" : "none";
|
|
35518
|
+
} else {
|
|
35519
|
+
this.encryption = "tls";
|
|
35520
|
+
}
|
|
35521
|
+
this.useTls = ["tls", "starttls"].includes(this.encryption);
|
|
35522
|
+
this.imapHost = options?.imapHost ?? process.env.TINA4_MAIL_IMAP_HOST ?? "";
|
|
35523
|
+
this.imapPort = options?.imapPort ?? parseInt(process.env.TINA4_MAIL_IMAP_PORT ?? "993", 10);
|
|
35524
|
+
this.imapUser = options?.imapUser ?? process.env.TINA4_MAIL_IMAP_USERNAME ?? this.username;
|
|
35525
|
+
this.imapPass = options?.imapPass ?? process.env.TINA4_MAIL_IMAP_PASSWORD ?? this.password;
|
|
35526
|
+
this.imapEncryption = (options?.imapEncryption ?? process.env.TINA4_MAIL_IMAP_ENCRYPTION ?? "tls").toLowerCase();
|
|
35527
|
+
}
|
|
35528
|
+
/**
|
|
35529
|
+
* Read-only IMAP encryption mode for inspection / tests.
|
|
35530
|
+
* Returns one of "tls", "starttls", "none", "ssl".
|
|
35531
|
+
*/
|
|
35532
|
+
getImapEncryption() {
|
|
35533
|
+
return this.imapEncryption;
|
|
35534
|
+
}
|
|
35535
|
+
/**
|
|
35536
|
+
* Send an email via SMTP.
|
|
35537
|
+
*/
|
|
35538
|
+
/**
|
|
35539
|
+
* Should send() capture locally instead of talking to SMTP?
|
|
35540
|
+
*
|
|
35541
|
+
* Availability decides, not verbosity. With no SMTP host configured sending is
|
|
35542
|
+
* impossible, so simulate it into a folder rather than failing -- that is what
|
|
35543
|
+
* makes a laptop with no mail server usable. TINA4_MAIL_CAPTURE forces capture
|
|
35544
|
+
* even when a host IS configured.
|
|
35545
|
+
*
|
|
35546
|
+
* TINA4_DEBUG deliberately does NOT gate this, and neither does NODE_ENV. Debug
|
|
35547
|
+
* must still be able to send, and the old `NODE_ENV !== "production"` clause
|
|
35548
|
+
* silently swallowed every staging email.
|
|
35549
|
+
*/
|
|
35550
|
+
shouldCapture() {
|
|
35551
|
+
if (isTruthy(process.env.TINA4_MAIL_CAPTURE)) return true;
|
|
35552
|
+
return !this.smtpConfigured;
|
|
35553
|
+
}
|
|
35554
|
+
/** The local mailbox, created on first capture and reused after. */
|
|
35555
|
+
getDevMailbox() {
|
|
35556
|
+
if (this.devMailbox === null) {
|
|
35557
|
+
this.devMailbox = new DevMailbox();
|
|
35558
|
+
}
|
|
35559
|
+
return this.devMailbox;
|
|
35560
|
+
}
|
|
35561
|
+
async send(to, subject, body, html = false, text, cc, bcc, replyTo, attachments, headers) {
|
|
35562
|
+
const options = { to, subject, body, html, text, cc, bcc, replyTo, attachments, headers };
|
|
35563
|
+
const toList = Array.isArray(options.to) ? options.to : [options.to];
|
|
35564
|
+
const ccList = Array.isArray(options.cc) ? options.cc : options.cc ? [options.cc] : [];
|
|
35565
|
+
const bccList = Array.isArray(options.bcc) ? options.bcc : options.bcc ? [options.bcc] : [];
|
|
35566
|
+
const allRecipients = [...toList, ...ccList, ...bccList];
|
|
35567
|
+
if (this.shouldCapture()) {
|
|
35568
|
+
return this.getDevMailbox().capture(
|
|
35569
|
+
to,
|
|
35570
|
+
subject,
|
|
35571
|
+
body,
|
|
35572
|
+
html,
|
|
35573
|
+
text,
|
|
35574
|
+
ccList,
|
|
35575
|
+
bccList,
|
|
35576
|
+
replyTo,
|
|
35577
|
+
attachments,
|
|
35578
|
+
this.fromAddress || void 0
|
|
35579
|
+
);
|
|
35580
|
+
}
|
|
35581
|
+
const messageId = `${randomUUID9()}@${this.host}`;
|
|
35582
|
+
if (allRecipients.length === 0) {
|
|
35583
|
+
return { success: false, message: "No recipients specified" };
|
|
35584
|
+
}
|
|
35585
|
+
if (!this.fromAddress) {
|
|
35586
|
+
return { success: false, message: "No from address configured" };
|
|
35587
|
+
}
|
|
35588
|
+
try {
|
|
35589
|
+
let socket;
|
|
35590
|
+
if (this.port === 465) {
|
|
35591
|
+
socket = tls2.connect({ host: this.host, port: this.port, rejectUnauthorized: tlsRejectUnauthorized() });
|
|
35592
|
+
await new Promise((resolve31, reject) => {
|
|
35593
|
+
socket.once("secureConnect", resolve31);
|
|
35594
|
+
socket.once("error", reject);
|
|
35595
|
+
});
|
|
35596
|
+
} else {
|
|
35597
|
+
socket = net3.createConnection({ host: this.host, port: this.port });
|
|
35598
|
+
await new Promise((resolve31, reject) => {
|
|
35599
|
+
socket.once("connect", resolve31);
|
|
35600
|
+
socket.once("error", reject);
|
|
35601
|
+
});
|
|
35602
|
+
}
|
|
35603
|
+
const greeting = await readResponse(socket);
|
|
35604
|
+
if (greeting.code !== 220) {
|
|
35605
|
+
socket.destroy();
|
|
35606
|
+
return { success: false, message: `SMTP greeting failed: ${greeting.text}` };
|
|
35607
|
+
}
|
|
35608
|
+
const ehlo = await sendCommand(socket, `EHLO ${this.host}`);
|
|
35609
|
+
if (ehlo.code !== 250) {
|
|
35610
|
+
socket.destroy();
|
|
35611
|
+
return { success: false, message: `EHLO failed: ${ehlo.text}` };
|
|
35612
|
+
}
|
|
35613
|
+
if (this.useTls && this.port !== 465 && ehlo.text.includes("STARTTLS")) {
|
|
35614
|
+
const starttls = await sendCommand(socket, "STARTTLS");
|
|
35615
|
+
if (starttls.code !== 220) {
|
|
35616
|
+
socket.destroy();
|
|
35617
|
+
return { success: false, message: `STARTTLS failed: ${starttls.text}` };
|
|
35618
|
+
}
|
|
35619
|
+
const plainSocket = socket;
|
|
35620
|
+
socket = tls2.connect(
|
|
35621
|
+
{ socket: plainSocket, host: this.host, rejectUnauthorized: tlsRejectUnauthorized() }
|
|
35622
|
+
);
|
|
35623
|
+
await new Promise((resolve31, reject) => {
|
|
35624
|
+
socket.once("secureConnect", resolve31);
|
|
35625
|
+
socket.once("error", reject);
|
|
35626
|
+
});
|
|
35627
|
+
const ehlo2 = await sendCommand(socket, `EHLO ${this.host}`);
|
|
35628
|
+
if (ehlo2.code !== 250) {
|
|
35629
|
+
socket.destroy();
|
|
35630
|
+
return { success: false, message: `EHLO after STARTTLS failed: ${ehlo2.text}` };
|
|
35631
|
+
}
|
|
35632
|
+
}
|
|
35633
|
+
if (this.username && this.password) {
|
|
35634
|
+
const auth = await sendCommand(socket, "AUTH LOGIN");
|
|
35635
|
+
if (auth.code !== 334) {
|
|
35636
|
+
socket.destroy();
|
|
35637
|
+
return { success: false, message: `AUTH LOGIN failed: ${auth.text}` };
|
|
35638
|
+
}
|
|
35639
|
+
const userResp = await sendCommand(socket, Buffer.from(this.username).toString("base64"));
|
|
35640
|
+
if (userResp.code !== 334) {
|
|
35641
|
+
socket.destroy();
|
|
35642
|
+
return { success: false, message: `AUTH username failed: ${userResp.text}` };
|
|
35643
|
+
}
|
|
35644
|
+
const passResp = await sendCommand(socket, Buffer.from(this.password).toString("base64"));
|
|
35645
|
+
if (passResp.code !== 235) {
|
|
35646
|
+
socket.destroy();
|
|
35647
|
+
return { success: false, message: `AUTH password failed: ${passResp.text}` };
|
|
35648
|
+
}
|
|
35649
|
+
}
|
|
35650
|
+
const mailFrom = await sendCommand(socket, `MAIL FROM:<${this.fromAddress}>`);
|
|
35651
|
+
if (mailFrom.code !== 250) {
|
|
35652
|
+
socket.destroy();
|
|
35653
|
+
return { success: false, message: `MAIL FROM failed: ${mailFrom.text}` };
|
|
35654
|
+
}
|
|
35655
|
+
for (const recipient of allRecipients) {
|
|
35656
|
+
const rcpt = await sendCommand(socket, `RCPT TO:<${recipient}>`);
|
|
35657
|
+
if (rcpt.code !== 250 && rcpt.code !== 251) {
|
|
35658
|
+
socket.destroy();
|
|
35659
|
+
return { success: false, message: `RCPT TO <${recipient}> failed: ${rcpt.text}` };
|
|
35660
|
+
}
|
|
35661
|
+
}
|
|
35662
|
+
const dataCmd = await sendCommand(socket, "DATA");
|
|
35663
|
+
if (dataCmd.code !== 354) {
|
|
35664
|
+
socket.destroy();
|
|
35665
|
+
return { success: false, message: `DATA failed: ${dataCmd.text}` };
|
|
35666
|
+
}
|
|
35667
|
+
const mimeMessage = buildMimeMessage({
|
|
35668
|
+
from: this.fromAddress,
|
|
35669
|
+
fromName: this.fromName,
|
|
35670
|
+
to: toList,
|
|
35671
|
+
cc: ccList,
|
|
35672
|
+
subject: options.subject,
|
|
35673
|
+
body: options.body,
|
|
35674
|
+
html: options.html ?? false,
|
|
35675
|
+
text: options.text,
|
|
35676
|
+
replyTo: options.replyTo,
|
|
35677
|
+
attachments: options.attachments,
|
|
35678
|
+
headers: options.headers,
|
|
35679
|
+
messageId
|
|
35680
|
+
});
|
|
35681
|
+
const endData = await sendCommand(socket, mimeMessage + "\r\n.");
|
|
35682
|
+
if (endData.code !== 250) {
|
|
35683
|
+
socket.destroy();
|
|
35684
|
+
return { success: false, message: `Message delivery failed: ${endData.text}` };
|
|
35685
|
+
}
|
|
35686
|
+
await sendCommand(socket, "QUIT");
|
|
35687
|
+
socket.destroy();
|
|
35688
|
+
return { success: true, message: "Email sent successfully", id: messageId };
|
|
35689
|
+
} catch (err) {
|
|
35690
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
35691
|
+
return { success: false, message: `SMTP error: ${errMsg}` };
|
|
35692
|
+
}
|
|
35693
|
+
}
|
|
35694
|
+
/**
|
|
35695
|
+
* Test the SMTP connection without sending an email.
|
|
35696
|
+
*/
|
|
35697
|
+
async testConnection() {
|
|
35698
|
+
try {
|
|
35699
|
+
let socket;
|
|
35700
|
+
if (this.port === 465) {
|
|
35701
|
+
socket = tls2.connect({ host: this.host, port: this.port, rejectUnauthorized: tlsRejectUnauthorized() });
|
|
35702
|
+
await new Promise((resolve31, reject) => {
|
|
35703
|
+
socket.once("secureConnect", resolve31);
|
|
35704
|
+
socket.once("error", reject);
|
|
35705
|
+
});
|
|
35706
|
+
} else {
|
|
35707
|
+
socket = net3.createConnection({ host: this.host, port: this.port });
|
|
35708
|
+
await new Promise((resolve31, reject) => {
|
|
35709
|
+
socket.once("connect", resolve31);
|
|
35710
|
+
socket.once("error", reject);
|
|
35711
|
+
});
|
|
35712
|
+
}
|
|
35713
|
+
const greeting = await readResponse(socket);
|
|
35714
|
+
if (greeting.code !== 220) {
|
|
35715
|
+
socket.destroy();
|
|
35716
|
+
return { success: false, message: `SMTP greeting failed: ${greeting.text}` };
|
|
35717
|
+
}
|
|
35718
|
+
const ehlo = await sendCommand(socket, `EHLO ${this.host}`);
|
|
35719
|
+
if (ehlo.code !== 250) {
|
|
35720
|
+
socket.destroy();
|
|
35721
|
+
return { success: false, message: `EHLO failed: ${ehlo.text}` };
|
|
35722
|
+
}
|
|
35723
|
+
await sendCommand(socket, "QUIT");
|
|
35724
|
+
socket.destroy();
|
|
35725
|
+
return { success: true, message: `Connected to ${this.host}:${this.port}` };
|
|
35726
|
+
} catch (err) {
|
|
35727
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
35728
|
+
return { success: false, message: `Connection failed: ${errMsg}` };
|
|
35729
|
+
}
|
|
35730
|
+
}
|
|
35731
|
+
// ── IMAP (Read) ────────────────────────────────────────────
|
|
35732
|
+
/**
|
|
35733
|
+
* Connect to the IMAP server via raw TCP/TLS.
|
|
35734
|
+
* Returns the socket and reads the greeting.
|
|
35735
|
+
*/
|
|
35736
|
+
async imapConnect() {
|
|
35737
|
+
if (!this.imapHost) {
|
|
35738
|
+
throw new Error("IMAP host not configured (set imapHost or IMAP_HOST env)");
|
|
35739
|
+
}
|
|
35740
|
+
let socket;
|
|
35741
|
+
const useTls = this.imapEncryption === "tls" || this.imapEncryption === "ssl" || this.imapEncryption === "" && this.imapPort === 993;
|
|
35742
|
+
if (useTls) {
|
|
35743
|
+
socket = tls2.connect({ host: this.imapHost, port: this.imapPort, rejectUnauthorized: tlsRejectUnauthorized() });
|
|
35744
|
+
await new Promise((resolve31, reject) => {
|
|
35745
|
+
socket.once("secureConnect", resolve31);
|
|
35746
|
+
socket.once("error", reject);
|
|
35747
|
+
});
|
|
35748
|
+
} else {
|
|
35749
|
+
socket = net3.createConnection({ host: this.imapHost, port: this.imapPort });
|
|
35750
|
+
await new Promise((resolve31, reject) => {
|
|
35751
|
+
socket.once("connect", resolve31);
|
|
35752
|
+
socket.once("error", reject);
|
|
35753
|
+
});
|
|
35754
|
+
}
|
|
35755
|
+
await imapReadLine(socket);
|
|
35756
|
+
if (this.imapUser && this.imapPass) {
|
|
35757
|
+
const loginResp = await imapCommand(socket, `LOGIN ${imapQuote(this.imapUser)} ${imapQuote(this.imapPass)}`);
|
|
35758
|
+
if (!loginResp.includes("OK")) {
|
|
35759
|
+
socket.destroy();
|
|
35760
|
+
throw new Error(`IMAP login failed: ${loginResp}`);
|
|
35761
|
+
}
|
|
35762
|
+
}
|
|
35763
|
+
return socket;
|
|
35764
|
+
}
|
|
35765
|
+
/**
|
|
35766
|
+
* Disconnect from IMAP cleanly.
|
|
35767
|
+
*/
|
|
35768
|
+
async imapDisconnect(socket) {
|
|
35769
|
+
try {
|
|
35770
|
+
await imapCommand(socket, "LOGOUT");
|
|
35771
|
+
} catch {
|
|
35772
|
+
}
|
|
35773
|
+
socket.destroy();
|
|
35774
|
+
}
|
|
35775
|
+
/**
|
|
35776
|
+
* Fetch latest messages from a folder.
|
|
35777
|
+
* Returns list of message summaries.
|
|
35778
|
+
*/
|
|
35779
|
+
async inbox(limit = 20, offset = 0, folder = "INBOX") {
|
|
35780
|
+
let socket;
|
|
35781
|
+
try {
|
|
35782
|
+
socket = await this.imapConnect();
|
|
35783
|
+
} catch (err) {
|
|
35784
|
+
throw imapFail("inbox", err);
|
|
35785
|
+
}
|
|
35786
|
+
try {
|
|
35787
|
+
await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
|
|
35788
|
+
const searchResp = await imapCommand(socket, "SEARCH ALL");
|
|
35789
|
+
const uids = parseSearchResponse(searchResp);
|
|
35790
|
+
if (uids.length === 0) return [];
|
|
35791
|
+
uids.reverse();
|
|
35792
|
+
const selected = uids.slice(offset, offset + limit);
|
|
35793
|
+
if (selected.length === 0) return [];
|
|
35794
|
+
const messages = [];
|
|
35795
|
+
for (const uid of selected) {
|
|
35796
|
+
const fetchResp = await imapCommand(socket, `FETCH ${uid} (FLAGS BODY.PEEK[HEADER.FIELDS (FROM TO SUBJECT DATE)])`);
|
|
35797
|
+
messages.push(parseHeaderResponse(uid, fetchResp));
|
|
35798
|
+
}
|
|
35799
|
+
return messages;
|
|
35800
|
+
} catch (err) {
|
|
35801
|
+
throw imapFail("inbox", err);
|
|
35802
|
+
} finally {
|
|
35803
|
+
await this.imapDisconnect(socket);
|
|
35804
|
+
}
|
|
35805
|
+
}
|
|
35806
|
+
/**
|
|
35807
|
+
* Read a single message by sequence number or UID.
|
|
35808
|
+
*/
|
|
35809
|
+
async read(uid, folder = "INBOX") {
|
|
35810
|
+
let socket;
|
|
35811
|
+
try {
|
|
35812
|
+
socket = await this.imapConnect();
|
|
35813
|
+
} catch (err) {
|
|
35814
|
+
throw imapFail("read", err);
|
|
35815
|
+
}
|
|
35816
|
+
try {
|
|
35817
|
+
await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
|
|
35818
|
+
const fetchResp = await imapCommand(socket, `FETCH ${uid} (FLAGS BODY[])`);
|
|
35819
|
+
if (!/\{\d+\}/.test(fetchResp)) {
|
|
35820
|
+
return emptyFullMessage(uid);
|
|
35821
|
+
}
|
|
35822
|
+
await imapCommand(socket, `STORE ${uid} +FLAGS (\\Seen)`);
|
|
35823
|
+
return parseFullMessage(uid, fetchResp);
|
|
35824
|
+
} catch (err) {
|
|
35825
|
+
throw imapFail("read", err);
|
|
35826
|
+
} finally {
|
|
35827
|
+
await this.imapDisconnect(socket);
|
|
35828
|
+
}
|
|
35829
|
+
}
|
|
35830
|
+
/**
|
|
35831
|
+
* Search messages using IMAP search criteria.
|
|
35832
|
+
*/
|
|
35833
|
+
async search(folder = "INBOX", subject, sender, since, before, unseenOnly = false, limit = 50) {
|
|
35834
|
+
const criteria = ["ALL"];
|
|
35835
|
+
if (subject) criteria.push(`SUBJECT "${subject}"`);
|
|
35836
|
+
if (sender) criteria.push(`FROM "${sender}"`);
|
|
35837
|
+
if (since) criteria.push(`SINCE ${since}`);
|
|
35838
|
+
if (before) criteria.push(`BEFORE ${before}`);
|
|
35839
|
+
if (unseenOnly) criteria.push("UNSEEN");
|
|
35840
|
+
const query = criteria.join(" ");
|
|
35841
|
+
let socket;
|
|
35842
|
+
try {
|
|
35843
|
+
socket = await this.imapConnect();
|
|
35844
|
+
} catch (err) {
|
|
35845
|
+
throw imapFail("search", err);
|
|
35846
|
+
}
|
|
35847
|
+
try {
|
|
35848
|
+
await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
|
|
35849
|
+
const searchResp = await imapCommand(socket, `SEARCH ${query}`);
|
|
35850
|
+
const uids = parseSearchResponse(searchResp);
|
|
35851
|
+
if (uids.length === 0) return [];
|
|
35852
|
+
uids.reverse();
|
|
35853
|
+
const messages = [];
|
|
35854
|
+
for (const uid of uids.slice(0, limit)) {
|
|
35855
|
+
const fetchResp = await imapCommand(socket, `FETCH ${uid} (FLAGS BODY.PEEK[HEADER.FIELDS (FROM TO SUBJECT DATE)])`);
|
|
35856
|
+
messages.push(parseHeaderResponse(uid, fetchResp));
|
|
35857
|
+
}
|
|
35858
|
+
return messages;
|
|
35859
|
+
} catch (err) {
|
|
35860
|
+
throw imapFail("search", err);
|
|
35861
|
+
} finally {
|
|
35862
|
+
await this.imapDisconnect(socket);
|
|
35863
|
+
}
|
|
35864
|
+
}
|
|
35865
|
+
/**
|
|
35866
|
+
* Delete a message by UID.
|
|
35867
|
+
*/
|
|
35868
|
+
async deleteMessage(uid, folder = "INBOX") {
|
|
35869
|
+
const socket = await this.imapConnect();
|
|
35870
|
+
try {
|
|
35871
|
+
await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
|
|
35872
|
+
await imapCommand(socket, `STORE ${uid} +FLAGS (\\Deleted)`);
|
|
35873
|
+
await imapCommand(socket, "EXPUNGE");
|
|
35874
|
+
} finally {
|
|
35875
|
+
await this.imapDisconnect(socket);
|
|
35876
|
+
}
|
|
35877
|
+
}
|
|
35878
|
+
/**
|
|
35879
|
+
* Mark a message as read.
|
|
35880
|
+
*/
|
|
35881
|
+
async markRead(uid, folder = "INBOX") {
|
|
35882
|
+
const socket = await this.imapConnect();
|
|
35883
|
+
try {
|
|
35884
|
+
await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
|
|
35885
|
+
await imapCommand(socket, `STORE ${uid} +FLAGS (\\Seen)`);
|
|
35886
|
+
} finally {
|
|
35887
|
+
await this.imapDisconnect(socket);
|
|
35888
|
+
}
|
|
35889
|
+
}
|
|
35890
|
+
/**
|
|
35891
|
+
* Count unseen messages in a folder.
|
|
35892
|
+
*/
|
|
35893
|
+
async unread(folder = "INBOX") {
|
|
35894
|
+
let socket;
|
|
35895
|
+
try {
|
|
35896
|
+
socket = await this.imapConnect();
|
|
35897
|
+
} catch (err) {
|
|
35898
|
+
throw imapFail("unread", err);
|
|
35899
|
+
}
|
|
35900
|
+
try {
|
|
35901
|
+
await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
|
|
35902
|
+
const searchResp = await imapCommand(socket, "SEARCH UNSEEN");
|
|
35903
|
+
return parseSearchResponse(searchResp).length;
|
|
35904
|
+
} catch (err) {
|
|
35905
|
+
throw imapFail("unread", err);
|
|
35906
|
+
} finally {
|
|
35907
|
+
await this.imapDisconnect(socket);
|
|
35908
|
+
}
|
|
35909
|
+
}
|
|
35910
|
+
/**
|
|
35911
|
+
* List available IMAP folders/mailboxes.
|
|
35912
|
+
*/
|
|
35913
|
+
async folders() {
|
|
35914
|
+
let socket;
|
|
35915
|
+
try {
|
|
35916
|
+
socket = await this.imapConnect();
|
|
35917
|
+
} catch (err) {
|
|
35918
|
+
throw imapFail("folders", err);
|
|
35919
|
+
}
|
|
35920
|
+
try {
|
|
35921
|
+
const resp = await imapCommand(socket, 'LIST "" "*"');
|
|
35922
|
+
const result = [];
|
|
35923
|
+
for (const line of resp.split("\r\n")) {
|
|
35924
|
+
const m = line.match(/\* LIST \([^)]*\) "[^"]*" "?([^"\r\n]+)"?/i);
|
|
35925
|
+
if (m) result.push(m[1]);
|
|
35926
|
+
}
|
|
35927
|
+
return result;
|
|
35928
|
+
} catch (err) {
|
|
35929
|
+
throw imapFail("folders", err);
|
|
35930
|
+
} finally {
|
|
35931
|
+
await this.imapDisconnect(socket);
|
|
35932
|
+
}
|
|
35933
|
+
}
|
|
35934
|
+
/**
|
|
35935
|
+
* Test IMAP connectivity without reading.
|
|
35936
|
+
*/
|
|
35937
|
+
async testImapConnection() {
|
|
35938
|
+
try {
|
|
35939
|
+
const socket = await this.imapConnect();
|
|
35940
|
+
await this.imapDisconnect(socket);
|
|
35941
|
+
return { success: true, message: `Connected to ${this.imapHost}:${this.imapPort}` };
|
|
35942
|
+
} catch (err) {
|
|
35943
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
35944
|
+
return { success: false, message: `IMAP connection failed: ${errMsg}` };
|
|
35945
|
+
}
|
|
35946
|
+
}
|
|
35947
|
+
};
|
|
35948
|
+
imapTagCounter = 0;
|
|
35949
|
+
}
|
|
35950
|
+
});
|
|
35951
|
+
|
|
35627
35952
|
// ../core/src/wsdl.ts
|
|
35628
35953
|
function escapeXml(value) {
|
|
35629
35954
|
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|