@zaclinley/dsh-mailharbor 0.5.0 → 0.6.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 +26 -0
- package/README.md +22 -19
- package/README.zh.md +22 -19
- package/lib/client.js +405 -366
- package/lib/client.js.map +3 -3
- package/lib/index.js +84 -475
- 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,6 +379,11 @@ 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();
|
|
@@ -423,6 +462,7 @@ var init_imap = __esm({
|
|
|
423
462
|
});
|
|
424
463
|
}
|
|
425
464
|
async setSeen(mailbox, uid, seen) {
|
|
465
|
+
this.pageCache.clear();
|
|
426
466
|
return this.run(async (client) => {
|
|
427
467
|
const lock = await client.getMailboxLock(mailbox);
|
|
428
468
|
try {
|
|
@@ -461,6 +501,8 @@ var init_imap = __esm({
|
|
|
461
501
|
}
|
|
462
502
|
async dispose() {
|
|
463
503
|
this.disposed = true;
|
|
504
|
+
this.pageCache.clear();
|
|
505
|
+
this.stopKeepalive();
|
|
464
506
|
await this.reset("dispose");
|
|
465
507
|
}
|
|
466
508
|
};
|
|
@@ -1081,8 +1123,8 @@ function normalizeId(value) {
|
|
|
1081
1123
|
function stamp(value) {
|
|
1082
1124
|
const date = value instanceof Date ? value : value === void 0 || value === null ? void 0 : new Date(String(value));
|
|
1083
1125
|
if (!(date instanceof Date) || Number.isNaN(date.getTime())) return "";
|
|
1084
|
-
const
|
|
1085
|
-
return `${String(date.getFullYear())}-${
|
|
1126
|
+
const pad = (number) => String(number).padStart(2, "0");
|
|
1127
|
+
return `${String(date.getFullYear())}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
|
|
1086
1128
|
}
|
|
1087
1129
|
function abortIfNeeded(signal) {
|
|
1088
1130
|
if (signal?.aborted === true) {
|
|
@@ -1859,365 +1901,6 @@ function textToHtml(text2) {
|
|
|
1859
1901
|
).join("");
|
|
1860
1902
|
}
|
|
1861
1903
|
|
|
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
1904
|
// src/host/projects.ts
|
|
2222
1905
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
2223
1906
|
import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync4, renameSync as renameSync3, writeFileSync as writeFileSync4 } from "node:fs";
|
|
@@ -2430,7 +2113,7 @@ ${conversation.trim()}
|
|
|
2430
2113
|
}
|
|
2431
2114
|
|
|
2432
2115
|
// src/host/version.ts
|
|
2433
|
-
var VERSION = "0.
|
|
2116
|
+
var VERSION = "0.6.0";
|
|
2434
2117
|
|
|
2435
2118
|
// src/host/api.ts
|
|
2436
2119
|
init_smtp();
|
|
@@ -2494,7 +2177,7 @@ function buildRouter(runtime) {
|
|
|
2494
2177
|
const accounts = requested === "all" ? runtime.current.accounts.filter((item) => item.enabled !== false) : [accountFrom(runtime, url).account];
|
|
2495
2178
|
if (source === "server") {
|
|
2496
2179
|
const provider = runtime.provider(accounts[0]);
|
|
2497
|
-
const boxes = await provider.mailboxes();
|
|
2180
|
+
const boxes = await provider.mailboxes(url.searchParams.get("refresh") === "1");
|
|
2498
2181
|
return {
|
|
2499
2182
|
items: boxes.map((box) => ({ name: box.path, label: box.name, count: box.messages }))
|
|
2500
2183
|
};
|
|
@@ -2933,7 +2616,7 @@ function buildRouter(runtime) {
|
|
|
2933
2616
|
projectId: url.searchParams.get("projectId") ?? "",
|
|
2934
2617
|
query: url.searchParams.get("query") ?? ""
|
|
2935
2618
|
});
|
|
2936
|
-
return { items: items.map(taskView)
|
|
2619
|
+
return { items: items.map(taskView) };
|
|
2937
2620
|
});
|
|
2938
2621
|
router.post("/tasks", async ({ body }) => {
|
|
2939
2622
|
const payload = await body();
|
|
@@ -2987,58 +2670,6 @@ function buildRouter(runtime) {
|
|
|
2987
2670
|
throw aiHttpError(error);
|
|
2988
2671
|
}
|
|
2989
2672
|
});
|
|
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
2673
|
router.get("/projects", () => ({ items: runtime.projects().list().map(projectView) }));
|
|
3043
2674
|
router.get("/projects/for", ({ url }) => {
|
|
3044
2675
|
const account = accountFromPayload(runtime, { accountId: url.searchParams.get("accountId") ?? void 0 });
|
|
@@ -4065,41 +3696,6 @@ var TaskStore = class {
|
|
|
4065
3696
|
}
|
|
4066
3697
|
return removed;
|
|
4067
3698
|
}
|
|
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
3699
|
};
|
|
4104
3700
|
|
|
4105
3701
|
// src/host/drafts.ts
|
|
@@ -4283,7 +3879,7 @@ var MailHarborRuntime = class {
|
|
|
4283
3879
|
aiStores = /* @__PURE__ */ new Map();
|
|
4284
3880
|
backups = /* @__PURE__ */ new Map();
|
|
4285
3881
|
llmProvider;
|
|
4286
|
-
|
|
3882
|
+
workspaceRegistryProvider;
|
|
4287
3883
|
sessions;
|
|
4288
3884
|
aiService = null;
|
|
4289
3885
|
projectStore = null;
|
|
@@ -4293,9 +3889,12 @@ var MailHarborRuntime = class {
|
|
|
4293
3889
|
this.config = loadConfig();
|
|
4294
3890
|
this.credentials = options.credentials ?? new CredentialManager();
|
|
4295
3891
|
this.llmProvider = options.llm ?? (() => void 0);
|
|
4296
|
-
this.
|
|
3892
|
+
this.workspaceRegistryProvider = options.workspaceRegistry ?? (() => void 0);
|
|
4297
3893
|
this.sessions = options.sessions;
|
|
4298
3894
|
}
|
|
3895
|
+
get workspaceRegistry() {
|
|
3896
|
+
return this.workspaceRegistryProvider();
|
|
3897
|
+
}
|
|
4299
3898
|
projects() {
|
|
4300
3899
|
if (this.projectStore === null) {
|
|
4301
3900
|
this.projectStore = new ProjectStore(join7(this.config.dataDir, "projects.json"));
|
|
@@ -4915,8 +4514,18 @@ function optionalService(ctx, name2) {
|
|
|
4915
4514
|
}
|
|
4916
4515
|
}
|
|
4917
4516
|
function apply(ctx) {
|
|
4517
|
+
let registry;
|
|
4518
|
+
if (typeof ctx.inject === "function") {
|
|
4519
|
+
ctx.inject(["workspaceRegistry"], (scope) => {
|
|
4520
|
+
try {
|
|
4521
|
+
registry = scope.workspaceRegistry;
|
|
4522
|
+
} catch {
|
|
4523
|
+
registry = void 0;
|
|
4524
|
+
}
|
|
4525
|
+
});
|
|
4526
|
+
}
|
|
4918
4527
|
const runtime = new MailHarborRuntime({
|
|
4919
|
-
workspaceRegistry:
|
|
4528
|
+
workspaceRegistry: () => registry,
|
|
4920
4529
|
sessions: optionalService(ctx, "sessions"),
|
|
4921
4530
|
llm: () => {
|
|
4922
4531
|
if (typeof ctx.get !== "function") return void 0;
|