@zaclinley/dsh-mailharbor 0.13.0 → 0.13.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
@@ -751,6 +751,7 @@ function normalizeAccount(input, index = 0, existing = []) {
751
751
  dataDir: stringOr(input.dataDir, `accounts/${id}`),
752
752
  auth: input.auth?.type === "oauth2" ? { type: "oauth2", provider: input.auth.provider } : { type: "password" },
753
753
  caldav: normalizeCaldav(input.caldav),
754
+ signature: stringOr(input.signature, "").slice(0, 5e3),
754
755
  enabled: input.enabled !== false,
755
756
  createdAt: stringOr(input.createdAt, (/* @__PURE__ */ new Date()).toISOString())
756
757
  };
@@ -990,6 +991,9 @@ async function openPath(path) {
990
991
  await run("xdg-open", [path]);
991
992
  }
992
993
 
994
+ // src/host/api.ts
995
+ import { existsSync as existsSync6, rmSync } from "node:fs";
996
+
993
997
  // src/host/mail-write.ts
994
998
  import { DatabaseSync } from "node:sqlite";
995
999
  function moveLocalMessage(file, id, mailbox) {
@@ -1287,6 +1291,50 @@ var MailDatabase = class {
1287
1291
  const rows = db.prepare(`${columns} FROM emails e ${clause} ORDER BY e.date DESC, e.id DESC LIMIT ?`).all(...params, limit);
1288
1292
  return { items: rows.map((row) => rowToSummary(row)), total, offset: 0, limit };
1289
1293
  }
1294
+ /** Frequent senders and recipients, for compose autocomplete. */
1295
+ contacts(query = "", limit = 50) {
1296
+ const text2 = query.trim().toLowerCase();
1297
+ const counts = /* @__PURE__ */ new Map();
1298
+ const add = (name2, address) => {
1299
+ const key = address.toLowerCase();
1300
+ if (key === "" || !key.includes("@")) return;
1301
+ const existing = counts.get(key);
1302
+ if (existing === void 0) counts.set(key, { name: name2, address, count: 1 });
1303
+ else {
1304
+ existing.count += 1;
1305
+ if (existing.name === "" && name2 !== "") existing.name = name2;
1306
+ }
1307
+ };
1308
+ try {
1309
+ const db = this.open();
1310
+ for (const row of db.prepare(
1311
+ `SELECT from_name AS name, from_addr AS address FROM emails
1312
+ WHERE from_addr <> '' AND from_addr LIKE '%@%' ORDER BY date DESC LIMIT 3000`
1313
+ ).all()) {
1314
+ add(String(row.name ?? ""), String(row.address ?? ""));
1315
+ }
1316
+ for (const row of db.prepare(
1317
+ `SELECT to_addr AS address, cc AS copies FROM emails
1318
+ WHERE (to_addr <> '' OR cc <> '') ORDER BY date DESC LIMIT 1500`
1319
+ ).all()) {
1320
+ for (const entry of parseAddressList(String(row.address ?? ""))) add(entry.name, entry.address);
1321
+ for (const entry of parseAddressList(String(row.copies ?? ""))) add(entry.name, entry.address);
1322
+ }
1323
+ } catch {
1324
+ return [];
1325
+ }
1326
+ return [...counts.values()].filter(
1327
+ (entry) => text2 === "" || entry.address.toLowerCase().includes(text2) || entry.name.toLowerCase().includes(text2)
1328
+ ).sort((left, right) => right.count - left.count).slice(0, limit);
1329
+ }
1330
+ /** All archived messages in the same conversation (normalized subject). */
1331
+ thread(subject, limit = 200) {
1332
+ const normalize2 = (value) => value.replace(/^(\s*(re|fw|fwd|回复|转发)\s*[::]\s*)+/gi, "").replace(/\s+/g, " ").trim().toLowerCase();
1333
+ const base = normalize2(subject);
1334
+ if (base === "") return [];
1335
+ const page = this.list({ mailbox: "all", query: base, limit: Math.min(500, limit * 2) });
1336
+ return page.items.filter((item) => normalize2(item.subject) === base).slice(0, limit);
1337
+ }
1290
1338
  /** Number of unread keys that still exist in this backup. */
1291
1339
  countUnread(keys) {
1292
1340
  const mids = keys.mids.slice(0, 500);
@@ -2699,7 +2747,7 @@ ${conversation.trim()}
2699
2747
  }
2700
2748
 
2701
2749
  // src/host/version.ts
2702
- var VERSION = "0.13.0";
2750
+ var VERSION = "0.13.1";
2703
2751
 
2704
2752
  // src/host/api.ts
2705
2753
  init_smtp();
@@ -2913,6 +2961,17 @@ function buildRouter(runtime) {
2913
2961
  }
2914
2962
  return { ok: true, count };
2915
2963
  });
2964
+ router.get("/messages/thread", ({ url }) => {
2965
+ const { account } = accountFrom(runtime, url);
2966
+ const subject = url.searchParams.get("subject") ?? "";
2967
+ const items = runtime.database(account).thread(subject);
2968
+ return { items: items.map((item) => localSummary(runtime, account, item)) };
2969
+ });
2970
+ router.get("/contacts", ({ url }) => {
2971
+ const { account } = accountFrom(runtime, url);
2972
+ const query = url.searchParams.get("q") ?? "";
2973
+ return { items: runtime.database(account).contacts(query, 50) };
2974
+ });
2916
2975
  router.get("/messages/:id", async ({ url, params }) => {
2917
2976
  const { account } = accountFrom(runtime, url);
2918
2977
  const source = sourceOf(url);
@@ -3035,6 +3094,7 @@ function buildRouter(runtime) {
3035
3094
  imap: { ...existing.imap, ...asObject(payload.imap) },
3036
3095
  smtp: { ...existing.smtp, ...asObject(payload.smtp) },
3037
3096
  caldav: payload.caldav === null ? null : payload.caldav === void 0 ? existing.caldav : { ...asObject(payload.caldav) },
3097
+ signature: typeof payload.signature === "string" ? payload.signature.slice(0, 5e3) : existing.signature,
3038
3098
  enabled: payload.enabled === void 0 ? existing.enabled : payload.enabled !== false
3039
3099
  };
3040
3100
  const account = normalizeAccount(
@@ -3443,17 +3503,48 @@ function buildRouter(runtime) {
3443
3503
  if (project === void 0) throw new HttpError(404, "\u9879\u76EE\u4E0D\u5B58\u5728");
3444
3504
  return { project: projectView(project) };
3445
3505
  });
3446
- router.delete("/projects/:id", ({ params }) => {
3506
+ router.delete("/projects/:id", async ({ params, url }) => {
3447
3507
  const id = String(params.id ?? "");
3448
3508
  const project = runtime.projects().get(id);
3449
3509
  if (project === void 0) throw new HttpError(404, "\u9879\u76EE\u4E0D\u5B58\u5728");
3510
+ const warnings = [];
3450
3511
  const removed = runtime.projects().remove(id);
3451
3512
  const unlinked = runtime.tasks().clearProject(id);
3513
+ let workspaceRemoved = false;
3514
+ if (project.workspaceId !== "" && runtime.workspaceRegistry?.delete !== void 0) {
3515
+ try {
3516
+ workspaceRemoved = await runtime.workspaceRegistry.delete(project.workspaceId);
3517
+ } catch (error) {
3518
+ warnings.push(`\u79FB\u9664\u5DE5\u4F5C\u7A7A\u95F4\u5931\u8D25\uFF1A${error instanceof Error ? error.message : String(error)}`);
3519
+ }
3520
+ } else if (project.workspaceId !== "") {
3521
+ warnings.push("\u5F53\u524D DSH \u7EC4\u5408\u4E0D\u652F\u6301\u81EA\u52A8\u79FB\u9664\u5DE5\u4F5C\u7A7A\u95F4\uFF0C\u8BF7\u5728\u4FA7\u680F\u624B\u52A8\u5220\u9664");
3522
+ }
3523
+ let filesRemoved = false;
3524
+ if (url.searchParams.get("files") === "1") {
3525
+ try {
3526
+ const root = runtime.current.projects.root;
3527
+ const insideRoot = project.path === root || project.path.startsWith(`${root}/`);
3528
+ const marker = existsSync6(`${project.path}/project.json`);
3529
+ if (!insideRoot && !marker) {
3530
+ warnings.push("\u9879\u76EE\u76EE\u5F55\u4E0D\u5728\u9ED8\u8BA4\u9879\u76EE\u6839\u76EE\u5F55\u4E0B\uFF0C\u51FA\u4E8E\u5B89\u5168\u8003\u8651\u672A\u5220\u9664\u6587\u4EF6");
3531
+ } else if (project.path === root || project.path === "/") {
3532
+ warnings.push("\u62D2\u7EDD\u5220\u9664\u9879\u76EE\u6839\u76EE\u5F55");
3533
+ } else {
3534
+ rmSync(project.path, { recursive: true, force: true });
3535
+ filesRemoved = true;
3536
+ }
3537
+ } catch (error) {
3538
+ warnings.push(`\u5220\u9664\u6587\u4EF6\u5939\u5931\u8D25\uFF1A${error instanceof Error ? error.message : String(error)}`);
3539
+ }
3540
+ }
3452
3541
  return {
3453
3542
  ok: removed,
3454
3543
  path: project.path,
3455
3544
  unlinkedTasks: unlinked,
3456
- workspaceKept: project.workspaceId !== ""
3545
+ workspaceRemoved,
3546
+ filesRemoved,
3547
+ warnings
3457
3548
  };
3458
3549
  });
3459
3550
  router.post("/projects/:id/open", async ({ params }) => {
@@ -3835,7 +3926,7 @@ async function sendFromPayload(runtime, account, payload) {
3835
3926
  const totalBytes = attachments.reduce((sum, file) => sum + file.content.length, 0);
3836
3927
  if (totalBytes > 15 * 1024 * 1024) throw new HttpError(413, "\u9644\u4EF6\u5408\u8BA1\u4E0D\u80FD\u8D85\u8FC7 15 MB");
3837
3928
  const { sendMail: sendMail2 } = await Promise.resolve().then(() => (init_smtp(), smtp_exports));
3838
- const signature = runtime.current.signature;
3929
+ const signature = (account.signature ?? "").trim() !== "" ? String(account.signature).trim() : runtime.current.signature;
3839
3930
  const bodyText2 = signature === "" ? text2 : `${text2}
3840
3931
 
3841
3932
  --
@@ -4051,7 +4142,7 @@ var CredentialManager = class {
4051
4142
  };
4052
4143
 
4053
4144
  // src/host/ai-store.ts
4054
- import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync5, renameSync as renameSync4, writeFileSync as writeFileSync5 } from "node:fs";
4145
+ import { existsSync as existsSync7, mkdirSync as mkdirSync5, readFileSync as readFileSync5, renameSync as renameSync4, writeFileSync as writeFileSync5 } from "node:fs";
4055
4146
  import { dirname as dirname2 } from "node:path";
4056
4147
  var MAX_THREADS = 50;
4057
4148
  var MAX_CARDS_PER_THREAD = 50;
@@ -4066,7 +4157,7 @@ var AiThreadStore = class {
4066
4157
  if (this.cache !== null) return this.cache;
4067
4158
  let parsed = null;
4068
4159
  try {
4069
- if (existsSync6(this.file)) parsed = JSON.parse(readFileSync5(this.file, "utf8"));
4160
+ if (existsSync7(this.file)) parsed = JSON.parse(readFileSync5(this.file, "utf8"));
4070
4161
  } catch {
4071
4162
  parsed = null;
4072
4163
  }
@@ -4900,12 +4991,13 @@ var MailHarborRuntime = class {
4900
4991
  const running = this.backups.get(account.id);
4901
4992
  if (running !== void 0 && running.status.running) throw new Error("\u5907\u4EFD\u5DF2\u5728\u8FDB\u884C\u4E2D");
4902
4993
  const controller = new AbortController();
4994
+ const previous = this.backups.get(account.id)?.status;
4903
4995
  const status = {
4904
4996
  running: true,
4905
4997
  accountId: account.id,
4906
4998
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
4907
4999
  finishedAt: null,
4908
- lastSuccessAt: null,
5000
+ lastSuccessAt: previous?.lastSuccessAt ?? null,
4909
5001
  total: 0,
4910
5002
  added: 0,
4911
5003
  message: "\u51C6\u5907\u5907\u4EFD",