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
package/packages/cli/dist/bin.js
CHANGED
|
@@ -1298,11 +1298,14 @@ var init_sqlite = __esm({
|
|
|
1298
1298
|
const pragma = schema && isIdentifier(schema) && isIdentifier(tbl) ? `PRAGMA ${schema}.table_info("${tbl}")` : `PRAGMA table_info("${table2}")`;
|
|
1299
1299
|
const rows = this.db.prepare(pragma).all();
|
|
1300
1300
|
return rows.map((r) => ({
|
|
1301
|
+
// PRAGMA table_info reports `pk` as the 1-BASED POSITION within the primary
|
|
1302
|
+
// key, not a boolean: a composite key gives pk=1, pk=2, ... Testing `=== 1`
|
|
1303
|
+
// reported only the first column of a composite key.
|
|
1301
1304
|
name: r.name,
|
|
1302
1305
|
type: r.type,
|
|
1303
1306
|
nullable: r.notnull === 0,
|
|
1304
1307
|
default: r.dflt_value,
|
|
1305
|
-
primaryKey: r.pk
|
|
1308
|
+
primaryKey: Number(r.pk) > 0
|
|
1306
1309
|
}));
|
|
1307
1310
|
}
|
|
1308
1311
|
lastInsertId() {
|
|
@@ -4298,6 +4301,8 @@ var init_database = __esm({
|
|
|
4298
4301
|
poolIndex = 0;
|
|
4299
4302
|
/** Factory for creating new adapters (used by pool) */
|
|
4300
4303
|
adapterFactory = null;
|
|
4304
|
+
/** table -> primary-key column name (or null), introspected once */
|
|
4305
|
+
_pkCache = /* @__PURE__ */ new Map();
|
|
4301
4306
|
/**
|
|
4302
4307
|
* Whether a standalone write auto-commits. ON by default — a write made
|
|
4303
4308
|
* outside an explicit transaction commits on its own connection before
|
|
@@ -4556,29 +4561,112 @@ var init_database = __esm({
|
|
|
4556
4561
|
}
|
|
4557
4562
|
return result;
|
|
4558
4563
|
}
|
|
4559
|
-
/**
|
|
4564
|
+
/**
|
|
4565
|
+
* The table's primary-key column, introspected once and cached.
|
|
4566
|
+
*
|
|
4567
|
+
* Uses the cross-engine getColumns() contract (v3.13.14, #48), which reports
|
|
4568
|
+
* primaryKey per column on every adapter. Resolves to null when the table has
|
|
4569
|
+
* no primary key or cannot be introspected.
|
|
4570
|
+
*/
|
|
4571
|
+
async primaryKey(table2) {
|
|
4572
|
+
if (!this._pkCache.has(table2)) {
|
|
4573
|
+
let pk = [];
|
|
4574
|
+
try {
|
|
4575
|
+
const columns = await this.getColumns(table2);
|
|
4576
|
+
pk = columns.filter((c) => c.primaryKey).map((c) => c.name);
|
|
4577
|
+
} catch {
|
|
4578
|
+
pk = [];
|
|
4579
|
+
}
|
|
4580
|
+
this._pkCache.set(table2, pk);
|
|
4581
|
+
}
|
|
4582
|
+
return this._pkCache.get(table2) ?? [];
|
|
4583
|
+
}
|
|
4584
|
+
/**
|
|
4585
|
+
* A failed write must be loud.
|
|
4586
|
+
*
|
|
4587
|
+
* The adapters catch a SQL error and return { success: false, affectedRows: 0 },
|
|
4588
|
+
* so a filterless update produced invalid SQL ("... WHERE ") and reported
|
|
4589
|
+
* nothing rather than raising. A caller who does not inspect the result
|
|
4590
|
+
* believes the write landed (audit feature 4, P1).
|
|
4591
|
+
*/
|
|
4592
|
+
static assertWrote(result, verb, table2) {
|
|
4593
|
+
if (result && result.success === false) {
|
|
4594
|
+
throw new Error(
|
|
4595
|
+
`${verb} failed on ${table2}: ${result.error ?? "unknown error"}`
|
|
4596
|
+
);
|
|
4597
|
+
}
|
|
4598
|
+
return result;
|
|
4599
|
+
}
|
|
4600
|
+
/**
|
|
4601
|
+
* Update rows. A write with no filter is an error, not a full-table write.
|
|
4602
|
+
*
|
|
4603
|
+
* With no explicit filter the primary key is taken out of `data` and used as
|
|
4604
|
+
* the WHERE clause. With neither a filter nor a primary key in `data` this
|
|
4605
|
+
* throws rather than silently changing nothing (audit feature 4, P1).
|
|
4606
|
+
*/
|
|
4560
4607
|
async update(table2, data, filter, params) {
|
|
4608
|
+
let effectiveFilter = filter ?? {};
|
|
4609
|
+
let effectiveData = data;
|
|
4610
|
+
if (Object.keys(effectiveFilter).length === 0) {
|
|
4611
|
+
const pkColumns = await this.primaryKey(table2);
|
|
4612
|
+
const missing = pkColumns.filter((c) => !(c in data));
|
|
4613
|
+
if (pkColumns.length === 0 || missing.length > 0) {
|
|
4614
|
+
throw new Error(
|
|
4615
|
+
`update requires a filter or the complete primary key in the data; pass filter explicitly to update multiple rows (table=${table2}, primary key=[${pkColumns.join(", ")}], missing from data=[${missing.join(", ")}]). To empty a table use truncate(${table2}).`
|
|
4616
|
+
);
|
|
4617
|
+
}
|
|
4618
|
+
effectiveData = { ...data };
|
|
4619
|
+
const keyed = {};
|
|
4620
|
+
for (const col of pkColumns) {
|
|
4621
|
+
keyed[col] = effectiveData[col];
|
|
4622
|
+
delete effectiveData[col];
|
|
4623
|
+
}
|
|
4624
|
+
if (Object.keys(effectiveData).length === 0) {
|
|
4625
|
+
throw new Error(
|
|
4626
|
+
`update was given only the primary key [${pkColumns.join(", ")}] and no columns to set (table=${table2})`
|
|
4627
|
+
);
|
|
4628
|
+
}
|
|
4629
|
+
effectiveFilter = keyed;
|
|
4630
|
+
}
|
|
4561
4631
|
const adapter = this.getNextAdapter();
|
|
4562
|
-
const result = adapter.updateAsync ? await adapter.updateAsync(table2,
|
|
4632
|
+
const result = adapter.updateAsync ? await adapter.updateAsync(table2, effectiveData, effectiveFilter, params) : adapter.update(table2, effectiveData, effectiveFilter, params);
|
|
4563
4633
|
if (this.autoCommit && !this.inExplicitTransaction()) {
|
|
4564
4634
|
try {
|
|
4565
4635
|
await adapterCommit(adapter);
|
|
4566
4636
|
} catch {
|
|
4567
4637
|
}
|
|
4568
4638
|
}
|
|
4569
|
-
return result;
|
|
4639
|
+
return _Database.assertWrote(result, "update", table2);
|
|
4570
4640
|
}
|
|
4571
|
-
/** Delete rows
|
|
4641
|
+
/** Delete rows. A filterless delete throws; use truncate() to empty a table. */
|
|
4572
4642
|
async delete(table2, filter, params) {
|
|
4643
|
+
const effectiveFilter = filter ?? {};
|
|
4644
|
+
if (!Array.isArray(effectiveFilter) && typeof effectiveFilter !== "string" && Object.keys(effectiveFilter).length === 0) {
|
|
4645
|
+
throw new Error(
|
|
4646
|
+
`delete requires a filter (table=${table2}). To remove every row use truncate(${table2}).`
|
|
4647
|
+
);
|
|
4648
|
+
}
|
|
4573
4649
|
const adapter = this.getNextAdapter();
|
|
4574
|
-
const result = adapter.deleteAsync ? await adapter.deleteAsync(table2,
|
|
4650
|
+
const result = adapter.deleteAsync ? await adapter.deleteAsync(table2, effectiveFilter, params) : adapter.delete(table2, effectiveFilter, params);
|
|
4575
4651
|
if (this.autoCommit && !this.inExplicitTransaction()) {
|
|
4576
4652
|
try {
|
|
4577
4653
|
await adapterCommit(adapter);
|
|
4578
4654
|
} catch {
|
|
4579
4655
|
}
|
|
4580
4656
|
}
|
|
4581
|
-
return result;
|
|
4657
|
+
return _Database.assertWrote(result, "delete", table2);
|
|
4658
|
+
}
|
|
4659
|
+
/** Remove every row. The explicit spelling of a whole-table delete. */
|
|
4660
|
+
async truncate(table2) {
|
|
4661
|
+
const adapter = this.getNextAdapter();
|
|
4662
|
+
const result = adapter.deleteAsync ? await adapter.deleteAsync(table2, "1 = 1", []) : adapter.delete(table2, "1 = 1", []);
|
|
4663
|
+
if (this.autoCommit && !this.inExplicitTransaction()) {
|
|
4664
|
+
try {
|
|
4665
|
+
await adapterCommit(adapter);
|
|
4666
|
+
} catch {
|
|
4667
|
+
}
|
|
4668
|
+
}
|
|
4669
|
+
return _Database.assertWrote(result, "truncate", table2);
|
|
4582
4670
|
}
|
|
4583
4671
|
/** Close all database connections (pool or single). */
|
|
4584
4672
|
close() {
|
|
@@ -13827,7 +13915,7 @@ function _generateFormToken(descriptor = "") {
|
|
|
13827
13915
|
function _generateFormTokenValue(descriptor = "") {
|
|
13828
13916
|
return new SafeString(_buildFormTokenJwt(descriptor));
|
|
13829
13917
|
}
|
|
13830
|
-
var SafeString, KNOWN_TAGS, TERMINATOR_TAGS, JSON_UNSAFE_RE, JSON_UNSAFE_MAP, NUMERIC_RE, METHOD_CALL_RE, FN_CALL_RE, IS_NOT_RE, IS_RE, NOT_IN_RE, IN_RE, DIVISIBLE_BY_RE, FILTER_WITH_ARGS_RE, FILTER_COMPARISON_RE, TITLE_WORD_RE, STRIP_TAGS_RE, FORMAT_RE, LEADING_WS_RE, TRAILING_WS_RE, THOUSANDS_RE, LIVE_RE, LIVE_WS_RE, LIVE_SRC_RE, filterChainCache, pathParseCache, TEMPLATE_CACHE_MAX, TOKEN_RE, RAW_BLOCK_RE, VarRef, BUILTIN_FILTERS, _formTokenSessionId, Frond;
|
|
13918
|
+
var SafeString, KNOWN_TAGS, TERMINATOR_TAGS, GATEABLE_TAGS, BLOCK_TAG_ENDS, JSON_UNSAFE_RE, JSON_UNSAFE_MAP, NUMERIC_RE, METHOD_CALL_RE, FN_CALL_RE, IS_NOT_RE, IS_RE, NOT_IN_RE, IN_RE, DIVISIBLE_BY_RE, FILTER_WITH_ARGS_RE, FILTER_COMPARISON_RE, TITLE_WORD_RE, STRIP_TAGS_RE, FORMAT_RE, LEADING_WS_RE, TRAILING_WS_RE, THOUSANDS_RE, LIVE_RE, LIVE_WS_RE, LIVE_SRC_RE, filterChainCache, pathParseCache, TEMPLATE_CACHE_MAX, TOKEN_RE, RAW_BLOCK_RE, VarRef, BUILTIN_FILTERS, _formTokenSessionId, Frond;
|
|
13831
13919
|
var init_engine = __esm({
|
|
13832
13920
|
"../frond/src/engine.ts"() {
|
|
13833
13921
|
"use strict";
|
|
@@ -13870,6 +13958,29 @@ var init_engine = __esm({
|
|
|
13870
13958
|
"endset",
|
|
13871
13959
|
"endspaceless"
|
|
13872
13960
|
]);
|
|
13961
|
+
GATEABLE_TAGS = /* @__PURE__ */ new Set([
|
|
13962
|
+
"autoescape",
|
|
13963
|
+
"cache",
|
|
13964
|
+
"for",
|
|
13965
|
+
"from",
|
|
13966
|
+
"if",
|
|
13967
|
+
"import",
|
|
13968
|
+
"include",
|
|
13969
|
+
"live",
|
|
13970
|
+
"macro",
|
|
13971
|
+
"set",
|
|
13972
|
+
"spaceless"
|
|
13973
|
+
]);
|
|
13974
|
+
BLOCK_TAG_ENDS = {
|
|
13975
|
+
autoescape: "endautoescape",
|
|
13976
|
+
cache: "endcache",
|
|
13977
|
+
for: "endfor",
|
|
13978
|
+
if: "endif",
|
|
13979
|
+
live: "endlive",
|
|
13980
|
+
macro: "endmacro",
|
|
13981
|
+
set: "endset",
|
|
13982
|
+
spaceless: "endspaceless"
|
|
13983
|
+
};
|
|
13873
13984
|
JSON_UNSAFE_RE = /[<>&'\u2028\u2029]/g;
|
|
13874
13985
|
JSON_UNSAFE_MAP = {
|
|
13875
13986
|
"<": "\\u003c",
|
|
@@ -14505,42 +14616,27 @@ var init_engine = __esm({
|
|
|
14505
14616
|
if (stripA && i + 1 < tokens.length && tokens[i + 1][0] === "TEXT") {
|
|
14506
14617
|
tokens[i + 1] = ["TEXT", tokens[i + 1][1].replace(LEADING_WS_RE, "")];
|
|
14507
14618
|
}
|
|
14508
|
-
if (tag
|
|
14509
|
-
|
|
14510
|
-
|
|
14511
|
-
|
|
14512
|
-
|
|
14513
|
-
|
|
14514
|
-
output.push(result);
|
|
14515
|
-
i = skip;
|
|
14516
|
-
}
|
|
14619
|
+
if (!this.tagPermitted(tag)) {
|
|
14620
|
+
i = this.skipDeniedTag(tokens, i, tag, content);
|
|
14621
|
+
} else if (tag === "if") {
|
|
14622
|
+
const [result, skip] = this.handleIf(tokens, i, context);
|
|
14623
|
+
output.push(result);
|
|
14624
|
+
i = skip;
|
|
14517
14625
|
} else if (tag === "for") {
|
|
14518
|
-
|
|
14519
|
-
|
|
14520
|
-
|
|
14521
|
-
} else {
|
|
14522
|
-
const [result, skip] = this.handleFor(tokens, i, context);
|
|
14523
|
-
output.push(result);
|
|
14524
|
-
i = skip;
|
|
14525
|
-
}
|
|
14626
|
+
const [result, skip] = this.handleFor(tokens, i, context);
|
|
14627
|
+
output.push(result);
|
|
14628
|
+
i = skip;
|
|
14526
14629
|
} else if (tag === "set") {
|
|
14527
|
-
|
|
14528
|
-
if (this._sandbox && this._allowedTags !== null && !this._allowedTags.has("set")) {
|
|
14529
|
-
i = isBlockSet ? this.skipBlock(tokens, i, "set", "endset") : i + 1;
|
|
14530
|
-
} else if (isBlockSet) {
|
|
14630
|
+
if (!content.includes("=")) {
|
|
14531
14631
|
i = this.handleSetBlock(tokens, i, context);
|
|
14532
14632
|
} else {
|
|
14533
14633
|
this.handleSet(content, context);
|
|
14534
14634
|
i++;
|
|
14535
14635
|
}
|
|
14536
14636
|
} else if (tag === "include") {
|
|
14537
|
-
|
|
14538
|
-
|
|
14539
|
-
|
|
14540
|
-
const result = this.handleInclude(content, context);
|
|
14541
|
-
output.push(result);
|
|
14542
|
-
i++;
|
|
14543
|
-
}
|
|
14637
|
+
const result = this.handleInclude(content, context);
|
|
14638
|
+
output.push(result);
|
|
14639
|
+
i++;
|
|
14544
14640
|
} else if (tag === "macro") {
|
|
14545
14641
|
const skip = this.handleMacro(tokens, i, context);
|
|
14546
14642
|
i = skip;
|
|
@@ -14585,6 +14681,41 @@ var init_engine = __esm({
|
|
|
14585
14681
|
}
|
|
14586
14682
|
return output.join("");
|
|
14587
14683
|
}
|
|
14684
|
+
/**
|
|
14685
|
+
* May this filter RUN under the current sandbox?
|
|
14686
|
+
*
|
|
14687
|
+
* The escaping decision has to ask this rather than read the filter name out of
|
|
14688
|
+
* the source. Node carries safety as a FLAG rather than as a value-level marker
|
|
14689
|
+
* (Python and Ruby return a SafeString, PHP prepends a RAW_MARKER -- all three
|
|
14690
|
+
* produced only by actually running the filter), so here the name alone was
|
|
14691
|
+
* enough to suppress auto-escaping even when the filter was denied and skipped.
|
|
14692
|
+
*/
|
|
14693
|
+
filterPermitted(name) {
|
|
14694
|
+
if (!this._sandbox || this._allowedFilters === null) return true;
|
|
14695
|
+
return this._allowedFilters.has(name);
|
|
14696
|
+
}
|
|
14697
|
+
/**
|
|
14698
|
+
* May this tag run under the current sandbox?
|
|
14699
|
+
*
|
|
14700
|
+
* One gate for every tag, so the allow-list governs the whole tag vocabulary
|
|
14701
|
+
* instead of the four names that happened to be checked individually.
|
|
14702
|
+
*/
|
|
14703
|
+
tagPermitted(tag) {
|
|
14704
|
+
if (!this._sandbox || this._allowedTags === null) return true;
|
|
14705
|
+
if (!GATEABLE_TAGS.has(tag)) return true;
|
|
14706
|
+
return this._allowedTags.has(tag);
|
|
14707
|
+
}
|
|
14708
|
+
/**
|
|
14709
|
+
* Consume a denied tag WITHOUT running it, returning the index past its body.
|
|
14710
|
+
*
|
|
14711
|
+
* Advancing a single token past a body-owning tag would leave the body's tokens
|
|
14712
|
+
* to render at the TOP level, leaking exactly the content the sandbox denied.
|
|
14713
|
+
*/
|
|
14714
|
+
skipDeniedTag(tokens, start2, tag, content) {
|
|
14715
|
+
const closeTag = BLOCK_TAG_ENDS[tag];
|
|
14716
|
+
if (closeTag === void 0 || tag === "set" && content.includes("=")) return start2 + 1;
|
|
14717
|
+
return this.skipBlock(tokens, start2, tag, closeTag);
|
|
14718
|
+
}
|
|
14588
14719
|
skipBlock(tokens, start2, openTag, closeTag) {
|
|
14589
14720
|
let depth = 0;
|
|
14590
14721
|
let i = start2 + 1;
|
|
@@ -14592,7 +14723,7 @@ var init_engine = __esm({
|
|
|
14592
14723
|
if (tokens[i][0] === "BLOCK") {
|
|
14593
14724
|
const [content] = stripTag(tokens[i][1]);
|
|
14594
14725
|
const tag = content.split(/\s+/)[0] || "";
|
|
14595
|
-
if (tag === openTag) depth++;
|
|
14726
|
+
if (tag === openTag && !(openTag === "set" && content.includes("="))) depth++;
|
|
14596
14727
|
else if (tag === closeTag) {
|
|
14597
14728
|
if (depth === 0) return i + 1;
|
|
14598
14729
|
depth--;
|
|
@@ -14777,11 +14908,11 @@ var init_engine = __esm({
|
|
|
14777
14908
|
for (const [fname, rawArgs] of filters) {
|
|
14778
14909
|
const args = rawArgs.map((a) => a instanceof VarRef ? evalExpr(a.name, context) : a);
|
|
14779
14910
|
if (fname === "raw" || fname === "safe") {
|
|
14780
|
-
isSafe = true;
|
|
14911
|
+
if (this.filterPermitted(fname)) isSafe = true;
|
|
14781
14912
|
continue;
|
|
14782
14913
|
}
|
|
14783
14914
|
if (fname === "escape" || fname === "e") {
|
|
14784
|
-
isSafe = true;
|
|
14915
|
+
if (this.filterPermitted(fname)) isSafe = true;
|
|
14785
14916
|
}
|
|
14786
14917
|
if (this._sandbox && this._allowedFilters !== null) {
|
|
14787
14918
|
if (!this._allowedFilters.has(fname)) {
|
|
@@ -16592,861 +16723,131 @@ var init_rateLimiter = __esm({
|
|
|
16592
16723
|
}
|
|
16593
16724
|
});
|
|
16594
16725
|
|
|
16595
|
-
// ../core/src/
|
|
16596
|
-
import
|
|
16597
|
-
import
|
|
16598
|
-
import { readFileSync as readFileSync8 } from "node:fs";
|
|
16599
|
-
import { basename as basename3 } from "node:path";
|
|
16726
|
+
// ../core/src/devMailbox.ts
|
|
16727
|
+
import { mkdirSync as mkdirSync9, readdirSync as readdirSync6, readFileSync as readFileSync8, writeFileSync as writeFileSync7, unlinkSync as unlinkSync4, existsSync as existsSync11 } from "node:fs";
|
|
16728
|
+
import { join as join13 } from "node:path";
|
|
16600
16729
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
16601
|
-
|
|
16602
|
-
|
|
16603
|
-
|
|
16604
|
-
|
|
16605
|
-
|
|
16606
|
-
|
|
16607
|
-
|
|
16608
|
-
|
|
16609
|
-
const lines = buffer.split("\r\n");
|
|
16610
|
-
for (let i = 0; i < lines.length; i++) {
|
|
16611
|
-
const line = lines[i];
|
|
16612
|
-
if (line.length < 3) continue;
|
|
16613
|
-
const code = parseInt(line.substring(0, 3), 10);
|
|
16614
|
-
if (line.length >= 4 && line[3] === " ") {
|
|
16615
|
-
socket.removeListener("data", onData);
|
|
16616
|
-
socket.removeListener("error", onError);
|
|
16617
|
-
resolve31({ code, text: buffer.trim() });
|
|
16618
|
-
return;
|
|
16619
|
-
}
|
|
16620
|
-
}
|
|
16621
|
-
};
|
|
16622
|
-
const onError = (err) => {
|
|
16623
|
-
socket.removeListener("data", onData);
|
|
16624
|
-
reject(err);
|
|
16625
|
-
};
|
|
16626
|
-
socket.on("data", onData);
|
|
16627
|
-
socket.on("error", onError);
|
|
16628
|
-
});
|
|
16629
|
-
}
|
|
16630
|
-
function sendCommand(socket, command) {
|
|
16631
|
-
return new Promise((resolve31, reject) => {
|
|
16632
|
-
socket.write(command + "\r\n", "utf-8", (err) => {
|
|
16633
|
-
if (err) return reject(err);
|
|
16634
|
-
readResponse(socket).then(resolve31, reject);
|
|
16635
|
-
});
|
|
16636
|
-
});
|
|
16637
|
-
}
|
|
16638
|
-
function buildMimeMessage(options) {
|
|
16639
|
-
const boundary = `----=_Tina4_${Date.now()}_${Math.random().toString(36).substring(2)}`;
|
|
16640
|
-
const altBoundary = `----=_Tina4Alt_${Date.now()}_${Math.random().toString(36).substring(2)}`;
|
|
16641
|
-
const hasAttachments = options.attachments && options.attachments.length > 0;
|
|
16642
|
-
const hasTextAlt = options.text !== void 0 && options.html;
|
|
16643
|
-
const lines = [];
|
|
16644
|
-
const fromHeader = options.fromName ? `"${options.fromName}" <${options.from}>` : options.from;
|
|
16645
|
-
lines.push(`From: ${fromHeader}`);
|
|
16646
|
-
lines.push(`To: ${options.to.join(", ")}`);
|
|
16647
|
-
if (options.cc.length > 0) {
|
|
16648
|
-
lines.push(`Cc: ${options.cc.join(", ")}`);
|
|
16649
|
-
}
|
|
16650
|
-
lines.push(`Subject: ${options.subject}`);
|
|
16651
|
-
lines.push(`Date: ${(/* @__PURE__ */ new Date()).toUTCString()}`);
|
|
16652
|
-
lines.push(`Message-ID: <${options.messageId}>`);
|
|
16653
|
-
lines.push("MIME-Version: 1.0");
|
|
16654
|
-
if (options.replyTo) {
|
|
16655
|
-
lines.push(`Reply-To: ${options.replyTo}`);
|
|
16656
|
-
}
|
|
16657
|
-
if (options.headers) {
|
|
16658
|
-
for (const [key, value] of Object.entries(options.headers)) {
|
|
16659
|
-
lines.push(`${key}: ${value}`);
|
|
16660
|
-
}
|
|
16661
|
-
}
|
|
16662
|
-
if (hasAttachments) {
|
|
16663
|
-
lines.push(`Content-Type: multipart/mixed; boundary="${boundary}"`);
|
|
16664
|
-
lines.push("");
|
|
16665
|
-
lines.push(`--${boundary}`);
|
|
16666
|
-
if (hasTextAlt) {
|
|
16667
|
-
lines.push(`Content-Type: multipart/alternative; boundary="${altBoundary}"`);
|
|
16668
|
-
lines.push("");
|
|
16669
|
-
lines.push(`--${altBoundary}`);
|
|
16670
|
-
lines.push("Content-Type: text/plain; charset=UTF-8");
|
|
16671
|
-
lines.push("Content-Transfer-Encoding: 7bit");
|
|
16672
|
-
lines.push("");
|
|
16673
|
-
lines.push(options.text);
|
|
16674
|
-
lines.push("");
|
|
16675
|
-
lines.push(`--${altBoundary}`);
|
|
16676
|
-
lines.push("Content-Type: text/html; charset=UTF-8");
|
|
16677
|
-
lines.push("Content-Transfer-Encoding: 7bit");
|
|
16678
|
-
lines.push("");
|
|
16679
|
-
lines.push(options.body);
|
|
16680
|
-
lines.push("");
|
|
16681
|
-
lines.push(`--${altBoundary}--`);
|
|
16682
|
-
} else {
|
|
16683
|
-
const contentType = options.html ? "text/html" : "text/plain";
|
|
16684
|
-
lines.push(`Content-Type: ${contentType}; charset=UTF-8`);
|
|
16685
|
-
lines.push("Content-Transfer-Encoding: 7bit");
|
|
16686
|
-
lines.push("");
|
|
16687
|
-
lines.push(options.body);
|
|
16688
|
-
}
|
|
16689
|
-
for (const filePath of options.attachments) {
|
|
16690
|
-
const fileName = basename3(filePath);
|
|
16691
|
-
const fileData = readFileSync8(filePath);
|
|
16692
|
-
const base64Data = fileData.toString("base64");
|
|
16693
|
-
lines.push("");
|
|
16694
|
-
lines.push(`--${boundary}`);
|
|
16695
|
-
lines.push(`Content-Type: application/octet-stream; name="${fileName}"`);
|
|
16696
|
-
lines.push("Content-Transfer-Encoding: base64");
|
|
16697
|
-
lines.push(`Content-Disposition: attachment; filename="${fileName}"`);
|
|
16698
|
-
lines.push("");
|
|
16699
|
-
for (let i = 0; i < base64Data.length; i += 76) {
|
|
16700
|
-
lines.push(base64Data.substring(i, i + 76));
|
|
16701
|
-
}
|
|
16702
|
-
}
|
|
16703
|
-
lines.push("");
|
|
16704
|
-
lines.push(`--${boundary}--`);
|
|
16705
|
-
} else if (hasTextAlt) {
|
|
16706
|
-
lines.push(`Content-Type: multipart/alternative; boundary="${altBoundary}"`);
|
|
16707
|
-
lines.push("");
|
|
16708
|
-
lines.push(`--${altBoundary}`);
|
|
16709
|
-
lines.push("Content-Type: text/plain; charset=UTF-8");
|
|
16710
|
-
lines.push("Content-Transfer-Encoding: 7bit");
|
|
16711
|
-
lines.push("");
|
|
16712
|
-
lines.push(options.text);
|
|
16713
|
-
lines.push("");
|
|
16714
|
-
lines.push(`--${altBoundary}`);
|
|
16715
|
-
lines.push("Content-Type: text/html; charset=UTF-8");
|
|
16716
|
-
lines.push("Content-Transfer-Encoding: 7bit");
|
|
16717
|
-
lines.push("");
|
|
16718
|
-
lines.push(options.body);
|
|
16719
|
-
lines.push("");
|
|
16720
|
-
lines.push(`--${altBoundary}--`);
|
|
16721
|
-
} else {
|
|
16722
|
-
const contentType = options.html ? "text/html" : "text/plain";
|
|
16723
|
-
lines.push(`Content-Type: ${contentType}; charset=UTF-8`);
|
|
16724
|
-
lines.push("");
|
|
16725
|
-
lines.push(options.body);
|
|
16726
|
-
}
|
|
16727
|
-
return lines.join("\r\n");
|
|
16728
|
-
}
|
|
16729
|
-
function imapQuote(s) {
|
|
16730
|
-
if (/^[a-zA-Z0-9_./-]+$/.test(s)) return s;
|
|
16731
|
-
return '"' + s.replace(/\\/g, "\\\\").replace(/"/g, '\\"') + '"';
|
|
16732
|
-
}
|
|
16733
|
-
function imapReadLine(socket) {
|
|
16734
|
-
return new Promise((resolve31, reject) => {
|
|
16735
|
-
let buffer = "";
|
|
16736
|
-
const onData = (chunk) => {
|
|
16737
|
-
buffer += chunk.toString("utf-8");
|
|
16738
|
-
const nlIndex = buffer.indexOf("\r\n");
|
|
16739
|
-
if (nlIndex !== -1) {
|
|
16740
|
-
socket.removeListener("data", onData);
|
|
16741
|
-
socket.removeListener("error", onError);
|
|
16742
|
-
resolve31(buffer);
|
|
16730
|
+
var DevMailbox;
|
|
16731
|
+
var init_devMailbox = __esm({
|
|
16732
|
+
"../core/src/devMailbox.ts"() {
|
|
16733
|
+
"use strict";
|
|
16734
|
+
DevMailbox = class {
|
|
16735
|
+
mailboxDir;
|
|
16736
|
+
constructor(mailboxDir) {
|
|
16737
|
+
this.mailboxDir = mailboxDir ?? process.env.TINA4_MAILBOX_DIR ?? "data/mailbox";
|
|
16743
16738
|
}
|
|
16744
|
-
|
|
16745
|
-
|
|
16746
|
-
|
|
16747
|
-
|
|
16748
|
-
|
|
16749
|
-
|
|
16750
|
-
|
|
16751
|
-
});
|
|
16752
|
-
}
|
|
16753
|
-
function imapCommand(socket, command) {
|
|
16754
|
-
return new Promise((resolve31, reject) => {
|
|
16755
|
-
imapTagCounter++;
|
|
16756
|
-
const tag = `T${imapTagCounter}`;
|
|
16757
|
-
const fullCommand = `${tag} ${command}\r
|
|
16758
|
-
`;
|
|
16759
|
-
let buffer = "";
|
|
16760
|
-
const onData = (chunk) => {
|
|
16761
|
-
buffer += chunk.toString("utf-8");
|
|
16762
|
-
if (buffer.includes(`${tag} OK`)) {
|
|
16763
|
-
socket.removeListener("data", onData);
|
|
16764
|
-
socket.removeListener("error", onError);
|
|
16765
|
-
resolve31(buffer);
|
|
16766
|
-
return;
|
|
16739
|
+
/**
|
|
16740
|
+
* Ensure a folder directory exists.
|
|
16741
|
+
*/
|
|
16742
|
+
ensureFolder(folder) {
|
|
16743
|
+
const dir = join13(this.mailboxDir, folder);
|
|
16744
|
+
mkdirSync9(dir, { recursive: true });
|
|
16745
|
+
return dir;
|
|
16767
16746
|
}
|
|
16768
|
-
|
|
16769
|
-
|
|
16770
|
-
|
|
16771
|
-
|
|
16747
|
+
/**
|
|
16748
|
+
* Capture an email to the dev mailbox instead of sending it.
|
|
16749
|
+
*
|
|
16750
|
+
* The parameter order MATCHES Messenger.send() on purpose. It did not before:
|
|
16751
|
+
* send()'s 5th positional was `text` and capture()'s was `cc`, so the same call
|
|
16752
|
+
* meant different things depending on which door it came through -- that mismatch
|
|
16753
|
+
* IS nodejs#42.
|
|
16754
|
+
*
|
|
16755
|
+
* BREAKING: `text` is now the 5th positional. A caller passing cc positionally
|
|
16756
|
+
* must move it. Aligning the two signatures is the fix; leaving them apart would
|
|
16757
|
+
* preserve the bug.
|
|
16758
|
+
*/
|
|
16759
|
+
capture(to, subject, body, html = false, text, cc = [], bcc = [], replyTo, attachments = [], from) {
|
|
16760
|
+
const id = randomUUID2();
|
|
16761
|
+
const toList = Array.isArray(to) ? to : [to];
|
|
16762
|
+
const ccList = Array.isArray(cc) ? cc : cc ? [cc] : [];
|
|
16763
|
+
const bccList = Array.isArray(bcc) ? bcc : bcc ? [bcc] : [];
|
|
16764
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
16765
|
+
const message = {
|
|
16766
|
+
id,
|
|
16767
|
+
type: "outbox",
|
|
16768
|
+
from: from ?? process.env.TINA4_MAIL_FROM ?? "dev@localhost",
|
|
16769
|
+
to: toList,
|
|
16770
|
+
cc: ccList,
|
|
16771
|
+
bcc: bccList,
|
|
16772
|
+
reply_to: replyTo,
|
|
16773
|
+
subject,
|
|
16774
|
+
body,
|
|
16775
|
+
text,
|
|
16776
|
+
html,
|
|
16777
|
+
attachments,
|
|
16778
|
+
date: now,
|
|
16779
|
+
read: false
|
|
16780
|
+
};
|
|
16781
|
+
const outboxDir = this.ensureFolder("outbox");
|
|
16782
|
+
writeFileSync7(join13(outboxDir, `${id}.json`), JSON.stringify(message, null, 2));
|
|
16783
|
+
const inboxDir = this.ensureFolder("inbox");
|
|
16784
|
+
const inboxMessage = { ...message, type: "inbox" };
|
|
16785
|
+
writeFileSync7(join13(inboxDir, `${id}.json`), JSON.stringify(inboxMessage, null, 2));
|
|
16786
|
+
return { success: true, message: "Email captured to dev mailbox", id };
|
|
16772
16787
|
}
|
|
16773
|
-
|
|
16774
|
-
|
|
16775
|
-
|
|
16776
|
-
|
|
16777
|
-
|
|
16778
|
-
|
|
16779
|
-
|
|
16780
|
-
|
|
16781
|
-
|
|
16782
|
-
}
|
|
16783
|
-
|
|
16784
|
-
const e = err instanceof Error ? err : new Error(String(err));
|
|
16785
|
-
Log.error(`Messenger IMAP ${method}() failed: ${e.name}: ${e.message}`);
|
|
16786
|
-
if (e instanceof MessengerConnectionError) return e;
|
|
16787
|
-
return new MessengerConnectionError(`IMAP ${method} failed: ${e.message}`);
|
|
16788
|
-
}
|
|
16789
|
-
function parseSearchResponse(response) {
|
|
16790
|
-
const match = response.match(/\* SEARCH (.+)/);
|
|
16791
|
-
if (!match) return [];
|
|
16792
|
-
return match[1].trim().split(/\s+/).filter((s) => /^\d+$/.test(s));
|
|
16793
|
-
}
|
|
16794
|
-
function parseHeaderResponse(uid, response) {
|
|
16795
|
-
const headers = {};
|
|
16796
|
-
const headerBlock = response.match(/\r\n([\s\S]*?)\r\n\)/);
|
|
16797
|
-
if (headerBlock) {
|
|
16798
|
-
const lines = headerBlock[1].split(/\r\n/);
|
|
16799
|
-
let currentKey = "";
|
|
16800
|
-
for (const line of lines) {
|
|
16801
|
-
if (/^\s/.test(line) && currentKey) {
|
|
16802
|
-
headers[currentKey] += " " + line.trim();
|
|
16803
|
-
} else {
|
|
16804
|
-
const colonIdx = line.indexOf(":");
|
|
16805
|
-
if (colonIdx > 0) {
|
|
16806
|
-
currentKey = line.substring(0, colonIdx).trim().toLowerCase();
|
|
16807
|
-
headers[currentKey] = line.substring(colonIdx + 1).trim();
|
|
16788
|
+
/**
|
|
16789
|
+
* List messages from a folder (default: inbox).
|
|
16790
|
+
*/
|
|
16791
|
+
inbox(limit = 50, offset = 0, folder = "inbox") {
|
|
16792
|
+
const dir = this.ensureFolder(folder);
|
|
16793
|
+
const results = [];
|
|
16794
|
+
let files;
|
|
16795
|
+
try {
|
|
16796
|
+
files = readdirSync6(dir).filter((f) => f.endsWith(".json")).sort().reverse();
|
|
16797
|
+
} catch {
|
|
16798
|
+
return [];
|
|
16808
16799
|
}
|
|
16809
|
-
|
|
16810
|
-
|
|
16811
|
-
|
|
16812
|
-
|
|
16813
|
-
|
|
16814
|
-
|
|
16815
|
-
|
|
16816
|
-
from: headers["from"] ?? "",
|
|
16817
|
-
to: headers["to"] ?? "",
|
|
16818
|
-
date: headers["date"] ?? "",
|
|
16819
|
-
snippet: "",
|
|
16820
|
-
seen
|
|
16821
|
-
};
|
|
16822
|
-
}
|
|
16823
|
-
function emptyFullMessage(uid) {
|
|
16824
|
-
return { uid, subject: "", from: "", to: "", cc: "", date: "", bodyText: "", bodyHtml: "", headers: {} };
|
|
16825
|
-
}
|
|
16826
|
-
function parseFullMessage(uid, response) {
|
|
16827
|
-
const bodyMatch = response.match(/\{(\d+)\}\r\n([\s\S]*)/);
|
|
16828
|
-
const rawMessage = bodyMatch ? bodyMatch[2] : response;
|
|
16829
|
-
const headerEnd = rawMessage.indexOf("\r\n\r\n");
|
|
16830
|
-
const headerSection = headerEnd > 0 ? rawMessage.substring(0, headerEnd) : rawMessage;
|
|
16831
|
-
const bodySection = headerEnd > 0 ? rawMessage.substring(headerEnd + 4) : "";
|
|
16832
|
-
const headers = {};
|
|
16833
|
-
const headerLines = headerSection.split(/\r\n/);
|
|
16834
|
-
let currentKey = "";
|
|
16835
|
-
for (const line of headerLines) {
|
|
16836
|
-
if (/^\s/.test(line) && currentKey) {
|
|
16837
|
-
headers[currentKey] += " " + line.trim();
|
|
16838
|
-
} else {
|
|
16839
|
-
const colonIdx = line.indexOf(":");
|
|
16840
|
-
if (colonIdx > 0) {
|
|
16841
|
-
currentKey = line.substring(0, colonIdx).trim().toLowerCase();
|
|
16842
|
-
headers[currentKey] = line.substring(colonIdx + 1).trim();
|
|
16843
|
-
}
|
|
16844
|
-
}
|
|
16845
|
-
}
|
|
16846
|
-
const contentType = headers["content-type"] ?? "text/plain";
|
|
16847
|
-
let bodyText = "";
|
|
16848
|
-
let bodyHtml = "";
|
|
16849
|
-
if (contentType.includes("multipart")) {
|
|
16850
|
-
const boundaryMatch = contentType.match(/boundary="?([^";\s]+)"?/);
|
|
16851
|
-
if (boundaryMatch) {
|
|
16852
|
-
const boundary = boundaryMatch[1];
|
|
16853
|
-
const parts = bodySection.split("--" + boundary);
|
|
16854
|
-
for (const part of parts) {
|
|
16855
|
-
if (part.trim() === "" || part.trim() === "--") continue;
|
|
16856
|
-
const partHeaderEnd = part.indexOf("\r\n\r\n");
|
|
16857
|
-
const partHeaders = partHeaderEnd > 0 ? part.substring(0, partHeaderEnd).toLowerCase() : "";
|
|
16858
|
-
const partBody = partHeaderEnd > 0 ? part.substring(partHeaderEnd + 4).trim() : "";
|
|
16859
|
-
if (partHeaders.includes("text/html")) {
|
|
16860
|
-
bodyHtml = partBody;
|
|
16861
|
-
} else if (partHeaders.includes("text/plain")) {
|
|
16862
|
-
bodyText = partBody;
|
|
16800
|
+
const sliced = files.slice(offset, offset + limit);
|
|
16801
|
+
for (const file of sliced) {
|
|
16802
|
+
try {
|
|
16803
|
+
const msg = JSON.parse(readFileSync8(join13(dir, file), "utf-8"));
|
|
16804
|
+
results.push(msg);
|
|
16805
|
+
} catch {
|
|
16806
|
+
}
|
|
16863
16807
|
}
|
|
16808
|
+
return results;
|
|
16864
16809
|
}
|
|
16865
|
-
|
|
16866
|
-
|
|
16867
|
-
|
|
16868
|
-
|
|
16869
|
-
|
|
16870
|
-
|
|
16871
|
-
|
|
16872
|
-
|
|
16873
|
-
|
|
16874
|
-
|
|
16875
|
-
|
|
16876
|
-
|
|
16877
|
-
|
|
16878
|
-
|
|
16879
|
-
|
|
16880
|
-
|
|
16881
|
-
|
|
16882
|
-
headers
|
|
16883
|
-
};
|
|
16884
|
-
}
|
|
16885
|
-
var MessengerConnectionError, Messenger, imapTagCounter;
|
|
16886
|
-
var init_messenger = __esm({
|
|
16887
|
-
"../core/src/messenger.ts"() {
|
|
16888
|
-
"use strict";
|
|
16889
|
-
init_dotenv();
|
|
16890
|
-
init_logger();
|
|
16891
|
-
MessengerConnectionError = class extends Error {
|
|
16892
|
-
constructor(message) {
|
|
16893
|
-
super(message);
|
|
16894
|
-
this.name = "MessengerConnectionError";
|
|
16895
|
-
}
|
|
16896
|
-
};
|
|
16897
|
-
Messenger = class {
|
|
16898
|
-
host;
|
|
16899
|
-
port;
|
|
16900
|
-
username;
|
|
16901
|
-
password;
|
|
16902
|
-
fromAddress;
|
|
16903
|
-
fromName;
|
|
16904
|
-
encryption;
|
|
16905
|
-
useTls;
|
|
16906
|
-
imapHost;
|
|
16907
|
-
imapPort;
|
|
16908
|
-
imapUser;
|
|
16909
|
-
imapPass;
|
|
16910
|
-
imapEncryption;
|
|
16911
|
-
constructor(options) {
|
|
16912
|
-
this.host = options?.host ?? process.env.TINA4_MAIL_HOST ?? "localhost";
|
|
16913
|
-
this.port = options?.port ?? parseInt(process.env.TINA4_MAIL_PORT ?? "587", 10);
|
|
16914
|
-
this.username = options?.username ?? process.env.TINA4_MAIL_USERNAME ?? "";
|
|
16915
|
-
this.password = options?.password ?? process.env.TINA4_MAIL_PASSWORD ?? "";
|
|
16916
|
-
this.fromAddress = options?.fromAddress ?? process.env.TINA4_MAIL_FROM ?? (this.username || "noreply@localhost");
|
|
16917
|
-
this.fromName = options?.fromName ?? process.env.TINA4_MAIL_FROM_NAME ?? "";
|
|
16918
|
-
const envEncryption = options?.encryption ?? process.env.TINA4_MAIL_ENCRYPTION;
|
|
16919
|
-
if (envEncryption) {
|
|
16920
|
-
this.encryption = envEncryption.toLowerCase();
|
|
16921
|
-
} else if (options?.useTls !== void 0) {
|
|
16922
|
-
this.encryption = options.useTls ? "tls" : "none";
|
|
16923
|
-
} else {
|
|
16924
|
-
this.encryption = "tls";
|
|
16810
|
+
/**
|
|
16811
|
+
* Read a single message by ID. Searches all folders.
|
|
16812
|
+
*/
|
|
16813
|
+
read(msgId) {
|
|
16814
|
+
const folders = ["inbox", "outbox"];
|
|
16815
|
+
for (const folder of folders) {
|
|
16816
|
+
const filePath = join13(this.mailboxDir, folder, `${msgId}.json`);
|
|
16817
|
+
if (existsSync11(filePath)) {
|
|
16818
|
+
try {
|
|
16819
|
+
const msg = JSON.parse(readFileSync8(filePath, "utf-8"));
|
|
16820
|
+
msg.read = true;
|
|
16821
|
+
writeFileSync7(filePath, JSON.stringify(msg, null, 2));
|
|
16822
|
+
return msg;
|
|
16823
|
+
} catch {
|
|
16824
|
+
return null;
|
|
16825
|
+
}
|
|
16826
|
+
}
|
|
16925
16827
|
}
|
|
16926
|
-
|
|
16927
|
-
this.imapHost = options?.imapHost ?? process.env.TINA4_MAIL_IMAP_HOST ?? "";
|
|
16928
|
-
this.imapPort = options?.imapPort ?? parseInt(process.env.TINA4_MAIL_IMAP_PORT ?? "993", 10);
|
|
16929
|
-
this.imapUser = options?.imapUser ?? process.env.TINA4_MAIL_IMAP_USERNAME ?? this.username;
|
|
16930
|
-
this.imapPass = options?.imapPass ?? process.env.TINA4_MAIL_IMAP_PASSWORD ?? this.password;
|
|
16931
|
-
this.imapEncryption = (options?.imapEncryption ?? process.env.TINA4_MAIL_IMAP_ENCRYPTION ?? "tls").toLowerCase();
|
|
16828
|
+
return null;
|
|
16932
16829
|
}
|
|
16933
16830
|
/**
|
|
16934
|
-
*
|
|
16935
|
-
* Returns one of "tls", "starttls", "none", "ssl".
|
|
16831
|
+
* Count unread messages in the inbox.
|
|
16936
16832
|
*/
|
|
16937
|
-
|
|
16938
|
-
|
|
16833
|
+
unreadCount() {
|
|
16834
|
+
const dir = this.ensureFolder("inbox");
|
|
16835
|
+
let count = 0;
|
|
16836
|
+
try {
|
|
16837
|
+
const files = readdirSync6(dir).filter((f) => f.endsWith(".json"));
|
|
16838
|
+
for (const file of files) {
|
|
16839
|
+
try {
|
|
16840
|
+
const msg = JSON.parse(readFileSync8(join13(dir, file), "utf-8"));
|
|
16841
|
+
if (!msg.read) count++;
|
|
16842
|
+
} catch {
|
|
16843
|
+
}
|
|
16844
|
+
}
|
|
16845
|
+
} catch {
|
|
16846
|
+
}
|
|
16847
|
+
return count;
|
|
16939
16848
|
}
|
|
16940
16849
|
/**
|
|
16941
|
-
*
|
|
16942
|
-
*/
|
|
16943
|
-
async send(to, subject, body, html = false, text, cc, bcc, replyTo, attachments, headers) {
|
|
16944
|
-
const options = { to, subject, body, html, text, cc, bcc, replyTo, attachments, headers };
|
|
16945
|
-
const toList = Array.isArray(options.to) ? options.to : [options.to];
|
|
16946
|
-
const ccList = Array.isArray(options.cc) ? options.cc : options.cc ? [options.cc] : [];
|
|
16947
|
-
const bccList = Array.isArray(options.bcc) ? options.bcc : options.bcc ? [options.bcc] : [];
|
|
16948
|
-
const allRecipients = [...toList, ...ccList, ...bccList];
|
|
16949
|
-
const messageId = `${randomUUID2()}@${this.host}`;
|
|
16950
|
-
if (allRecipients.length === 0) {
|
|
16951
|
-
return { success: false, message: "No recipients specified" };
|
|
16952
|
-
}
|
|
16953
|
-
if (!this.fromAddress) {
|
|
16954
|
-
return { success: false, message: "No from address configured" };
|
|
16955
|
-
}
|
|
16956
|
-
try {
|
|
16957
|
-
let socket;
|
|
16958
|
-
if (this.port === 465) {
|
|
16959
|
-
socket = tls.connect({ host: this.host, port: this.port, rejectUnauthorized: tlsRejectUnauthorized() });
|
|
16960
|
-
await new Promise((resolve31, reject) => {
|
|
16961
|
-
socket.once("secureConnect", resolve31);
|
|
16962
|
-
socket.once("error", reject);
|
|
16963
|
-
});
|
|
16964
|
-
} else {
|
|
16965
|
-
socket = net2.createConnection({ host: this.host, port: this.port });
|
|
16966
|
-
await new Promise((resolve31, reject) => {
|
|
16967
|
-
socket.once("connect", resolve31);
|
|
16968
|
-
socket.once("error", reject);
|
|
16969
|
-
});
|
|
16970
|
-
}
|
|
16971
|
-
const greeting = await readResponse(socket);
|
|
16972
|
-
if (greeting.code !== 220) {
|
|
16973
|
-
socket.destroy();
|
|
16974
|
-
return { success: false, message: `SMTP greeting failed: ${greeting.text}` };
|
|
16975
|
-
}
|
|
16976
|
-
const ehlo = await sendCommand(socket, `EHLO ${this.host}`);
|
|
16977
|
-
if (ehlo.code !== 250) {
|
|
16978
|
-
socket.destroy();
|
|
16979
|
-
return { success: false, message: `EHLO failed: ${ehlo.text}` };
|
|
16980
|
-
}
|
|
16981
|
-
if (this.useTls && this.port !== 465 && ehlo.text.includes("STARTTLS")) {
|
|
16982
|
-
const starttls = await sendCommand(socket, "STARTTLS");
|
|
16983
|
-
if (starttls.code !== 220) {
|
|
16984
|
-
socket.destroy();
|
|
16985
|
-
return { success: false, message: `STARTTLS failed: ${starttls.text}` };
|
|
16986
|
-
}
|
|
16987
|
-
const plainSocket = socket;
|
|
16988
|
-
socket = tls.connect(
|
|
16989
|
-
{ socket: plainSocket, host: this.host, rejectUnauthorized: tlsRejectUnauthorized() }
|
|
16990
|
-
);
|
|
16991
|
-
await new Promise((resolve31, reject) => {
|
|
16992
|
-
socket.once("secureConnect", resolve31);
|
|
16993
|
-
socket.once("error", reject);
|
|
16994
|
-
});
|
|
16995
|
-
const ehlo2 = await sendCommand(socket, `EHLO ${this.host}`);
|
|
16996
|
-
if (ehlo2.code !== 250) {
|
|
16997
|
-
socket.destroy();
|
|
16998
|
-
return { success: false, message: `EHLO after STARTTLS failed: ${ehlo2.text}` };
|
|
16999
|
-
}
|
|
17000
|
-
}
|
|
17001
|
-
if (this.username && this.password) {
|
|
17002
|
-
const auth = await sendCommand(socket, "AUTH LOGIN");
|
|
17003
|
-
if (auth.code !== 334) {
|
|
17004
|
-
socket.destroy();
|
|
17005
|
-
return { success: false, message: `AUTH LOGIN failed: ${auth.text}` };
|
|
17006
|
-
}
|
|
17007
|
-
const userResp = await sendCommand(socket, Buffer.from(this.username).toString("base64"));
|
|
17008
|
-
if (userResp.code !== 334) {
|
|
17009
|
-
socket.destroy();
|
|
17010
|
-
return { success: false, message: `AUTH username failed: ${userResp.text}` };
|
|
17011
|
-
}
|
|
17012
|
-
const passResp = await sendCommand(socket, Buffer.from(this.password).toString("base64"));
|
|
17013
|
-
if (passResp.code !== 235) {
|
|
17014
|
-
socket.destroy();
|
|
17015
|
-
return { success: false, message: `AUTH password failed: ${passResp.text}` };
|
|
17016
|
-
}
|
|
17017
|
-
}
|
|
17018
|
-
const mailFrom = await sendCommand(socket, `MAIL FROM:<${this.fromAddress}>`);
|
|
17019
|
-
if (mailFrom.code !== 250) {
|
|
17020
|
-
socket.destroy();
|
|
17021
|
-
return { success: false, message: `MAIL FROM failed: ${mailFrom.text}` };
|
|
17022
|
-
}
|
|
17023
|
-
for (const recipient of allRecipients) {
|
|
17024
|
-
const rcpt = await sendCommand(socket, `RCPT TO:<${recipient}>`);
|
|
17025
|
-
if (rcpt.code !== 250 && rcpt.code !== 251) {
|
|
17026
|
-
socket.destroy();
|
|
17027
|
-
return { success: false, message: `RCPT TO <${recipient}> failed: ${rcpt.text}` };
|
|
17028
|
-
}
|
|
17029
|
-
}
|
|
17030
|
-
const dataCmd = await sendCommand(socket, "DATA");
|
|
17031
|
-
if (dataCmd.code !== 354) {
|
|
17032
|
-
socket.destroy();
|
|
17033
|
-
return { success: false, message: `DATA failed: ${dataCmd.text}` };
|
|
17034
|
-
}
|
|
17035
|
-
const mimeMessage = buildMimeMessage({
|
|
17036
|
-
from: this.fromAddress,
|
|
17037
|
-
fromName: this.fromName,
|
|
17038
|
-
to: toList,
|
|
17039
|
-
cc: ccList,
|
|
17040
|
-
subject: options.subject,
|
|
17041
|
-
body: options.body,
|
|
17042
|
-
html: options.html ?? false,
|
|
17043
|
-
text: options.text,
|
|
17044
|
-
replyTo: options.replyTo,
|
|
17045
|
-
attachments: options.attachments,
|
|
17046
|
-
headers: options.headers,
|
|
17047
|
-
messageId
|
|
17048
|
-
});
|
|
17049
|
-
const endData = await sendCommand(socket, mimeMessage + "\r\n.");
|
|
17050
|
-
if (endData.code !== 250) {
|
|
17051
|
-
socket.destroy();
|
|
17052
|
-
return { success: false, message: `Message delivery failed: ${endData.text}` };
|
|
17053
|
-
}
|
|
17054
|
-
await sendCommand(socket, "QUIT");
|
|
17055
|
-
socket.destroy();
|
|
17056
|
-
return { success: true, message: "Email sent successfully", id: messageId };
|
|
17057
|
-
} catch (err) {
|
|
17058
|
-
const errMsg = err instanceof Error ? err.message : String(err);
|
|
17059
|
-
return { success: false, message: `SMTP error: ${errMsg}` };
|
|
17060
|
-
}
|
|
17061
|
-
}
|
|
17062
|
-
/**
|
|
17063
|
-
* Test the SMTP connection without sending an email.
|
|
17064
|
-
*/
|
|
17065
|
-
async testConnection() {
|
|
17066
|
-
try {
|
|
17067
|
-
let socket;
|
|
17068
|
-
if (this.port === 465) {
|
|
17069
|
-
socket = tls.connect({ host: this.host, port: this.port, rejectUnauthorized: tlsRejectUnauthorized() });
|
|
17070
|
-
await new Promise((resolve31, reject) => {
|
|
17071
|
-
socket.once("secureConnect", resolve31);
|
|
17072
|
-
socket.once("error", reject);
|
|
17073
|
-
});
|
|
17074
|
-
} else {
|
|
17075
|
-
socket = net2.createConnection({ host: this.host, port: this.port });
|
|
17076
|
-
await new Promise((resolve31, reject) => {
|
|
17077
|
-
socket.once("connect", resolve31);
|
|
17078
|
-
socket.once("error", reject);
|
|
17079
|
-
});
|
|
17080
|
-
}
|
|
17081
|
-
const greeting = await readResponse(socket);
|
|
17082
|
-
if (greeting.code !== 220) {
|
|
17083
|
-
socket.destroy();
|
|
17084
|
-
return { success: false, message: `SMTP greeting failed: ${greeting.text}` };
|
|
17085
|
-
}
|
|
17086
|
-
const ehlo = await sendCommand(socket, `EHLO ${this.host}`);
|
|
17087
|
-
if (ehlo.code !== 250) {
|
|
17088
|
-
socket.destroy();
|
|
17089
|
-
return { success: false, message: `EHLO failed: ${ehlo.text}` };
|
|
17090
|
-
}
|
|
17091
|
-
await sendCommand(socket, "QUIT");
|
|
17092
|
-
socket.destroy();
|
|
17093
|
-
return { success: true, message: `Connected to ${this.host}:${this.port}` };
|
|
17094
|
-
} catch (err) {
|
|
17095
|
-
const errMsg = err instanceof Error ? err.message : String(err);
|
|
17096
|
-
return { success: false, message: `Connection failed: ${errMsg}` };
|
|
17097
|
-
}
|
|
17098
|
-
}
|
|
17099
|
-
// ── IMAP (Read) ────────────────────────────────────────────
|
|
17100
|
-
/**
|
|
17101
|
-
* Connect to the IMAP server via raw TCP/TLS.
|
|
17102
|
-
* Returns the socket and reads the greeting.
|
|
17103
|
-
*/
|
|
17104
|
-
async imapConnect() {
|
|
17105
|
-
if (!this.imapHost) {
|
|
17106
|
-
throw new Error("IMAP host not configured (set imapHost or IMAP_HOST env)");
|
|
17107
|
-
}
|
|
17108
|
-
let socket;
|
|
17109
|
-
const useTls = this.imapEncryption === "tls" || this.imapEncryption === "ssl" || this.imapEncryption === "" && this.imapPort === 993;
|
|
17110
|
-
if (useTls) {
|
|
17111
|
-
socket = tls.connect({ host: this.imapHost, port: this.imapPort, rejectUnauthorized: tlsRejectUnauthorized() });
|
|
17112
|
-
await new Promise((resolve31, reject) => {
|
|
17113
|
-
socket.once("secureConnect", resolve31);
|
|
17114
|
-
socket.once("error", reject);
|
|
17115
|
-
});
|
|
17116
|
-
} else {
|
|
17117
|
-
socket = net2.createConnection({ host: this.imapHost, port: this.imapPort });
|
|
17118
|
-
await new Promise((resolve31, reject) => {
|
|
17119
|
-
socket.once("connect", resolve31);
|
|
17120
|
-
socket.once("error", reject);
|
|
17121
|
-
});
|
|
17122
|
-
}
|
|
17123
|
-
await imapReadLine(socket);
|
|
17124
|
-
if (this.imapUser && this.imapPass) {
|
|
17125
|
-
const loginResp = await imapCommand(socket, `LOGIN ${imapQuote(this.imapUser)} ${imapQuote(this.imapPass)}`);
|
|
17126
|
-
if (!loginResp.includes("OK")) {
|
|
17127
|
-
socket.destroy();
|
|
17128
|
-
throw new Error(`IMAP login failed: ${loginResp}`);
|
|
17129
|
-
}
|
|
17130
|
-
}
|
|
17131
|
-
return socket;
|
|
17132
|
-
}
|
|
17133
|
-
/**
|
|
17134
|
-
* Disconnect from IMAP cleanly.
|
|
17135
|
-
*/
|
|
17136
|
-
async imapDisconnect(socket) {
|
|
17137
|
-
try {
|
|
17138
|
-
await imapCommand(socket, "LOGOUT");
|
|
17139
|
-
} catch {
|
|
17140
|
-
}
|
|
17141
|
-
socket.destroy();
|
|
17142
|
-
}
|
|
17143
|
-
/**
|
|
17144
|
-
* Fetch latest messages from a folder.
|
|
17145
|
-
* Returns list of message summaries.
|
|
17146
|
-
*/
|
|
17147
|
-
async inbox(limit = 20, offset = 0, folder = "INBOX") {
|
|
17148
|
-
let socket;
|
|
17149
|
-
try {
|
|
17150
|
-
socket = await this.imapConnect();
|
|
17151
|
-
} catch (err) {
|
|
17152
|
-
throw imapFail("inbox", err);
|
|
17153
|
-
}
|
|
17154
|
-
try {
|
|
17155
|
-
await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
|
|
17156
|
-
const searchResp = await imapCommand(socket, "SEARCH ALL");
|
|
17157
|
-
const uids = parseSearchResponse(searchResp);
|
|
17158
|
-
if (uids.length === 0) return [];
|
|
17159
|
-
uids.reverse();
|
|
17160
|
-
const selected = uids.slice(offset, offset + limit);
|
|
17161
|
-
if (selected.length === 0) return [];
|
|
17162
|
-
const messages = [];
|
|
17163
|
-
for (const uid of selected) {
|
|
17164
|
-
const fetchResp = await imapCommand(socket, `FETCH ${uid} (FLAGS BODY.PEEK[HEADER.FIELDS (FROM TO SUBJECT DATE)])`);
|
|
17165
|
-
messages.push(parseHeaderResponse(uid, fetchResp));
|
|
17166
|
-
}
|
|
17167
|
-
return messages;
|
|
17168
|
-
} catch (err) {
|
|
17169
|
-
throw imapFail("inbox", err);
|
|
17170
|
-
} finally {
|
|
17171
|
-
await this.imapDisconnect(socket);
|
|
17172
|
-
}
|
|
17173
|
-
}
|
|
17174
|
-
/**
|
|
17175
|
-
* Read a single message by sequence number or UID.
|
|
17176
|
-
*/
|
|
17177
|
-
async read(uid, folder = "INBOX") {
|
|
17178
|
-
let socket;
|
|
17179
|
-
try {
|
|
17180
|
-
socket = await this.imapConnect();
|
|
17181
|
-
} catch (err) {
|
|
17182
|
-
throw imapFail("read", err);
|
|
17183
|
-
}
|
|
17184
|
-
try {
|
|
17185
|
-
await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
|
|
17186
|
-
const fetchResp = await imapCommand(socket, `FETCH ${uid} (FLAGS BODY[])`);
|
|
17187
|
-
if (!/\{\d+\}/.test(fetchResp)) {
|
|
17188
|
-
return emptyFullMessage(uid);
|
|
17189
|
-
}
|
|
17190
|
-
await imapCommand(socket, `STORE ${uid} +FLAGS (\\Seen)`);
|
|
17191
|
-
return parseFullMessage(uid, fetchResp);
|
|
17192
|
-
} catch (err) {
|
|
17193
|
-
throw imapFail("read", err);
|
|
17194
|
-
} finally {
|
|
17195
|
-
await this.imapDisconnect(socket);
|
|
17196
|
-
}
|
|
17197
|
-
}
|
|
17198
|
-
/**
|
|
17199
|
-
* Search messages using IMAP search criteria.
|
|
17200
|
-
*/
|
|
17201
|
-
async search(folder = "INBOX", subject, sender, since, before, unseenOnly = false, limit = 50) {
|
|
17202
|
-
const criteria = ["ALL"];
|
|
17203
|
-
if (subject) criteria.push(`SUBJECT "${subject}"`);
|
|
17204
|
-
if (sender) criteria.push(`FROM "${sender}"`);
|
|
17205
|
-
if (since) criteria.push(`SINCE ${since}`);
|
|
17206
|
-
if (before) criteria.push(`BEFORE ${before}`);
|
|
17207
|
-
if (unseenOnly) criteria.push("UNSEEN");
|
|
17208
|
-
const query = criteria.join(" ");
|
|
17209
|
-
let socket;
|
|
17210
|
-
try {
|
|
17211
|
-
socket = await this.imapConnect();
|
|
17212
|
-
} catch (err) {
|
|
17213
|
-
throw imapFail("search", err);
|
|
17214
|
-
}
|
|
17215
|
-
try {
|
|
17216
|
-
await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
|
|
17217
|
-
const searchResp = await imapCommand(socket, `SEARCH ${query}`);
|
|
17218
|
-
const uids = parseSearchResponse(searchResp);
|
|
17219
|
-
if (uids.length === 0) return [];
|
|
17220
|
-
uids.reverse();
|
|
17221
|
-
const messages = [];
|
|
17222
|
-
for (const uid of uids.slice(0, limit)) {
|
|
17223
|
-
const fetchResp = await imapCommand(socket, `FETCH ${uid} (FLAGS BODY.PEEK[HEADER.FIELDS (FROM TO SUBJECT DATE)])`);
|
|
17224
|
-
messages.push(parseHeaderResponse(uid, fetchResp));
|
|
17225
|
-
}
|
|
17226
|
-
return messages;
|
|
17227
|
-
} catch (err) {
|
|
17228
|
-
throw imapFail("search", err);
|
|
17229
|
-
} finally {
|
|
17230
|
-
await this.imapDisconnect(socket);
|
|
17231
|
-
}
|
|
17232
|
-
}
|
|
17233
|
-
/**
|
|
17234
|
-
* Delete a message by UID.
|
|
17235
|
-
*/
|
|
17236
|
-
async deleteMessage(uid, folder = "INBOX") {
|
|
17237
|
-
const socket = await this.imapConnect();
|
|
17238
|
-
try {
|
|
17239
|
-
await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
|
|
17240
|
-
await imapCommand(socket, `STORE ${uid} +FLAGS (\\Deleted)`);
|
|
17241
|
-
await imapCommand(socket, "EXPUNGE");
|
|
17242
|
-
} finally {
|
|
17243
|
-
await this.imapDisconnect(socket);
|
|
17244
|
-
}
|
|
17245
|
-
}
|
|
17246
|
-
/**
|
|
17247
|
-
* Mark a message as read.
|
|
17248
|
-
*/
|
|
17249
|
-
async markRead(uid, folder = "INBOX") {
|
|
17250
|
-
const socket = await this.imapConnect();
|
|
17251
|
-
try {
|
|
17252
|
-
await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
|
|
17253
|
-
await imapCommand(socket, `STORE ${uid} +FLAGS (\\Seen)`);
|
|
17254
|
-
} finally {
|
|
17255
|
-
await this.imapDisconnect(socket);
|
|
17256
|
-
}
|
|
17257
|
-
}
|
|
17258
|
-
/**
|
|
17259
|
-
* Count unseen messages in a folder.
|
|
17260
|
-
*/
|
|
17261
|
-
async unread(folder = "INBOX") {
|
|
17262
|
-
let socket;
|
|
17263
|
-
try {
|
|
17264
|
-
socket = await this.imapConnect();
|
|
17265
|
-
} catch (err) {
|
|
17266
|
-
throw imapFail("unread", err);
|
|
17267
|
-
}
|
|
17268
|
-
try {
|
|
17269
|
-
await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
|
|
17270
|
-
const searchResp = await imapCommand(socket, "SEARCH UNSEEN");
|
|
17271
|
-
return parseSearchResponse(searchResp).length;
|
|
17272
|
-
} catch (err) {
|
|
17273
|
-
throw imapFail("unread", err);
|
|
17274
|
-
} finally {
|
|
17275
|
-
await this.imapDisconnect(socket);
|
|
17276
|
-
}
|
|
17277
|
-
}
|
|
17278
|
-
/**
|
|
17279
|
-
* List available IMAP folders/mailboxes.
|
|
17280
|
-
*/
|
|
17281
|
-
async folders() {
|
|
17282
|
-
let socket;
|
|
17283
|
-
try {
|
|
17284
|
-
socket = await this.imapConnect();
|
|
17285
|
-
} catch (err) {
|
|
17286
|
-
throw imapFail("folders", err);
|
|
17287
|
-
}
|
|
17288
|
-
try {
|
|
17289
|
-
const resp = await imapCommand(socket, 'LIST "" "*"');
|
|
17290
|
-
const result = [];
|
|
17291
|
-
for (const line of resp.split("\r\n")) {
|
|
17292
|
-
const m = line.match(/\* LIST \([^)]*\) "[^"]*" "?([^"\r\n]+)"?/i);
|
|
17293
|
-
if (m) result.push(m[1]);
|
|
17294
|
-
}
|
|
17295
|
-
return result;
|
|
17296
|
-
} catch (err) {
|
|
17297
|
-
throw imapFail("folders", err);
|
|
17298
|
-
} finally {
|
|
17299
|
-
await this.imapDisconnect(socket);
|
|
17300
|
-
}
|
|
17301
|
-
}
|
|
17302
|
-
/**
|
|
17303
|
-
* Test IMAP connectivity without reading.
|
|
17304
|
-
*/
|
|
17305
|
-
async testImapConnection() {
|
|
17306
|
-
try {
|
|
17307
|
-
const socket = await this.imapConnect();
|
|
17308
|
-
await this.imapDisconnect(socket);
|
|
17309
|
-
return { success: true, message: `Connected to ${this.imapHost}:${this.imapPort}` };
|
|
17310
|
-
} catch (err) {
|
|
17311
|
-
const errMsg = err instanceof Error ? err.message : String(err);
|
|
17312
|
-
return { success: false, message: `IMAP connection failed: ${errMsg}` };
|
|
17313
|
-
}
|
|
17314
|
-
}
|
|
17315
|
-
};
|
|
17316
|
-
imapTagCounter = 0;
|
|
17317
|
-
}
|
|
17318
|
-
});
|
|
17319
|
-
|
|
17320
|
-
// ../core/src/devMailbox.ts
|
|
17321
|
-
import { mkdirSync as mkdirSync9, readdirSync as readdirSync6, readFileSync as readFileSync9, writeFileSync as writeFileSync7, unlinkSync as unlinkSync4, existsSync as existsSync11 } from "node:fs";
|
|
17322
|
-
import { join as join13 } from "node:path";
|
|
17323
|
-
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
17324
|
-
function createMessenger() {
|
|
17325
|
-
const debug = process.env.TINA4_DEBUG;
|
|
17326
|
-
const smtpHost = process.env.TINA4_MAIL_HOST;
|
|
17327
|
-
const isProd = !isTruthy(debug) && process.env.NODE_ENV === "production";
|
|
17328
|
-
if (isTruthy(debug)) {
|
|
17329
|
-
return new DevMailbox();
|
|
17330
|
-
}
|
|
17331
|
-
if (!smtpHost) {
|
|
17332
|
-
return new DevMailbox();
|
|
17333
|
-
}
|
|
17334
|
-
if (!isProd) {
|
|
17335
|
-
return new DevMailbox();
|
|
17336
|
-
}
|
|
17337
|
-
return new Messenger();
|
|
17338
|
-
}
|
|
17339
|
-
var DevMailbox;
|
|
17340
|
-
var init_devMailbox = __esm({
|
|
17341
|
-
"../core/src/devMailbox.ts"() {
|
|
17342
|
-
"use strict";
|
|
17343
|
-
init_messenger();
|
|
17344
|
-
init_dotenv();
|
|
17345
|
-
DevMailbox = class {
|
|
17346
|
-
mailboxDir;
|
|
17347
|
-
constructor(mailboxDir) {
|
|
17348
|
-
this.mailboxDir = mailboxDir ?? process.env.TINA4_MAILBOX_DIR ?? "data/mailbox";
|
|
17349
|
-
}
|
|
17350
|
-
/**
|
|
17351
|
-
* Ensure a folder directory exists.
|
|
17352
|
-
*/
|
|
17353
|
-
ensureFolder(folder) {
|
|
17354
|
-
const dir = join13(this.mailboxDir, folder);
|
|
17355
|
-
mkdirSync9(dir, { recursive: true });
|
|
17356
|
-
return dir;
|
|
17357
|
-
}
|
|
17358
|
-
/**
|
|
17359
|
-
* Capture an email to the dev mailbox instead of sending it.
|
|
17360
|
-
*/
|
|
17361
|
-
capture(to, subject, body, html = false, cc = [], bcc = [], replyTo, attachments = [], from) {
|
|
17362
|
-
const id = randomUUID3();
|
|
17363
|
-
const toList = Array.isArray(to) ? to : [to];
|
|
17364
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
17365
|
-
const message = {
|
|
17366
|
-
id,
|
|
17367
|
-
type: "outbox",
|
|
17368
|
-
from: from ?? process.env.TINA4_MAIL_FROM ?? "dev@localhost",
|
|
17369
|
-
to: toList,
|
|
17370
|
-
cc,
|
|
17371
|
-
bcc,
|
|
17372
|
-
reply_to: replyTo,
|
|
17373
|
-
subject,
|
|
17374
|
-
body,
|
|
17375
|
-
html,
|
|
17376
|
-
attachments,
|
|
17377
|
-
date: now,
|
|
17378
|
-
read: false
|
|
17379
|
-
};
|
|
17380
|
-
const outboxDir = this.ensureFolder("outbox");
|
|
17381
|
-
writeFileSync7(join13(outboxDir, `${id}.json`), JSON.stringify(message, null, 2));
|
|
17382
|
-
const inboxDir = this.ensureFolder("inbox");
|
|
17383
|
-
const inboxMessage = { ...message, type: "inbox" };
|
|
17384
|
-
writeFileSync7(join13(inboxDir, `${id}.json`), JSON.stringify(inboxMessage, null, 2));
|
|
17385
|
-
return { success: true, message: "Email captured to dev mailbox", id };
|
|
17386
|
-
}
|
|
17387
|
-
/**
|
|
17388
|
-
* List messages from a folder (default: inbox).
|
|
17389
|
-
*/
|
|
17390
|
-
inbox(limit = 50, offset = 0, folder = "inbox") {
|
|
17391
|
-
const dir = this.ensureFolder(folder);
|
|
17392
|
-
const results = [];
|
|
17393
|
-
let files;
|
|
17394
|
-
try {
|
|
17395
|
-
files = readdirSync6(dir).filter((f) => f.endsWith(".json")).sort().reverse();
|
|
17396
|
-
} catch {
|
|
17397
|
-
return [];
|
|
17398
|
-
}
|
|
17399
|
-
const sliced = files.slice(offset, offset + limit);
|
|
17400
|
-
for (const file of sliced) {
|
|
17401
|
-
try {
|
|
17402
|
-
const msg = JSON.parse(readFileSync9(join13(dir, file), "utf-8"));
|
|
17403
|
-
results.push(msg);
|
|
17404
|
-
} catch {
|
|
17405
|
-
}
|
|
17406
|
-
}
|
|
17407
|
-
return results;
|
|
17408
|
-
}
|
|
17409
|
-
/**
|
|
17410
|
-
* Read a single message by ID. Searches all folders.
|
|
17411
|
-
*/
|
|
17412
|
-
read(msgId) {
|
|
17413
|
-
const folders = ["inbox", "outbox"];
|
|
17414
|
-
for (const folder of folders) {
|
|
17415
|
-
const filePath = join13(this.mailboxDir, folder, `${msgId}.json`);
|
|
17416
|
-
if (existsSync11(filePath)) {
|
|
17417
|
-
try {
|
|
17418
|
-
const msg = JSON.parse(readFileSync9(filePath, "utf-8"));
|
|
17419
|
-
msg.read = true;
|
|
17420
|
-
writeFileSync7(filePath, JSON.stringify(msg, null, 2));
|
|
17421
|
-
return msg;
|
|
17422
|
-
} catch {
|
|
17423
|
-
return null;
|
|
17424
|
-
}
|
|
17425
|
-
}
|
|
17426
|
-
}
|
|
17427
|
-
return null;
|
|
17428
|
-
}
|
|
17429
|
-
/**
|
|
17430
|
-
* Count unread messages in the inbox.
|
|
17431
|
-
*/
|
|
17432
|
-
unreadCount() {
|
|
17433
|
-
const dir = this.ensureFolder("inbox");
|
|
17434
|
-
let count = 0;
|
|
17435
|
-
try {
|
|
17436
|
-
const files = readdirSync6(dir).filter((f) => f.endsWith(".json"));
|
|
17437
|
-
for (const file of files) {
|
|
17438
|
-
try {
|
|
17439
|
-
const msg = JSON.parse(readFileSync9(join13(dir, file), "utf-8"));
|
|
17440
|
-
if (!msg.read) count++;
|
|
17441
|
-
} catch {
|
|
17442
|
-
}
|
|
17443
|
-
}
|
|
17444
|
-
} catch {
|
|
17445
|
-
}
|
|
17446
|
-
return count;
|
|
17447
|
-
}
|
|
17448
|
-
/**
|
|
17449
|
-
* Delete a message by ID. Removes from all folders.
|
|
16850
|
+
* Delete a message by ID. Removes from all folders.
|
|
17450
16851
|
*/
|
|
17451
16852
|
delete(msgId) {
|
|
17452
16853
|
let deleted = false;
|
|
@@ -17508,7 +16909,7 @@ var init_devMailbox = __esm({
|
|
|
17508
16909
|
const subject = subjects[i % subjects.length];
|
|
17509
16910
|
const from = senders[i % senders.length];
|
|
17510
16911
|
const date = new Date(Date.now() - i * 36e5).toISOString();
|
|
17511
|
-
const id =
|
|
16912
|
+
const id = randomUUID2();
|
|
17512
16913
|
const message = {
|
|
17513
16914
|
id,
|
|
17514
16915
|
type: "inbox",
|
|
@@ -18629,7 +18030,7 @@ var init_metrics = __esm({
|
|
|
18629
18030
|
});
|
|
18630
18031
|
|
|
18631
18032
|
// ../core/src/feedback.ts
|
|
18632
|
-
import { readFileSync as
|
|
18033
|
+
import { readFileSync as readFileSync10, existsSync as existsSync13 } from "node:fs";
|
|
18633
18034
|
import { dirname as dirname6, join as join15, resolve as resolve9 } from "node:path";
|
|
18634
18035
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
18635
18036
|
function feedbackEnabled() {
|
|
@@ -18770,7 +18171,7 @@ var init_feedback = __esm({
|
|
|
18770
18171
|
handleFeedbackWidgetJs = (_req, res) => {
|
|
18771
18172
|
let body;
|
|
18772
18173
|
if (existsSync13(WIDGET_BUNDLE_PATH)) {
|
|
18773
|
-
body =
|
|
18174
|
+
body = readFileSync10(WIDGET_BUNDLE_PATH);
|
|
18774
18175
|
} else {
|
|
18775
18176
|
body = "console.warn('tina4-feedback-widget bundle not built yet');";
|
|
18776
18177
|
}
|
|
@@ -20478,7 +19879,7 @@ __export(errorOverlay_exports, {
|
|
|
20478
19879
|
renderErrorOverlay: () => renderErrorOverlay,
|
|
20479
19880
|
renderProductionError: () => renderProductionError
|
|
20480
19881
|
});
|
|
20481
|
-
import { readFileSync as
|
|
19882
|
+
import { readFileSync as readFileSync12, statSync as statSync10 } from "node:fs";
|
|
20482
19883
|
import { resolve as resolve11 } from "node:path";
|
|
20483
19884
|
function esc(text) {
|
|
20484
19885
|
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
@@ -20502,7 +19903,7 @@ function parseStack(stack) {
|
|
|
20502
19903
|
function readSourceLines(filename, lineno) {
|
|
20503
19904
|
try {
|
|
20504
19905
|
const absPath = resolve11(filename);
|
|
20505
|
-
const content =
|
|
19906
|
+
const content = readFileSync12(absPath, "utf-8");
|
|
20506
19907
|
const allLines = content.split("\n");
|
|
20507
19908
|
const start2 = Math.max(0, lineno - CONTEXT_LINES - 1);
|
|
20508
19909
|
const end = Math.min(allLines.length, lineno + CONTEXT_LINES);
|
|
@@ -21766,8 +21167,8 @@ __export(context_exports, {
|
|
|
21766
21167
|
fts5Supported: () => fts5Supported
|
|
21767
21168
|
});
|
|
21768
21169
|
import { DatabaseSync as DatabaseSync3 } from "node:sqlite";
|
|
21769
|
-
import { existsSync as existsSync15, mkdirSync as mkdirSync11, readFileSync as
|
|
21770
|
-
import { basename as
|
|
21170
|
+
import { existsSync as existsSync15, mkdirSync as mkdirSync11, readFileSync as readFileSync13, readdirSync as readdirSync9, realpathSync as realpathSync2 } from "node:fs";
|
|
21171
|
+
import { basename as basename4, dirname as dirname8, extname as extname6, isAbsolute as isAbsolute5, join as join17, relative as relative4, resolve as resolve12 } from "node:path";
|
|
21771
21172
|
function fts5Supported() {
|
|
21772
21173
|
try {
|
|
21773
21174
|
const conn = new DatabaseSync3(":memory:");
|
|
@@ -21790,7 +21191,7 @@ function realResolve(abs) {
|
|
|
21790
21191
|
} catch {
|
|
21791
21192
|
}
|
|
21792
21193
|
try {
|
|
21793
|
-
return join17(realpathSync2(dirname8(abs)),
|
|
21194
|
+
return join17(realpathSync2(dirname8(abs)), basename4(abs));
|
|
21794
21195
|
} catch {
|
|
21795
21196
|
return abs;
|
|
21796
21197
|
}
|
|
@@ -21903,7 +21304,7 @@ var init_context = __esm({
|
|
|
21903
21304
|
// ── indexing ───────────────────────────────────────────────
|
|
21904
21305
|
static chunksFor(label, text) {
|
|
21905
21306
|
const ext = extname6(label).toLowerCase();
|
|
21906
|
-
const special = SPECIAL_FILES.has(
|
|
21307
|
+
const special = SPECIAL_FILES.has(basename4(label).toLowerCase());
|
|
21907
21308
|
if (CODE_EXTS.has(ext) || CONFIG_EXTS.has(ext) || special) {
|
|
21908
21309
|
return chunkCode(text, label);
|
|
21909
21310
|
}
|
|
@@ -21920,7 +21321,7 @@ var init_context = __esm({
|
|
|
21920
21321
|
const stored = label != null ? String(label) : String(file);
|
|
21921
21322
|
let text;
|
|
21922
21323
|
try {
|
|
21923
|
-
text =
|
|
21324
|
+
text = readFileSync13(file, "utf-8");
|
|
21924
21325
|
} catch {
|
|
21925
21326
|
return 0;
|
|
21926
21327
|
}
|
|
@@ -22000,7 +21401,7 @@ var init_context = __esm({
|
|
|
22000
21401
|
if (parts.some((seg) => SKIP_DIRS.has(seg)) || dirParts.some((seg) => seg.startsWith("."))) {
|
|
22001
21402
|
return -1;
|
|
22002
21403
|
}
|
|
22003
|
-
if (!_Context.eligible(
|
|
21404
|
+
if (!_Context.eligible(basename4(rel))) return -1;
|
|
22004
21405
|
const stored = rel;
|
|
22005
21406
|
if (!existsSync15(abs)) {
|
|
22006
21407
|
this.conn.prepare("DELETE FROM chunks WHERE path = ?").run(stored);
|
|
@@ -22092,7 +21493,7 @@ var init_context = __esm({
|
|
|
22092
21493
|
});
|
|
22093
21494
|
|
|
22094
21495
|
// ../core/src/websocketBackplane.ts
|
|
22095
|
-
import { randomUUID as
|
|
21496
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
22096
21497
|
function createBackplane(url) {
|
|
22097
21498
|
const backend = (process.env.TINA4_WS_BACKPLANE ?? "").trim().toLowerCase();
|
|
22098
21499
|
switch (backend) {
|
|
@@ -22122,7 +21523,7 @@ function buildEnvelope(src, kind, message, opts = {}) {
|
|
|
22122
21523
|
return envelope;
|
|
22123
21524
|
}
|
|
22124
21525
|
function randomInstanceId() {
|
|
22125
|
-
return
|
|
21526
|
+
return randomUUID3().replace(/-/g, "").slice(0, 16);
|
|
22126
21527
|
}
|
|
22127
21528
|
var RedisBackplane, NATSBackplane, WS_BACKPLANE_CHANNEL, WsBackplaneManager;
|
|
22128
21529
|
var init_websocketBackplane = __esm({
|
|
@@ -22350,7 +21751,7 @@ __export(websocket_exports, {
|
|
|
22350
21751
|
});
|
|
22351
21752
|
import { createServer } from "node:http";
|
|
22352
21753
|
import { createHash as createHash4 } from "node:crypto";
|
|
22353
|
-
import { randomUUID as
|
|
21754
|
+
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
22354
21755
|
function computeAcceptKey(key) {
|
|
22355
21756
|
return createHash4("sha1").update(key + MAGIC_STRING).digest("base64");
|
|
22356
21757
|
}
|
|
@@ -22478,7 +21879,7 @@ function parseFrame(data) {
|
|
|
22478
21879
|
return { fin: !!fin, opcode, payload: Buffer.from(payload), bytesConsumed: offset + payloadLen };
|
|
22479
21880
|
}
|
|
22480
21881
|
function createRouteConnection(socket, path8, headers, params, auth) {
|
|
22481
|
-
const id =
|
|
21882
|
+
const id = randomUUID4().slice(0, 8);
|
|
22482
21883
|
const send = (message) => {
|
|
22483
21884
|
try {
|
|
22484
21885
|
socket.write(buildFrame(OP_TEXT, Buffer.from(message, "utf-8")));
|
|
@@ -23035,7 +22436,7 @@ var init_websocket = __esm({
|
|
|
23035
22436
|
}
|
|
23036
22437
|
responseLines.push("", "");
|
|
23037
22438
|
socket.write(responseLines.join("\r\n"));
|
|
23038
|
-
const clientId =
|
|
22439
|
+
const clientId = randomUUID4().slice(0, 8);
|
|
23039
22440
|
const client = {
|
|
23040
22441
|
id: clientId,
|
|
23041
22442
|
socket,
|
|
@@ -23323,7 +22724,7 @@ var init_websocket = __esm({
|
|
|
23323
22724
|
|
|
23324
22725
|
// ../core/src/queueBackends/rabbitmqBackend.ts
|
|
23325
22726
|
import { execFileSync } from "node:child_process";
|
|
23326
|
-
import { randomUUID as
|
|
22727
|
+
import { randomUUID as randomUUID5 } from "node:crypto";
|
|
23327
22728
|
function parseAmqpUrl(url) {
|
|
23328
22729
|
const config = {};
|
|
23329
22730
|
let rest = url.replace(/^amqps:\/\//, "").replace(/^amqp:\/\//, "");
|
|
@@ -23767,7 +23168,7 @@ var init_rabbitmqBackend = __esm({
|
|
|
23767
23168
|
}
|
|
23768
23169
|
}
|
|
23769
23170
|
push(queue, payload, _delay) {
|
|
23770
|
-
const id =
|
|
23171
|
+
const id = randomUUID5();
|
|
23771
23172
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
23772
23173
|
const job = {
|
|
23773
23174
|
id,
|
|
@@ -23806,7 +23207,7 @@ var init_rabbitmqBackend = __esm({
|
|
|
23806
23207
|
|
|
23807
23208
|
// ../core/src/queueBackends/kafkaBackend.ts
|
|
23808
23209
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
23809
|
-
import { randomUUID as
|
|
23210
|
+
import { randomUUID as randomUUID6 } from "node:crypto";
|
|
23810
23211
|
function kafkaSecurityConfig(env = process.env) {
|
|
23811
23212
|
const mapping = [
|
|
23812
23213
|
["security.protocol", "SECURITY_PROTOCOL"],
|
|
@@ -23830,7 +23231,7 @@ var init_kafkaBackend = __esm({
|
|
|
23830
23231
|
"use strict";
|
|
23831
23232
|
API_PRODUCE = 0;
|
|
23832
23233
|
API_FETCH = 1;
|
|
23833
|
-
KafkaBackend = class {
|
|
23234
|
+
KafkaBackend = class _KafkaBackend {
|
|
23834
23235
|
brokers;
|
|
23835
23236
|
groupId;
|
|
23836
23237
|
constructor(config) {
|
|
@@ -24192,12 +23593,15 @@ var init_kafkaBackend = __esm({
|
|
|
24192
23593
|
if (errCode === 0) {
|
|
24193
23594
|
finish("__PUBLISHED__", 0);
|
|
24194
23595
|
} else {
|
|
23596
|
+
// Report the CODE, not just "it failed" \u2014 the caller decides
|
|
23597
|
+
// whether it is retriable (3/5, the async topic-creation race)
|
|
23598
|
+
// or fatal (e.g. 29 TOPIC_AUTHORIZATION_FAILED).
|
|
24195
23599
|
process.stderr.write("Produce error code " + errCode);
|
|
24196
|
-
finish("
|
|
23600
|
+
finish("__PRODUCEERROR__" + errCode, 0);
|
|
24197
23601
|
}
|
|
24198
23602
|
} catch (e) {
|
|
24199
23603
|
process.stderr.write("produce parse: " + e.message);
|
|
24200
|
-
finish("
|
|
23604
|
+
finish("__PARSEERROR__produce: " + e.message, 0);
|
|
24201
23605
|
}
|
|
24202
23606
|
return;
|
|
24203
23607
|
} else if (operation === "get") {
|
|
@@ -24211,6 +23615,7 @@ var init_kafkaBackend = __esm({
|
|
|
24211
23615
|
pos += 4; // throttleTimeMs (v1+)
|
|
24212
23616
|
const topicCount = buffer.readInt32BE(pos); pos += 4;
|
|
24213
23617
|
let out = "__EMPTY__";
|
|
23618
|
+
let fatalCode = 0;
|
|
24214
23619
|
for (let t = 0; t < topicCount; t++) {
|
|
24215
23620
|
const tl = buffer.readInt16BE(pos); pos += 2 + tl;
|
|
24216
23621
|
const pc = buffer.readInt32BE(pos); pos += 4;
|
|
@@ -24222,6 +23627,14 @@ var init_kafkaBackend = __esm({
|
|
|
24222
23627
|
const abortedCount = buffer.readInt32BE(pos); pos += 4;
|
|
24223
23628
|
if (abortedCount > 0) pos += abortedCount * 16; // (-1 => none, skip)
|
|
24224
23629
|
const recSetSize = buffer.readInt32BE(pos); pos += 4;
|
|
23630
|
+
// 3 = UNKNOWN_TOPIC_OR_PARTITION, 5 = LEADER_NOT_AVAILABLE:
|
|
23631
|
+
// "nothing to read here yet", which a consumer that starts
|
|
23632
|
+
// before its producer hits on every cold start. Any OTHER code
|
|
23633
|
+
// (29 TOPIC_AUTHORIZATION_FAILED, 13 STALE_CONTROLLER_EPOCH, \u2026)
|
|
23634
|
+
// is a real failure and must NOT be reported as an empty queue.
|
|
23635
|
+
if (errCode !== 0 && errCode !== 3 && errCode !== 5) {
|
|
23636
|
+
fatalCode = errCode;
|
|
23637
|
+
}
|
|
24225
23638
|
if (errCode === 0 && recSetSize > 0) {
|
|
24226
23639
|
const val = firstRecordValue(buffer, pos, pos + recSetSize);
|
|
24227
23640
|
if (val !== null) out = val;
|
|
@@ -24229,21 +23642,33 @@ var init_kafkaBackend = __esm({
|
|
|
24229
23642
|
pos += recSetSize > 0 ? recSetSize : 0;
|
|
24230
23643
|
}
|
|
24231
23644
|
}
|
|
23645
|
+
if (fatalCode !== 0) {
|
|
23646
|
+
process.stderr.write("Fetch error code " + fatalCode);
|
|
23647
|
+
finish("__FETCHERROR__" + fatalCode, 0);
|
|
23648
|
+
return;
|
|
23649
|
+
}
|
|
24232
23650
|
finish(out, 0);
|
|
24233
23651
|
} catch (e) {
|
|
23652
|
+
// A parse failure is NOT an empty queue either \u2014 say so.
|
|
24234
23653
|
process.stderr.write("fetch parse: " + e.message);
|
|
24235
|
-
finish("
|
|
23654
|
+
finish("__PARSEERROR__fetch: " + e.message, 0);
|
|
24236
23655
|
}
|
|
24237
23656
|
return;
|
|
24238
23657
|
}
|
|
24239
23658
|
});
|
|
24240
23659
|
|
|
23660
|
+
// Report the reason on STDOUT and exit 0. Writing it to stderr and
|
|
23661
|
+
// exiting non-zero LOST it: stderr to a pipe is an async write and
|
|
23662
|
+
// process.exit() truncates it, so the parent saw an empty stderr and fell
|
|
23663
|
+
// back to execFileSync's message -- which embeds this entire script.
|
|
23664
|
+
// stdout is flushed by finish()'s write callback, so it survives.
|
|
24241
23665
|
sock.on("error", (err) => {
|
|
24242
|
-
|
|
24243
|
-
finish("", 1);
|
|
23666
|
+
finish("__TRANSPORTERROR__" + err.message, 0);
|
|
24244
23667
|
});
|
|
24245
23668
|
|
|
24246
|
-
var timer = setTimeout(() => {
|
|
23669
|
+
var timer = setTimeout(() => {
|
|
23670
|
+
finish("__TRANSPORTERROR__timed out after 10s talking to " + host + ":" + port, 0);
|
|
23671
|
+
}, 10000);
|
|
24247
23672
|
`;
|
|
24248
23673
|
try {
|
|
24249
23674
|
const result = execFileSync2(process.execPath, ["-e", script], {
|
|
@@ -24252,12 +23677,51 @@ var init_kafkaBackend = __esm({
|
|
|
24252
23677
|
stdio: ["pipe", "pipe", "pipe"]
|
|
24253
23678
|
});
|
|
24254
23679
|
return result;
|
|
24255
|
-
} catch {
|
|
24256
|
-
|
|
23680
|
+
} catch (err) {
|
|
23681
|
+
const e = err;
|
|
23682
|
+
const reason = String(e.stderr ?? "").trim() || e.message || "unknown error";
|
|
23683
|
+
const firstLine2 = reason.split("\n", 1)[0].slice(0, 200);
|
|
23684
|
+
return "__TRANSPORTERROR__" + firstLine2;
|
|
23685
|
+
}
|
|
23686
|
+
}
|
|
23687
|
+
/**
|
|
23688
|
+
* Sleep synchronously between produce retries.
|
|
23689
|
+
*
|
|
23690
|
+
* `push()` is synchronous (the whole backend drives its socket through a child
|
|
23691
|
+
* process), so there is no event loop to await on. `Atomics.wait` on a
|
|
23692
|
+
* SharedArrayBuffer is the stdlib way to block a thread for a fixed time --
|
|
23693
|
+
* no dependency, no busy-wait burning CPU.
|
|
23694
|
+
*/
|
|
23695
|
+
static sleepSync(ms) {
|
|
23696
|
+
const shared = new Int32Array(new SharedArrayBuffer(4));
|
|
23697
|
+
Atomics.wait(shared, 0, 0, ms);
|
|
23698
|
+
}
|
|
23699
|
+
/**
|
|
23700
|
+
* Turn a sentinel from the protocol child into a thrown error, or return.
|
|
23701
|
+
*
|
|
23702
|
+
* The wording matches the Python and PHP backends exactly -- the parity rule
|
|
23703
|
+
* covers user-visible error messages, not just behaviour.
|
|
23704
|
+
*/
|
|
23705
|
+
static assertNoError(result, operation, topic) {
|
|
23706
|
+
const fatal = /^__(PRODUCEERROR|FETCHERROR)__(\d+)/.exec(result);
|
|
23707
|
+
if (fatal) {
|
|
23708
|
+
throw new Error(
|
|
23709
|
+
`Kafka rejected the ${operation} for topic ${topic}: error code ${fatal[2]}`
|
|
23710
|
+
);
|
|
23711
|
+
}
|
|
23712
|
+
if (result.startsWith("__TRANSPORTERROR__")) {
|
|
23713
|
+
throw new Error(
|
|
23714
|
+
`Kafka ${operation} for topic ${topic} failed: ` + result.slice("__TRANSPORTERROR__".length)
|
|
23715
|
+
);
|
|
23716
|
+
}
|
|
23717
|
+
if (result.startsWith("__PARSEERROR__")) {
|
|
23718
|
+
throw new Error(
|
|
23719
|
+
`Kafka ${operation} for topic ${topic} returned an unreadable response: ` + result.slice("__PARSEERROR__".length)
|
|
23720
|
+
);
|
|
24257
23721
|
}
|
|
24258
23722
|
}
|
|
24259
23723
|
push(queue, payload, _delay) {
|
|
24260
|
-
const id =
|
|
23724
|
+
const id = randomUUID6();
|
|
24261
23725
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
24262
23726
|
const job = {
|
|
24263
23727
|
id,
|
|
@@ -24267,14 +23731,25 @@ var init_kafkaBackend = __esm({
|
|
|
24267
23731
|
attempts: 0,
|
|
24268
23732
|
delayUntil: null
|
|
24269
23733
|
};
|
|
24270
|
-
const
|
|
24271
|
-
|
|
24272
|
-
|
|
23734
|
+
const body = JSON.stringify(job);
|
|
23735
|
+
let result = "";
|
|
23736
|
+
for (let attempt = 1; attempt <= 10; attempt++) {
|
|
23737
|
+
result = this.execSync("publish", queue, body);
|
|
23738
|
+
if (result.includes("__PUBLISHED__")) {
|
|
23739
|
+
return id;
|
|
23740
|
+
}
|
|
23741
|
+
const retriable = /^__PRODUCEERROR__(3|5)\b/.test(result);
|
|
23742
|
+
if (!retriable || attempt === 10) {
|
|
23743
|
+
break;
|
|
23744
|
+
}
|
|
23745
|
+
_KafkaBackend.sleepSync(200);
|
|
24273
23746
|
}
|
|
24274
|
-
|
|
23747
|
+
_KafkaBackend.assertNoError(result, "produce", queue);
|
|
23748
|
+
throw new Error(`Kafka publish failed for topic ${queue}: ${result || "no response"}`);
|
|
24275
23749
|
}
|
|
24276
23750
|
pop(queue) {
|
|
24277
23751
|
const result = this.execSync("get", queue);
|
|
23752
|
+
_KafkaBackend.assertNoError(result, "fetch", queue);
|
|
24278
23753
|
if (!result || result === "__EMPTY__" || result === "__UNSUPPORTED__") return null;
|
|
24279
23754
|
try {
|
|
24280
23755
|
return JSON.parse(result);
|
|
@@ -24292,7 +23767,7 @@ var init_kafkaBackend = __esm({
|
|
|
24292
23767
|
});
|
|
24293
23768
|
|
|
24294
23769
|
// ../core/src/queueBackends/mongoBackend.ts
|
|
24295
|
-
import { randomUUID as
|
|
23770
|
+
import { randomUUID as randomUUID7 } from "node:crypto";
|
|
24296
23771
|
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
24297
23772
|
var MongoBackend2;
|
|
24298
23773
|
var init_mongoBackend = __esm({
|
|
@@ -24605,7 +24080,7 @@ var init_mongoBackend = __esm({
|
|
|
24605
24080
|
}
|
|
24606
24081
|
}
|
|
24607
24082
|
push(queue, payload, delay) {
|
|
24608
|
-
const id =
|
|
24083
|
+
const id = randomUUID7();
|
|
24609
24084
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
24610
24085
|
const job = {
|
|
24611
24086
|
id,
|
|
@@ -24740,9 +24215,9 @@ var init_job = __esm({
|
|
|
24740
24215
|
});
|
|
24741
24216
|
|
|
24742
24217
|
// ../core/src/queueBackends/liteBackend.ts
|
|
24743
|
-
import { mkdirSync as mkdirSync12, readdirSync as readdirSync10, readFileSync as
|
|
24218
|
+
import { mkdirSync as mkdirSync12, readdirSync as readdirSync10, readFileSync as readFileSync14, writeFileSync as writeFileSync9, unlinkSync as unlinkSync5, existsSync as existsSync16 } from "node:fs";
|
|
24744
24219
|
import { join as join18 } from "node:path";
|
|
24745
|
-
import { randomUUID as
|
|
24220
|
+
import { randomUUID as randomUUID8 } from "node:crypto";
|
|
24746
24221
|
var LiteBackend;
|
|
24747
24222
|
var init_liteBackend = __esm({
|
|
24748
24223
|
"../core/src/queueBackends/liteBackend.ts"() {
|
|
@@ -24794,7 +24269,7 @@ var init_liteBackend = __esm({
|
|
|
24794
24269
|
}
|
|
24795
24270
|
push(queue, payload, delay, priority) {
|
|
24796
24271
|
const dir = this.ensureDir(queue);
|
|
24797
|
-
const id =
|
|
24272
|
+
const id = randomUUID8();
|
|
24798
24273
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
24799
24274
|
const job = {
|
|
24800
24275
|
id,
|
|
@@ -24830,7 +24305,7 @@ var init_liteBackend = __esm({
|
|
|
24830
24305
|
const filePath = join18(dir, filename);
|
|
24831
24306
|
let job;
|
|
24832
24307
|
try {
|
|
24833
|
-
job = JSON.parse(
|
|
24308
|
+
job = JSON.parse(readFileSync14(filePath, "utf-8"));
|
|
24834
24309
|
} catch {
|
|
24835
24310
|
continue;
|
|
24836
24311
|
}
|
|
@@ -24894,7 +24369,7 @@ var init_liteBackend = __esm({
|
|
|
24894
24369
|
const filePath = join18(reservedDir, filename);
|
|
24895
24370
|
let record;
|
|
24896
24371
|
try {
|
|
24897
|
-
record = JSON.parse(
|
|
24372
|
+
record = JSON.parse(readFileSync14(filePath, "utf-8"));
|
|
24898
24373
|
} catch {
|
|
24899
24374
|
continue;
|
|
24900
24375
|
}
|
|
@@ -25007,7 +24482,7 @@ var init_liteBackend = __esm({
|
|
|
25007
24482
|
let count = 0;
|
|
25008
24483
|
for (const file of files) {
|
|
25009
24484
|
try {
|
|
25010
|
-
const job = JSON.parse(
|
|
24485
|
+
const job = JSON.parse(readFileSync14(join18(scanDir, file), "utf-8"));
|
|
25011
24486
|
if (job.status === status2) count++;
|
|
25012
24487
|
} catch {
|
|
25013
24488
|
}
|
|
@@ -25064,7 +24539,7 @@ var init_liteBackend = __esm({
|
|
|
25064
24539
|
const files = readdirSync10(dir).filter((f) => f.endsWith(".queue-data")).sort();
|
|
25065
24540
|
for (const file of files) {
|
|
25066
24541
|
try {
|
|
25067
|
-
const job = JSON.parse(
|
|
24542
|
+
const job = JSON.parse(readFileSync14(join18(dir, file), "utf-8"));
|
|
25068
24543
|
const attempts = job.attempts || 0;
|
|
25069
24544
|
if (attempts > 0 && attempts < maxRetries) {
|
|
25070
24545
|
results.push(job);
|
|
@@ -25090,7 +24565,7 @@ var init_liteBackend = __esm({
|
|
|
25090
24565
|
const failedDir = join18(this.basePath, q, "failed");
|
|
25091
24566
|
const filePath = join18(failedDir, `${jobId}.queue-data`);
|
|
25092
24567
|
if (existsSync16(filePath)) {
|
|
25093
|
-
const job = JSON.parse(
|
|
24568
|
+
const job = JSON.parse(readFileSync14(filePath, "utf-8"));
|
|
25094
24569
|
job.status = "pending";
|
|
25095
24570
|
job.attempts = (job.attempts || 0) + 1;
|
|
25096
24571
|
job.error = void 0;
|
|
@@ -25114,7 +24589,7 @@ var init_liteBackend = __esm({
|
|
|
25114
24589
|
const files = readdirSync10(failedDir).filter((f) => f.endsWith(".queue-data")).sort();
|
|
25115
24590
|
for (const file of files) {
|
|
25116
24591
|
try {
|
|
25117
|
-
const job = JSON.parse(
|
|
24592
|
+
const job = JSON.parse(readFileSync14(join18(failedDir, file), "utf-8"));
|
|
25118
24593
|
if ((job.attempts || 0) >= maxRetries) {
|
|
25119
24594
|
job.status = "dead";
|
|
25120
24595
|
results.push(job);
|
|
@@ -25148,7 +24623,7 @@ var init_liteBackend = __esm({
|
|
|
25148
24623
|
const files = readdirSync10(dir).filter((f) => f.endsWith(".queue-data"));
|
|
25149
24624
|
for (const file of files) {
|
|
25150
24625
|
try {
|
|
25151
|
-
const job = JSON.parse(
|
|
24626
|
+
const job = JSON.parse(readFileSync14(join18(dir, file), "utf-8"));
|
|
25152
24627
|
if (job.status === status2) {
|
|
25153
24628
|
unlinkSync5(join18(dir, file));
|
|
25154
24629
|
count++;
|
|
@@ -25175,7 +24650,7 @@ var init_liteBackend = __esm({
|
|
|
25175
24650
|
for (const file of files) {
|
|
25176
24651
|
try {
|
|
25177
24652
|
const filePath = join18(failedDir, file);
|
|
25178
|
-
const job = JSON.parse(
|
|
24653
|
+
const job = JSON.parse(readFileSync14(filePath, "utf-8"));
|
|
25179
24654
|
if ((job.attempts || 0) >= maxRetries) {
|
|
25180
24655
|
continue;
|
|
25181
24656
|
}
|
|
@@ -25207,7 +24682,7 @@ var init_liteBackend = __esm({
|
|
|
25207
24682
|
const filePath = join18(dir, file);
|
|
25208
24683
|
let job;
|
|
25209
24684
|
try {
|
|
25210
|
-
job = JSON.parse(
|
|
24685
|
+
job = JSON.parse(readFileSync14(filePath, "utf-8"));
|
|
25211
24686
|
} catch {
|
|
25212
24687
|
continue;
|
|
25213
24688
|
}
|
|
@@ -27697,7 +27172,7 @@ ${end}
|
|
|
27697
27172
|
|
|
27698
27173
|
// ../core/src/devAdmin.ts
|
|
27699
27174
|
import { cpus as osCpus } from "node:os";
|
|
27700
|
-
import { readFileSync as
|
|
27175
|
+
import { readFileSync as readFileSync18, writeFileSync as writeFileSync13, existsSync as existsSync20, readdirSync as readdirSync14, mkdirSync as mkdirSync15, copyFileSync as copyFileSync2, statSync as statSync15 } from "node:fs";
|
|
27701
27176
|
import { join as join22, dirname as dirname10, resolve as resolve16, relative as relative8 } from "node:path";
|
|
27702
27177
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
27703
27178
|
import { timingSafeEqual as timingSafeEqual2 } from "node:crypto";
|
|
@@ -27883,7 +27358,7 @@ function resolveDevEnvVar(key) {
|
|
|
27883
27358
|
if (live !== void 0 && live !== "") return live;
|
|
27884
27359
|
const envPath = join22(process.cwd(), ".env");
|
|
27885
27360
|
if (!existsSync20(envPath)) return "";
|
|
27886
|
-
for (const line of
|
|
27361
|
+
for (const line of readFileSync18(envPath, "utf-8").split("\n")) {
|
|
27887
27362
|
const t = line.trim();
|
|
27888
27363
|
if (!t || t.startsWith("#") || !t.includes("=")) continue;
|
|
27889
27364
|
const eq = t.indexOf("=");
|
|
@@ -27893,7 +27368,7 @@ function resolveDevEnvVar(key) {
|
|
|
27893
27368
|
}
|
|
27894
27369
|
function upsertDevEnvVar(key, value) {
|
|
27895
27370
|
const envPath = join22(process.cwd(), ".env");
|
|
27896
|
-
const lines = existsSync20(envPath) ?
|
|
27371
|
+
const lines = existsSync20(envPath) ? readFileSync18(envPath, "utf-8").split("\n") : [];
|
|
27897
27372
|
let found = false;
|
|
27898
27373
|
const out = [];
|
|
27899
27374
|
for (const line of lines) {
|
|
@@ -27926,7 +27401,7 @@ function parseEnvFile() {
|
|
|
27926
27401
|
const envPath = join22(process.cwd(), ".env");
|
|
27927
27402
|
const result = {};
|
|
27928
27403
|
if (!existsSync20(envPath)) return result;
|
|
27929
|
-
const lines =
|
|
27404
|
+
const lines = readFileSync18(envPath, "utf-8").split("\n");
|
|
27930
27405
|
for (const line of lines) {
|
|
27931
27406
|
const trimmed = line.trim();
|
|
27932
27407
|
if (!trimmed || trimmed.startsWith("#") || !trimmed.includes("=")) continue;
|
|
@@ -28283,7 +27758,7 @@ var init_devAdmin = __esm({
|
|
|
28283
27758
|
for (const rel of ["../../../package.json", "../../package.json"]) {
|
|
28284
27759
|
const p = resolve16(__dirname2, rel);
|
|
28285
27760
|
if (existsSync20(p)) {
|
|
28286
|
-
const pkg = JSON.parse(
|
|
27761
|
+
const pkg = JSON.parse(readFileSync18(p, "utf-8"));
|
|
28287
27762
|
if (pkg.version) return pkg.version;
|
|
28288
27763
|
}
|
|
28289
27764
|
}
|
|
@@ -28941,7 +28416,7 @@ var init_devAdmin = __esm({
|
|
|
28941
28416
|
for (const filename of readdirSync14(queueDir).sort()) {
|
|
28942
28417
|
if (!filename.endsWith(".queue-data")) continue;
|
|
28943
28418
|
try {
|
|
28944
|
-
const job = JSON.parse(
|
|
28419
|
+
const job = JSON.parse(readFileSync18(join22(queueDir, filename), "utf-8"));
|
|
28945
28420
|
jobs.push(mapQueueJob(job, topic, "pending"));
|
|
28946
28421
|
} catch {
|
|
28947
28422
|
}
|
|
@@ -29427,7 +28902,7 @@ var init_devAdmin = __esm({
|
|
|
29427
28902
|
}
|
|
29428
28903
|
try {
|
|
29429
28904
|
const envPath = join22(process.cwd(), ".env");
|
|
29430
|
-
const lines = existsSync20(envPath) ?
|
|
28905
|
+
const lines = existsSync20(envPath) ? readFileSync18(envPath, "utf-8").split("\n") : [];
|
|
29431
28906
|
const keysFound = { TINA4_DATABASE_URL: false, TINA4_DATABASE_USERNAME: false, TINA4_DATABASE_PASSWORD: false };
|
|
29432
28907
|
const newLines = [];
|
|
29433
28908
|
for (const line of lines) {
|
|
@@ -29473,7 +28948,7 @@ var init_devAdmin = __esm({
|
|
|
29473
28948
|
const metaFile = join22(entryPath, "meta.json");
|
|
29474
28949
|
if (statSync15(entryPath).isDirectory() && existsSync20(metaFile)) {
|
|
29475
28950
|
try {
|
|
29476
|
-
const meta = JSON.parse(
|
|
28951
|
+
const meta = JSON.parse(readFileSync18(metaFile, "utf-8"));
|
|
29477
28952
|
meta.id = entry;
|
|
29478
28953
|
const srcDir = join22(entryPath, "src");
|
|
29479
28954
|
if (existsSync20(srcDir)) {
|
|
@@ -29688,7 +29163,7 @@ var init_devAdmin = __esm({
|
|
|
29688
29163
|
return;
|
|
29689
29164
|
}
|
|
29690
29165
|
try {
|
|
29691
|
-
const content =
|
|
29166
|
+
const content = readFileSync18(target, "utf-8");
|
|
29692
29167
|
const path8 = relative8(root, target);
|
|
29693
29168
|
res.json({ path: path8, content, language: devAdminLanguage(path8), bytes: Buffer.byteLength(content, "utf-8") });
|
|
29694
29169
|
} catch (e) {
|
|
@@ -29730,7 +29205,7 @@ var init_devAdmin = __esm({
|
|
|
29730
29205
|
return;
|
|
29731
29206
|
}
|
|
29732
29207
|
try {
|
|
29733
|
-
const buf =
|
|
29208
|
+
const buf = readFileSync18(target);
|
|
29734
29209
|
const ext = target.slice(target.lastIndexOf(".") + 1).toLowerCase();
|
|
29735
29210
|
const mime = {
|
|
29736
29211
|
js: "application/javascript",
|
|
@@ -30158,7 +29633,7 @@ var init_devAdmin = __esm({
|
|
|
30158
29633
|
});
|
|
30159
29634
|
|
|
30160
29635
|
// ../core/src/i18n.ts
|
|
30161
|
-
import { readFileSync as
|
|
29636
|
+
import { readFileSync as readFileSync19, readdirSync as readdirSync15, existsSync as existsSync21 } from "node:fs";
|
|
30162
29637
|
import { join as join23, resolve as resolve17 } from "node:path";
|
|
30163
29638
|
var I18n;
|
|
30164
29639
|
var init_i18n = __esm({
|
|
@@ -30255,7 +29730,7 @@ var init_i18n = __esm({
|
|
|
30255
29730
|
const filePath = join23(this._localeDir, `${locale}.json`);
|
|
30256
29731
|
if (existsSync21(filePath)) {
|
|
30257
29732
|
try {
|
|
30258
|
-
const raw =
|
|
29733
|
+
const raw = readFileSync19(filePath, "utf-8");
|
|
30259
29734
|
const data = JSON.parse(raw);
|
|
30260
29735
|
this._translations.set(locale, _I18n._flatten(data));
|
|
30261
29736
|
return;
|
|
@@ -30268,7 +29743,7 @@ var init_i18n = __esm({
|
|
|
30268
29743
|
const yamlPath = join23(this._localeDir, `${locale}${ext}`);
|
|
30269
29744
|
if (existsSync21(yamlPath)) {
|
|
30270
29745
|
try {
|
|
30271
|
-
const raw =
|
|
29746
|
+
const raw = readFileSync19(yamlPath, "utf-8");
|
|
30272
29747
|
const data = _I18n._parseSimpleYaml(raw);
|
|
30273
29748
|
this._translations.set(locale, _I18n._flatten(data));
|
|
30274
29749
|
return;
|
|
@@ -30936,6 +30411,36 @@ var init_docsAutoDiscovery = __esm({
|
|
|
30936
30411
|
}
|
|
30937
30412
|
});
|
|
30938
30413
|
|
|
30414
|
+
// ../core/src/sessionHandlers/childError.ts
|
|
30415
|
+
function childFailureReason(err) {
|
|
30416
|
+
const e = err ?? {};
|
|
30417
|
+
const stderr = String(e.stderr ?? "").trim();
|
|
30418
|
+
if (stderr !== "") {
|
|
30419
|
+
return firstLine(stderr);
|
|
30420
|
+
}
|
|
30421
|
+
if (e.code === "ETIMEDOUT" || e.signal) {
|
|
30422
|
+
return `timed out or was killed (${e.code ?? e.signal})`;
|
|
30423
|
+
}
|
|
30424
|
+
if (typeof e.status === "number" && e.status !== 0) {
|
|
30425
|
+
return `child exited with code ${e.status} and no output`;
|
|
30426
|
+
}
|
|
30427
|
+
return firstLine(String(e.message ?? "unknown error"));
|
|
30428
|
+
}
|
|
30429
|
+
function firstLine(text) {
|
|
30430
|
+
const line = text.split("\n", 1)[0] ?? "";
|
|
30431
|
+
return line.length > MAX_FALLBACK ? `${line.slice(0, MAX_FALLBACK)}...` : line;
|
|
30432
|
+
}
|
|
30433
|
+
function childFailureError(label, err) {
|
|
30434
|
+
return new Error(`${label} command failed: ${childFailureReason(err)}`);
|
|
30435
|
+
}
|
|
30436
|
+
var MAX_FALLBACK;
|
|
30437
|
+
var init_childError = __esm({
|
|
30438
|
+
"../core/src/sessionHandlers/childError.ts"() {
|
|
30439
|
+
"use strict";
|
|
30440
|
+
MAX_FALLBACK = 200;
|
|
30441
|
+
}
|
|
30442
|
+
});
|
|
30443
|
+
|
|
30939
30444
|
// ../core/src/sessionHandlers/respClient.ts
|
|
30940
30445
|
import { execFileSync as execFileSync4 } from "node:child_process";
|
|
30941
30446
|
function respCommandSync(target, args, label = "Redis") {
|
|
@@ -31055,7 +30560,7 @@ function respCommandSync(target, args, label = "Redis") {
|
|
|
31055
30560
|
stdio: ["pipe", "pipe", "pipe"]
|
|
31056
30561
|
});
|
|
31057
30562
|
} catch (err) {
|
|
31058
|
-
throw
|
|
30563
|
+
throw childFailureError(label, err);
|
|
31059
30564
|
}
|
|
31060
30565
|
if (result === "__NULL__") return "";
|
|
31061
30566
|
if (result.startsWith("__ERR__")) {
|
|
@@ -31066,6 +30571,7 @@ function respCommandSync(target, args, label = "Redis") {
|
|
|
31066
30571
|
var init_respClient = __esm({
|
|
31067
30572
|
"../core/src/sessionHandlers/respClient.ts"() {
|
|
31068
30573
|
"use strict";
|
|
30574
|
+
init_childError();
|
|
31069
30575
|
}
|
|
31070
30576
|
});
|
|
31071
30577
|
|
|
@@ -31084,6 +30590,7 @@ var moduleRequire, RedisNpmSessionHandler;
|
|
|
31084
30590
|
var init_redisHandler = __esm({
|
|
31085
30591
|
"../core/src/sessionHandlers/redisHandler.ts"() {
|
|
31086
30592
|
"use strict";
|
|
30593
|
+
init_childError();
|
|
31087
30594
|
init_respClient();
|
|
31088
30595
|
moduleRequire = createRequire8(import.meta.url);
|
|
31089
30596
|
RedisNpmSessionHandler = class {
|
|
@@ -31144,9 +30651,17 @@ var init_redisHandler = __esm({
|
|
|
31144
30651
|
(async () => {
|
|
31145
30652
|
try {
|
|
31146
30653
|
const redis = require("redis");
|
|
30654
|
+
// reconnectStrategy: false \u2014 this child runs ONE command and exits, so
|
|
30655
|
+
// retrying inside it is pointless: the handler is called again on the
|
|
30656
|
+
// next request anyway. With the driver's default strategy a refused
|
|
30657
|
+
// connection never rejects, the child hangs until execFileSync's 5s
|
|
30658
|
+
// timeout kills it, and the caller is told "timed out" when the truth
|
|
30659
|
+
// is "connection refused". Off, connect() rejects in ~5ms with the real
|
|
30660
|
+
// reason -- a better message AND no 5s stall per request when Redis is
|
|
30661
|
+
// down.
|
|
31147
30662
|
const clientOpts = useUrl
|
|
31148
|
-
? { url }
|
|
31149
|
-
: { socket: { host, port }, password: password || undefined, database: db };
|
|
30663
|
+
? { url, socket: { reconnectStrategy: false } }
|
|
30664
|
+
: { socket: { host, port, reconnectStrategy: false }, password: password || undefined, database: db };
|
|
31150
30665
|
const client = redis.createClient(clientOpts);
|
|
31151
30666
|
client.on("error", () => {});
|
|
31152
30667
|
await client.connect();
|
|
@@ -31160,8 +30675,10 @@ var init_redisHandler = __esm({
|
|
|
31160
30675
|
const out = (result === null || result === undefined) ? "__NULL__" : String(result);
|
|
31161
30676
|
process.stdout.write(out, () => process.exit(0));
|
|
31162
30677
|
} catch (err) {
|
|
31163
|
-
|
|
31164
|
-
process.exit(
|
|
30678
|
+
// Exit from the write CALLBACK: stderr to a pipe is an async write and
|
|
30679
|
+
// a bare process.exit() truncates it, which left the parent with an
|
|
30680
|
+
// empty stderr and nothing but execFileSync's script-dump message.
|
|
30681
|
+
process.stderr.write(String((err && err.message) || err), () => process.exit(1));
|
|
31165
30682
|
}
|
|
31166
30683
|
})();
|
|
31167
30684
|
`;
|
|
@@ -31173,7 +30690,7 @@ var init_redisHandler = __esm({
|
|
|
31173
30690
|
stdio: ["pipe", "pipe", "pipe"]
|
|
31174
30691
|
});
|
|
31175
30692
|
} catch (err) {
|
|
31176
|
-
throw
|
|
30693
|
+
throw childFailureError("Redis", err);
|
|
31177
30694
|
}
|
|
31178
30695
|
if (result === "__NULL__") return "";
|
|
31179
30696
|
return result;
|
|
@@ -31305,8 +30822,10 @@ function mongoCommandSync(target, command, args, label = "MongoDB") {
|
|
|
31305
30822
|
process.stdout.write(out, () => process.exit(0));
|
|
31306
30823
|
} catch (err) {
|
|
31307
30824
|
try { if (client) await client.close(); } catch (e) {}
|
|
31308
|
-
|
|
31309
|
-
process.exit(
|
|
30825
|
+
// Exit from the write CALLBACK: stderr to a pipe is an async write and
|
|
30826
|
+
// a bare process.exit() truncates it, which left the parent with an
|
|
30827
|
+
// empty stderr and nothing but execFileSync's script-dump message.
|
|
30828
|
+
process.stderr.write(String((err && err.message) || err), () => process.exit(1));
|
|
31310
30829
|
}
|
|
31311
30830
|
})();
|
|
31312
30831
|
} else {
|
|
@@ -31447,12 +30966,13 @@ function mongoCommandSync(target, command, args, label = "MongoDB") {
|
|
|
31447
30966
|
stdio: ["pipe", "pipe", "pipe"]
|
|
31448
30967
|
});
|
|
31449
30968
|
} catch (err) {
|
|
31450
|
-
throw
|
|
30969
|
+
throw childFailureError(label, err);
|
|
31451
30970
|
}
|
|
31452
30971
|
}
|
|
31453
30972
|
var init_mongoClient = __esm({
|
|
31454
30973
|
"../core/src/sessionHandlers/mongoClient.ts"() {
|
|
31455
30974
|
"use strict";
|
|
30975
|
+
init_childError();
|
|
31456
30976
|
}
|
|
31457
30977
|
});
|
|
31458
30978
|
|
|
@@ -31605,7 +31125,7 @@ __export(session_exports, {
|
|
|
31605
31125
|
sessionCookieName: () => sessionCookieName
|
|
31606
31126
|
});
|
|
31607
31127
|
import { randomBytes as randomBytes6 } from "node:crypto";
|
|
31608
|
-
import { existsSync as existsSync23, mkdirSync as mkdirSync18, readFileSync as
|
|
31128
|
+
import { existsSync as existsSync23, mkdirSync as mkdirSync18, readFileSync as readFileSync21, writeFileSync as writeFileSync16, unlinkSync as unlinkSync7, readdirSync as readdirSync16 } from "node:fs";
|
|
31609
31129
|
import { join as join25 } from "node:path";
|
|
31610
31130
|
function isSecureScheme(forwardedProto, socketEncrypted) {
|
|
31611
31131
|
const forwarded = (forwardedProto ?? "").trim();
|
|
@@ -31658,7 +31178,7 @@ var init_session = __esm({
|
|
|
31658
31178
|
const filePath = this.filePath(sessionId);
|
|
31659
31179
|
try {
|
|
31660
31180
|
if (!existsSync23(filePath)) return null;
|
|
31661
|
-
const raw =
|
|
31181
|
+
const raw = readFileSync21(filePath, "utf-8");
|
|
31662
31182
|
const wrapper = JSON.parse(raw);
|
|
31663
31183
|
if (wrapper._expires && wrapper._expires > 0 && Date.now() / 1e3 > wrapper._expires) {
|
|
31664
31184
|
try {
|
|
@@ -31694,7 +31214,7 @@ var init_session = __esm({
|
|
|
31694
31214
|
if (!file.endsWith(".json")) continue;
|
|
31695
31215
|
const fullPath = join25(this.storagePath, file);
|
|
31696
31216
|
try {
|
|
31697
|
-
const raw =
|
|
31217
|
+
const raw = readFileSync21(fullPath, "utf-8");
|
|
31698
31218
|
const wrapper = JSON.parse(raw);
|
|
31699
31219
|
if (wrapper._expires && wrapper._expires > 0 && now > wrapper._expires) {
|
|
31700
31220
|
unlinkSync7(fullPath);
|
|
@@ -32211,7 +31731,7 @@ var init_events = __esm({
|
|
|
32211
31731
|
// ../core/src/server.ts
|
|
32212
31732
|
import { createServer as createServer2 } from "node:http";
|
|
32213
31733
|
import { resolve as resolve19, dirname as dirname11, join as join26, relative as relative9 } from "node:path";
|
|
32214
|
-
import { existsSync as existsSync24, readdirSync as readdirSync17, readFileSync as
|
|
31734
|
+
import { existsSync as existsSync24, readdirSync as readdirSync17, readFileSync as readFileSync22, statSync as statSync16 } from "node:fs";
|
|
32215
31735
|
import { isatty } from "node:tty";
|
|
32216
31736
|
import { fileURLToPath as fileURLToPath5 } from "node:url";
|
|
32217
31737
|
import { execFileSync as execFileSync7, exec } from "node:child_process";
|
|
@@ -32273,7 +31793,7 @@ async function autoMigrateOnStartup(migrationDir = "migrations", base = process.
|
|
|
32273
31793
|
function readPackageVersion() {
|
|
32274
31794
|
try {
|
|
32275
31795
|
const pkgPath = resolve19(dirname11(fileURLToPath5(import.meta.url)), "..", "..", "..", "package.json");
|
|
32276
|
-
const pkg = JSON.parse(
|
|
31796
|
+
const pkg = JSON.parse(readFileSync22(pkgPath, "utf-8"));
|
|
32277
31797
|
return pkg.version ?? "0.0.0";
|
|
32278
31798
|
} catch {
|
|
32279
31799
|
return "0.0.0";
|
|
@@ -33103,7 +32623,7 @@ ${reset2}
|
|
|
33103
32623
|
res.raw.writeHead(200, void 0, { "Content-Type": "text/html; charset=utf-8" });
|
|
33104
32624
|
res.raw.end(html);
|
|
33105
32625
|
} else {
|
|
33106
|
-
const html =
|
|
32626
|
+
const html = readFileSync22(resolve19(templatesDir, tplFile), "utf-8");
|
|
33107
32627
|
res.raw.writeHead(200, void 0, { "Content-Type": "text/html; charset=utf-8" });
|
|
33108
32628
|
res.raw.end(html);
|
|
33109
32629
|
}
|
|
@@ -33494,7 +33014,7 @@ var init_constants = __esm({
|
|
|
33494
33014
|
});
|
|
33495
33015
|
|
|
33496
33016
|
// ../core/src/scss.ts
|
|
33497
|
-
import { readFileSync as
|
|
33017
|
+
import { readFileSync as readFileSync23, writeFileSync as writeFileSync17, existsSync as existsSync25, mkdirSync as mkdirSync19, readdirSync as readdirSync18 } from "node:fs";
|
|
33498
33018
|
import { join as join27, resolve as resolve20, dirname as dirname12 } from "node:path";
|
|
33499
33019
|
function compileString(scss, importPaths, variables) {
|
|
33500
33020
|
const imported = /* @__PURE__ */ new Set();
|
|
@@ -33525,7 +33045,7 @@ function resolveImports(content, paths, imported) {
|
|
|
33525
33045
|
for (const candidate of candidates) {
|
|
33526
33046
|
if (existsSync25(candidate) && !imported.has(candidate)) {
|
|
33527
33047
|
imported.add(candidate);
|
|
33528
|
-
const fileContent =
|
|
33048
|
+
const fileContent = readFileSync23(candidate, "utf-8");
|
|
33529
33049
|
return resolveImports(fileContent, [dirname12(candidate), ...paths], imported);
|
|
33530
33050
|
}
|
|
33531
33051
|
}
|
|
@@ -33866,7 +33386,7 @@ var init_scss = __esm({
|
|
|
33866
33386
|
/** Compile an SCSS file to CSS. */
|
|
33867
33387
|
compileFile(filePath) {
|
|
33868
33388
|
const absPath = resolve20(filePath);
|
|
33869
|
-
const content =
|
|
33389
|
+
const content = readFileSync23(absPath, "utf-8");
|
|
33870
33390
|
const paths = [dirname12(absPath), ...this._importPaths];
|
|
33871
33391
|
return compileString(content, paths, { ...this._variables });
|
|
33872
33392
|
}
|
|
@@ -33889,7 +33409,7 @@ var init_scss = __esm({
|
|
|
33889
33409
|
const imported = /* @__PURE__ */ new Set();
|
|
33890
33410
|
let merged = "";
|
|
33891
33411
|
for (const file of files) {
|
|
33892
|
-
const content =
|
|
33412
|
+
const content = readFileSync23(file, "utf-8");
|
|
33893
33413
|
imported.add(file);
|
|
33894
33414
|
merged += resolveImports(content, paths, imported) + "\n";
|
|
33895
33415
|
}
|
|
@@ -33906,7 +33426,7 @@ var init_scss = __esm({
|
|
|
33906
33426
|
if (!existsSync25(outDir)) mkdirSync19(outDir, { recursive: true });
|
|
33907
33427
|
let existing = null;
|
|
33908
33428
|
try {
|
|
33909
|
-
existing = existsSync25(absOutput) ?
|
|
33429
|
+
existing = existsSync25(absOutput) ? readFileSync23(absOutput, "utf-8") : null;
|
|
33910
33430
|
} catch {
|
|
33911
33431
|
existing = null;
|
|
33912
33432
|
}
|
|
@@ -33990,10 +33510,10 @@ var init_mqttMessage = __esm({
|
|
|
33990
33510
|
});
|
|
33991
33511
|
|
|
33992
33512
|
// ../core/src/mqtt.ts
|
|
33993
|
-
import
|
|
33994
|
-
import
|
|
33513
|
+
import net2 from "node:net";
|
|
33514
|
+
import tls from "node:tls";
|
|
33995
33515
|
import { randomBytes as randomBytes7 } from "node:crypto";
|
|
33996
|
-
import { existsSync as existsSync26, readFileSync as
|
|
33516
|
+
import { existsSync as existsSync26, readFileSync as readFileSync24 } from "node:fs";
|
|
33997
33517
|
var MqttError, MqttTimeoutError, CONNECT, CONNACK, PUBLISH, PUBACK, SUBSCRIBE, SUBACK, PINGREQ, PINGRESP, DISCONNECT, PROTOCOL_LEVEL, DEFAULT_PORT, DEFAULT_TLS_PORT, DEFAULT_URL, DEFAULT_KEEPALIVE, SUBSCRIPTION_REFUSED, MAX_REMAINING_LENGTH, QOS2_REFUSED_MESSAGE, CONNACK_RETURN_CODES, Mqtt;
|
|
33998
33518
|
var init_mqtt = __esm({
|
|
33999
33519
|
"../core/src/mqtt.ts"() {
|
|
@@ -34458,10 +33978,10 @@ var init_mqtt = __esm({
|
|
|
34458
33978
|
servername: this.host,
|
|
34459
33979
|
rejectUnauthorized: this.tlsVerify
|
|
34460
33980
|
};
|
|
34461
|
-
if (this.tlsVerify && this.caFile) opts.ca =
|
|
34462
|
-
sock =
|
|
33981
|
+
if (this.tlsVerify && this.caFile) opts.ca = readFileSync24(this.caFile);
|
|
33982
|
+
sock = tls.connect(opts, () => settle(() => resolve31(sock)));
|
|
34463
33983
|
} else {
|
|
34464
|
-
sock =
|
|
33984
|
+
sock = net2.createConnection({ host: this.host, port: this.port }, () => settle(() => resolve31(sock)));
|
|
34465
33985
|
}
|
|
34466
33986
|
sock.once("error", (err) => {
|
|
34467
33987
|
settle(() => {
|
|
@@ -35012,7 +34532,7 @@ import https from "node:https";
|
|
|
35012
34532
|
import { URL as URL2 } from "node:url";
|
|
35013
34533
|
import { randomBytes as randomBytes8 } from "node:crypto";
|
|
35014
34534
|
import { promises as fsp, createWriteStream } from "node:fs";
|
|
35015
|
-
import { basename as
|
|
34535
|
+
import { basename as basename6 } from "node:path";
|
|
35016
34536
|
import { pipeline } from "node:stream/promises";
|
|
35017
34537
|
function sameOrigin(urlA, urlB) {
|
|
35018
34538
|
try {
|
|
@@ -35302,7 +34822,7 @@ var init_api = __esm({
|
|
|
35302
34822
|
error: err instanceof Error ? err.message : String(err)
|
|
35303
34823
|
};
|
|
35304
34824
|
}
|
|
35305
|
-
uploadName = filename ||
|
|
34825
|
+
uploadName = filename || basename6(filePath);
|
|
35306
34826
|
} else {
|
|
35307
34827
|
return { http_code: null, body: null, headers: {}, error: "upload requires filePath or fileBytes" };
|
|
35308
34828
|
}
|
|
@@ -35658,6 +35178,777 @@ var init_api = __esm({
|
|
|
35658
35178
|
}
|
|
35659
35179
|
});
|
|
35660
35180
|
|
|
35181
|
+
// ../core/src/messenger.ts
|
|
35182
|
+
import net3 from "node:net";
|
|
35183
|
+
import tls2 from "node:tls";
|
|
35184
|
+
import { readFileSync as readFileSync25 } from "node:fs";
|
|
35185
|
+
import { basename as basename7 } from "node:path";
|
|
35186
|
+
import { randomUUID as randomUUID9 } from "node:crypto";
|
|
35187
|
+
function tlsRejectUnauthorized() {
|
|
35188
|
+
return !isTruthy(process.env.TINA4_MAIL_TLS_INSECURE);
|
|
35189
|
+
}
|
|
35190
|
+
function readResponse(socket) {
|
|
35191
|
+
return new Promise((resolve31, reject) => {
|
|
35192
|
+
let buffer = "";
|
|
35193
|
+
const onData = (chunk) => {
|
|
35194
|
+
buffer += chunk.toString("utf-8");
|
|
35195
|
+
const lines = buffer.split("\r\n");
|
|
35196
|
+
for (let i = 0; i < lines.length; i++) {
|
|
35197
|
+
const line = lines[i];
|
|
35198
|
+
if (line.length < 3) continue;
|
|
35199
|
+
const code = parseInt(line.substring(0, 3), 10);
|
|
35200
|
+
if (line.length >= 4 && line[3] === " ") {
|
|
35201
|
+
socket.removeListener("data", onData);
|
|
35202
|
+
socket.removeListener("error", onError);
|
|
35203
|
+
resolve31({ code, text: buffer.trim() });
|
|
35204
|
+
return;
|
|
35205
|
+
}
|
|
35206
|
+
}
|
|
35207
|
+
};
|
|
35208
|
+
const onError = (err) => {
|
|
35209
|
+
socket.removeListener("data", onData);
|
|
35210
|
+
reject(err);
|
|
35211
|
+
};
|
|
35212
|
+
socket.on("data", onData);
|
|
35213
|
+
socket.on("error", onError);
|
|
35214
|
+
});
|
|
35215
|
+
}
|
|
35216
|
+
function sendCommand(socket, command) {
|
|
35217
|
+
return new Promise((resolve31, reject) => {
|
|
35218
|
+
socket.write(command + "\r\n", "utf-8", (err) => {
|
|
35219
|
+
if (err) return reject(err);
|
|
35220
|
+
readResponse(socket).then(resolve31, reject);
|
|
35221
|
+
});
|
|
35222
|
+
});
|
|
35223
|
+
}
|
|
35224
|
+
function buildMimeMessage(options) {
|
|
35225
|
+
const boundary = `----=_Tina4_${Date.now()}_${Math.random().toString(36).substring(2)}`;
|
|
35226
|
+
const altBoundary = `----=_Tina4Alt_${Date.now()}_${Math.random().toString(36).substring(2)}`;
|
|
35227
|
+
const hasAttachments = options.attachments && options.attachments.length > 0;
|
|
35228
|
+
const hasTextAlt = options.text !== void 0 && options.html;
|
|
35229
|
+
const lines = [];
|
|
35230
|
+
const fromHeader = options.fromName ? `"${options.fromName}" <${options.from}>` : options.from;
|
|
35231
|
+
lines.push(`From: ${fromHeader}`);
|
|
35232
|
+
lines.push(`To: ${options.to.join(", ")}`);
|
|
35233
|
+
if (options.cc.length > 0) {
|
|
35234
|
+
lines.push(`Cc: ${options.cc.join(", ")}`);
|
|
35235
|
+
}
|
|
35236
|
+
lines.push(`Subject: ${options.subject}`);
|
|
35237
|
+
lines.push(`Date: ${(/* @__PURE__ */ new Date()).toUTCString()}`);
|
|
35238
|
+
lines.push(`Message-ID: <${options.messageId}>`);
|
|
35239
|
+
lines.push("MIME-Version: 1.0");
|
|
35240
|
+
if (options.replyTo) {
|
|
35241
|
+
lines.push(`Reply-To: ${options.replyTo}`);
|
|
35242
|
+
}
|
|
35243
|
+
if (options.headers) {
|
|
35244
|
+
for (const [key, value] of Object.entries(options.headers)) {
|
|
35245
|
+
lines.push(`${key}: ${value}`);
|
|
35246
|
+
}
|
|
35247
|
+
}
|
|
35248
|
+
if (hasAttachments) {
|
|
35249
|
+
lines.push(`Content-Type: multipart/mixed; boundary="${boundary}"`);
|
|
35250
|
+
lines.push("");
|
|
35251
|
+
lines.push(`--${boundary}`);
|
|
35252
|
+
if (hasTextAlt) {
|
|
35253
|
+
lines.push(`Content-Type: multipart/alternative; boundary="${altBoundary}"`);
|
|
35254
|
+
lines.push("");
|
|
35255
|
+
lines.push(`--${altBoundary}`);
|
|
35256
|
+
lines.push("Content-Type: text/plain; charset=UTF-8");
|
|
35257
|
+
lines.push("Content-Transfer-Encoding: 7bit");
|
|
35258
|
+
lines.push("");
|
|
35259
|
+
lines.push(options.text);
|
|
35260
|
+
lines.push("");
|
|
35261
|
+
lines.push(`--${altBoundary}`);
|
|
35262
|
+
lines.push("Content-Type: text/html; charset=UTF-8");
|
|
35263
|
+
lines.push("Content-Transfer-Encoding: 7bit");
|
|
35264
|
+
lines.push("");
|
|
35265
|
+
lines.push(options.body);
|
|
35266
|
+
lines.push("");
|
|
35267
|
+
lines.push(`--${altBoundary}--`);
|
|
35268
|
+
} else {
|
|
35269
|
+
const contentType = options.html ? "text/html" : "text/plain";
|
|
35270
|
+
lines.push(`Content-Type: ${contentType}; charset=UTF-8`);
|
|
35271
|
+
lines.push("Content-Transfer-Encoding: 7bit");
|
|
35272
|
+
lines.push("");
|
|
35273
|
+
lines.push(options.body);
|
|
35274
|
+
}
|
|
35275
|
+
for (const filePath of options.attachments) {
|
|
35276
|
+
const fileName = basename7(filePath);
|
|
35277
|
+
const fileData = readFileSync25(filePath);
|
|
35278
|
+
const base64Data = fileData.toString("base64");
|
|
35279
|
+
lines.push("");
|
|
35280
|
+
lines.push(`--${boundary}`);
|
|
35281
|
+
lines.push(`Content-Type: application/octet-stream; name="${fileName}"`);
|
|
35282
|
+
lines.push("Content-Transfer-Encoding: base64");
|
|
35283
|
+
lines.push(`Content-Disposition: attachment; filename="${fileName}"`);
|
|
35284
|
+
lines.push("");
|
|
35285
|
+
for (let i = 0; i < base64Data.length; i += 76) {
|
|
35286
|
+
lines.push(base64Data.substring(i, i + 76));
|
|
35287
|
+
}
|
|
35288
|
+
}
|
|
35289
|
+
lines.push("");
|
|
35290
|
+
lines.push(`--${boundary}--`);
|
|
35291
|
+
} else if (hasTextAlt) {
|
|
35292
|
+
lines.push(`Content-Type: multipart/alternative; boundary="${altBoundary}"`);
|
|
35293
|
+
lines.push("");
|
|
35294
|
+
lines.push(`--${altBoundary}`);
|
|
35295
|
+
lines.push("Content-Type: text/plain; charset=UTF-8");
|
|
35296
|
+
lines.push("Content-Transfer-Encoding: 7bit");
|
|
35297
|
+
lines.push("");
|
|
35298
|
+
lines.push(options.text);
|
|
35299
|
+
lines.push("");
|
|
35300
|
+
lines.push(`--${altBoundary}`);
|
|
35301
|
+
lines.push("Content-Type: text/html; charset=UTF-8");
|
|
35302
|
+
lines.push("Content-Transfer-Encoding: 7bit");
|
|
35303
|
+
lines.push("");
|
|
35304
|
+
lines.push(options.body);
|
|
35305
|
+
lines.push("");
|
|
35306
|
+
lines.push(`--${altBoundary}--`);
|
|
35307
|
+
} else {
|
|
35308
|
+
const contentType = options.html ? "text/html" : "text/plain";
|
|
35309
|
+
lines.push(`Content-Type: ${contentType}; charset=UTF-8`);
|
|
35310
|
+
lines.push("");
|
|
35311
|
+
lines.push(options.body);
|
|
35312
|
+
}
|
|
35313
|
+
return lines.join("\r\n");
|
|
35314
|
+
}
|
|
35315
|
+
function imapQuote(s) {
|
|
35316
|
+
if (/^[a-zA-Z0-9_./-]+$/.test(s)) return s;
|
|
35317
|
+
return '"' + s.replace(/\\/g, "\\\\").replace(/"/g, '\\"') + '"';
|
|
35318
|
+
}
|
|
35319
|
+
function imapReadLine(socket) {
|
|
35320
|
+
return new Promise((resolve31, reject) => {
|
|
35321
|
+
let buffer = "";
|
|
35322
|
+
const onData = (chunk) => {
|
|
35323
|
+
buffer += chunk.toString("utf-8");
|
|
35324
|
+
const nlIndex = buffer.indexOf("\r\n");
|
|
35325
|
+
if (nlIndex !== -1) {
|
|
35326
|
+
socket.removeListener("data", onData);
|
|
35327
|
+
socket.removeListener("error", onError);
|
|
35328
|
+
resolve31(buffer);
|
|
35329
|
+
}
|
|
35330
|
+
};
|
|
35331
|
+
const onError = (err) => {
|
|
35332
|
+
socket.removeListener("data", onData);
|
|
35333
|
+
reject(err);
|
|
35334
|
+
};
|
|
35335
|
+
socket.on("data", onData);
|
|
35336
|
+
socket.on("error", onError);
|
|
35337
|
+
});
|
|
35338
|
+
}
|
|
35339
|
+
function imapCommand(socket, command) {
|
|
35340
|
+
return new Promise((resolve31, reject) => {
|
|
35341
|
+
imapTagCounter++;
|
|
35342
|
+
const tag = `T${imapTagCounter}`;
|
|
35343
|
+
const fullCommand = `${tag} ${command}\r
|
|
35344
|
+
`;
|
|
35345
|
+
let buffer = "";
|
|
35346
|
+
const onData = (chunk) => {
|
|
35347
|
+
buffer += chunk.toString("utf-8");
|
|
35348
|
+
if (buffer.includes(`${tag} OK`)) {
|
|
35349
|
+
socket.removeListener("data", onData);
|
|
35350
|
+
socket.removeListener("error", onError);
|
|
35351
|
+
resolve31(buffer);
|
|
35352
|
+
return;
|
|
35353
|
+
}
|
|
35354
|
+
if (buffer.includes(`${tag} NO`) || buffer.includes(`${tag} BAD`)) {
|
|
35355
|
+
socket.removeListener("data", onData);
|
|
35356
|
+
socket.removeListener("error", onError);
|
|
35357
|
+
reject(new MessengerConnectionError(`IMAP command failed: ${command.split(" ")[0]} \u2192 ${buffer.trim()}`));
|
|
35358
|
+
}
|
|
35359
|
+
};
|
|
35360
|
+
const onError = (err) => {
|
|
35361
|
+
socket.removeListener("data", onData);
|
|
35362
|
+
reject(err);
|
|
35363
|
+
};
|
|
35364
|
+
socket.on("data", onData);
|
|
35365
|
+
socket.on("error", onError);
|
|
35366
|
+
socket.write(fullCommand, "utf-8");
|
|
35367
|
+
});
|
|
35368
|
+
}
|
|
35369
|
+
function imapFail(method, err) {
|
|
35370
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
35371
|
+
Log.error(`Messenger IMAP ${method}() failed: ${e.name}: ${e.message}`);
|
|
35372
|
+
if (e instanceof MessengerConnectionError) return e;
|
|
35373
|
+
return new MessengerConnectionError(`IMAP ${method} failed: ${e.message}`);
|
|
35374
|
+
}
|
|
35375
|
+
function parseSearchResponse(response) {
|
|
35376
|
+
const match = response.match(/\* SEARCH (.+)/);
|
|
35377
|
+
if (!match) return [];
|
|
35378
|
+
return match[1].trim().split(/\s+/).filter((s) => /^\d+$/.test(s));
|
|
35379
|
+
}
|
|
35380
|
+
function parseHeaderResponse(uid, response) {
|
|
35381
|
+
const headers = {};
|
|
35382
|
+
const headerBlock = response.match(/\r\n([\s\S]*?)\r\n\)/);
|
|
35383
|
+
if (headerBlock) {
|
|
35384
|
+
const lines = headerBlock[1].split(/\r\n/);
|
|
35385
|
+
let currentKey = "";
|
|
35386
|
+
for (const line of lines) {
|
|
35387
|
+
if (/^\s/.test(line) && currentKey) {
|
|
35388
|
+
headers[currentKey] += " " + line.trim();
|
|
35389
|
+
} else {
|
|
35390
|
+
const colonIdx = line.indexOf(":");
|
|
35391
|
+
if (colonIdx > 0) {
|
|
35392
|
+
currentKey = line.substring(0, colonIdx).trim().toLowerCase();
|
|
35393
|
+
headers[currentKey] = line.substring(colonIdx + 1).trim();
|
|
35394
|
+
}
|
|
35395
|
+
}
|
|
35396
|
+
}
|
|
35397
|
+
}
|
|
35398
|
+
const seen = /\\Seen/i.test(response);
|
|
35399
|
+
return {
|
|
35400
|
+
uid,
|
|
35401
|
+
subject: headers["subject"] ?? "",
|
|
35402
|
+
from: headers["from"] ?? "",
|
|
35403
|
+
to: headers["to"] ?? "",
|
|
35404
|
+
date: headers["date"] ?? "",
|
|
35405
|
+
snippet: "",
|
|
35406
|
+
seen
|
|
35407
|
+
};
|
|
35408
|
+
}
|
|
35409
|
+
function emptyFullMessage(uid) {
|
|
35410
|
+
return { uid, subject: "", from: "", to: "", cc: "", date: "", bodyText: "", bodyHtml: "", headers: {} };
|
|
35411
|
+
}
|
|
35412
|
+
function parseFullMessage(uid, response) {
|
|
35413
|
+
const bodyMatch = response.match(/\{(\d+)\}\r\n([\s\S]*)/);
|
|
35414
|
+
const rawMessage = bodyMatch ? bodyMatch[2] : response;
|
|
35415
|
+
const headerEnd = rawMessage.indexOf("\r\n\r\n");
|
|
35416
|
+
const headerSection = headerEnd > 0 ? rawMessage.substring(0, headerEnd) : rawMessage;
|
|
35417
|
+
const bodySection = headerEnd > 0 ? rawMessage.substring(headerEnd + 4) : "";
|
|
35418
|
+
const headers = {};
|
|
35419
|
+
const headerLines = headerSection.split(/\r\n/);
|
|
35420
|
+
let currentKey = "";
|
|
35421
|
+
for (const line of headerLines) {
|
|
35422
|
+
if (/^\s/.test(line) && currentKey) {
|
|
35423
|
+
headers[currentKey] += " " + line.trim();
|
|
35424
|
+
} else {
|
|
35425
|
+
const colonIdx = line.indexOf(":");
|
|
35426
|
+
if (colonIdx > 0) {
|
|
35427
|
+
currentKey = line.substring(0, colonIdx).trim().toLowerCase();
|
|
35428
|
+
headers[currentKey] = line.substring(colonIdx + 1).trim();
|
|
35429
|
+
}
|
|
35430
|
+
}
|
|
35431
|
+
}
|
|
35432
|
+
const contentType = headers["content-type"] ?? "text/plain";
|
|
35433
|
+
let bodyText = "";
|
|
35434
|
+
let bodyHtml = "";
|
|
35435
|
+
if (contentType.includes("multipart")) {
|
|
35436
|
+
const boundaryMatch = contentType.match(/boundary="?([^";\s]+)"?/);
|
|
35437
|
+
if (boundaryMatch) {
|
|
35438
|
+
const boundary = boundaryMatch[1];
|
|
35439
|
+
const parts = bodySection.split("--" + boundary);
|
|
35440
|
+
for (const part of parts) {
|
|
35441
|
+
if (part.trim() === "" || part.trim() === "--") continue;
|
|
35442
|
+
const partHeaderEnd = part.indexOf("\r\n\r\n");
|
|
35443
|
+
const partHeaders = partHeaderEnd > 0 ? part.substring(0, partHeaderEnd).toLowerCase() : "";
|
|
35444
|
+
const partBody = partHeaderEnd > 0 ? part.substring(partHeaderEnd + 4).trim() : "";
|
|
35445
|
+
if (partHeaders.includes("text/html")) {
|
|
35446
|
+
bodyHtml = partBody;
|
|
35447
|
+
} else if (partHeaders.includes("text/plain")) {
|
|
35448
|
+
bodyText = partBody;
|
|
35449
|
+
}
|
|
35450
|
+
}
|
|
35451
|
+
}
|
|
35452
|
+
} else if (contentType.includes("text/html")) {
|
|
35453
|
+
bodyHtml = bodySection;
|
|
35454
|
+
} else {
|
|
35455
|
+
bodyText = bodySection;
|
|
35456
|
+
}
|
|
35457
|
+
bodyText = bodyText.replace(/\)\r\n[A-Z]\d+ OK.*$/s, "").trim();
|
|
35458
|
+
bodyHtml = bodyHtml.replace(/\)\r\n[A-Z]\d+ OK.*$/s, "").trim();
|
|
35459
|
+
return {
|
|
35460
|
+
uid,
|
|
35461
|
+
subject: headers["subject"] ?? "",
|
|
35462
|
+
from: headers["from"] ?? "",
|
|
35463
|
+
to: headers["to"] ?? "",
|
|
35464
|
+
cc: headers["cc"] ?? "",
|
|
35465
|
+
date: headers["date"] ?? "",
|
|
35466
|
+
bodyText,
|
|
35467
|
+
bodyHtml,
|
|
35468
|
+
headers
|
|
35469
|
+
};
|
|
35470
|
+
}
|
|
35471
|
+
function createMessenger() {
|
|
35472
|
+
return new Messenger();
|
|
35473
|
+
}
|
|
35474
|
+
var MessengerConnectionError, Messenger, imapTagCounter;
|
|
35475
|
+
var init_messenger = __esm({
|
|
35476
|
+
"../core/src/messenger.ts"() {
|
|
35477
|
+
"use strict";
|
|
35478
|
+
init_dotenv();
|
|
35479
|
+
init_devMailbox();
|
|
35480
|
+
init_logger();
|
|
35481
|
+
MessengerConnectionError = class extends Error {
|
|
35482
|
+
constructor(message) {
|
|
35483
|
+
super(message);
|
|
35484
|
+
this.name = "MessengerConnectionError";
|
|
35485
|
+
}
|
|
35486
|
+
};
|
|
35487
|
+
Messenger = class {
|
|
35488
|
+
host;
|
|
35489
|
+
port;
|
|
35490
|
+
username;
|
|
35491
|
+
password;
|
|
35492
|
+
fromAddress;
|
|
35493
|
+
fromName;
|
|
35494
|
+
encryption;
|
|
35495
|
+
useTls;
|
|
35496
|
+
/** Whether an SMTP host was actually configured (see the constructor). */
|
|
35497
|
+
smtpConfigured = false;
|
|
35498
|
+
/** The local mailbox, present only when this messenger captures. */
|
|
35499
|
+
devMailbox = null;
|
|
35500
|
+
imapHost;
|
|
35501
|
+
imapPort;
|
|
35502
|
+
imapUser;
|
|
35503
|
+
imapPass;
|
|
35504
|
+
imapEncryption;
|
|
35505
|
+
constructor(options) {
|
|
35506
|
+
this.smtpConfigured = Boolean(options?.host ?? process.env.TINA4_MAIL_HOST);
|
|
35507
|
+
this.host = options?.host ?? process.env.TINA4_MAIL_HOST ?? "localhost";
|
|
35508
|
+
this.port = options?.port ?? parseInt(process.env.TINA4_MAIL_PORT ?? "587", 10);
|
|
35509
|
+
this.username = options?.username ?? process.env.TINA4_MAIL_USERNAME ?? "";
|
|
35510
|
+
this.password = options?.password ?? process.env.TINA4_MAIL_PASSWORD ?? "";
|
|
35511
|
+
this.fromAddress = options?.fromAddress ?? process.env.TINA4_MAIL_FROM ?? (this.username || "noreply@localhost");
|
|
35512
|
+
this.fromName = options?.fromName ?? process.env.TINA4_MAIL_FROM_NAME ?? "";
|
|
35513
|
+
const envEncryption = options?.encryption ?? process.env.TINA4_MAIL_ENCRYPTION;
|
|
35514
|
+
if (envEncryption) {
|
|
35515
|
+
this.encryption = envEncryption.toLowerCase();
|
|
35516
|
+
} else if (options?.useTls !== void 0) {
|
|
35517
|
+
this.encryption = options.useTls ? "tls" : "none";
|
|
35518
|
+
} else {
|
|
35519
|
+
this.encryption = "tls";
|
|
35520
|
+
}
|
|
35521
|
+
this.useTls = ["tls", "starttls"].includes(this.encryption);
|
|
35522
|
+
this.imapHost = options?.imapHost ?? process.env.TINA4_MAIL_IMAP_HOST ?? "";
|
|
35523
|
+
this.imapPort = options?.imapPort ?? parseInt(process.env.TINA4_MAIL_IMAP_PORT ?? "993", 10);
|
|
35524
|
+
this.imapUser = options?.imapUser ?? process.env.TINA4_MAIL_IMAP_USERNAME ?? this.username;
|
|
35525
|
+
this.imapPass = options?.imapPass ?? process.env.TINA4_MAIL_IMAP_PASSWORD ?? this.password;
|
|
35526
|
+
this.imapEncryption = (options?.imapEncryption ?? process.env.TINA4_MAIL_IMAP_ENCRYPTION ?? "tls").toLowerCase();
|
|
35527
|
+
}
|
|
35528
|
+
/**
|
|
35529
|
+
* Read-only IMAP encryption mode for inspection / tests.
|
|
35530
|
+
* Returns one of "tls", "starttls", "none", "ssl".
|
|
35531
|
+
*/
|
|
35532
|
+
getImapEncryption() {
|
|
35533
|
+
return this.imapEncryption;
|
|
35534
|
+
}
|
|
35535
|
+
/**
|
|
35536
|
+
* Send an email via SMTP.
|
|
35537
|
+
*/
|
|
35538
|
+
/**
|
|
35539
|
+
* Should send() capture locally instead of talking to SMTP?
|
|
35540
|
+
*
|
|
35541
|
+
* Availability decides, not verbosity. With no SMTP host configured sending is
|
|
35542
|
+
* impossible, so simulate it into a folder rather than failing -- that is what
|
|
35543
|
+
* makes a laptop with no mail server usable. TINA4_MAIL_CAPTURE forces capture
|
|
35544
|
+
* even when a host IS configured.
|
|
35545
|
+
*
|
|
35546
|
+
* TINA4_DEBUG deliberately does NOT gate this, and neither does NODE_ENV. Debug
|
|
35547
|
+
* must still be able to send, and the old `NODE_ENV !== "production"` clause
|
|
35548
|
+
* silently swallowed every staging email.
|
|
35549
|
+
*/
|
|
35550
|
+
shouldCapture() {
|
|
35551
|
+
if (isTruthy(process.env.TINA4_MAIL_CAPTURE)) return true;
|
|
35552
|
+
return !this.smtpConfigured;
|
|
35553
|
+
}
|
|
35554
|
+
/** The local mailbox, created on first capture and reused after. */
|
|
35555
|
+
getDevMailbox() {
|
|
35556
|
+
if (this.devMailbox === null) {
|
|
35557
|
+
this.devMailbox = new DevMailbox();
|
|
35558
|
+
}
|
|
35559
|
+
return this.devMailbox;
|
|
35560
|
+
}
|
|
35561
|
+
async send(to, subject, body, html = false, text, cc, bcc, replyTo, attachments, headers) {
|
|
35562
|
+
const options = { to, subject, body, html, text, cc, bcc, replyTo, attachments, headers };
|
|
35563
|
+
const toList = Array.isArray(options.to) ? options.to : [options.to];
|
|
35564
|
+
const ccList = Array.isArray(options.cc) ? options.cc : options.cc ? [options.cc] : [];
|
|
35565
|
+
const bccList = Array.isArray(options.bcc) ? options.bcc : options.bcc ? [options.bcc] : [];
|
|
35566
|
+
const allRecipients = [...toList, ...ccList, ...bccList];
|
|
35567
|
+
if (this.shouldCapture()) {
|
|
35568
|
+
return this.getDevMailbox().capture(
|
|
35569
|
+
to,
|
|
35570
|
+
subject,
|
|
35571
|
+
body,
|
|
35572
|
+
html,
|
|
35573
|
+
text,
|
|
35574
|
+
ccList,
|
|
35575
|
+
bccList,
|
|
35576
|
+
replyTo,
|
|
35577
|
+
attachments,
|
|
35578
|
+
this.fromAddress || void 0
|
|
35579
|
+
);
|
|
35580
|
+
}
|
|
35581
|
+
const messageId = `${randomUUID9()}@${this.host}`;
|
|
35582
|
+
if (allRecipients.length === 0) {
|
|
35583
|
+
return { success: false, message: "No recipients specified" };
|
|
35584
|
+
}
|
|
35585
|
+
if (!this.fromAddress) {
|
|
35586
|
+
return { success: false, message: "No from address configured" };
|
|
35587
|
+
}
|
|
35588
|
+
try {
|
|
35589
|
+
let socket;
|
|
35590
|
+
if (this.port === 465) {
|
|
35591
|
+
socket = tls2.connect({ host: this.host, port: this.port, rejectUnauthorized: tlsRejectUnauthorized() });
|
|
35592
|
+
await new Promise((resolve31, reject) => {
|
|
35593
|
+
socket.once("secureConnect", resolve31);
|
|
35594
|
+
socket.once("error", reject);
|
|
35595
|
+
});
|
|
35596
|
+
} else {
|
|
35597
|
+
socket = net3.createConnection({ host: this.host, port: this.port });
|
|
35598
|
+
await new Promise((resolve31, reject) => {
|
|
35599
|
+
socket.once("connect", resolve31);
|
|
35600
|
+
socket.once("error", reject);
|
|
35601
|
+
});
|
|
35602
|
+
}
|
|
35603
|
+
const greeting = await readResponse(socket);
|
|
35604
|
+
if (greeting.code !== 220) {
|
|
35605
|
+
socket.destroy();
|
|
35606
|
+
return { success: false, message: `SMTP greeting failed: ${greeting.text}` };
|
|
35607
|
+
}
|
|
35608
|
+
const ehlo = await sendCommand(socket, `EHLO ${this.host}`);
|
|
35609
|
+
if (ehlo.code !== 250) {
|
|
35610
|
+
socket.destroy();
|
|
35611
|
+
return { success: false, message: `EHLO failed: ${ehlo.text}` };
|
|
35612
|
+
}
|
|
35613
|
+
if (this.useTls && this.port !== 465 && ehlo.text.includes("STARTTLS")) {
|
|
35614
|
+
const starttls = await sendCommand(socket, "STARTTLS");
|
|
35615
|
+
if (starttls.code !== 220) {
|
|
35616
|
+
socket.destroy();
|
|
35617
|
+
return { success: false, message: `STARTTLS failed: ${starttls.text}` };
|
|
35618
|
+
}
|
|
35619
|
+
const plainSocket = socket;
|
|
35620
|
+
socket = tls2.connect(
|
|
35621
|
+
{ socket: plainSocket, host: this.host, rejectUnauthorized: tlsRejectUnauthorized() }
|
|
35622
|
+
);
|
|
35623
|
+
await new Promise((resolve31, reject) => {
|
|
35624
|
+
socket.once("secureConnect", resolve31);
|
|
35625
|
+
socket.once("error", reject);
|
|
35626
|
+
});
|
|
35627
|
+
const ehlo2 = await sendCommand(socket, `EHLO ${this.host}`);
|
|
35628
|
+
if (ehlo2.code !== 250) {
|
|
35629
|
+
socket.destroy();
|
|
35630
|
+
return { success: false, message: `EHLO after STARTTLS failed: ${ehlo2.text}` };
|
|
35631
|
+
}
|
|
35632
|
+
}
|
|
35633
|
+
if (this.username && this.password) {
|
|
35634
|
+
const auth = await sendCommand(socket, "AUTH LOGIN");
|
|
35635
|
+
if (auth.code !== 334) {
|
|
35636
|
+
socket.destroy();
|
|
35637
|
+
return { success: false, message: `AUTH LOGIN failed: ${auth.text}` };
|
|
35638
|
+
}
|
|
35639
|
+
const userResp = await sendCommand(socket, Buffer.from(this.username).toString("base64"));
|
|
35640
|
+
if (userResp.code !== 334) {
|
|
35641
|
+
socket.destroy();
|
|
35642
|
+
return { success: false, message: `AUTH username failed: ${userResp.text}` };
|
|
35643
|
+
}
|
|
35644
|
+
const passResp = await sendCommand(socket, Buffer.from(this.password).toString("base64"));
|
|
35645
|
+
if (passResp.code !== 235) {
|
|
35646
|
+
socket.destroy();
|
|
35647
|
+
return { success: false, message: `AUTH password failed: ${passResp.text}` };
|
|
35648
|
+
}
|
|
35649
|
+
}
|
|
35650
|
+
const mailFrom = await sendCommand(socket, `MAIL FROM:<${this.fromAddress}>`);
|
|
35651
|
+
if (mailFrom.code !== 250) {
|
|
35652
|
+
socket.destroy();
|
|
35653
|
+
return { success: false, message: `MAIL FROM failed: ${mailFrom.text}` };
|
|
35654
|
+
}
|
|
35655
|
+
for (const recipient of allRecipients) {
|
|
35656
|
+
const rcpt = await sendCommand(socket, `RCPT TO:<${recipient}>`);
|
|
35657
|
+
if (rcpt.code !== 250 && rcpt.code !== 251) {
|
|
35658
|
+
socket.destroy();
|
|
35659
|
+
return { success: false, message: `RCPT TO <${recipient}> failed: ${rcpt.text}` };
|
|
35660
|
+
}
|
|
35661
|
+
}
|
|
35662
|
+
const dataCmd = await sendCommand(socket, "DATA");
|
|
35663
|
+
if (dataCmd.code !== 354) {
|
|
35664
|
+
socket.destroy();
|
|
35665
|
+
return { success: false, message: `DATA failed: ${dataCmd.text}` };
|
|
35666
|
+
}
|
|
35667
|
+
const mimeMessage = buildMimeMessage({
|
|
35668
|
+
from: this.fromAddress,
|
|
35669
|
+
fromName: this.fromName,
|
|
35670
|
+
to: toList,
|
|
35671
|
+
cc: ccList,
|
|
35672
|
+
subject: options.subject,
|
|
35673
|
+
body: options.body,
|
|
35674
|
+
html: options.html ?? false,
|
|
35675
|
+
text: options.text,
|
|
35676
|
+
replyTo: options.replyTo,
|
|
35677
|
+
attachments: options.attachments,
|
|
35678
|
+
headers: options.headers,
|
|
35679
|
+
messageId
|
|
35680
|
+
});
|
|
35681
|
+
const endData = await sendCommand(socket, mimeMessage + "\r\n.");
|
|
35682
|
+
if (endData.code !== 250) {
|
|
35683
|
+
socket.destroy();
|
|
35684
|
+
return { success: false, message: `Message delivery failed: ${endData.text}` };
|
|
35685
|
+
}
|
|
35686
|
+
await sendCommand(socket, "QUIT");
|
|
35687
|
+
socket.destroy();
|
|
35688
|
+
return { success: true, message: "Email sent successfully", id: messageId };
|
|
35689
|
+
} catch (err) {
|
|
35690
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
35691
|
+
return { success: false, message: `SMTP error: ${errMsg}` };
|
|
35692
|
+
}
|
|
35693
|
+
}
|
|
35694
|
+
/**
|
|
35695
|
+
* Test the SMTP connection without sending an email.
|
|
35696
|
+
*/
|
|
35697
|
+
async testConnection() {
|
|
35698
|
+
try {
|
|
35699
|
+
let socket;
|
|
35700
|
+
if (this.port === 465) {
|
|
35701
|
+
socket = tls2.connect({ host: this.host, port: this.port, rejectUnauthorized: tlsRejectUnauthorized() });
|
|
35702
|
+
await new Promise((resolve31, reject) => {
|
|
35703
|
+
socket.once("secureConnect", resolve31);
|
|
35704
|
+
socket.once("error", reject);
|
|
35705
|
+
});
|
|
35706
|
+
} else {
|
|
35707
|
+
socket = net3.createConnection({ host: this.host, port: this.port });
|
|
35708
|
+
await new Promise((resolve31, reject) => {
|
|
35709
|
+
socket.once("connect", resolve31);
|
|
35710
|
+
socket.once("error", reject);
|
|
35711
|
+
});
|
|
35712
|
+
}
|
|
35713
|
+
const greeting = await readResponse(socket);
|
|
35714
|
+
if (greeting.code !== 220) {
|
|
35715
|
+
socket.destroy();
|
|
35716
|
+
return { success: false, message: `SMTP greeting failed: ${greeting.text}` };
|
|
35717
|
+
}
|
|
35718
|
+
const ehlo = await sendCommand(socket, `EHLO ${this.host}`);
|
|
35719
|
+
if (ehlo.code !== 250) {
|
|
35720
|
+
socket.destroy();
|
|
35721
|
+
return { success: false, message: `EHLO failed: ${ehlo.text}` };
|
|
35722
|
+
}
|
|
35723
|
+
await sendCommand(socket, "QUIT");
|
|
35724
|
+
socket.destroy();
|
|
35725
|
+
return { success: true, message: `Connected to ${this.host}:${this.port}` };
|
|
35726
|
+
} catch (err) {
|
|
35727
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
35728
|
+
return { success: false, message: `Connection failed: ${errMsg}` };
|
|
35729
|
+
}
|
|
35730
|
+
}
|
|
35731
|
+
// ── IMAP (Read) ────────────────────────────────────────────
|
|
35732
|
+
/**
|
|
35733
|
+
* Connect to the IMAP server via raw TCP/TLS.
|
|
35734
|
+
* Returns the socket and reads the greeting.
|
|
35735
|
+
*/
|
|
35736
|
+
async imapConnect() {
|
|
35737
|
+
if (!this.imapHost) {
|
|
35738
|
+
throw new Error("IMAP host not configured (set imapHost or IMAP_HOST env)");
|
|
35739
|
+
}
|
|
35740
|
+
let socket;
|
|
35741
|
+
const useTls = this.imapEncryption === "tls" || this.imapEncryption === "ssl" || this.imapEncryption === "" && this.imapPort === 993;
|
|
35742
|
+
if (useTls) {
|
|
35743
|
+
socket = tls2.connect({ host: this.imapHost, port: this.imapPort, rejectUnauthorized: tlsRejectUnauthorized() });
|
|
35744
|
+
await new Promise((resolve31, reject) => {
|
|
35745
|
+
socket.once("secureConnect", resolve31);
|
|
35746
|
+
socket.once("error", reject);
|
|
35747
|
+
});
|
|
35748
|
+
} else {
|
|
35749
|
+
socket = net3.createConnection({ host: this.imapHost, port: this.imapPort });
|
|
35750
|
+
await new Promise((resolve31, reject) => {
|
|
35751
|
+
socket.once("connect", resolve31);
|
|
35752
|
+
socket.once("error", reject);
|
|
35753
|
+
});
|
|
35754
|
+
}
|
|
35755
|
+
await imapReadLine(socket);
|
|
35756
|
+
if (this.imapUser && this.imapPass) {
|
|
35757
|
+
const loginResp = await imapCommand(socket, `LOGIN ${imapQuote(this.imapUser)} ${imapQuote(this.imapPass)}`);
|
|
35758
|
+
if (!loginResp.includes("OK")) {
|
|
35759
|
+
socket.destroy();
|
|
35760
|
+
throw new Error(`IMAP login failed: ${loginResp}`);
|
|
35761
|
+
}
|
|
35762
|
+
}
|
|
35763
|
+
return socket;
|
|
35764
|
+
}
|
|
35765
|
+
/**
|
|
35766
|
+
* Disconnect from IMAP cleanly.
|
|
35767
|
+
*/
|
|
35768
|
+
async imapDisconnect(socket) {
|
|
35769
|
+
try {
|
|
35770
|
+
await imapCommand(socket, "LOGOUT");
|
|
35771
|
+
} catch {
|
|
35772
|
+
}
|
|
35773
|
+
socket.destroy();
|
|
35774
|
+
}
|
|
35775
|
+
/**
|
|
35776
|
+
* Fetch latest messages from a folder.
|
|
35777
|
+
* Returns list of message summaries.
|
|
35778
|
+
*/
|
|
35779
|
+
async inbox(limit = 20, offset = 0, folder = "INBOX") {
|
|
35780
|
+
let socket;
|
|
35781
|
+
try {
|
|
35782
|
+
socket = await this.imapConnect();
|
|
35783
|
+
} catch (err) {
|
|
35784
|
+
throw imapFail("inbox", err);
|
|
35785
|
+
}
|
|
35786
|
+
try {
|
|
35787
|
+
await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
|
|
35788
|
+
const searchResp = await imapCommand(socket, "SEARCH ALL");
|
|
35789
|
+
const uids = parseSearchResponse(searchResp);
|
|
35790
|
+
if (uids.length === 0) return [];
|
|
35791
|
+
uids.reverse();
|
|
35792
|
+
const selected = uids.slice(offset, offset + limit);
|
|
35793
|
+
if (selected.length === 0) return [];
|
|
35794
|
+
const messages = [];
|
|
35795
|
+
for (const uid of selected) {
|
|
35796
|
+
const fetchResp = await imapCommand(socket, `FETCH ${uid} (FLAGS BODY.PEEK[HEADER.FIELDS (FROM TO SUBJECT DATE)])`);
|
|
35797
|
+
messages.push(parseHeaderResponse(uid, fetchResp));
|
|
35798
|
+
}
|
|
35799
|
+
return messages;
|
|
35800
|
+
} catch (err) {
|
|
35801
|
+
throw imapFail("inbox", err);
|
|
35802
|
+
} finally {
|
|
35803
|
+
await this.imapDisconnect(socket);
|
|
35804
|
+
}
|
|
35805
|
+
}
|
|
35806
|
+
/**
|
|
35807
|
+
* Read a single message by sequence number or UID.
|
|
35808
|
+
*/
|
|
35809
|
+
async read(uid, folder = "INBOX") {
|
|
35810
|
+
let socket;
|
|
35811
|
+
try {
|
|
35812
|
+
socket = await this.imapConnect();
|
|
35813
|
+
} catch (err) {
|
|
35814
|
+
throw imapFail("read", err);
|
|
35815
|
+
}
|
|
35816
|
+
try {
|
|
35817
|
+
await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
|
|
35818
|
+
const fetchResp = await imapCommand(socket, `FETCH ${uid} (FLAGS BODY[])`);
|
|
35819
|
+
if (!/\{\d+\}/.test(fetchResp)) {
|
|
35820
|
+
return emptyFullMessage(uid);
|
|
35821
|
+
}
|
|
35822
|
+
await imapCommand(socket, `STORE ${uid} +FLAGS (\\Seen)`);
|
|
35823
|
+
return parseFullMessage(uid, fetchResp);
|
|
35824
|
+
} catch (err) {
|
|
35825
|
+
throw imapFail("read", err);
|
|
35826
|
+
} finally {
|
|
35827
|
+
await this.imapDisconnect(socket);
|
|
35828
|
+
}
|
|
35829
|
+
}
|
|
35830
|
+
/**
|
|
35831
|
+
* Search messages using IMAP search criteria.
|
|
35832
|
+
*/
|
|
35833
|
+
async search(folder = "INBOX", subject, sender, since, before, unseenOnly = false, limit = 50) {
|
|
35834
|
+
const criteria = ["ALL"];
|
|
35835
|
+
if (subject) criteria.push(`SUBJECT "${subject}"`);
|
|
35836
|
+
if (sender) criteria.push(`FROM "${sender}"`);
|
|
35837
|
+
if (since) criteria.push(`SINCE ${since}`);
|
|
35838
|
+
if (before) criteria.push(`BEFORE ${before}`);
|
|
35839
|
+
if (unseenOnly) criteria.push("UNSEEN");
|
|
35840
|
+
const query = criteria.join(" ");
|
|
35841
|
+
let socket;
|
|
35842
|
+
try {
|
|
35843
|
+
socket = await this.imapConnect();
|
|
35844
|
+
} catch (err) {
|
|
35845
|
+
throw imapFail("search", err);
|
|
35846
|
+
}
|
|
35847
|
+
try {
|
|
35848
|
+
await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
|
|
35849
|
+
const searchResp = await imapCommand(socket, `SEARCH ${query}`);
|
|
35850
|
+
const uids = parseSearchResponse(searchResp);
|
|
35851
|
+
if (uids.length === 0) return [];
|
|
35852
|
+
uids.reverse();
|
|
35853
|
+
const messages = [];
|
|
35854
|
+
for (const uid of uids.slice(0, limit)) {
|
|
35855
|
+
const fetchResp = await imapCommand(socket, `FETCH ${uid} (FLAGS BODY.PEEK[HEADER.FIELDS (FROM TO SUBJECT DATE)])`);
|
|
35856
|
+
messages.push(parseHeaderResponse(uid, fetchResp));
|
|
35857
|
+
}
|
|
35858
|
+
return messages;
|
|
35859
|
+
} catch (err) {
|
|
35860
|
+
throw imapFail("search", err);
|
|
35861
|
+
} finally {
|
|
35862
|
+
await this.imapDisconnect(socket);
|
|
35863
|
+
}
|
|
35864
|
+
}
|
|
35865
|
+
/**
|
|
35866
|
+
* Delete a message by UID.
|
|
35867
|
+
*/
|
|
35868
|
+
async deleteMessage(uid, folder = "INBOX") {
|
|
35869
|
+
const socket = await this.imapConnect();
|
|
35870
|
+
try {
|
|
35871
|
+
await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
|
|
35872
|
+
await imapCommand(socket, `STORE ${uid} +FLAGS (\\Deleted)`);
|
|
35873
|
+
await imapCommand(socket, "EXPUNGE");
|
|
35874
|
+
} finally {
|
|
35875
|
+
await this.imapDisconnect(socket);
|
|
35876
|
+
}
|
|
35877
|
+
}
|
|
35878
|
+
/**
|
|
35879
|
+
* Mark a message as read.
|
|
35880
|
+
*/
|
|
35881
|
+
async markRead(uid, folder = "INBOX") {
|
|
35882
|
+
const socket = await this.imapConnect();
|
|
35883
|
+
try {
|
|
35884
|
+
await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
|
|
35885
|
+
await imapCommand(socket, `STORE ${uid} +FLAGS (\\Seen)`);
|
|
35886
|
+
} finally {
|
|
35887
|
+
await this.imapDisconnect(socket);
|
|
35888
|
+
}
|
|
35889
|
+
}
|
|
35890
|
+
/**
|
|
35891
|
+
* Count unseen messages in a folder.
|
|
35892
|
+
*/
|
|
35893
|
+
async unread(folder = "INBOX") {
|
|
35894
|
+
let socket;
|
|
35895
|
+
try {
|
|
35896
|
+
socket = await this.imapConnect();
|
|
35897
|
+
} catch (err) {
|
|
35898
|
+
throw imapFail("unread", err);
|
|
35899
|
+
}
|
|
35900
|
+
try {
|
|
35901
|
+
await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
|
|
35902
|
+
const searchResp = await imapCommand(socket, "SEARCH UNSEEN");
|
|
35903
|
+
return parseSearchResponse(searchResp).length;
|
|
35904
|
+
} catch (err) {
|
|
35905
|
+
throw imapFail("unread", err);
|
|
35906
|
+
} finally {
|
|
35907
|
+
await this.imapDisconnect(socket);
|
|
35908
|
+
}
|
|
35909
|
+
}
|
|
35910
|
+
/**
|
|
35911
|
+
* List available IMAP folders/mailboxes.
|
|
35912
|
+
*/
|
|
35913
|
+
async folders() {
|
|
35914
|
+
let socket;
|
|
35915
|
+
try {
|
|
35916
|
+
socket = await this.imapConnect();
|
|
35917
|
+
} catch (err) {
|
|
35918
|
+
throw imapFail("folders", err);
|
|
35919
|
+
}
|
|
35920
|
+
try {
|
|
35921
|
+
const resp = await imapCommand(socket, 'LIST "" "*"');
|
|
35922
|
+
const result = [];
|
|
35923
|
+
for (const line of resp.split("\r\n")) {
|
|
35924
|
+
const m = line.match(/\* LIST \([^)]*\) "[^"]*" "?([^"\r\n]+)"?/i);
|
|
35925
|
+
if (m) result.push(m[1]);
|
|
35926
|
+
}
|
|
35927
|
+
return result;
|
|
35928
|
+
} catch (err) {
|
|
35929
|
+
throw imapFail("folders", err);
|
|
35930
|
+
} finally {
|
|
35931
|
+
await this.imapDisconnect(socket);
|
|
35932
|
+
}
|
|
35933
|
+
}
|
|
35934
|
+
/**
|
|
35935
|
+
* Test IMAP connectivity without reading.
|
|
35936
|
+
*/
|
|
35937
|
+
async testImapConnection() {
|
|
35938
|
+
try {
|
|
35939
|
+
const socket = await this.imapConnect();
|
|
35940
|
+
await this.imapDisconnect(socket);
|
|
35941
|
+
return { success: true, message: `Connected to ${this.imapHost}:${this.imapPort}` };
|
|
35942
|
+
} catch (err) {
|
|
35943
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
35944
|
+
return { success: false, message: `IMAP connection failed: ${errMsg}` };
|
|
35945
|
+
}
|
|
35946
|
+
}
|
|
35947
|
+
};
|
|
35948
|
+
imapTagCounter = 0;
|
|
35949
|
+
}
|
|
35950
|
+
});
|
|
35951
|
+
|
|
35661
35952
|
// ../core/src/wsdl.ts
|
|
35662
35953
|
function escapeXml(value) {
|
|
35663
35954
|
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|