@zaclinley/dsh-mailharbor 0.6.0 → 0.7.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 +28 -0
- package/README.md +18 -18
- package/README.zh.md +18 -18
- package/lib/client.js +414 -164
- package/lib/client.js.map +3 -3
- package/lib/index.js +248 -1
- package/lib/index.js.map +4 -4
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -390,6 +390,41 @@ var init_imap = __esm({
|
|
|
390
390
|
}
|
|
391
391
|
});
|
|
392
392
|
}
|
|
393
|
+
async search(mailbox, options) {
|
|
394
|
+
return this.run(async (client) => {
|
|
395
|
+
const lock = await client.getMailboxLock(mailbox);
|
|
396
|
+
try {
|
|
397
|
+
const criteria = {};
|
|
398
|
+
if ((options.from ?? "") !== "") criteria.from = options.from;
|
|
399
|
+
if ((options.subject ?? "") !== "") criteria.subject = options.subject;
|
|
400
|
+
const keywords = (options.keywords ?? []).filter((keyword) => keyword !== "");
|
|
401
|
+
if (keywords.length > 0) criteria.text = keywords.join(" ");
|
|
402
|
+
if ((options.since ?? "") !== "") criteria.since = /* @__PURE__ */ new Date(`${String(options.since)}T00:00:00Z`);
|
|
403
|
+
if ((options.before ?? "") !== "") criteria.before = /* @__PURE__ */ new Date(`${String(options.before)}T23:59:59Z`);
|
|
404
|
+
if (Object.keys(criteria).length === 0) return { items: [], total: 0 };
|
|
405
|
+
const found = await client.search(criteria, { uid: true });
|
|
406
|
+
const uids = (Array.isArray(found) ? found : []).map((uid) => Number(uid)).filter((uid) => Number.isFinite(uid)).sort((left, right) => left - right);
|
|
407
|
+
const take = uids.slice(-Math.min(100, options.limit ?? 60));
|
|
408
|
+
if (take.length === 0) return { items: [], total: uids.length };
|
|
409
|
+
const wanted = new Set(take);
|
|
410
|
+
const items = [];
|
|
411
|
+
const first = take[0] ?? 0;
|
|
412
|
+
const last = take[take.length - 1] ?? 0;
|
|
413
|
+
for await (const message of client.fetch(
|
|
414
|
+
`${String(first)}:${String(last)}`,
|
|
415
|
+
{ uid: true, envelope: true, flags: true, internalDate: true, size: true },
|
|
416
|
+
{ uid: true }
|
|
417
|
+
)) {
|
|
418
|
+
const item = toMessage(message);
|
|
419
|
+
if (wanted.has(item.uid)) items.push(item);
|
|
420
|
+
}
|
|
421
|
+
items.sort((left, right) => right.date.localeCompare(left.date));
|
|
422
|
+
return { items, total: uids.length };
|
|
423
|
+
} finally {
|
|
424
|
+
lock.release();
|
|
425
|
+
}
|
|
426
|
+
});
|
|
427
|
+
}
|
|
393
428
|
async read(mailbox, uid) {
|
|
394
429
|
return this.run(async (client) => {
|
|
395
430
|
const lock = await client.getMailboxLock(mailbox);
|
|
@@ -784,6 +819,66 @@ function activeAccount(config) {
|
|
|
784
819
|
return config.accounts.find((account) => account.id === config.activeAccountId) ?? null;
|
|
785
820
|
}
|
|
786
821
|
|
|
822
|
+
// src/host/picker.ts
|
|
823
|
+
import { execFile } from "node:child_process";
|
|
824
|
+
import { createRequire } from "node:module";
|
|
825
|
+
import { promisify } from "node:util";
|
|
826
|
+
var run = promisify(execFile);
|
|
827
|
+
async function electronDialog(defaultPath) {
|
|
828
|
+
try {
|
|
829
|
+
const require2 = createRequire(import.meta.url);
|
|
830
|
+
const electron = require2("electron");
|
|
831
|
+
if (electron.dialog === void 0) return void 0;
|
|
832
|
+
const result = await electron.dialog.showOpenDialog({
|
|
833
|
+
title: "\u9009\u62E9\u9879\u76EE\u76EE\u5F55",
|
|
834
|
+
...defaultPath === void 0 ? {} : { defaultPath },
|
|
835
|
+
properties: ["openDirectory", "createDirectory"]
|
|
836
|
+
});
|
|
837
|
+
return result.canceled ? null : result.filePaths[0] ?? null;
|
|
838
|
+
} catch {
|
|
839
|
+
return void 0;
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
async function pickDirectory(defaultPath) {
|
|
843
|
+
const electron = await electronDialog(defaultPath);
|
|
844
|
+
if (electron !== void 0) return electron;
|
|
845
|
+
if (process.platform === "darwin") {
|
|
846
|
+
try {
|
|
847
|
+
const { stdout } = await run(
|
|
848
|
+
"osascript",
|
|
849
|
+
["-e", 'POSIX path of (choose folder with prompt "\u9009\u62E9\u9879\u76EE\u76EE\u5F55")'],
|
|
850
|
+
{ timeout: 18e4 }
|
|
851
|
+
);
|
|
852
|
+
const path = stdout.trim();
|
|
853
|
+
return path === "" ? null : path.replace(/\/$/, "");
|
|
854
|
+
} catch {
|
|
855
|
+
return null;
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
if (process.platform === "win32") {
|
|
859
|
+
const script = [
|
|
860
|
+
"Add-Type -AssemblyName System.Windows.Forms",
|
|
861
|
+
"$d = New-Object System.Windows.Forms.FolderBrowserDialog",
|
|
862
|
+
'$d.Description = "\u9009\u62E9\u9879\u76EE\u76EE\u5F55"',
|
|
863
|
+
'if ($d.ShowDialog() -eq "OK") { Write-Output $d.SelectedPath }'
|
|
864
|
+
].join("; ");
|
|
865
|
+
try {
|
|
866
|
+
const { stdout } = await run("powershell", ["-NoProfile", "-Command", script], { timeout: 18e4 });
|
|
867
|
+
return stdout.trim() === "" ? null : stdout.trim();
|
|
868
|
+
} catch {
|
|
869
|
+
return null;
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
try {
|
|
873
|
+
const { stdout } = await run("zenity", ["--file-selection", "--directory", "--title=\u9009\u62E9\u9879\u76EE\u76EE\u5F55"], {
|
|
874
|
+
timeout: 18e4
|
|
875
|
+
});
|
|
876
|
+
return stdout.trim() === "" ? null : stdout.trim();
|
|
877
|
+
} catch {
|
|
878
|
+
throw new Error("\u5F53\u524D\u73AF\u5883\u65E0\u6CD5\u6253\u5F00\u76EE\u5F55\u9009\u62E9\u5668\uFF0C\u8BF7\u624B\u52A8\u8F93\u5165\u8DEF\u5F84");
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
|
|
787
882
|
// src/host/backup.ts
|
|
788
883
|
init_address();
|
|
789
884
|
import { createHash as createHash2 } from "node:crypto";
|
|
@@ -971,6 +1066,58 @@ var MailDatabase = class {
|
|
|
971
1066
|
).all(...params, limit, offset);
|
|
972
1067
|
return { items: rows.map((row) => rowToSummary(row)), total, offset, limit };
|
|
973
1068
|
}
|
|
1069
|
+
/** Structured search used by AI search (keywords + sender + dates). */
|
|
1070
|
+
searchAdvanced(options) {
|
|
1071
|
+
const db = this.open();
|
|
1072
|
+
const where = [];
|
|
1073
|
+
const params = [];
|
|
1074
|
+
const mailbox = String(options.mailbox ?? "all");
|
|
1075
|
+
if (mailbox !== "" && mailbox !== "all") {
|
|
1076
|
+
where.push("e.mailbox = ?");
|
|
1077
|
+
params.push(mailbox);
|
|
1078
|
+
}
|
|
1079
|
+
const keywords = (options.keywords ?? []).filter((keyword) => keyword !== "");
|
|
1080
|
+
if (keywords.length > 0) {
|
|
1081
|
+
const clauses = [];
|
|
1082
|
+
for (const keyword of keywords) {
|
|
1083
|
+
clauses.push(
|
|
1084
|
+
"(e.subject LIKE ? OR e.from_name LIKE ? OR e.from_addr LIKE ? OR e.to_addr LIKE ? OR e.body_text LIKE ?)"
|
|
1085
|
+
);
|
|
1086
|
+
const like = `%${keyword}%`;
|
|
1087
|
+
params.push(like, like, like, like, like);
|
|
1088
|
+
}
|
|
1089
|
+
where.push(`(${clauses.join(" OR ")})`);
|
|
1090
|
+
}
|
|
1091
|
+
if ((options.from ?? "") !== "") {
|
|
1092
|
+
const like = `%${String(options.from)}%`;
|
|
1093
|
+
where.push("(e.from_name LIKE ? OR e.from_addr LIKE ?)");
|
|
1094
|
+
params.push(like, like);
|
|
1095
|
+
}
|
|
1096
|
+
if ((options.subject ?? "") !== "") {
|
|
1097
|
+
where.push("e.subject LIKE ?");
|
|
1098
|
+
params.push(`%${String(options.subject)}%`);
|
|
1099
|
+
}
|
|
1100
|
+
if ((options.since ?? "") !== "") {
|
|
1101
|
+
where.push("substr(e.date, 1, 10) >= ?");
|
|
1102
|
+
params.push(String(options.since));
|
|
1103
|
+
}
|
|
1104
|
+
if ((options.before ?? "") !== "") {
|
|
1105
|
+
where.push("substr(e.date, 1, 10) <= ?");
|
|
1106
|
+
params.push(String(options.before));
|
|
1107
|
+
}
|
|
1108
|
+
const limit = Math.min(100, Math.max(1, options.limit ?? 60));
|
|
1109
|
+
if (where.length === 0) return { items: [], total: 0, offset: 0, limit };
|
|
1110
|
+
const clause = `WHERE ${where.join(" AND ")}`;
|
|
1111
|
+
const columns = `SELECT e.id, e.mailbox, e.uid, e.subject, e.from_name, e.from_addr, e.to_addr, e.cc,
|
|
1112
|
+
e.message_id, e.date, length(e.body_text) AS text_length,
|
|
1113
|
+
(SELECT COUNT(*) FROM attachments a WHERE a.email_id = e.id) AS attachment_count,
|
|
1114
|
+
substr(COALESCE(e.body_text, ''), 1, 240) AS snippet`;
|
|
1115
|
+
const total = Number(
|
|
1116
|
+
db.prepare(`SELECT COUNT(*) AS count FROM emails e ${clause}`).get(...params)?.count ?? 0
|
|
1117
|
+
);
|
|
1118
|
+
const rows = db.prepare(`${columns} FROM emails e ${clause} ORDER BY e.date DESC, e.id DESC LIMIT ?`).all(...params, limit);
|
|
1119
|
+
return { items: rows.map((row) => rowToSummary(row)), total, offset: 0, limit };
|
|
1120
|
+
}
|
|
974
1121
|
get(localId) {
|
|
975
1122
|
const db = this.open();
|
|
976
1123
|
const row = db.prepare(
|
|
@@ -1599,6 +1746,17 @@ ${shared}${styleLine}`;
|
|
|
1599
1746
|
\u8981\u6C42\uFF1A
|
|
1600
1747
|
${shared}${styleLine}`;
|
|
1601
1748
|
};
|
|
1749
|
+
var SEARCH_SYSTEM = `\u4F60\u662F\u90AE\u4EF6\u641C\u7D22\u52A9\u624B\u3002\u628A\u7528\u6237\u7684\u4E00\u53E5\u8BDD\u641C\u7D22\u9700\u6C42\u7FFB\u8BD1\u6210\u7ED3\u6784\u5316\u6761\u4EF6\u3002
|
|
1750
|
+
|
|
1751
|
+
\u53EA\u8F93\u51FA\u4E00\u4E2A JSON \u5BF9\u8C61\uFF0C\u4E0D\u8981\u89E3\u91CA\u6216\u4EE3\u7801\u5757\uFF1A
|
|
1752
|
+
{"keywords": ["\u5173\u952E\u8BCD"], "from": "\u53D1\u4EF6\u4EBA\u6216\u7A7A\u5B57\u7B26\u4E32", "subject": "\u4E3B\u9898\u5173\u952E\u8BCD\u6216\u7A7A\u5B57\u7B26\u4E32", "since": "YYYY-MM-DD \u6216\u7A7A\u5B57\u7B26\u4E32", "before": "YYYY-MM-DD \u6216\u7A7A\u5B57\u7B26\u4E32", "explanation": "\u4E00\u53E5\u4E2D\u6587\u8BF4\u660E\u4F60\u7406\u89E3\u6210\u4E86\u4EC0\u4E48"}
|
|
1753
|
+
|
|
1754
|
+
\u89C4\u5219\uFF1A
|
|
1755
|
+
- \u4ECA\u5929\u662F {{today}}\uFF0C\u76F8\u5BF9\u65F6\u95F4\uFF08\u4E0A\u5468\u3001\u4E0A\u4E2A\u6708\u3001\u6700\u8FD1\u4E09\u5929\uFF09\u6362\u7B97\u6210 since/before
|
|
1756
|
+
- keywords \u53EA\u653E\u6B63\u6587/\u4E3B\u9898\u91CC\u53EF\u80FD\u51FA\u73B0\u7684\u5B9E\u8BCD\uFF0C2-4 \u4E2A\u5373\u53EF
|
|
1757
|
+
- subject \u53EA\u5728\u7528\u6237\u660E\u786E\u8BF4\u201C\u4E3B\u9898\u662F/\u6807\u9898\u662F\u201D\u65F6\u586B\u5199\uFF0C\u5426\u5219\u7559\u7A7A\uFF1B\u4E0D\u8981\u628A\u666E\u901A\u5173\u952E\u8BCD\u585E\u8FDB subject
|
|
1758
|
+
- \u4E0D\u786E\u5B9A\u7684\u5B57\u6BB5\u7559\u7A7A\u5B57\u7B26\u4E32\uFF0C\u4E0D\u8981\u7F16\u9020
|
|
1759
|
+
- \u6CA1\u6709\u53EF\u7528\u6761\u4EF6\u65F6 keywords \u4E3A\u7A7A\u6570\u7EC4`;
|
|
1602
1760
|
var TASK_SYSTEM = `\u4F60\u662F\u4EFB\u52A1\u63D0\u53D6\u52A9\u624B\u3002\u4ECE\u90AE\u4EF6\u4E2D\u63D0\u53D6\u9700\u8981\u7528\u6237\u5904\u7406\u7684\u5177\u4F53\u4E8B\u9879\u3002
|
|
1603
1761
|
|
|
1604
1762
|
\u53EA\u8F93\u51FA\u4E00\u4E2A JSON \u6570\u7EC4\uFF0C\u4E0D\u8981\u8F93\u51FA\u89E3\u91CA\u3001Markdown \u6216\u4EE3\u7801\u5757\u6807\u8BB0\u3002\u6BCF\u4E2A\u5143\u7D20\uFF1A
|
|
@@ -1785,6 +1943,13 @@ ${clip(item, 4e3)}`).join("\n\n");
|
|
|
1785
1943
|
].join("\n");
|
|
1786
1944
|
return this.complete(ASK_SYSTEM, user, signal);
|
|
1787
1945
|
}
|
|
1946
|
+
/** Translate a natural-language search request into structured filters. */
|
|
1947
|
+
async parseSearch(query, signal) {
|
|
1948
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
1949
|
+
const system = SEARCH_SYSTEM.replace("{{today}}", today);
|
|
1950
|
+
const text2 = await this.complete(system, `\u641C\u7D22\u9700\u6C42\uFF1A${query.trim()}`, signal);
|
|
1951
|
+
return parseSearchJson(text2);
|
|
1952
|
+
}
|
|
1788
1953
|
/** Extract actionable tasks from one mail. */
|
|
1789
1954
|
async extractTasks(message, signal) {
|
|
1790
1955
|
const text2 = await this.complete(TASK_SYSTEM, contextOf(message), signal);
|
|
@@ -1848,6 +2013,37 @@ function normalizeTranslated(value) {
|
|
|
1848
2013
|
function htmlToPlain(html) {
|
|
1849
2014
|
return html.replace(/<style[\s\S]*?<\/style>/gi, "").replace(/<script[\s\S]*?<\/script>/gi, "").replace(/<br\s*\/?>/gi, "\n").replace(/<\/(p|div|li|tr|h[1-6])>/gi, "\n").replace(/<[^>]+>/g, "").replace(/ /g, " ").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/\n{3,}/g, "\n\n").trim();
|
|
1850
2015
|
}
|
|
2016
|
+
function parseSearchJson(text2) {
|
|
2017
|
+
const start = text2.indexOf("{");
|
|
2018
|
+
const end = text2.lastIndexOf("}");
|
|
2019
|
+
const empty = {
|
|
2020
|
+
keywords: [],
|
|
2021
|
+
from: "",
|
|
2022
|
+
subject: "",
|
|
2023
|
+
since: "",
|
|
2024
|
+
before: "",
|
|
2025
|
+
explanation: ""
|
|
2026
|
+
};
|
|
2027
|
+
if (start < 0 || end <= start) return empty;
|
|
2028
|
+
let parsed;
|
|
2029
|
+
try {
|
|
2030
|
+
parsed = JSON.parse(text2.slice(start, end + 1));
|
|
2031
|
+
} catch {
|
|
2032
|
+
return empty;
|
|
2033
|
+
}
|
|
2034
|
+
if (parsed === null || typeof parsed !== "object") return empty;
|
|
2035
|
+
const value = parsed;
|
|
2036
|
+
const keywords = Array.isArray(value.keywords) ? value.keywords.map((item) => String(item).trim()).filter((item) => item !== "").slice(0, 4) : [];
|
|
2037
|
+
const date = (input) => /^\d{4}-\d{2}-\d{2}$/.test(String(input ?? "")) ? String(input) : "";
|
|
2038
|
+
return {
|
|
2039
|
+
keywords,
|
|
2040
|
+
from: String(value.from ?? "").trim().slice(0, 120),
|
|
2041
|
+
subject: String(value.subject ?? "").trim().slice(0, 120),
|
|
2042
|
+
since: date(value.since),
|
|
2043
|
+
before: date(value.before),
|
|
2044
|
+
explanation: String(value.explanation ?? "").trim().slice(0, 200)
|
|
2045
|
+
};
|
|
2046
|
+
}
|
|
1851
2047
|
function parseTaskJson(text2) {
|
|
1852
2048
|
const start = text2.indexOf("[");
|
|
1853
2049
|
const end = text2.lastIndexOf("]");
|
|
@@ -2113,7 +2309,7 @@ ${conversation.trim()}
|
|
|
2113
2309
|
}
|
|
2114
2310
|
|
|
2115
2311
|
// src/host/version.ts
|
|
2116
|
-
var VERSION = "0.
|
|
2312
|
+
var VERSION = "0.7.1";
|
|
2117
2313
|
|
|
2118
2314
|
// src/host/api.ts
|
|
2119
2315
|
init_smtp();
|
|
@@ -2516,6 +2712,53 @@ function buildRouter(runtime) {
|
|
|
2516
2712
|
throw aiHttpError(error);
|
|
2517
2713
|
}
|
|
2518
2714
|
});
|
|
2715
|
+
router.post("/ai/search", async ({ body }) => {
|
|
2716
|
+
const payload = await body();
|
|
2717
|
+
const account = accountFromPayload(runtime, payload);
|
|
2718
|
+
const query = String(payload.query ?? "").trim();
|
|
2719
|
+
if (query === "") throw new HttpError(400, "\u641C\u7D22\u5185\u5BB9\u4E0D\u80FD\u4E3A\u7A7A");
|
|
2720
|
+
const source = sourceOfPayload(payload);
|
|
2721
|
+
const mailbox = String(payload.mailbox ?? "all");
|
|
2722
|
+
try {
|
|
2723
|
+
const filters = await runtime.ai().parseSearch(query);
|
|
2724
|
+
const hasCondition = filters.keywords.length > 0 || filters.from !== "" || filters.subject !== "" || filters.since !== "" || filters.before !== "";
|
|
2725
|
+
if (!hasCondition) throw new HttpError(400, "\u6CA1\u6709\u7406\u89E3\u51FA\u53EF\u7528\u7684\u641C\u7D22\u6761\u4EF6\uFF0C\u8BF7\u6362\u4E2A\u8BF4\u6CD5");
|
|
2726
|
+
if (source === "server") {
|
|
2727
|
+
const provider = runtime.provider(account);
|
|
2728
|
+
const result = await provider.search(mailbox === "all" ? "INBOX" : mailbox, {
|
|
2729
|
+
...filters,
|
|
2730
|
+
limit: 60
|
|
2731
|
+
});
|
|
2732
|
+
return {
|
|
2733
|
+
items: result.items.map((item) => ({ ...item, accountId: account.id })),
|
|
2734
|
+
total: result.total,
|
|
2735
|
+
offset: 0,
|
|
2736
|
+
limit: 60,
|
|
2737
|
+
ai: filters
|
|
2738
|
+
};
|
|
2739
|
+
}
|
|
2740
|
+
const database = runtime.database(account);
|
|
2741
|
+
const keywords = [...filters.keywords];
|
|
2742
|
+
let page = database.searchAdvanced({ ...filters, subject: "", keywords, mailbox: "all", limit: 60 });
|
|
2743
|
+
if (page.total === 0 && filters.subject !== "") {
|
|
2744
|
+
page = database.searchAdvanced({
|
|
2745
|
+
...filters,
|
|
2746
|
+
subject: "",
|
|
2747
|
+
keywords: [...keywords, filters.subject],
|
|
2748
|
+
mailbox: "all",
|
|
2749
|
+
limit: 60
|
|
2750
|
+
});
|
|
2751
|
+
}
|
|
2752
|
+
return {
|
|
2753
|
+
...page,
|
|
2754
|
+
items: page.items.map((item) => ({ ...item, accountId: account.id })),
|
|
2755
|
+
ai: filters
|
|
2756
|
+
};
|
|
2757
|
+
} catch (error) {
|
|
2758
|
+
if (error instanceof HttpError) throw error;
|
|
2759
|
+
throw aiHttpError(error);
|
|
2760
|
+
}
|
|
2761
|
+
});
|
|
2519
2762
|
router.post("/ai/compose", async ({ body }) => {
|
|
2520
2763
|
const payload = await body();
|
|
2521
2764
|
const account = accountFromPayload(runtime, payload);
|
|
@@ -2681,6 +2924,10 @@ function buildRouter(runtime) {
|
|
|
2681
2924
|
})()
|
|
2682
2925
|
};
|
|
2683
2926
|
});
|
|
2927
|
+
router.post("/projects/pick-directory", async () => {
|
|
2928
|
+
const path = await pickDirectory();
|
|
2929
|
+
return { path };
|
|
2930
|
+
});
|
|
2684
2931
|
router.post("/projects", async ({ body }) => {
|
|
2685
2932
|
const payload = await body();
|
|
2686
2933
|
const account = accountFromPayload(runtime, payload);
|