@zaclinley/dsh-mailharbor 0.12.2 → 0.13.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
@@ -66,6 +66,44 @@ var init_address = __esm({
66
66
  }
67
67
  });
68
68
 
69
+ // src/host/inline-images.ts
70
+ function normalize(value) {
71
+ return value.trim().replace(/^<|>$/g, "").toLowerCase();
72
+ }
73
+ function dataUrl(image) {
74
+ if (image.data === void 0 || !image.contentType.toLowerCase().startsWith("image/")) return null;
75
+ if (image.data.byteLength > MAX_INLINE_BYTES) return null;
76
+ return `data:${image.contentType};base64,${Buffer.from(image.data).toString("base64")}`;
77
+ }
78
+ function inlineCidImages(html, images) {
79
+ if (!/cid:/i.test(html)) return html;
80
+ const byCid = /* @__PURE__ */ new Map();
81
+ const byName = [];
82
+ for (const image of images) {
83
+ const url = dataUrl(image);
84
+ if (url === null) continue;
85
+ if (image.cid !== void 0 && image.cid.trim() !== "") byCid.set(normalize(image.cid), url);
86
+ if (image.filename !== void 0 && image.filename.trim() !== "") {
87
+ byName.push({ name: normalize(image.filename), url });
88
+ }
89
+ }
90
+ if (byCid.size === 0 && byName.length === 0) return html;
91
+ return html.replace(/cid:([^"'\s)>]+)/gi, (match, rawCid) => {
92
+ const cid = normalize(rawCid);
93
+ const exact = byCid.get(cid);
94
+ if (exact !== void 0) return exact;
95
+ const named = byName.find((entry) => cid.includes(entry.name));
96
+ return named === void 0 ? match : named.url;
97
+ });
98
+ }
99
+ var MAX_INLINE_BYTES;
100
+ var init_inline_images = __esm({
101
+ "src/host/inline-images.ts"() {
102
+ "use strict";
103
+ MAX_INLINE_BYTES = 2e6;
104
+ }
105
+ });
106
+
69
107
  // src/host/smtp.ts
70
108
  var smtp_exports = {};
71
109
  __export(smtp_exports, {
@@ -221,6 +259,7 @@ var init_imap = __esm({
221
259
  "src/host/imap.ts"() {
222
260
  "use strict";
223
261
  init_address();
262
+ init_inline_images();
224
263
  ImapProvider = class _ImapProvider {
225
264
  settings;
226
265
  credentials;
@@ -451,10 +490,19 @@ var init_imap = __esm({
451
490
  const parsed = await simpleParser2(message.source);
452
491
  const base = toMessage(message);
453
492
  const html = typeof parsed.html === "string" ? parsed.html : Buffer.isBuffer(parsed.html) ? parsed.html.toString("utf8") : null;
493
+ const inlineImages = (parsed.attachments ?? []).filter((attachment) => {
494
+ const type = String(attachment.contentType ?? "").toLowerCase();
495
+ return type.startsWith("image/") && attachment.content !== void 0;
496
+ }).map((attachment) => ({
497
+ cid: attachment.cid,
498
+ filename: attachment.filename,
499
+ contentType: String(attachment.contentType ?? "image/png"),
500
+ data: Buffer.isBuffer(attachment.content) ? attachment.content : new Uint8Array(attachment.content)
501
+ }));
454
502
  const detail = {
455
503
  ...base,
456
504
  text: typeof parsed.text === "string" ? parsed.text : "",
457
- html: html === "" ? null : html,
505
+ html: html === null || html === "" ? null : inlineCidImages(html, inlineImages),
458
506
  attachments: attachmentList(parsed),
459
507
  inReplyTo: normalizeId2(parsed.inReplyTo),
460
508
  references: String(parsed.references ?? "").split(/\s+/).map(normalizeId2).filter((item) => item !== "")
@@ -513,6 +561,28 @@ var init_imap = __esm({
513
561
  }
514
562
  });
515
563
  }
564
+ async setFlag(mailbox, uid, flag, value) {
565
+ return this.run(async (client) => {
566
+ const lock = await client.getMailboxLock(mailbox);
567
+ try {
568
+ if (value) await client.messageFlagsAdd(String(uid), [flag], { uid: true });
569
+ else await client.messageFlagsRemove(String(uid), [flag], { uid: true });
570
+ } finally {
571
+ lock.release();
572
+ }
573
+ });
574
+ }
575
+ /** Move one message to another mailbox (server-side). */
576
+ async move(mailbox, uid, target) {
577
+ return this.run(async (client) => {
578
+ const lock = await client.getMailboxLock(mailbox);
579
+ try {
580
+ await client.messageMove(String(uid), target, { uid: true });
581
+ } finally {
582
+ lock.release();
583
+ }
584
+ });
585
+ }
516
586
  async setSeen(mailbox, uid, seen) {
517
587
  this.pageCache.clear();
518
588
  return this.run(async (client) => {
@@ -920,6 +990,18 @@ async function openPath(path) {
920
990
  await run("xdg-open", [path]);
921
991
  }
922
992
 
993
+ // src/host/mail-write.ts
994
+ import { DatabaseSync } from "node:sqlite";
995
+ function moveLocalMessage(file, id, mailbox) {
996
+ const db = new DatabaseSync(file);
997
+ try {
998
+ db.exec("PRAGMA busy_timeout = 5000");
999
+ db.prepare("UPDATE emails SET mailbox = ? WHERE id = ?").run(mailbox, id);
1000
+ } finally {
1001
+ db.close();
1002
+ }
1003
+ }
1004
+
923
1005
  // src/host/backup.ts
924
1006
  init_address();
925
1007
  import { createHash as createHash2 } from "node:crypto";
@@ -927,12 +1009,13 @@ import { existsSync as existsSync3, mkdirSync as mkdirSync2, writeFileSync as wr
927
1009
  import { join as join3 } from "node:path";
928
1010
  import { ImapFlow } from "imapflow";
929
1011
  import { simpleParser } from "mailparser";
930
- import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
1012
+ import { DatabaseSync as DatabaseSync3 } from "node:sqlite";
931
1013
 
932
1014
  // src/host/mail-db.ts
933
1015
  init_address();
1016
+ init_inline_images();
934
1017
  import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
935
- import { DatabaseSync } from "node:sqlite";
1018
+ import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
936
1019
  var SCHEMA = `
937
1020
  CREATE TABLE IF NOT EXISTS emails (
938
1021
  id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -990,7 +1073,7 @@ var FTS = `CREATE VIRTUAL TABLE IF NOT EXISTS emails_fts USING fts5(
990
1073
  content='', tokenize='trigram'
991
1074
  )`;
992
1075
  function ensureMailSchema(file) {
993
- const db = new DatabaseSync(file);
1076
+ const db = new DatabaseSync2(file);
994
1077
  try {
995
1078
  db.exec("PRAGMA journal_mode=WAL");
996
1079
  db.exec(SCHEMA);
@@ -1029,7 +1112,7 @@ var MailDatabase = class {
1029
1112
  open() {
1030
1113
  if (this.db !== void 0) return this.db;
1031
1114
  if (!existsSync2(this.file)) throw new Error(`\u90AE\u4EF6\u6570\u636E\u5E93\u4E0D\u5B58\u5728: ${this.file}`);
1032
- this.db = new DatabaseSync(this.file, { readOnly: true });
1115
+ this.db = new DatabaseSync2(this.file, { readOnly: true });
1033
1116
  this.db.exec("PRAGMA busy_timeout = 3000");
1034
1117
  return this.db;
1035
1118
  }
@@ -1234,9 +1317,14 @@ var MailDatabase = class {
1234
1317
  FROM emails WHERE id = ?`
1235
1318
  ).get(localId);
1236
1319
  if (row === void 0) return void 0;
1237
- const cidText = String(row.body_html ?? "").toLowerCase();
1320
+ const bodyHtml = String(row.body_html ?? "");
1321
+ const cidText = bodyHtml.toLowerCase();
1322
+ const wantsInline = cidText.includes("cid:");
1238
1323
  const attachmentColumns = this.hasAttachmentColumns() ? "id, filename, content_type, size, content_id, disposition" : "id, filename, content_type, size";
1239
- const attachments = db.prepare(`SELECT ${attachmentColumns} FROM attachments WHERE email_id = ? ORDER BY id`).all(localId).filter((item) => {
1324
+ const rows = db.prepare(
1325
+ `SELECT ${attachmentColumns}${wantsInline ? ", data, hash" : ""} FROM attachments WHERE email_id = ? ORDER BY id`
1326
+ ).all(localId);
1327
+ const attachments = rows.filter((item) => {
1240
1328
  const type = String(item.content_type ?? "").toLowerCase();
1241
1329
  if (!type.startsWith("image/")) return true;
1242
1330
  const disposition = String(item.disposition ?? "").toLowerCase();
@@ -1255,17 +1343,38 @@ var MailDatabase = class {
1255
1343
  }));
1256
1344
  const max = this.options.maxBodyChars ?? 4e5;
1257
1345
  const bodyText2 = String(row.body_text ?? "");
1258
- const bodyHtml = String(row.body_html ?? "");
1346
+ const inlineImages = wantsInline ? rows.filter(
1347
+ (item) => String(item.content_type ?? "").toLowerCase().startsWith("image/")
1348
+ ).map((item) => ({
1349
+ cid: item.content_id === null || item.content_id === void 0 ? void 0 : String(item.content_id),
1350
+ filename: String(item.filename ?? ""),
1351
+ contentType: String(item.content_type ?? "image/png"),
1352
+ data: this.attachmentBytes(item)
1353
+ })).filter((item) => item.data !== void 0) : [];
1259
1354
  return {
1260
1355
  ...rowToSummary(row),
1261
1356
  cc: parseAddressList(String(row.cc ?? "")),
1262
1357
  replyTo: [],
1263
1358
  text: truncate(bodyText2, max),
1264
- html: bodyHtml === "" ? null : truncate(bodyHtml, max * 2),
1359
+ html: bodyHtml === "" ? null : truncate(inlineCidImages(bodyHtml, inlineImages), max * 2),
1265
1360
  attachments,
1266
1361
  references: []
1267
1362
  };
1268
1363
  }
1364
+ attachmentBytes(row) {
1365
+ const data = row.data;
1366
+ if (data instanceof Uint8Array) return data;
1367
+ const hash = typeof row.hash === "string" ? row.hash : "";
1368
+ const root = this.options.attachmentsRoot ?? "";
1369
+ if (hash === "" || root === "") return void 0;
1370
+ const file = `${root}/${hash.slice(0, 2)}/${hash.slice(2, 4)}/${hash}`;
1371
+ if (!existsSync2(file)) return void 0;
1372
+ try {
1373
+ return readFileSyncSafe(file) ?? void 0;
1374
+ } catch {
1375
+ return void 0;
1376
+ }
1377
+ }
1269
1378
  /** Read one attachment's bytes; the payload may live in a file (content-addressed). */
1270
1379
  attachment(emailId, attachmentId) {
1271
1380
  const db = this.open();
@@ -1462,7 +1571,7 @@ var BackupEngine = class {
1462
1571
  }
1463
1572
  openDb() {
1464
1573
  ensureMailSchema(this.runtime.mailDb);
1465
- const db = new DatabaseSync2(this.runtime.mailDb);
1574
+ const db = new DatabaseSync3(this.runtime.mailDb);
1466
1575
  db.exec("PRAGMA journal_mode=WAL");
1467
1576
  db.exec("PRAGMA busy_timeout=10000");
1468
1577
  db.exec("PRAGMA synchronous=NORMAL");
@@ -1714,7 +1823,7 @@ var BackupEngine = class {
1714
1823
  };
1715
1824
  if (!existsSync3(dbFile)) return empty;
1716
1825
  try {
1717
- const db = new DatabaseSync2(dbFile, { readOnly: true });
1826
+ const db = new DatabaseSync3(dbFile, { readOnly: true });
1718
1827
  try {
1719
1828
  const latest = db.prepare("SELECT * FROM backup_logs ORDER BY id DESC LIMIT 1").get();
1720
1829
  const success = db.prepare("SELECT completed_at FROM backup_logs WHERE status = 'completed' ORDER BY id DESC LIMIT 1").get();
@@ -2590,7 +2699,7 @@ ${conversation.trim()}
2590
2699
  }
2591
2700
 
2592
2701
  // src/host/version.ts
2593
- var VERSION = "0.12.2";
2702
+ var VERSION = "0.13.0";
2594
2703
 
2595
2704
  // src/host/api.ts
2596
2705
  init_smtp();
@@ -2719,6 +2828,91 @@ function buildRouter(runtime) {
2719
2828
  }
2720
2829
  throw new HttpError(400, "\u65E0\u6CD5\u5B9A\u4F4D\u8FD9\u5C01\u90AE\u4EF6");
2721
2830
  });
2831
+ router.post("/messages/:id/star", async ({ url, params, body }) => {
2832
+ const payload = await body();
2833
+ const { account } = accountFrom(runtime, url);
2834
+ const detail = await loadDetail(
2835
+ runtime,
2836
+ account,
2837
+ sourceOfPayload(payload),
2838
+ String(params.id ?? ""),
2839
+ mailboxOfPayload(payload)
2840
+ );
2841
+ const starred = payload.starred !== false;
2842
+ const key = messageKeyOf(detail);
2843
+ runtime.flags(account).set(key, starred);
2844
+ if (detail.uid !== void 0) {
2845
+ try {
2846
+ await runtime.provider(account).setFlag(detail.mailbox, detail.uid, "\\Flagged", starred);
2847
+ } catch {
2848
+ }
2849
+ }
2850
+ return { starred };
2851
+ });
2852
+ router.post("/messages/:id/move", async ({ url, params, body }) => {
2853
+ const payload = await body();
2854
+ const { account } = accountFrom(runtime, url);
2855
+ const target = String(payload.target ?? "").trim();
2856
+ if (target === "") throw new HttpError(400, "\u7F3A\u5C11\u76EE\u6807\u6587\u4EF6\u5939");
2857
+ const detail = runtime.database(account).get(Number(params.id));
2858
+ if (detail === void 0) throw new HttpError(404, "\u90AE\u4EF6\u4E0D\u5B58\u5728");
2859
+ if (detail.uid !== void 0) {
2860
+ try {
2861
+ await runtime.provider(account).move(detail.mailbox, detail.uid, target);
2862
+ } catch {
2863
+ }
2864
+ }
2865
+ moveLocalMessage(runtime.paths(account).mailDb, Number(params.id), target);
2866
+ return { ok: true, mailbox: target };
2867
+ });
2868
+ router.post("/messages/bulk", async ({ body }) => {
2869
+ const payload = await body();
2870
+ const account = accountFromPayload(runtime, payload);
2871
+ const action = String(payload.action ?? "");
2872
+ const ids = Array.isArray(payload.ids) ? payload.ids.map((id) => Number(id)) : [];
2873
+ if (ids.length === 0) return { ok: true, count: 0 };
2874
+ const database = runtime.database(account);
2875
+ const flags = runtime.flags(account);
2876
+ const readState = runtime.readState(account);
2877
+ let count = 0;
2878
+ for (const id of ids) {
2879
+ const detail = database.get(id);
2880
+ if (detail === void 0) continue;
2881
+ const key = messageKeyOf(detail);
2882
+ if (action === "read" || action === "unread") {
2883
+ readState.setUnread(key, action === "unread");
2884
+ if (detail.uid !== void 0) {
2885
+ try {
2886
+ await runtime.provider(account).setSeen(detail.mailbox, detail.uid, action === "read");
2887
+ } catch {
2888
+ }
2889
+ }
2890
+ count += 1;
2891
+ } else if (action === "archive" || action === "delete" || action === "move") {
2892
+ const target = String(payload.target ?? "").trim();
2893
+ if (target === "") continue;
2894
+ if (detail.uid !== void 0) {
2895
+ try {
2896
+ await runtime.provider(account).move(detail.mailbox, detail.uid, target);
2897
+ } catch {
2898
+ }
2899
+ }
2900
+ moveLocalMessage(runtime.paths(account).mailDb, Number(detail.id), target);
2901
+ count += 1;
2902
+ } else if (action === "star" || action === "unstar") {
2903
+ const starred = action === "star";
2904
+ flags.set(key, starred);
2905
+ if (detail.uid !== void 0) {
2906
+ try {
2907
+ await runtime.provider(account).setFlag(detail.mailbox, detail.uid, "\\Flagged", starred);
2908
+ } catch {
2909
+ }
2910
+ }
2911
+ count += 1;
2912
+ }
2913
+ }
2914
+ return { ok: true, count };
2915
+ });
2722
2916
  router.get("/messages/:id", async ({ url, params }) => {
2723
2917
  const { account } = accountFrom(runtime, url);
2724
2918
  const source = sourceOf(url);
@@ -3507,10 +3701,19 @@ function unreadKeys(runtime, account) {
3507
3701
  }
3508
3702
  return { mids, ids };
3509
3703
  }
3704
+ function messageKeyOf(row) {
3705
+ return row.messageId !== void 0 && row.messageId !== "" ? `mid:${row.messageId}` : `local:${row.id}`;
3706
+ }
3510
3707
  function localSummary(runtime, account, row) {
3511
3708
  const store = runtime.readState(account);
3512
- const key = row.messageId !== void 0 && row.messageId !== "" ? `mid:${row.messageId}` : `local:${row.id}`;
3513
- return { ...row, accountId: account.id, unread: store.isUnread(key), backedUp: true };
3709
+ const key = messageKeyOf(row);
3710
+ return {
3711
+ ...row,
3712
+ accountId: account.id,
3713
+ unread: store.isUnread(key),
3714
+ starred: runtime.flags(account).has(key),
3715
+ backedUp: true
3716
+ };
3514
3717
  }
3515
3718
  function localDetail(runtime, account, detail) {
3516
3719
  return localSummary(runtime, account, detail);
@@ -3987,11 +4190,63 @@ function prune(data) {
3987
4190
  }
3988
4191
  }
3989
4192
 
4193
+ // src/host/mail-flags.ts
4194
+ import { mkdirSync as mkdirSync6, readFileSync as readFileSync6, renameSync as renameSync5, writeFileSync as writeFileSync6 } from "node:fs";
4195
+ import { dirname as dirname3 } from "node:path";
4196
+ var MailFlagsStore = class {
4197
+ file;
4198
+ keys = null;
4199
+ constructor(file) {
4200
+ this.file = file;
4201
+ }
4202
+ load() {
4203
+ if (this.keys !== null) return this.keys;
4204
+ try {
4205
+ const parsed = JSON.parse(readFileSync6(this.file, "utf8"));
4206
+ const list = Array.isArray(parsed) ? parsed : Array.isArray(parsed?.starred) ? parsed.starred : [];
4207
+ this.keys = new Set(list.filter((key) => typeof key === "string" && key !== ""));
4208
+ } catch {
4209
+ this.keys = /* @__PURE__ */ new Set();
4210
+ }
4211
+ return this.keys;
4212
+ }
4213
+ flush() {
4214
+ mkdirSync6(dirname3(this.file), { recursive: true });
4215
+ const temp = `${this.file}.${String(process.pid)}.tmp`;
4216
+ writeFileSync6(temp, `${JSON.stringify({ starred: [...this.load()] }, null, 2)}
4217
+ `, {
4218
+ encoding: "utf8",
4219
+ mode: 384
4220
+ });
4221
+ renameSync5(temp, this.file);
4222
+ }
4223
+ has(key) {
4224
+ return key !== "" && this.load().has(key);
4225
+ }
4226
+ set(key, starred) {
4227
+ const normalized = key.trim();
4228
+ if (normalized === "") return false;
4229
+ const keys = this.load();
4230
+ const has = keys.has(normalized);
4231
+ if (starred && !has) {
4232
+ keys.add(normalized);
4233
+ this.flush();
4234
+ return true;
4235
+ }
4236
+ if (!starred && has) {
4237
+ keys.delete(normalized);
4238
+ this.flush();
4239
+ return true;
4240
+ }
4241
+ return false;
4242
+ }
4243
+ };
4244
+
3990
4245
  // src/host/tasks.ts
3991
4246
  import { randomUUID as randomUUID4 } from "node:crypto";
3992
- import { mkdirSync as mkdirSync6 } from "node:fs";
3993
- import { dirname as dirname3 } from "node:path";
3994
- import { DatabaseSync as DatabaseSync3 } from "node:sqlite";
4247
+ import { mkdirSync as mkdirSync7 } from "node:fs";
4248
+ import { dirname as dirname4 } from "node:path";
4249
+ import { DatabaseSync as DatabaseSync4 } from "node:sqlite";
3995
4250
  var STATUSES = /* @__PURE__ */ new Set(["NEEDS-ACTION", "IN-PROCESS", "COMPLETED", "CANCELLED"]);
3996
4251
  function now() {
3997
4252
  return (/* @__PURE__ */ new Date()).toISOString();
@@ -4039,8 +4294,8 @@ var TaskStore = class {
4039
4294
  }
4040
4295
  open() {
4041
4296
  if (this.db !== null) return this.db;
4042
- mkdirSync6(dirname3(this.file), { recursive: true });
4043
- const db = new DatabaseSync3(this.file);
4297
+ mkdirSync7(dirname4(this.file), { recursive: true });
4298
+ const db = new DatabaseSync4(this.file);
4044
4299
  db.exec("PRAGMA journal_mode=WAL");
4045
4300
  db.exec("PRAGMA busy_timeout=10000");
4046
4301
  db.exec(`
@@ -4279,8 +4534,8 @@ var TaskStore = class {
4279
4534
 
4280
4535
  // src/host/drafts.ts
4281
4536
  import { randomUUID as randomUUID5 } from "node:crypto";
4282
- import { mkdirSync as mkdirSync7, readFileSync as readFileSync6, renameSync as renameSync5, writeFileSync as writeFileSync6 } from "node:fs";
4283
- import { dirname as dirname4, join as join6 } from "node:path";
4537
+ import { mkdirSync as mkdirSync8, readFileSync as readFileSync7, renameSync as renameSync6, writeFileSync as writeFileSync7 } from "node:fs";
4538
+ import { dirname as dirname5, join as join6 } from "node:path";
4284
4539
  function text(value) {
4285
4540
  return value === void 0 || value === null ? "" : String(value);
4286
4541
  }
@@ -4304,18 +4559,18 @@ var DraftStore = class {
4304
4559
  }
4305
4560
  load() {
4306
4561
  try {
4307
- const parsed = JSON.parse(readFileSync6(this.file, "utf8"));
4562
+ const parsed = JSON.parse(readFileSync7(this.file, "utf8"));
4308
4563
  this.items = Array.isArray(parsed) ? parsed.filter(isDraft) : [];
4309
4564
  } catch {
4310
4565
  this.items = [];
4311
4566
  }
4312
4567
  }
4313
4568
  flush() {
4314
- mkdirSync7(dirname4(this.file), { recursive: true });
4569
+ mkdirSync8(dirname5(this.file), { recursive: true });
4315
4570
  const temp = `${this.file}.${String(process.pid)}.${String(Date.now())}.tmp`;
4316
- writeFileSync6(temp, `${JSON.stringify(this.items, null, 2)}
4571
+ writeFileSync7(temp, `${JSON.stringify(this.items, null, 2)}
4317
4572
  `, { encoding: "utf8", mode: 384 });
4318
- renameSync5(temp, this.file);
4573
+ renameSync6(temp, this.file);
4319
4574
  }
4320
4575
  list() {
4321
4576
  return [...this.items].sort((left, right) => right.updatedAt.localeCompare(left.updatedAt)).map((draft) => ({ ...draft }));
@@ -4372,8 +4627,8 @@ var DraftStore = class {
4372
4627
  init_imap();
4373
4628
 
4374
4629
  // src/host/read-state.ts
4375
- import { mkdirSync as mkdirSync8, readFileSync as readFileSync7, renameSync as renameSync6, writeFileSync as writeFileSync7 } from "node:fs";
4376
- import { dirname as dirname5 } from "node:path";
4630
+ import { mkdirSync as mkdirSync9, readFileSync as readFileSync8, renameSync as renameSync7, writeFileSync as writeFileSync8 } from "node:fs";
4631
+ import { dirname as dirname6 } from "node:path";
4377
4632
  var ReadStateStore = class {
4378
4633
  file;
4379
4634
  keys = null;
@@ -4383,7 +4638,7 @@ var ReadStateStore = class {
4383
4638
  load() {
4384
4639
  if (this.keys !== null) return this.keys;
4385
4640
  try {
4386
- const parsed = JSON.parse(readFileSync7(this.file, "utf8"));
4641
+ const parsed = JSON.parse(readFileSync8(this.file, "utf8"));
4387
4642
  const list = Array.isArray(parsed) ? parsed : Array.isArray(parsed?.unread) ? parsed.unread : [];
4388
4643
  this.keys = new Set(list.filter((key) => typeof key === "string" && key !== ""));
4389
4644
  } catch {
@@ -4392,14 +4647,14 @@ var ReadStateStore = class {
4392
4647
  return this.keys;
4393
4648
  }
4394
4649
  flush() {
4395
- mkdirSync8(dirname5(this.file), { recursive: true });
4650
+ mkdirSync9(dirname6(this.file), { recursive: true });
4396
4651
  const temp = `${this.file}.${String(process.pid)}.tmp`;
4397
- writeFileSync7(temp, `${JSON.stringify({ unread: [...this.load()] }, null, 2)}
4652
+ writeFileSync8(temp, `${JSON.stringify({ unread: [...this.load()] }, null, 2)}
4398
4653
  `, {
4399
4654
  encoding: "utf8",
4400
4655
  mode: 384
4401
4656
  });
4402
- renameSync6(temp, this.file);
4657
+ renameSync7(temp, this.file);
4403
4658
  }
4404
4659
  isUnread(key) {
4405
4660
  return key !== "" && this.load().has(key);
@@ -4463,6 +4718,7 @@ var MailHarborRuntime = class {
4463
4718
  aiService = null;
4464
4719
  projectStore = null;
4465
4720
  taskStore = null;
4721
+ flagStores = /* @__PURE__ */ new Map();
4466
4722
  disposed = false;
4467
4723
  constructor(options = {}) {
4468
4724
  this.config = loadConfig();
@@ -4617,6 +4873,14 @@ var MailHarborRuntime = class {
4617
4873
  this.aiStores.set(account.id, { signature: file, value: created });
4618
4874
  return created;
4619
4875
  }
4876
+ flags(account) {
4877
+ const file = join7(this.paths(account).root, "starred.json");
4878
+ const existing = this.flagStores.get(account.id);
4879
+ if (existing !== void 0 && existing.signature === file) return existing.value;
4880
+ const created = new MailFlagsStore(file);
4881
+ this.flagStores.set(account.id, { signature: file, value: created });
4882
+ return created;
4883
+ }
4620
4884
  readState(account) {
4621
4885
  const file = join7(this.paths(account).root, "read-state.json");
4622
4886
  const existing = this.readStores.get(account.id);