@zaclinley/dsh-mailharbor 0.12.2 → 0.13.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -0
- package/lib/client.js +259 -18
- package/lib/client.js.map +2 -2
- package/lib/index.js +393 -37
- 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) => {
|
|
@@ -681,6 +751,7 @@ function normalizeAccount(input, index = 0, existing = []) {
|
|
|
681
751
|
dataDir: stringOr(input.dataDir, `accounts/${id}`),
|
|
682
752
|
auth: input.auth?.type === "oauth2" ? { type: "oauth2", provider: input.auth.provider } : { type: "password" },
|
|
683
753
|
caldav: normalizeCaldav(input.caldav),
|
|
754
|
+
signature: stringOr(input.signature, "").slice(0, 5e3),
|
|
684
755
|
enabled: input.enabled !== false,
|
|
685
756
|
createdAt: stringOr(input.createdAt, (/* @__PURE__ */ new Date()).toISOString())
|
|
686
757
|
};
|
|
@@ -920,6 +991,21 @@ async function openPath(path) {
|
|
|
920
991
|
await run("xdg-open", [path]);
|
|
921
992
|
}
|
|
922
993
|
|
|
994
|
+
// src/host/api.ts
|
|
995
|
+
import { existsSync as existsSync6, rmSync } from "node:fs";
|
|
996
|
+
|
|
997
|
+
// src/host/mail-write.ts
|
|
998
|
+
import { DatabaseSync } from "node:sqlite";
|
|
999
|
+
function moveLocalMessage(file, id, mailbox) {
|
|
1000
|
+
const db = new DatabaseSync(file);
|
|
1001
|
+
try {
|
|
1002
|
+
db.exec("PRAGMA busy_timeout = 5000");
|
|
1003
|
+
db.prepare("UPDATE emails SET mailbox = ? WHERE id = ?").run(mailbox, id);
|
|
1004
|
+
} finally {
|
|
1005
|
+
db.close();
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
|
|
923
1009
|
// src/host/backup.ts
|
|
924
1010
|
init_address();
|
|
925
1011
|
import { createHash as createHash2 } from "node:crypto";
|
|
@@ -927,12 +1013,13 @@ import { existsSync as existsSync3, mkdirSync as mkdirSync2, writeFileSync as wr
|
|
|
927
1013
|
import { join as join3 } from "node:path";
|
|
928
1014
|
import { ImapFlow } from "imapflow";
|
|
929
1015
|
import { simpleParser } from "mailparser";
|
|
930
|
-
import { DatabaseSync as
|
|
1016
|
+
import { DatabaseSync as DatabaseSync3 } from "node:sqlite";
|
|
931
1017
|
|
|
932
1018
|
// src/host/mail-db.ts
|
|
933
1019
|
init_address();
|
|
1020
|
+
init_inline_images();
|
|
934
1021
|
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
|
|
935
|
-
import { DatabaseSync } from "node:sqlite";
|
|
1022
|
+
import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
|
|
936
1023
|
var SCHEMA = `
|
|
937
1024
|
CREATE TABLE IF NOT EXISTS emails (
|
|
938
1025
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
@@ -990,7 +1077,7 @@ var FTS = `CREATE VIRTUAL TABLE IF NOT EXISTS emails_fts USING fts5(
|
|
|
990
1077
|
content='', tokenize='trigram'
|
|
991
1078
|
)`;
|
|
992
1079
|
function ensureMailSchema(file) {
|
|
993
|
-
const db = new
|
|
1080
|
+
const db = new DatabaseSync2(file);
|
|
994
1081
|
try {
|
|
995
1082
|
db.exec("PRAGMA journal_mode=WAL");
|
|
996
1083
|
db.exec(SCHEMA);
|
|
@@ -1029,7 +1116,7 @@ var MailDatabase = class {
|
|
|
1029
1116
|
open() {
|
|
1030
1117
|
if (this.db !== void 0) return this.db;
|
|
1031
1118
|
if (!existsSync2(this.file)) throw new Error(`\u90AE\u4EF6\u6570\u636E\u5E93\u4E0D\u5B58\u5728: ${this.file}`);
|
|
1032
|
-
this.db = new
|
|
1119
|
+
this.db = new DatabaseSync2(this.file, { readOnly: true });
|
|
1033
1120
|
this.db.exec("PRAGMA busy_timeout = 3000");
|
|
1034
1121
|
return this.db;
|
|
1035
1122
|
}
|
|
@@ -1204,6 +1291,50 @@ var MailDatabase = class {
|
|
|
1204
1291
|
const rows = db.prepare(`${columns} FROM emails e ${clause} ORDER BY e.date DESC, e.id DESC LIMIT ?`).all(...params, limit);
|
|
1205
1292
|
return { items: rows.map((row) => rowToSummary(row)), total, offset: 0, limit };
|
|
1206
1293
|
}
|
|
1294
|
+
/** Frequent senders and recipients, for compose autocomplete. */
|
|
1295
|
+
contacts(query = "", limit = 50) {
|
|
1296
|
+
const text2 = query.trim().toLowerCase();
|
|
1297
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1298
|
+
const add = (name2, address) => {
|
|
1299
|
+
const key = address.toLowerCase();
|
|
1300
|
+
if (key === "" || !key.includes("@")) return;
|
|
1301
|
+
const existing = counts.get(key);
|
|
1302
|
+
if (existing === void 0) counts.set(key, { name: name2, address, count: 1 });
|
|
1303
|
+
else {
|
|
1304
|
+
existing.count += 1;
|
|
1305
|
+
if (existing.name === "" && name2 !== "") existing.name = name2;
|
|
1306
|
+
}
|
|
1307
|
+
};
|
|
1308
|
+
try {
|
|
1309
|
+
const db = this.open();
|
|
1310
|
+
for (const row of db.prepare(
|
|
1311
|
+
`SELECT from_name AS name, from_addr AS address FROM emails
|
|
1312
|
+
WHERE from_addr <> '' AND from_addr LIKE '%@%' ORDER BY date DESC LIMIT 3000`
|
|
1313
|
+
).all()) {
|
|
1314
|
+
add(String(row.name ?? ""), String(row.address ?? ""));
|
|
1315
|
+
}
|
|
1316
|
+
for (const row of db.prepare(
|
|
1317
|
+
`SELECT to_addr AS address, cc AS copies FROM emails
|
|
1318
|
+
WHERE (to_addr <> '' OR cc <> '') ORDER BY date DESC LIMIT 1500`
|
|
1319
|
+
).all()) {
|
|
1320
|
+
for (const entry of parseAddressList(String(row.address ?? ""))) add(entry.name, entry.address);
|
|
1321
|
+
for (const entry of parseAddressList(String(row.copies ?? ""))) add(entry.name, entry.address);
|
|
1322
|
+
}
|
|
1323
|
+
} catch {
|
|
1324
|
+
return [];
|
|
1325
|
+
}
|
|
1326
|
+
return [...counts.values()].filter(
|
|
1327
|
+
(entry) => text2 === "" || entry.address.toLowerCase().includes(text2) || entry.name.toLowerCase().includes(text2)
|
|
1328
|
+
).sort((left, right) => right.count - left.count).slice(0, limit);
|
|
1329
|
+
}
|
|
1330
|
+
/** All archived messages in the same conversation (normalized subject). */
|
|
1331
|
+
thread(subject, limit = 200) {
|
|
1332
|
+
const normalize2 = (value) => value.replace(/^(\s*(re|fw|fwd|回复|转发)\s*[::]\s*)+/gi, "").replace(/\s+/g, " ").trim().toLowerCase();
|
|
1333
|
+
const base = normalize2(subject);
|
|
1334
|
+
if (base === "") return [];
|
|
1335
|
+
const page = this.list({ mailbox: "all", query: base, limit: Math.min(500, limit * 2) });
|
|
1336
|
+
return page.items.filter((item) => normalize2(item.subject) === base).slice(0, limit);
|
|
1337
|
+
}
|
|
1207
1338
|
/** Number of unread keys that still exist in this backup. */
|
|
1208
1339
|
countUnread(keys) {
|
|
1209
1340
|
const mids = keys.mids.slice(0, 500);
|
|
@@ -1234,9 +1365,14 @@ var MailDatabase = class {
|
|
|
1234
1365
|
FROM emails WHERE id = ?`
|
|
1235
1366
|
).get(localId);
|
|
1236
1367
|
if (row === void 0) return void 0;
|
|
1237
|
-
const
|
|
1368
|
+
const bodyHtml = String(row.body_html ?? "");
|
|
1369
|
+
const cidText = bodyHtml.toLowerCase();
|
|
1370
|
+
const wantsInline = cidText.includes("cid:");
|
|
1238
1371
|
const attachmentColumns = this.hasAttachmentColumns() ? "id, filename, content_type, size, content_id, disposition" : "id, filename, content_type, size";
|
|
1239
|
-
const
|
|
1372
|
+
const rows = db.prepare(
|
|
1373
|
+
`SELECT ${attachmentColumns}${wantsInline ? ", data, hash" : ""} FROM attachments WHERE email_id = ? ORDER BY id`
|
|
1374
|
+
).all(localId);
|
|
1375
|
+
const attachments = rows.filter((item) => {
|
|
1240
1376
|
const type = String(item.content_type ?? "").toLowerCase();
|
|
1241
1377
|
if (!type.startsWith("image/")) return true;
|
|
1242
1378
|
const disposition = String(item.disposition ?? "").toLowerCase();
|
|
@@ -1255,17 +1391,38 @@ var MailDatabase = class {
|
|
|
1255
1391
|
}));
|
|
1256
1392
|
const max = this.options.maxBodyChars ?? 4e5;
|
|
1257
1393
|
const bodyText2 = String(row.body_text ?? "");
|
|
1258
|
-
const
|
|
1394
|
+
const inlineImages = wantsInline ? rows.filter(
|
|
1395
|
+
(item) => String(item.content_type ?? "").toLowerCase().startsWith("image/")
|
|
1396
|
+
).map((item) => ({
|
|
1397
|
+
cid: item.content_id === null || item.content_id === void 0 ? void 0 : String(item.content_id),
|
|
1398
|
+
filename: String(item.filename ?? ""),
|
|
1399
|
+
contentType: String(item.content_type ?? "image/png"),
|
|
1400
|
+
data: this.attachmentBytes(item)
|
|
1401
|
+
})).filter((item) => item.data !== void 0) : [];
|
|
1259
1402
|
return {
|
|
1260
1403
|
...rowToSummary(row),
|
|
1261
1404
|
cc: parseAddressList(String(row.cc ?? "")),
|
|
1262
1405
|
replyTo: [],
|
|
1263
1406
|
text: truncate(bodyText2, max),
|
|
1264
|
-
html: bodyHtml === "" ? null : truncate(bodyHtml, max * 2),
|
|
1407
|
+
html: bodyHtml === "" ? null : truncate(inlineCidImages(bodyHtml, inlineImages), max * 2),
|
|
1265
1408
|
attachments,
|
|
1266
1409
|
references: []
|
|
1267
1410
|
};
|
|
1268
1411
|
}
|
|
1412
|
+
attachmentBytes(row) {
|
|
1413
|
+
const data = row.data;
|
|
1414
|
+
if (data instanceof Uint8Array) return data;
|
|
1415
|
+
const hash = typeof row.hash === "string" ? row.hash : "";
|
|
1416
|
+
const root = this.options.attachmentsRoot ?? "";
|
|
1417
|
+
if (hash === "" || root === "") return void 0;
|
|
1418
|
+
const file = `${root}/${hash.slice(0, 2)}/${hash.slice(2, 4)}/${hash}`;
|
|
1419
|
+
if (!existsSync2(file)) return void 0;
|
|
1420
|
+
try {
|
|
1421
|
+
return readFileSyncSafe(file) ?? void 0;
|
|
1422
|
+
} catch {
|
|
1423
|
+
return void 0;
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1269
1426
|
/** Read one attachment's bytes; the payload may live in a file (content-addressed). */
|
|
1270
1427
|
attachment(emailId, attachmentId) {
|
|
1271
1428
|
const db = this.open();
|
|
@@ -1462,7 +1619,7 @@ var BackupEngine = class {
|
|
|
1462
1619
|
}
|
|
1463
1620
|
openDb() {
|
|
1464
1621
|
ensureMailSchema(this.runtime.mailDb);
|
|
1465
|
-
const db = new
|
|
1622
|
+
const db = new DatabaseSync3(this.runtime.mailDb);
|
|
1466
1623
|
db.exec("PRAGMA journal_mode=WAL");
|
|
1467
1624
|
db.exec("PRAGMA busy_timeout=10000");
|
|
1468
1625
|
db.exec("PRAGMA synchronous=NORMAL");
|
|
@@ -1714,7 +1871,7 @@ var BackupEngine = class {
|
|
|
1714
1871
|
};
|
|
1715
1872
|
if (!existsSync3(dbFile)) return empty;
|
|
1716
1873
|
try {
|
|
1717
|
-
const db = new
|
|
1874
|
+
const db = new DatabaseSync3(dbFile, { readOnly: true });
|
|
1718
1875
|
try {
|
|
1719
1876
|
const latest = db.prepare("SELECT * FROM backup_logs ORDER BY id DESC LIMIT 1").get();
|
|
1720
1877
|
const success = db.prepare("SELECT completed_at FROM backup_logs WHERE status = 'completed' ORDER BY id DESC LIMIT 1").get();
|
|
@@ -2590,7 +2747,7 @@ ${conversation.trim()}
|
|
|
2590
2747
|
}
|
|
2591
2748
|
|
|
2592
2749
|
// src/host/version.ts
|
|
2593
|
-
var VERSION = "0.
|
|
2750
|
+
var VERSION = "0.13.1";
|
|
2594
2751
|
|
|
2595
2752
|
// src/host/api.ts
|
|
2596
2753
|
init_smtp();
|
|
@@ -2719,6 +2876,102 @@ function buildRouter(runtime) {
|
|
|
2719
2876
|
}
|
|
2720
2877
|
throw new HttpError(400, "\u65E0\u6CD5\u5B9A\u4F4D\u8FD9\u5C01\u90AE\u4EF6");
|
|
2721
2878
|
});
|
|
2879
|
+
router.post("/messages/:id/star", async ({ url, params, body }) => {
|
|
2880
|
+
const payload = await body();
|
|
2881
|
+
const { account } = accountFrom(runtime, url);
|
|
2882
|
+
const detail = await loadDetail(
|
|
2883
|
+
runtime,
|
|
2884
|
+
account,
|
|
2885
|
+
sourceOfPayload(payload),
|
|
2886
|
+
String(params.id ?? ""),
|
|
2887
|
+
mailboxOfPayload(payload)
|
|
2888
|
+
);
|
|
2889
|
+
const starred = payload.starred !== false;
|
|
2890
|
+
const key = messageKeyOf(detail);
|
|
2891
|
+
runtime.flags(account).set(key, starred);
|
|
2892
|
+
if (detail.uid !== void 0) {
|
|
2893
|
+
try {
|
|
2894
|
+
await runtime.provider(account).setFlag(detail.mailbox, detail.uid, "\\Flagged", starred);
|
|
2895
|
+
} catch {
|
|
2896
|
+
}
|
|
2897
|
+
}
|
|
2898
|
+
return { starred };
|
|
2899
|
+
});
|
|
2900
|
+
router.post("/messages/:id/move", async ({ url, params, body }) => {
|
|
2901
|
+
const payload = await body();
|
|
2902
|
+
const { account } = accountFrom(runtime, url);
|
|
2903
|
+
const target = String(payload.target ?? "").trim();
|
|
2904
|
+
if (target === "") throw new HttpError(400, "\u7F3A\u5C11\u76EE\u6807\u6587\u4EF6\u5939");
|
|
2905
|
+
const detail = runtime.database(account).get(Number(params.id));
|
|
2906
|
+
if (detail === void 0) throw new HttpError(404, "\u90AE\u4EF6\u4E0D\u5B58\u5728");
|
|
2907
|
+
if (detail.uid !== void 0) {
|
|
2908
|
+
try {
|
|
2909
|
+
await runtime.provider(account).move(detail.mailbox, detail.uid, target);
|
|
2910
|
+
} catch {
|
|
2911
|
+
}
|
|
2912
|
+
}
|
|
2913
|
+
moveLocalMessage(runtime.paths(account).mailDb, Number(params.id), target);
|
|
2914
|
+
return { ok: true, mailbox: target };
|
|
2915
|
+
});
|
|
2916
|
+
router.post("/messages/bulk", async ({ body }) => {
|
|
2917
|
+
const payload = await body();
|
|
2918
|
+
const account = accountFromPayload(runtime, payload);
|
|
2919
|
+
const action = String(payload.action ?? "");
|
|
2920
|
+
const ids = Array.isArray(payload.ids) ? payload.ids.map((id) => Number(id)) : [];
|
|
2921
|
+
if (ids.length === 0) return { ok: true, count: 0 };
|
|
2922
|
+
const database = runtime.database(account);
|
|
2923
|
+
const flags = runtime.flags(account);
|
|
2924
|
+
const readState = runtime.readState(account);
|
|
2925
|
+
let count = 0;
|
|
2926
|
+
for (const id of ids) {
|
|
2927
|
+
const detail = database.get(id);
|
|
2928
|
+
if (detail === void 0) continue;
|
|
2929
|
+
const key = messageKeyOf(detail);
|
|
2930
|
+
if (action === "read" || action === "unread") {
|
|
2931
|
+
readState.setUnread(key, action === "unread");
|
|
2932
|
+
if (detail.uid !== void 0) {
|
|
2933
|
+
try {
|
|
2934
|
+
await runtime.provider(account).setSeen(detail.mailbox, detail.uid, action === "read");
|
|
2935
|
+
} catch {
|
|
2936
|
+
}
|
|
2937
|
+
}
|
|
2938
|
+
count += 1;
|
|
2939
|
+
} else if (action === "archive" || action === "delete" || action === "move") {
|
|
2940
|
+
const target = String(payload.target ?? "").trim();
|
|
2941
|
+
if (target === "") continue;
|
|
2942
|
+
if (detail.uid !== void 0) {
|
|
2943
|
+
try {
|
|
2944
|
+
await runtime.provider(account).move(detail.mailbox, detail.uid, target);
|
|
2945
|
+
} catch {
|
|
2946
|
+
}
|
|
2947
|
+
}
|
|
2948
|
+
moveLocalMessage(runtime.paths(account).mailDb, Number(detail.id), target);
|
|
2949
|
+
count += 1;
|
|
2950
|
+
} else if (action === "star" || action === "unstar") {
|
|
2951
|
+
const starred = action === "star";
|
|
2952
|
+
flags.set(key, starred);
|
|
2953
|
+
if (detail.uid !== void 0) {
|
|
2954
|
+
try {
|
|
2955
|
+
await runtime.provider(account).setFlag(detail.mailbox, detail.uid, "\\Flagged", starred);
|
|
2956
|
+
} catch {
|
|
2957
|
+
}
|
|
2958
|
+
}
|
|
2959
|
+
count += 1;
|
|
2960
|
+
}
|
|
2961
|
+
}
|
|
2962
|
+
return { ok: true, count };
|
|
2963
|
+
});
|
|
2964
|
+
router.get("/messages/thread", ({ url }) => {
|
|
2965
|
+
const { account } = accountFrom(runtime, url);
|
|
2966
|
+
const subject = url.searchParams.get("subject") ?? "";
|
|
2967
|
+
const items = runtime.database(account).thread(subject);
|
|
2968
|
+
return { items: items.map((item) => localSummary(runtime, account, item)) };
|
|
2969
|
+
});
|
|
2970
|
+
router.get("/contacts", ({ url }) => {
|
|
2971
|
+
const { account } = accountFrom(runtime, url);
|
|
2972
|
+
const query = url.searchParams.get("q") ?? "";
|
|
2973
|
+
return { items: runtime.database(account).contacts(query, 50) };
|
|
2974
|
+
});
|
|
2722
2975
|
router.get("/messages/:id", async ({ url, params }) => {
|
|
2723
2976
|
const { account } = accountFrom(runtime, url);
|
|
2724
2977
|
const source = sourceOf(url);
|
|
@@ -2841,6 +3094,7 @@ function buildRouter(runtime) {
|
|
|
2841
3094
|
imap: { ...existing.imap, ...asObject(payload.imap) },
|
|
2842
3095
|
smtp: { ...existing.smtp, ...asObject(payload.smtp) },
|
|
2843
3096
|
caldav: payload.caldav === null ? null : payload.caldav === void 0 ? existing.caldav : { ...asObject(payload.caldav) },
|
|
3097
|
+
signature: typeof payload.signature === "string" ? payload.signature.slice(0, 5e3) : existing.signature,
|
|
2844
3098
|
enabled: payload.enabled === void 0 ? existing.enabled : payload.enabled !== false
|
|
2845
3099
|
};
|
|
2846
3100
|
const account = normalizeAccount(
|
|
@@ -3249,17 +3503,48 @@ function buildRouter(runtime) {
|
|
|
3249
3503
|
if (project === void 0) throw new HttpError(404, "\u9879\u76EE\u4E0D\u5B58\u5728");
|
|
3250
3504
|
return { project: projectView(project) };
|
|
3251
3505
|
});
|
|
3252
|
-
router.delete("/projects/:id", ({ params }) => {
|
|
3506
|
+
router.delete("/projects/:id", async ({ params, url }) => {
|
|
3253
3507
|
const id = String(params.id ?? "");
|
|
3254
3508
|
const project = runtime.projects().get(id);
|
|
3255
3509
|
if (project === void 0) throw new HttpError(404, "\u9879\u76EE\u4E0D\u5B58\u5728");
|
|
3510
|
+
const warnings = [];
|
|
3256
3511
|
const removed = runtime.projects().remove(id);
|
|
3257
3512
|
const unlinked = runtime.tasks().clearProject(id);
|
|
3513
|
+
let workspaceRemoved = false;
|
|
3514
|
+
if (project.workspaceId !== "" && runtime.workspaceRegistry?.delete !== void 0) {
|
|
3515
|
+
try {
|
|
3516
|
+
workspaceRemoved = await runtime.workspaceRegistry.delete(project.workspaceId);
|
|
3517
|
+
} catch (error) {
|
|
3518
|
+
warnings.push(`\u79FB\u9664\u5DE5\u4F5C\u7A7A\u95F4\u5931\u8D25\uFF1A${error instanceof Error ? error.message : String(error)}`);
|
|
3519
|
+
}
|
|
3520
|
+
} else if (project.workspaceId !== "") {
|
|
3521
|
+
warnings.push("\u5F53\u524D DSH \u7EC4\u5408\u4E0D\u652F\u6301\u81EA\u52A8\u79FB\u9664\u5DE5\u4F5C\u7A7A\u95F4\uFF0C\u8BF7\u5728\u4FA7\u680F\u624B\u52A8\u5220\u9664");
|
|
3522
|
+
}
|
|
3523
|
+
let filesRemoved = false;
|
|
3524
|
+
if (url.searchParams.get("files") === "1") {
|
|
3525
|
+
try {
|
|
3526
|
+
const root = runtime.current.projects.root;
|
|
3527
|
+
const insideRoot = project.path === root || project.path.startsWith(`${root}/`);
|
|
3528
|
+
const marker = existsSync6(`${project.path}/project.json`);
|
|
3529
|
+
if (!insideRoot && !marker) {
|
|
3530
|
+
warnings.push("\u9879\u76EE\u76EE\u5F55\u4E0D\u5728\u9ED8\u8BA4\u9879\u76EE\u6839\u76EE\u5F55\u4E0B\uFF0C\u51FA\u4E8E\u5B89\u5168\u8003\u8651\u672A\u5220\u9664\u6587\u4EF6");
|
|
3531
|
+
} else if (project.path === root || project.path === "/") {
|
|
3532
|
+
warnings.push("\u62D2\u7EDD\u5220\u9664\u9879\u76EE\u6839\u76EE\u5F55");
|
|
3533
|
+
} else {
|
|
3534
|
+
rmSync(project.path, { recursive: true, force: true });
|
|
3535
|
+
filesRemoved = true;
|
|
3536
|
+
}
|
|
3537
|
+
} catch (error) {
|
|
3538
|
+
warnings.push(`\u5220\u9664\u6587\u4EF6\u5939\u5931\u8D25\uFF1A${error instanceof Error ? error.message : String(error)}`);
|
|
3539
|
+
}
|
|
3540
|
+
}
|
|
3258
3541
|
return {
|
|
3259
3542
|
ok: removed,
|
|
3260
3543
|
path: project.path,
|
|
3261
3544
|
unlinkedTasks: unlinked,
|
|
3262
|
-
|
|
3545
|
+
workspaceRemoved,
|
|
3546
|
+
filesRemoved,
|
|
3547
|
+
warnings
|
|
3263
3548
|
};
|
|
3264
3549
|
});
|
|
3265
3550
|
router.post("/projects/:id/open", async ({ params }) => {
|
|
@@ -3507,10 +3792,19 @@ function unreadKeys(runtime, account) {
|
|
|
3507
3792
|
}
|
|
3508
3793
|
return { mids, ids };
|
|
3509
3794
|
}
|
|
3795
|
+
function messageKeyOf(row) {
|
|
3796
|
+
return row.messageId !== void 0 && row.messageId !== "" ? `mid:${row.messageId}` : `local:${row.id}`;
|
|
3797
|
+
}
|
|
3510
3798
|
function localSummary(runtime, account, row) {
|
|
3511
3799
|
const store = runtime.readState(account);
|
|
3512
|
-
const key = row
|
|
3513
|
-
return {
|
|
3800
|
+
const key = messageKeyOf(row);
|
|
3801
|
+
return {
|
|
3802
|
+
...row,
|
|
3803
|
+
accountId: account.id,
|
|
3804
|
+
unread: store.isUnread(key),
|
|
3805
|
+
starred: runtime.flags(account).has(key),
|
|
3806
|
+
backedUp: true
|
|
3807
|
+
};
|
|
3514
3808
|
}
|
|
3515
3809
|
function localDetail(runtime, account, detail) {
|
|
3516
3810
|
return localSummary(runtime, account, detail);
|
|
@@ -3632,7 +3926,7 @@ async function sendFromPayload(runtime, account, payload) {
|
|
|
3632
3926
|
const totalBytes = attachments.reduce((sum, file) => sum + file.content.length, 0);
|
|
3633
3927
|
if (totalBytes > 15 * 1024 * 1024) throw new HttpError(413, "\u9644\u4EF6\u5408\u8BA1\u4E0D\u80FD\u8D85\u8FC7 15 MB");
|
|
3634
3928
|
const { sendMail: sendMail2 } = await Promise.resolve().then(() => (init_smtp(), smtp_exports));
|
|
3635
|
-
const signature = runtime.current.signature;
|
|
3929
|
+
const signature = (account.signature ?? "").trim() !== "" ? String(account.signature).trim() : runtime.current.signature;
|
|
3636
3930
|
const bodyText2 = signature === "" ? text2 : `${text2}
|
|
3637
3931
|
|
|
3638
3932
|
--
|
|
@@ -3848,7 +4142,7 @@ var CredentialManager = class {
|
|
|
3848
4142
|
};
|
|
3849
4143
|
|
|
3850
4144
|
// src/host/ai-store.ts
|
|
3851
|
-
import { existsSync as
|
|
4145
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync5, readFileSync as readFileSync5, renameSync as renameSync4, writeFileSync as writeFileSync5 } from "node:fs";
|
|
3852
4146
|
import { dirname as dirname2 } from "node:path";
|
|
3853
4147
|
var MAX_THREADS = 50;
|
|
3854
4148
|
var MAX_CARDS_PER_THREAD = 50;
|
|
@@ -3863,7 +4157,7 @@ var AiThreadStore = class {
|
|
|
3863
4157
|
if (this.cache !== null) return this.cache;
|
|
3864
4158
|
let parsed = null;
|
|
3865
4159
|
try {
|
|
3866
|
-
if (
|
|
4160
|
+
if (existsSync7(this.file)) parsed = JSON.parse(readFileSync5(this.file, "utf8"));
|
|
3867
4161
|
} catch {
|
|
3868
4162
|
parsed = null;
|
|
3869
4163
|
}
|
|
@@ -3987,11 +4281,63 @@ function prune(data) {
|
|
|
3987
4281
|
}
|
|
3988
4282
|
}
|
|
3989
4283
|
|
|
4284
|
+
// src/host/mail-flags.ts
|
|
4285
|
+
import { mkdirSync as mkdirSync6, readFileSync as readFileSync6, renameSync as renameSync5, writeFileSync as writeFileSync6 } from "node:fs";
|
|
4286
|
+
import { dirname as dirname3 } from "node:path";
|
|
4287
|
+
var MailFlagsStore = class {
|
|
4288
|
+
file;
|
|
4289
|
+
keys = null;
|
|
4290
|
+
constructor(file) {
|
|
4291
|
+
this.file = file;
|
|
4292
|
+
}
|
|
4293
|
+
load() {
|
|
4294
|
+
if (this.keys !== null) return this.keys;
|
|
4295
|
+
try {
|
|
4296
|
+
const parsed = JSON.parse(readFileSync6(this.file, "utf8"));
|
|
4297
|
+
const list = Array.isArray(parsed) ? parsed : Array.isArray(parsed?.starred) ? parsed.starred : [];
|
|
4298
|
+
this.keys = new Set(list.filter((key) => typeof key === "string" && key !== ""));
|
|
4299
|
+
} catch {
|
|
4300
|
+
this.keys = /* @__PURE__ */ new Set();
|
|
4301
|
+
}
|
|
4302
|
+
return this.keys;
|
|
4303
|
+
}
|
|
4304
|
+
flush() {
|
|
4305
|
+
mkdirSync6(dirname3(this.file), { recursive: true });
|
|
4306
|
+
const temp = `${this.file}.${String(process.pid)}.tmp`;
|
|
4307
|
+
writeFileSync6(temp, `${JSON.stringify({ starred: [...this.load()] }, null, 2)}
|
|
4308
|
+
`, {
|
|
4309
|
+
encoding: "utf8",
|
|
4310
|
+
mode: 384
|
|
4311
|
+
});
|
|
4312
|
+
renameSync5(temp, this.file);
|
|
4313
|
+
}
|
|
4314
|
+
has(key) {
|
|
4315
|
+
return key !== "" && this.load().has(key);
|
|
4316
|
+
}
|
|
4317
|
+
set(key, starred) {
|
|
4318
|
+
const normalized = key.trim();
|
|
4319
|
+
if (normalized === "") return false;
|
|
4320
|
+
const keys = this.load();
|
|
4321
|
+
const has = keys.has(normalized);
|
|
4322
|
+
if (starred && !has) {
|
|
4323
|
+
keys.add(normalized);
|
|
4324
|
+
this.flush();
|
|
4325
|
+
return true;
|
|
4326
|
+
}
|
|
4327
|
+
if (!starred && has) {
|
|
4328
|
+
keys.delete(normalized);
|
|
4329
|
+
this.flush();
|
|
4330
|
+
return true;
|
|
4331
|
+
}
|
|
4332
|
+
return false;
|
|
4333
|
+
}
|
|
4334
|
+
};
|
|
4335
|
+
|
|
3990
4336
|
// src/host/tasks.ts
|
|
3991
4337
|
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
3992
|
-
import { mkdirSync as
|
|
3993
|
-
import { dirname as
|
|
3994
|
-
import { DatabaseSync as
|
|
4338
|
+
import { mkdirSync as mkdirSync7 } from "node:fs";
|
|
4339
|
+
import { dirname as dirname4 } from "node:path";
|
|
4340
|
+
import { DatabaseSync as DatabaseSync4 } from "node:sqlite";
|
|
3995
4341
|
var STATUSES = /* @__PURE__ */ new Set(["NEEDS-ACTION", "IN-PROCESS", "COMPLETED", "CANCELLED"]);
|
|
3996
4342
|
function now() {
|
|
3997
4343
|
return (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -4039,8 +4385,8 @@ var TaskStore = class {
|
|
|
4039
4385
|
}
|
|
4040
4386
|
open() {
|
|
4041
4387
|
if (this.db !== null) return this.db;
|
|
4042
|
-
|
|
4043
|
-
const db = new
|
|
4388
|
+
mkdirSync7(dirname4(this.file), { recursive: true });
|
|
4389
|
+
const db = new DatabaseSync4(this.file);
|
|
4044
4390
|
db.exec("PRAGMA journal_mode=WAL");
|
|
4045
4391
|
db.exec("PRAGMA busy_timeout=10000");
|
|
4046
4392
|
db.exec(`
|
|
@@ -4279,8 +4625,8 @@ var TaskStore = class {
|
|
|
4279
4625
|
|
|
4280
4626
|
// src/host/drafts.ts
|
|
4281
4627
|
import { randomUUID as randomUUID5 } from "node:crypto";
|
|
4282
|
-
import { mkdirSync as
|
|
4283
|
-
import { dirname as
|
|
4628
|
+
import { mkdirSync as mkdirSync8, readFileSync as readFileSync7, renameSync as renameSync6, writeFileSync as writeFileSync7 } from "node:fs";
|
|
4629
|
+
import { dirname as dirname5, join as join6 } from "node:path";
|
|
4284
4630
|
function text(value) {
|
|
4285
4631
|
return value === void 0 || value === null ? "" : String(value);
|
|
4286
4632
|
}
|
|
@@ -4304,18 +4650,18 @@ var DraftStore = class {
|
|
|
4304
4650
|
}
|
|
4305
4651
|
load() {
|
|
4306
4652
|
try {
|
|
4307
|
-
const parsed = JSON.parse(
|
|
4653
|
+
const parsed = JSON.parse(readFileSync7(this.file, "utf8"));
|
|
4308
4654
|
this.items = Array.isArray(parsed) ? parsed.filter(isDraft) : [];
|
|
4309
4655
|
} catch {
|
|
4310
4656
|
this.items = [];
|
|
4311
4657
|
}
|
|
4312
4658
|
}
|
|
4313
4659
|
flush() {
|
|
4314
|
-
|
|
4660
|
+
mkdirSync8(dirname5(this.file), { recursive: true });
|
|
4315
4661
|
const temp = `${this.file}.${String(process.pid)}.${String(Date.now())}.tmp`;
|
|
4316
|
-
|
|
4662
|
+
writeFileSync7(temp, `${JSON.stringify(this.items, null, 2)}
|
|
4317
4663
|
`, { encoding: "utf8", mode: 384 });
|
|
4318
|
-
|
|
4664
|
+
renameSync6(temp, this.file);
|
|
4319
4665
|
}
|
|
4320
4666
|
list() {
|
|
4321
4667
|
return [...this.items].sort((left, right) => right.updatedAt.localeCompare(left.updatedAt)).map((draft) => ({ ...draft }));
|
|
@@ -4372,8 +4718,8 @@ var DraftStore = class {
|
|
|
4372
4718
|
init_imap();
|
|
4373
4719
|
|
|
4374
4720
|
// src/host/read-state.ts
|
|
4375
|
-
import { mkdirSync as
|
|
4376
|
-
import { dirname as
|
|
4721
|
+
import { mkdirSync as mkdirSync9, readFileSync as readFileSync8, renameSync as renameSync7, writeFileSync as writeFileSync8 } from "node:fs";
|
|
4722
|
+
import { dirname as dirname6 } from "node:path";
|
|
4377
4723
|
var ReadStateStore = class {
|
|
4378
4724
|
file;
|
|
4379
4725
|
keys = null;
|
|
@@ -4383,7 +4729,7 @@ var ReadStateStore = class {
|
|
|
4383
4729
|
load() {
|
|
4384
4730
|
if (this.keys !== null) return this.keys;
|
|
4385
4731
|
try {
|
|
4386
|
-
const parsed = JSON.parse(
|
|
4732
|
+
const parsed = JSON.parse(readFileSync8(this.file, "utf8"));
|
|
4387
4733
|
const list = Array.isArray(parsed) ? parsed : Array.isArray(parsed?.unread) ? parsed.unread : [];
|
|
4388
4734
|
this.keys = new Set(list.filter((key) => typeof key === "string" && key !== ""));
|
|
4389
4735
|
} catch {
|
|
@@ -4392,14 +4738,14 @@ var ReadStateStore = class {
|
|
|
4392
4738
|
return this.keys;
|
|
4393
4739
|
}
|
|
4394
4740
|
flush() {
|
|
4395
|
-
|
|
4741
|
+
mkdirSync9(dirname6(this.file), { recursive: true });
|
|
4396
4742
|
const temp = `${this.file}.${String(process.pid)}.tmp`;
|
|
4397
|
-
|
|
4743
|
+
writeFileSync8(temp, `${JSON.stringify({ unread: [...this.load()] }, null, 2)}
|
|
4398
4744
|
`, {
|
|
4399
4745
|
encoding: "utf8",
|
|
4400
4746
|
mode: 384
|
|
4401
4747
|
});
|
|
4402
|
-
|
|
4748
|
+
renameSync7(temp, this.file);
|
|
4403
4749
|
}
|
|
4404
4750
|
isUnread(key) {
|
|
4405
4751
|
return key !== "" && this.load().has(key);
|
|
@@ -4463,6 +4809,7 @@ var MailHarborRuntime = class {
|
|
|
4463
4809
|
aiService = null;
|
|
4464
4810
|
projectStore = null;
|
|
4465
4811
|
taskStore = null;
|
|
4812
|
+
flagStores = /* @__PURE__ */ new Map();
|
|
4466
4813
|
disposed = false;
|
|
4467
4814
|
constructor(options = {}) {
|
|
4468
4815
|
this.config = loadConfig();
|
|
@@ -4617,6 +4964,14 @@ var MailHarborRuntime = class {
|
|
|
4617
4964
|
this.aiStores.set(account.id, { signature: file, value: created });
|
|
4618
4965
|
return created;
|
|
4619
4966
|
}
|
|
4967
|
+
flags(account) {
|
|
4968
|
+
const file = join7(this.paths(account).root, "starred.json");
|
|
4969
|
+
const existing = this.flagStores.get(account.id);
|
|
4970
|
+
if (existing !== void 0 && existing.signature === file) return existing.value;
|
|
4971
|
+
const created = new MailFlagsStore(file);
|
|
4972
|
+
this.flagStores.set(account.id, { signature: file, value: created });
|
|
4973
|
+
return created;
|
|
4974
|
+
}
|
|
4620
4975
|
readState(account) {
|
|
4621
4976
|
const file = join7(this.paths(account).root, "read-state.json");
|
|
4622
4977
|
const existing = this.readStores.get(account.id);
|
|
@@ -4636,12 +4991,13 @@ var MailHarborRuntime = class {
|
|
|
4636
4991
|
const running = this.backups.get(account.id);
|
|
4637
4992
|
if (running !== void 0 && running.status.running) throw new Error("\u5907\u4EFD\u5DF2\u5728\u8FDB\u884C\u4E2D");
|
|
4638
4993
|
const controller = new AbortController();
|
|
4994
|
+
const previous = this.backups.get(account.id)?.status;
|
|
4639
4995
|
const status = {
|
|
4640
4996
|
running: true,
|
|
4641
4997
|
accountId: account.id,
|
|
4642
4998
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4643
4999
|
finishedAt: null,
|
|
4644
|
-
lastSuccessAt: null,
|
|
5000
|
+
lastSuccessAt: previous?.lastSuccessAt ?? null,
|
|
4645
5001
|
total: 0,
|
|
4646
5002
|
added: 0,
|
|
4647
5003
|
message: "\u51C6\u5907\u5907\u4EFD",
|