@zaclinley/dsh-mailharbor 0.18.0 → 0.19.0

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
@@ -1121,6 +1121,9 @@ function ensureMailSchema(file) {
1121
1121
  if (!emailColumns.some((column) => column.name === "references")) {
1122
1122
  db.exec('ALTER TABLE emails ADD COLUMN "references" TEXT');
1123
1123
  }
1124
+ if (!emailColumns.some((column) => column.name === "refs_indexed")) {
1125
+ db.exec("ALTER TABLE emails ADD COLUMN refs_indexed INTEGER DEFAULT 0");
1126
+ }
1124
1127
  } finally {
1125
1128
  db.close();
1126
1129
  }
@@ -2969,7 +2972,7 @@ ${conversation.trim()}
2969
2972
  }
2970
2973
 
2971
2974
  // src/host/version.ts
2972
- var VERSION = "0.18.0";
2975
+ var VERSION = "0.19.0";
2973
2976
 
2974
2977
  // src/host/api.ts
2975
2978
  init_smtp();
@@ -3191,6 +3194,15 @@ function buildRouter(runtime) {
3191
3194
  }
3192
3195
  return { ok: true, count };
3193
3196
  });
3197
+ router.get("/maintenance/references", ({ url }) => {
3198
+ const { account } = accountFrom(runtime, url);
3199
+ return runtime.referencesStatus(account);
3200
+ });
3201
+ router.post("/maintenance/references", ({ url }) => {
3202
+ const { account } = accountFrom(runtime, url);
3203
+ runtime.startReferencesBackfill(account);
3204
+ return runtime.referencesStatus(account);
3205
+ });
3194
3206
  router.get("/transfer/export", () => {
3195
3207
  const config = runtime.current;
3196
3208
  return {
@@ -4304,7 +4316,7 @@ ${signature}`;
4304
4316
  }
4305
4317
 
4306
4318
  // src/host/runtime.ts
4307
- import { statSync } from "node:fs";
4319
+ import { existsSync as existsSync8, statSync } from "node:fs";
4308
4320
  import { join as join7 } from "node:path";
4309
4321
 
4310
4322
  // src/host/secrets.ts
@@ -4686,11 +4698,63 @@ var MailFlagsStore = class {
4686
4698
  }
4687
4699
  };
4688
4700
 
4701
+ // src/host/references.ts
4702
+ import { DatabaseSync as DatabaseSync4 } from "node:sqlite";
4703
+ function extractReferences(raw, messageId) {
4704
+ const empty = { inReplyTo: null, references: null };
4705
+ if (raw === null || raw === void 0) return empty;
4706
+ const text2 = Buffer.from(raw.subarray(0, Math.min(raw.byteLength, 2e5))).toString("utf8");
4707
+ const separator = text2.search(/\r?\n\r?\n/);
4708
+ const head = separator >= 0 ? text2.slice(0, separator) : text2;
4709
+ const unfolded = head.replace(/\r?\n[ \t]+/g, " ");
4710
+ const value = (name2) => {
4711
+ const match = new RegExp(`^${name2}:\\s*(.*)$`, "im").exec(unfolded);
4712
+ return match === null ? "" : String(match[1] ?? "");
4713
+ };
4714
+ const ids = (value2) => value2.split(/\s+/).map((item) => item.replace(/^<|>$/g, "").toLowerCase()).filter((item) => item.includes("@"));
4715
+ const referenceIds = ids(value("references"));
4716
+ const replyTo = ids(value("in-reply-to"));
4717
+ const filteredRefs = referenceIds.filter((id) => id !== messageId.toLowerCase());
4718
+ return {
4719
+ inReplyTo: replyTo[0] ?? null,
4720
+ references: filteredRefs.length > 0 ? filteredRefs.join(" ") : null
4721
+ };
4722
+ }
4723
+ function backfillReferences(file, batchSize = 300) {
4724
+ ensureMailSchema(file);
4725
+ const db = new DatabaseSync4(file);
4726
+ try {
4727
+ db.exec("PRAGMA busy_timeout = 10000");
4728
+ const rows = db.prepare("SELECT id, message_id, raw_eml FROM emails WHERE refs_indexed = 0 ORDER BY id LIMIT ?").all(batchSize);
4729
+ if (rows.length === 0) return { updated: 0, remaining: 0 };
4730
+ const update = db.prepare(
4731
+ 'UPDATE emails SET in_reply_to = ?, "references" = ?, refs_indexed = 1 WHERE id = ?'
4732
+ );
4733
+ db.exec("BEGIN");
4734
+ try {
4735
+ for (const row of rows) {
4736
+ const headers = extractReferences(row.raw_eml, String(row.message_id ?? ""));
4737
+ update.run(headers.inReplyTo, headers.references, Number(row.id));
4738
+ }
4739
+ db.exec("COMMIT");
4740
+ } catch (error) {
4741
+ db.exec("ROLLBACK");
4742
+ throw error;
4743
+ }
4744
+ const remaining = Number(
4745
+ db.prepare("SELECT COUNT(*) AS count FROM emails WHERE refs_indexed = 0").get()?.count ?? 0
4746
+ );
4747
+ return { updated: rows.length, remaining };
4748
+ } finally {
4749
+ db.close();
4750
+ }
4751
+ }
4752
+
4689
4753
  // src/host/tasks.ts
4690
4754
  import { randomUUID as randomUUID4 } from "node:crypto";
4691
4755
  import { mkdirSync as mkdirSync7 } from "node:fs";
4692
4756
  import { dirname as dirname4 } from "node:path";
4693
- import { DatabaseSync as DatabaseSync4 } from "node:sqlite";
4757
+ import { DatabaseSync as DatabaseSync5 } from "node:sqlite";
4694
4758
  var STATUSES = /* @__PURE__ */ new Set(["NEEDS-ACTION", "IN-PROCESS", "COMPLETED", "CANCELLED"]);
4695
4759
  function now() {
4696
4760
  return (/* @__PURE__ */ new Date()).toISOString();
@@ -4739,7 +4803,7 @@ var TaskStore = class {
4739
4803
  open() {
4740
4804
  if (this.db !== null) return this.db;
4741
4805
  mkdirSync7(dirname4(this.file), { recursive: true });
4742
- const db = new DatabaseSync4(this.file);
4806
+ const db = new DatabaseSync5(this.file);
4743
4807
  db.exec("PRAGMA journal_mode=WAL");
4744
4808
  db.exec("PRAGMA busy_timeout=10000");
4745
4809
  db.exec(`
@@ -5165,6 +5229,7 @@ var MailHarborRuntime = class {
5165
5229
  projectStore = null;
5166
5230
  taskStore = null;
5167
5231
  flagStores = /* @__PURE__ */ new Map();
5232
+ referenceJobs = /* @__PURE__ */ new Map();
5168
5233
  disposed = false;
5169
5234
  constructor(options = {}) {
5170
5235
  this.config = loadConfig();
@@ -5264,6 +5329,43 @@ var MailHarborRuntime = class {
5264
5329
  paths(account) {
5265
5330
  return accountRuntime(account);
5266
5331
  }
5332
+ referencesStatus(account) {
5333
+ return this.referenceJobs.get(account.id) ?? {
5334
+ running: false,
5335
+ indexed: 0,
5336
+ remaining: null,
5337
+ error: null
5338
+ };
5339
+ }
5340
+ /** Backfill In-Reply-To/References for archived mail, one batch at a time. */
5341
+ startReferencesBackfill(account) {
5342
+ const existing = this.referenceJobs.get(account.id);
5343
+ if (existing?.running === true) return;
5344
+ const dbFile = this.paths(account).mailDb;
5345
+ if (!existsSync8(dbFile)) return;
5346
+ const status = {
5347
+ running: true,
5348
+ indexed: 0,
5349
+ remaining: null,
5350
+ error: null
5351
+ };
5352
+ this.referenceJobs.set(account.id, status);
5353
+ void (async () => {
5354
+ try {
5355
+ for (; ; ) {
5356
+ const result = backfillReferences(dbFile, 300);
5357
+ status.indexed += result.updated;
5358
+ status.remaining = result.remaining;
5359
+ if (result.remaining <= 0 || result.updated === 0) break;
5360
+ await new Promise((resolve) => setTimeout(resolve, 40));
5361
+ }
5362
+ } catch (error) {
5363
+ status.error = error instanceof Error ? error.message : String(error);
5364
+ } finally {
5365
+ status.running = false;
5366
+ }
5367
+ })();
5368
+ }
5267
5369
  /** Credentials for an account, with a clear message when the user must log in. */
5268
5370
  credentialsFor(account) {
5269
5371
  const resolved = this.credentials.resolve(account);
@@ -5833,6 +5935,9 @@ function apply(ctx) {
5833
5935
  }
5834
5936
  const handler = createApiHandler(runtime);
5835
5937
  const disposers = [];
5938
+ if (activeAccount2 !== void 0) {
5939
+ setTimeout(() => runtime.startReferencesBackfill(activeAccount2), 3e3);
5940
+ }
5836
5941
  const start = () => {
5837
5942
  disposers.push(ctx.webServer.register({ kind: "prefix", path: ROUTE, handler }));
5838
5943
  if (ctx.tools !== void 0) disposers.push(...registerMailTools(ctx.tools, runtime));