tina4-nodejs 3.13.92 → 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 +1260 -969
- package/packages/core/dist/index.js +1260 -969
- 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 +3055 -2764
- 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() {
|
|
@@ -13826,7 +13914,7 @@ function _generateFormToken(descriptor = "") {
|
|
|
13826
13914
|
function _generateFormTokenValue(descriptor = "") {
|
|
13827
13915
|
return new SafeString(_buildFormTokenJwt(descriptor));
|
|
13828
13916
|
}
|
|
13829
|
-
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;
|
|
13830
13918
|
var init_engine = __esm({
|
|
13831
13919
|
"../frond/src/engine.ts"() {
|
|
13832
13920
|
"use strict";
|
|
@@ -13869,6 +13957,29 @@ var init_engine = __esm({
|
|
|
13869
13957
|
"endset",
|
|
13870
13958
|
"endspaceless"
|
|
13871
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
|
+
};
|
|
13872
13983
|
JSON_UNSAFE_RE = /[<>&'\u2028\u2029]/g;
|
|
13873
13984
|
JSON_UNSAFE_MAP = {
|
|
13874
13985
|
"<": "\\u003c",
|
|
@@ -14504,42 +14615,27 @@ var init_engine = __esm({
|
|
|
14504
14615
|
if (stripA && i + 1 < tokens.length && tokens[i + 1][0] === "TEXT") {
|
|
14505
14616
|
tokens[i + 1] = ["TEXT", tokens[i + 1][1].replace(LEADING_WS_RE, "")];
|
|
14506
14617
|
}
|
|
14507
|
-
if (tag
|
|
14508
|
-
|
|
14509
|
-
|
|
14510
|
-
|
|
14511
|
-
|
|
14512
|
-
|
|
14513
|
-
output.push(result);
|
|
14514
|
-
i = skip;
|
|
14515
|
-
}
|
|
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;
|
|
14516
14624
|
} else if (tag === "for") {
|
|
14517
|
-
|
|
14518
|
-
|
|
14519
|
-
|
|
14520
|
-
} else {
|
|
14521
|
-
const [result, skip] = this.handleFor(tokens, i, context);
|
|
14522
|
-
output.push(result);
|
|
14523
|
-
i = skip;
|
|
14524
|
-
}
|
|
14625
|
+
const [result, skip] = this.handleFor(tokens, i, context);
|
|
14626
|
+
output.push(result);
|
|
14627
|
+
i = skip;
|
|
14525
14628
|
} else if (tag === "set") {
|
|
14526
|
-
|
|
14527
|
-
if (this._sandbox && this._allowedTags !== null && !this._allowedTags.has("set")) {
|
|
14528
|
-
i = isBlockSet ? this.skipBlock(tokens, i, "set", "endset") : i + 1;
|
|
14529
|
-
} else if (isBlockSet) {
|
|
14629
|
+
if (!content.includes("=")) {
|
|
14530
14630
|
i = this.handleSetBlock(tokens, i, context);
|
|
14531
14631
|
} else {
|
|
14532
14632
|
this.handleSet(content, context);
|
|
14533
14633
|
i++;
|
|
14534
14634
|
}
|
|
14535
14635
|
} else if (tag === "include") {
|
|
14536
|
-
|
|
14537
|
-
|
|
14538
|
-
|
|
14539
|
-
const result = this.handleInclude(content, context);
|
|
14540
|
-
output.push(result);
|
|
14541
|
-
i++;
|
|
14542
|
-
}
|
|
14636
|
+
const result = this.handleInclude(content, context);
|
|
14637
|
+
output.push(result);
|
|
14638
|
+
i++;
|
|
14543
14639
|
} else if (tag === "macro") {
|
|
14544
14640
|
const skip = this.handleMacro(tokens, i, context);
|
|
14545
14641
|
i = skip;
|
|
@@ -14584,6 +14680,41 @@ var init_engine = __esm({
|
|
|
14584
14680
|
}
|
|
14585
14681
|
return output.join("");
|
|
14586
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
|
+
}
|
|
14587
14718
|
skipBlock(tokens, start2, openTag, closeTag) {
|
|
14588
14719
|
let depth = 0;
|
|
14589
14720
|
let i = start2 + 1;
|
|
@@ -14591,7 +14722,7 @@ var init_engine = __esm({
|
|
|
14591
14722
|
if (tokens[i][0] === "BLOCK") {
|
|
14592
14723
|
const [content] = stripTag(tokens[i][1]);
|
|
14593
14724
|
const tag = content.split(/\s+/)[0] || "";
|
|
14594
|
-
if (tag === openTag) depth++;
|
|
14725
|
+
if (tag === openTag && !(openTag === "set" && content.includes("="))) depth++;
|
|
14595
14726
|
else if (tag === closeTag) {
|
|
14596
14727
|
if (depth === 0) return i + 1;
|
|
14597
14728
|
depth--;
|
|
@@ -14776,11 +14907,11 @@ var init_engine = __esm({
|
|
|
14776
14907
|
for (const [fname, rawArgs] of filters) {
|
|
14777
14908
|
const args = rawArgs.map((a) => a instanceof VarRef ? evalExpr(a.name, context) : a);
|
|
14778
14909
|
if (fname === "raw" || fname === "safe") {
|
|
14779
|
-
isSafe = true;
|
|
14910
|
+
if (this.filterPermitted(fname)) isSafe = true;
|
|
14780
14911
|
continue;
|
|
14781
14912
|
}
|
|
14782
14913
|
if (fname === "escape" || fname === "e") {
|
|
14783
|
-
isSafe = true;
|
|
14914
|
+
if (this.filterPermitted(fname)) isSafe = true;
|
|
14784
14915
|
}
|
|
14785
14916
|
if (this._sandbox && this._allowedFilters !== null) {
|
|
14786
14917
|
if (!this._allowedFilters.has(fname)) {
|
|
@@ -16591,861 +16722,131 @@ var init_rateLimiter = __esm({
|
|
|
16591
16722
|
}
|
|
16592
16723
|
});
|
|
16593
16724
|
|
|
16594
|
-
// src/
|
|
16595
|
-
import
|
|
16596
|
-
import
|
|
16597
|
-
import { readFileSync as readFileSync8 } from "node:fs";
|
|
16598
|
-
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";
|
|
16599
16728
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
16600
|
-
|
|
16601
|
-
|
|
16602
|
-
|
|
16603
|
-
|
|
16604
|
-
|
|
16605
|
-
|
|
16606
|
-
|
|
16607
|
-
|
|
16608
|
-
const lines = buffer.split("\r\n");
|
|
16609
|
-
for (let i = 0; i < lines.length; i++) {
|
|
16610
|
-
const line = lines[i];
|
|
16611
|
-
if (line.length < 3) continue;
|
|
16612
|
-
const code = parseInt(line.substring(0, 3), 10);
|
|
16613
|
-
if (line.length >= 4 && line[3] === " ") {
|
|
16614
|
-
socket.removeListener("data", onData);
|
|
16615
|
-
socket.removeListener("error", onError);
|
|
16616
|
-
resolve21({ code, text: buffer.trim() });
|
|
16617
|
-
return;
|
|
16618
|
-
}
|
|
16619
|
-
}
|
|
16620
|
-
};
|
|
16621
|
-
const onError = (err) => {
|
|
16622
|
-
socket.removeListener("data", onData);
|
|
16623
|
-
reject(err);
|
|
16624
|
-
};
|
|
16625
|
-
socket.on("data", onData);
|
|
16626
|
-
socket.on("error", onError);
|
|
16627
|
-
});
|
|
16628
|
-
}
|
|
16629
|
-
function sendCommand(socket, command) {
|
|
16630
|
-
return new Promise((resolve21, reject) => {
|
|
16631
|
-
socket.write(command + "\r\n", "utf-8", (err) => {
|
|
16632
|
-
if (err) return reject(err);
|
|
16633
|
-
readResponse(socket).then(resolve21, reject);
|
|
16634
|
-
});
|
|
16635
|
-
});
|
|
16636
|
-
}
|
|
16637
|
-
function buildMimeMessage(options) {
|
|
16638
|
-
const boundary = `----=_Tina4_${Date.now()}_${Math.random().toString(36).substring(2)}`;
|
|
16639
|
-
const altBoundary = `----=_Tina4Alt_${Date.now()}_${Math.random().toString(36).substring(2)}`;
|
|
16640
|
-
const hasAttachments = options.attachments && options.attachments.length > 0;
|
|
16641
|
-
const hasTextAlt = options.text !== void 0 && options.html;
|
|
16642
|
-
const lines = [];
|
|
16643
|
-
const fromHeader = options.fromName ? `"${options.fromName}" <${options.from}>` : options.from;
|
|
16644
|
-
lines.push(`From: ${fromHeader}`);
|
|
16645
|
-
lines.push(`To: ${options.to.join(", ")}`);
|
|
16646
|
-
if (options.cc.length > 0) {
|
|
16647
|
-
lines.push(`Cc: ${options.cc.join(", ")}`);
|
|
16648
|
-
}
|
|
16649
|
-
lines.push(`Subject: ${options.subject}`);
|
|
16650
|
-
lines.push(`Date: ${(/* @__PURE__ */ new Date()).toUTCString()}`);
|
|
16651
|
-
lines.push(`Message-ID: <${options.messageId}>`);
|
|
16652
|
-
lines.push("MIME-Version: 1.0");
|
|
16653
|
-
if (options.replyTo) {
|
|
16654
|
-
lines.push(`Reply-To: ${options.replyTo}`);
|
|
16655
|
-
}
|
|
16656
|
-
if (options.headers) {
|
|
16657
|
-
for (const [key, value] of Object.entries(options.headers)) {
|
|
16658
|
-
lines.push(`${key}: ${value}`);
|
|
16659
|
-
}
|
|
16660
|
-
}
|
|
16661
|
-
if (hasAttachments) {
|
|
16662
|
-
lines.push(`Content-Type: multipart/mixed; boundary="${boundary}"`);
|
|
16663
|
-
lines.push("");
|
|
16664
|
-
lines.push(`--${boundary}`);
|
|
16665
|
-
if (hasTextAlt) {
|
|
16666
|
-
lines.push(`Content-Type: multipart/alternative; boundary="${altBoundary}"`);
|
|
16667
|
-
lines.push("");
|
|
16668
|
-
lines.push(`--${altBoundary}`);
|
|
16669
|
-
lines.push("Content-Type: text/plain; charset=UTF-8");
|
|
16670
|
-
lines.push("Content-Transfer-Encoding: 7bit");
|
|
16671
|
-
lines.push("");
|
|
16672
|
-
lines.push(options.text);
|
|
16673
|
-
lines.push("");
|
|
16674
|
-
lines.push(`--${altBoundary}`);
|
|
16675
|
-
lines.push("Content-Type: text/html; charset=UTF-8");
|
|
16676
|
-
lines.push("Content-Transfer-Encoding: 7bit");
|
|
16677
|
-
lines.push("");
|
|
16678
|
-
lines.push(options.body);
|
|
16679
|
-
lines.push("");
|
|
16680
|
-
lines.push(`--${altBoundary}--`);
|
|
16681
|
-
} else {
|
|
16682
|
-
const contentType = options.html ? "text/html" : "text/plain";
|
|
16683
|
-
lines.push(`Content-Type: ${contentType}; charset=UTF-8`);
|
|
16684
|
-
lines.push("Content-Transfer-Encoding: 7bit");
|
|
16685
|
-
lines.push("");
|
|
16686
|
-
lines.push(options.body);
|
|
16687
|
-
}
|
|
16688
|
-
for (const filePath of options.attachments) {
|
|
16689
|
-
const fileName = basename2(filePath);
|
|
16690
|
-
const fileData = readFileSync8(filePath);
|
|
16691
|
-
const base64Data = fileData.toString("base64");
|
|
16692
|
-
lines.push("");
|
|
16693
|
-
lines.push(`--${boundary}`);
|
|
16694
|
-
lines.push(`Content-Type: application/octet-stream; name="${fileName}"`);
|
|
16695
|
-
lines.push("Content-Transfer-Encoding: base64");
|
|
16696
|
-
lines.push(`Content-Disposition: attachment; filename="${fileName}"`);
|
|
16697
|
-
lines.push("");
|
|
16698
|
-
for (let i = 0; i < base64Data.length; i += 76) {
|
|
16699
|
-
lines.push(base64Data.substring(i, i + 76));
|
|
16700
|
-
}
|
|
16701
|
-
}
|
|
16702
|
-
lines.push("");
|
|
16703
|
-
lines.push(`--${boundary}--`);
|
|
16704
|
-
} else if (hasTextAlt) {
|
|
16705
|
-
lines.push(`Content-Type: multipart/alternative; boundary="${altBoundary}"`);
|
|
16706
|
-
lines.push("");
|
|
16707
|
-
lines.push(`--${altBoundary}`);
|
|
16708
|
-
lines.push("Content-Type: text/plain; charset=UTF-8");
|
|
16709
|
-
lines.push("Content-Transfer-Encoding: 7bit");
|
|
16710
|
-
lines.push("");
|
|
16711
|
-
lines.push(options.text);
|
|
16712
|
-
lines.push("");
|
|
16713
|
-
lines.push(`--${altBoundary}`);
|
|
16714
|
-
lines.push("Content-Type: text/html; charset=UTF-8");
|
|
16715
|
-
lines.push("Content-Transfer-Encoding: 7bit");
|
|
16716
|
-
lines.push("");
|
|
16717
|
-
lines.push(options.body);
|
|
16718
|
-
lines.push("");
|
|
16719
|
-
lines.push(`--${altBoundary}--`);
|
|
16720
|
-
} else {
|
|
16721
|
-
const contentType = options.html ? "text/html" : "text/plain";
|
|
16722
|
-
lines.push(`Content-Type: ${contentType}; charset=UTF-8`);
|
|
16723
|
-
lines.push("");
|
|
16724
|
-
lines.push(options.body);
|
|
16725
|
-
}
|
|
16726
|
-
return lines.join("\r\n");
|
|
16727
|
-
}
|
|
16728
|
-
function imapQuote(s) {
|
|
16729
|
-
if (/^[a-zA-Z0-9_./-]+$/.test(s)) return s;
|
|
16730
|
-
return '"' + s.replace(/\\/g, "\\\\").replace(/"/g, '\\"') + '"';
|
|
16731
|
-
}
|
|
16732
|
-
function imapReadLine(socket) {
|
|
16733
|
-
return new Promise((resolve21, reject) => {
|
|
16734
|
-
let buffer = "";
|
|
16735
|
-
const onData = (chunk) => {
|
|
16736
|
-
buffer += chunk.toString("utf-8");
|
|
16737
|
-
const nlIndex = buffer.indexOf("\r\n");
|
|
16738
|
-
if (nlIndex !== -1) {
|
|
16739
|
-
socket.removeListener("data", onData);
|
|
16740
|
-
socket.removeListener("error", onError);
|
|
16741
|
-
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";
|
|
16742
16737
|
}
|
|
16743
|
-
|
|
16744
|
-
|
|
16745
|
-
|
|
16746
|
-
|
|
16747
|
-
|
|
16748
|
-
|
|
16749
|
-
|
|
16750
|
-
});
|
|
16751
|
-
}
|
|
16752
|
-
function imapCommand(socket, command) {
|
|
16753
|
-
return new Promise((resolve21, reject) => {
|
|
16754
|
-
imapTagCounter++;
|
|
16755
|
-
const tag = `T${imapTagCounter}`;
|
|
16756
|
-
const fullCommand = `${tag} ${command}\r
|
|
16757
|
-
`;
|
|
16758
|
-
let buffer = "";
|
|
16759
|
-
const onData = (chunk) => {
|
|
16760
|
-
buffer += chunk.toString("utf-8");
|
|
16761
|
-
if (buffer.includes(`${tag} OK`)) {
|
|
16762
|
-
socket.removeListener("data", onData);
|
|
16763
|
-
socket.removeListener("error", onError);
|
|
16764
|
-
resolve21(buffer);
|
|
16765
|
-
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;
|
|
16766
16745
|
}
|
|
16767
|
-
|
|
16768
|
-
|
|
16769
|
-
|
|
16770
|
-
|
|
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 };
|
|
16771
16786
|
}
|
|
16772
|
-
|
|
16773
|
-
|
|
16774
|
-
|
|
16775
|
-
|
|
16776
|
-
|
|
16777
|
-
|
|
16778
|
-
|
|
16779
|
-
|
|
16780
|
-
|
|
16781
|
-
}
|
|
16782
|
-
|
|
16783
|
-
const e = err instanceof Error ? err : new Error(String(err));
|
|
16784
|
-
Log.error(`Messenger IMAP ${method}() failed: ${e.name}: ${e.message}`);
|
|
16785
|
-
if (e instanceof MessengerConnectionError) return e;
|
|
16786
|
-
return new MessengerConnectionError(`IMAP ${method} failed: ${e.message}`);
|
|
16787
|
-
}
|
|
16788
|
-
function parseSearchResponse(response) {
|
|
16789
|
-
const match = response.match(/\* SEARCH (.+)/);
|
|
16790
|
-
if (!match) return [];
|
|
16791
|
-
return match[1].trim().split(/\s+/).filter((s) => /^\d+$/.test(s));
|
|
16792
|
-
}
|
|
16793
|
-
function parseHeaderResponse(uid, response) {
|
|
16794
|
-
const headers = {};
|
|
16795
|
-
const headerBlock = response.match(/\r\n([\s\S]*?)\r\n\)/);
|
|
16796
|
-
if (headerBlock) {
|
|
16797
|
-
const lines = headerBlock[1].split(/\r\n/);
|
|
16798
|
-
let currentKey = "";
|
|
16799
|
-
for (const line of lines) {
|
|
16800
|
-
if (/^\s/.test(line) && currentKey) {
|
|
16801
|
-
headers[currentKey] += " " + line.trim();
|
|
16802
|
-
} else {
|
|
16803
|
-
const colonIdx = line.indexOf(":");
|
|
16804
|
-
if (colonIdx > 0) {
|
|
16805
|
-
currentKey = line.substring(0, colonIdx).trim().toLowerCase();
|
|
16806
|
-
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 [];
|
|
16807
16798
|
}
|
|
16808
|
-
|
|
16809
|
-
|
|
16810
|
-
|
|
16811
|
-
|
|
16812
|
-
|
|
16813
|
-
|
|
16814
|
-
|
|
16815
|
-
from: headers["from"] ?? "",
|
|
16816
|
-
to: headers["to"] ?? "",
|
|
16817
|
-
date: headers["date"] ?? "",
|
|
16818
|
-
snippet: "",
|
|
16819
|
-
seen
|
|
16820
|
-
};
|
|
16821
|
-
}
|
|
16822
|
-
function emptyFullMessage(uid) {
|
|
16823
|
-
return { uid, subject: "", from: "", to: "", cc: "", date: "", bodyText: "", bodyHtml: "", headers: {} };
|
|
16824
|
-
}
|
|
16825
|
-
function parseFullMessage(uid, response) {
|
|
16826
|
-
const bodyMatch = response.match(/\{(\d+)\}\r\n([\s\S]*)/);
|
|
16827
|
-
const rawMessage = bodyMatch ? bodyMatch[2] : response;
|
|
16828
|
-
const headerEnd = rawMessage.indexOf("\r\n\r\n");
|
|
16829
|
-
const headerSection = headerEnd > 0 ? rawMessage.substring(0, headerEnd) : rawMessage;
|
|
16830
|
-
const bodySection = headerEnd > 0 ? rawMessage.substring(headerEnd + 4) : "";
|
|
16831
|
-
const headers = {};
|
|
16832
|
-
const headerLines = headerSection.split(/\r\n/);
|
|
16833
|
-
let currentKey = "";
|
|
16834
|
-
for (const line of headerLines) {
|
|
16835
|
-
if (/^\s/.test(line) && currentKey) {
|
|
16836
|
-
headers[currentKey] += " " + line.trim();
|
|
16837
|
-
} else {
|
|
16838
|
-
const colonIdx = line.indexOf(":");
|
|
16839
|
-
if (colonIdx > 0) {
|
|
16840
|
-
currentKey = line.substring(0, colonIdx).trim().toLowerCase();
|
|
16841
|
-
headers[currentKey] = line.substring(colonIdx + 1).trim();
|
|
16842
|
-
}
|
|
16843
|
-
}
|
|
16844
|
-
}
|
|
16845
|
-
const contentType = headers["content-type"] ?? "text/plain";
|
|
16846
|
-
let bodyText = "";
|
|
16847
|
-
let bodyHtml = "";
|
|
16848
|
-
if (contentType.includes("multipart")) {
|
|
16849
|
-
const boundaryMatch = contentType.match(/boundary="?([^";\s]+)"?/);
|
|
16850
|
-
if (boundaryMatch) {
|
|
16851
|
-
const boundary = boundaryMatch[1];
|
|
16852
|
-
const parts = bodySection.split("--" + boundary);
|
|
16853
|
-
for (const part of parts) {
|
|
16854
|
-
if (part.trim() === "" || part.trim() === "--") continue;
|
|
16855
|
-
const partHeaderEnd = part.indexOf("\r\n\r\n");
|
|
16856
|
-
const partHeaders = partHeaderEnd > 0 ? part.substring(0, partHeaderEnd).toLowerCase() : "";
|
|
16857
|
-
const partBody = partHeaderEnd > 0 ? part.substring(partHeaderEnd + 4).trim() : "";
|
|
16858
|
-
if (partHeaders.includes("text/html")) {
|
|
16859
|
-
bodyHtml = partBody;
|
|
16860
|
-
} else if (partHeaders.includes("text/plain")) {
|
|
16861
|
-
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
|
+
}
|
|
16862
16806
|
}
|
|
16807
|
+
return results;
|
|
16863
16808
|
}
|
|
16864
|
-
|
|
16865
|
-
|
|
16866
|
-
|
|
16867
|
-
|
|
16868
|
-
|
|
16869
|
-
|
|
16870
|
-
|
|
16871
|
-
|
|
16872
|
-
|
|
16873
|
-
|
|
16874
|
-
|
|
16875
|
-
|
|
16876
|
-
|
|
16877
|
-
|
|
16878
|
-
|
|
16879
|
-
|
|
16880
|
-
|
|
16881
|
-
headers
|
|
16882
|
-
};
|
|
16883
|
-
}
|
|
16884
|
-
var MessengerConnectionError, Messenger, imapTagCounter;
|
|
16885
|
-
var init_messenger = __esm({
|
|
16886
|
-
"src/messenger.ts"() {
|
|
16887
|
-
"use strict";
|
|
16888
|
-
init_dotenv();
|
|
16889
|
-
init_logger();
|
|
16890
|
-
MessengerConnectionError = class extends Error {
|
|
16891
|
-
constructor(message) {
|
|
16892
|
-
super(message);
|
|
16893
|
-
this.name = "MessengerConnectionError";
|
|
16894
|
-
}
|
|
16895
|
-
};
|
|
16896
|
-
Messenger = class {
|
|
16897
|
-
host;
|
|
16898
|
-
port;
|
|
16899
|
-
username;
|
|
16900
|
-
password;
|
|
16901
|
-
fromAddress;
|
|
16902
|
-
fromName;
|
|
16903
|
-
encryption;
|
|
16904
|
-
useTls;
|
|
16905
|
-
imapHost;
|
|
16906
|
-
imapPort;
|
|
16907
|
-
imapUser;
|
|
16908
|
-
imapPass;
|
|
16909
|
-
imapEncryption;
|
|
16910
|
-
constructor(options) {
|
|
16911
|
-
this.host = options?.host ?? process.env.TINA4_MAIL_HOST ?? "localhost";
|
|
16912
|
-
this.port = options?.port ?? parseInt(process.env.TINA4_MAIL_PORT ?? "587", 10);
|
|
16913
|
-
this.username = options?.username ?? process.env.TINA4_MAIL_USERNAME ?? "";
|
|
16914
|
-
this.password = options?.password ?? process.env.TINA4_MAIL_PASSWORD ?? "";
|
|
16915
|
-
this.fromAddress = options?.fromAddress ?? process.env.TINA4_MAIL_FROM ?? (this.username || "noreply@localhost");
|
|
16916
|
-
this.fromName = options?.fromName ?? process.env.TINA4_MAIL_FROM_NAME ?? "";
|
|
16917
|
-
const envEncryption = options?.encryption ?? process.env.TINA4_MAIL_ENCRYPTION;
|
|
16918
|
-
if (envEncryption) {
|
|
16919
|
-
this.encryption = envEncryption.toLowerCase();
|
|
16920
|
-
} else if (options?.useTls !== void 0) {
|
|
16921
|
-
this.encryption = options.useTls ? "tls" : "none";
|
|
16922
|
-
} else {
|
|
16923
|
-
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
|
+
}
|
|
16924
16826
|
}
|
|
16925
|
-
|
|
16926
|
-
this.imapHost = options?.imapHost ?? process.env.TINA4_MAIL_IMAP_HOST ?? "";
|
|
16927
|
-
this.imapPort = options?.imapPort ?? parseInt(process.env.TINA4_MAIL_IMAP_PORT ?? "993", 10);
|
|
16928
|
-
this.imapUser = options?.imapUser ?? process.env.TINA4_MAIL_IMAP_USERNAME ?? this.username;
|
|
16929
|
-
this.imapPass = options?.imapPass ?? process.env.TINA4_MAIL_IMAP_PASSWORD ?? this.password;
|
|
16930
|
-
this.imapEncryption = (options?.imapEncryption ?? process.env.TINA4_MAIL_IMAP_ENCRYPTION ?? "tls").toLowerCase();
|
|
16827
|
+
return null;
|
|
16931
16828
|
}
|
|
16932
16829
|
/**
|
|
16933
|
-
*
|
|
16934
|
-
* Returns one of "tls", "starttls", "none", "ssl".
|
|
16830
|
+
* Count unread messages in the inbox.
|
|
16935
16831
|
*/
|
|
16936
|
-
|
|
16937
|
-
|
|
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;
|
|
16938
16847
|
}
|
|
16939
16848
|
/**
|
|
16940
|
-
*
|
|
16941
|
-
*/
|
|
16942
|
-
async send(to, subject, body, html = false, text, cc, bcc, replyTo, attachments, headers) {
|
|
16943
|
-
const options = { to, subject, body, html, text, cc, bcc, replyTo, attachments, headers };
|
|
16944
|
-
const toList = Array.isArray(options.to) ? options.to : [options.to];
|
|
16945
|
-
const ccList = Array.isArray(options.cc) ? options.cc : options.cc ? [options.cc] : [];
|
|
16946
|
-
const bccList = Array.isArray(options.bcc) ? options.bcc : options.bcc ? [options.bcc] : [];
|
|
16947
|
-
const allRecipients = [...toList, ...ccList, ...bccList];
|
|
16948
|
-
const messageId = `${randomUUID2()}@${this.host}`;
|
|
16949
|
-
if (allRecipients.length === 0) {
|
|
16950
|
-
return { success: false, message: "No recipients specified" };
|
|
16951
|
-
}
|
|
16952
|
-
if (!this.fromAddress) {
|
|
16953
|
-
return { success: false, message: "No from address configured" };
|
|
16954
|
-
}
|
|
16955
|
-
try {
|
|
16956
|
-
let socket;
|
|
16957
|
-
if (this.port === 465) {
|
|
16958
|
-
socket = tls.connect({ host: this.host, port: this.port, rejectUnauthorized: tlsRejectUnauthorized() });
|
|
16959
|
-
await new Promise((resolve21, reject) => {
|
|
16960
|
-
socket.once("secureConnect", resolve21);
|
|
16961
|
-
socket.once("error", reject);
|
|
16962
|
-
});
|
|
16963
|
-
} else {
|
|
16964
|
-
socket = net2.createConnection({ host: this.host, port: this.port });
|
|
16965
|
-
await new Promise((resolve21, reject) => {
|
|
16966
|
-
socket.once("connect", resolve21);
|
|
16967
|
-
socket.once("error", reject);
|
|
16968
|
-
});
|
|
16969
|
-
}
|
|
16970
|
-
const greeting = await readResponse(socket);
|
|
16971
|
-
if (greeting.code !== 220) {
|
|
16972
|
-
socket.destroy();
|
|
16973
|
-
return { success: false, message: `SMTP greeting failed: ${greeting.text}` };
|
|
16974
|
-
}
|
|
16975
|
-
const ehlo = await sendCommand(socket, `EHLO ${this.host}`);
|
|
16976
|
-
if (ehlo.code !== 250) {
|
|
16977
|
-
socket.destroy();
|
|
16978
|
-
return { success: false, message: `EHLO failed: ${ehlo.text}` };
|
|
16979
|
-
}
|
|
16980
|
-
if (this.useTls && this.port !== 465 && ehlo.text.includes("STARTTLS")) {
|
|
16981
|
-
const starttls = await sendCommand(socket, "STARTTLS");
|
|
16982
|
-
if (starttls.code !== 220) {
|
|
16983
|
-
socket.destroy();
|
|
16984
|
-
return { success: false, message: `STARTTLS failed: ${starttls.text}` };
|
|
16985
|
-
}
|
|
16986
|
-
const plainSocket = socket;
|
|
16987
|
-
socket = tls.connect(
|
|
16988
|
-
{ socket: plainSocket, host: this.host, rejectUnauthorized: tlsRejectUnauthorized() }
|
|
16989
|
-
);
|
|
16990
|
-
await new Promise((resolve21, reject) => {
|
|
16991
|
-
socket.once("secureConnect", resolve21);
|
|
16992
|
-
socket.once("error", reject);
|
|
16993
|
-
});
|
|
16994
|
-
const ehlo2 = await sendCommand(socket, `EHLO ${this.host}`);
|
|
16995
|
-
if (ehlo2.code !== 250) {
|
|
16996
|
-
socket.destroy();
|
|
16997
|
-
return { success: false, message: `EHLO after STARTTLS failed: ${ehlo2.text}` };
|
|
16998
|
-
}
|
|
16999
|
-
}
|
|
17000
|
-
if (this.username && this.password) {
|
|
17001
|
-
const auth = await sendCommand(socket, "AUTH LOGIN");
|
|
17002
|
-
if (auth.code !== 334) {
|
|
17003
|
-
socket.destroy();
|
|
17004
|
-
return { success: false, message: `AUTH LOGIN failed: ${auth.text}` };
|
|
17005
|
-
}
|
|
17006
|
-
const userResp = await sendCommand(socket, Buffer.from(this.username).toString("base64"));
|
|
17007
|
-
if (userResp.code !== 334) {
|
|
17008
|
-
socket.destroy();
|
|
17009
|
-
return { success: false, message: `AUTH username failed: ${userResp.text}` };
|
|
17010
|
-
}
|
|
17011
|
-
const passResp = await sendCommand(socket, Buffer.from(this.password).toString("base64"));
|
|
17012
|
-
if (passResp.code !== 235) {
|
|
17013
|
-
socket.destroy();
|
|
17014
|
-
return { success: false, message: `AUTH password failed: ${passResp.text}` };
|
|
17015
|
-
}
|
|
17016
|
-
}
|
|
17017
|
-
const mailFrom = await sendCommand(socket, `MAIL FROM:<${this.fromAddress}>`);
|
|
17018
|
-
if (mailFrom.code !== 250) {
|
|
17019
|
-
socket.destroy();
|
|
17020
|
-
return { success: false, message: `MAIL FROM failed: ${mailFrom.text}` };
|
|
17021
|
-
}
|
|
17022
|
-
for (const recipient of allRecipients) {
|
|
17023
|
-
const rcpt = await sendCommand(socket, `RCPT TO:<${recipient}>`);
|
|
17024
|
-
if (rcpt.code !== 250 && rcpt.code !== 251) {
|
|
17025
|
-
socket.destroy();
|
|
17026
|
-
return { success: false, message: `RCPT TO <${recipient}> failed: ${rcpt.text}` };
|
|
17027
|
-
}
|
|
17028
|
-
}
|
|
17029
|
-
const dataCmd = await sendCommand(socket, "DATA");
|
|
17030
|
-
if (dataCmd.code !== 354) {
|
|
17031
|
-
socket.destroy();
|
|
17032
|
-
return { success: false, message: `DATA failed: ${dataCmd.text}` };
|
|
17033
|
-
}
|
|
17034
|
-
const mimeMessage = buildMimeMessage({
|
|
17035
|
-
from: this.fromAddress,
|
|
17036
|
-
fromName: this.fromName,
|
|
17037
|
-
to: toList,
|
|
17038
|
-
cc: ccList,
|
|
17039
|
-
subject: options.subject,
|
|
17040
|
-
body: options.body,
|
|
17041
|
-
html: options.html ?? false,
|
|
17042
|
-
text: options.text,
|
|
17043
|
-
replyTo: options.replyTo,
|
|
17044
|
-
attachments: options.attachments,
|
|
17045
|
-
headers: options.headers,
|
|
17046
|
-
messageId
|
|
17047
|
-
});
|
|
17048
|
-
const endData = await sendCommand(socket, mimeMessage + "\r\n.");
|
|
17049
|
-
if (endData.code !== 250) {
|
|
17050
|
-
socket.destroy();
|
|
17051
|
-
return { success: false, message: `Message delivery failed: ${endData.text}` };
|
|
17052
|
-
}
|
|
17053
|
-
await sendCommand(socket, "QUIT");
|
|
17054
|
-
socket.destroy();
|
|
17055
|
-
return { success: true, message: "Email sent successfully", id: messageId };
|
|
17056
|
-
} catch (err) {
|
|
17057
|
-
const errMsg = err instanceof Error ? err.message : String(err);
|
|
17058
|
-
return { success: false, message: `SMTP error: ${errMsg}` };
|
|
17059
|
-
}
|
|
17060
|
-
}
|
|
17061
|
-
/**
|
|
17062
|
-
* Test the SMTP connection without sending an email.
|
|
17063
|
-
*/
|
|
17064
|
-
async testConnection() {
|
|
17065
|
-
try {
|
|
17066
|
-
let socket;
|
|
17067
|
-
if (this.port === 465) {
|
|
17068
|
-
socket = tls.connect({ host: this.host, port: this.port, rejectUnauthorized: tlsRejectUnauthorized() });
|
|
17069
|
-
await new Promise((resolve21, reject) => {
|
|
17070
|
-
socket.once("secureConnect", resolve21);
|
|
17071
|
-
socket.once("error", reject);
|
|
17072
|
-
});
|
|
17073
|
-
} else {
|
|
17074
|
-
socket = net2.createConnection({ host: this.host, port: this.port });
|
|
17075
|
-
await new Promise((resolve21, reject) => {
|
|
17076
|
-
socket.once("connect", resolve21);
|
|
17077
|
-
socket.once("error", reject);
|
|
17078
|
-
});
|
|
17079
|
-
}
|
|
17080
|
-
const greeting = await readResponse(socket);
|
|
17081
|
-
if (greeting.code !== 220) {
|
|
17082
|
-
socket.destroy();
|
|
17083
|
-
return { success: false, message: `SMTP greeting failed: ${greeting.text}` };
|
|
17084
|
-
}
|
|
17085
|
-
const ehlo = await sendCommand(socket, `EHLO ${this.host}`);
|
|
17086
|
-
if (ehlo.code !== 250) {
|
|
17087
|
-
socket.destroy();
|
|
17088
|
-
return { success: false, message: `EHLO failed: ${ehlo.text}` };
|
|
17089
|
-
}
|
|
17090
|
-
await sendCommand(socket, "QUIT");
|
|
17091
|
-
socket.destroy();
|
|
17092
|
-
return { success: true, message: `Connected to ${this.host}:${this.port}` };
|
|
17093
|
-
} catch (err) {
|
|
17094
|
-
const errMsg = err instanceof Error ? err.message : String(err);
|
|
17095
|
-
return { success: false, message: `Connection failed: ${errMsg}` };
|
|
17096
|
-
}
|
|
17097
|
-
}
|
|
17098
|
-
// ── IMAP (Read) ────────────────────────────────────────────
|
|
17099
|
-
/**
|
|
17100
|
-
* Connect to the IMAP server via raw TCP/TLS.
|
|
17101
|
-
* Returns the socket and reads the greeting.
|
|
17102
|
-
*/
|
|
17103
|
-
async imapConnect() {
|
|
17104
|
-
if (!this.imapHost) {
|
|
17105
|
-
throw new Error("IMAP host not configured (set imapHost or IMAP_HOST env)");
|
|
17106
|
-
}
|
|
17107
|
-
let socket;
|
|
17108
|
-
const useTls = this.imapEncryption === "tls" || this.imapEncryption === "ssl" || this.imapEncryption === "" && this.imapPort === 993;
|
|
17109
|
-
if (useTls) {
|
|
17110
|
-
socket = tls.connect({ host: this.imapHost, port: this.imapPort, rejectUnauthorized: tlsRejectUnauthorized() });
|
|
17111
|
-
await new Promise((resolve21, reject) => {
|
|
17112
|
-
socket.once("secureConnect", resolve21);
|
|
17113
|
-
socket.once("error", reject);
|
|
17114
|
-
});
|
|
17115
|
-
} else {
|
|
17116
|
-
socket = net2.createConnection({ host: this.imapHost, port: this.imapPort });
|
|
17117
|
-
await new Promise((resolve21, reject) => {
|
|
17118
|
-
socket.once("connect", resolve21);
|
|
17119
|
-
socket.once("error", reject);
|
|
17120
|
-
});
|
|
17121
|
-
}
|
|
17122
|
-
await imapReadLine(socket);
|
|
17123
|
-
if (this.imapUser && this.imapPass) {
|
|
17124
|
-
const loginResp = await imapCommand(socket, `LOGIN ${imapQuote(this.imapUser)} ${imapQuote(this.imapPass)}`);
|
|
17125
|
-
if (!loginResp.includes("OK")) {
|
|
17126
|
-
socket.destroy();
|
|
17127
|
-
throw new Error(`IMAP login failed: ${loginResp}`);
|
|
17128
|
-
}
|
|
17129
|
-
}
|
|
17130
|
-
return socket;
|
|
17131
|
-
}
|
|
17132
|
-
/**
|
|
17133
|
-
* Disconnect from IMAP cleanly.
|
|
17134
|
-
*/
|
|
17135
|
-
async imapDisconnect(socket) {
|
|
17136
|
-
try {
|
|
17137
|
-
await imapCommand(socket, "LOGOUT");
|
|
17138
|
-
} catch {
|
|
17139
|
-
}
|
|
17140
|
-
socket.destroy();
|
|
17141
|
-
}
|
|
17142
|
-
/**
|
|
17143
|
-
* Fetch latest messages from a folder.
|
|
17144
|
-
* Returns list of message summaries.
|
|
17145
|
-
*/
|
|
17146
|
-
async inbox(limit = 20, offset = 0, folder = "INBOX") {
|
|
17147
|
-
let socket;
|
|
17148
|
-
try {
|
|
17149
|
-
socket = await this.imapConnect();
|
|
17150
|
-
} catch (err) {
|
|
17151
|
-
throw imapFail("inbox", err);
|
|
17152
|
-
}
|
|
17153
|
-
try {
|
|
17154
|
-
await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
|
|
17155
|
-
const searchResp = await imapCommand(socket, "SEARCH ALL");
|
|
17156
|
-
const uids = parseSearchResponse(searchResp);
|
|
17157
|
-
if (uids.length === 0) return [];
|
|
17158
|
-
uids.reverse();
|
|
17159
|
-
const selected = uids.slice(offset, offset + limit);
|
|
17160
|
-
if (selected.length === 0) return [];
|
|
17161
|
-
const messages = [];
|
|
17162
|
-
for (const uid of selected) {
|
|
17163
|
-
const fetchResp = await imapCommand(socket, `FETCH ${uid} (FLAGS BODY.PEEK[HEADER.FIELDS (FROM TO SUBJECT DATE)])`);
|
|
17164
|
-
messages.push(parseHeaderResponse(uid, fetchResp));
|
|
17165
|
-
}
|
|
17166
|
-
return messages;
|
|
17167
|
-
} catch (err) {
|
|
17168
|
-
throw imapFail("inbox", err);
|
|
17169
|
-
} finally {
|
|
17170
|
-
await this.imapDisconnect(socket);
|
|
17171
|
-
}
|
|
17172
|
-
}
|
|
17173
|
-
/**
|
|
17174
|
-
* Read a single message by sequence number or UID.
|
|
17175
|
-
*/
|
|
17176
|
-
async read(uid, folder = "INBOX") {
|
|
17177
|
-
let socket;
|
|
17178
|
-
try {
|
|
17179
|
-
socket = await this.imapConnect();
|
|
17180
|
-
} catch (err) {
|
|
17181
|
-
throw imapFail("read", err);
|
|
17182
|
-
}
|
|
17183
|
-
try {
|
|
17184
|
-
await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
|
|
17185
|
-
const fetchResp = await imapCommand(socket, `FETCH ${uid} (FLAGS BODY[])`);
|
|
17186
|
-
if (!/\{\d+\}/.test(fetchResp)) {
|
|
17187
|
-
return emptyFullMessage(uid);
|
|
17188
|
-
}
|
|
17189
|
-
await imapCommand(socket, `STORE ${uid} +FLAGS (\\Seen)`);
|
|
17190
|
-
return parseFullMessage(uid, fetchResp);
|
|
17191
|
-
} catch (err) {
|
|
17192
|
-
throw imapFail("read", err);
|
|
17193
|
-
} finally {
|
|
17194
|
-
await this.imapDisconnect(socket);
|
|
17195
|
-
}
|
|
17196
|
-
}
|
|
17197
|
-
/**
|
|
17198
|
-
* Search messages using IMAP search criteria.
|
|
17199
|
-
*/
|
|
17200
|
-
async search(folder = "INBOX", subject, sender, since, before, unseenOnly = false, limit = 50) {
|
|
17201
|
-
const criteria = ["ALL"];
|
|
17202
|
-
if (subject) criteria.push(`SUBJECT "${subject}"`);
|
|
17203
|
-
if (sender) criteria.push(`FROM "${sender}"`);
|
|
17204
|
-
if (since) criteria.push(`SINCE ${since}`);
|
|
17205
|
-
if (before) criteria.push(`BEFORE ${before}`);
|
|
17206
|
-
if (unseenOnly) criteria.push("UNSEEN");
|
|
17207
|
-
const query = criteria.join(" ");
|
|
17208
|
-
let socket;
|
|
17209
|
-
try {
|
|
17210
|
-
socket = await this.imapConnect();
|
|
17211
|
-
} catch (err) {
|
|
17212
|
-
throw imapFail("search", err);
|
|
17213
|
-
}
|
|
17214
|
-
try {
|
|
17215
|
-
await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
|
|
17216
|
-
const searchResp = await imapCommand(socket, `SEARCH ${query}`);
|
|
17217
|
-
const uids = parseSearchResponse(searchResp);
|
|
17218
|
-
if (uids.length === 0) return [];
|
|
17219
|
-
uids.reverse();
|
|
17220
|
-
const messages = [];
|
|
17221
|
-
for (const uid of uids.slice(0, limit)) {
|
|
17222
|
-
const fetchResp = await imapCommand(socket, `FETCH ${uid} (FLAGS BODY.PEEK[HEADER.FIELDS (FROM TO SUBJECT DATE)])`);
|
|
17223
|
-
messages.push(parseHeaderResponse(uid, fetchResp));
|
|
17224
|
-
}
|
|
17225
|
-
return messages;
|
|
17226
|
-
} catch (err) {
|
|
17227
|
-
throw imapFail("search", err);
|
|
17228
|
-
} finally {
|
|
17229
|
-
await this.imapDisconnect(socket);
|
|
17230
|
-
}
|
|
17231
|
-
}
|
|
17232
|
-
/**
|
|
17233
|
-
* Delete a message by UID.
|
|
17234
|
-
*/
|
|
17235
|
-
async deleteMessage(uid, folder = "INBOX") {
|
|
17236
|
-
const socket = await this.imapConnect();
|
|
17237
|
-
try {
|
|
17238
|
-
await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
|
|
17239
|
-
await imapCommand(socket, `STORE ${uid} +FLAGS (\\Deleted)`);
|
|
17240
|
-
await imapCommand(socket, "EXPUNGE");
|
|
17241
|
-
} finally {
|
|
17242
|
-
await this.imapDisconnect(socket);
|
|
17243
|
-
}
|
|
17244
|
-
}
|
|
17245
|
-
/**
|
|
17246
|
-
* Mark a message as read.
|
|
17247
|
-
*/
|
|
17248
|
-
async markRead(uid, folder = "INBOX") {
|
|
17249
|
-
const socket = await this.imapConnect();
|
|
17250
|
-
try {
|
|
17251
|
-
await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
|
|
17252
|
-
await imapCommand(socket, `STORE ${uid} +FLAGS (\\Seen)`);
|
|
17253
|
-
} finally {
|
|
17254
|
-
await this.imapDisconnect(socket);
|
|
17255
|
-
}
|
|
17256
|
-
}
|
|
17257
|
-
/**
|
|
17258
|
-
* Count unseen messages in a folder.
|
|
17259
|
-
*/
|
|
17260
|
-
async unread(folder = "INBOX") {
|
|
17261
|
-
let socket;
|
|
17262
|
-
try {
|
|
17263
|
-
socket = await this.imapConnect();
|
|
17264
|
-
} catch (err) {
|
|
17265
|
-
throw imapFail("unread", err);
|
|
17266
|
-
}
|
|
17267
|
-
try {
|
|
17268
|
-
await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
|
|
17269
|
-
const searchResp = await imapCommand(socket, "SEARCH UNSEEN");
|
|
17270
|
-
return parseSearchResponse(searchResp).length;
|
|
17271
|
-
} catch (err) {
|
|
17272
|
-
throw imapFail("unread", err);
|
|
17273
|
-
} finally {
|
|
17274
|
-
await this.imapDisconnect(socket);
|
|
17275
|
-
}
|
|
17276
|
-
}
|
|
17277
|
-
/**
|
|
17278
|
-
* List available IMAP folders/mailboxes.
|
|
17279
|
-
*/
|
|
17280
|
-
async folders() {
|
|
17281
|
-
let socket;
|
|
17282
|
-
try {
|
|
17283
|
-
socket = await this.imapConnect();
|
|
17284
|
-
} catch (err) {
|
|
17285
|
-
throw imapFail("folders", err);
|
|
17286
|
-
}
|
|
17287
|
-
try {
|
|
17288
|
-
const resp = await imapCommand(socket, 'LIST "" "*"');
|
|
17289
|
-
const result = [];
|
|
17290
|
-
for (const line of resp.split("\r\n")) {
|
|
17291
|
-
const m = line.match(/\* LIST \([^)]*\) "[^"]*" "?([^"\r\n]+)"?/i);
|
|
17292
|
-
if (m) result.push(m[1]);
|
|
17293
|
-
}
|
|
17294
|
-
return result;
|
|
17295
|
-
} catch (err) {
|
|
17296
|
-
throw imapFail("folders", err);
|
|
17297
|
-
} finally {
|
|
17298
|
-
await this.imapDisconnect(socket);
|
|
17299
|
-
}
|
|
17300
|
-
}
|
|
17301
|
-
/**
|
|
17302
|
-
* Test IMAP connectivity without reading.
|
|
17303
|
-
*/
|
|
17304
|
-
async testImapConnection() {
|
|
17305
|
-
try {
|
|
17306
|
-
const socket = await this.imapConnect();
|
|
17307
|
-
await this.imapDisconnect(socket);
|
|
17308
|
-
return { success: true, message: `Connected to ${this.imapHost}:${this.imapPort}` };
|
|
17309
|
-
} catch (err) {
|
|
17310
|
-
const errMsg = err instanceof Error ? err.message : String(err);
|
|
17311
|
-
return { success: false, message: `IMAP connection failed: ${errMsg}` };
|
|
17312
|
-
}
|
|
17313
|
-
}
|
|
17314
|
-
};
|
|
17315
|
-
imapTagCounter = 0;
|
|
17316
|
-
}
|
|
17317
|
-
});
|
|
17318
|
-
|
|
17319
|
-
// src/devMailbox.ts
|
|
17320
|
-
import { mkdirSync as mkdirSync8, readdirSync as readdirSync6, readFileSync as readFileSync9, writeFileSync as writeFileSync6, unlinkSync as unlinkSync4, existsSync as existsSync10 } from "node:fs";
|
|
17321
|
-
import { join as join12 } from "node:path";
|
|
17322
|
-
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
17323
|
-
function createMessenger() {
|
|
17324
|
-
const debug = process.env.TINA4_DEBUG;
|
|
17325
|
-
const smtpHost = process.env.TINA4_MAIL_HOST;
|
|
17326
|
-
const isProd = !isTruthy(debug) && process.env.NODE_ENV === "production";
|
|
17327
|
-
if (isTruthy(debug)) {
|
|
17328
|
-
return new DevMailbox();
|
|
17329
|
-
}
|
|
17330
|
-
if (!smtpHost) {
|
|
17331
|
-
return new DevMailbox();
|
|
17332
|
-
}
|
|
17333
|
-
if (!isProd) {
|
|
17334
|
-
return new DevMailbox();
|
|
17335
|
-
}
|
|
17336
|
-
return new Messenger();
|
|
17337
|
-
}
|
|
17338
|
-
var DevMailbox;
|
|
17339
|
-
var init_devMailbox = __esm({
|
|
17340
|
-
"src/devMailbox.ts"() {
|
|
17341
|
-
"use strict";
|
|
17342
|
-
init_messenger();
|
|
17343
|
-
init_dotenv();
|
|
17344
|
-
DevMailbox = class {
|
|
17345
|
-
mailboxDir;
|
|
17346
|
-
constructor(mailboxDir) {
|
|
17347
|
-
this.mailboxDir = mailboxDir ?? process.env.TINA4_MAILBOX_DIR ?? "data/mailbox";
|
|
17348
|
-
}
|
|
17349
|
-
/**
|
|
17350
|
-
* Ensure a folder directory exists.
|
|
17351
|
-
*/
|
|
17352
|
-
ensureFolder(folder) {
|
|
17353
|
-
const dir = join12(this.mailboxDir, folder);
|
|
17354
|
-
mkdirSync8(dir, { recursive: true });
|
|
17355
|
-
return dir;
|
|
17356
|
-
}
|
|
17357
|
-
/**
|
|
17358
|
-
* Capture an email to the dev mailbox instead of sending it.
|
|
17359
|
-
*/
|
|
17360
|
-
capture(to, subject, body, html = false, cc = [], bcc = [], replyTo, attachments = [], from) {
|
|
17361
|
-
const id = randomUUID3();
|
|
17362
|
-
const toList = Array.isArray(to) ? to : [to];
|
|
17363
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
17364
|
-
const message = {
|
|
17365
|
-
id,
|
|
17366
|
-
type: "outbox",
|
|
17367
|
-
from: from ?? process.env.TINA4_MAIL_FROM ?? "dev@localhost",
|
|
17368
|
-
to: toList,
|
|
17369
|
-
cc,
|
|
17370
|
-
bcc,
|
|
17371
|
-
reply_to: replyTo,
|
|
17372
|
-
subject,
|
|
17373
|
-
body,
|
|
17374
|
-
html,
|
|
17375
|
-
attachments,
|
|
17376
|
-
date: now,
|
|
17377
|
-
read: false
|
|
17378
|
-
};
|
|
17379
|
-
const outboxDir = this.ensureFolder("outbox");
|
|
17380
|
-
writeFileSync6(join12(outboxDir, `${id}.json`), JSON.stringify(message, null, 2));
|
|
17381
|
-
const inboxDir = this.ensureFolder("inbox");
|
|
17382
|
-
const inboxMessage = { ...message, type: "inbox" };
|
|
17383
|
-
writeFileSync6(join12(inboxDir, `${id}.json`), JSON.stringify(inboxMessage, null, 2));
|
|
17384
|
-
return { success: true, message: "Email captured to dev mailbox", id };
|
|
17385
|
-
}
|
|
17386
|
-
/**
|
|
17387
|
-
* List messages from a folder (default: inbox).
|
|
17388
|
-
*/
|
|
17389
|
-
inbox(limit = 50, offset = 0, folder = "inbox") {
|
|
17390
|
-
const dir = this.ensureFolder(folder);
|
|
17391
|
-
const results = [];
|
|
17392
|
-
let files;
|
|
17393
|
-
try {
|
|
17394
|
-
files = readdirSync6(dir).filter((f) => f.endsWith(".json")).sort().reverse();
|
|
17395
|
-
} catch {
|
|
17396
|
-
return [];
|
|
17397
|
-
}
|
|
17398
|
-
const sliced = files.slice(offset, offset + limit);
|
|
17399
|
-
for (const file of sliced) {
|
|
17400
|
-
try {
|
|
17401
|
-
const msg = JSON.parse(readFileSync9(join12(dir, file), "utf-8"));
|
|
17402
|
-
results.push(msg);
|
|
17403
|
-
} catch {
|
|
17404
|
-
}
|
|
17405
|
-
}
|
|
17406
|
-
return results;
|
|
17407
|
-
}
|
|
17408
|
-
/**
|
|
17409
|
-
* Read a single message by ID. Searches all folders.
|
|
17410
|
-
*/
|
|
17411
|
-
read(msgId) {
|
|
17412
|
-
const folders = ["inbox", "outbox"];
|
|
17413
|
-
for (const folder of folders) {
|
|
17414
|
-
const filePath = join12(this.mailboxDir, folder, `${msgId}.json`);
|
|
17415
|
-
if (existsSync10(filePath)) {
|
|
17416
|
-
try {
|
|
17417
|
-
const msg = JSON.parse(readFileSync9(filePath, "utf-8"));
|
|
17418
|
-
msg.read = true;
|
|
17419
|
-
writeFileSync6(filePath, JSON.stringify(msg, null, 2));
|
|
17420
|
-
return msg;
|
|
17421
|
-
} catch {
|
|
17422
|
-
return null;
|
|
17423
|
-
}
|
|
17424
|
-
}
|
|
17425
|
-
}
|
|
17426
|
-
return null;
|
|
17427
|
-
}
|
|
17428
|
-
/**
|
|
17429
|
-
* Count unread messages in the inbox.
|
|
17430
|
-
*/
|
|
17431
|
-
unreadCount() {
|
|
17432
|
-
const dir = this.ensureFolder("inbox");
|
|
17433
|
-
let count = 0;
|
|
17434
|
-
try {
|
|
17435
|
-
const files = readdirSync6(dir).filter((f) => f.endsWith(".json"));
|
|
17436
|
-
for (const file of files) {
|
|
17437
|
-
try {
|
|
17438
|
-
const msg = JSON.parse(readFileSync9(join12(dir, file), "utf-8"));
|
|
17439
|
-
if (!msg.read) count++;
|
|
17440
|
-
} catch {
|
|
17441
|
-
}
|
|
17442
|
-
}
|
|
17443
|
-
} catch {
|
|
17444
|
-
}
|
|
17445
|
-
return count;
|
|
17446
|
-
}
|
|
17447
|
-
/**
|
|
17448
|
-
* Delete a message by ID. Removes from all folders.
|
|
16849
|
+
* Delete a message by ID. Removes from all folders.
|
|
17449
16850
|
*/
|
|
17450
16851
|
delete(msgId) {
|
|
17451
16852
|
let deleted = false;
|
|
@@ -17507,7 +16908,7 @@ var init_devMailbox = __esm({
|
|
|
17507
16908
|
const subject = subjects[i % subjects.length];
|
|
17508
16909
|
const from = senders[i % senders.length];
|
|
17509
16910
|
const date = new Date(Date.now() - i * 36e5).toISOString();
|
|
17510
|
-
const id =
|
|
16911
|
+
const id = randomUUID2();
|
|
17511
16912
|
const message = {
|
|
17512
16913
|
id,
|
|
17513
16914
|
type: "inbox",
|
|
@@ -18546,7 +17947,7 @@ var init_metrics = __esm({
|
|
|
18546
17947
|
});
|
|
18547
17948
|
|
|
18548
17949
|
// src/feedback.ts
|
|
18549
|
-
import { readFileSync as
|
|
17950
|
+
import { readFileSync as readFileSync10, existsSync as existsSync12 } from "node:fs";
|
|
18550
17951
|
import { dirname as dirname5, join as join14, resolve as resolve8 } from "node:path";
|
|
18551
17952
|
import { fileURLToPath } from "node:url";
|
|
18552
17953
|
function feedbackEnabled() {
|
|
@@ -18687,7 +18088,7 @@ var init_feedback = __esm({
|
|
|
18687
18088
|
handleFeedbackWidgetJs = (_req, res) => {
|
|
18688
18089
|
let body;
|
|
18689
18090
|
if (existsSync12(WIDGET_BUNDLE_PATH)) {
|
|
18690
|
-
body =
|
|
18091
|
+
body = readFileSync10(WIDGET_BUNDLE_PATH);
|
|
18691
18092
|
} else {
|
|
18692
18093
|
body = "console.warn('tina4-feedback-widget bundle not built yet');";
|
|
18693
18094
|
}
|
|
@@ -20395,7 +19796,7 @@ __export(errorOverlay_exports, {
|
|
|
20395
19796
|
renderErrorOverlay: () => renderErrorOverlay,
|
|
20396
19797
|
renderProductionError: () => renderProductionError
|
|
20397
19798
|
});
|
|
20398
|
-
import { readFileSync as
|
|
19799
|
+
import { readFileSync as readFileSync12, statSync as statSync10 } from "node:fs";
|
|
20399
19800
|
import { resolve as resolve10 } from "node:path";
|
|
20400
19801
|
function esc(text) {
|
|
20401
19802
|
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
@@ -20419,7 +19820,7 @@ function parseStack(stack) {
|
|
|
20419
19820
|
function readSourceLines(filename, lineno) {
|
|
20420
19821
|
try {
|
|
20421
19822
|
const absPath = resolve10(filename);
|
|
20422
|
-
const content =
|
|
19823
|
+
const content = readFileSync12(absPath, "utf-8");
|
|
20423
19824
|
const allLines = content.split("\n");
|
|
20424
19825
|
const start2 = Math.max(0, lineno - CONTEXT_LINES - 1);
|
|
20425
19826
|
const end = Math.min(allLines.length, lineno + CONTEXT_LINES);
|
|
@@ -21683,8 +21084,8 @@ __export(context_exports, {
|
|
|
21683
21084
|
fts5Supported: () => fts5Supported
|
|
21684
21085
|
});
|
|
21685
21086
|
import { DatabaseSync as DatabaseSync3 } from "node:sqlite";
|
|
21686
|
-
import { existsSync as existsSync14, mkdirSync as mkdirSync10, readFileSync as
|
|
21687
|
-
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";
|
|
21688
21089
|
function fts5Supported() {
|
|
21689
21090
|
try {
|
|
21690
21091
|
const conn = new DatabaseSync3(":memory:");
|
|
@@ -21707,7 +21108,7 @@ function realResolve(abs) {
|
|
|
21707
21108
|
} catch {
|
|
21708
21109
|
}
|
|
21709
21110
|
try {
|
|
21710
|
-
return join16(realpathSync2(dirname7(abs)),
|
|
21111
|
+
return join16(realpathSync2(dirname7(abs)), basename3(abs));
|
|
21711
21112
|
} catch {
|
|
21712
21113
|
return abs;
|
|
21713
21114
|
}
|
|
@@ -21820,7 +21221,7 @@ var init_context = __esm({
|
|
|
21820
21221
|
// ── indexing ───────────────────────────────────────────────
|
|
21821
21222
|
static chunksFor(label, text) {
|
|
21822
21223
|
const ext = extname6(label).toLowerCase();
|
|
21823
|
-
const special = SPECIAL_FILES.has(
|
|
21224
|
+
const special = SPECIAL_FILES.has(basename3(label).toLowerCase());
|
|
21824
21225
|
if (CODE_EXTS.has(ext) || CONFIG_EXTS.has(ext) || special) {
|
|
21825
21226
|
return chunkCode(text, label);
|
|
21826
21227
|
}
|
|
@@ -21837,7 +21238,7 @@ var init_context = __esm({
|
|
|
21837
21238
|
const stored = label != null ? String(label) : String(file);
|
|
21838
21239
|
let text;
|
|
21839
21240
|
try {
|
|
21840
|
-
text =
|
|
21241
|
+
text = readFileSync13(file, "utf-8");
|
|
21841
21242
|
} catch {
|
|
21842
21243
|
return 0;
|
|
21843
21244
|
}
|
|
@@ -21917,7 +21318,7 @@ var init_context = __esm({
|
|
|
21917
21318
|
if (parts.some((seg) => SKIP_DIRS.has(seg)) || dirParts.some((seg) => seg.startsWith("."))) {
|
|
21918
21319
|
return -1;
|
|
21919
21320
|
}
|
|
21920
|
-
if (!_Context.eligible(
|
|
21321
|
+
if (!_Context.eligible(basename3(rel))) return -1;
|
|
21921
21322
|
const stored = rel;
|
|
21922
21323
|
if (!existsSync14(abs)) {
|
|
21923
21324
|
this.conn.prepare("DELETE FROM chunks WHERE path = ?").run(stored);
|
|
@@ -22009,7 +21410,7 @@ var init_context = __esm({
|
|
|
22009
21410
|
});
|
|
22010
21411
|
|
|
22011
21412
|
// src/websocketBackplane.ts
|
|
22012
|
-
import { randomUUID as
|
|
21413
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
22013
21414
|
function createBackplane(url) {
|
|
22014
21415
|
const backend = (process.env.TINA4_WS_BACKPLANE ?? "").trim().toLowerCase();
|
|
22015
21416
|
switch (backend) {
|
|
@@ -22039,7 +21440,7 @@ function buildEnvelope(src, kind, message, opts = {}) {
|
|
|
22039
21440
|
return envelope;
|
|
22040
21441
|
}
|
|
22041
21442
|
function randomInstanceId() {
|
|
22042
|
-
return
|
|
21443
|
+
return randomUUID3().replace(/-/g, "").slice(0, 16);
|
|
22043
21444
|
}
|
|
22044
21445
|
var RedisBackplane, NATSBackplane, WS_BACKPLANE_CHANNEL, WsBackplaneManager;
|
|
22045
21446
|
var init_websocketBackplane = __esm({
|
|
@@ -22267,7 +21668,7 @@ __export(websocket_exports, {
|
|
|
22267
21668
|
});
|
|
22268
21669
|
import { createServer } from "node:http";
|
|
22269
21670
|
import { createHash as createHash4 } from "node:crypto";
|
|
22270
|
-
import { randomUUID as
|
|
21671
|
+
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
22271
21672
|
function computeAcceptKey(key) {
|
|
22272
21673
|
return createHash4("sha1").update(key + MAGIC_STRING).digest("base64");
|
|
22273
21674
|
}
|
|
@@ -22395,7 +21796,7 @@ function parseFrame(data) {
|
|
|
22395
21796
|
return { fin: !!fin, opcode, payload: Buffer.from(payload), bytesConsumed: offset + payloadLen };
|
|
22396
21797
|
}
|
|
22397
21798
|
function createRouteConnection(socket, path8, headers, params, auth) {
|
|
22398
|
-
const id =
|
|
21799
|
+
const id = randomUUID4().slice(0, 8);
|
|
22399
21800
|
const send = (message) => {
|
|
22400
21801
|
try {
|
|
22401
21802
|
socket.write(buildFrame(OP_TEXT, Buffer.from(message, "utf-8")));
|
|
@@ -22952,7 +22353,7 @@ var init_websocket = __esm({
|
|
|
22952
22353
|
}
|
|
22953
22354
|
responseLines.push("", "");
|
|
22954
22355
|
socket.write(responseLines.join("\r\n"));
|
|
22955
|
-
const clientId =
|
|
22356
|
+
const clientId = randomUUID4().slice(0, 8);
|
|
22956
22357
|
const client = {
|
|
22957
22358
|
id: clientId,
|
|
22958
22359
|
socket,
|
|
@@ -23240,7 +22641,7 @@ var init_websocket = __esm({
|
|
|
23240
22641
|
|
|
23241
22642
|
// src/queueBackends/rabbitmqBackend.ts
|
|
23242
22643
|
import { execFileSync } from "node:child_process";
|
|
23243
|
-
import { randomUUID as
|
|
22644
|
+
import { randomUUID as randomUUID5 } from "node:crypto";
|
|
23244
22645
|
function parseAmqpUrl(url) {
|
|
23245
22646
|
const config = {};
|
|
23246
22647
|
let rest = url.replace(/^amqps:\/\//, "").replace(/^amqp:\/\//, "");
|
|
@@ -23684,7 +23085,7 @@ var init_rabbitmqBackend = __esm({
|
|
|
23684
23085
|
}
|
|
23685
23086
|
}
|
|
23686
23087
|
push(queue, payload, _delay) {
|
|
23687
|
-
const id =
|
|
23088
|
+
const id = randomUUID5();
|
|
23688
23089
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
23689
23090
|
const job = {
|
|
23690
23091
|
id,
|
|
@@ -23723,7 +23124,7 @@ var init_rabbitmqBackend = __esm({
|
|
|
23723
23124
|
|
|
23724
23125
|
// src/queueBackends/kafkaBackend.ts
|
|
23725
23126
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
23726
|
-
import { randomUUID as
|
|
23127
|
+
import { randomUUID as randomUUID6 } from "node:crypto";
|
|
23727
23128
|
function kafkaSecurityConfig(env = process.env) {
|
|
23728
23129
|
const mapping = [
|
|
23729
23130
|
["security.protocol", "SECURITY_PROTOCOL"],
|
|
@@ -23747,7 +23148,7 @@ var init_kafkaBackend = __esm({
|
|
|
23747
23148
|
"use strict";
|
|
23748
23149
|
API_PRODUCE = 0;
|
|
23749
23150
|
API_FETCH = 1;
|
|
23750
|
-
KafkaBackend = class {
|
|
23151
|
+
KafkaBackend = class _KafkaBackend {
|
|
23751
23152
|
brokers;
|
|
23752
23153
|
groupId;
|
|
23753
23154
|
constructor(config) {
|
|
@@ -24109,12 +23510,15 @@ var init_kafkaBackend = __esm({
|
|
|
24109
23510
|
if (errCode === 0) {
|
|
24110
23511
|
finish("__PUBLISHED__", 0);
|
|
24111
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).
|
|
24112
23516
|
process.stderr.write("Produce error code " + errCode);
|
|
24113
|
-
finish("
|
|
23517
|
+
finish("__PRODUCEERROR__" + errCode, 0);
|
|
24114
23518
|
}
|
|
24115
23519
|
} catch (e) {
|
|
24116
23520
|
process.stderr.write("produce parse: " + e.message);
|
|
24117
|
-
finish("
|
|
23521
|
+
finish("__PARSEERROR__produce: " + e.message, 0);
|
|
24118
23522
|
}
|
|
24119
23523
|
return;
|
|
24120
23524
|
} else if (operation === "get") {
|
|
@@ -24128,6 +23532,7 @@ var init_kafkaBackend = __esm({
|
|
|
24128
23532
|
pos += 4; // throttleTimeMs (v1+)
|
|
24129
23533
|
const topicCount = buffer.readInt32BE(pos); pos += 4;
|
|
24130
23534
|
let out = "__EMPTY__";
|
|
23535
|
+
let fatalCode = 0;
|
|
24131
23536
|
for (let t = 0; t < topicCount; t++) {
|
|
24132
23537
|
const tl = buffer.readInt16BE(pos); pos += 2 + tl;
|
|
24133
23538
|
const pc = buffer.readInt32BE(pos); pos += 4;
|
|
@@ -24139,6 +23544,14 @@ var init_kafkaBackend = __esm({
|
|
|
24139
23544
|
const abortedCount = buffer.readInt32BE(pos); pos += 4;
|
|
24140
23545
|
if (abortedCount > 0) pos += abortedCount * 16; // (-1 => none, skip)
|
|
24141
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
|
+
}
|
|
24142
23555
|
if (errCode === 0 && recSetSize > 0) {
|
|
24143
23556
|
const val = firstRecordValue(buffer, pos, pos + recSetSize);
|
|
24144
23557
|
if (val !== null) out = val;
|
|
@@ -24146,21 +23559,33 @@ var init_kafkaBackend = __esm({
|
|
|
24146
23559
|
pos += recSetSize > 0 ? recSetSize : 0;
|
|
24147
23560
|
}
|
|
24148
23561
|
}
|
|
23562
|
+
if (fatalCode !== 0) {
|
|
23563
|
+
process.stderr.write("Fetch error code " + fatalCode);
|
|
23564
|
+
finish("__FETCHERROR__" + fatalCode, 0);
|
|
23565
|
+
return;
|
|
23566
|
+
}
|
|
24149
23567
|
finish(out, 0);
|
|
24150
23568
|
} catch (e) {
|
|
23569
|
+
// A parse failure is NOT an empty queue either \u2014 say so.
|
|
24151
23570
|
process.stderr.write("fetch parse: " + e.message);
|
|
24152
|
-
finish("
|
|
23571
|
+
finish("__PARSEERROR__fetch: " + e.message, 0);
|
|
24153
23572
|
}
|
|
24154
23573
|
return;
|
|
24155
23574
|
}
|
|
24156
23575
|
});
|
|
24157
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.
|
|
24158
23582
|
sock.on("error", (err) => {
|
|
24159
|
-
|
|
24160
|
-
finish("", 1);
|
|
23583
|
+
finish("__TRANSPORTERROR__" + err.message, 0);
|
|
24161
23584
|
});
|
|
24162
23585
|
|
|
24163
|
-
var timer = setTimeout(() => {
|
|
23586
|
+
var timer = setTimeout(() => {
|
|
23587
|
+
finish("__TRANSPORTERROR__timed out after 10s talking to " + host + ":" + port, 0);
|
|
23588
|
+
}, 10000);
|
|
24164
23589
|
`;
|
|
24165
23590
|
try {
|
|
24166
23591
|
const result = execFileSync2(process.execPath, ["-e", script], {
|
|
@@ -24169,12 +23594,51 @@ var init_kafkaBackend = __esm({
|
|
|
24169
23594
|
stdio: ["pipe", "pipe", "pipe"]
|
|
24170
23595
|
});
|
|
24171
23596
|
return result;
|
|
24172
|
-
} catch {
|
|
24173
|
-
|
|
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
|
+
);
|
|
24174
23638
|
}
|
|
24175
23639
|
}
|
|
24176
23640
|
push(queue, payload, _delay) {
|
|
24177
|
-
const id =
|
|
23641
|
+
const id = randomUUID6();
|
|
24178
23642
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
24179
23643
|
const job = {
|
|
24180
23644
|
id,
|
|
@@ -24184,14 +23648,25 @@ var init_kafkaBackend = __esm({
|
|
|
24184
23648
|
attempts: 0,
|
|
24185
23649
|
delayUntil: null
|
|
24186
23650
|
};
|
|
24187
|
-
const
|
|
24188
|
-
|
|
24189
|
-
|
|
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);
|
|
24190
23663
|
}
|
|
24191
|
-
|
|
23664
|
+
_KafkaBackend.assertNoError(result, "produce", queue);
|
|
23665
|
+
throw new Error(`Kafka publish failed for topic ${queue}: ${result || "no response"}`);
|
|
24192
23666
|
}
|
|
24193
23667
|
pop(queue) {
|
|
24194
23668
|
const result = this.execSync("get", queue);
|
|
23669
|
+
_KafkaBackend.assertNoError(result, "fetch", queue);
|
|
24195
23670
|
if (!result || result === "__EMPTY__" || result === "__UNSUPPORTED__") return null;
|
|
24196
23671
|
try {
|
|
24197
23672
|
return JSON.parse(result);
|
|
@@ -24209,7 +23684,7 @@ var init_kafkaBackend = __esm({
|
|
|
24209
23684
|
});
|
|
24210
23685
|
|
|
24211
23686
|
// src/queueBackends/mongoBackend.ts
|
|
24212
|
-
import { randomUUID as
|
|
23687
|
+
import { randomUUID as randomUUID7 } from "node:crypto";
|
|
24213
23688
|
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
24214
23689
|
var MongoBackend2;
|
|
24215
23690
|
var init_mongoBackend = __esm({
|
|
@@ -24522,7 +23997,7 @@ var init_mongoBackend = __esm({
|
|
|
24522
23997
|
}
|
|
24523
23998
|
}
|
|
24524
23999
|
push(queue, payload, delay) {
|
|
24525
|
-
const id =
|
|
24000
|
+
const id = randomUUID7();
|
|
24526
24001
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
24527
24002
|
const job = {
|
|
24528
24003
|
id,
|
|
@@ -24657,9 +24132,9 @@ var init_job = __esm({
|
|
|
24657
24132
|
});
|
|
24658
24133
|
|
|
24659
24134
|
// src/queueBackends/liteBackend.ts
|
|
24660
|
-
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";
|
|
24661
24136
|
import { join as join17 } from "node:path";
|
|
24662
|
-
import { randomUUID as
|
|
24137
|
+
import { randomUUID as randomUUID8 } from "node:crypto";
|
|
24663
24138
|
var LiteBackend;
|
|
24664
24139
|
var init_liteBackend = __esm({
|
|
24665
24140
|
"src/queueBackends/liteBackend.ts"() {
|
|
@@ -24711,7 +24186,7 @@ var init_liteBackend = __esm({
|
|
|
24711
24186
|
}
|
|
24712
24187
|
push(queue, payload, delay, priority) {
|
|
24713
24188
|
const dir = this.ensureDir(queue);
|
|
24714
|
-
const id =
|
|
24189
|
+
const id = randomUUID8();
|
|
24715
24190
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
24716
24191
|
const job = {
|
|
24717
24192
|
id,
|
|
@@ -24747,7 +24222,7 @@ var init_liteBackend = __esm({
|
|
|
24747
24222
|
const filePath = join17(dir, filename);
|
|
24748
24223
|
let job;
|
|
24749
24224
|
try {
|
|
24750
|
-
job = JSON.parse(
|
|
24225
|
+
job = JSON.parse(readFileSync14(filePath, "utf-8"));
|
|
24751
24226
|
} catch {
|
|
24752
24227
|
continue;
|
|
24753
24228
|
}
|
|
@@ -24811,7 +24286,7 @@ var init_liteBackend = __esm({
|
|
|
24811
24286
|
const filePath = join17(reservedDir, filename);
|
|
24812
24287
|
let record;
|
|
24813
24288
|
try {
|
|
24814
|
-
record = JSON.parse(
|
|
24289
|
+
record = JSON.parse(readFileSync14(filePath, "utf-8"));
|
|
24815
24290
|
} catch {
|
|
24816
24291
|
continue;
|
|
24817
24292
|
}
|
|
@@ -24924,7 +24399,7 @@ var init_liteBackend = __esm({
|
|
|
24924
24399
|
let count = 0;
|
|
24925
24400
|
for (const file of files) {
|
|
24926
24401
|
try {
|
|
24927
|
-
const job = JSON.parse(
|
|
24402
|
+
const job = JSON.parse(readFileSync14(join17(scanDir, file), "utf-8"));
|
|
24928
24403
|
if (job.status === status2) count++;
|
|
24929
24404
|
} catch {
|
|
24930
24405
|
}
|
|
@@ -24981,7 +24456,7 @@ var init_liteBackend = __esm({
|
|
|
24981
24456
|
const files = readdirSync10(dir).filter((f) => f.endsWith(".queue-data")).sort();
|
|
24982
24457
|
for (const file of files) {
|
|
24983
24458
|
try {
|
|
24984
|
-
const job = JSON.parse(
|
|
24459
|
+
const job = JSON.parse(readFileSync14(join17(dir, file), "utf-8"));
|
|
24985
24460
|
const attempts = job.attempts || 0;
|
|
24986
24461
|
if (attempts > 0 && attempts < maxRetries) {
|
|
24987
24462
|
results.push(job);
|
|
@@ -25007,7 +24482,7 @@ var init_liteBackend = __esm({
|
|
|
25007
24482
|
const failedDir = join17(this.basePath, q, "failed");
|
|
25008
24483
|
const filePath = join17(failedDir, `${jobId}.queue-data`);
|
|
25009
24484
|
if (existsSync15(filePath)) {
|
|
25010
|
-
const job = JSON.parse(
|
|
24485
|
+
const job = JSON.parse(readFileSync14(filePath, "utf-8"));
|
|
25011
24486
|
job.status = "pending";
|
|
25012
24487
|
job.attempts = (job.attempts || 0) + 1;
|
|
25013
24488
|
job.error = void 0;
|
|
@@ -25031,7 +24506,7 @@ var init_liteBackend = __esm({
|
|
|
25031
24506
|
const files = readdirSync10(failedDir).filter((f) => f.endsWith(".queue-data")).sort();
|
|
25032
24507
|
for (const file of files) {
|
|
25033
24508
|
try {
|
|
25034
|
-
const job = JSON.parse(
|
|
24509
|
+
const job = JSON.parse(readFileSync14(join17(failedDir, file), "utf-8"));
|
|
25035
24510
|
if ((job.attempts || 0) >= maxRetries) {
|
|
25036
24511
|
job.status = "dead";
|
|
25037
24512
|
results.push(job);
|
|
@@ -25065,7 +24540,7 @@ var init_liteBackend = __esm({
|
|
|
25065
24540
|
const files = readdirSync10(dir).filter((f) => f.endsWith(".queue-data"));
|
|
25066
24541
|
for (const file of files) {
|
|
25067
24542
|
try {
|
|
25068
|
-
const job = JSON.parse(
|
|
24543
|
+
const job = JSON.parse(readFileSync14(join17(dir, file), "utf-8"));
|
|
25069
24544
|
if (job.status === status2) {
|
|
25070
24545
|
unlinkSync5(join17(dir, file));
|
|
25071
24546
|
count++;
|
|
@@ -25092,7 +24567,7 @@ var init_liteBackend = __esm({
|
|
|
25092
24567
|
for (const file of files) {
|
|
25093
24568
|
try {
|
|
25094
24569
|
const filePath = join17(failedDir, file);
|
|
25095
|
-
const job = JSON.parse(
|
|
24570
|
+
const job = JSON.parse(readFileSync14(filePath, "utf-8"));
|
|
25096
24571
|
if ((job.attempts || 0) >= maxRetries) {
|
|
25097
24572
|
continue;
|
|
25098
24573
|
}
|
|
@@ -25124,7 +24599,7 @@ var init_liteBackend = __esm({
|
|
|
25124
24599
|
const filePath = join17(dir, file);
|
|
25125
24600
|
let job;
|
|
25126
24601
|
try {
|
|
25127
|
-
job = JSON.parse(
|
|
24602
|
+
job = JSON.parse(readFileSync14(filePath, "utf-8"));
|
|
25128
24603
|
} catch {
|
|
25129
24604
|
continue;
|
|
25130
24605
|
}
|
|
@@ -27614,7 +27089,7 @@ ${end}
|
|
|
27614
27089
|
|
|
27615
27090
|
// src/devAdmin.ts
|
|
27616
27091
|
import { cpus as osCpus } from "node:os";
|
|
27617
|
-
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";
|
|
27618
27093
|
import { join as join21, dirname as dirname9, resolve as resolve15, relative as relative8 } from "node:path";
|
|
27619
27094
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
27620
27095
|
import { timingSafeEqual as timingSafeEqual2 } from "node:crypto";
|
|
@@ -27800,7 +27275,7 @@ function resolveDevEnvVar(key) {
|
|
|
27800
27275
|
if (live !== void 0 && live !== "") return live;
|
|
27801
27276
|
const envPath = join21(process.cwd(), ".env");
|
|
27802
27277
|
if (!existsSync19(envPath)) return "";
|
|
27803
|
-
for (const line of
|
|
27278
|
+
for (const line of readFileSync18(envPath, "utf-8").split("\n")) {
|
|
27804
27279
|
const t = line.trim();
|
|
27805
27280
|
if (!t || t.startsWith("#") || !t.includes("=")) continue;
|
|
27806
27281
|
const eq = t.indexOf("=");
|
|
@@ -27810,7 +27285,7 @@ function resolveDevEnvVar(key) {
|
|
|
27810
27285
|
}
|
|
27811
27286
|
function upsertDevEnvVar(key, value) {
|
|
27812
27287
|
const envPath = join21(process.cwd(), ".env");
|
|
27813
|
-
const lines = existsSync19(envPath) ?
|
|
27288
|
+
const lines = existsSync19(envPath) ? readFileSync18(envPath, "utf-8").split("\n") : [];
|
|
27814
27289
|
let found = false;
|
|
27815
27290
|
const out = [];
|
|
27816
27291
|
for (const line of lines) {
|
|
@@ -27843,7 +27318,7 @@ function parseEnvFile() {
|
|
|
27843
27318
|
const envPath = join21(process.cwd(), ".env");
|
|
27844
27319
|
const result = {};
|
|
27845
27320
|
if (!existsSync19(envPath)) return result;
|
|
27846
|
-
const lines =
|
|
27321
|
+
const lines = readFileSync18(envPath, "utf-8").split("\n");
|
|
27847
27322
|
for (const line of lines) {
|
|
27848
27323
|
const trimmed = line.trim();
|
|
27849
27324
|
if (!trimmed || trimmed.startsWith("#") || !trimmed.includes("=")) continue;
|
|
@@ -28200,7 +27675,7 @@ var init_devAdmin = __esm({
|
|
|
28200
27675
|
for (const rel of ["../../../package.json", "../../package.json"]) {
|
|
28201
27676
|
const p = resolve15(__dirname2, rel);
|
|
28202
27677
|
if (existsSync19(p)) {
|
|
28203
|
-
const pkg = JSON.parse(
|
|
27678
|
+
const pkg = JSON.parse(readFileSync18(p, "utf-8"));
|
|
28204
27679
|
if (pkg.version) return pkg.version;
|
|
28205
27680
|
}
|
|
28206
27681
|
}
|
|
@@ -28858,7 +28333,7 @@ var init_devAdmin = __esm({
|
|
|
28858
28333
|
for (const filename of readdirSync14(queueDir).sort()) {
|
|
28859
28334
|
if (!filename.endsWith(".queue-data")) continue;
|
|
28860
28335
|
try {
|
|
28861
|
-
const job = JSON.parse(
|
|
28336
|
+
const job = JSON.parse(readFileSync18(join21(queueDir, filename), "utf-8"));
|
|
28862
28337
|
jobs.push(mapQueueJob(job, topic, "pending"));
|
|
28863
28338
|
} catch {
|
|
28864
28339
|
}
|
|
@@ -29344,7 +28819,7 @@ var init_devAdmin = __esm({
|
|
|
29344
28819
|
}
|
|
29345
28820
|
try {
|
|
29346
28821
|
const envPath = join21(process.cwd(), ".env");
|
|
29347
|
-
const lines = existsSync19(envPath) ?
|
|
28822
|
+
const lines = existsSync19(envPath) ? readFileSync18(envPath, "utf-8").split("\n") : [];
|
|
29348
28823
|
const keysFound = { TINA4_DATABASE_URL: false, TINA4_DATABASE_USERNAME: false, TINA4_DATABASE_PASSWORD: false };
|
|
29349
28824
|
const newLines = [];
|
|
29350
28825
|
for (const line of lines) {
|
|
@@ -29390,7 +28865,7 @@ var init_devAdmin = __esm({
|
|
|
29390
28865
|
const metaFile = join21(entryPath, "meta.json");
|
|
29391
28866
|
if (statSync15(entryPath).isDirectory() && existsSync19(metaFile)) {
|
|
29392
28867
|
try {
|
|
29393
|
-
const meta = JSON.parse(
|
|
28868
|
+
const meta = JSON.parse(readFileSync18(metaFile, "utf-8"));
|
|
29394
28869
|
meta.id = entry;
|
|
29395
28870
|
const srcDir = join21(entryPath, "src");
|
|
29396
28871
|
if (existsSync19(srcDir)) {
|
|
@@ -29605,7 +29080,7 @@ var init_devAdmin = __esm({
|
|
|
29605
29080
|
return;
|
|
29606
29081
|
}
|
|
29607
29082
|
try {
|
|
29608
|
-
const content =
|
|
29083
|
+
const content = readFileSync18(target, "utf-8");
|
|
29609
29084
|
const path8 = relative8(root, target);
|
|
29610
29085
|
res.json({ path: path8, content, language: devAdminLanguage(path8), bytes: Buffer.byteLength(content, "utf-8") });
|
|
29611
29086
|
} catch (e) {
|
|
@@ -29647,7 +29122,7 @@ var init_devAdmin = __esm({
|
|
|
29647
29122
|
return;
|
|
29648
29123
|
}
|
|
29649
29124
|
try {
|
|
29650
|
-
const buf =
|
|
29125
|
+
const buf = readFileSync18(target);
|
|
29651
29126
|
const ext = target.slice(target.lastIndexOf(".") + 1).toLowerCase();
|
|
29652
29127
|
const mime = {
|
|
29653
29128
|
js: "application/javascript",
|
|
@@ -30075,7 +29550,7 @@ var init_devAdmin = __esm({
|
|
|
30075
29550
|
});
|
|
30076
29551
|
|
|
30077
29552
|
// src/i18n.ts
|
|
30078
|
-
import { readFileSync as
|
|
29553
|
+
import { readFileSync as readFileSync19, readdirSync as readdirSync15, existsSync as existsSync20 } from "node:fs";
|
|
30079
29554
|
import { join as join22, resolve as resolve16 } from "node:path";
|
|
30080
29555
|
var I18n;
|
|
30081
29556
|
var init_i18n = __esm({
|
|
@@ -30172,7 +29647,7 @@ var init_i18n = __esm({
|
|
|
30172
29647
|
const filePath = join22(this._localeDir, `${locale}.json`);
|
|
30173
29648
|
if (existsSync20(filePath)) {
|
|
30174
29649
|
try {
|
|
30175
|
-
const raw =
|
|
29650
|
+
const raw = readFileSync19(filePath, "utf-8");
|
|
30176
29651
|
const data = JSON.parse(raw);
|
|
30177
29652
|
this._translations.set(locale, _I18n._flatten(data));
|
|
30178
29653
|
return;
|
|
@@ -30185,7 +29660,7 @@ var init_i18n = __esm({
|
|
|
30185
29660
|
const yamlPath = join22(this._localeDir, `${locale}${ext}`);
|
|
30186
29661
|
if (existsSync20(yamlPath)) {
|
|
30187
29662
|
try {
|
|
30188
|
-
const raw =
|
|
29663
|
+
const raw = readFileSync19(yamlPath, "utf-8");
|
|
30189
29664
|
const data = _I18n._parseSimpleYaml(raw);
|
|
30190
29665
|
this._translations.set(locale, _I18n._flatten(data));
|
|
30191
29666
|
return;
|
|
@@ -30853,6 +30328,36 @@ var init_docsAutoDiscovery = __esm({
|
|
|
30853
30328
|
}
|
|
30854
30329
|
});
|
|
30855
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
|
+
|
|
30856
30361
|
// src/sessionHandlers/respClient.ts
|
|
30857
30362
|
import { execFileSync as execFileSync4 } from "node:child_process";
|
|
30858
30363
|
function respCommandSync(target, args, label = "Redis") {
|
|
@@ -30972,7 +30477,7 @@ function respCommandSync(target, args, label = "Redis") {
|
|
|
30972
30477
|
stdio: ["pipe", "pipe", "pipe"]
|
|
30973
30478
|
});
|
|
30974
30479
|
} catch (err) {
|
|
30975
|
-
throw
|
|
30480
|
+
throw childFailureError(label, err);
|
|
30976
30481
|
}
|
|
30977
30482
|
if (result === "__NULL__") return "";
|
|
30978
30483
|
if (result.startsWith("__ERR__")) {
|
|
@@ -30983,6 +30488,7 @@ function respCommandSync(target, args, label = "Redis") {
|
|
|
30983
30488
|
var init_respClient = __esm({
|
|
30984
30489
|
"src/sessionHandlers/respClient.ts"() {
|
|
30985
30490
|
"use strict";
|
|
30491
|
+
init_childError();
|
|
30986
30492
|
}
|
|
30987
30493
|
});
|
|
30988
30494
|
|
|
@@ -31001,6 +30507,7 @@ var moduleRequire, RedisNpmSessionHandler;
|
|
|
31001
30507
|
var init_redisHandler = __esm({
|
|
31002
30508
|
"src/sessionHandlers/redisHandler.ts"() {
|
|
31003
30509
|
"use strict";
|
|
30510
|
+
init_childError();
|
|
31004
30511
|
init_respClient();
|
|
31005
30512
|
moduleRequire = createRequire8(import.meta.url);
|
|
31006
30513
|
RedisNpmSessionHandler = class {
|
|
@@ -31061,9 +30568,17 @@ var init_redisHandler = __esm({
|
|
|
31061
30568
|
(async () => {
|
|
31062
30569
|
try {
|
|
31063
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.
|
|
31064
30579
|
const clientOpts = useUrl
|
|
31065
|
-
? { url }
|
|
31066
|
-
: { socket: { host, port }, password: password || undefined, database: db };
|
|
30580
|
+
? { url, socket: { reconnectStrategy: false } }
|
|
30581
|
+
: { socket: { host, port, reconnectStrategy: false }, password: password || undefined, database: db };
|
|
31067
30582
|
const client = redis.createClient(clientOpts);
|
|
31068
30583
|
client.on("error", () => {});
|
|
31069
30584
|
await client.connect();
|
|
@@ -31077,8 +30592,10 @@ var init_redisHandler = __esm({
|
|
|
31077
30592
|
const out = (result === null || result === undefined) ? "__NULL__" : String(result);
|
|
31078
30593
|
process.stdout.write(out, () => process.exit(0));
|
|
31079
30594
|
} catch (err) {
|
|
31080
|
-
|
|
31081
|
-
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));
|
|
31082
30599
|
}
|
|
31083
30600
|
})();
|
|
31084
30601
|
`;
|
|
@@ -31090,7 +30607,7 @@ var init_redisHandler = __esm({
|
|
|
31090
30607
|
stdio: ["pipe", "pipe", "pipe"]
|
|
31091
30608
|
});
|
|
31092
30609
|
} catch (err) {
|
|
31093
|
-
throw
|
|
30610
|
+
throw childFailureError("Redis", err);
|
|
31094
30611
|
}
|
|
31095
30612
|
if (result === "__NULL__") return "";
|
|
31096
30613
|
return result;
|
|
@@ -31222,8 +30739,10 @@ function mongoCommandSync(target, command, args, label = "MongoDB") {
|
|
|
31222
30739
|
process.stdout.write(out, () => process.exit(0));
|
|
31223
30740
|
} catch (err) {
|
|
31224
30741
|
try { if (client) await client.close(); } catch (e) {}
|
|
31225
|
-
|
|
31226
|
-
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));
|
|
31227
30746
|
}
|
|
31228
30747
|
})();
|
|
31229
30748
|
} else {
|
|
@@ -31364,12 +30883,13 @@ function mongoCommandSync(target, command, args, label = "MongoDB") {
|
|
|
31364
30883
|
stdio: ["pipe", "pipe", "pipe"]
|
|
31365
30884
|
});
|
|
31366
30885
|
} catch (err) {
|
|
31367
|
-
throw
|
|
30886
|
+
throw childFailureError(label, err);
|
|
31368
30887
|
}
|
|
31369
30888
|
}
|
|
31370
30889
|
var init_mongoClient = __esm({
|
|
31371
30890
|
"src/sessionHandlers/mongoClient.ts"() {
|
|
31372
30891
|
"use strict";
|
|
30892
|
+
init_childError();
|
|
31373
30893
|
}
|
|
31374
30894
|
});
|
|
31375
30895
|
|
|
@@ -31522,7 +31042,7 @@ __export(session_exports, {
|
|
|
31522
31042
|
sessionCookieName: () => sessionCookieName
|
|
31523
31043
|
});
|
|
31524
31044
|
import { randomBytes as randomBytes6 } from "node:crypto";
|
|
31525
|
-
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";
|
|
31526
31046
|
import { join as join24 } from "node:path";
|
|
31527
31047
|
function isSecureScheme(forwardedProto, socketEncrypted) {
|
|
31528
31048
|
const forwarded = (forwardedProto ?? "").trim();
|
|
@@ -31575,7 +31095,7 @@ var init_session = __esm({
|
|
|
31575
31095
|
const filePath = this.filePath(sessionId);
|
|
31576
31096
|
try {
|
|
31577
31097
|
if (!existsSync22(filePath)) return null;
|
|
31578
|
-
const raw =
|
|
31098
|
+
const raw = readFileSync21(filePath, "utf-8");
|
|
31579
31099
|
const wrapper = JSON.parse(raw);
|
|
31580
31100
|
if (wrapper._expires && wrapper._expires > 0 && Date.now() / 1e3 > wrapper._expires) {
|
|
31581
31101
|
try {
|
|
@@ -31611,7 +31131,7 @@ var init_session = __esm({
|
|
|
31611
31131
|
if (!file.endsWith(".json")) continue;
|
|
31612
31132
|
const fullPath = join24(this.storagePath, file);
|
|
31613
31133
|
try {
|
|
31614
|
-
const raw =
|
|
31134
|
+
const raw = readFileSync21(fullPath, "utf-8");
|
|
31615
31135
|
const wrapper = JSON.parse(raw);
|
|
31616
31136
|
if (wrapper._expires && wrapper._expires > 0 && now > wrapper._expires) {
|
|
31617
31137
|
unlinkSync7(fullPath);
|
|
@@ -32128,7 +31648,7 @@ var init_events = __esm({
|
|
|
32128
31648
|
// src/server.ts
|
|
32129
31649
|
import { createServer as createServer2 } from "node:http";
|
|
32130
31650
|
import { resolve as resolve18, dirname as dirname10, join as join25, relative as relative9 } from "node:path";
|
|
32131
|
-
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";
|
|
32132
31652
|
import { isatty } from "node:tty";
|
|
32133
31653
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
32134
31654
|
import { execFileSync as execFileSync7, exec } from "node:child_process";
|
|
@@ -32190,7 +31710,7 @@ async function autoMigrateOnStartup(migrationDir = "migrations", base = process.
|
|
|
32190
31710
|
function readPackageVersion() {
|
|
32191
31711
|
try {
|
|
32192
31712
|
const pkgPath = resolve18(dirname10(fileURLToPath4(import.meta.url)), "..", "..", "..", "package.json");
|
|
32193
|
-
const pkg = JSON.parse(
|
|
31713
|
+
const pkg = JSON.parse(readFileSync22(pkgPath, "utf-8"));
|
|
32194
31714
|
return pkg.version ?? "0.0.0";
|
|
32195
31715
|
} catch {
|
|
32196
31716
|
return "0.0.0";
|
|
@@ -33020,7 +32540,7 @@ ${reset2}
|
|
|
33020
32540
|
res.raw.writeHead(200, void 0, { "Content-Type": "text/html; charset=utf-8" });
|
|
33021
32541
|
res.raw.end(html);
|
|
33022
32542
|
} else {
|
|
33023
|
-
const html =
|
|
32543
|
+
const html = readFileSync22(resolve18(templatesDir, tplFile), "utf-8");
|
|
33024
32544
|
res.raw.writeHead(200, void 0, { "Content-Type": "text/html; charset=utf-8" });
|
|
33025
32545
|
res.raw.end(html);
|
|
33026
32546
|
}
|
|
@@ -33411,7 +32931,7 @@ var init_constants = __esm({
|
|
|
33411
32931
|
});
|
|
33412
32932
|
|
|
33413
32933
|
// src/scss.ts
|
|
33414
|
-
import { readFileSync as
|
|
32934
|
+
import { readFileSync as readFileSync23, writeFileSync as writeFileSync16, existsSync as existsSync24, mkdirSync as mkdirSync18, readdirSync as readdirSync18 } from "node:fs";
|
|
33415
32935
|
import { join as join26, resolve as resolve19, dirname as dirname11 } from "node:path";
|
|
33416
32936
|
function compileString(scss, importPaths, variables) {
|
|
33417
32937
|
const imported = /* @__PURE__ */ new Set();
|
|
@@ -33442,7 +32962,7 @@ function resolveImports(content, paths, imported) {
|
|
|
33442
32962
|
for (const candidate of candidates) {
|
|
33443
32963
|
if (existsSync24(candidate) && !imported.has(candidate)) {
|
|
33444
32964
|
imported.add(candidate);
|
|
33445
|
-
const fileContent =
|
|
32965
|
+
const fileContent = readFileSync23(candidate, "utf-8");
|
|
33446
32966
|
return resolveImports(fileContent, [dirname11(candidate), ...paths], imported);
|
|
33447
32967
|
}
|
|
33448
32968
|
}
|
|
@@ -33783,7 +33303,7 @@ var init_scss = __esm({
|
|
|
33783
33303
|
/** Compile an SCSS file to CSS. */
|
|
33784
33304
|
compileFile(filePath) {
|
|
33785
33305
|
const absPath = resolve19(filePath);
|
|
33786
|
-
const content =
|
|
33306
|
+
const content = readFileSync23(absPath, "utf-8");
|
|
33787
33307
|
const paths = [dirname11(absPath), ...this._importPaths];
|
|
33788
33308
|
return compileString(content, paths, { ...this._variables });
|
|
33789
33309
|
}
|
|
@@ -33806,7 +33326,7 @@ var init_scss = __esm({
|
|
|
33806
33326
|
const imported = /* @__PURE__ */ new Set();
|
|
33807
33327
|
let merged = "";
|
|
33808
33328
|
for (const file of files) {
|
|
33809
|
-
const content =
|
|
33329
|
+
const content = readFileSync23(file, "utf-8");
|
|
33810
33330
|
imported.add(file);
|
|
33811
33331
|
merged += resolveImports(content, paths, imported) + "\n";
|
|
33812
33332
|
}
|
|
@@ -33823,7 +33343,7 @@ var init_scss = __esm({
|
|
|
33823
33343
|
if (!existsSync24(outDir)) mkdirSync18(outDir, { recursive: true });
|
|
33824
33344
|
let existing = null;
|
|
33825
33345
|
try {
|
|
33826
|
-
existing = existsSync24(absOutput) ?
|
|
33346
|
+
existing = existsSync24(absOutput) ? readFileSync23(absOutput, "utf-8") : null;
|
|
33827
33347
|
} catch {
|
|
33828
33348
|
existing = null;
|
|
33829
33349
|
}
|
|
@@ -33907,10 +33427,10 @@ var init_mqttMessage = __esm({
|
|
|
33907
33427
|
});
|
|
33908
33428
|
|
|
33909
33429
|
// src/mqtt.ts
|
|
33910
|
-
import
|
|
33911
|
-
import
|
|
33430
|
+
import net2 from "node:net";
|
|
33431
|
+
import tls from "node:tls";
|
|
33912
33432
|
import { randomBytes as randomBytes7 } from "node:crypto";
|
|
33913
|
-
import { existsSync as existsSync25, readFileSync as
|
|
33433
|
+
import { existsSync as existsSync25, readFileSync as readFileSync24 } from "node:fs";
|
|
33914
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;
|
|
33915
33435
|
var init_mqtt = __esm({
|
|
33916
33436
|
"src/mqtt.ts"() {
|
|
@@ -34375,10 +33895,10 @@ var init_mqtt = __esm({
|
|
|
34375
33895
|
servername: this.host,
|
|
34376
33896
|
rejectUnauthorized: this.tlsVerify
|
|
34377
33897
|
};
|
|
34378
|
-
if (this.tlsVerify && this.caFile) opts.ca =
|
|
34379
|
-
sock =
|
|
33898
|
+
if (this.tlsVerify && this.caFile) opts.ca = readFileSync24(this.caFile);
|
|
33899
|
+
sock = tls.connect(opts, () => settle(() => resolve21(sock)));
|
|
34380
33900
|
} else {
|
|
34381
|
-
sock =
|
|
33901
|
+
sock = net2.createConnection({ host: this.host, port: this.port }, () => settle(() => resolve21(sock)));
|
|
34382
33902
|
}
|
|
34383
33903
|
sock.once("error", (err) => {
|
|
34384
33904
|
settle(() => {
|
|
@@ -34929,7 +34449,7 @@ import https from "node:https";
|
|
|
34929
34449
|
import { URL as URL2 } from "node:url";
|
|
34930
34450
|
import { randomBytes as randomBytes8 } from "node:crypto";
|
|
34931
34451
|
import { promises as fsp, createWriteStream } from "node:fs";
|
|
34932
|
-
import { basename as
|
|
34452
|
+
import { basename as basename5 } from "node:path";
|
|
34933
34453
|
import { pipeline } from "node:stream/promises";
|
|
34934
34454
|
function sameOrigin(urlA, urlB) {
|
|
34935
34455
|
try {
|
|
@@ -35219,7 +34739,7 @@ var init_api = __esm({
|
|
|
35219
34739
|
error: err instanceof Error ? err.message : String(err)
|
|
35220
34740
|
};
|
|
35221
34741
|
}
|
|
35222
|
-
uploadName = filename ||
|
|
34742
|
+
uploadName = filename || basename5(filePath);
|
|
35223
34743
|
} else {
|
|
35224
34744
|
return { http_code: null, body: null, headers: {}, error: "upload requires filePath or fileBytes" };
|
|
35225
34745
|
}
|
|
@@ -35575,6 +35095,777 @@ var init_api = __esm({
|
|
|
35575
35095
|
}
|
|
35576
35096
|
});
|
|
35577
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
|
+
|
|
35578
35869
|
// src/wsdl.ts
|
|
35579
35870
|
function escapeXml(value) {
|
|
35580
35871
|
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|