@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/CHANGELOG.md +16 -0
- package/lib/client.js +93 -16
- package/lib/client.js.map +2 -2
- package/lib/index.js +99 -7
- package/lib/index.js.map +3 -3
- package/package.json +1 -1
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.
|
|
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
|
-
|
|
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
|
|
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 (
|
|
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",
|