@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/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) this.client = null;
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
- return this.run((client) => this.listMailboxes(client));
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 result = [];
312
- for (const box of listed) {
313
- if (box.flags?.has?.("\\Noselect") === true) continue;
314
- let messages = 0;
315
- try {
316
- const status = await client.status(box.path, { messages: true });
317
- if (status !== false) messages = Number(status.messages ?? 0);
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 pad2 = (number) => String(number).padStart(2, "0");
1085
- return `${String(date.getFullYear())}-${pad2(date.getMonth() + 1)}-${pad2(date.getDate())} ${pad2(date.getHours())}:${pad2(date.getMinutes())}:${pad2(date.getSeconds())}`;
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("&lt;", "<").replaceAll("&gt;", ">").replaceAll("&quot;", '"').replaceAll("&apos;", "'").replaceAll("&amp;", "&");
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.5.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), calendar: runtime.tasks().calendarState(account.id) };
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
- workspaceRegistry;
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.workspaceRegistry = options.workspaceRegistry;
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: optionalService(ctx, "workspaceRegistry"),
4528
+ workspaceRegistry: () => registry,
4920
4529
  sessions: optionalService(ctx, "sessions"),
4921
4530
  llm: () => {
4922
4531
  if (typeof ctx.get !== "function") return void 0;