@zaclinley/dsh-mailharbor 0.5.1 → 0.7.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/CHANGELOG.md +32 -0
- package/README.md +22 -19
- package/README.zh.md +22 -19
- package/lib/client.js +605 -528
- package/lib/client.js.map +3 -3
- package/lib/index.js +315 -472
- package/lib/index.js.map +4 -4
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -214,7 +214,7 @@ var init_imap = __esm({
|
|
|
214
214
|
"src/host/imap.ts"() {
|
|
215
215
|
"use strict";
|
|
216
216
|
init_address();
|
|
217
|
-
ImapProvider = class {
|
|
217
|
+
ImapProvider = class _ImapProvider {
|
|
218
218
|
settings;
|
|
219
219
|
credentials;
|
|
220
220
|
options;
|
|
@@ -222,6 +222,11 @@ var init_imap = __esm({
|
|
|
222
222
|
connecting = null;
|
|
223
223
|
queue = Promise.resolve();
|
|
224
224
|
disposed = false;
|
|
225
|
+
mailboxCache = null;
|
|
226
|
+
countCache = /* @__PURE__ */ new Map();
|
|
227
|
+
pageCache = /* @__PURE__ */ new Map();
|
|
228
|
+
keepalive = null;
|
|
229
|
+
static MAILBOX_TTL_MS = 6e4;
|
|
225
230
|
constructor(settings, credentials, options = {}) {
|
|
226
231
|
this.settings = settings;
|
|
227
232
|
this.credentials = credentials;
|
|
@@ -245,7 +250,10 @@ var init_imap = __esm({
|
|
|
245
250
|
client.on("error", () => {
|
|
246
251
|
});
|
|
247
252
|
client.on("close", () => {
|
|
248
|
-
if (this.client === client)
|
|
253
|
+
if (this.client === client) {
|
|
254
|
+
this.client = null;
|
|
255
|
+
this.stopKeepalive();
|
|
256
|
+
}
|
|
249
257
|
});
|
|
250
258
|
try {
|
|
251
259
|
await client.connect();
|
|
@@ -255,6 +263,7 @@ var init_imap = __esm({
|
|
|
255
263
|
);
|
|
256
264
|
}
|
|
257
265
|
this.client = client;
|
|
266
|
+
this.startKeepalive(client);
|
|
258
267
|
return client;
|
|
259
268
|
})();
|
|
260
269
|
try {
|
|
@@ -263,6 +272,27 @@ var init_imap = __esm({
|
|
|
263
272
|
this.connecting = null;
|
|
264
273
|
}
|
|
265
274
|
}
|
|
275
|
+
startKeepalive(client) {
|
|
276
|
+
this.stopKeepalive();
|
|
277
|
+
this.keepalive = setInterval(() => {
|
|
278
|
+
if (this.disposed || this.client !== client || !client.usable) return;
|
|
279
|
+
void this.run(async (active) => {
|
|
280
|
+
await active.noop();
|
|
281
|
+
}).catch(() => {
|
|
282
|
+
});
|
|
283
|
+
}, 24e4);
|
|
284
|
+
this.keepalive.unref?.();
|
|
285
|
+
}
|
|
286
|
+
stopKeepalive() {
|
|
287
|
+
if (this.keepalive !== null) {
|
|
288
|
+
clearInterval(this.keepalive);
|
|
289
|
+
this.keepalive = null;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
/** Connect eagerly so the first list does not pay the handshake. */
|
|
293
|
+
async warm() {
|
|
294
|
+
await this.connect();
|
|
295
|
+
}
|
|
266
296
|
async reset(reason) {
|
|
267
297
|
const client = this.client;
|
|
268
298
|
this.client = null;
|
|
@@ -303,34 +333,38 @@ var init_imap = __esm({
|
|
|
303
333
|
return { ok: true, mailboxes: boxes.map((box) => ({ name: box.path, count: box.messages })) };
|
|
304
334
|
});
|
|
305
335
|
}
|
|
306
|
-
async mailboxes() {
|
|
307
|
-
|
|
336
|
+
async mailboxes(force = false) {
|
|
337
|
+
const cached = this.mailboxCache;
|
|
338
|
+
if (!force && cached !== null && Date.now() - cached.at < _ImapProvider.MAILBOX_TTL_MS) {
|
|
339
|
+
return cached.items;
|
|
340
|
+
}
|
|
341
|
+
const items = await this.run((client) => this.listMailboxes(client));
|
|
342
|
+
this.mailboxCache = { at: Date.now(), items };
|
|
343
|
+
return items;
|
|
308
344
|
}
|
|
309
345
|
async listMailboxes(client) {
|
|
310
346
|
const listed = await client.list();
|
|
311
|
-
const
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
} catch {
|
|
319
|
-
}
|
|
320
|
-
result.push({
|
|
321
|
-
path: box.path,
|
|
322
|
-
name: box.name ?? box.path,
|
|
323
|
-
specialUse: box.specialUse ?? null,
|
|
324
|
-
messages
|
|
325
|
-
});
|
|
326
|
-
}
|
|
327
|
-
return result;
|
|
347
|
+
const boxes = listed.filter((box) => box.flags?.has?.("\\Noselect") !== true);
|
|
348
|
+
return boxes.map((box) => ({
|
|
349
|
+
path: box.path,
|
|
350
|
+
name: box.name ?? box.path,
|
|
351
|
+
specialUse: box.specialUse ?? null,
|
|
352
|
+
messages: this.countCache.get(box.path) ?? 0
|
|
353
|
+
}));
|
|
328
354
|
}
|
|
329
355
|
async list(mailbox, limit, offset = 0) {
|
|
356
|
+
const cacheKey = `${mailbox}:${String(limit)}:${String(offset)}`;
|
|
357
|
+
const cached = this.pageCache.get(cacheKey);
|
|
358
|
+
if (cached !== void 0 && Date.now() - cached.at < 15e3) {
|
|
359
|
+
return { total: cached.total, items: cached.items };
|
|
360
|
+
}
|
|
330
361
|
return this.run(async (client) => {
|
|
331
362
|
const lock = await client.getMailboxLock(mailbox);
|
|
332
363
|
try {
|
|
333
364
|
const total = Number(client.mailbox?.exists ?? 0);
|
|
365
|
+
const cachedBox = this.mailboxCache?.items.find((box) => box.path === mailbox);
|
|
366
|
+
if (cachedBox !== void 0) cachedBox.messages = total;
|
|
367
|
+
this.countCache.set(mailbox, total);
|
|
334
368
|
const end = total - offset;
|
|
335
369
|
if (total === 0 || end < 1) return { total, items: [] };
|
|
336
370
|
const start = Math.max(1, end - limit + 1);
|
|
@@ -345,12 +379,52 @@ var init_imap = __esm({
|
|
|
345
379
|
items.push(toMessage(message));
|
|
346
380
|
}
|
|
347
381
|
items.sort((left, right) => right.date.localeCompare(left.date));
|
|
382
|
+
this.pageCache.set(cacheKey, { at: Date.now(), total, items });
|
|
383
|
+
if (this.pageCache.size > 30) {
|
|
384
|
+
const oldest = this.pageCache.keys().next().value;
|
|
385
|
+
if (oldest !== void 0) this.pageCache.delete(oldest);
|
|
386
|
+
}
|
|
348
387
|
return { total, items };
|
|
349
388
|
} finally {
|
|
350
389
|
lock.release();
|
|
351
390
|
}
|
|
352
391
|
});
|
|
353
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
|
+
}
|
|
354
428
|
async read(mailbox, uid) {
|
|
355
429
|
return this.run(async (client) => {
|
|
356
430
|
const lock = await client.getMailboxLock(mailbox);
|
|
@@ -423,6 +497,7 @@ var init_imap = __esm({
|
|
|
423
497
|
});
|
|
424
498
|
}
|
|
425
499
|
async setSeen(mailbox, uid, seen) {
|
|
500
|
+
this.pageCache.clear();
|
|
426
501
|
return this.run(async (client) => {
|
|
427
502
|
const lock = await client.getMailboxLock(mailbox);
|
|
428
503
|
try {
|
|
@@ -461,6 +536,8 @@ var init_imap = __esm({
|
|
|
461
536
|
}
|
|
462
537
|
async dispose() {
|
|
463
538
|
this.disposed = true;
|
|
539
|
+
this.pageCache.clear();
|
|
540
|
+
this.stopKeepalive();
|
|
464
541
|
await this.reset("dispose");
|
|
465
542
|
}
|
|
466
543
|
};
|
|
@@ -742,6 +819,66 @@ function activeAccount(config) {
|
|
|
742
819
|
return config.accounts.find((account) => account.id === config.activeAccountId) ?? null;
|
|
743
820
|
}
|
|
744
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
|
+
|
|
745
882
|
// src/host/backup.ts
|
|
746
883
|
init_address();
|
|
747
884
|
import { createHash as createHash2 } from "node:crypto";
|
|
@@ -929,6 +1066,58 @@ var MailDatabase = class {
|
|
|
929
1066
|
).all(...params, limit, offset);
|
|
930
1067
|
return { items: rows.map((row) => rowToSummary(row)), total, offset, limit };
|
|
931
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
|
+
}
|
|
932
1121
|
get(localId) {
|
|
933
1122
|
const db = this.open();
|
|
934
1123
|
const row = db.prepare(
|
|
@@ -1081,8 +1270,8 @@ function normalizeId(value) {
|
|
|
1081
1270
|
function stamp(value) {
|
|
1082
1271
|
const date = value instanceof Date ? value : value === void 0 || value === null ? void 0 : new Date(String(value));
|
|
1083
1272
|
if (!(date instanceof Date) || Number.isNaN(date.getTime())) return "";
|
|
1084
|
-
const
|
|
1085
|
-
return `${String(date.getFullYear())}-${
|
|
1273
|
+
const pad = (number) => String(number).padStart(2, "0");
|
|
1274
|
+
return `${String(date.getFullYear())}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
|
|
1086
1275
|
}
|
|
1087
1276
|
function abortIfNeeded(signal) {
|
|
1088
1277
|
if (signal?.aborted === true) {
|
|
@@ -1557,6 +1746,17 @@ ${shared}${styleLine}`;
|
|
|
1557
1746
|
\u8981\u6C42\uFF1A
|
|
1558
1747
|
${shared}${styleLine}`;
|
|
1559
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`;
|
|
1560
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
|
|
1561
1761
|
|
|
1562
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
|
|
@@ -1743,6 +1943,13 @@ ${clip(item, 4e3)}`).join("\n\n");
|
|
|
1743
1943
|
].join("\n");
|
|
1744
1944
|
return this.complete(ASK_SYSTEM, user, signal);
|
|
1745
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
|
+
}
|
|
1746
1953
|
/** Extract actionable tasks from one mail. */
|
|
1747
1954
|
async extractTasks(message, signal) {
|
|
1748
1955
|
const text2 = await this.complete(TASK_SYSTEM, contextOf(message), signal);
|
|
@@ -1806,6 +2013,37 @@ function normalizeTranslated(value) {
|
|
|
1806
2013
|
function htmlToPlain(html) {
|
|
1807
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();
|
|
1808
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
|
+
}
|
|
1809
2047
|
function parseTaskJson(text2) {
|
|
1810
2048
|
const start = text2.indexOf("[");
|
|
1811
2049
|
const end = text2.lastIndexOf("]");
|
|
@@ -1859,365 +2097,6 @@ function textToHtml(text2) {
|
|
|
1859
2097
|
).join("");
|
|
1860
2098
|
}
|
|
1861
2099
|
|
|
1862
|
-
// src/host/ics.ts
|
|
1863
|
-
var CRLF = "\r\n";
|
|
1864
|
-
function escapeText(value) {
|
|
1865
|
-
return String(value ?? "").replaceAll("\\", "\\\\").replaceAll(";", "\\;").replaceAll(",", "\\,").replaceAll("\r\n", "\\n").replaceAll("\n", "\\n").replaceAll("\r", "\\n");
|
|
1866
|
-
}
|
|
1867
|
-
function unescapeText(value) {
|
|
1868
|
-
return String(value ?? "").replaceAll("\\n", "\n").replaceAll("\\N", "\n").replaceAll("\\,", ",").replaceAll("\\;", ";").replaceAll("\\\\", "\\");
|
|
1869
|
-
}
|
|
1870
|
-
function fold(line2) {
|
|
1871
|
-
const text2 = String(line2);
|
|
1872
|
-
if (text2.length <= 74) return text2;
|
|
1873
|
-
const chunks = [];
|
|
1874
|
-
let rest = text2;
|
|
1875
|
-
while (rest.length > 74) {
|
|
1876
|
-
chunks.push(rest.slice(0, 74));
|
|
1877
|
-
rest = ` ${rest.slice(74)}`;
|
|
1878
|
-
}
|
|
1879
|
-
chunks.push(rest);
|
|
1880
|
-
return chunks.join(CRLF);
|
|
1881
|
-
}
|
|
1882
|
-
function utcStamp(date) {
|
|
1883
|
-
const pad2 = (value) => String(value).padStart(2, "0");
|
|
1884
|
-
return `${String(date.getUTCFullYear())}${pad2(date.getUTCMonth() + 1)}${pad2(date.getUTCDate())}T${pad2(date.getUTCHours())}${pad2(date.getUTCMinutes())}${pad2(date.getUTCSeconds())}Z`;
|
|
1885
|
-
}
|
|
1886
|
-
function pad(value) {
|
|
1887
|
-
return String(value).padStart(2, "0");
|
|
1888
|
-
}
|
|
1889
|
-
function dueProperty(due) {
|
|
1890
|
-
const text2 = String(due ?? "").trim();
|
|
1891
|
-
if (text2 === "") return null;
|
|
1892
|
-
if (/^\d{4}-\d{2}-\d{2}$/.test(text2)) return `DUE;VALUE=DATE:${text2.replaceAll("-", "")}`;
|
|
1893
|
-
const parsed = new Date(text2);
|
|
1894
|
-
if (Number.isNaN(parsed.getTime())) return null;
|
|
1895
|
-
return `DUE:${utcStamp(parsed)}`;
|
|
1896
|
-
}
|
|
1897
|
-
function serializeTodos(tasks) {
|
|
1898
|
-
const lines = [
|
|
1899
|
-
"BEGIN:VCALENDAR",
|
|
1900
|
-
"VERSION:2.0",
|
|
1901
|
-
"PRODID:-//dsh-mailharbor//VTODO//EN",
|
|
1902
|
-
"CALSCALE:GREGORIAN"
|
|
1903
|
-
];
|
|
1904
|
-
for (const task of tasks) {
|
|
1905
|
-
lines.push("BEGIN:VTODO");
|
|
1906
|
-
lines.push(`UID:${String(task.uid ?? task.id ?? "")}`);
|
|
1907
|
-
lines.push(`DTSTAMP:${utcStamp(/* @__PURE__ */ new Date())}`);
|
|
1908
|
-
lines.push(`SUMMARY:${escapeText(task.title ?? "(\u65E0\u6807\u9898)")}`);
|
|
1909
|
-
if (task.notes !== void 0 && task.notes !== null && String(task.notes) !== "") {
|
|
1910
|
-
lines.push(`DESCRIPTION:${escapeText(task.notes)}`);
|
|
1911
|
-
}
|
|
1912
|
-
const due = dueProperty(String(task.due ?? ""));
|
|
1913
|
-
if (due !== null) lines.push(due);
|
|
1914
|
-
lines.push(`STATUS:${String(task.status ?? "NEEDS-ACTION").toUpperCase()}`);
|
|
1915
|
-
if (Number(task.priority ?? 0) > 0)
|
|
1916
|
-
lines.push(`PRIORITY:${String(Math.max(0, Math.min(9, Number(task.priority))))}`);
|
|
1917
|
-
if (String(task.status ?? "").toUpperCase() === "COMPLETED") {
|
|
1918
|
-
const completed = task.completedAt === void 0 || task.completedAt === null ? /* @__PURE__ */ new Date() : new Date(task.completedAt);
|
|
1919
|
-
if (!Number.isNaN(completed.getTime())) lines.push(`COMPLETED:${utcStamp(completed)}`);
|
|
1920
|
-
}
|
|
1921
|
-
lines.push("END:VTODO");
|
|
1922
|
-
}
|
|
1923
|
-
lines.push("END:VCALENDAR");
|
|
1924
|
-
return `${lines.map(fold).join(CRLF)}${CRLF}`;
|
|
1925
|
-
}
|
|
1926
|
-
function unfold(text2) {
|
|
1927
|
-
return String(text2 ?? "").replace(/\r?\n[ \t]/g, "");
|
|
1928
|
-
}
|
|
1929
|
-
function decodeDue(value, params) {
|
|
1930
|
-
const text2 = String(value ?? "").trim();
|
|
1931
|
-
if (params.includes("VALUE=DATE") || /^\d{8}$/.test(text2)) {
|
|
1932
|
-
if (/^\d{8}$/.test(text2)) return `${text2.slice(0, 4)}-${text2.slice(4, 6)}-${text2.slice(6, 8)}`;
|
|
1933
|
-
return "";
|
|
1934
|
-
}
|
|
1935
|
-
const match = /^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})Z$/.exec(text2);
|
|
1936
|
-
if (match !== null) {
|
|
1937
|
-
const date = new Date(
|
|
1938
|
-
Date.UTC(
|
|
1939
|
-
Number(match[1]),
|
|
1940
|
-
Number(match[2]) - 1,
|
|
1941
|
-
Number(match[3]),
|
|
1942
|
-
Number(match[4]),
|
|
1943
|
-
Number(match[5]),
|
|
1944
|
-
Number(match[6])
|
|
1945
|
-
)
|
|
1946
|
-
);
|
|
1947
|
-
return `${String(date.getFullYear())}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
|
1948
|
-
}
|
|
1949
|
-
return "";
|
|
1950
|
-
}
|
|
1951
|
-
function parseTodos(ics) {
|
|
1952
|
-
const lines = unfold(ics).split(/\r?\n/);
|
|
1953
|
-
const todos = [];
|
|
1954
|
-
let current = null;
|
|
1955
|
-
for (const line2 of lines) {
|
|
1956
|
-
const trimmed = line2.trim();
|
|
1957
|
-
if (trimmed === "BEGIN:VTODO") {
|
|
1958
|
-
current = {};
|
|
1959
|
-
continue;
|
|
1960
|
-
}
|
|
1961
|
-
if (trimmed === "END:VTODO") {
|
|
1962
|
-
if (current !== null && String(current.uid ?? "") !== "") {
|
|
1963
|
-
todos.push({
|
|
1964
|
-
uid: String(current.uid),
|
|
1965
|
-
title: String(current.title ?? "(\u65E0\u6807\u9898)"),
|
|
1966
|
-
notes: String(current.notes ?? ""),
|
|
1967
|
-
due: String(current.due ?? ""),
|
|
1968
|
-
status: String(current.status ?? "NEEDS-ACTION"),
|
|
1969
|
-
priority: Number(current.priority ?? 0),
|
|
1970
|
-
completedAt: current.completedAt ?? null
|
|
1971
|
-
});
|
|
1972
|
-
}
|
|
1973
|
-
current = null;
|
|
1974
|
-
continue;
|
|
1975
|
-
}
|
|
1976
|
-
if (current === null) continue;
|
|
1977
|
-
const colon = trimmed.indexOf(":");
|
|
1978
|
-
if (colon < 0) continue;
|
|
1979
|
-
const left = trimmed.slice(0, colon);
|
|
1980
|
-
const value = trimmed.slice(colon + 1);
|
|
1981
|
-
const semicolon = left.indexOf(";");
|
|
1982
|
-
const name2 = (semicolon < 0 ? left : left.slice(0, semicolon)).toUpperCase();
|
|
1983
|
-
const params = semicolon < 0 ? "" : left.slice(semicolon + 1).toUpperCase();
|
|
1984
|
-
if (name2 === "UID") current.uid = unescapeText(value).trim();
|
|
1985
|
-
else if (name2 === "SUMMARY") current.title = unescapeText(value);
|
|
1986
|
-
else if (name2 === "DESCRIPTION") current.notes = unescapeText(value);
|
|
1987
|
-
else if (name2 === "STATUS") current.status = value.trim().toUpperCase();
|
|
1988
|
-
else if (name2 === "PRIORITY") current.priority = Number(value) || 0;
|
|
1989
|
-
else if (name2 === "DUE") current.due = decodeDue(value, params);
|
|
1990
|
-
else if (name2 === "COMPLETED") {
|
|
1991
|
-
const match = /^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})Z$/.exec(value.trim());
|
|
1992
|
-
if (match !== null) {
|
|
1993
|
-
current.completedAt = new Date(
|
|
1994
|
-
Date.UTC(
|
|
1995
|
-
Number(match[1]),
|
|
1996
|
-
Number(match[2]) - 1,
|
|
1997
|
-
Number(match[3]),
|
|
1998
|
-
Number(match[4]),
|
|
1999
|
-
Number(match[5]),
|
|
2000
|
-
Number(match[6])
|
|
2001
|
-
)
|
|
2002
|
-
).toISOString();
|
|
2003
|
-
}
|
|
2004
|
-
}
|
|
2005
|
-
}
|
|
2006
|
-
return todos;
|
|
2007
|
-
}
|
|
2008
|
-
|
|
2009
|
-
// src/host/caldav.ts
|
|
2010
|
-
function decodeXml(value) {
|
|
2011
|
-
return String(value ?? "").replaceAll("<", "<").replaceAll(">", ">").replaceAll(""", '"').replaceAll("'", "'").replaceAll("&", "&");
|
|
2012
|
-
}
|
|
2013
|
-
function tagValue(xml, name2) {
|
|
2014
|
-
const pattern = new RegExp(`<(?:[\\w-]+:)?${name2}\\b[^>]*>([\\s\\S]*?)</(?:[\\w-]+:)?${name2}>`, "i");
|
|
2015
|
-
const match = pattern.exec(String(xml ?? ""));
|
|
2016
|
-
return match === null ? "" : decodeXml(match[1] ?? "");
|
|
2017
|
-
}
|
|
2018
|
-
function responseBlocks(xml) {
|
|
2019
|
-
return [
|
|
2020
|
-
...String(xml ?? "").matchAll(/<(?:[\w-]+:)?response\b[^>]*>([\s\S]*?)<\/(?:[\w-]+:)?response>/gi)
|
|
2021
|
-
].map((match) => match[1] ?? "");
|
|
2022
|
-
}
|
|
2023
|
-
function resolveHref(base, href) {
|
|
2024
|
-
const text2 = String(href ?? "").trim();
|
|
2025
|
-
if (text2 === "") return "";
|
|
2026
|
-
return new URL(text2, base).href;
|
|
2027
|
-
}
|
|
2028
|
-
function withSlash(url) {
|
|
2029
|
-
return url.endsWith("/") ? url : `${url}/`;
|
|
2030
|
-
}
|
|
2031
|
-
var PROPFIND_ROOT = `<?xml version="1.0" encoding="utf-8"?>
|
|
2032
|
-
<d:propfind xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav">
|
|
2033
|
-
<d:prop><d:current-user-principal/><c:calendar-home-set/></d:prop>
|
|
2034
|
-
</d:propfind>`;
|
|
2035
|
-
var PROPFIND_HOME = `<?xml version="1.0" encoding="utf-8"?>
|
|
2036
|
-
<d:propfind xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav">
|
|
2037
|
-
<d:prop><d:resourcetype/><d:displayname/><c:supported-calendar-component-set/></d:prop>
|
|
2038
|
-
</d:propfind>`;
|
|
2039
|
-
var REPORT_TODOS = `<?xml version="1.0" encoding="utf-8"?>
|
|
2040
|
-
<c:calendar-query xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav">
|
|
2041
|
-
<d:prop><d:getetag/><c:calendar-data/></d:prop>
|
|
2042
|
-
<c:filter><c:comp-filter name="VCALENDAR"><c:comp-filter name="VTODO"/></c:comp-filter></c:filter>
|
|
2043
|
-
</c:calendar-query>`;
|
|
2044
|
-
var CalDavClient = class {
|
|
2045
|
-
url;
|
|
2046
|
-
username;
|
|
2047
|
-
password;
|
|
2048
|
-
calendarHref;
|
|
2049
|
-
constructor(config, password) {
|
|
2050
|
-
this.url = withSlash(String(config.url ?? "").trim());
|
|
2051
|
-
this.username = String(config.username ?? "");
|
|
2052
|
-
this.password = String(password ?? "");
|
|
2053
|
-
this.calendarHref = String(config.calendarHref ?? "").trim();
|
|
2054
|
-
}
|
|
2055
|
-
headers(extra = {}) {
|
|
2056
|
-
const auth = Buffer.from(`${this.username}:${this.password}`, "utf8").toString("base64");
|
|
2057
|
-
return { Authorization: `Basic ${auth}`, ...extra };
|
|
2058
|
-
}
|
|
2059
|
-
async request(method, url, options = {}) {
|
|
2060
|
-
if (this.url === "") throw new Error("CalDAV \u5730\u5740\u672A\u914D\u7F6E");
|
|
2061
|
-
const controller = new AbortController();
|
|
2062
|
-
const timer = setTimeout(() => {
|
|
2063
|
-
controller.abort();
|
|
2064
|
-
}, 2e4);
|
|
2065
|
-
let response;
|
|
2066
|
-
try {
|
|
2067
|
-
response = await fetch(url, {
|
|
2068
|
-
method,
|
|
2069
|
-
headers: this.headers({
|
|
2070
|
-
...options.depth === void 0 ? {} : { Depth: String(options.depth) },
|
|
2071
|
-
...options.body === void 0 ? {} : { "Content-Type": "application/xml; charset=utf-8" },
|
|
2072
|
-
...options.headers ?? {}
|
|
2073
|
-
}),
|
|
2074
|
-
body: options.body,
|
|
2075
|
-
signal: controller.signal,
|
|
2076
|
-
redirect: "follow"
|
|
2077
|
-
});
|
|
2078
|
-
} catch (error) {
|
|
2079
|
-
if (error instanceof Error && error.name === "AbortError") throw new Error("CalDAV \u8FDE\u63A5\u8D85\u65F6");
|
|
2080
|
-
throw new Error(`CalDAV \u8FDE\u63A5\u5931\u8D25\uFF1A${error instanceof Error ? error.message : String(error)}`);
|
|
2081
|
-
} finally {
|
|
2082
|
-
clearTimeout(timer);
|
|
2083
|
-
}
|
|
2084
|
-
const text2 = await response.text();
|
|
2085
|
-
if (response.status === 401 || response.status === 403)
|
|
2086
|
-
throw new Error("CalDAV \u8BA4\u8BC1\u5931\u8D25\uFF0C\u8BF7\u68C0\u67E5\u7528\u6237\u540D\u548C\u5E94\u7528\u5BC6\u7801");
|
|
2087
|
-
if (response.status === 404) throw new Error("CalDAV \u5730\u5740\u6216\u65E5\u5386\u4E0D\u5B58\u5728");
|
|
2088
|
-
if (response.status === 412) throw new Error("CalDAV \u4EFB\u52A1\u5DF2\u88AB\u5176\u4ED6\u5BA2\u6237\u7AEF\u4FEE\u6539\uFF0C\u8BF7\u5148\u540C\u6B65");
|
|
2089
|
-
if (!response.ok && response.status !== 207) {
|
|
2090
|
-
throw new Error(`CalDAV \u8FD4\u56DE ${String(response.status)}${text2 === "" ? "" : `\uFF1A${text2.slice(0, 160)}`}`);
|
|
2091
|
-
}
|
|
2092
|
-
return { status: response.status, text: text2, headers: response.headers };
|
|
2093
|
-
}
|
|
2094
|
-
async discover() {
|
|
2095
|
-
if (this.calendarHref !== "") return this.calendarHref;
|
|
2096
|
-
const root = await this.request("PROPFIND", this.url, { body: PROPFIND_ROOT, depth: 0 });
|
|
2097
|
-
const principalBlock = tagValue(root.text, "current-user-principal");
|
|
2098
|
-
const principal = tagValue(principalBlock, "href") || (principalBlock.trim().startsWith("/") ? principalBlock.trim() : "");
|
|
2099
|
-
const homeBlock = tagValue(root.text, "calendar-home-set");
|
|
2100
|
-
let home = tagValue(homeBlock, "href") || (homeBlock.trim().startsWith("/") ? homeBlock.trim() : "");
|
|
2101
|
-
if (home === "") {
|
|
2102
|
-
if (principal === "") throw new Error("CalDAV \u670D\u52A1\u6CA1\u6709\u8FD4\u56DE calendar-home-set");
|
|
2103
|
-
const principalUrl = resolveHref(this.url, principal);
|
|
2104
|
-
const principalResponse = await this.request("PROPFIND", principalUrl, {
|
|
2105
|
-
body: PROPFIND_ROOT,
|
|
2106
|
-
depth: 0
|
|
2107
|
-
});
|
|
2108
|
-
const principalHome = tagValue(principalResponse.text, "calendar-home-set");
|
|
2109
|
-
home = tagValue(principalHome, "href") || (principalHome.trim().startsWith("/") ? principalHome.trim() : "");
|
|
2110
|
-
if (home === "") throw new Error("CalDAV \u670D\u52A1\u6CA1\u6709\u8FD4\u56DE\u65E5\u5386\u76EE\u5F55");
|
|
2111
|
-
}
|
|
2112
|
-
const homeUrl = withSlash(resolveHref(this.url, home));
|
|
2113
|
-
const listing = await this.request("PROPFIND", homeUrl, { body: PROPFIND_HOME, depth: 1 });
|
|
2114
|
-
const calendars = [];
|
|
2115
|
-
for (const block of responseBlocks(listing.text)) {
|
|
2116
|
-
const href = tagValue(block, "href");
|
|
2117
|
-
if (href === "") continue;
|
|
2118
|
-
if (!/calendar/i.test(tagValue(block, "resourcetype"))) continue;
|
|
2119
|
-
const supported = tagValue(block, "supported-calendar-component-set");
|
|
2120
|
-
if (supported !== "" && !/VTODO/i.test(supported)) continue;
|
|
2121
|
-
calendars.push({
|
|
2122
|
-
href: withSlash(resolveHref(homeUrl, href)),
|
|
2123
|
-
displayName: tagValue(block, "displayname") || href
|
|
2124
|
-
});
|
|
2125
|
-
}
|
|
2126
|
-
if (calendars.length === 0) throw new Error("\u6CA1\u6709\u627E\u5230\u652F\u6301 VTODO \u7684\u4EFB\u52A1\u65E5\u5386");
|
|
2127
|
-
this.calendarHref = calendars[0]?.href ?? "";
|
|
2128
|
-
return this.calendarHref;
|
|
2129
|
-
}
|
|
2130
|
-
async listCalendars() {
|
|
2131
|
-
if (this.calendarHref !== "") return [{ href: this.calendarHref, displayName: "" }];
|
|
2132
|
-
const root = await this.request("PROPFIND", this.url, { body: PROPFIND_ROOT, depth: 0 });
|
|
2133
|
-
const homeBlock = tagValue(root.text, "calendar-home-set");
|
|
2134
|
-
const home = tagValue(homeBlock, "href") || (homeBlock.trim().startsWith("/") ? homeBlock.trim() : "");
|
|
2135
|
-
if (home === "") throw new Error("CalDAV \u670D\u52A1\u6CA1\u6709\u8FD4\u56DE\u65E5\u5386\u76EE\u5F55");
|
|
2136
|
-
const homeUrl = withSlash(resolveHref(this.url, home));
|
|
2137
|
-
const listing = await this.request("PROPFIND", homeUrl, { body: PROPFIND_HOME, depth: 1 });
|
|
2138
|
-
return responseBlocks(listing.text).filter((block) => /calendar/i.test(tagValue(block, "resourcetype"))).map((block) => ({
|
|
2139
|
-
href: withSlash(resolveHref(homeUrl, tagValue(block, "href"))),
|
|
2140
|
-
displayName: tagValue(block, "displayname")
|
|
2141
|
-
}));
|
|
2142
|
-
}
|
|
2143
|
-
async fetchTodos(collectionHref) {
|
|
2144
|
-
const collection = withSlash(resolveHref(this.url, collectionHref));
|
|
2145
|
-
const response = await this.request("REPORT", collection, { body: REPORT_TODOS, depth: 1 });
|
|
2146
|
-
const items = [];
|
|
2147
|
-
for (const block of responseBlocks(response.text)) {
|
|
2148
|
-
const href = tagValue(block, "href");
|
|
2149
|
-
if (href === "") continue;
|
|
2150
|
-
const etag = tagValue(block, "getetag") || null;
|
|
2151
|
-
const data = tagValue(block, "calendar-data");
|
|
2152
|
-
if (data === "") continue;
|
|
2153
|
-
for (const todo of parseTodos(data)) {
|
|
2154
|
-
if (todo.uid === "") continue;
|
|
2155
|
-
items.push({ href: resolveHref(collection, href), etag, todo });
|
|
2156
|
-
}
|
|
2157
|
-
}
|
|
2158
|
-
return items;
|
|
2159
|
-
}
|
|
2160
|
-
async putTodo(collectionHref, task) {
|
|
2161
|
-
const collection = withSlash(resolveHref(this.url, collectionHref));
|
|
2162
|
-
const href = task.href !== null && task.href !== "" ? task.href : new URL(`${encodeURIComponent(task.uid)}.ics`, collection).href;
|
|
2163
|
-
const headers = { "Content-Type": "text/calendar; charset=utf-8" };
|
|
2164
|
-
if (task.etag !== null && task.etag !== "") headers["If-Match"] = task.etag;
|
|
2165
|
-
else headers["If-None-Match"] = "*";
|
|
2166
|
-
const response = await this.request("PUT", href, { body: serializeTodos([task]), headers });
|
|
2167
|
-
return { href, etag: response.headers.get("etag") ?? task.etag, uid: task.uid };
|
|
2168
|
-
}
|
|
2169
|
-
async deleteTodo(href, etag) {
|
|
2170
|
-
const headers = {};
|
|
2171
|
-
if (etag !== null && etag !== "") headers["If-Match"] = etag;
|
|
2172
|
-
await this.request("DELETE", resolveHref(this.url, href), { headers });
|
|
2173
|
-
}
|
|
2174
|
-
/** Pull remote VTODOs, then push local dirty/tombstoned tasks. */
|
|
2175
|
-
async sync(store, accountId) {
|
|
2176
|
-
const collection = await this.discover();
|
|
2177
|
-
const remote = await this.fetchTodos(collection);
|
|
2178
|
-
let pulled = 0;
|
|
2179
|
-
for (const item of remote) {
|
|
2180
|
-
store.applyRemote({
|
|
2181
|
-
accountId,
|
|
2182
|
-
calendarHref: collection,
|
|
2183
|
-
href: item.href,
|
|
2184
|
-
etag: item.etag,
|
|
2185
|
-
todo: item.todo
|
|
2186
|
-
});
|
|
2187
|
-
pulled += 1;
|
|
2188
|
-
}
|
|
2189
|
-
const removed = store.pruneMissingRemote(
|
|
2190
|
-
accountId,
|
|
2191
|
-
collection,
|
|
2192
|
-
remote.map((item) => item.href)
|
|
2193
|
-
);
|
|
2194
|
-
let pushed = 0;
|
|
2195
|
-
let deleted = 0;
|
|
2196
|
-
for (const task of store.pending(accountId)) {
|
|
2197
|
-
if (task.deleted) {
|
|
2198
|
-
if (task.href !== null && task.href !== "") {
|
|
2199
|
-
await this.deleteTodo(task.href, task.etag);
|
|
2200
|
-
deleted += 1;
|
|
2201
|
-
}
|
|
2202
|
-
store.purge(task.id);
|
|
2203
|
-
continue;
|
|
2204
|
-
}
|
|
2205
|
-
const result = await this.putTodo(
|
|
2206
|
-
task.calendarHref !== null && task.calendarHref !== "" ? task.calendarHref : collection,
|
|
2207
|
-
task
|
|
2208
|
-
);
|
|
2209
|
-
store.markPushed(task.id, result);
|
|
2210
|
-
pushed += 1;
|
|
2211
|
-
}
|
|
2212
|
-
store.setCalendarState(accountId, {
|
|
2213
|
-
calendarHref: collection,
|
|
2214
|
-
lastSync: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2215
|
-
error: null
|
|
2216
|
-
});
|
|
2217
|
-
return { pulled, pushed, deleted, removed, calendarHref: collection, total: remote.length };
|
|
2218
|
-
}
|
|
2219
|
-
};
|
|
2220
|
-
|
|
2221
2100
|
// src/host/projects.ts
|
|
2222
2101
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
2223
2102
|
import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync4, renameSync as renameSync3, writeFileSync as writeFileSync4 } from "node:fs";
|
|
@@ -2430,7 +2309,7 @@ ${conversation.trim()}
|
|
|
2430
2309
|
}
|
|
2431
2310
|
|
|
2432
2311
|
// src/host/version.ts
|
|
2433
|
-
var VERSION = "0.
|
|
2312
|
+
var VERSION = "0.7.0";
|
|
2434
2313
|
|
|
2435
2314
|
// src/host/api.ts
|
|
2436
2315
|
init_smtp();
|
|
@@ -2494,7 +2373,7 @@ function buildRouter(runtime) {
|
|
|
2494
2373
|
const accounts = requested === "all" ? runtime.current.accounts.filter((item) => item.enabled !== false) : [accountFrom(runtime, url).account];
|
|
2495
2374
|
if (source === "server") {
|
|
2496
2375
|
const provider = runtime.provider(accounts[0]);
|
|
2497
|
-
const boxes = await provider.mailboxes();
|
|
2376
|
+
const boxes = await provider.mailboxes(url.searchParams.get("refresh") === "1");
|
|
2498
2377
|
return {
|
|
2499
2378
|
items: boxes.map((box) => ({ name: box.path, label: box.name, count: box.messages }))
|
|
2500
2379
|
};
|
|
@@ -2833,6 +2712,53 @@ function buildRouter(runtime) {
|
|
|
2833
2712
|
throw aiHttpError(error);
|
|
2834
2713
|
}
|
|
2835
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
|
+
});
|
|
2836
2762
|
router.post("/ai/compose", async ({ body }) => {
|
|
2837
2763
|
const payload = await body();
|
|
2838
2764
|
const account = accountFromPayload(runtime, payload);
|
|
@@ -2933,7 +2859,7 @@ function buildRouter(runtime) {
|
|
|
2933
2859
|
projectId: url.searchParams.get("projectId") ?? "",
|
|
2934
2860
|
query: url.searchParams.get("query") ?? ""
|
|
2935
2861
|
});
|
|
2936
|
-
return { items: items.map(taskView)
|
|
2862
|
+
return { items: items.map(taskView) };
|
|
2937
2863
|
});
|
|
2938
2864
|
router.post("/tasks", async ({ body }) => {
|
|
2939
2865
|
const payload = await body();
|
|
@@ -2987,58 +2913,6 @@ function buildRouter(runtime) {
|
|
|
2987
2913
|
throw aiHttpError(error);
|
|
2988
2914
|
}
|
|
2989
2915
|
});
|
|
2990
|
-
router.get("/tasks/caldav", ({ url }) => {
|
|
2991
|
-
const account = accountFromPayload(runtime, { accountId: url.searchParams.get("accountId") ?? void 0 });
|
|
2992
|
-
return {
|
|
2993
|
-
caldav: account.caldav,
|
|
2994
|
-
calendar: runtime.tasks().calendarState(account.id),
|
|
2995
|
-
hasPassword: runtime.credentials.readSecret(`${account.keychainService}:caldav`, account.email) !== void 0
|
|
2996
|
-
};
|
|
2997
|
-
});
|
|
2998
|
-
router.put("/tasks/caldav", async ({ body }) => {
|
|
2999
|
-
const payload = await body();
|
|
3000
|
-
const account = accountFromPayload(runtime, payload);
|
|
3001
|
-
const url = String(payload.url ?? "").trim();
|
|
3002
|
-
const username = String(payload.username ?? "").trim();
|
|
3003
|
-
const calendarHref = String(payload.calendarHref ?? "").trim();
|
|
3004
|
-
const caldav = url === "" ? null : { url, username, ...calendarHref === "" ? {} : { calendarHref } };
|
|
3005
|
-
const accounts = runtime.current.accounts.map(
|
|
3006
|
-
(item) => item.id === account.id ? { ...item, caldav } : item
|
|
3007
|
-
);
|
|
3008
|
-
saveAccounts(accounts, runtime.current.activeAccountId);
|
|
3009
|
-
if (typeof payload.password === "string" && payload.password !== "") {
|
|
3010
|
-
runtime.credentials.writeSecret(`${account.keychainService}:caldav`, account.email, payload.password);
|
|
3011
|
-
}
|
|
3012
|
-
runtime.refresh();
|
|
3013
|
-
const updated = runtime.current.accounts.find((item) => item.id === account.id) ?? account;
|
|
3014
|
-
return {
|
|
3015
|
-
caldav: updated.caldav,
|
|
3016
|
-
title: "",
|
|
3017
|
-
hasPassword: runtime.credentials.readSecret(`${updated.keychainService}:caldav`, updated.email) !== void 0
|
|
3018
|
-
};
|
|
3019
|
-
});
|
|
3020
|
-
router.post("/tasks/sync", async ({ body }) => {
|
|
3021
|
-
const payload = await body();
|
|
3022
|
-
const account = accountFromPayload(runtime, payload);
|
|
3023
|
-
const caldav = account.caldav;
|
|
3024
|
-
if (caldav === null) throw new HttpError(400, "\u8BF7\u5148\u914D\u7F6E CalDAV \u5730\u5740\u548C\u8D26\u53F7");
|
|
3025
|
-
const password = runtime.credentials.readSecret(`${account.keychainService}:caldav`, account.email);
|
|
3026
|
-
if (password === void 0 || password === "") throw new HttpError(400, "\u8BF7\u5148\u4FDD\u5B58 CalDAV \u5E94\u7528\u5BC6\u7801");
|
|
3027
|
-
try {
|
|
3028
|
-
const client = new CalDavClient(caldav, password);
|
|
3029
|
-
const result = await runtime.tasks().sync(account.id, client);
|
|
3030
|
-
if (result.calendarHref !== "" && result.calendarHref !== (caldav.calendarHref ?? "")) {
|
|
3031
|
-
const accounts = runtime.current.accounts.map(
|
|
3032
|
-
(item) => item.id === account.id ? { ...item, caldav: { ...caldav, calendarHref: result.calendarHref } } : item
|
|
3033
|
-
);
|
|
3034
|
-
saveAccounts(accounts, runtime.current.activeAccountId);
|
|
3035
|
-
runtime.refresh();
|
|
3036
|
-
}
|
|
3037
|
-
return { result, calendar: runtime.tasks().calendarState(account.id) };
|
|
3038
|
-
} catch (error) {
|
|
3039
|
-
throw new HttpError(400, error instanceof Error ? error.message : String(error));
|
|
3040
|
-
}
|
|
3041
|
-
});
|
|
3042
2916
|
router.get("/projects", () => ({ items: runtime.projects().list().map(projectView) }));
|
|
3043
2917
|
router.get("/projects/for", ({ url }) => {
|
|
3044
2918
|
const account = accountFromPayload(runtime, { accountId: url.searchParams.get("accountId") ?? void 0 });
|
|
@@ -3050,6 +2924,10 @@ function buildRouter(runtime) {
|
|
|
3050
2924
|
})()
|
|
3051
2925
|
};
|
|
3052
2926
|
});
|
|
2927
|
+
router.post("/projects/pick-directory", async () => {
|
|
2928
|
+
const path = await pickDirectory();
|
|
2929
|
+
return { path };
|
|
2930
|
+
});
|
|
3053
2931
|
router.post("/projects", async ({ body }) => {
|
|
3054
2932
|
const payload = await body();
|
|
3055
2933
|
const account = accountFromPayload(runtime, payload);
|
|
@@ -4065,41 +3943,6 @@ var TaskStore = class {
|
|
|
4065
3943
|
}
|
|
4066
3944
|
return removed;
|
|
4067
3945
|
}
|
|
4068
|
-
calendarState(accountId) {
|
|
4069
|
-
const row = this.open().prepare("SELECT * FROM calendar_state WHERE account_id = ?").get(accountId);
|
|
4070
|
-
if (row === void 0) return null;
|
|
4071
|
-
return {
|
|
4072
|
-
calendarHref: String(row.calendar_href ?? ""),
|
|
4073
|
-
lastSync: String(row.last_sync ?? ""),
|
|
4074
|
-
error: String(row.error ?? "")
|
|
4075
|
-
};
|
|
4076
|
-
}
|
|
4077
|
-
setCalendarState(accountId, state) {
|
|
4078
|
-
this.open().prepare(
|
|
4079
|
-
`INSERT INTO calendar_state (account_id, calendar_href, display_name, ctag, last_sync, error)
|
|
4080
|
-
VALUES (?, ?, NULL, NULL, ?, ?)
|
|
4081
|
-
ON CONFLICT(account_id) DO UPDATE SET calendar_href = excluded.calendar_href,
|
|
4082
|
-
last_sync = excluded.last_sync, error = excluded.error`
|
|
4083
|
-
).run(accountId, state.calendarHref ?? null, state.lastSync ?? null, state.error ?? null);
|
|
4084
|
-
}
|
|
4085
|
-
/** Pull remote tasks, push local edits, and record the sync result. */
|
|
4086
|
-
async sync(accountId, client) {
|
|
4087
|
-
try {
|
|
4088
|
-
const result = await client.sync(this, accountId);
|
|
4089
|
-
this.setCalendarState(accountId, {
|
|
4090
|
-
calendarHref: result.calendarHref,
|
|
4091
|
-
lastSync: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4092
|
-
error: null
|
|
4093
|
-
});
|
|
4094
|
-
return result;
|
|
4095
|
-
} catch (error) {
|
|
4096
|
-
this.setCalendarState(accountId, {
|
|
4097
|
-
lastSync: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4098
|
-
error: error instanceof Error ? error.message : String(error)
|
|
4099
|
-
});
|
|
4100
|
-
throw error;
|
|
4101
|
-
}
|
|
4102
|
-
}
|
|
4103
3946
|
};
|
|
4104
3947
|
|
|
4105
3948
|
// src/host/drafts.ts
|