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
|
@@ -1297,11 +1297,14 @@ var init_sqlite = __esm({
|
|
|
1297
1297
|
const pragma = schema && isIdentifier(schema) && isIdentifier(tbl) ? `PRAGMA ${schema}.table_info("${tbl}")` : `PRAGMA table_info("${table2}")`;
|
|
1298
1298
|
const rows = this.db.prepare(pragma).all();
|
|
1299
1299
|
return rows.map((r) => ({
|
|
1300
|
+
// PRAGMA table_info reports `pk` as the 1-BASED POSITION within the primary
|
|
1301
|
+
// key, not a boolean: a composite key gives pk=1, pk=2, ... Testing `=== 1`
|
|
1302
|
+
// reported only the first column of a composite key.
|
|
1300
1303
|
name: r.name,
|
|
1301
1304
|
type: r.type,
|
|
1302
1305
|
nullable: r.notnull === 0,
|
|
1303
1306
|
default: r.dflt_value,
|
|
1304
|
-
primaryKey: r.pk
|
|
1307
|
+
primaryKey: Number(r.pk) > 0
|
|
1305
1308
|
}));
|
|
1306
1309
|
}
|
|
1307
1310
|
lastInsertId() {
|
|
@@ -4297,6 +4300,8 @@ var init_database = __esm({
|
|
|
4297
4300
|
poolIndex = 0;
|
|
4298
4301
|
/** Factory for creating new adapters (used by pool) */
|
|
4299
4302
|
adapterFactory = null;
|
|
4303
|
+
/** table -> primary-key column name (or null), introspected once */
|
|
4304
|
+
_pkCache = /* @__PURE__ */ new Map();
|
|
4300
4305
|
/**
|
|
4301
4306
|
* Whether a standalone write auto-commits. ON by default — a write made
|
|
4302
4307
|
* outside an explicit transaction commits on its own connection before
|
|
@@ -4555,29 +4560,112 @@ var init_database = __esm({
|
|
|
4555
4560
|
}
|
|
4556
4561
|
return result;
|
|
4557
4562
|
}
|
|
4558
|
-
/**
|
|
4563
|
+
/**
|
|
4564
|
+
* The table's primary-key column, introspected once and cached.
|
|
4565
|
+
*
|
|
4566
|
+
* Uses the cross-engine getColumns() contract (v3.13.14, #48), which reports
|
|
4567
|
+
* primaryKey per column on every adapter. Resolves to null when the table has
|
|
4568
|
+
* no primary key or cannot be introspected.
|
|
4569
|
+
*/
|
|
4570
|
+
async primaryKey(table2) {
|
|
4571
|
+
if (!this._pkCache.has(table2)) {
|
|
4572
|
+
let pk = [];
|
|
4573
|
+
try {
|
|
4574
|
+
const columns = await this.getColumns(table2);
|
|
4575
|
+
pk = columns.filter((c) => c.primaryKey).map((c) => c.name);
|
|
4576
|
+
} catch {
|
|
4577
|
+
pk = [];
|
|
4578
|
+
}
|
|
4579
|
+
this._pkCache.set(table2, pk);
|
|
4580
|
+
}
|
|
4581
|
+
return this._pkCache.get(table2) ?? [];
|
|
4582
|
+
}
|
|
4583
|
+
/**
|
|
4584
|
+
* A failed write must be loud.
|
|
4585
|
+
*
|
|
4586
|
+
* The adapters catch a SQL error and return { success: false, affectedRows: 0 },
|
|
4587
|
+
* so a filterless update produced invalid SQL ("... WHERE ") and reported
|
|
4588
|
+
* nothing rather than raising. A caller who does not inspect the result
|
|
4589
|
+
* believes the write landed (audit feature 4, P1).
|
|
4590
|
+
*/
|
|
4591
|
+
static assertWrote(result, verb, table2) {
|
|
4592
|
+
if (result && result.success === false) {
|
|
4593
|
+
throw new Error(
|
|
4594
|
+
`${verb} failed on ${table2}: ${result.error ?? "unknown error"}`
|
|
4595
|
+
);
|
|
4596
|
+
}
|
|
4597
|
+
return result;
|
|
4598
|
+
}
|
|
4599
|
+
/**
|
|
4600
|
+
* Update rows. A write with no filter is an error, not a full-table write.
|
|
4601
|
+
*
|
|
4602
|
+
* With no explicit filter the primary key is taken out of `data` and used as
|
|
4603
|
+
* the WHERE clause. With neither a filter nor a primary key in `data` this
|
|
4604
|
+
* throws rather than silently changing nothing (audit feature 4, P1).
|
|
4605
|
+
*/
|
|
4559
4606
|
async update(table2, data, filter, params) {
|
|
4607
|
+
let effectiveFilter = filter ?? {};
|
|
4608
|
+
let effectiveData = data;
|
|
4609
|
+
if (Object.keys(effectiveFilter).length === 0) {
|
|
4610
|
+
const pkColumns = await this.primaryKey(table2);
|
|
4611
|
+
const missing = pkColumns.filter((c) => !(c in data));
|
|
4612
|
+
if (pkColumns.length === 0 || missing.length > 0) {
|
|
4613
|
+
throw new Error(
|
|
4614
|
+
`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}).`
|
|
4615
|
+
);
|
|
4616
|
+
}
|
|
4617
|
+
effectiveData = { ...data };
|
|
4618
|
+
const keyed = {};
|
|
4619
|
+
for (const col of pkColumns) {
|
|
4620
|
+
keyed[col] = effectiveData[col];
|
|
4621
|
+
delete effectiveData[col];
|
|
4622
|
+
}
|
|
4623
|
+
if (Object.keys(effectiveData).length === 0) {
|
|
4624
|
+
throw new Error(
|
|
4625
|
+
`update was given only the primary key [${pkColumns.join(", ")}] and no columns to set (table=${table2})`
|
|
4626
|
+
);
|
|
4627
|
+
}
|
|
4628
|
+
effectiveFilter = keyed;
|
|
4629
|
+
}
|
|
4560
4630
|
const adapter = this.getNextAdapter();
|
|
4561
|
-
const result = adapter.updateAsync ? await adapter.updateAsync(table2,
|
|
4631
|
+
const result = adapter.updateAsync ? await adapter.updateAsync(table2, effectiveData, effectiveFilter, params) : adapter.update(table2, effectiveData, effectiveFilter, params);
|
|
4562
4632
|
if (this.autoCommit && !this.inExplicitTransaction()) {
|
|
4563
4633
|
try {
|
|
4564
4634
|
await adapterCommit(adapter);
|
|
4565
4635
|
} catch {
|
|
4566
4636
|
}
|
|
4567
4637
|
}
|
|
4568
|
-
return result;
|
|
4638
|
+
return _Database.assertWrote(result, "update", table2);
|
|
4569
4639
|
}
|
|
4570
|
-
/** Delete rows
|
|
4640
|
+
/** Delete rows. A filterless delete throws; use truncate() to empty a table. */
|
|
4571
4641
|
async delete(table2, filter, params) {
|
|
4642
|
+
const effectiveFilter = filter ?? {};
|
|
4643
|
+
if (!Array.isArray(effectiveFilter) && typeof effectiveFilter !== "string" && Object.keys(effectiveFilter).length === 0) {
|
|
4644
|
+
throw new Error(
|
|
4645
|
+
`delete requires a filter (table=${table2}). To remove every row use truncate(${table2}).`
|
|
4646
|
+
);
|
|
4647
|
+
}
|
|
4572
4648
|
const adapter = this.getNextAdapter();
|
|
4573
|
-
const result = adapter.deleteAsync ? await adapter.deleteAsync(table2,
|
|
4649
|
+
const result = adapter.deleteAsync ? await adapter.deleteAsync(table2, effectiveFilter, params) : adapter.delete(table2, effectiveFilter, params);
|
|
4574
4650
|
if (this.autoCommit && !this.inExplicitTransaction()) {
|
|
4575
4651
|
try {
|
|
4576
4652
|
await adapterCommit(adapter);
|
|
4577
4653
|
} catch {
|
|
4578
4654
|
}
|
|
4579
4655
|
}
|
|
4580
|
-
return result;
|
|
4656
|
+
return _Database.assertWrote(result, "delete", table2);
|
|
4657
|
+
}
|
|
4658
|
+
/** Remove every row. The explicit spelling of a whole-table delete. */
|
|
4659
|
+
async truncate(table2) {
|
|
4660
|
+
const adapter = this.getNextAdapter();
|
|
4661
|
+
const result = adapter.deleteAsync ? await adapter.deleteAsync(table2, "1 = 1", []) : adapter.delete(table2, "1 = 1", []);
|
|
4662
|
+
if (this.autoCommit && !this.inExplicitTransaction()) {
|
|
4663
|
+
try {
|
|
4664
|
+
await adapterCommit(adapter);
|
|
4665
|
+
} catch {
|
|
4666
|
+
}
|
|
4667
|
+
}
|
|
4668
|
+
return _Database.assertWrote(result, "truncate", table2);
|
|
4581
4669
|
}
|
|
4582
4670
|
/** Close all database connections (pool or single). */
|
|
4583
4671
|
close() {
|
|
@@ -12121,6 +12209,7 @@ var init_logger = __esm({
|
|
|
12121
12209
|
var auth_exports = {};
|
|
12122
12210
|
__export(auth_exports, {
|
|
12123
12211
|
Auth: () => Auth,
|
|
12212
|
+
JWT_LEEWAY_SECONDS: () => JWT_LEEWAY_SECONDS,
|
|
12124
12213
|
authMiddleware: () => authMiddleware,
|
|
12125
12214
|
authenticateRequest: () => authenticateRequest,
|
|
12126
12215
|
checkPassword: () => checkPassword,
|
|
@@ -12129,6 +12218,7 @@ __export(auth_exports, {
|
|
|
12129
12218
|
getToken: () => getToken,
|
|
12130
12219
|
hashPassword: () => hashPassword,
|
|
12131
12220
|
refreshToken: () => refreshToken,
|
|
12221
|
+
resolveAlgorithm: () => resolveAlgorithm,
|
|
12132
12222
|
validToken: () => validToken,
|
|
12133
12223
|
validateApiKey: () => validateApiKey
|
|
12134
12224
|
});
|
|
@@ -12189,6 +12279,18 @@ function ensureDevSecret(cwd) {
|
|
|
12189
12279
|
}
|
|
12190
12280
|
return newSecret;
|
|
12191
12281
|
}
|
|
12282
|
+
function unsupportedAlgorithmError(algorithm) {
|
|
12283
|
+
return new Error(
|
|
12284
|
+
`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.`
|
|
12285
|
+
);
|
|
12286
|
+
}
|
|
12287
|
+
function resolveAlgorithm(algorithm) {
|
|
12288
|
+
const chosen = (algorithm || process.env.TINA4_JWT_ALGORITHM || "HS256").trim();
|
|
12289
|
+
if (!HMAC_DIGESTS.has(chosen) && !RSA_SIGN_ALGORITHMS.has(chosen)) {
|
|
12290
|
+
throw unsupportedAlgorithmError(chosen);
|
|
12291
|
+
}
|
|
12292
|
+
return chosen;
|
|
12293
|
+
}
|
|
12192
12294
|
function base64urlEncode(data) {
|
|
12193
12295
|
return data.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
12194
12296
|
}
|
|
@@ -12211,7 +12313,7 @@ function getToken(payload, secretOrExpiresIn, expiresIn = 60, algorithm) {
|
|
|
12211
12313
|
if (!resolvedSecret) {
|
|
12212
12314
|
_warnBlankSecret();
|
|
12213
12315
|
}
|
|
12214
|
-
const resolvedAlgorithm = algorithm
|
|
12316
|
+
const resolvedAlgorithm = resolveAlgorithm(algorithm);
|
|
12215
12317
|
const header = { alg: resolvedAlgorithm, typ: "JWT" };
|
|
12216
12318
|
const now = Math.floor(Date.now() / 1e3);
|
|
12217
12319
|
const claims = { ...payload, iat: now };
|
|
@@ -12229,19 +12331,27 @@ function validToken(token, secret, algorithm) {
|
|
|
12229
12331
|
if (!resolvedSecret) {
|
|
12230
12332
|
_warnBlankSecret();
|
|
12231
12333
|
}
|
|
12232
|
-
const resolvedAlgorithm = algorithm
|
|
12334
|
+
const resolvedAlgorithm = resolveAlgorithm(algorithm);
|
|
12233
12335
|
try {
|
|
12234
12336
|
const parts = token.split(".");
|
|
12235
12337
|
if (parts.length !== 3) return null;
|
|
12236
12338
|
const [h, p, sig] = parts;
|
|
12339
|
+
const header = JSON.parse(base64urlDecode(h).toString());
|
|
12340
|
+
if (header.alg !== resolvedAlgorithm) return null;
|
|
12237
12341
|
const signingInput = `${h}.${p}`;
|
|
12238
12342
|
if (!verifySignature(signingInput, sig, resolvedSecret, resolvedAlgorithm)) {
|
|
12239
12343
|
return null;
|
|
12240
12344
|
}
|
|
12241
12345
|
const payload = JSON.parse(base64urlDecode(p).toString());
|
|
12242
|
-
|
|
12346
|
+
const now = Date.now() / 1e3;
|
|
12347
|
+
if (typeof payload.exp === "number" && now > payload.exp) {
|
|
12243
12348
|
return null;
|
|
12244
12349
|
}
|
|
12350
|
+
if (Object.hasOwn(payload, "nbf")) {
|
|
12351
|
+
const notBefore = payload.nbf;
|
|
12352
|
+
if (typeof notBefore !== "number" || !Number.isFinite(notBefore)) return null;
|
|
12353
|
+
if (now + JWT_LEEWAY_SECONDS < notBefore) return null;
|
|
12354
|
+
}
|
|
12245
12355
|
return payload;
|
|
12246
12356
|
} catch {
|
|
12247
12357
|
return null;
|
|
@@ -12257,32 +12367,33 @@ function getPayload(token) {
|
|
|
12257
12367
|
}
|
|
12258
12368
|
}
|
|
12259
12369
|
function sign(input, secret, algorithm) {
|
|
12260
|
-
|
|
12261
|
-
|
|
12262
|
-
return base64urlEncode(
|
|
12370
|
+
const digest = HMAC_DIGESTS.get(algorithm);
|
|
12371
|
+
if (digest) {
|
|
12372
|
+
return base64urlEncode(createHmac2(digest, secret).update(input).digest());
|
|
12263
12373
|
}
|
|
12264
|
-
|
|
12265
|
-
|
|
12374
|
+
const rsaAlgorithm = RSA_SIGN_ALGORITHMS.get(algorithm);
|
|
12375
|
+
if (rsaAlgorithm) {
|
|
12376
|
+
const signer = createSign(rsaAlgorithm);
|
|
12266
12377
|
signer.update(input);
|
|
12267
|
-
|
|
12268
|
-
return base64urlEncode(sig);
|
|
12378
|
+
return base64urlEncode(signer.sign(secret));
|
|
12269
12379
|
}
|
|
12270
|
-
throw
|
|
12380
|
+
throw unsupportedAlgorithmError(algorithm);
|
|
12271
12381
|
}
|
|
12272
12382
|
function verifySignature(input, sig, secret, algorithm) {
|
|
12273
|
-
if (algorithm
|
|
12383
|
+
if (HMAC_DIGESTS.has(algorithm)) {
|
|
12274
12384
|
const expected = sign(input, secret, algorithm);
|
|
12275
12385
|
const a = Buffer.from(sig);
|
|
12276
12386
|
const b = Buffer.from(expected);
|
|
12277
12387
|
if (a.length !== b.length) return false;
|
|
12278
12388
|
return timingSafeEqual(a, b);
|
|
12279
12389
|
}
|
|
12280
|
-
|
|
12281
|
-
|
|
12390
|
+
const rsaAlgorithm = RSA_SIGN_ALGORITHMS.get(algorithm);
|
|
12391
|
+
if (rsaAlgorithm) {
|
|
12392
|
+
const verifier = createVerify(rsaAlgorithm);
|
|
12282
12393
|
verifier.update(input);
|
|
12283
12394
|
return verifier.verify(secret, base64urlDecode(sig));
|
|
12284
12395
|
}
|
|
12285
|
-
throw
|
|
12396
|
+
throw unsupportedAlgorithmError(algorithm);
|
|
12286
12397
|
}
|
|
12287
12398
|
function hashPassword(password, salt, iterations = 26e4) {
|
|
12288
12399
|
const actualSalt = salt ?? randomBytes3(16).toString("hex");
|
|
@@ -12307,7 +12418,7 @@ function checkPassword(password, hash) {
|
|
|
12307
12418
|
return false;
|
|
12308
12419
|
}
|
|
12309
12420
|
}
|
|
12310
|
-
function authMiddleware(secret, algorithm
|
|
12421
|
+
function authMiddleware(secret, algorithm) {
|
|
12311
12422
|
return (req2, res, next) => {
|
|
12312
12423
|
const authHeader = req2.headers.authorization ?? "";
|
|
12313
12424
|
if (!authHeader.startsWith("Bearer ")) {
|
|
@@ -12330,11 +12441,11 @@ function refreshToken(token, expiresIn = 60) {
|
|
|
12330
12441
|
const { iat: _iat, exp: _exp, ...claims } = payload;
|
|
12331
12442
|
return getToken(claims, expiresIn);
|
|
12332
12443
|
}
|
|
12333
|
-
function authenticateRequest(headers, secret, algorithm
|
|
12444
|
+
function authenticateRequest(headers, secret, algorithm) {
|
|
12334
12445
|
const authHeader = headers.authorization ?? headers.Authorization ?? "";
|
|
12335
12446
|
if (!authHeader.startsWith("Bearer ")) return null;
|
|
12336
12447
|
const token = authHeader.slice(7);
|
|
12337
|
-
if (validToken(token)) return getPayload(token);
|
|
12448
|
+
if (validToken(token, secret, algorithm)) return getPayload(token);
|
|
12338
12449
|
if (validateApiKey(token)) {
|
|
12339
12450
|
return { _auth: "api_key" };
|
|
12340
12451
|
}
|
|
@@ -12348,12 +12459,23 @@ function validateApiKey(provided, expected) {
|
|
|
12348
12459
|
if (a.length !== b.length) return false;
|
|
12349
12460
|
return timingSafeEqual(a, b);
|
|
12350
12461
|
}
|
|
12351
|
-
var BLANK_SECRET_WARNING, Auth;
|
|
12462
|
+
var BLANK_SECRET_WARNING, HMAC_DIGESTS, RSA_SIGN_ALGORITHMS, SUPPORTED_ALGORITHMS, JWT_LEEWAY_SECONDS, Auth;
|
|
12352
12463
|
var init_auth = __esm({
|
|
12353
12464
|
"src/auth.ts"() {
|
|
12354
12465
|
"use strict";
|
|
12355
12466
|
init_dotenv();
|
|
12356
12467
|
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.";
|
|
12468
|
+
HMAC_DIGESTS = /* @__PURE__ */ new Map([
|
|
12469
|
+
["HS256", "sha256"],
|
|
12470
|
+
["HS384", "sha384"],
|
|
12471
|
+
["HS512", "sha512"]
|
|
12472
|
+
]);
|
|
12473
|
+
RSA_SIGN_ALGORITHMS = /* @__PURE__ */ new Map([["RS256", "RSA-SHA256"]]);
|
|
12474
|
+
SUPPORTED_ALGORITHMS = [
|
|
12475
|
+
...HMAC_DIGESTS.keys(),
|
|
12476
|
+
...RSA_SIGN_ALGORITHMS.keys()
|
|
12477
|
+
];
|
|
12478
|
+
JWT_LEEWAY_SECONDS = 60;
|
|
12357
12479
|
Auth = class {
|
|
12358
12480
|
static getToken = getToken;
|
|
12359
12481
|
static validToken = validToken;
|
|
@@ -13792,7 +13914,7 @@ function _generateFormToken(descriptor = "") {
|
|
|
13792
13914
|
function _generateFormTokenValue(descriptor = "") {
|
|
13793
13915
|
return new SafeString(_buildFormTokenJwt(descriptor));
|
|
13794
13916
|
}
|
|
13795
|
-
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;
|
|
13917
|
+
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;
|
|
13796
13918
|
var init_engine = __esm({
|
|
13797
13919
|
"../frond/src/engine.ts"() {
|
|
13798
13920
|
"use strict";
|
|
@@ -13835,6 +13957,29 @@ var init_engine = __esm({
|
|
|
13835
13957
|
"endset",
|
|
13836
13958
|
"endspaceless"
|
|
13837
13959
|
]);
|
|
13960
|
+
GATEABLE_TAGS = /* @__PURE__ */ new Set([
|
|
13961
|
+
"autoescape",
|
|
13962
|
+
"cache",
|
|
13963
|
+
"for",
|
|
13964
|
+
"from",
|
|
13965
|
+
"if",
|
|
13966
|
+
"import",
|
|
13967
|
+
"include",
|
|
13968
|
+
"live",
|
|
13969
|
+
"macro",
|
|
13970
|
+
"set",
|
|
13971
|
+
"spaceless"
|
|
13972
|
+
]);
|
|
13973
|
+
BLOCK_TAG_ENDS = {
|
|
13974
|
+
autoescape: "endautoescape",
|
|
13975
|
+
cache: "endcache",
|
|
13976
|
+
for: "endfor",
|
|
13977
|
+
if: "endif",
|
|
13978
|
+
live: "endlive",
|
|
13979
|
+
macro: "endmacro",
|
|
13980
|
+
set: "endset",
|
|
13981
|
+
spaceless: "endspaceless"
|
|
13982
|
+
};
|
|
13838
13983
|
JSON_UNSAFE_RE = /[<>&'\u2028\u2029]/g;
|
|
13839
13984
|
JSON_UNSAFE_MAP = {
|
|
13840
13985
|
"<": "\\u003c",
|
|
@@ -14470,42 +14615,27 @@ var init_engine = __esm({
|
|
|
14470
14615
|
if (stripA && i + 1 < tokens.length && tokens[i + 1][0] === "TEXT") {
|
|
14471
14616
|
tokens[i + 1] = ["TEXT", tokens[i + 1][1].replace(LEADING_WS_RE, "")];
|
|
14472
14617
|
}
|
|
14473
|
-
if (tag
|
|
14474
|
-
|
|
14475
|
-
|
|
14476
|
-
|
|
14477
|
-
|
|
14478
|
-
|
|
14479
|
-
output.push(result);
|
|
14480
|
-
i = skip;
|
|
14481
|
-
}
|
|
14618
|
+
if (!this.tagPermitted(tag)) {
|
|
14619
|
+
i = this.skipDeniedTag(tokens, i, tag, content);
|
|
14620
|
+
} else if (tag === "if") {
|
|
14621
|
+
const [result, skip] = this.handleIf(tokens, i, context);
|
|
14622
|
+
output.push(result);
|
|
14623
|
+
i = skip;
|
|
14482
14624
|
} else if (tag === "for") {
|
|
14483
|
-
|
|
14484
|
-
|
|
14485
|
-
|
|
14486
|
-
} else {
|
|
14487
|
-
const [result, skip] = this.handleFor(tokens, i, context);
|
|
14488
|
-
output.push(result);
|
|
14489
|
-
i = skip;
|
|
14490
|
-
}
|
|
14625
|
+
const [result, skip] = this.handleFor(tokens, i, context);
|
|
14626
|
+
output.push(result);
|
|
14627
|
+
i = skip;
|
|
14491
14628
|
} else if (tag === "set") {
|
|
14492
|
-
|
|
14493
|
-
if (this._sandbox && this._allowedTags !== null && !this._allowedTags.has("set")) {
|
|
14494
|
-
i = isBlockSet ? this.skipBlock(tokens, i, "set", "endset") : i + 1;
|
|
14495
|
-
} else if (isBlockSet) {
|
|
14629
|
+
if (!content.includes("=")) {
|
|
14496
14630
|
i = this.handleSetBlock(tokens, i, context);
|
|
14497
14631
|
} else {
|
|
14498
14632
|
this.handleSet(content, context);
|
|
14499
14633
|
i++;
|
|
14500
14634
|
}
|
|
14501
14635
|
} else if (tag === "include") {
|
|
14502
|
-
|
|
14503
|
-
|
|
14504
|
-
|
|
14505
|
-
const result = this.handleInclude(content, context);
|
|
14506
|
-
output.push(result);
|
|
14507
|
-
i++;
|
|
14508
|
-
}
|
|
14636
|
+
const result = this.handleInclude(content, context);
|
|
14637
|
+
output.push(result);
|
|
14638
|
+
i++;
|
|
14509
14639
|
} else if (tag === "macro") {
|
|
14510
14640
|
const skip = this.handleMacro(tokens, i, context);
|
|
14511
14641
|
i = skip;
|
|
@@ -14550,6 +14680,41 @@ var init_engine = __esm({
|
|
|
14550
14680
|
}
|
|
14551
14681
|
return output.join("");
|
|
14552
14682
|
}
|
|
14683
|
+
/**
|
|
14684
|
+
* May this filter RUN under the current sandbox?
|
|
14685
|
+
*
|
|
14686
|
+
* The escaping decision has to ask this rather than read the filter name out of
|
|
14687
|
+
* the source. Node carries safety as a FLAG rather than as a value-level marker
|
|
14688
|
+
* (Python and Ruby return a SafeString, PHP prepends a RAW_MARKER -- all three
|
|
14689
|
+
* produced only by actually running the filter), so here the name alone was
|
|
14690
|
+
* enough to suppress auto-escaping even when the filter was denied and skipped.
|
|
14691
|
+
*/
|
|
14692
|
+
filterPermitted(name) {
|
|
14693
|
+
if (!this._sandbox || this._allowedFilters === null) return true;
|
|
14694
|
+
return this._allowedFilters.has(name);
|
|
14695
|
+
}
|
|
14696
|
+
/**
|
|
14697
|
+
* May this tag run under the current sandbox?
|
|
14698
|
+
*
|
|
14699
|
+
* One gate for every tag, so the allow-list governs the whole tag vocabulary
|
|
14700
|
+
* instead of the four names that happened to be checked individually.
|
|
14701
|
+
*/
|
|
14702
|
+
tagPermitted(tag) {
|
|
14703
|
+
if (!this._sandbox || this._allowedTags === null) return true;
|
|
14704
|
+
if (!GATEABLE_TAGS.has(tag)) return true;
|
|
14705
|
+
return this._allowedTags.has(tag);
|
|
14706
|
+
}
|
|
14707
|
+
/**
|
|
14708
|
+
* Consume a denied tag WITHOUT running it, returning the index past its body.
|
|
14709
|
+
*
|
|
14710
|
+
* Advancing a single token past a body-owning tag would leave the body's tokens
|
|
14711
|
+
* to render at the TOP level, leaking exactly the content the sandbox denied.
|
|
14712
|
+
*/
|
|
14713
|
+
skipDeniedTag(tokens, start2, tag, content) {
|
|
14714
|
+
const closeTag = BLOCK_TAG_ENDS[tag];
|
|
14715
|
+
if (closeTag === void 0 || tag === "set" && content.includes("=")) return start2 + 1;
|
|
14716
|
+
return this.skipBlock(tokens, start2, tag, closeTag);
|
|
14717
|
+
}
|
|
14553
14718
|
skipBlock(tokens, start2, openTag, closeTag) {
|
|
14554
14719
|
let depth = 0;
|
|
14555
14720
|
let i = start2 + 1;
|
|
@@ -14557,7 +14722,7 @@ var init_engine = __esm({
|
|
|
14557
14722
|
if (tokens[i][0] === "BLOCK") {
|
|
14558
14723
|
const [content] = stripTag(tokens[i][1]);
|
|
14559
14724
|
const tag = content.split(/\s+/)[0] || "";
|
|
14560
|
-
if (tag === openTag) depth++;
|
|
14725
|
+
if (tag === openTag && !(openTag === "set" && content.includes("="))) depth++;
|
|
14561
14726
|
else if (tag === closeTag) {
|
|
14562
14727
|
if (depth === 0) return i + 1;
|
|
14563
14728
|
depth--;
|
|
@@ -14742,11 +14907,11 @@ var init_engine = __esm({
|
|
|
14742
14907
|
for (const [fname, rawArgs] of filters) {
|
|
14743
14908
|
const args = rawArgs.map((a) => a instanceof VarRef ? evalExpr(a.name, context) : a);
|
|
14744
14909
|
if (fname === "raw" || fname === "safe") {
|
|
14745
|
-
isSafe = true;
|
|
14910
|
+
if (this.filterPermitted(fname)) isSafe = true;
|
|
14746
14911
|
continue;
|
|
14747
14912
|
}
|
|
14748
14913
|
if (fname === "escape" || fname === "e") {
|
|
14749
|
-
isSafe = true;
|
|
14914
|
+
if (this.filterPermitted(fname)) isSafe = true;
|
|
14750
14915
|
}
|
|
14751
14916
|
if (this._sandbox && this._allowedFilters !== null) {
|
|
14752
14917
|
if (!this._allowedFilters.has(fname)) {
|
|
@@ -16557,858 +16722,128 @@ var init_rateLimiter = __esm({
|
|
|
16557
16722
|
}
|
|
16558
16723
|
});
|
|
16559
16724
|
|
|
16560
|
-
// src/
|
|
16561
|
-
import
|
|
16562
|
-
import
|
|
16563
|
-
import { readFileSync as readFileSync8 } from "node:fs";
|
|
16564
|
-
import { basename as basename2 } from "node:path";
|
|
16725
|
+
// src/devMailbox.ts
|
|
16726
|
+
import { mkdirSync as mkdirSync8, readdirSync as readdirSync6, readFileSync as readFileSync8, writeFileSync as writeFileSync6, unlinkSync as unlinkSync4, existsSync as existsSync10 } from "node:fs";
|
|
16727
|
+
import { join as join12 } from "node:path";
|
|
16565
16728
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
16566
|
-
|
|
16567
|
-
|
|
16568
|
-
|
|
16569
|
-
|
|
16570
|
-
|
|
16571
|
-
|
|
16572
|
-
|
|
16573
|
-
|
|
16574
|
-
const lines = buffer.split("\r\n");
|
|
16575
|
-
for (let i = 0; i < lines.length; i++) {
|
|
16576
|
-
const line = lines[i];
|
|
16577
|
-
if (line.length < 3) continue;
|
|
16578
|
-
const code = parseInt(line.substring(0, 3), 10);
|
|
16579
|
-
if (line.length >= 4 && line[3] === " ") {
|
|
16580
|
-
socket.removeListener("data", onData);
|
|
16581
|
-
socket.removeListener("error", onError);
|
|
16582
|
-
resolve21({ code, text: buffer.trim() });
|
|
16583
|
-
return;
|
|
16584
|
-
}
|
|
16585
|
-
}
|
|
16586
|
-
};
|
|
16587
|
-
const onError = (err) => {
|
|
16588
|
-
socket.removeListener("data", onData);
|
|
16589
|
-
reject(err);
|
|
16590
|
-
};
|
|
16591
|
-
socket.on("data", onData);
|
|
16592
|
-
socket.on("error", onError);
|
|
16593
|
-
});
|
|
16594
|
-
}
|
|
16595
|
-
function sendCommand(socket, command) {
|
|
16596
|
-
return new Promise((resolve21, reject) => {
|
|
16597
|
-
socket.write(command + "\r\n", "utf-8", (err) => {
|
|
16598
|
-
if (err) return reject(err);
|
|
16599
|
-
readResponse(socket).then(resolve21, reject);
|
|
16600
|
-
});
|
|
16601
|
-
});
|
|
16602
|
-
}
|
|
16603
|
-
function buildMimeMessage(options) {
|
|
16604
|
-
const boundary = `----=_Tina4_${Date.now()}_${Math.random().toString(36).substring(2)}`;
|
|
16605
|
-
const altBoundary = `----=_Tina4Alt_${Date.now()}_${Math.random().toString(36).substring(2)}`;
|
|
16606
|
-
const hasAttachments = options.attachments && options.attachments.length > 0;
|
|
16607
|
-
const hasTextAlt = options.text !== void 0 && options.html;
|
|
16608
|
-
const lines = [];
|
|
16609
|
-
const fromHeader = options.fromName ? `"${options.fromName}" <${options.from}>` : options.from;
|
|
16610
|
-
lines.push(`From: ${fromHeader}`);
|
|
16611
|
-
lines.push(`To: ${options.to.join(", ")}`);
|
|
16612
|
-
if (options.cc.length > 0) {
|
|
16613
|
-
lines.push(`Cc: ${options.cc.join(", ")}`);
|
|
16614
|
-
}
|
|
16615
|
-
lines.push(`Subject: ${options.subject}`);
|
|
16616
|
-
lines.push(`Date: ${(/* @__PURE__ */ new Date()).toUTCString()}`);
|
|
16617
|
-
lines.push(`Message-ID: <${options.messageId}>`);
|
|
16618
|
-
lines.push("MIME-Version: 1.0");
|
|
16619
|
-
if (options.replyTo) {
|
|
16620
|
-
lines.push(`Reply-To: ${options.replyTo}`);
|
|
16621
|
-
}
|
|
16622
|
-
if (options.headers) {
|
|
16623
|
-
for (const [key, value] of Object.entries(options.headers)) {
|
|
16624
|
-
lines.push(`${key}: ${value}`);
|
|
16625
|
-
}
|
|
16626
|
-
}
|
|
16627
|
-
if (hasAttachments) {
|
|
16628
|
-
lines.push(`Content-Type: multipart/mixed; boundary="${boundary}"`);
|
|
16629
|
-
lines.push("");
|
|
16630
|
-
lines.push(`--${boundary}`);
|
|
16631
|
-
if (hasTextAlt) {
|
|
16632
|
-
lines.push(`Content-Type: multipart/alternative; boundary="${altBoundary}"`);
|
|
16633
|
-
lines.push("");
|
|
16634
|
-
lines.push(`--${altBoundary}`);
|
|
16635
|
-
lines.push("Content-Type: text/plain; charset=UTF-8");
|
|
16636
|
-
lines.push("Content-Transfer-Encoding: 7bit");
|
|
16637
|
-
lines.push("");
|
|
16638
|
-
lines.push(options.text);
|
|
16639
|
-
lines.push("");
|
|
16640
|
-
lines.push(`--${altBoundary}`);
|
|
16641
|
-
lines.push("Content-Type: text/html; charset=UTF-8");
|
|
16642
|
-
lines.push("Content-Transfer-Encoding: 7bit");
|
|
16643
|
-
lines.push("");
|
|
16644
|
-
lines.push(options.body);
|
|
16645
|
-
lines.push("");
|
|
16646
|
-
lines.push(`--${altBoundary}--`);
|
|
16647
|
-
} else {
|
|
16648
|
-
const contentType = options.html ? "text/html" : "text/plain";
|
|
16649
|
-
lines.push(`Content-Type: ${contentType}; charset=UTF-8`);
|
|
16650
|
-
lines.push("Content-Transfer-Encoding: 7bit");
|
|
16651
|
-
lines.push("");
|
|
16652
|
-
lines.push(options.body);
|
|
16653
|
-
}
|
|
16654
|
-
for (const filePath of options.attachments) {
|
|
16655
|
-
const fileName = basename2(filePath);
|
|
16656
|
-
const fileData = readFileSync8(filePath);
|
|
16657
|
-
const base64Data = fileData.toString("base64");
|
|
16658
|
-
lines.push("");
|
|
16659
|
-
lines.push(`--${boundary}`);
|
|
16660
|
-
lines.push(`Content-Type: application/octet-stream; name="${fileName}"`);
|
|
16661
|
-
lines.push("Content-Transfer-Encoding: base64");
|
|
16662
|
-
lines.push(`Content-Disposition: attachment; filename="${fileName}"`);
|
|
16663
|
-
lines.push("");
|
|
16664
|
-
for (let i = 0; i < base64Data.length; i += 76) {
|
|
16665
|
-
lines.push(base64Data.substring(i, i + 76));
|
|
16666
|
-
}
|
|
16667
|
-
}
|
|
16668
|
-
lines.push("");
|
|
16669
|
-
lines.push(`--${boundary}--`);
|
|
16670
|
-
} else if (hasTextAlt) {
|
|
16671
|
-
lines.push(`Content-Type: multipart/alternative; boundary="${altBoundary}"`);
|
|
16672
|
-
lines.push("");
|
|
16673
|
-
lines.push(`--${altBoundary}`);
|
|
16674
|
-
lines.push("Content-Type: text/plain; charset=UTF-8");
|
|
16675
|
-
lines.push("Content-Transfer-Encoding: 7bit");
|
|
16676
|
-
lines.push("");
|
|
16677
|
-
lines.push(options.text);
|
|
16678
|
-
lines.push("");
|
|
16679
|
-
lines.push(`--${altBoundary}`);
|
|
16680
|
-
lines.push("Content-Type: text/html; charset=UTF-8");
|
|
16681
|
-
lines.push("Content-Transfer-Encoding: 7bit");
|
|
16682
|
-
lines.push("");
|
|
16683
|
-
lines.push(options.body);
|
|
16684
|
-
lines.push("");
|
|
16685
|
-
lines.push(`--${altBoundary}--`);
|
|
16686
|
-
} else {
|
|
16687
|
-
const contentType = options.html ? "text/html" : "text/plain";
|
|
16688
|
-
lines.push(`Content-Type: ${contentType}; charset=UTF-8`);
|
|
16689
|
-
lines.push("");
|
|
16690
|
-
lines.push(options.body);
|
|
16691
|
-
}
|
|
16692
|
-
return lines.join("\r\n");
|
|
16693
|
-
}
|
|
16694
|
-
function imapQuote(s) {
|
|
16695
|
-
if (/^[a-zA-Z0-9_./-]+$/.test(s)) return s;
|
|
16696
|
-
return '"' + s.replace(/\\/g, "\\\\").replace(/"/g, '\\"') + '"';
|
|
16697
|
-
}
|
|
16698
|
-
function imapReadLine(socket) {
|
|
16699
|
-
return new Promise((resolve21, reject) => {
|
|
16700
|
-
let buffer = "";
|
|
16701
|
-
const onData = (chunk) => {
|
|
16702
|
-
buffer += chunk.toString("utf-8");
|
|
16703
|
-
const nlIndex = buffer.indexOf("\r\n");
|
|
16704
|
-
if (nlIndex !== -1) {
|
|
16705
|
-
socket.removeListener("data", onData);
|
|
16706
|
-
socket.removeListener("error", onError);
|
|
16707
|
-
resolve21(buffer);
|
|
16729
|
+
var DevMailbox;
|
|
16730
|
+
var init_devMailbox = __esm({
|
|
16731
|
+
"src/devMailbox.ts"() {
|
|
16732
|
+
"use strict";
|
|
16733
|
+
DevMailbox = class {
|
|
16734
|
+
mailboxDir;
|
|
16735
|
+
constructor(mailboxDir) {
|
|
16736
|
+
this.mailboxDir = mailboxDir ?? process.env.TINA4_MAILBOX_DIR ?? "data/mailbox";
|
|
16708
16737
|
}
|
|
16709
|
-
|
|
16710
|
-
|
|
16711
|
-
|
|
16712
|
-
|
|
16713
|
-
|
|
16714
|
-
|
|
16715
|
-
|
|
16716
|
-
});
|
|
16717
|
-
}
|
|
16718
|
-
function imapCommand(socket, command) {
|
|
16719
|
-
return new Promise((resolve21, reject) => {
|
|
16720
|
-
imapTagCounter++;
|
|
16721
|
-
const tag = `T${imapTagCounter}`;
|
|
16722
|
-
const fullCommand = `${tag} ${command}\r
|
|
16723
|
-
`;
|
|
16724
|
-
let buffer = "";
|
|
16725
|
-
const onData = (chunk) => {
|
|
16726
|
-
buffer += chunk.toString("utf-8");
|
|
16727
|
-
if (buffer.includes(`${tag} OK`)) {
|
|
16728
|
-
socket.removeListener("data", onData);
|
|
16729
|
-
socket.removeListener("error", onError);
|
|
16730
|
-
resolve21(buffer);
|
|
16731
|
-
return;
|
|
16738
|
+
/**
|
|
16739
|
+
* Ensure a folder directory exists.
|
|
16740
|
+
*/
|
|
16741
|
+
ensureFolder(folder) {
|
|
16742
|
+
const dir = join12(this.mailboxDir, folder);
|
|
16743
|
+
mkdirSync8(dir, { recursive: true });
|
|
16744
|
+
return dir;
|
|
16732
16745
|
}
|
|
16733
|
-
|
|
16734
|
-
|
|
16735
|
-
|
|
16736
|
-
|
|
16746
|
+
/**
|
|
16747
|
+
* Capture an email to the dev mailbox instead of sending it.
|
|
16748
|
+
*
|
|
16749
|
+
* The parameter order MATCHES Messenger.send() on purpose. It did not before:
|
|
16750
|
+
* send()'s 5th positional was `text` and capture()'s was `cc`, so the same call
|
|
16751
|
+
* meant different things depending on which door it came through -- that mismatch
|
|
16752
|
+
* IS nodejs#42.
|
|
16753
|
+
*
|
|
16754
|
+
* BREAKING: `text` is now the 5th positional. A caller passing cc positionally
|
|
16755
|
+
* must move it. Aligning the two signatures is the fix; leaving them apart would
|
|
16756
|
+
* preserve the bug.
|
|
16757
|
+
*/
|
|
16758
|
+
capture(to, subject, body, html = false, text, cc = [], bcc = [], replyTo, attachments = [], from) {
|
|
16759
|
+
const id = randomUUID2();
|
|
16760
|
+
const toList = Array.isArray(to) ? to : [to];
|
|
16761
|
+
const ccList = Array.isArray(cc) ? cc : cc ? [cc] : [];
|
|
16762
|
+
const bccList = Array.isArray(bcc) ? bcc : bcc ? [bcc] : [];
|
|
16763
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
16764
|
+
const message = {
|
|
16765
|
+
id,
|
|
16766
|
+
type: "outbox",
|
|
16767
|
+
from: from ?? process.env.TINA4_MAIL_FROM ?? "dev@localhost",
|
|
16768
|
+
to: toList,
|
|
16769
|
+
cc: ccList,
|
|
16770
|
+
bcc: bccList,
|
|
16771
|
+
reply_to: replyTo,
|
|
16772
|
+
subject,
|
|
16773
|
+
body,
|
|
16774
|
+
text,
|
|
16775
|
+
html,
|
|
16776
|
+
attachments,
|
|
16777
|
+
date: now,
|
|
16778
|
+
read: false
|
|
16779
|
+
};
|
|
16780
|
+
const outboxDir = this.ensureFolder("outbox");
|
|
16781
|
+
writeFileSync6(join12(outboxDir, `${id}.json`), JSON.stringify(message, null, 2));
|
|
16782
|
+
const inboxDir = this.ensureFolder("inbox");
|
|
16783
|
+
const inboxMessage = { ...message, type: "inbox" };
|
|
16784
|
+
writeFileSync6(join12(inboxDir, `${id}.json`), JSON.stringify(inboxMessage, null, 2));
|
|
16785
|
+
return { success: true, message: "Email captured to dev mailbox", id };
|
|
16737
16786
|
}
|
|
16738
|
-
|
|
16739
|
-
|
|
16740
|
-
|
|
16741
|
-
|
|
16742
|
-
|
|
16743
|
-
|
|
16744
|
-
|
|
16745
|
-
|
|
16746
|
-
|
|
16747
|
-
}
|
|
16748
|
-
|
|
16749
|
-
const e = err instanceof Error ? err : new Error(String(err));
|
|
16750
|
-
Log.error(`Messenger IMAP ${method}() failed: ${e.name}: ${e.message}`);
|
|
16751
|
-
if (e instanceof MessengerConnectionError) return e;
|
|
16752
|
-
return new MessengerConnectionError(`IMAP ${method} failed: ${e.message}`);
|
|
16753
|
-
}
|
|
16754
|
-
function parseSearchResponse(response) {
|
|
16755
|
-
const match = response.match(/\* SEARCH (.+)/);
|
|
16756
|
-
if (!match) return [];
|
|
16757
|
-
return match[1].trim().split(/\s+/).filter((s) => /^\d+$/.test(s));
|
|
16758
|
-
}
|
|
16759
|
-
function parseHeaderResponse(uid, response) {
|
|
16760
|
-
const headers = {};
|
|
16761
|
-
const headerBlock = response.match(/\r\n([\s\S]*?)\r\n\)/);
|
|
16762
|
-
if (headerBlock) {
|
|
16763
|
-
const lines = headerBlock[1].split(/\r\n/);
|
|
16764
|
-
let currentKey = "";
|
|
16765
|
-
for (const line of lines) {
|
|
16766
|
-
if (/^\s/.test(line) && currentKey) {
|
|
16767
|
-
headers[currentKey] += " " + line.trim();
|
|
16768
|
-
} else {
|
|
16769
|
-
const colonIdx = line.indexOf(":");
|
|
16770
|
-
if (colonIdx > 0) {
|
|
16771
|
-
currentKey = line.substring(0, colonIdx).trim().toLowerCase();
|
|
16772
|
-
headers[currentKey] = line.substring(colonIdx + 1).trim();
|
|
16787
|
+
/**
|
|
16788
|
+
* List messages from a folder (default: inbox).
|
|
16789
|
+
*/
|
|
16790
|
+
inbox(limit = 50, offset = 0, folder = "inbox") {
|
|
16791
|
+
const dir = this.ensureFolder(folder);
|
|
16792
|
+
const results = [];
|
|
16793
|
+
let files;
|
|
16794
|
+
try {
|
|
16795
|
+
files = readdirSync6(dir).filter((f) => f.endsWith(".json")).sort().reverse();
|
|
16796
|
+
} catch {
|
|
16797
|
+
return [];
|
|
16773
16798
|
}
|
|
16774
|
-
|
|
16775
|
-
|
|
16776
|
-
|
|
16777
|
-
|
|
16778
|
-
|
|
16779
|
-
|
|
16780
|
-
|
|
16781
|
-
from: headers["from"] ?? "",
|
|
16782
|
-
to: headers["to"] ?? "",
|
|
16783
|
-
date: headers["date"] ?? "",
|
|
16784
|
-
snippet: "",
|
|
16785
|
-
seen
|
|
16786
|
-
};
|
|
16787
|
-
}
|
|
16788
|
-
function emptyFullMessage(uid) {
|
|
16789
|
-
return { uid, subject: "", from: "", to: "", cc: "", date: "", bodyText: "", bodyHtml: "", headers: {} };
|
|
16790
|
-
}
|
|
16791
|
-
function parseFullMessage(uid, response) {
|
|
16792
|
-
const bodyMatch = response.match(/\{(\d+)\}\r\n([\s\S]*)/);
|
|
16793
|
-
const rawMessage = bodyMatch ? bodyMatch[2] : response;
|
|
16794
|
-
const headerEnd = rawMessage.indexOf("\r\n\r\n");
|
|
16795
|
-
const headerSection = headerEnd > 0 ? rawMessage.substring(0, headerEnd) : rawMessage;
|
|
16796
|
-
const bodySection = headerEnd > 0 ? rawMessage.substring(headerEnd + 4) : "";
|
|
16797
|
-
const headers = {};
|
|
16798
|
-
const headerLines = headerSection.split(/\r\n/);
|
|
16799
|
-
let currentKey = "";
|
|
16800
|
-
for (const line of headerLines) {
|
|
16801
|
-
if (/^\s/.test(line) && currentKey) {
|
|
16802
|
-
headers[currentKey] += " " + line.trim();
|
|
16803
|
-
} else {
|
|
16804
|
-
const colonIdx = line.indexOf(":");
|
|
16805
|
-
if (colonIdx > 0) {
|
|
16806
|
-
currentKey = line.substring(0, colonIdx).trim().toLowerCase();
|
|
16807
|
-
headers[currentKey] = line.substring(colonIdx + 1).trim();
|
|
16808
|
-
}
|
|
16809
|
-
}
|
|
16810
|
-
}
|
|
16811
|
-
const contentType = headers["content-type"] ?? "text/plain";
|
|
16812
|
-
let bodyText = "";
|
|
16813
|
-
let bodyHtml = "";
|
|
16814
|
-
if (contentType.includes("multipart")) {
|
|
16815
|
-
const boundaryMatch = contentType.match(/boundary="?([^";\s]+)"?/);
|
|
16816
|
-
if (boundaryMatch) {
|
|
16817
|
-
const boundary = boundaryMatch[1];
|
|
16818
|
-
const parts = bodySection.split("--" + boundary);
|
|
16819
|
-
for (const part of parts) {
|
|
16820
|
-
if (part.trim() === "" || part.trim() === "--") continue;
|
|
16821
|
-
const partHeaderEnd = part.indexOf("\r\n\r\n");
|
|
16822
|
-
const partHeaders = partHeaderEnd > 0 ? part.substring(0, partHeaderEnd).toLowerCase() : "";
|
|
16823
|
-
const partBody = partHeaderEnd > 0 ? part.substring(partHeaderEnd + 4).trim() : "";
|
|
16824
|
-
if (partHeaders.includes("text/html")) {
|
|
16825
|
-
bodyHtml = partBody;
|
|
16826
|
-
} else if (partHeaders.includes("text/plain")) {
|
|
16827
|
-
bodyText = partBody;
|
|
16799
|
+
const sliced = files.slice(offset, offset + limit);
|
|
16800
|
+
for (const file of sliced) {
|
|
16801
|
+
try {
|
|
16802
|
+
const msg = JSON.parse(readFileSync8(join12(dir, file), "utf-8"));
|
|
16803
|
+
results.push(msg);
|
|
16804
|
+
} catch {
|
|
16805
|
+
}
|
|
16828
16806
|
}
|
|
16807
|
+
return results;
|
|
16829
16808
|
}
|
|
16830
|
-
|
|
16831
|
-
|
|
16832
|
-
|
|
16833
|
-
|
|
16834
|
-
|
|
16835
|
-
|
|
16836
|
-
|
|
16837
|
-
|
|
16838
|
-
|
|
16839
|
-
|
|
16840
|
-
|
|
16841
|
-
|
|
16842
|
-
|
|
16843
|
-
|
|
16844
|
-
|
|
16845
|
-
|
|
16846
|
-
|
|
16847
|
-
headers
|
|
16848
|
-
};
|
|
16849
|
-
}
|
|
16850
|
-
var MessengerConnectionError, Messenger, imapTagCounter;
|
|
16851
|
-
var init_messenger = __esm({
|
|
16852
|
-
"src/messenger.ts"() {
|
|
16853
|
-
"use strict";
|
|
16854
|
-
init_dotenv();
|
|
16855
|
-
init_logger();
|
|
16856
|
-
MessengerConnectionError = class extends Error {
|
|
16857
|
-
constructor(message) {
|
|
16858
|
-
super(message);
|
|
16859
|
-
this.name = "MessengerConnectionError";
|
|
16860
|
-
}
|
|
16861
|
-
};
|
|
16862
|
-
Messenger = class {
|
|
16863
|
-
host;
|
|
16864
|
-
port;
|
|
16865
|
-
username;
|
|
16866
|
-
password;
|
|
16867
|
-
fromAddress;
|
|
16868
|
-
fromName;
|
|
16869
|
-
encryption;
|
|
16870
|
-
useTls;
|
|
16871
|
-
imapHost;
|
|
16872
|
-
imapPort;
|
|
16873
|
-
imapUser;
|
|
16874
|
-
imapPass;
|
|
16875
|
-
imapEncryption;
|
|
16876
|
-
constructor(options) {
|
|
16877
|
-
this.host = options?.host ?? process.env.TINA4_MAIL_HOST ?? "localhost";
|
|
16878
|
-
this.port = options?.port ?? parseInt(process.env.TINA4_MAIL_PORT ?? "587", 10);
|
|
16879
|
-
this.username = options?.username ?? process.env.TINA4_MAIL_USERNAME ?? "";
|
|
16880
|
-
this.password = options?.password ?? process.env.TINA4_MAIL_PASSWORD ?? "";
|
|
16881
|
-
this.fromAddress = options?.fromAddress ?? process.env.TINA4_MAIL_FROM ?? (this.username || "noreply@localhost");
|
|
16882
|
-
this.fromName = options?.fromName ?? process.env.TINA4_MAIL_FROM_NAME ?? "";
|
|
16883
|
-
const envEncryption = options?.encryption ?? process.env.TINA4_MAIL_ENCRYPTION;
|
|
16884
|
-
if (envEncryption) {
|
|
16885
|
-
this.encryption = envEncryption.toLowerCase();
|
|
16886
|
-
} else if (options?.useTls !== void 0) {
|
|
16887
|
-
this.encryption = options.useTls ? "tls" : "none";
|
|
16888
|
-
} else {
|
|
16889
|
-
this.encryption = "tls";
|
|
16809
|
+
/**
|
|
16810
|
+
* Read a single message by ID. Searches all folders.
|
|
16811
|
+
*/
|
|
16812
|
+
read(msgId) {
|
|
16813
|
+
const folders = ["inbox", "outbox"];
|
|
16814
|
+
for (const folder of folders) {
|
|
16815
|
+
const filePath = join12(this.mailboxDir, folder, `${msgId}.json`);
|
|
16816
|
+
if (existsSync10(filePath)) {
|
|
16817
|
+
try {
|
|
16818
|
+
const msg = JSON.parse(readFileSync8(filePath, "utf-8"));
|
|
16819
|
+
msg.read = true;
|
|
16820
|
+
writeFileSync6(filePath, JSON.stringify(msg, null, 2));
|
|
16821
|
+
return msg;
|
|
16822
|
+
} catch {
|
|
16823
|
+
return null;
|
|
16824
|
+
}
|
|
16825
|
+
}
|
|
16890
16826
|
}
|
|
16891
|
-
|
|
16892
|
-
this.imapHost = options?.imapHost ?? process.env.TINA4_MAIL_IMAP_HOST ?? "";
|
|
16893
|
-
this.imapPort = options?.imapPort ?? parseInt(process.env.TINA4_MAIL_IMAP_PORT ?? "993", 10);
|
|
16894
|
-
this.imapUser = options?.imapUser ?? process.env.TINA4_MAIL_IMAP_USERNAME ?? this.username;
|
|
16895
|
-
this.imapPass = options?.imapPass ?? process.env.TINA4_MAIL_IMAP_PASSWORD ?? this.password;
|
|
16896
|
-
this.imapEncryption = (options?.imapEncryption ?? process.env.TINA4_MAIL_IMAP_ENCRYPTION ?? "tls").toLowerCase();
|
|
16827
|
+
return null;
|
|
16897
16828
|
}
|
|
16898
16829
|
/**
|
|
16899
|
-
*
|
|
16900
|
-
* Returns one of "tls", "starttls", "none", "ssl".
|
|
16830
|
+
* Count unread messages in the inbox.
|
|
16901
16831
|
*/
|
|
16902
|
-
|
|
16903
|
-
|
|
16904
|
-
|
|
16905
|
-
|
|
16906
|
-
|
|
16907
|
-
|
|
16908
|
-
|
|
16909
|
-
|
|
16910
|
-
|
|
16911
|
-
|
|
16912
|
-
|
|
16913
|
-
|
|
16914
|
-
|
|
16915
|
-
|
|
16916
|
-
|
|
16917
|
-
}
|
|
16918
|
-
if (!this.fromAddress) {
|
|
16919
|
-
return { success: false, message: "No from address configured" };
|
|
16920
|
-
}
|
|
16921
|
-
try {
|
|
16922
|
-
let socket;
|
|
16923
|
-
if (this.port === 465) {
|
|
16924
|
-
socket = tls.connect({ host: this.host, port: this.port, rejectUnauthorized: tlsRejectUnauthorized() });
|
|
16925
|
-
await new Promise((resolve21, reject) => {
|
|
16926
|
-
socket.once("secureConnect", resolve21);
|
|
16927
|
-
socket.once("error", reject);
|
|
16928
|
-
});
|
|
16929
|
-
} else {
|
|
16930
|
-
socket = net2.createConnection({ host: this.host, port: this.port });
|
|
16931
|
-
await new Promise((resolve21, reject) => {
|
|
16932
|
-
socket.once("connect", resolve21);
|
|
16933
|
-
socket.once("error", reject);
|
|
16934
|
-
});
|
|
16935
|
-
}
|
|
16936
|
-
const greeting = await readResponse(socket);
|
|
16937
|
-
if (greeting.code !== 220) {
|
|
16938
|
-
socket.destroy();
|
|
16939
|
-
return { success: false, message: `SMTP greeting failed: ${greeting.text}` };
|
|
16940
|
-
}
|
|
16941
|
-
const ehlo = await sendCommand(socket, `EHLO ${this.host}`);
|
|
16942
|
-
if (ehlo.code !== 250) {
|
|
16943
|
-
socket.destroy();
|
|
16944
|
-
return { success: false, message: `EHLO failed: ${ehlo.text}` };
|
|
16945
|
-
}
|
|
16946
|
-
if (this.useTls && this.port !== 465 && ehlo.text.includes("STARTTLS")) {
|
|
16947
|
-
const starttls = await sendCommand(socket, "STARTTLS");
|
|
16948
|
-
if (starttls.code !== 220) {
|
|
16949
|
-
socket.destroy();
|
|
16950
|
-
return { success: false, message: `STARTTLS failed: ${starttls.text}` };
|
|
16951
|
-
}
|
|
16952
|
-
const plainSocket = socket;
|
|
16953
|
-
socket = tls.connect(
|
|
16954
|
-
{ socket: plainSocket, host: this.host, rejectUnauthorized: tlsRejectUnauthorized() }
|
|
16955
|
-
);
|
|
16956
|
-
await new Promise((resolve21, reject) => {
|
|
16957
|
-
socket.once("secureConnect", resolve21);
|
|
16958
|
-
socket.once("error", reject);
|
|
16959
|
-
});
|
|
16960
|
-
const ehlo2 = await sendCommand(socket, `EHLO ${this.host}`);
|
|
16961
|
-
if (ehlo2.code !== 250) {
|
|
16962
|
-
socket.destroy();
|
|
16963
|
-
return { success: false, message: `EHLO after STARTTLS failed: ${ehlo2.text}` };
|
|
16964
|
-
}
|
|
16965
|
-
}
|
|
16966
|
-
if (this.username && this.password) {
|
|
16967
|
-
const auth = await sendCommand(socket, "AUTH LOGIN");
|
|
16968
|
-
if (auth.code !== 334) {
|
|
16969
|
-
socket.destroy();
|
|
16970
|
-
return { success: false, message: `AUTH LOGIN failed: ${auth.text}` };
|
|
16971
|
-
}
|
|
16972
|
-
const userResp = await sendCommand(socket, Buffer.from(this.username).toString("base64"));
|
|
16973
|
-
if (userResp.code !== 334) {
|
|
16974
|
-
socket.destroy();
|
|
16975
|
-
return { success: false, message: `AUTH username failed: ${userResp.text}` };
|
|
16976
|
-
}
|
|
16977
|
-
const passResp = await sendCommand(socket, Buffer.from(this.password).toString("base64"));
|
|
16978
|
-
if (passResp.code !== 235) {
|
|
16979
|
-
socket.destroy();
|
|
16980
|
-
return { success: false, message: `AUTH password failed: ${passResp.text}` };
|
|
16981
|
-
}
|
|
16982
|
-
}
|
|
16983
|
-
const mailFrom = await sendCommand(socket, `MAIL FROM:<${this.fromAddress}>`);
|
|
16984
|
-
if (mailFrom.code !== 250) {
|
|
16985
|
-
socket.destroy();
|
|
16986
|
-
return { success: false, message: `MAIL FROM failed: ${mailFrom.text}` };
|
|
16987
|
-
}
|
|
16988
|
-
for (const recipient of allRecipients) {
|
|
16989
|
-
const rcpt = await sendCommand(socket, `RCPT TO:<${recipient}>`);
|
|
16990
|
-
if (rcpt.code !== 250 && rcpt.code !== 251) {
|
|
16991
|
-
socket.destroy();
|
|
16992
|
-
return { success: false, message: `RCPT TO <${recipient}> failed: ${rcpt.text}` };
|
|
16993
|
-
}
|
|
16994
|
-
}
|
|
16995
|
-
const dataCmd = await sendCommand(socket, "DATA");
|
|
16996
|
-
if (dataCmd.code !== 354) {
|
|
16997
|
-
socket.destroy();
|
|
16998
|
-
return { success: false, message: `DATA failed: ${dataCmd.text}` };
|
|
16999
|
-
}
|
|
17000
|
-
const mimeMessage = buildMimeMessage({
|
|
17001
|
-
from: this.fromAddress,
|
|
17002
|
-
fromName: this.fromName,
|
|
17003
|
-
to: toList,
|
|
17004
|
-
cc: ccList,
|
|
17005
|
-
subject: options.subject,
|
|
17006
|
-
body: options.body,
|
|
17007
|
-
html: options.html ?? false,
|
|
17008
|
-
text: options.text,
|
|
17009
|
-
replyTo: options.replyTo,
|
|
17010
|
-
attachments: options.attachments,
|
|
17011
|
-
headers: options.headers,
|
|
17012
|
-
messageId
|
|
17013
|
-
});
|
|
17014
|
-
const endData = await sendCommand(socket, mimeMessage + "\r\n.");
|
|
17015
|
-
if (endData.code !== 250) {
|
|
17016
|
-
socket.destroy();
|
|
17017
|
-
return { success: false, message: `Message delivery failed: ${endData.text}` };
|
|
17018
|
-
}
|
|
17019
|
-
await sendCommand(socket, "QUIT");
|
|
17020
|
-
socket.destroy();
|
|
17021
|
-
return { success: true, message: "Email sent successfully", id: messageId };
|
|
17022
|
-
} catch (err) {
|
|
17023
|
-
const errMsg = err instanceof Error ? err.message : String(err);
|
|
17024
|
-
return { success: false, message: `SMTP error: ${errMsg}` };
|
|
17025
|
-
}
|
|
17026
|
-
}
|
|
17027
|
-
/**
|
|
17028
|
-
* Test the SMTP connection without sending an email.
|
|
17029
|
-
*/
|
|
17030
|
-
async testConnection() {
|
|
17031
|
-
try {
|
|
17032
|
-
let socket;
|
|
17033
|
-
if (this.port === 465) {
|
|
17034
|
-
socket = tls.connect({ host: this.host, port: this.port, rejectUnauthorized: tlsRejectUnauthorized() });
|
|
17035
|
-
await new Promise((resolve21, reject) => {
|
|
17036
|
-
socket.once("secureConnect", resolve21);
|
|
17037
|
-
socket.once("error", reject);
|
|
17038
|
-
});
|
|
17039
|
-
} else {
|
|
17040
|
-
socket = net2.createConnection({ host: this.host, port: this.port });
|
|
17041
|
-
await new Promise((resolve21, reject) => {
|
|
17042
|
-
socket.once("connect", resolve21);
|
|
17043
|
-
socket.once("error", reject);
|
|
17044
|
-
});
|
|
17045
|
-
}
|
|
17046
|
-
const greeting = await readResponse(socket);
|
|
17047
|
-
if (greeting.code !== 220) {
|
|
17048
|
-
socket.destroy();
|
|
17049
|
-
return { success: false, message: `SMTP greeting failed: ${greeting.text}` };
|
|
17050
|
-
}
|
|
17051
|
-
const ehlo = await sendCommand(socket, `EHLO ${this.host}`);
|
|
17052
|
-
if (ehlo.code !== 250) {
|
|
17053
|
-
socket.destroy();
|
|
17054
|
-
return { success: false, message: `EHLO failed: ${ehlo.text}` };
|
|
17055
|
-
}
|
|
17056
|
-
await sendCommand(socket, "QUIT");
|
|
17057
|
-
socket.destroy();
|
|
17058
|
-
return { success: true, message: `Connected to ${this.host}:${this.port}` };
|
|
17059
|
-
} catch (err) {
|
|
17060
|
-
const errMsg = err instanceof Error ? err.message : String(err);
|
|
17061
|
-
return { success: false, message: `Connection failed: ${errMsg}` };
|
|
17062
|
-
}
|
|
17063
|
-
}
|
|
17064
|
-
// ── IMAP (Read) ────────────────────────────────────────────
|
|
17065
|
-
/**
|
|
17066
|
-
* Connect to the IMAP server via raw TCP/TLS.
|
|
17067
|
-
* Returns the socket and reads the greeting.
|
|
17068
|
-
*/
|
|
17069
|
-
async imapConnect() {
|
|
17070
|
-
if (!this.imapHost) {
|
|
17071
|
-
throw new Error("IMAP host not configured (set imapHost or IMAP_HOST env)");
|
|
17072
|
-
}
|
|
17073
|
-
let socket;
|
|
17074
|
-
const useTls = this.imapEncryption === "tls" || this.imapEncryption === "ssl" || this.imapEncryption === "" && this.imapPort === 993;
|
|
17075
|
-
if (useTls) {
|
|
17076
|
-
socket = tls.connect({ host: this.imapHost, port: this.imapPort, rejectUnauthorized: tlsRejectUnauthorized() });
|
|
17077
|
-
await new Promise((resolve21, reject) => {
|
|
17078
|
-
socket.once("secureConnect", resolve21);
|
|
17079
|
-
socket.once("error", reject);
|
|
17080
|
-
});
|
|
17081
|
-
} else {
|
|
17082
|
-
socket = net2.createConnection({ host: this.imapHost, port: this.imapPort });
|
|
17083
|
-
await new Promise((resolve21, reject) => {
|
|
17084
|
-
socket.once("connect", resolve21);
|
|
17085
|
-
socket.once("error", reject);
|
|
17086
|
-
});
|
|
17087
|
-
}
|
|
17088
|
-
await imapReadLine(socket);
|
|
17089
|
-
if (this.imapUser && this.imapPass) {
|
|
17090
|
-
const loginResp = await imapCommand(socket, `LOGIN ${imapQuote(this.imapUser)} ${imapQuote(this.imapPass)}`);
|
|
17091
|
-
if (!loginResp.includes("OK")) {
|
|
17092
|
-
socket.destroy();
|
|
17093
|
-
throw new Error(`IMAP login failed: ${loginResp}`);
|
|
17094
|
-
}
|
|
17095
|
-
}
|
|
17096
|
-
return socket;
|
|
17097
|
-
}
|
|
17098
|
-
/**
|
|
17099
|
-
* Disconnect from IMAP cleanly.
|
|
17100
|
-
*/
|
|
17101
|
-
async imapDisconnect(socket) {
|
|
17102
|
-
try {
|
|
17103
|
-
await imapCommand(socket, "LOGOUT");
|
|
17104
|
-
} catch {
|
|
17105
|
-
}
|
|
17106
|
-
socket.destroy();
|
|
17107
|
-
}
|
|
17108
|
-
/**
|
|
17109
|
-
* Fetch latest messages from a folder.
|
|
17110
|
-
* Returns list of message summaries.
|
|
17111
|
-
*/
|
|
17112
|
-
async inbox(limit = 20, offset = 0, folder = "INBOX") {
|
|
17113
|
-
let socket;
|
|
17114
|
-
try {
|
|
17115
|
-
socket = await this.imapConnect();
|
|
17116
|
-
} catch (err) {
|
|
17117
|
-
throw imapFail("inbox", err);
|
|
17118
|
-
}
|
|
17119
|
-
try {
|
|
17120
|
-
await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
|
|
17121
|
-
const searchResp = await imapCommand(socket, "SEARCH ALL");
|
|
17122
|
-
const uids = parseSearchResponse(searchResp);
|
|
17123
|
-
if (uids.length === 0) return [];
|
|
17124
|
-
uids.reverse();
|
|
17125
|
-
const selected = uids.slice(offset, offset + limit);
|
|
17126
|
-
if (selected.length === 0) return [];
|
|
17127
|
-
const messages = [];
|
|
17128
|
-
for (const uid of selected) {
|
|
17129
|
-
const fetchResp = await imapCommand(socket, `FETCH ${uid} (FLAGS BODY.PEEK[HEADER.FIELDS (FROM TO SUBJECT DATE)])`);
|
|
17130
|
-
messages.push(parseHeaderResponse(uid, fetchResp));
|
|
17131
|
-
}
|
|
17132
|
-
return messages;
|
|
17133
|
-
} catch (err) {
|
|
17134
|
-
throw imapFail("inbox", err);
|
|
17135
|
-
} finally {
|
|
17136
|
-
await this.imapDisconnect(socket);
|
|
17137
|
-
}
|
|
17138
|
-
}
|
|
17139
|
-
/**
|
|
17140
|
-
* Read a single message by sequence number or UID.
|
|
17141
|
-
*/
|
|
17142
|
-
async read(uid, folder = "INBOX") {
|
|
17143
|
-
let socket;
|
|
17144
|
-
try {
|
|
17145
|
-
socket = await this.imapConnect();
|
|
17146
|
-
} catch (err) {
|
|
17147
|
-
throw imapFail("read", err);
|
|
17148
|
-
}
|
|
17149
|
-
try {
|
|
17150
|
-
await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
|
|
17151
|
-
const fetchResp = await imapCommand(socket, `FETCH ${uid} (FLAGS BODY[])`);
|
|
17152
|
-
if (!/\{\d+\}/.test(fetchResp)) {
|
|
17153
|
-
return emptyFullMessage(uid);
|
|
17154
|
-
}
|
|
17155
|
-
await imapCommand(socket, `STORE ${uid} +FLAGS (\\Seen)`);
|
|
17156
|
-
return parseFullMessage(uid, fetchResp);
|
|
17157
|
-
} catch (err) {
|
|
17158
|
-
throw imapFail("read", err);
|
|
17159
|
-
} finally {
|
|
17160
|
-
await this.imapDisconnect(socket);
|
|
17161
|
-
}
|
|
17162
|
-
}
|
|
17163
|
-
/**
|
|
17164
|
-
* Search messages using IMAP search criteria.
|
|
17165
|
-
*/
|
|
17166
|
-
async search(folder = "INBOX", subject, sender, since, before, unseenOnly = false, limit = 50) {
|
|
17167
|
-
const criteria = ["ALL"];
|
|
17168
|
-
if (subject) criteria.push(`SUBJECT "${subject}"`);
|
|
17169
|
-
if (sender) criteria.push(`FROM "${sender}"`);
|
|
17170
|
-
if (since) criteria.push(`SINCE ${since}`);
|
|
17171
|
-
if (before) criteria.push(`BEFORE ${before}`);
|
|
17172
|
-
if (unseenOnly) criteria.push("UNSEEN");
|
|
17173
|
-
const query = criteria.join(" ");
|
|
17174
|
-
let socket;
|
|
17175
|
-
try {
|
|
17176
|
-
socket = await this.imapConnect();
|
|
17177
|
-
} catch (err) {
|
|
17178
|
-
throw imapFail("search", err);
|
|
17179
|
-
}
|
|
17180
|
-
try {
|
|
17181
|
-
await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
|
|
17182
|
-
const searchResp = await imapCommand(socket, `SEARCH ${query}`);
|
|
17183
|
-
const uids = parseSearchResponse(searchResp);
|
|
17184
|
-
if (uids.length === 0) return [];
|
|
17185
|
-
uids.reverse();
|
|
17186
|
-
const messages = [];
|
|
17187
|
-
for (const uid of uids.slice(0, limit)) {
|
|
17188
|
-
const fetchResp = await imapCommand(socket, `FETCH ${uid} (FLAGS BODY.PEEK[HEADER.FIELDS (FROM TO SUBJECT DATE)])`);
|
|
17189
|
-
messages.push(parseHeaderResponse(uid, fetchResp));
|
|
17190
|
-
}
|
|
17191
|
-
return messages;
|
|
17192
|
-
} catch (err) {
|
|
17193
|
-
throw imapFail("search", err);
|
|
17194
|
-
} finally {
|
|
17195
|
-
await this.imapDisconnect(socket);
|
|
17196
|
-
}
|
|
17197
|
-
}
|
|
17198
|
-
/**
|
|
17199
|
-
* Delete a message by UID.
|
|
17200
|
-
*/
|
|
17201
|
-
async deleteMessage(uid, folder = "INBOX") {
|
|
17202
|
-
const socket = await this.imapConnect();
|
|
17203
|
-
try {
|
|
17204
|
-
await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
|
|
17205
|
-
await imapCommand(socket, `STORE ${uid} +FLAGS (\\Deleted)`);
|
|
17206
|
-
await imapCommand(socket, "EXPUNGE");
|
|
17207
|
-
} finally {
|
|
17208
|
-
await this.imapDisconnect(socket);
|
|
17209
|
-
}
|
|
17210
|
-
}
|
|
17211
|
-
/**
|
|
17212
|
-
* Mark a message as read.
|
|
17213
|
-
*/
|
|
17214
|
-
async markRead(uid, folder = "INBOX") {
|
|
17215
|
-
const socket = await this.imapConnect();
|
|
17216
|
-
try {
|
|
17217
|
-
await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
|
|
17218
|
-
await imapCommand(socket, `STORE ${uid} +FLAGS (\\Seen)`);
|
|
17219
|
-
} finally {
|
|
17220
|
-
await this.imapDisconnect(socket);
|
|
17221
|
-
}
|
|
17222
|
-
}
|
|
17223
|
-
/**
|
|
17224
|
-
* Count unseen messages in a folder.
|
|
17225
|
-
*/
|
|
17226
|
-
async unread(folder = "INBOX") {
|
|
17227
|
-
let socket;
|
|
17228
|
-
try {
|
|
17229
|
-
socket = await this.imapConnect();
|
|
17230
|
-
} catch (err) {
|
|
17231
|
-
throw imapFail("unread", err);
|
|
17232
|
-
}
|
|
17233
|
-
try {
|
|
17234
|
-
await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
|
|
17235
|
-
const searchResp = await imapCommand(socket, "SEARCH UNSEEN");
|
|
17236
|
-
return parseSearchResponse(searchResp).length;
|
|
17237
|
-
} catch (err) {
|
|
17238
|
-
throw imapFail("unread", err);
|
|
17239
|
-
} finally {
|
|
17240
|
-
await this.imapDisconnect(socket);
|
|
17241
|
-
}
|
|
17242
|
-
}
|
|
17243
|
-
/**
|
|
17244
|
-
* List available IMAP folders/mailboxes.
|
|
17245
|
-
*/
|
|
17246
|
-
async folders() {
|
|
17247
|
-
let socket;
|
|
17248
|
-
try {
|
|
17249
|
-
socket = await this.imapConnect();
|
|
17250
|
-
} catch (err) {
|
|
17251
|
-
throw imapFail("folders", err);
|
|
17252
|
-
}
|
|
17253
|
-
try {
|
|
17254
|
-
const resp = await imapCommand(socket, 'LIST "" "*"');
|
|
17255
|
-
const result = [];
|
|
17256
|
-
for (const line of resp.split("\r\n")) {
|
|
17257
|
-
const m = line.match(/\* LIST \([^)]*\) "[^"]*" "?([^"\r\n]+)"?/i);
|
|
17258
|
-
if (m) result.push(m[1]);
|
|
17259
|
-
}
|
|
17260
|
-
return result;
|
|
17261
|
-
} catch (err) {
|
|
17262
|
-
throw imapFail("folders", err);
|
|
17263
|
-
} finally {
|
|
17264
|
-
await this.imapDisconnect(socket);
|
|
17265
|
-
}
|
|
17266
|
-
}
|
|
17267
|
-
/**
|
|
17268
|
-
* Test IMAP connectivity without reading.
|
|
17269
|
-
*/
|
|
17270
|
-
async testImapConnection() {
|
|
17271
|
-
try {
|
|
17272
|
-
const socket = await this.imapConnect();
|
|
17273
|
-
await this.imapDisconnect(socket);
|
|
17274
|
-
return { success: true, message: `Connected to ${this.imapHost}:${this.imapPort}` };
|
|
17275
|
-
} catch (err) {
|
|
17276
|
-
const errMsg = err instanceof Error ? err.message : String(err);
|
|
17277
|
-
return { success: false, message: `IMAP connection failed: ${errMsg}` };
|
|
17278
|
-
}
|
|
17279
|
-
}
|
|
17280
|
-
};
|
|
17281
|
-
imapTagCounter = 0;
|
|
17282
|
-
}
|
|
17283
|
-
});
|
|
17284
|
-
|
|
17285
|
-
// src/devMailbox.ts
|
|
17286
|
-
import { mkdirSync as mkdirSync8, readdirSync as readdirSync6, readFileSync as readFileSync9, writeFileSync as writeFileSync6, unlinkSync as unlinkSync4, existsSync as existsSync10 } from "node:fs";
|
|
17287
|
-
import { join as join12 } from "node:path";
|
|
17288
|
-
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
17289
|
-
function createMessenger() {
|
|
17290
|
-
const debug = process.env.TINA4_DEBUG;
|
|
17291
|
-
const smtpHost = process.env.TINA4_MAIL_HOST;
|
|
17292
|
-
const isProd = !isTruthy(debug) && process.env.NODE_ENV === "production";
|
|
17293
|
-
if (isTruthy(debug)) {
|
|
17294
|
-
return new DevMailbox();
|
|
17295
|
-
}
|
|
17296
|
-
if (!smtpHost) {
|
|
17297
|
-
return new DevMailbox();
|
|
17298
|
-
}
|
|
17299
|
-
if (!isProd) {
|
|
17300
|
-
return new DevMailbox();
|
|
17301
|
-
}
|
|
17302
|
-
return new Messenger();
|
|
17303
|
-
}
|
|
17304
|
-
var DevMailbox;
|
|
17305
|
-
var init_devMailbox = __esm({
|
|
17306
|
-
"src/devMailbox.ts"() {
|
|
17307
|
-
"use strict";
|
|
17308
|
-
init_messenger();
|
|
17309
|
-
init_dotenv();
|
|
17310
|
-
DevMailbox = class {
|
|
17311
|
-
mailboxDir;
|
|
17312
|
-
constructor(mailboxDir) {
|
|
17313
|
-
this.mailboxDir = mailboxDir ?? process.env.TINA4_MAILBOX_DIR ?? "data/mailbox";
|
|
17314
|
-
}
|
|
17315
|
-
/**
|
|
17316
|
-
* Ensure a folder directory exists.
|
|
17317
|
-
*/
|
|
17318
|
-
ensureFolder(folder) {
|
|
17319
|
-
const dir = join12(this.mailboxDir, folder);
|
|
17320
|
-
mkdirSync8(dir, { recursive: true });
|
|
17321
|
-
return dir;
|
|
17322
|
-
}
|
|
17323
|
-
/**
|
|
17324
|
-
* Capture an email to the dev mailbox instead of sending it.
|
|
17325
|
-
*/
|
|
17326
|
-
capture(to, subject, body, html = false, cc = [], bcc = [], replyTo, attachments = [], from) {
|
|
17327
|
-
const id = randomUUID3();
|
|
17328
|
-
const toList = Array.isArray(to) ? to : [to];
|
|
17329
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
17330
|
-
const message = {
|
|
17331
|
-
id,
|
|
17332
|
-
type: "outbox",
|
|
17333
|
-
from: from ?? process.env.TINA4_MAIL_FROM ?? "dev@localhost",
|
|
17334
|
-
to: toList,
|
|
17335
|
-
cc,
|
|
17336
|
-
bcc,
|
|
17337
|
-
reply_to: replyTo,
|
|
17338
|
-
subject,
|
|
17339
|
-
body,
|
|
17340
|
-
html,
|
|
17341
|
-
attachments,
|
|
17342
|
-
date: now,
|
|
17343
|
-
read: false
|
|
17344
|
-
};
|
|
17345
|
-
const outboxDir = this.ensureFolder("outbox");
|
|
17346
|
-
writeFileSync6(join12(outboxDir, `${id}.json`), JSON.stringify(message, null, 2));
|
|
17347
|
-
const inboxDir = this.ensureFolder("inbox");
|
|
17348
|
-
const inboxMessage = { ...message, type: "inbox" };
|
|
17349
|
-
writeFileSync6(join12(inboxDir, `${id}.json`), JSON.stringify(inboxMessage, null, 2));
|
|
17350
|
-
return { success: true, message: "Email captured to dev mailbox", id };
|
|
17351
|
-
}
|
|
17352
|
-
/**
|
|
17353
|
-
* List messages from a folder (default: inbox).
|
|
17354
|
-
*/
|
|
17355
|
-
inbox(limit = 50, offset = 0, folder = "inbox") {
|
|
17356
|
-
const dir = this.ensureFolder(folder);
|
|
17357
|
-
const results = [];
|
|
17358
|
-
let files;
|
|
17359
|
-
try {
|
|
17360
|
-
files = readdirSync6(dir).filter((f) => f.endsWith(".json")).sort().reverse();
|
|
17361
|
-
} catch {
|
|
17362
|
-
return [];
|
|
17363
|
-
}
|
|
17364
|
-
const sliced = files.slice(offset, offset + limit);
|
|
17365
|
-
for (const file of sliced) {
|
|
17366
|
-
try {
|
|
17367
|
-
const msg = JSON.parse(readFileSync9(join12(dir, file), "utf-8"));
|
|
17368
|
-
results.push(msg);
|
|
17369
|
-
} catch {
|
|
17370
|
-
}
|
|
17371
|
-
}
|
|
17372
|
-
return results;
|
|
17373
|
-
}
|
|
17374
|
-
/**
|
|
17375
|
-
* Read a single message by ID. Searches all folders.
|
|
17376
|
-
*/
|
|
17377
|
-
read(msgId) {
|
|
17378
|
-
const folders = ["inbox", "outbox"];
|
|
17379
|
-
for (const folder of folders) {
|
|
17380
|
-
const filePath = join12(this.mailboxDir, folder, `${msgId}.json`);
|
|
17381
|
-
if (existsSync10(filePath)) {
|
|
17382
|
-
try {
|
|
17383
|
-
const msg = JSON.parse(readFileSync9(filePath, "utf-8"));
|
|
17384
|
-
msg.read = true;
|
|
17385
|
-
writeFileSync6(filePath, JSON.stringify(msg, null, 2));
|
|
17386
|
-
return msg;
|
|
17387
|
-
} catch {
|
|
17388
|
-
return null;
|
|
17389
|
-
}
|
|
17390
|
-
}
|
|
17391
|
-
}
|
|
17392
|
-
return null;
|
|
17393
|
-
}
|
|
17394
|
-
/**
|
|
17395
|
-
* Count unread messages in the inbox.
|
|
17396
|
-
*/
|
|
17397
|
-
unreadCount() {
|
|
17398
|
-
const dir = this.ensureFolder("inbox");
|
|
17399
|
-
let count = 0;
|
|
17400
|
-
try {
|
|
17401
|
-
const files = readdirSync6(dir).filter((f) => f.endsWith(".json"));
|
|
17402
|
-
for (const file of files) {
|
|
17403
|
-
try {
|
|
17404
|
-
const msg = JSON.parse(readFileSync9(join12(dir, file), "utf-8"));
|
|
17405
|
-
if (!msg.read) count++;
|
|
17406
|
-
} catch {
|
|
17407
|
-
}
|
|
17408
|
-
}
|
|
17409
|
-
} catch {
|
|
17410
|
-
}
|
|
17411
|
-
return count;
|
|
16832
|
+
unreadCount() {
|
|
16833
|
+
const dir = this.ensureFolder("inbox");
|
|
16834
|
+
let count = 0;
|
|
16835
|
+
try {
|
|
16836
|
+
const files = readdirSync6(dir).filter((f) => f.endsWith(".json"));
|
|
16837
|
+
for (const file of files) {
|
|
16838
|
+
try {
|
|
16839
|
+
const msg = JSON.parse(readFileSync8(join12(dir, file), "utf-8"));
|
|
16840
|
+
if (!msg.read) count++;
|
|
16841
|
+
} catch {
|
|
16842
|
+
}
|
|
16843
|
+
}
|
|
16844
|
+
} catch {
|
|
16845
|
+
}
|
|
16846
|
+
return count;
|
|
17412
16847
|
}
|
|
17413
16848
|
/**
|
|
17414
16849
|
* Delete a message by ID. Removes from all folders.
|
|
@@ -17473,7 +16908,7 @@ var init_devMailbox = __esm({
|
|
|
17473
16908
|
const subject = subjects[i % subjects.length];
|
|
17474
16909
|
const from = senders[i % senders.length];
|
|
17475
16910
|
const date = new Date(Date.now() - i * 36e5).toISOString();
|
|
17476
|
-
const id =
|
|
16911
|
+
const id = randomUUID2();
|
|
17477
16912
|
const message = {
|
|
17478
16913
|
id,
|
|
17479
16914
|
type: "inbox",
|
|
@@ -18512,7 +17947,7 @@ var init_metrics = __esm({
|
|
|
18512
17947
|
});
|
|
18513
17948
|
|
|
18514
17949
|
// src/feedback.ts
|
|
18515
|
-
import { readFileSync as
|
|
17950
|
+
import { readFileSync as readFileSync10, existsSync as existsSync12 } from "node:fs";
|
|
18516
17951
|
import { dirname as dirname5, join as join14, resolve as resolve8 } from "node:path";
|
|
18517
17952
|
import { fileURLToPath } from "node:url";
|
|
18518
17953
|
function feedbackEnabled() {
|
|
@@ -18653,7 +18088,7 @@ var init_feedback = __esm({
|
|
|
18653
18088
|
handleFeedbackWidgetJs = (_req, res) => {
|
|
18654
18089
|
let body;
|
|
18655
18090
|
if (existsSync12(WIDGET_BUNDLE_PATH)) {
|
|
18656
|
-
body =
|
|
18091
|
+
body = readFileSync10(WIDGET_BUNDLE_PATH);
|
|
18657
18092
|
} else {
|
|
18658
18093
|
body = "console.warn('tina4-feedback-widget bundle not built yet');";
|
|
18659
18094
|
}
|
|
@@ -20361,7 +19796,7 @@ __export(errorOverlay_exports, {
|
|
|
20361
19796
|
renderErrorOverlay: () => renderErrorOverlay,
|
|
20362
19797
|
renderProductionError: () => renderProductionError
|
|
20363
19798
|
});
|
|
20364
|
-
import { readFileSync as
|
|
19799
|
+
import { readFileSync as readFileSync12, statSync as statSync10 } from "node:fs";
|
|
20365
19800
|
import { resolve as resolve10 } from "node:path";
|
|
20366
19801
|
function esc(text) {
|
|
20367
19802
|
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
@@ -20385,7 +19820,7 @@ function parseStack(stack) {
|
|
|
20385
19820
|
function readSourceLines(filename, lineno) {
|
|
20386
19821
|
try {
|
|
20387
19822
|
const absPath = resolve10(filename);
|
|
20388
|
-
const content =
|
|
19823
|
+
const content = readFileSync12(absPath, "utf-8");
|
|
20389
19824
|
const allLines = content.split("\n");
|
|
20390
19825
|
const start2 = Math.max(0, lineno - CONTEXT_LINES - 1);
|
|
20391
19826
|
const end = Math.min(allLines.length, lineno + CONTEXT_LINES);
|
|
@@ -21649,8 +21084,8 @@ __export(context_exports, {
|
|
|
21649
21084
|
fts5Supported: () => fts5Supported
|
|
21650
21085
|
});
|
|
21651
21086
|
import { DatabaseSync as DatabaseSync3 } from "node:sqlite";
|
|
21652
|
-
import { existsSync as existsSync14, mkdirSync as mkdirSync10, readFileSync as
|
|
21653
|
-
import { basename as
|
|
21087
|
+
import { existsSync as existsSync14, mkdirSync as mkdirSync10, readFileSync as readFileSync13, readdirSync as readdirSync9, realpathSync as realpathSync2 } from "node:fs";
|
|
21088
|
+
import { basename as basename3, dirname as dirname7, extname as extname6, isAbsolute as isAbsolute5, join as join16, relative as relative4, resolve as resolve11 } from "node:path";
|
|
21654
21089
|
function fts5Supported() {
|
|
21655
21090
|
try {
|
|
21656
21091
|
const conn = new DatabaseSync3(":memory:");
|
|
@@ -21673,7 +21108,7 @@ function realResolve(abs) {
|
|
|
21673
21108
|
} catch {
|
|
21674
21109
|
}
|
|
21675
21110
|
try {
|
|
21676
|
-
return join16(realpathSync2(dirname7(abs)),
|
|
21111
|
+
return join16(realpathSync2(dirname7(abs)), basename3(abs));
|
|
21677
21112
|
} catch {
|
|
21678
21113
|
return abs;
|
|
21679
21114
|
}
|
|
@@ -21786,7 +21221,7 @@ var init_context = __esm({
|
|
|
21786
21221
|
// ── indexing ───────────────────────────────────────────────
|
|
21787
21222
|
static chunksFor(label, text) {
|
|
21788
21223
|
const ext = extname6(label).toLowerCase();
|
|
21789
|
-
const special = SPECIAL_FILES.has(
|
|
21224
|
+
const special = SPECIAL_FILES.has(basename3(label).toLowerCase());
|
|
21790
21225
|
if (CODE_EXTS.has(ext) || CONFIG_EXTS.has(ext) || special) {
|
|
21791
21226
|
return chunkCode(text, label);
|
|
21792
21227
|
}
|
|
@@ -21803,7 +21238,7 @@ var init_context = __esm({
|
|
|
21803
21238
|
const stored = label != null ? String(label) : String(file);
|
|
21804
21239
|
let text;
|
|
21805
21240
|
try {
|
|
21806
|
-
text =
|
|
21241
|
+
text = readFileSync13(file, "utf-8");
|
|
21807
21242
|
} catch {
|
|
21808
21243
|
return 0;
|
|
21809
21244
|
}
|
|
@@ -21883,7 +21318,7 @@ var init_context = __esm({
|
|
|
21883
21318
|
if (parts.some((seg) => SKIP_DIRS.has(seg)) || dirParts.some((seg) => seg.startsWith("."))) {
|
|
21884
21319
|
return -1;
|
|
21885
21320
|
}
|
|
21886
|
-
if (!_Context.eligible(
|
|
21321
|
+
if (!_Context.eligible(basename3(rel))) return -1;
|
|
21887
21322
|
const stored = rel;
|
|
21888
21323
|
if (!existsSync14(abs)) {
|
|
21889
21324
|
this.conn.prepare("DELETE FROM chunks WHERE path = ?").run(stored);
|
|
@@ -21975,7 +21410,7 @@ var init_context = __esm({
|
|
|
21975
21410
|
});
|
|
21976
21411
|
|
|
21977
21412
|
// src/websocketBackplane.ts
|
|
21978
|
-
import { randomUUID as
|
|
21413
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
21979
21414
|
function createBackplane(url) {
|
|
21980
21415
|
const backend = (process.env.TINA4_WS_BACKPLANE ?? "").trim().toLowerCase();
|
|
21981
21416
|
switch (backend) {
|
|
@@ -22005,7 +21440,7 @@ function buildEnvelope(src, kind, message, opts = {}) {
|
|
|
22005
21440
|
return envelope;
|
|
22006
21441
|
}
|
|
22007
21442
|
function randomInstanceId() {
|
|
22008
|
-
return
|
|
21443
|
+
return randomUUID3().replace(/-/g, "").slice(0, 16);
|
|
22009
21444
|
}
|
|
22010
21445
|
var RedisBackplane, NATSBackplane, WS_BACKPLANE_CHANNEL, WsBackplaneManager;
|
|
22011
21446
|
var init_websocketBackplane = __esm({
|
|
@@ -22233,7 +21668,7 @@ __export(websocket_exports, {
|
|
|
22233
21668
|
});
|
|
22234
21669
|
import { createServer } from "node:http";
|
|
22235
21670
|
import { createHash as createHash4 } from "node:crypto";
|
|
22236
|
-
import { randomUUID as
|
|
21671
|
+
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
22237
21672
|
function computeAcceptKey(key) {
|
|
22238
21673
|
return createHash4("sha1").update(key + MAGIC_STRING).digest("base64");
|
|
22239
21674
|
}
|
|
@@ -22361,7 +21796,7 @@ function parseFrame(data) {
|
|
|
22361
21796
|
return { fin: !!fin, opcode, payload: Buffer.from(payload), bytesConsumed: offset + payloadLen };
|
|
22362
21797
|
}
|
|
22363
21798
|
function createRouteConnection(socket, path8, headers, params, auth) {
|
|
22364
|
-
const id =
|
|
21799
|
+
const id = randomUUID4().slice(0, 8);
|
|
22365
21800
|
const send = (message) => {
|
|
22366
21801
|
try {
|
|
22367
21802
|
socket.write(buildFrame(OP_TEXT, Buffer.from(message, "utf-8")));
|
|
@@ -22918,7 +22353,7 @@ var init_websocket = __esm({
|
|
|
22918
22353
|
}
|
|
22919
22354
|
responseLines.push("", "");
|
|
22920
22355
|
socket.write(responseLines.join("\r\n"));
|
|
22921
|
-
const clientId =
|
|
22356
|
+
const clientId = randomUUID4().slice(0, 8);
|
|
22922
22357
|
const client = {
|
|
22923
22358
|
id: clientId,
|
|
22924
22359
|
socket,
|
|
@@ -23206,7 +22641,7 @@ var init_websocket = __esm({
|
|
|
23206
22641
|
|
|
23207
22642
|
// src/queueBackends/rabbitmqBackend.ts
|
|
23208
22643
|
import { execFileSync } from "node:child_process";
|
|
23209
|
-
import { randomUUID as
|
|
22644
|
+
import { randomUUID as randomUUID5 } from "node:crypto";
|
|
23210
22645
|
function parseAmqpUrl(url) {
|
|
23211
22646
|
const config = {};
|
|
23212
22647
|
let rest = url.replace(/^amqps:\/\//, "").replace(/^amqp:\/\//, "");
|
|
@@ -23650,7 +23085,7 @@ var init_rabbitmqBackend = __esm({
|
|
|
23650
23085
|
}
|
|
23651
23086
|
}
|
|
23652
23087
|
push(queue, payload, _delay) {
|
|
23653
|
-
const id =
|
|
23088
|
+
const id = randomUUID5();
|
|
23654
23089
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
23655
23090
|
const job = {
|
|
23656
23091
|
id,
|
|
@@ -23689,7 +23124,7 @@ var init_rabbitmqBackend = __esm({
|
|
|
23689
23124
|
|
|
23690
23125
|
// src/queueBackends/kafkaBackend.ts
|
|
23691
23126
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
23692
|
-
import { randomUUID as
|
|
23127
|
+
import { randomUUID as randomUUID6 } from "node:crypto";
|
|
23693
23128
|
function kafkaSecurityConfig(env = process.env) {
|
|
23694
23129
|
const mapping = [
|
|
23695
23130
|
["security.protocol", "SECURITY_PROTOCOL"],
|
|
@@ -23713,7 +23148,7 @@ var init_kafkaBackend = __esm({
|
|
|
23713
23148
|
"use strict";
|
|
23714
23149
|
API_PRODUCE = 0;
|
|
23715
23150
|
API_FETCH = 1;
|
|
23716
|
-
KafkaBackend = class {
|
|
23151
|
+
KafkaBackend = class _KafkaBackend {
|
|
23717
23152
|
brokers;
|
|
23718
23153
|
groupId;
|
|
23719
23154
|
constructor(config) {
|
|
@@ -24075,12 +23510,15 @@ var init_kafkaBackend = __esm({
|
|
|
24075
23510
|
if (errCode === 0) {
|
|
24076
23511
|
finish("__PUBLISHED__", 0);
|
|
24077
23512
|
} else {
|
|
23513
|
+
// Report the CODE, not just "it failed" \u2014 the caller decides
|
|
23514
|
+
// whether it is retriable (3/5, the async topic-creation race)
|
|
23515
|
+
// or fatal (e.g. 29 TOPIC_AUTHORIZATION_FAILED).
|
|
24078
23516
|
process.stderr.write("Produce error code " + errCode);
|
|
24079
|
-
finish("
|
|
23517
|
+
finish("__PRODUCEERROR__" + errCode, 0);
|
|
24080
23518
|
}
|
|
24081
23519
|
} catch (e) {
|
|
24082
23520
|
process.stderr.write("produce parse: " + e.message);
|
|
24083
|
-
finish("
|
|
23521
|
+
finish("__PARSEERROR__produce: " + e.message, 0);
|
|
24084
23522
|
}
|
|
24085
23523
|
return;
|
|
24086
23524
|
} else if (operation === "get") {
|
|
@@ -24094,6 +23532,7 @@ var init_kafkaBackend = __esm({
|
|
|
24094
23532
|
pos += 4; // throttleTimeMs (v1+)
|
|
24095
23533
|
const topicCount = buffer.readInt32BE(pos); pos += 4;
|
|
24096
23534
|
let out = "__EMPTY__";
|
|
23535
|
+
let fatalCode = 0;
|
|
24097
23536
|
for (let t = 0; t < topicCount; t++) {
|
|
24098
23537
|
const tl = buffer.readInt16BE(pos); pos += 2 + tl;
|
|
24099
23538
|
const pc = buffer.readInt32BE(pos); pos += 4;
|
|
@@ -24105,6 +23544,14 @@ var init_kafkaBackend = __esm({
|
|
|
24105
23544
|
const abortedCount = buffer.readInt32BE(pos); pos += 4;
|
|
24106
23545
|
if (abortedCount > 0) pos += abortedCount * 16; // (-1 => none, skip)
|
|
24107
23546
|
const recSetSize = buffer.readInt32BE(pos); pos += 4;
|
|
23547
|
+
// 3 = UNKNOWN_TOPIC_OR_PARTITION, 5 = LEADER_NOT_AVAILABLE:
|
|
23548
|
+
// "nothing to read here yet", which a consumer that starts
|
|
23549
|
+
// before its producer hits on every cold start. Any OTHER code
|
|
23550
|
+
// (29 TOPIC_AUTHORIZATION_FAILED, 13 STALE_CONTROLLER_EPOCH, \u2026)
|
|
23551
|
+
// is a real failure and must NOT be reported as an empty queue.
|
|
23552
|
+
if (errCode !== 0 && errCode !== 3 && errCode !== 5) {
|
|
23553
|
+
fatalCode = errCode;
|
|
23554
|
+
}
|
|
24108
23555
|
if (errCode === 0 && recSetSize > 0) {
|
|
24109
23556
|
const val = firstRecordValue(buffer, pos, pos + recSetSize);
|
|
24110
23557
|
if (val !== null) out = val;
|
|
@@ -24112,21 +23559,33 @@ var init_kafkaBackend = __esm({
|
|
|
24112
23559
|
pos += recSetSize > 0 ? recSetSize : 0;
|
|
24113
23560
|
}
|
|
24114
23561
|
}
|
|
23562
|
+
if (fatalCode !== 0) {
|
|
23563
|
+
process.stderr.write("Fetch error code " + fatalCode);
|
|
23564
|
+
finish("__FETCHERROR__" + fatalCode, 0);
|
|
23565
|
+
return;
|
|
23566
|
+
}
|
|
24115
23567
|
finish(out, 0);
|
|
24116
23568
|
} catch (e) {
|
|
23569
|
+
// A parse failure is NOT an empty queue either \u2014 say so.
|
|
24117
23570
|
process.stderr.write("fetch parse: " + e.message);
|
|
24118
|
-
finish("
|
|
23571
|
+
finish("__PARSEERROR__fetch: " + e.message, 0);
|
|
24119
23572
|
}
|
|
24120
23573
|
return;
|
|
24121
23574
|
}
|
|
24122
23575
|
});
|
|
24123
23576
|
|
|
23577
|
+
// Report the reason on STDOUT and exit 0. Writing it to stderr and
|
|
23578
|
+
// exiting non-zero LOST it: stderr to a pipe is an async write and
|
|
23579
|
+
// process.exit() truncates it, so the parent saw an empty stderr and fell
|
|
23580
|
+
// back to execFileSync's message -- which embeds this entire script.
|
|
23581
|
+
// stdout is flushed by finish()'s write callback, so it survives.
|
|
24124
23582
|
sock.on("error", (err) => {
|
|
24125
|
-
|
|
24126
|
-
finish("", 1);
|
|
23583
|
+
finish("__TRANSPORTERROR__" + err.message, 0);
|
|
24127
23584
|
});
|
|
24128
23585
|
|
|
24129
|
-
var timer = setTimeout(() => {
|
|
23586
|
+
var timer = setTimeout(() => {
|
|
23587
|
+
finish("__TRANSPORTERROR__timed out after 10s talking to " + host + ":" + port, 0);
|
|
23588
|
+
}, 10000);
|
|
24130
23589
|
`;
|
|
24131
23590
|
try {
|
|
24132
23591
|
const result = execFileSync2(process.execPath, ["-e", script], {
|
|
@@ -24135,12 +23594,51 @@ var init_kafkaBackend = __esm({
|
|
|
24135
23594
|
stdio: ["pipe", "pipe", "pipe"]
|
|
24136
23595
|
});
|
|
24137
23596
|
return result;
|
|
24138
|
-
} catch {
|
|
24139
|
-
|
|
23597
|
+
} catch (err) {
|
|
23598
|
+
const e = err;
|
|
23599
|
+
const reason = String(e.stderr ?? "").trim() || e.message || "unknown error";
|
|
23600
|
+
const firstLine2 = reason.split("\n", 1)[0].slice(0, 200);
|
|
23601
|
+
return "__TRANSPORTERROR__" + firstLine2;
|
|
23602
|
+
}
|
|
23603
|
+
}
|
|
23604
|
+
/**
|
|
23605
|
+
* Sleep synchronously between produce retries.
|
|
23606
|
+
*
|
|
23607
|
+
* `push()` is synchronous (the whole backend drives its socket through a child
|
|
23608
|
+
* process), so there is no event loop to await on. `Atomics.wait` on a
|
|
23609
|
+
* SharedArrayBuffer is the stdlib way to block a thread for a fixed time --
|
|
23610
|
+
* no dependency, no busy-wait burning CPU.
|
|
23611
|
+
*/
|
|
23612
|
+
static sleepSync(ms) {
|
|
23613
|
+
const shared = new Int32Array(new SharedArrayBuffer(4));
|
|
23614
|
+
Atomics.wait(shared, 0, 0, ms);
|
|
23615
|
+
}
|
|
23616
|
+
/**
|
|
23617
|
+
* Turn a sentinel from the protocol child into a thrown error, or return.
|
|
23618
|
+
*
|
|
23619
|
+
* The wording matches the Python and PHP backends exactly -- the parity rule
|
|
23620
|
+
* covers user-visible error messages, not just behaviour.
|
|
23621
|
+
*/
|
|
23622
|
+
static assertNoError(result, operation, topic) {
|
|
23623
|
+
const fatal = /^__(PRODUCEERROR|FETCHERROR)__(\d+)/.exec(result);
|
|
23624
|
+
if (fatal) {
|
|
23625
|
+
throw new Error(
|
|
23626
|
+
`Kafka rejected the ${operation} for topic ${topic}: error code ${fatal[2]}`
|
|
23627
|
+
);
|
|
23628
|
+
}
|
|
23629
|
+
if (result.startsWith("__TRANSPORTERROR__")) {
|
|
23630
|
+
throw new Error(
|
|
23631
|
+
`Kafka ${operation} for topic ${topic} failed: ` + result.slice("__TRANSPORTERROR__".length)
|
|
23632
|
+
);
|
|
23633
|
+
}
|
|
23634
|
+
if (result.startsWith("__PARSEERROR__")) {
|
|
23635
|
+
throw new Error(
|
|
23636
|
+
`Kafka ${operation} for topic ${topic} returned an unreadable response: ` + result.slice("__PARSEERROR__".length)
|
|
23637
|
+
);
|
|
24140
23638
|
}
|
|
24141
23639
|
}
|
|
24142
23640
|
push(queue, payload, _delay) {
|
|
24143
|
-
const id =
|
|
23641
|
+
const id = randomUUID6();
|
|
24144
23642
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
24145
23643
|
const job = {
|
|
24146
23644
|
id,
|
|
@@ -24150,14 +23648,25 @@ var init_kafkaBackend = __esm({
|
|
|
24150
23648
|
attempts: 0,
|
|
24151
23649
|
delayUntil: null
|
|
24152
23650
|
};
|
|
24153
|
-
const
|
|
24154
|
-
|
|
24155
|
-
|
|
23651
|
+
const body = JSON.stringify(job);
|
|
23652
|
+
let result = "";
|
|
23653
|
+
for (let attempt = 1; attempt <= 10; attempt++) {
|
|
23654
|
+
result = this.execSync("publish", queue, body);
|
|
23655
|
+
if (result.includes("__PUBLISHED__")) {
|
|
23656
|
+
return id;
|
|
23657
|
+
}
|
|
23658
|
+
const retriable = /^__PRODUCEERROR__(3|5)\b/.test(result);
|
|
23659
|
+
if (!retriable || attempt === 10) {
|
|
23660
|
+
break;
|
|
23661
|
+
}
|
|
23662
|
+
_KafkaBackend.sleepSync(200);
|
|
24156
23663
|
}
|
|
24157
|
-
|
|
23664
|
+
_KafkaBackend.assertNoError(result, "produce", queue);
|
|
23665
|
+
throw new Error(`Kafka publish failed for topic ${queue}: ${result || "no response"}`);
|
|
24158
23666
|
}
|
|
24159
23667
|
pop(queue) {
|
|
24160
23668
|
const result = this.execSync("get", queue);
|
|
23669
|
+
_KafkaBackend.assertNoError(result, "fetch", queue);
|
|
24161
23670
|
if (!result || result === "__EMPTY__" || result === "__UNSUPPORTED__") return null;
|
|
24162
23671
|
try {
|
|
24163
23672
|
return JSON.parse(result);
|
|
@@ -24175,7 +23684,7 @@ var init_kafkaBackend = __esm({
|
|
|
24175
23684
|
});
|
|
24176
23685
|
|
|
24177
23686
|
// src/queueBackends/mongoBackend.ts
|
|
24178
|
-
import { randomUUID as
|
|
23687
|
+
import { randomUUID as randomUUID7 } from "node:crypto";
|
|
24179
23688
|
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
24180
23689
|
var MongoBackend2;
|
|
24181
23690
|
var init_mongoBackend = __esm({
|
|
@@ -24488,7 +23997,7 @@ var init_mongoBackend = __esm({
|
|
|
24488
23997
|
}
|
|
24489
23998
|
}
|
|
24490
23999
|
push(queue, payload, delay) {
|
|
24491
|
-
const id =
|
|
24000
|
+
const id = randomUUID7();
|
|
24492
24001
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
24493
24002
|
const job = {
|
|
24494
24003
|
id,
|
|
@@ -24623,9 +24132,9 @@ var init_job = __esm({
|
|
|
24623
24132
|
});
|
|
24624
24133
|
|
|
24625
24134
|
// src/queueBackends/liteBackend.ts
|
|
24626
|
-
import { mkdirSync as mkdirSync11, readdirSync as readdirSync10, readFileSync as
|
|
24135
|
+
import { mkdirSync as mkdirSync11, readdirSync as readdirSync10, readFileSync as readFileSync14, writeFileSync as writeFileSync8, unlinkSync as unlinkSync5, existsSync as existsSync15 } from "node:fs";
|
|
24627
24136
|
import { join as join17 } from "node:path";
|
|
24628
|
-
import { randomUUID as
|
|
24137
|
+
import { randomUUID as randomUUID8 } from "node:crypto";
|
|
24629
24138
|
var LiteBackend;
|
|
24630
24139
|
var init_liteBackend = __esm({
|
|
24631
24140
|
"src/queueBackends/liteBackend.ts"() {
|
|
@@ -24677,7 +24186,7 @@ var init_liteBackend = __esm({
|
|
|
24677
24186
|
}
|
|
24678
24187
|
push(queue, payload, delay, priority) {
|
|
24679
24188
|
const dir = this.ensureDir(queue);
|
|
24680
|
-
const id =
|
|
24189
|
+
const id = randomUUID8();
|
|
24681
24190
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
24682
24191
|
const job = {
|
|
24683
24192
|
id,
|
|
@@ -24713,7 +24222,7 @@ var init_liteBackend = __esm({
|
|
|
24713
24222
|
const filePath = join17(dir, filename);
|
|
24714
24223
|
let job;
|
|
24715
24224
|
try {
|
|
24716
|
-
job = JSON.parse(
|
|
24225
|
+
job = JSON.parse(readFileSync14(filePath, "utf-8"));
|
|
24717
24226
|
} catch {
|
|
24718
24227
|
continue;
|
|
24719
24228
|
}
|
|
@@ -24777,7 +24286,7 @@ var init_liteBackend = __esm({
|
|
|
24777
24286
|
const filePath = join17(reservedDir, filename);
|
|
24778
24287
|
let record;
|
|
24779
24288
|
try {
|
|
24780
|
-
record = JSON.parse(
|
|
24289
|
+
record = JSON.parse(readFileSync14(filePath, "utf-8"));
|
|
24781
24290
|
} catch {
|
|
24782
24291
|
continue;
|
|
24783
24292
|
}
|
|
@@ -24890,7 +24399,7 @@ var init_liteBackend = __esm({
|
|
|
24890
24399
|
let count = 0;
|
|
24891
24400
|
for (const file of files) {
|
|
24892
24401
|
try {
|
|
24893
|
-
const job = JSON.parse(
|
|
24402
|
+
const job = JSON.parse(readFileSync14(join17(scanDir, file), "utf-8"));
|
|
24894
24403
|
if (job.status === status2) count++;
|
|
24895
24404
|
} catch {
|
|
24896
24405
|
}
|
|
@@ -24947,7 +24456,7 @@ var init_liteBackend = __esm({
|
|
|
24947
24456
|
const files = readdirSync10(dir).filter((f) => f.endsWith(".queue-data")).sort();
|
|
24948
24457
|
for (const file of files) {
|
|
24949
24458
|
try {
|
|
24950
|
-
const job = JSON.parse(
|
|
24459
|
+
const job = JSON.parse(readFileSync14(join17(dir, file), "utf-8"));
|
|
24951
24460
|
const attempts = job.attempts || 0;
|
|
24952
24461
|
if (attempts > 0 && attempts < maxRetries) {
|
|
24953
24462
|
results.push(job);
|
|
@@ -24973,7 +24482,7 @@ var init_liteBackend = __esm({
|
|
|
24973
24482
|
const failedDir = join17(this.basePath, q, "failed");
|
|
24974
24483
|
const filePath = join17(failedDir, `${jobId}.queue-data`);
|
|
24975
24484
|
if (existsSync15(filePath)) {
|
|
24976
|
-
const job = JSON.parse(
|
|
24485
|
+
const job = JSON.parse(readFileSync14(filePath, "utf-8"));
|
|
24977
24486
|
job.status = "pending";
|
|
24978
24487
|
job.attempts = (job.attempts || 0) + 1;
|
|
24979
24488
|
job.error = void 0;
|
|
@@ -24997,7 +24506,7 @@ var init_liteBackend = __esm({
|
|
|
24997
24506
|
const files = readdirSync10(failedDir).filter((f) => f.endsWith(".queue-data")).sort();
|
|
24998
24507
|
for (const file of files) {
|
|
24999
24508
|
try {
|
|
25000
|
-
const job = JSON.parse(
|
|
24509
|
+
const job = JSON.parse(readFileSync14(join17(failedDir, file), "utf-8"));
|
|
25001
24510
|
if ((job.attempts || 0) >= maxRetries) {
|
|
25002
24511
|
job.status = "dead";
|
|
25003
24512
|
results.push(job);
|
|
@@ -25031,7 +24540,7 @@ var init_liteBackend = __esm({
|
|
|
25031
24540
|
const files = readdirSync10(dir).filter((f) => f.endsWith(".queue-data"));
|
|
25032
24541
|
for (const file of files) {
|
|
25033
24542
|
try {
|
|
25034
|
-
const job = JSON.parse(
|
|
24543
|
+
const job = JSON.parse(readFileSync14(join17(dir, file), "utf-8"));
|
|
25035
24544
|
if (job.status === status2) {
|
|
25036
24545
|
unlinkSync5(join17(dir, file));
|
|
25037
24546
|
count++;
|
|
@@ -25058,7 +24567,7 @@ var init_liteBackend = __esm({
|
|
|
25058
24567
|
for (const file of files) {
|
|
25059
24568
|
try {
|
|
25060
24569
|
const filePath = join17(failedDir, file);
|
|
25061
|
-
const job = JSON.parse(
|
|
24570
|
+
const job = JSON.parse(readFileSync14(filePath, "utf-8"));
|
|
25062
24571
|
if ((job.attempts || 0) >= maxRetries) {
|
|
25063
24572
|
continue;
|
|
25064
24573
|
}
|
|
@@ -25090,7 +24599,7 @@ var init_liteBackend = __esm({
|
|
|
25090
24599
|
const filePath = join17(dir, file);
|
|
25091
24600
|
let job;
|
|
25092
24601
|
try {
|
|
25093
|
-
job = JSON.parse(
|
|
24602
|
+
job = JSON.parse(readFileSync14(filePath, "utf-8"));
|
|
25094
24603
|
} catch {
|
|
25095
24604
|
continue;
|
|
25096
24605
|
}
|
|
@@ -27580,7 +27089,7 @@ ${end}
|
|
|
27580
27089
|
|
|
27581
27090
|
// src/devAdmin.ts
|
|
27582
27091
|
import { cpus as osCpus } from "node:os";
|
|
27583
|
-
import { readFileSync as
|
|
27092
|
+
import { readFileSync as readFileSync18, writeFileSync as writeFileSync12, existsSync as existsSync19, readdirSync as readdirSync14, mkdirSync as mkdirSync14, copyFileSync, statSync as statSync15 } from "node:fs";
|
|
27584
27093
|
import { join as join21, dirname as dirname9, resolve as resolve15, relative as relative8 } from "node:path";
|
|
27585
27094
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
27586
27095
|
import { timingSafeEqual as timingSafeEqual2 } from "node:crypto";
|
|
@@ -27766,7 +27275,7 @@ function resolveDevEnvVar(key) {
|
|
|
27766
27275
|
if (live !== void 0 && live !== "") return live;
|
|
27767
27276
|
const envPath = join21(process.cwd(), ".env");
|
|
27768
27277
|
if (!existsSync19(envPath)) return "";
|
|
27769
|
-
for (const line of
|
|
27278
|
+
for (const line of readFileSync18(envPath, "utf-8").split("\n")) {
|
|
27770
27279
|
const t = line.trim();
|
|
27771
27280
|
if (!t || t.startsWith("#") || !t.includes("=")) continue;
|
|
27772
27281
|
const eq = t.indexOf("=");
|
|
@@ -27776,7 +27285,7 @@ function resolveDevEnvVar(key) {
|
|
|
27776
27285
|
}
|
|
27777
27286
|
function upsertDevEnvVar(key, value) {
|
|
27778
27287
|
const envPath = join21(process.cwd(), ".env");
|
|
27779
|
-
const lines = existsSync19(envPath) ?
|
|
27288
|
+
const lines = existsSync19(envPath) ? readFileSync18(envPath, "utf-8").split("\n") : [];
|
|
27780
27289
|
let found = false;
|
|
27781
27290
|
const out = [];
|
|
27782
27291
|
for (const line of lines) {
|
|
@@ -27809,7 +27318,7 @@ function parseEnvFile() {
|
|
|
27809
27318
|
const envPath = join21(process.cwd(), ".env");
|
|
27810
27319
|
const result = {};
|
|
27811
27320
|
if (!existsSync19(envPath)) return result;
|
|
27812
|
-
const lines =
|
|
27321
|
+
const lines = readFileSync18(envPath, "utf-8").split("\n");
|
|
27813
27322
|
for (const line of lines) {
|
|
27814
27323
|
const trimmed = line.trim();
|
|
27815
27324
|
if (!trimmed || trimmed.startsWith("#") || !trimmed.includes("=")) continue;
|
|
@@ -28166,7 +27675,7 @@ var init_devAdmin = __esm({
|
|
|
28166
27675
|
for (const rel of ["../../../package.json", "../../package.json"]) {
|
|
28167
27676
|
const p = resolve15(__dirname2, rel);
|
|
28168
27677
|
if (existsSync19(p)) {
|
|
28169
|
-
const pkg = JSON.parse(
|
|
27678
|
+
const pkg = JSON.parse(readFileSync18(p, "utf-8"));
|
|
28170
27679
|
if (pkg.version) return pkg.version;
|
|
28171
27680
|
}
|
|
28172
27681
|
}
|
|
@@ -28824,7 +28333,7 @@ var init_devAdmin = __esm({
|
|
|
28824
28333
|
for (const filename of readdirSync14(queueDir).sort()) {
|
|
28825
28334
|
if (!filename.endsWith(".queue-data")) continue;
|
|
28826
28335
|
try {
|
|
28827
|
-
const job = JSON.parse(
|
|
28336
|
+
const job = JSON.parse(readFileSync18(join21(queueDir, filename), "utf-8"));
|
|
28828
28337
|
jobs.push(mapQueueJob(job, topic, "pending"));
|
|
28829
28338
|
} catch {
|
|
28830
28339
|
}
|
|
@@ -29310,7 +28819,7 @@ var init_devAdmin = __esm({
|
|
|
29310
28819
|
}
|
|
29311
28820
|
try {
|
|
29312
28821
|
const envPath = join21(process.cwd(), ".env");
|
|
29313
|
-
const lines = existsSync19(envPath) ?
|
|
28822
|
+
const lines = existsSync19(envPath) ? readFileSync18(envPath, "utf-8").split("\n") : [];
|
|
29314
28823
|
const keysFound = { TINA4_DATABASE_URL: false, TINA4_DATABASE_USERNAME: false, TINA4_DATABASE_PASSWORD: false };
|
|
29315
28824
|
const newLines = [];
|
|
29316
28825
|
for (const line of lines) {
|
|
@@ -29356,7 +28865,7 @@ var init_devAdmin = __esm({
|
|
|
29356
28865
|
const metaFile = join21(entryPath, "meta.json");
|
|
29357
28866
|
if (statSync15(entryPath).isDirectory() && existsSync19(metaFile)) {
|
|
29358
28867
|
try {
|
|
29359
|
-
const meta = JSON.parse(
|
|
28868
|
+
const meta = JSON.parse(readFileSync18(metaFile, "utf-8"));
|
|
29360
28869
|
meta.id = entry;
|
|
29361
28870
|
const srcDir = join21(entryPath, "src");
|
|
29362
28871
|
if (existsSync19(srcDir)) {
|
|
@@ -29571,7 +29080,7 @@ var init_devAdmin = __esm({
|
|
|
29571
29080
|
return;
|
|
29572
29081
|
}
|
|
29573
29082
|
try {
|
|
29574
|
-
const content =
|
|
29083
|
+
const content = readFileSync18(target, "utf-8");
|
|
29575
29084
|
const path8 = relative8(root, target);
|
|
29576
29085
|
res.json({ path: path8, content, language: devAdminLanguage(path8), bytes: Buffer.byteLength(content, "utf-8") });
|
|
29577
29086
|
} catch (e) {
|
|
@@ -29613,7 +29122,7 @@ var init_devAdmin = __esm({
|
|
|
29613
29122
|
return;
|
|
29614
29123
|
}
|
|
29615
29124
|
try {
|
|
29616
|
-
const buf =
|
|
29125
|
+
const buf = readFileSync18(target);
|
|
29617
29126
|
const ext = target.slice(target.lastIndexOf(".") + 1).toLowerCase();
|
|
29618
29127
|
const mime = {
|
|
29619
29128
|
js: "application/javascript",
|
|
@@ -30041,7 +29550,7 @@ var init_devAdmin = __esm({
|
|
|
30041
29550
|
});
|
|
30042
29551
|
|
|
30043
29552
|
// src/i18n.ts
|
|
30044
|
-
import { readFileSync as
|
|
29553
|
+
import { readFileSync as readFileSync19, readdirSync as readdirSync15, existsSync as existsSync20 } from "node:fs";
|
|
30045
29554
|
import { join as join22, resolve as resolve16 } from "node:path";
|
|
30046
29555
|
var I18n;
|
|
30047
29556
|
var init_i18n = __esm({
|
|
@@ -30138,7 +29647,7 @@ var init_i18n = __esm({
|
|
|
30138
29647
|
const filePath = join22(this._localeDir, `${locale}.json`);
|
|
30139
29648
|
if (existsSync20(filePath)) {
|
|
30140
29649
|
try {
|
|
30141
|
-
const raw =
|
|
29650
|
+
const raw = readFileSync19(filePath, "utf-8");
|
|
30142
29651
|
const data = JSON.parse(raw);
|
|
30143
29652
|
this._translations.set(locale, _I18n._flatten(data));
|
|
30144
29653
|
return;
|
|
@@ -30151,7 +29660,7 @@ var init_i18n = __esm({
|
|
|
30151
29660
|
const yamlPath = join22(this._localeDir, `${locale}${ext}`);
|
|
30152
29661
|
if (existsSync20(yamlPath)) {
|
|
30153
29662
|
try {
|
|
30154
|
-
const raw =
|
|
29663
|
+
const raw = readFileSync19(yamlPath, "utf-8");
|
|
30155
29664
|
const data = _I18n._parseSimpleYaml(raw);
|
|
30156
29665
|
this._translations.set(locale, _I18n._flatten(data));
|
|
30157
29666
|
return;
|
|
@@ -30819,6 +30328,36 @@ var init_docsAutoDiscovery = __esm({
|
|
|
30819
30328
|
}
|
|
30820
30329
|
});
|
|
30821
30330
|
|
|
30331
|
+
// src/sessionHandlers/childError.ts
|
|
30332
|
+
function childFailureReason(err) {
|
|
30333
|
+
const e = err ?? {};
|
|
30334
|
+
const stderr = String(e.stderr ?? "").trim();
|
|
30335
|
+
if (stderr !== "") {
|
|
30336
|
+
return firstLine(stderr);
|
|
30337
|
+
}
|
|
30338
|
+
if (e.code === "ETIMEDOUT" || e.signal) {
|
|
30339
|
+
return `timed out or was killed (${e.code ?? e.signal})`;
|
|
30340
|
+
}
|
|
30341
|
+
if (typeof e.status === "number" && e.status !== 0) {
|
|
30342
|
+
return `child exited with code ${e.status} and no output`;
|
|
30343
|
+
}
|
|
30344
|
+
return firstLine(String(e.message ?? "unknown error"));
|
|
30345
|
+
}
|
|
30346
|
+
function firstLine(text) {
|
|
30347
|
+
const line = text.split("\n", 1)[0] ?? "";
|
|
30348
|
+
return line.length > MAX_FALLBACK ? `${line.slice(0, MAX_FALLBACK)}...` : line;
|
|
30349
|
+
}
|
|
30350
|
+
function childFailureError(label, err) {
|
|
30351
|
+
return new Error(`${label} command failed: ${childFailureReason(err)}`);
|
|
30352
|
+
}
|
|
30353
|
+
var MAX_FALLBACK;
|
|
30354
|
+
var init_childError = __esm({
|
|
30355
|
+
"src/sessionHandlers/childError.ts"() {
|
|
30356
|
+
"use strict";
|
|
30357
|
+
MAX_FALLBACK = 200;
|
|
30358
|
+
}
|
|
30359
|
+
});
|
|
30360
|
+
|
|
30822
30361
|
// src/sessionHandlers/respClient.ts
|
|
30823
30362
|
import { execFileSync as execFileSync4 } from "node:child_process";
|
|
30824
30363
|
function respCommandSync(target, args, label = "Redis") {
|
|
@@ -30938,7 +30477,7 @@ function respCommandSync(target, args, label = "Redis") {
|
|
|
30938
30477
|
stdio: ["pipe", "pipe", "pipe"]
|
|
30939
30478
|
});
|
|
30940
30479
|
} catch (err) {
|
|
30941
|
-
throw
|
|
30480
|
+
throw childFailureError(label, err);
|
|
30942
30481
|
}
|
|
30943
30482
|
if (result === "__NULL__") return "";
|
|
30944
30483
|
if (result.startsWith("__ERR__")) {
|
|
@@ -30949,6 +30488,7 @@ function respCommandSync(target, args, label = "Redis") {
|
|
|
30949
30488
|
var init_respClient = __esm({
|
|
30950
30489
|
"src/sessionHandlers/respClient.ts"() {
|
|
30951
30490
|
"use strict";
|
|
30491
|
+
init_childError();
|
|
30952
30492
|
}
|
|
30953
30493
|
});
|
|
30954
30494
|
|
|
@@ -30967,6 +30507,7 @@ var moduleRequire, RedisNpmSessionHandler;
|
|
|
30967
30507
|
var init_redisHandler = __esm({
|
|
30968
30508
|
"src/sessionHandlers/redisHandler.ts"() {
|
|
30969
30509
|
"use strict";
|
|
30510
|
+
init_childError();
|
|
30970
30511
|
init_respClient();
|
|
30971
30512
|
moduleRequire = createRequire8(import.meta.url);
|
|
30972
30513
|
RedisNpmSessionHandler = class {
|
|
@@ -31027,9 +30568,17 @@ var init_redisHandler = __esm({
|
|
|
31027
30568
|
(async () => {
|
|
31028
30569
|
try {
|
|
31029
30570
|
const redis = require("redis");
|
|
30571
|
+
// reconnectStrategy: false \u2014 this child runs ONE command and exits, so
|
|
30572
|
+
// retrying inside it is pointless: the handler is called again on the
|
|
30573
|
+
// next request anyway. With the driver's default strategy a refused
|
|
30574
|
+
// connection never rejects, the child hangs until execFileSync's 5s
|
|
30575
|
+
// timeout kills it, and the caller is told "timed out" when the truth
|
|
30576
|
+
// is "connection refused". Off, connect() rejects in ~5ms with the real
|
|
30577
|
+
// reason -- a better message AND no 5s stall per request when Redis is
|
|
30578
|
+
// down.
|
|
31030
30579
|
const clientOpts = useUrl
|
|
31031
|
-
? { url }
|
|
31032
|
-
: { socket: { host, port }, password: password || undefined, database: db };
|
|
30580
|
+
? { url, socket: { reconnectStrategy: false } }
|
|
30581
|
+
: { socket: { host, port, reconnectStrategy: false }, password: password || undefined, database: db };
|
|
31033
30582
|
const client = redis.createClient(clientOpts);
|
|
31034
30583
|
client.on("error", () => {});
|
|
31035
30584
|
await client.connect();
|
|
@@ -31043,8 +30592,10 @@ var init_redisHandler = __esm({
|
|
|
31043
30592
|
const out = (result === null || result === undefined) ? "__NULL__" : String(result);
|
|
31044
30593
|
process.stdout.write(out, () => process.exit(0));
|
|
31045
30594
|
} catch (err) {
|
|
31046
|
-
|
|
31047
|
-
process.exit(
|
|
30595
|
+
// Exit from the write CALLBACK: stderr to a pipe is an async write and
|
|
30596
|
+
// a bare process.exit() truncates it, which left the parent with an
|
|
30597
|
+
// empty stderr and nothing but execFileSync's script-dump message.
|
|
30598
|
+
process.stderr.write(String((err && err.message) || err), () => process.exit(1));
|
|
31048
30599
|
}
|
|
31049
30600
|
})();
|
|
31050
30601
|
`;
|
|
@@ -31056,7 +30607,7 @@ var init_redisHandler = __esm({
|
|
|
31056
30607
|
stdio: ["pipe", "pipe", "pipe"]
|
|
31057
30608
|
});
|
|
31058
30609
|
} catch (err) {
|
|
31059
|
-
throw
|
|
30610
|
+
throw childFailureError("Redis", err);
|
|
31060
30611
|
}
|
|
31061
30612
|
if (result === "__NULL__") return "";
|
|
31062
30613
|
return result;
|
|
@@ -31188,8 +30739,10 @@ function mongoCommandSync(target, command, args, label = "MongoDB") {
|
|
|
31188
30739
|
process.stdout.write(out, () => process.exit(0));
|
|
31189
30740
|
} catch (err) {
|
|
31190
30741
|
try { if (client) await client.close(); } catch (e) {}
|
|
31191
|
-
|
|
31192
|
-
process.exit(
|
|
30742
|
+
// Exit from the write CALLBACK: stderr to a pipe is an async write and
|
|
30743
|
+
// a bare process.exit() truncates it, which left the parent with an
|
|
30744
|
+
// empty stderr and nothing but execFileSync's script-dump message.
|
|
30745
|
+
process.stderr.write(String((err && err.message) || err), () => process.exit(1));
|
|
31193
30746
|
}
|
|
31194
30747
|
})();
|
|
31195
30748
|
} else {
|
|
@@ -31330,12 +30883,13 @@ function mongoCommandSync(target, command, args, label = "MongoDB") {
|
|
|
31330
30883
|
stdio: ["pipe", "pipe", "pipe"]
|
|
31331
30884
|
});
|
|
31332
30885
|
} catch (err) {
|
|
31333
|
-
throw
|
|
30886
|
+
throw childFailureError(label, err);
|
|
31334
30887
|
}
|
|
31335
30888
|
}
|
|
31336
30889
|
var init_mongoClient = __esm({
|
|
31337
30890
|
"src/sessionHandlers/mongoClient.ts"() {
|
|
31338
30891
|
"use strict";
|
|
30892
|
+
init_childError();
|
|
31339
30893
|
}
|
|
31340
30894
|
});
|
|
31341
30895
|
|
|
@@ -31488,7 +31042,7 @@ __export(session_exports, {
|
|
|
31488
31042
|
sessionCookieName: () => sessionCookieName
|
|
31489
31043
|
});
|
|
31490
31044
|
import { randomBytes as randomBytes6 } from "node:crypto";
|
|
31491
|
-
import { existsSync as existsSync22, mkdirSync as mkdirSync17, readFileSync as
|
|
31045
|
+
import { existsSync as existsSync22, mkdirSync as mkdirSync17, readFileSync as readFileSync21, writeFileSync as writeFileSync15, unlinkSync as unlinkSync7, readdirSync as readdirSync16 } from "node:fs";
|
|
31492
31046
|
import { join as join24 } from "node:path";
|
|
31493
31047
|
function isSecureScheme(forwardedProto, socketEncrypted) {
|
|
31494
31048
|
const forwarded = (forwardedProto ?? "").trim();
|
|
@@ -31541,7 +31095,7 @@ var init_session = __esm({
|
|
|
31541
31095
|
const filePath = this.filePath(sessionId);
|
|
31542
31096
|
try {
|
|
31543
31097
|
if (!existsSync22(filePath)) return null;
|
|
31544
|
-
const raw =
|
|
31098
|
+
const raw = readFileSync21(filePath, "utf-8");
|
|
31545
31099
|
const wrapper = JSON.parse(raw);
|
|
31546
31100
|
if (wrapper._expires && wrapper._expires > 0 && Date.now() / 1e3 > wrapper._expires) {
|
|
31547
31101
|
try {
|
|
@@ -31577,7 +31131,7 @@ var init_session = __esm({
|
|
|
31577
31131
|
if (!file.endsWith(".json")) continue;
|
|
31578
31132
|
const fullPath = join24(this.storagePath, file);
|
|
31579
31133
|
try {
|
|
31580
|
-
const raw =
|
|
31134
|
+
const raw = readFileSync21(fullPath, "utf-8");
|
|
31581
31135
|
const wrapper = JSON.parse(raw);
|
|
31582
31136
|
if (wrapper._expires && wrapper._expires > 0 && now > wrapper._expires) {
|
|
31583
31137
|
unlinkSync7(fullPath);
|
|
@@ -32094,7 +31648,7 @@ var init_events = __esm({
|
|
|
32094
31648
|
// src/server.ts
|
|
32095
31649
|
import { createServer as createServer2 } from "node:http";
|
|
32096
31650
|
import { resolve as resolve18, dirname as dirname10, join as join25, relative as relative9 } from "node:path";
|
|
32097
|
-
import { existsSync as existsSync23, readdirSync as readdirSync17, readFileSync as
|
|
31651
|
+
import { existsSync as existsSync23, readdirSync as readdirSync17, readFileSync as readFileSync22, statSync as statSync16 } from "node:fs";
|
|
32098
31652
|
import { isatty } from "node:tty";
|
|
32099
31653
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
32100
31654
|
import { execFileSync as execFileSync7, exec } from "node:child_process";
|
|
@@ -32156,7 +31710,7 @@ async function autoMigrateOnStartup(migrationDir = "migrations", base = process.
|
|
|
32156
31710
|
function readPackageVersion() {
|
|
32157
31711
|
try {
|
|
32158
31712
|
const pkgPath = resolve18(dirname10(fileURLToPath4(import.meta.url)), "..", "..", "..", "package.json");
|
|
32159
|
-
const pkg = JSON.parse(
|
|
31713
|
+
const pkg = JSON.parse(readFileSync22(pkgPath, "utf-8"));
|
|
32160
31714
|
return pkg.version ?? "0.0.0";
|
|
32161
31715
|
} catch {
|
|
32162
31716
|
return "0.0.0";
|
|
@@ -32986,7 +32540,7 @@ ${reset2}
|
|
|
32986
32540
|
res.raw.writeHead(200, void 0, { "Content-Type": "text/html; charset=utf-8" });
|
|
32987
32541
|
res.raw.end(html);
|
|
32988
32542
|
} else {
|
|
32989
|
-
const html =
|
|
32543
|
+
const html = readFileSync22(resolve18(templatesDir, tplFile), "utf-8");
|
|
32990
32544
|
res.raw.writeHead(200, void 0, { "Content-Type": "text/html; charset=utf-8" });
|
|
32991
32545
|
res.raw.end(html);
|
|
32992
32546
|
}
|
|
@@ -33377,7 +32931,7 @@ var init_constants = __esm({
|
|
|
33377
32931
|
});
|
|
33378
32932
|
|
|
33379
32933
|
// src/scss.ts
|
|
33380
|
-
import { readFileSync as
|
|
32934
|
+
import { readFileSync as readFileSync23, writeFileSync as writeFileSync16, existsSync as existsSync24, mkdirSync as mkdirSync18, readdirSync as readdirSync18 } from "node:fs";
|
|
33381
32935
|
import { join as join26, resolve as resolve19, dirname as dirname11 } from "node:path";
|
|
33382
32936
|
function compileString(scss, importPaths, variables) {
|
|
33383
32937
|
const imported = /* @__PURE__ */ new Set();
|
|
@@ -33408,7 +32962,7 @@ function resolveImports(content, paths, imported) {
|
|
|
33408
32962
|
for (const candidate of candidates) {
|
|
33409
32963
|
if (existsSync24(candidate) && !imported.has(candidate)) {
|
|
33410
32964
|
imported.add(candidate);
|
|
33411
|
-
const fileContent =
|
|
32965
|
+
const fileContent = readFileSync23(candidate, "utf-8");
|
|
33412
32966
|
return resolveImports(fileContent, [dirname11(candidate), ...paths], imported);
|
|
33413
32967
|
}
|
|
33414
32968
|
}
|
|
@@ -33749,7 +33303,7 @@ var init_scss = __esm({
|
|
|
33749
33303
|
/** Compile an SCSS file to CSS. */
|
|
33750
33304
|
compileFile(filePath) {
|
|
33751
33305
|
const absPath = resolve19(filePath);
|
|
33752
|
-
const content =
|
|
33306
|
+
const content = readFileSync23(absPath, "utf-8");
|
|
33753
33307
|
const paths = [dirname11(absPath), ...this._importPaths];
|
|
33754
33308
|
return compileString(content, paths, { ...this._variables });
|
|
33755
33309
|
}
|
|
@@ -33772,7 +33326,7 @@ var init_scss = __esm({
|
|
|
33772
33326
|
const imported = /* @__PURE__ */ new Set();
|
|
33773
33327
|
let merged = "";
|
|
33774
33328
|
for (const file of files) {
|
|
33775
|
-
const content =
|
|
33329
|
+
const content = readFileSync23(file, "utf-8");
|
|
33776
33330
|
imported.add(file);
|
|
33777
33331
|
merged += resolveImports(content, paths, imported) + "\n";
|
|
33778
33332
|
}
|
|
@@ -33789,7 +33343,7 @@ var init_scss = __esm({
|
|
|
33789
33343
|
if (!existsSync24(outDir)) mkdirSync18(outDir, { recursive: true });
|
|
33790
33344
|
let existing = null;
|
|
33791
33345
|
try {
|
|
33792
|
-
existing = existsSync24(absOutput) ?
|
|
33346
|
+
existing = existsSync24(absOutput) ? readFileSync23(absOutput, "utf-8") : null;
|
|
33793
33347
|
} catch {
|
|
33794
33348
|
existing = null;
|
|
33795
33349
|
}
|
|
@@ -33873,10 +33427,10 @@ var init_mqttMessage = __esm({
|
|
|
33873
33427
|
});
|
|
33874
33428
|
|
|
33875
33429
|
// src/mqtt.ts
|
|
33876
|
-
import
|
|
33877
|
-
import
|
|
33430
|
+
import net2 from "node:net";
|
|
33431
|
+
import tls from "node:tls";
|
|
33878
33432
|
import { randomBytes as randomBytes7 } from "node:crypto";
|
|
33879
|
-
import { existsSync as existsSync25, readFileSync as
|
|
33433
|
+
import { existsSync as existsSync25, readFileSync as readFileSync24 } from "node:fs";
|
|
33880
33434
|
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;
|
|
33881
33435
|
var init_mqtt = __esm({
|
|
33882
33436
|
"src/mqtt.ts"() {
|
|
@@ -34341,10 +33895,10 @@ var init_mqtt = __esm({
|
|
|
34341
33895
|
servername: this.host,
|
|
34342
33896
|
rejectUnauthorized: this.tlsVerify
|
|
34343
33897
|
};
|
|
34344
|
-
if (this.tlsVerify && this.caFile) opts.ca =
|
|
34345
|
-
sock =
|
|
33898
|
+
if (this.tlsVerify && this.caFile) opts.ca = readFileSync24(this.caFile);
|
|
33899
|
+
sock = tls.connect(opts, () => settle(() => resolve21(sock)));
|
|
34346
33900
|
} else {
|
|
34347
|
-
sock =
|
|
33901
|
+
sock = net2.createConnection({ host: this.host, port: this.port }, () => settle(() => resolve21(sock)));
|
|
34348
33902
|
}
|
|
34349
33903
|
sock.once("error", (err) => {
|
|
34350
33904
|
settle(() => {
|
|
@@ -34895,7 +34449,7 @@ import https from "node:https";
|
|
|
34895
34449
|
import { URL as URL2 } from "node:url";
|
|
34896
34450
|
import { randomBytes as randomBytes8 } from "node:crypto";
|
|
34897
34451
|
import { promises as fsp, createWriteStream } from "node:fs";
|
|
34898
|
-
import { basename as
|
|
34452
|
+
import { basename as basename5 } from "node:path";
|
|
34899
34453
|
import { pipeline } from "node:stream/promises";
|
|
34900
34454
|
function sameOrigin(urlA, urlB) {
|
|
34901
34455
|
try {
|
|
@@ -35185,7 +34739,7 @@ var init_api = __esm({
|
|
|
35185
34739
|
error: err instanceof Error ? err.message : String(err)
|
|
35186
34740
|
};
|
|
35187
34741
|
}
|
|
35188
|
-
uploadName = filename ||
|
|
34742
|
+
uploadName = filename || basename5(filePath);
|
|
35189
34743
|
} else {
|
|
35190
34744
|
return { http_code: null, body: null, headers: {}, error: "upload requires filePath or fileBytes" };
|
|
35191
34745
|
}
|
|
@@ -35541,6 +35095,777 @@ var init_api = __esm({
|
|
|
35541
35095
|
}
|
|
35542
35096
|
});
|
|
35543
35097
|
|
|
35098
|
+
// src/messenger.ts
|
|
35099
|
+
import net3 from "node:net";
|
|
35100
|
+
import tls2 from "node:tls";
|
|
35101
|
+
import { readFileSync as readFileSync25 } from "node:fs";
|
|
35102
|
+
import { basename as basename6 } from "node:path";
|
|
35103
|
+
import { randomUUID as randomUUID9 } from "node:crypto";
|
|
35104
|
+
function tlsRejectUnauthorized() {
|
|
35105
|
+
return !isTruthy(process.env.TINA4_MAIL_TLS_INSECURE);
|
|
35106
|
+
}
|
|
35107
|
+
function readResponse(socket) {
|
|
35108
|
+
return new Promise((resolve21, reject) => {
|
|
35109
|
+
let buffer = "";
|
|
35110
|
+
const onData = (chunk) => {
|
|
35111
|
+
buffer += chunk.toString("utf-8");
|
|
35112
|
+
const lines = buffer.split("\r\n");
|
|
35113
|
+
for (let i = 0; i < lines.length; i++) {
|
|
35114
|
+
const line = lines[i];
|
|
35115
|
+
if (line.length < 3) continue;
|
|
35116
|
+
const code = parseInt(line.substring(0, 3), 10);
|
|
35117
|
+
if (line.length >= 4 && line[3] === " ") {
|
|
35118
|
+
socket.removeListener("data", onData);
|
|
35119
|
+
socket.removeListener("error", onError);
|
|
35120
|
+
resolve21({ code, text: buffer.trim() });
|
|
35121
|
+
return;
|
|
35122
|
+
}
|
|
35123
|
+
}
|
|
35124
|
+
};
|
|
35125
|
+
const onError = (err) => {
|
|
35126
|
+
socket.removeListener("data", onData);
|
|
35127
|
+
reject(err);
|
|
35128
|
+
};
|
|
35129
|
+
socket.on("data", onData);
|
|
35130
|
+
socket.on("error", onError);
|
|
35131
|
+
});
|
|
35132
|
+
}
|
|
35133
|
+
function sendCommand(socket, command) {
|
|
35134
|
+
return new Promise((resolve21, reject) => {
|
|
35135
|
+
socket.write(command + "\r\n", "utf-8", (err) => {
|
|
35136
|
+
if (err) return reject(err);
|
|
35137
|
+
readResponse(socket).then(resolve21, reject);
|
|
35138
|
+
});
|
|
35139
|
+
});
|
|
35140
|
+
}
|
|
35141
|
+
function buildMimeMessage(options) {
|
|
35142
|
+
const boundary = `----=_Tina4_${Date.now()}_${Math.random().toString(36).substring(2)}`;
|
|
35143
|
+
const altBoundary = `----=_Tina4Alt_${Date.now()}_${Math.random().toString(36).substring(2)}`;
|
|
35144
|
+
const hasAttachments = options.attachments && options.attachments.length > 0;
|
|
35145
|
+
const hasTextAlt = options.text !== void 0 && options.html;
|
|
35146
|
+
const lines = [];
|
|
35147
|
+
const fromHeader = options.fromName ? `"${options.fromName}" <${options.from}>` : options.from;
|
|
35148
|
+
lines.push(`From: ${fromHeader}`);
|
|
35149
|
+
lines.push(`To: ${options.to.join(", ")}`);
|
|
35150
|
+
if (options.cc.length > 0) {
|
|
35151
|
+
lines.push(`Cc: ${options.cc.join(", ")}`);
|
|
35152
|
+
}
|
|
35153
|
+
lines.push(`Subject: ${options.subject}`);
|
|
35154
|
+
lines.push(`Date: ${(/* @__PURE__ */ new Date()).toUTCString()}`);
|
|
35155
|
+
lines.push(`Message-ID: <${options.messageId}>`);
|
|
35156
|
+
lines.push("MIME-Version: 1.0");
|
|
35157
|
+
if (options.replyTo) {
|
|
35158
|
+
lines.push(`Reply-To: ${options.replyTo}`);
|
|
35159
|
+
}
|
|
35160
|
+
if (options.headers) {
|
|
35161
|
+
for (const [key, value] of Object.entries(options.headers)) {
|
|
35162
|
+
lines.push(`${key}: ${value}`);
|
|
35163
|
+
}
|
|
35164
|
+
}
|
|
35165
|
+
if (hasAttachments) {
|
|
35166
|
+
lines.push(`Content-Type: multipart/mixed; boundary="${boundary}"`);
|
|
35167
|
+
lines.push("");
|
|
35168
|
+
lines.push(`--${boundary}`);
|
|
35169
|
+
if (hasTextAlt) {
|
|
35170
|
+
lines.push(`Content-Type: multipart/alternative; boundary="${altBoundary}"`);
|
|
35171
|
+
lines.push("");
|
|
35172
|
+
lines.push(`--${altBoundary}`);
|
|
35173
|
+
lines.push("Content-Type: text/plain; charset=UTF-8");
|
|
35174
|
+
lines.push("Content-Transfer-Encoding: 7bit");
|
|
35175
|
+
lines.push("");
|
|
35176
|
+
lines.push(options.text);
|
|
35177
|
+
lines.push("");
|
|
35178
|
+
lines.push(`--${altBoundary}`);
|
|
35179
|
+
lines.push("Content-Type: text/html; charset=UTF-8");
|
|
35180
|
+
lines.push("Content-Transfer-Encoding: 7bit");
|
|
35181
|
+
lines.push("");
|
|
35182
|
+
lines.push(options.body);
|
|
35183
|
+
lines.push("");
|
|
35184
|
+
lines.push(`--${altBoundary}--`);
|
|
35185
|
+
} else {
|
|
35186
|
+
const contentType = options.html ? "text/html" : "text/plain";
|
|
35187
|
+
lines.push(`Content-Type: ${contentType}; charset=UTF-8`);
|
|
35188
|
+
lines.push("Content-Transfer-Encoding: 7bit");
|
|
35189
|
+
lines.push("");
|
|
35190
|
+
lines.push(options.body);
|
|
35191
|
+
}
|
|
35192
|
+
for (const filePath of options.attachments) {
|
|
35193
|
+
const fileName = basename6(filePath);
|
|
35194
|
+
const fileData = readFileSync25(filePath);
|
|
35195
|
+
const base64Data = fileData.toString("base64");
|
|
35196
|
+
lines.push("");
|
|
35197
|
+
lines.push(`--${boundary}`);
|
|
35198
|
+
lines.push(`Content-Type: application/octet-stream; name="${fileName}"`);
|
|
35199
|
+
lines.push("Content-Transfer-Encoding: base64");
|
|
35200
|
+
lines.push(`Content-Disposition: attachment; filename="${fileName}"`);
|
|
35201
|
+
lines.push("");
|
|
35202
|
+
for (let i = 0; i < base64Data.length; i += 76) {
|
|
35203
|
+
lines.push(base64Data.substring(i, i + 76));
|
|
35204
|
+
}
|
|
35205
|
+
}
|
|
35206
|
+
lines.push("");
|
|
35207
|
+
lines.push(`--${boundary}--`);
|
|
35208
|
+
} else if (hasTextAlt) {
|
|
35209
|
+
lines.push(`Content-Type: multipart/alternative; boundary="${altBoundary}"`);
|
|
35210
|
+
lines.push("");
|
|
35211
|
+
lines.push(`--${altBoundary}`);
|
|
35212
|
+
lines.push("Content-Type: text/plain; charset=UTF-8");
|
|
35213
|
+
lines.push("Content-Transfer-Encoding: 7bit");
|
|
35214
|
+
lines.push("");
|
|
35215
|
+
lines.push(options.text);
|
|
35216
|
+
lines.push("");
|
|
35217
|
+
lines.push(`--${altBoundary}`);
|
|
35218
|
+
lines.push("Content-Type: text/html; charset=UTF-8");
|
|
35219
|
+
lines.push("Content-Transfer-Encoding: 7bit");
|
|
35220
|
+
lines.push("");
|
|
35221
|
+
lines.push(options.body);
|
|
35222
|
+
lines.push("");
|
|
35223
|
+
lines.push(`--${altBoundary}--`);
|
|
35224
|
+
} else {
|
|
35225
|
+
const contentType = options.html ? "text/html" : "text/plain";
|
|
35226
|
+
lines.push(`Content-Type: ${contentType}; charset=UTF-8`);
|
|
35227
|
+
lines.push("");
|
|
35228
|
+
lines.push(options.body);
|
|
35229
|
+
}
|
|
35230
|
+
return lines.join("\r\n");
|
|
35231
|
+
}
|
|
35232
|
+
function imapQuote(s) {
|
|
35233
|
+
if (/^[a-zA-Z0-9_./-]+$/.test(s)) return s;
|
|
35234
|
+
return '"' + s.replace(/\\/g, "\\\\").replace(/"/g, '\\"') + '"';
|
|
35235
|
+
}
|
|
35236
|
+
function imapReadLine(socket) {
|
|
35237
|
+
return new Promise((resolve21, reject) => {
|
|
35238
|
+
let buffer = "";
|
|
35239
|
+
const onData = (chunk) => {
|
|
35240
|
+
buffer += chunk.toString("utf-8");
|
|
35241
|
+
const nlIndex = buffer.indexOf("\r\n");
|
|
35242
|
+
if (nlIndex !== -1) {
|
|
35243
|
+
socket.removeListener("data", onData);
|
|
35244
|
+
socket.removeListener("error", onError);
|
|
35245
|
+
resolve21(buffer);
|
|
35246
|
+
}
|
|
35247
|
+
};
|
|
35248
|
+
const onError = (err) => {
|
|
35249
|
+
socket.removeListener("data", onData);
|
|
35250
|
+
reject(err);
|
|
35251
|
+
};
|
|
35252
|
+
socket.on("data", onData);
|
|
35253
|
+
socket.on("error", onError);
|
|
35254
|
+
});
|
|
35255
|
+
}
|
|
35256
|
+
function imapCommand(socket, command) {
|
|
35257
|
+
return new Promise((resolve21, reject) => {
|
|
35258
|
+
imapTagCounter++;
|
|
35259
|
+
const tag = `T${imapTagCounter}`;
|
|
35260
|
+
const fullCommand = `${tag} ${command}\r
|
|
35261
|
+
`;
|
|
35262
|
+
let buffer = "";
|
|
35263
|
+
const onData = (chunk) => {
|
|
35264
|
+
buffer += chunk.toString("utf-8");
|
|
35265
|
+
if (buffer.includes(`${tag} OK`)) {
|
|
35266
|
+
socket.removeListener("data", onData);
|
|
35267
|
+
socket.removeListener("error", onError);
|
|
35268
|
+
resolve21(buffer);
|
|
35269
|
+
return;
|
|
35270
|
+
}
|
|
35271
|
+
if (buffer.includes(`${tag} NO`) || buffer.includes(`${tag} BAD`)) {
|
|
35272
|
+
socket.removeListener("data", onData);
|
|
35273
|
+
socket.removeListener("error", onError);
|
|
35274
|
+
reject(new MessengerConnectionError(`IMAP command failed: ${command.split(" ")[0]} \u2192 ${buffer.trim()}`));
|
|
35275
|
+
}
|
|
35276
|
+
};
|
|
35277
|
+
const onError = (err) => {
|
|
35278
|
+
socket.removeListener("data", onData);
|
|
35279
|
+
reject(err);
|
|
35280
|
+
};
|
|
35281
|
+
socket.on("data", onData);
|
|
35282
|
+
socket.on("error", onError);
|
|
35283
|
+
socket.write(fullCommand, "utf-8");
|
|
35284
|
+
});
|
|
35285
|
+
}
|
|
35286
|
+
function imapFail(method, err) {
|
|
35287
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
35288
|
+
Log.error(`Messenger IMAP ${method}() failed: ${e.name}: ${e.message}`);
|
|
35289
|
+
if (e instanceof MessengerConnectionError) return e;
|
|
35290
|
+
return new MessengerConnectionError(`IMAP ${method} failed: ${e.message}`);
|
|
35291
|
+
}
|
|
35292
|
+
function parseSearchResponse(response) {
|
|
35293
|
+
const match = response.match(/\* SEARCH (.+)/);
|
|
35294
|
+
if (!match) return [];
|
|
35295
|
+
return match[1].trim().split(/\s+/).filter((s) => /^\d+$/.test(s));
|
|
35296
|
+
}
|
|
35297
|
+
function parseHeaderResponse(uid, response) {
|
|
35298
|
+
const headers = {};
|
|
35299
|
+
const headerBlock = response.match(/\r\n([\s\S]*?)\r\n\)/);
|
|
35300
|
+
if (headerBlock) {
|
|
35301
|
+
const lines = headerBlock[1].split(/\r\n/);
|
|
35302
|
+
let currentKey = "";
|
|
35303
|
+
for (const line of lines) {
|
|
35304
|
+
if (/^\s/.test(line) && currentKey) {
|
|
35305
|
+
headers[currentKey] += " " + line.trim();
|
|
35306
|
+
} else {
|
|
35307
|
+
const colonIdx = line.indexOf(":");
|
|
35308
|
+
if (colonIdx > 0) {
|
|
35309
|
+
currentKey = line.substring(0, colonIdx).trim().toLowerCase();
|
|
35310
|
+
headers[currentKey] = line.substring(colonIdx + 1).trim();
|
|
35311
|
+
}
|
|
35312
|
+
}
|
|
35313
|
+
}
|
|
35314
|
+
}
|
|
35315
|
+
const seen = /\\Seen/i.test(response);
|
|
35316
|
+
return {
|
|
35317
|
+
uid,
|
|
35318
|
+
subject: headers["subject"] ?? "",
|
|
35319
|
+
from: headers["from"] ?? "",
|
|
35320
|
+
to: headers["to"] ?? "",
|
|
35321
|
+
date: headers["date"] ?? "",
|
|
35322
|
+
snippet: "",
|
|
35323
|
+
seen
|
|
35324
|
+
};
|
|
35325
|
+
}
|
|
35326
|
+
function emptyFullMessage(uid) {
|
|
35327
|
+
return { uid, subject: "", from: "", to: "", cc: "", date: "", bodyText: "", bodyHtml: "", headers: {} };
|
|
35328
|
+
}
|
|
35329
|
+
function parseFullMessage(uid, response) {
|
|
35330
|
+
const bodyMatch = response.match(/\{(\d+)\}\r\n([\s\S]*)/);
|
|
35331
|
+
const rawMessage = bodyMatch ? bodyMatch[2] : response;
|
|
35332
|
+
const headerEnd = rawMessage.indexOf("\r\n\r\n");
|
|
35333
|
+
const headerSection = headerEnd > 0 ? rawMessage.substring(0, headerEnd) : rawMessage;
|
|
35334
|
+
const bodySection = headerEnd > 0 ? rawMessage.substring(headerEnd + 4) : "";
|
|
35335
|
+
const headers = {};
|
|
35336
|
+
const headerLines = headerSection.split(/\r\n/);
|
|
35337
|
+
let currentKey = "";
|
|
35338
|
+
for (const line of headerLines) {
|
|
35339
|
+
if (/^\s/.test(line) && currentKey) {
|
|
35340
|
+
headers[currentKey] += " " + line.trim();
|
|
35341
|
+
} else {
|
|
35342
|
+
const colonIdx = line.indexOf(":");
|
|
35343
|
+
if (colonIdx > 0) {
|
|
35344
|
+
currentKey = line.substring(0, colonIdx).trim().toLowerCase();
|
|
35345
|
+
headers[currentKey] = line.substring(colonIdx + 1).trim();
|
|
35346
|
+
}
|
|
35347
|
+
}
|
|
35348
|
+
}
|
|
35349
|
+
const contentType = headers["content-type"] ?? "text/plain";
|
|
35350
|
+
let bodyText = "";
|
|
35351
|
+
let bodyHtml = "";
|
|
35352
|
+
if (contentType.includes("multipart")) {
|
|
35353
|
+
const boundaryMatch = contentType.match(/boundary="?([^";\s]+)"?/);
|
|
35354
|
+
if (boundaryMatch) {
|
|
35355
|
+
const boundary = boundaryMatch[1];
|
|
35356
|
+
const parts = bodySection.split("--" + boundary);
|
|
35357
|
+
for (const part of parts) {
|
|
35358
|
+
if (part.trim() === "" || part.trim() === "--") continue;
|
|
35359
|
+
const partHeaderEnd = part.indexOf("\r\n\r\n");
|
|
35360
|
+
const partHeaders = partHeaderEnd > 0 ? part.substring(0, partHeaderEnd).toLowerCase() : "";
|
|
35361
|
+
const partBody = partHeaderEnd > 0 ? part.substring(partHeaderEnd + 4).trim() : "";
|
|
35362
|
+
if (partHeaders.includes("text/html")) {
|
|
35363
|
+
bodyHtml = partBody;
|
|
35364
|
+
} else if (partHeaders.includes("text/plain")) {
|
|
35365
|
+
bodyText = partBody;
|
|
35366
|
+
}
|
|
35367
|
+
}
|
|
35368
|
+
}
|
|
35369
|
+
} else if (contentType.includes("text/html")) {
|
|
35370
|
+
bodyHtml = bodySection;
|
|
35371
|
+
} else {
|
|
35372
|
+
bodyText = bodySection;
|
|
35373
|
+
}
|
|
35374
|
+
bodyText = bodyText.replace(/\)\r\n[A-Z]\d+ OK.*$/s, "").trim();
|
|
35375
|
+
bodyHtml = bodyHtml.replace(/\)\r\n[A-Z]\d+ OK.*$/s, "").trim();
|
|
35376
|
+
return {
|
|
35377
|
+
uid,
|
|
35378
|
+
subject: headers["subject"] ?? "",
|
|
35379
|
+
from: headers["from"] ?? "",
|
|
35380
|
+
to: headers["to"] ?? "",
|
|
35381
|
+
cc: headers["cc"] ?? "",
|
|
35382
|
+
date: headers["date"] ?? "",
|
|
35383
|
+
bodyText,
|
|
35384
|
+
bodyHtml,
|
|
35385
|
+
headers
|
|
35386
|
+
};
|
|
35387
|
+
}
|
|
35388
|
+
function createMessenger() {
|
|
35389
|
+
return new Messenger();
|
|
35390
|
+
}
|
|
35391
|
+
var MessengerConnectionError, Messenger, imapTagCounter;
|
|
35392
|
+
var init_messenger = __esm({
|
|
35393
|
+
"src/messenger.ts"() {
|
|
35394
|
+
"use strict";
|
|
35395
|
+
init_dotenv();
|
|
35396
|
+
init_devMailbox();
|
|
35397
|
+
init_logger();
|
|
35398
|
+
MessengerConnectionError = class extends Error {
|
|
35399
|
+
constructor(message) {
|
|
35400
|
+
super(message);
|
|
35401
|
+
this.name = "MessengerConnectionError";
|
|
35402
|
+
}
|
|
35403
|
+
};
|
|
35404
|
+
Messenger = class {
|
|
35405
|
+
host;
|
|
35406
|
+
port;
|
|
35407
|
+
username;
|
|
35408
|
+
password;
|
|
35409
|
+
fromAddress;
|
|
35410
|
+
fromName;
|
|
35411
|
+
encryption;
|
|
35412
|
+
useTls;
|
|
35413
|
+
/** Whether an SMTP host was actually configured (see the constructor). */
|
|
35414
|
+
smtpConfigured = false;
|
|
35415
|
+
/** The local mailbox, present only when this messenger captures. */
|
|
35416
|
+
devMailbox = null;
|
|
35417
|
+
imapHost;
|
|
35418
|
+
imapPort;
|
|
35419
|
+
imapUser;
|
|
35420
|
+
imapPass;
|
|
35421
|
+
imapEncryption;
|
|
35422
|
+
constructor(options) {
|
|
35423
|
+
this.smtpConfigured = Boolean(options?.host ?? process.env.TINA4_MAIL_HOST);
|
|
35424
|
+
this.host = options?.host ?? process.env.TINA4_MAIL_HOST ?? "localhost";
|
|
35425
|
+
this.port = options?.port ?? parseInt(process.env.TINA4_MAIL_PORT ?? "587", 10);
|
|
35426
|
+
this.username = options?.username ?? process.env.TINA4_MAIL_USERNAME ?? "";
|
|
35427
|
+
this.password = options?.password ?? process.env.TINA4_MAIL_PASSWORD ?? "";
|
|
35428
|
+
this.fromAddress = options?.fromAddress ?? process.env.TINA4_MAIL_FROM ?? (this.username || "noreply@localhost");
|
|
35429
|
+
this.fromName = options?.fromName ?? process.env.TINA4_MAIL_FROM_NAME ?? "";
|
|
35430
|
+
const envEncryption = options?.encryption ?? process.env.TINA4_MAIL_ENCRYPTION;
|
|
35431
|
+
if (envEncryption) {
|
|
35432
|
+
this.encryption = envEncryption.toLowerCase();
|
|
35433
|
+
} else if (options?.useTls !== void 0) {
|
|
35434
|
+
this.encryption = options.useTls ? "tls" : "none";
|
|
35435
|
+
} else {
|
|
35436
|
+
this.encryption = "tls";
|
|
35437
|
+
}
|
|
35438
|
+
this.useTls = ["tls", "starttls"].includes(this.encryption);
|
|
35439
|
+
this.imapHost = options?.imapHost ?? process.env.TINA4_MAIL_IMAP_HOST ?? "";
|
|
35440
|
+
this.imapPort = options?.imapPort ?? parseInt(process.env.TINA4_MAIL_IMAP_PORT ?? "993", 10);
|
|
35441
|
+
this.imapUser = options?.imapUser ?? process.env.TINA4_MAIL_IMAP_USERNAME ?? this.username;
|
|
35442
|
+
this.imapPass = options?.imapPass ?? process.env.TINA4_MAIL_IMAP_PASSWORD ?? this.password;
|
|
35443
|
+
this.imapEncryption = (options?.imapEncryption ?? process.env.TINA4_MAIL_IMAP_ENCRYPTION ?? "tls").toLowerCase();
|
|
35444
|
+
}
|
|
35445
|
+
/**
|
|
35446
|
+
* Read-only IMAP encryption mode for inspection / tests.
|
|
35447
|
+
* Returns one of "tls", "starttls", "none", "ssl".
|
|
35448
|
+
*/
|
|
35449
|
+
getImapEncryption() {
|
|
35450
|
+
return this.imapEncryption;
|
|
35451
|
+
}
|
|
35452
|
+
/**
|
|
35453
|
+
* Send an email via SMTP.
|
|
35454
|
+
*/
|
|
35455
|
+
/**
|
|
35456
|
+
* Should send() capture locally instead of talking to SMTP?
|
|
35457
|
+
*
|
|
35458
|
+
* Availability decides, not verbosity. With no SMTP host configured sending is
|
|
35459
|
+
* impossible, so simulate it into a folder rather than failing -- that is what
|
|
35460
|
+
* makes a laptop with no mail server usable. TINA4_MAIL_CAPTURE forces capture
|
|
35461
|
+
* even when a host IS configured.
|
|
35462
|
+
*
|
|
35463
|
+
* TINA4_DEBUG deliberately does NOT gate this, and neither does NODE_ENV. Debug
|
|
35464
|
+
* must still be able to send, and the old `NODE_ENV !== "production"` clause
|
|
35465
|
+
* silently swallowed every staging email.
|
|
35466
|
+
*/
|
|
35467
|
+
shouldCapture() {
|
|
35468
|
+
if (isTruthy(process.env.TINA4_MAIL_CAPTURE)) return true;
|
|
35469
|
+
return !this.smtpConfigured;
|
|
35470
|
+
}
|
|
35471
|
+
/** The local mailbox, created on first capture and reused after. */
|
|
35472
|
+
getDevMailbox() {
|
|
35473
|
+
if (this.devMailbox === null) {
|
|
35474
|
+
this.devMailbox = new DevMailbox();
|
|
35475
|
+
}
|
|
35476
|
+
return this.devMailbox;
|
|
35477
|
+
}
|
|
35478
|
+
async send(to, subject, body, html = false, text, cc, bcc, replyTo, attachments, headers) {
|
|
35479
|
+
const options = { to, subject, body, html, text, cc, bcc, replyTo, attachments, headers };
|
|
35480
|
+
const toList = Array.isArray(options.to) ? options.to : [options.to];
|
|
35481
|
+
const ccList = Array.isArray(options.cc) ? options.cc : options.cc ? [options.cc] : [];
|
|
35482
|
+
const bccList = Array.isArray(options.bcc) ? options.bcc : options.bcc ? [options.bcc] : [];
|
|
35483
|
+
const allRecipients = [...toList, ...ccList, ...bccList];
|
|
35484
|
+
if (this.shouldCapture()) {
|
|
35485
|
+
return this.getDevMailbox().capture(
|
|
35486
|
+
to,
|
|
35487
|
+
subject,
|
|
35488
|
+
body,
|
|
35489
|
+
html,
|
|
35490
|
+
text,
|
|
35491
|
+
ccList,
|
|
35492
|
+
bccList,
|
|
35493
|
+
replyTo,
|
|
35494
|
+
attachments,
|
|
35495
|
+
this.fromAddress || void 0
|
|
35496
|
+
);
|
|
35497
|
+
}
|
|
35498
|
+
const messageId = `${randomUUID9()}@${this.host}`;
|
|
35499
|
+
if (allRecipients.length === 0) {
|
|
35500
|
+
return { success: false, message: "No recipients specified" };
|
|
35501
|
+
}
|
|
35502
|
+
if (!this.fromAddress) {
|
|
35503
|
+
return { success: false, message: "No from address configured" };
|
|
35504
|
+
}
|
|
35505
|
+
try {
|
|
35506
|
+
let socket;
|
|
35507
|
+
if (this.port === 465) {
|
|
35508
|
+
socket = tls2.connect({ host: this.host, port: this.port, rejectUnauthorized: tlsRejectUnauthorized() });
|
|
35509
|
+
await new Promise((resolve21, reject) => {
|
|
35510
|
+
socket.once("secureConnect", resolve21);
|
|
35511
|
+
socket.once("error", reject);
|
|
35512
|
+
});
|
|
35513
|
+
} else {
|
|
35514
|
+
socket = net3.createConnection({ host: this.host, port: this.port });
|
|
35515
|
+
await new Promise((resolve21, reject) => {
|
|
35516
|
+
socket.once("connect", resolve21);
|
|
35517
|
+
socket.once("error", reject);
|
|
35518
|
+
});
|
|
35519
|
+
}
|
|
35520
|
+
const greeting = await readResponse(socket);
|
|
35521
|
+
if (greeting.code !== 220) {
|
|
35522
|
+
socket.destroy();
|
|
35523
|
+
return { success: false, message: `SMTP greeting failed: ${greeting.text}` };
|
|
35524
|
+
}
|
|
35525
|
+
const ehlo = await sendCommand(socket, `EHLO ${this.host}`);
|
|
35526
|
+
if (ehlo.code !== 250) {
|
|
35527
|
+
socket.destroy();
|
|
35528
|
+
return { success: false, message: `EHLO failed: ${ehlo.text}` };
|
|
35529
|
+
}
|
|
35530
|
+
if (this.useTls && this.port !== 465 && ehlo.text.includes("STARTTLS")) {
|
|
35531
|
+
const starttls = await sendCommand(socket, "STARTTLS");
|
|
35532
|
+
if (starttls.code !== 220) {
|
|
35533
|
+
socket.destroy();
|
|
35534
|
+
return { success: false, message: `STARTTLS failed: ${starttls.text}` };
|
|
35535
|
+
}
|
|
35536
|
+
const plainSocket = socket;
|
|
35537
|
+
socket = tls2.connect(
|
|
35538
|
+
{ socket: plainSocket, host: this.host, rejectUnauthorized: tlsRejectUnauthorized() }
|
|
35539
|
+
);
|
|
35540
|
+
await new Promise((resolve21, reject) => {
|
|
35541
|
+
socket.once("secureConnect", resolve21);
|
|
35542
|
+
socket.once("error", reject);
|
|
35543
|
+
});
|
|
35544
|
+
const ehlo2 = await sendCommand(socket, `EHLO ${this.host}`);
|
|
35545
|
+
if (ehlo2.code !== 250) {
|
|
35546
|
+
socket.destroy();
|
|
35547
|
+
return { success: false, message: `EHLO after STARTTLS failed: ${ehlo2.text}` };
|
|
35548
|
+
}
|
|
35549
|
+
}
|
|
35550
|
+
if (this.username && this.password) {
|
|
35551
|
+
const auth = await sendCommand(socket, "AUTH LOGIN");
|
|
35552
|
+
if (auth.code !== 334) {
|
|
35553
|
+
socket.destroy();
|
|
35554
|
+
return { success: false, message: `AUTH LOGIN failed: ${auth.text}` };
|
|
35555
|
+
}
|
|
35556
|
+
const userResp = await sendCommand(socket, Buffer.from(this.username).toString("base64"));
|
|
35557
|
+
if (userResp.code !== 334) {
|
|
35558
|
+
socket.destroy();
|
|
35559
|
+
return { success: false, message: `AUTH username failed: ${userResp.text}` };
|
|
35560
|
+
}
|
|
35561
|
+
const passResp = await sendCommand(socket, Buffer.from(this.password).toString("base64"));
|
|
35562
|
+
if (passResp.code !== 235) {
|
|
35563
|
+
socket.destroy();
|
|
35564
|
+
return { success: false, message: `AUTH password failed: ${passResp.text}` };
|
|
35565
|
+
}
|
|
35566
|
+
}
|
|
35567
|
+
const mailFrom = await sendCommand(socket, `MAIL FROM:<${this.fromAddress}>`);
|
|
35568
|
+
if (mailFrom.code !== 250) {
|
|
35569
|
+
socket.destroy();
|
|
35570
|
+
return { success: false, message: `MAIL FROM failed: ${mailFrom.text}` };
|
|
35571
|
+
}
|
|
35572
|
+
for (const recipient of allRecipients) {
|
|
35573
|
+
const rcpt = await sendCommand(socket, `RCPT TO:<${recipient}>`);
|
|
35574
|
+
if (rcpt.code !== 250 && rcpt.code !== 251) {
|
|
35575
|
+
socket.destroy();
|
|
35576
|
+
return { success: false, message: `RCPT TO <${recipient}> failed: ${rcpt.text}` };
|
|
35577
|
+
}
|
|
35578
|
+
}
|
|
35579
|
+
const dataCmd = await sendCommand(socket, "DATA");
|
|
35580
|
+
if (dataCmd.code !== 354) {
|
|
35581
|
+
socket.destroy();
|
|
35582
|
+
return { success: false, message: `DATA failed: ${dataCmd.text}` };
|
|
35583
|
+
}
|
|
35584
|
+
const mimeMessage = buildMimeMessage({
|
|
35585
|
+
from: this.fromAddress,
|
|
35586
|
+
fromName: this.fromName,
|
|
35587
|
+
to: toList,
|
|
35588
|
+
cc: ccList,
|
|
35589
|
+
subject: options.subject,
|
|
35590
|
+
body: options.body,
|
|
35591
|
+
html: options.html ?? false,
|
|
35592
|
+
text: options.text,
|
|
35593
|
+
replyTo: options.replyTo,
|
|
35594
|
+
attachments: options.attachments,
|
|
35595
|
+
headers: options.headers,
|
|
35596
|
+
messageId
|
|
35597
|
+
});
|
|
35598
|
+
const endData = await sendCommand(socket, mimeMessage + "\r\n.");
|
|
35599
|
+
if (endData.code !== 250) {
|
|
35600
|
+
socket.destroy();
|
|
35601
|
+
return { success: false, message: `Message delivery failed: ${endData.text}` };
|
|
35602
|
+
}
|
|
35603
|
+
await sendCommand(socket, "QUIT");
|
|
35604
|
+
socket.destroy();
|
|
35605
|
+
return { success: true, message: "Email sent successfully", id: messageId };
|
|
35606
|
+
} catch (err) {
|
|
35607
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
35608
|
+
return { success: false, message: `SMTP error: ${errMsg}` };
|
|
35609
|
+
}
|
|
35610
|
+
}
|
|
35611
|
+
/**
|
|
35612
|
+
* Test the SMTP connection without sending an email.
|
|
35613
|
+
*/
|
|
35614
|
+
async testConnection() {
|
|
35615
|
+
try {
|
|
35616
|
+
let socket;
|
|
35617
|
+
if (this.port === 465) {
|
|
35618
|
+
socket = tls2.connect({ host: this.host, port: this.port, rejectUnauthorized: tlsRejectUnauthorized() });
|
|
35619
|
+
await new Promise((resolve21, reject) => {
|
|
35620
|
+
socket.once("secureConnect", resolve21);
|
|
35621
|
+
socket.once("error", reject);
|
|
35622
|
+
});
|
|
35623
|
+
} else {
|
|
35624
|
+
socket = net3.createConnection({ host: this.host, port: this.port });
|
|
35625
|
+
await new Promise((resolve21, reject) => {
|
|
35626
|
+
socket.once("connect", resolve21);
|
|
35627
|
+
socket.once("error", reject);
|
|
35628
|
+
});
|
|
35629
|
+
}
|
|
35630
|
+
const greeting = await readResponse(socket);
|
|
35631
|
+
if (greeting.code !== 220) {
|
|
35632
|
+
socket.destroy();
|
|
35633
|
+
return { success: false, message: `SMTP greeting failed: ${greeting.text}` };
|
|
35634
|
+
}
|
|
35635
|
+
const ehlo = await sendCommand(socket, `EHLO ${this.host}`);
|
|
35636
|
+
if (ehlo.code !== 250) {
|
|
35637
|
+
socket.destroy();
|
|
35638
|
+
return { success: false, message: `EHLO failed: ${ehlo.text}` };
|
|
35639
|
+
}
|
|
35640
|
+
await sendCommand(socket, "QUIT");
|
|
35641
|
+
socket.destroy();
|
|
35642
|
+
return { success: true, message: `Connected to ${this.host}:${this.port}` };
|
|
35643
|
+
} catch (err) {
|
|
35644
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
35645
|
+
return { success: false, message: `Connection failed: ${errMsg}` };
|
|
35646
|
+
}
|
|
35647
|
+
}
|
|
35648
|
+
// ── IMAP (Read) ────────────────────────────────────────────
|
|
35649
|
+
/**
|
|
35650
|
+
* Connect to the IMAP server via raw TCP/TLS.
|
|
35651
|
+
* Returns the socket and reads the greeting.
|
|
35652
|
+
*/
|
|
35653
|
+
async imapConnect() {
|
|
35654
|
+
if (!this.imapHost) {
|
|
35655
|
+
throw new Error("IMAP host not configured (set imapHost or IMAP_HOST env)");
|
|
35656
|
+
}
|
|
35657
|
+
let socket;
|
|
35658
|
+
const useTls = this.imapEncryption === "tls" || this.imapEncryption === "ssl" || this.imapEncryption === "" && this.imapPort === 993;
|
|
35659
|
+
if (useTls) {
|
|
35660
|
+
socket = tls2.connect({ host: this.imapHost, port: this.imapPort, rejectUnauthorized: tlsRejectUnauthorized() });
|
|
35661
|
+
await new Promise((resolve21, reject) => {
|
|
35662
|
+
socket.once("secureConnect", resolve21);
|
|
35663
|
+
socket.once("error", reject);
|
|
35664
|
+
});
|
|
35665
|
+
} else {
|
|
35666
|
+
socket = net3.createConnection({ host: this.imapHost, port: this.imapPort });
|
|
35667
|
+
await new Promise((resolve21, reject) => {
|
|
35668
|
+
socket.once("connect", resolve21);
|
|
35669
|
+
socket.once("error", reject);
|
|
35670
|
+
});
|
|
35671
|
+
}
|
|
35672
|
+
await imapReadLine(socket);
|
|
35673
|
+
if (this.imapUser && this.imapPass) {
|
|
35674
|
+
const loginResp = await imapCommand(socket, `LOGIN ${imapQuote(this.imapUser)} ${imapQuote(this.imapPass)}`);
|
|
35675
|
+
if (!loginResp.includes("OK")) {
|
|
35676
|
+
socket.destroy();
|
|
35677
|
+
throw new Error(`IMAP login failed: ${loginResp}`);
|
|
35678
|
+
}
|
|
35679
|
+
}
|
|
35680
|
+
return socket;
|
|
35681
|
+
}
|
|
35682
|
+
/**
|
|
35683
|
+
* Disconnect from IMAP cleanly.
|
|
35684
|
+
*/
|
|
35685
|
+
async imapDisconnect(socket) {
|
|
35686
|
+
try {
|
|
35687
|
+
await imapCommand(socket, "LOGOUT");
|
|
35688
|
+
} catch {
|
|
35689
|
+
}
|
|
35690
|
+
socket.destroy();
|
|
35691
|
+
}
|
|
35692
|
+
/**
|
|
35693
|
+
* Fetch latest messages from a folder.
|
|
35694
|
+
* Returns list of message summaries.
|
|
35695
|
+
*/
|
|
35696
|
+
async inbox(limit = 20, offset = 0, folder = "INBOX") {
|
|
35697
|
+
let socket;
|
|
35698
|
+
try {
|
|
35699
|
+
socket = await this.imapConnect();
|
|
35700
|
+
} catch (err) {
|
|
35701
|
+
throw imapFail("inbox", err);
|
|
35702
|
+
}
|
|
35703
|
+
try {
|
|
35704
|
+
await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
|
|
35705
|
+
const searchResp = await imapCommand(socket, "SEARCH ALL");
|
|
35706
|
+
const uids = parseSearchResponse(searchResp);
|
|
35707
|
+
if (uids.length === 0) return [];
|
|
35708
|
+
uids.reverse();
|
|
35709
|
+
const selected = uids.slice(offset, offset + limit);
|
|
35710
|
+
if (selected.length === 0) return [];
|
|
35711
|
+
const messages = [];
|
|
35712
|
+
for (const uid of selected) {
|
|
35713
|
+
const fetchResp = await imapCommand(socket, `FETCH ${uid} (FLAGS BODY.PEEK[HEADER.FIELDS (FROM TO SUBJECT DATE)])`);
|
|
35714
|
+
messages.push(parseHeaderResponse(uid, fetchResp));
|
|
35715
|
+
}
|
|
35716
|
+
return messages;
|
|
35717
|
+
} catch (err) {
|
|
35718
|
+
throw imapFail("inbox", err);
|
|
35719
|
+
} finally {
|
|
35720
|
+
await this.imapDisconnect(socket);
|
|
35721
|
+
}
|
|
35722
|
+
}
|
|
35723
|
+
/**
|
|
35724
|
+
* Read a single message by sequence number or UID.
|
|
35725
|
+
*/
|
|
35726
|
+
async read(uid, folder = "INBOX") {
|
|
35727
|
+
let socket;
|
|
35728
|
+
try {
|
|
35729
|
+
socket = await this.imapConnect();
|
|
35730
|
+
} catch (err) {
|
|
35731
|
+
throw imapFail("read", err);
|
|
35732
|
+
}
|
|
35733
|
+
try {
|
|
35734
|
+
await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
|
|
35735
|
+
const fetchResp = await imapCommand(socket, `FETCH ${uid} (FLAGS BODY[])`);
|
|
35736
|
+
if (!/\{\d+\}/.test(fetchResp)) {
|
|
35737
|
+
return emptyFullMessage(uid);
|
|
35738
|
+
}
|
|
35739
|
+
await imapCommand(socket, `STORE ${uid} +FLAGS (\\Seen)`);
|
|
35740
|
+
return parseFullMessage(uid, fetchResp);
|
|
35741
|
+
} catch (err) {
|
|
35742
|
+
throw imapFail("read", err);
|
|
35743
|
+
} finally {
|
|
35744
|
+
await this.imapDisconnect(socket);
|
|
35745
|
+
}
|
|
35746
|
+
}
|
|
35747
|
+
/**
|
|
35748
|
+
* Search messages using IMAP search criteria.
|
|
35749
|
+
*/
|
|
35750
|
+
async search(folder = "INBOX", subject, sender, since, before, unseenOnly = false, limit = 50) {
|
|
35751
|
+
const criteria = ["ALL"];
|
|
35752
|
+
if (subject) criteria.push(`SUBJECT "${subject}"`);
|
|
35753
|
+
if (sender) criteria.push(`FROM "${sender}"`);
|
|
35754
|
+
if (since) criteria.push(`SINCE ${since}`);
|
|
35755
|
+
if (before) criteria.push(`BEFORE ${before}`);
|
|
35756
|
+
if (unseenOnly) criteria.push("UNSEEN");
|
|
35757
|
+
const query = criteria.join(" ");
|
|
35758
|
+
let socket;
|
|
35759
|
+
try {
|
|
35760
|
+
socket = await this.imapConnect();
|
|
35761
|
+
} catch (err) {
|
|
35762
|
+
throw imapFail("search", err);
|
|
35763
|
+
}
|
|
35764
|
+
try {
|
|
35765
|
+
await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
|
|
35766
|
+
const searchResp = await imapCommand(socket, `SEARCH ${query}`);
|
|
35767
|
+
const uids = parseSearchResponse(searchResp);
|
|
35768
|
+
if (uids.length === 0) return [];
|
|
35769
|
+
uids.reverse();
|
|
35770
|
+
const messages = [];
|
|
35771
|
+
for (const uid of uids.slice(0, limit)) {
|
|
35772
|
+
const fetchResp = await imapCommand(socket, `FETCH ${uid} (FLAGS BODY.PEEK[HEADER.FIELDS (FROM TO SUBJECT DATE)])`);
|
|
35773
|
+
messages.push(parseHeaderResponse(uid, fetchResp));
|
|
35774
|
+
}
|
|
35775
|
+
return messages;
|
|
35776
|
+
} catch (err) {
|
|
35777
|
+
throw imapFail("search", err);
|
|
35778
|
+
} finally {
|
|
35779
|
+
await this.imapDisconnect(socket);
|
|
35780
|
+
}
|
|
35781
|
+
}
|
|
35782
|
+
/**
|
|
35783
|
+
* Delete a message by UID.
|
|
35784
|
+
*/
|
|
35785
|
+
async deleteMessage(uid, folder = "INBOX") {
|
|
35786
|
+
const socket = await this.imapConnect();
|
|
35787
|
+
try {
|
|
35788
|
+
await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
|
|
35789
|
+
await imapCommand(socket, `STORE ${uid} +FLAGS (\\Deleted)`);
|
|
35790
|
+
await imapCommand(socket, "EXPUNGE");
|
|
35791
|
+
} finally {
|
|
35792
|
+
await this.imapDisconnect(socket);
|
|
35793
|
+
}
|
|
35794
|
+
}
|
|
35795
|
+
/**
|
|
35796
|
+
* Mark a message as read.
|
|
35797
|
+
*/
|
|
35798
|
+
async markRead(uid, folder = "INBOX") {
|
|
35799
|
+
const socket = await this.imapConnect();
|
|
35800
|
+
try {
|
|
35801
|
+
await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
|
|
35802
|
+
await imapCommand(socket, `STORE ${uid} +FLAGS (\\Seen)`);
|
|
35803
|
+
} finally {
|
|
35804
|
+
await this.imapDisconnect(socket);
|
|
35805
|
+
}
|
|
35806
|
+
}
|
|
35807
|
+
/**
|
|
35808
|
+
* Count unseen messages in a folder.
|
|
35809
|
+
*/
|
|
35810
|
+
async unread(folder = "INBOX") {
|
|
35811
|
+
let socket;
|
|
35812
|
+
try {
|
|
35813
|
+
socket = await this.imapConnect();
|
|
35814
|
+
} catch (err) {
|
|
35815
|
+
throw imapFail("unread", err);
|
|
35816
|
+
}
|
|
35817
|
+
try {
|
|
35818
|
+
await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
|
|
35819
|
+
const searchResp = await imapCommand(socket, "SEARCH UNSEEN");
|
|
35820
|
+
return parseSearchResponse(searchResp).length;
|
|
35821
|
+
} catch (err) {
|
|
35822
|
+
throw imapFail("unread", err);
|
|
35823
|
+
} finally {
|
|
35824
|
+
await this.imapDisconnect(socket);
|
|
35825
|
+
}
|
|
35826
|
+
}
|
|
35827
|
+
/**
|
|
35828
|
+
* List available IMAP folders/mailboxes.
|
|
35829
|
+
*/
|
|
35830
|
+
async folders() {
|
|
35831
|
+
let socket;
|
|
35832
|
+
try {
|
|
35833
|
+
socket = await this.imapConnect();
|
|
35834
|
+
} catch (err) {
|
|
35835
|
+
throw imapFail("folders", err);
|
|
35836
|
+
}
|
|
35837
|
+
try {
|
|
35838
|
+
const resp = await imapCommand(socket, 'LIST "" "*"');
|
|
35839
|
+
const result = [];
|
|
35840
|
+
for (const line of resp.split("\r\n")) {
|
|
35841
|
+
const m = line.match(/\* LIST \([^)]*\) "[^"]*" "?([^"\r\n]+)"?/i);
|
|
35842
|
+
if (m) result.push(m[1]);
|
|
35843
|
+
}
|
|
35844
|
+
return result;
|
|
35845
|
+
} catch (err) {
|
|
35846
|
+
throw imapFail("folders", err);
|
|
35847
|
+
} finally {
|
|
35848
|
+
await this.imapDisconnect(socket);
|
|
35849
|
+
}
|
|
35850
|
+
}
|
|
35851
|
+
/**
|
|
35852
|
+
* Test IMAP connectivity without reading.
|
|
35853
|
+
*/
|
|
35854
|
+
async testImapConnection() {
|
|
35855
|
+
try {
|
|
35856
|
+
const socket = await this.imapConnect();
|
|
35857
|
+
await this.imapDisconnect(socket);
|
|
35858
|
+
return { success: true, message: `Connected to ${this.imapHost}:${this.imapPort}` };
|
|
35859
|
+
} catch (err) {
|
|
35860
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
35861
|
+
return { success: false, message: `IMAP connection failed: ${errMsg}` };
|
|
35862
|
+
}
|
|
35863
|
+
}
|
|
35864
|
+
};
|
|
35865
|
+
imapTagCounter = 0;
|
|
35866
|
+
}
|
|
35867
|
+
});
|
|
35868
|
+
|
|
35544
35869
|
// src/wsdl.ts
|
|
35545
35870
|
function escapeXml(value) {
|
|
35546
35871
|
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|