@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/CHANGELOG.md +16 -0
- package/lib/client.js +168 -4
- package/lib/client.js.map +2 -2
- package/lib/index.js +295 -31
- package/lib/index.js.map +4 -4
- package/package.json +1 -1
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
3513
|
-
return {
|
|
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
|
|
3993
|
-
import { dirname as
|
|
3994
|
-
import { DatabaseSync as
|
|
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
|
-
|
|
4043
|
-
const db = new
|
|
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
|
|
4283
|
-
import { dirname as
|
|
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(
|
|
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
|
-
|
|
4569
|
+
mkdirSync8(dirname5(this.file), { recursive: true });
|
|
4315
4570
|
const temp = `${this.file}.${String(process.pid)}.${String(Date.now())}.tmp`;
|
|
4316
|
-
|
|
4571
|
+
writeFileSync7(temp, `${JSON.stringify(this.items, null, 2)}
|
|
4317
4572
|
`, { encoding: "utf8", mode: 384 });
|
|
4318
|
-
|
|
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
|
|
4376
|
-
import { dirname as
|
|
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(
|
|
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
|
-
|
|
4650
|
+
mkdirSync9(dirname6(this.file), { recursive: true });
|
|
4396
4651
|
const temp = `${this.file}.${String(process.pid)}.tmp`;
|
|
4397
|
-
|
|
4652
|
+
writeFileSync8(temp, `${JSON.stringify({ unread: [...this.load()] }, null, 2)}
|
|
4398
4653
|
`, {
|
|
4399
4654
|
encoding: "utf8",
|
|
4400
4655
|
mode: 384
|
|
4401
4656
|
});
|
|
4402
|
-
|
|
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);
|