@zaclinley/dsh-mailharbor 0.19.0 → 0.19.1
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/CHANGELOG.md +16 -0
- package/lib/client.js +22 -12
- package/lib/client.js.map +2 -2
- package/lib/index.js +53 -21
- package/lib/index.js.map +2 -2
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -1124,6 +1124,11 @@ function ensureMailSchema(file) {
|
|
|
1124
1124
|
if (!emailColumns.some((column) => column.name === "refs_indexed")) {
|
|
1125
1125
|
db.exec("ALTER TABLE emails ADD COLUMN refs_indexed INTEGER DEFAULT 0");
|
|
1126
1126
|
}
|
|
1127
|
+
try {
|
|
1128
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_emails_in_reply ON emails(in_reply_to)");
|
|
1129
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_emails_message_id_lower ON emails(lower(message_id))");
|
|
1130
|
+
} catch {
|
|
1131
|
+
}
|
|
1127
1132
|
} finally {
|
|
1128
1133
|
db.close();
|
|
1129
1134
|
}
|
|
@@ -1135,6 +1140,7 @@ var MailDatabase = class {
|
|
|
1135
1140
|
ftsChecked = false;
|
|
1136
1141
|
ftsAvailable = false;
|
|
1137
1142
|
threadRefs = /* @__PURE__ */ new Map();
|
|
1143
|
+
threadCache = /* @__PURE__ */ new Map();
|
|
1138
1144
|
constructor(file, options = {}) {
|
|
1139
1145
|
this.file = file;
|
|
1140
1146
|
this.options = options;
|
|
@@ -1189,6 +1195,7 @@ var MailDatabase = class {
|
|
|
1189
1195
|
}
|
|
1190
1196
|
this.db = void 0;
|
|
1191
1197
|
this.threadRefs.clear();
|
|
1198
|
+
this.threadCache.clear();
|
|
1192
1199
|
}
|
|
1193
1200
|
status() {
|
|
1194
1201
|
try {
|
|
@@ -1392,9 +1399,15 @@ var MailDatabase = class {
|
|
|
1392
1399
|
const normalize2 = (value) => value.replace(/^(\s*(re|fw|fwd|回复|转发)\s*[::]\s*)+/gi, "").replace(/\s+/g, " ").trim().toLowerCase();
|
|
1393
1400
|
const base = normalize2(seed.subject);
|
|
1394
1401
|
if (base === "") return [];
|
|
1402
|
+
const cacheKey = `${seed.messageId ?? ""}|${base}`;
|
|
1403
|
+
const cached = this.threadCache.get(cacheKey);
|
|
1404
|
+
if (cached !== void 0 && Date.now() - cached.at < 3e4) return cached.items;
|
|
1395
1405
|
const seedIdForQuery = (seed.messageId ?? "").replace(/^<|>$/g, "").toLowerCase();
|
|
1396
1406
|
const candidates = this.threadCandidates(base, limit, seedIdForQuery);
|
|
1397
|
-
if (candidates.length <= 1)
|
|
1407
|
+
if (candidates.length <= 1) {
|
|
1408
|
+
this.threadCache.set(cacheKey, { at: Date.now(), items: candidates });
|
|
1409
|
+
return candidates;
|
|
1410
|
+
}
|
|
1398
1411
|
const refs = this.referencesFor(candidates.slice(0, 120).map((item) => Number(item.id)));
|
|
1399
1412
|
const byMessageId = /* @__PURE__ */ new Map();
|
|
1400
1413
|
for (const item of candidates) {
|
|
@@ -1432,7 +1445,10 @@ var MailDatabase = class {
|
|
|
1432
1445
|
refs: [...refs.entries()].slice(0, 3)
|
|
1433
1446
|
});
|
|
1434
1447
|
}
|
|
1435
|
-
if (edges === 0)
|
|
1448
|
+
if (edges === 0) {
|
|
1449
|
+
this.threadCache.set(cacheKey, { at: Date.now(), items: candidates });
|
|
1450
|
+
return candidates;
|
|
1451
|
+
}
|
|
1436
1452
|
const seen = /* @__PURE__ */ new Set();
|
|
1437
1453
|
const queue = [byMessageId.get(startId)];
|
|
1438
1454
|
while (queue.length > 0) {
|
|
@@ -1443,24 +1459,27 @@ var MailDatabase = class {
|
|
|
1443
1459
|
if (!seen.has(next)) queue.push(next);
|
|
1444
1460
|
}
|
|
1445
1461
|
}
|
|
1446
|
-
|
|
1447
|
-
|
|
1462
|
+
const result = seen.size <= 1 ? candidates : candidates.filter((item) => seen.has(Number(item.id)));
|
|
1463
|
+
this.threadCache.set(cacheKey, { at: Date.now(), items: result });
|
|
1464
|
+
return result;
|
|
1448
1465
|
}
|
|
1449
1466
|
/** Subject candidates via a distinctive token, not FTS (punctuation-safe). */
|
|
1450
1467
|
threadCandidates(base, limit, seedId = "") {
|
|
1451
1468
|
const normalize2 = (value) => value.replace(/^(\s*(re|fw|fwd|回复|转发)\s*[::]\s*)+/gi, "").replace(/\s+/g, " ").trim().toLowerCase();
|
|
1452
1469
|
const words = base.split(/[^\p{L}\p{N}]+/u).filter((word) => word.length >= 3);
|
|
1453
1470
|
const token = words.sort((left, right) => right.length - left.length)[0] ?? base.slice(0, 8);
|
|
1454
|
-
const
|
|
1455
|
-
try {
|
|
1456
|
-
const rows = this.open().prepare(
|
|
1457
|
-
`SELECT e.id, e.mailbox, e.uid, e.subject, e.from_name, e.from_addr, e.to_addr, e.cc,
|
|
1471
|
+
const select = `SELECT e.id, e.mailbox, e.uid, e.subject, e.from_name, e.from_addr, e.to_addr, e.cc,
|
|
1458
1472
|
e.message_id, e.date, length(e.body_text) AS text_length,
|
|
1459
1473
|
(SELECT COUNT(*) FROM attachments a WHERE a.email_id = e.id) AS attachment_count,
|
|
1460
1474
|
substr(COALESCE(e.body_text, ''), 1, 240) AS snippet
|
|
1461
|
-
FROM emails e
|
|
1462
|
-
|
|
1463
|
-
|
|
1475
|
+
FROM emails e`;
|
|
1476
|
+
try {
|
|
1477
|
+
const ftsQuery = `subject:"${token.replace(/"/g, '""')}"`;
|
|
1478
|
+
const rows = this.open().prepare(
|
|
1479
|
+
`${select} JOIN emails_fts f ON f.rowid = e.id
|
|
1480
|
+
WHERE emails_fts MATCH ?
|
|
1481
|
+
ORDER BY e.date DESC LIMIT 500`
|
|
1482
|
+
).all(ftsQuery);
|
|
1464
1483
|
const items = rows.map((row) => rowToSummary(row)).filter((item) => normalize2(item.subject) === base);
|
|
1465
1484
|
if (seedId !== "" && this.hasRefColumns()) {
|
|
1466
1485
|
const linked = this.open().prepare(
|
|
@@ -1469,9 +1488,9 @@ var MailDatabase = class {
|
|
|
1469
1488
|
(SELECT COUNT(*) FROM attachments a WHERE a.email_id = e.id) AS attachment_count,
|
|
1470
1489
|
substr(COALESCE(e.body_text, ''), 1, 240) AS snippet
|
|
1471
1490
|
FROM emails e
|
|
1472
|
-
WHERE
|
|
1491
|
+
WHERE e.in_reply_to = ?
|
|
1473
1492
|
ORDER BY e.date DESC LIMIT 100`
|
|
1474
|
-
).all(seedId
|
|
1493
|
+
).all(seedId);
|
|
1475
1494
|
const known = new Set(items.map((item) => item.id));
|
|
1476
1495
|
try {
|
|
1477
1496
|
const seedRow = this.open().prepare(
|
|
@@ -1512,7 +1531,8 @@ var MailDatabase = class {
|
|
|
1512
1531
|
}
|
|
1513
1532
|
return items.slice(0, limit);
|
|
1514
1533
|
} catch {
|
|
1515
|
-
|
|
1534
|
+
const page = this.list({ mailbox: "all", query: base, limit: Math.min(500, limit * 2) });
|
|
1535
|
+
return page.items.filter((item) => normalize2(item.subject) === base).slice(0, limit);
|
|
1516
1536
|
}
|
|
1517
1537
|
}
|
|
1518
1538
|
/** Read (and cache) In-Reply-To / References for a set of local ids. */
|
|
@@ -1522,11 +1542,11 @@ var MailDatabase = class {
|
|
|
1522
1542
|
const placeholders = missing.map(() => "?").join(",");
|
|
1523
1543
|
try {
|
|
1524
1544
|
const withRefs = this.hasRefColumns();
|
|
1525
|
-
const columns = withRefs ? 'id, message_id,
|
|
1545
|
+
const columns = withRefs ? 'id, message_id, in_reply_to, "references", refs_indexed' : "id, message_id, raw_eml";
|
|
1526
1546
|
const rows = this.open().prepare(`SELECT ${columns} FROM emails WHERE id IN (${placeholders})`).all(...missing);
|
|
1547
|
+
const needRaw = [];
|
|
1527
1548
|
for (const row of rows) {
|
|
1528
1549
|
const id = Number(row.id);
|
|
1529
|
-
const raw = row.raw_eml;
|
|
1530
1550
|
const refs = [];
|
|
1531
1551
|
for (const stored of [row.references ?? "", row.in_reply_to ?? ""]) {
|
|
1532
1552
|
for (const found of String(stored).split(/\s+/)) {
|
|
@@ -1534,16 +1554,28 @@ var MailDatabase = class {
|
|
|
1534
1554
|
if (value.includes("@")) refs.push(value);
|
|
1535
1555
|
}
|
|
1536
1556
|
}
|
|
1537
|
-
if (refs.length === 0 &&
|
|
1538
|
-
|
|
1539
|
-
for (const found of headerIds(raw, header)) refs.push(found);
|
|
1540
|
-
}
|
|
1557
|
+
if (refs.length === 0 && (!withRefs || Number(row.refs_indexed ?? 0) !== 1)) {
|
|
1558
|
+
needRaw.push(id);
|
|
1541
1559
|
}
|
|
1542
1560
|
this.threadRefs.set(id, {
|
|
1543
1561
|
messageId: String(row.message_id ?? "").replace(/^<|>$/g, "").toLowerCase(),
|
|
1544
1562
|
refs: [...new Set(refs)]
|
|
1545
1563
|
});
|
|
1546
1564
|
}
|
|
1565
|
+
if (needRaw.length > 0) {
|
|
1566
|
+
const marks = needRaw.map(() => "?").join(",");
|
|
1567
|
+
const rawRows = this.open().prepare(`SELECT id, raw_eml FROM emails WHERE id IN (${marks})`).all(...needRaw);
|
|
1568
|
+
for (const row of rawRows) {
|
|
1569
|
+
const id = Number(row.id);
|
|
1570
|
+
const entry = this.threadRefs.get(id);
|
|
1571
|
+
if (entry === void 0 || row.raw_eml === null || row.raw_eml === void 0) continue;
|
|
1572
|
+
const refs = [...entry.refs];
|
|
1573
|
+
for (const header of ["references", "in-reply-to"]) {
|
|
1574
|
+
for (const found of headerIds(row.raw_eml, header)) refs.push(found);
|
|
1575
|
+
}
|
|
1576
|
+
this.threadRefs.set(id, { ...entry, refs: [...new Set(refs)] });
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1547
1579
|
} catch {
|
|
1548
1580
|
}
|
|
1549
1581
|
}
|
|
@@ -2972,7 +3004,7 @@ ${conversation.trim()}
|
|
|
2972
3004
|
}
|
|
2973
3005
|
|
|
2974
3006
|
// src/host/version.ts
|
|
2975
|
-
var VERSION = "0.19.
|
|
3007
|
+
var VERSION = "0.19.1";
|
|
2976
3008
|
|
|
2977
3009
|
// src/host/api.ts
|
|
2978
3010
|
init_smtp();
|