@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/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) return candidates;
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) return candidates;
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
- if (seen.size <= 1) return candidates;
1447
- return candidates.filter((item) => seen.has(Number(item.id)));
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 pattern = `%${token.replace(/[%_]/g, "\\$&")}%`;
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 WHERE lower(e.subject) LIKE ? ESCAPE '\\'
1462
- ORDER BY e.date DESC LIMIT ?`
1463
- ).all(pattern, 500);
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 lower(e.in_reply_to) = ? OR lower(e."references") LIKE ? ESCAPE '\\'
1491
+ WHERE e.in_reply_to = ?
1473
1492
  ORDER BY e.date DESC LIMIT 100`
1474
- ).all(seedId, `%${seedId.replace(/[%_]/g, "\\$&")}%`);
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
- return [];
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, raw_eml, in_reply_to, "references"' : "id, message_id, raw_eml";
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 && raw !== void 0 && raw !== null) {
1538
- for (const header of ["references", "in-reply-to"]) {
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.0";
3007
+ var VERSION = "0.19.1";
2976
3008
 
2977
3009
  // src/host/api.ts
2978
3010
  init_smtp();