@zaclinley/dsh-mailharbor 0.19.1 → 0.21.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 +32 -0
- package/README.zh.md +17 -17
- package/lib/client.js +252 -26
- package/lib/client.js.map +3 -3
- package/lib/index.js +395 -145
- package/lib/index.js.map +4 -4
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -989,7 +989,8 @@ async function openPath(path) {
|
|
|
989
989
|
}
|
|
990
990
|
|
|
991
991
|
// src/host/api.ts
|
|
992
|
-
import { existsSync as existsSync6, rmSync } from "node:fs";
|
|
992
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync5, rmSync, writeFileSync as writeFileSync5 } from "node:fs";
|
|
993
|
+
import { basename, join as join6 } from "node:path";
|
|
993
994
|
|
|
994
995
|
// src/host/mail-write.ts
|
|
995
996
|
import { DatabaseSync } from "node:sqlite";
|
|
@@ -1027,20 +1028,21 @@ async function draftMime(account, draft) {
|
|
|
1027
1028
|
return Buffer.from(info.message);
|
|
1028
1029
|
}
|
|
1029
1030
|
|
|
1030
|
-
// src/host/
|
|
1031
|
-
init_address();
|
|
1032
|
-
import { createHash as createHash2 } from "node:crypto";
|
|
1033
|
-
import { existsSync as existsSync3, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
1034
|
-
import { join as join3 } from "node:path";
|
|
1035
|
-
import { ImapFlow } from "imapflow";
|
|
1036
|
-
import { simpleParser } from "mailparser";
|
|
1031
|
+
// src/host/references.ts
|
|
1037
1032
|
import { DatabaseSync as DatabaseSync3 } from "node:sqlite";
|
|
1038
1033
|
|
|
1039
1034
|
// src/host/mail-db.ts
|
|
1040
1035
|
init_address();
|
|
1041
|
-
init_inline_images();
|
|
1042
1036
|
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
|
|
1043
1037
|
import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
|
|
1038
|
+
|
|
1039
|
+
// src/host/subject-key.ts
|
|
1040
|
+
function subjectKey(subject) {
|
|
1041
|
+
return subject.replace(/^(\s*(re|fw|fwd|回复|答复|转发)\s*[::]\s*)+/gi, "").replace(/\s+/g, " ").trim().toLowerCase();
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
// src/host/mail-db.ts
|
|
1045
|
+
init_inline_images();
|
|
1044
1046
|
var SCHEMA = `
|
|
1045
1047
|
CREATE TABLE IF NOT EXISTS emails (
|
|
1046
1048
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
@@ -1125,6 +1127,8 @@ function ensureMailSchema(file) {
|
|
|
1125
1127
|
db.exec("ALTER TABLE emails ADD COLUMN refs_indexed INTEGER DEFAULT 0");
|
|
1126
1128
|
}
|
|
1127
1129
|
try {
|
|
1130
|
+
db.exec("CREATE TABLE IF NOT EXISTS email_subjects (email_id INTEGER PRIMARY KEY, key TEXT NOT NULL)");
|
|
1131
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_email_subjects_key ON email_subjects(key)");
|
|
1128
1132
|
db.exec("CREATE INDEX IF NOT EXISTS idx_emails_in_reply ON emails(in_reply_to)");
|
|
1129
1133
|
db.exec("CREATE INDEX IF NOT EXISTS idx_emails_message_id_lower ON emails(lower(message_id))");
|
|
1130
1134
|
} catch {
|
|
@@ -1463,64 +1467,45 @@ var MailDatabase = class {
|
|
|
1463
1467
|
this.threadCache.set(cacheKey, { at: Date.now(), items: result });
|
|
1464
1468
|
return result;
|
|
1465
1469
|
}
|
|
1466
|
-
/** Subject candidates
|
|
1470
|
+
/** Subject candidates from a normalized-key table, then FTS, then LIKE. */
|
|
1467
1471
|
threadCandidates(base, limit, seedId = "") {
|
|
1468
|
-
const normalize2 = (value) => value.replace(/^(\s*(re|fw|fwd|回复|转发)\s*[::]\s*)+/gi, "").replace(/\s+/g, " ").trim().toLowerCase();
|
|
1469
|
-
const words = base.split(/[^\p{L}\p{N}]+/u).filter((word) => word.length >= 3);
|
|
1470
|
-
const token = words.sort((left, right) => right.length - left.length)[0] ?? base.slice(0, 8);
|
|
1471
1472
|
const select = `SELECT e.id, e.mailbox, e.uid, e.subject, e.from_name, e.from_addr, e.to_addr, e.cc,
|
|
1472
1473
|
e.message_id, e.date, length(e.body_text) AS text_length,
|
|
1473
1474
|
(SELECT COUNT(*) FROM attachments a WHERE a.email_id = e.id) AS attachment_count,
|
|
1474
1475
|
substr(COALESCE(e.body_text, ''), 1, 240) AS snippet
|
|
1475
1476
|
FROM emails e`;
|
|
1477
|
+
let items = [];
|
|
1476
1478
|
try {
|
|
1477
|
-
const
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
WHERE emails_fts MATCH ?
|
|
1479
|
+
const byKey = this.open().prepare(
|
|
1480
|
+
`${select} JOIN email_subjects s ON s.email_id = e.id
|
|
1481
|
+
WHERE s.key = ?
|
|
1481
1482
|
ORDER BY e.date DESC LIMIT 500`
|
|
1482
|
-
).all(
|
|
1483
|
-
|
|
1484
|
-
|
|
1483
|
+
).all(base);
|
|
1484
|
+
if (byKey.length > 0) {
|
|
1485
|
+
items = byKey.map((row) => rowToSummary(row));
|
|
1486
|
+
} else {
|
|
1487
|
+
const words = base.split(/[^\p{L}\p{N}]+/u).filter((word) => word.length >= 3);
|
|
1488
|
+
const token = words.sort((left, right) => right.length - left.length)[0] ?? base.slice(0, 8);
|
|
1489
|
+
const ftsQuery = `subject:"${token.replace(/"/g, '""')}"`;
|
|
1490
|
+
const rows = this.open().prepare(
|
|
1491
|
+
`${select} JOIN emails_fts f ON f.rowid = e.id
|
|
1492
|
+
WHERE emails_fts MATCH ?
|
|
1493
|
+
ORDER BY e.date DESC LIMIT 500`
|
|
1494
|
+
).all(ftsQuery);
|
|
1495
|
+
items = rows.map((row) => rowToSummary(row)).filter((item) => subjectKey(item.subject) === base);
|
|
1496
|
+
}
|
|
1497
|
+
} catch {
|
|
1498
|
+
const page = this.list({ mailbox: "all", query: base, limit: Math.min(500, limit * 2) });
|
|
1499
|
+
return page.items.filter((item) => subjectKey(item.subject) === base).slice(0, limit);
|
|
1500
|
+
}
|
|
1501
|
+
if (seedId !== "" && this.hasRefColumns()) {
|
|
1502
|
+
try {
|
|
1485
1503
|
const linked = this.open().prepare(
|
|
1486
|
-
|
|
1487
|
-
e.message_id, e.date, length(e.body_text) AS text_length,
|
|
1488
|
-
(SELECT COUNT(*) FROM attachments a WHERE a.email_id = e.id) AS attachment_count,
|
|
1489
|
-
substr(COALESCE(e.body_text, ''), 1, 240) AS snippet
|
|
1490
|
-
FROM emails e
|
|
1504
|
+
`${select}
|
|
1491
1505
|
WHERE e.in_reply_to = ?
|
|
1492
1506
|
ORDER BY e.date DESC LIMIT 100`
|
|
1493
1507
|
).all(seedId);
|
|
1494
1508
|
const known = new Set(items.map((item) => item.id));
|
|
1495
|
-
try {
|
|
1496
|
-
const seedRow = this.open().prepare(
|
|
1497
|
-
'SELECT in_reply_to, "references", raw_eml FROM emails WHERE lower(message_id) = ? LIMIT 1'
|
|
1498
|
-
).get(seedId);
|
|
1499
|
-
const parents = `${seedRow?.references ?? ""} ${seedRow?.in_reply_to ?? ""}`.split(/\s+/).map((item) => item.replace(/^<|>$/g, "").toLowerCase()).filter((item) => item.includes("@"));
|
|
1500
|
-
if (parents.length === 0 && seedRow?.raw_eml !== void 0 && seedRow.raw_eml !== null) {
|
|
1501
|
-
for (const header of ["references", "in-reply-to"]) {
|
|
1502
|
-
for (const found of headerIds(seedRow.raw_eml, header)) parents.push(found);
|
|
1503
|
-
}
|
|
1504
|
-
}
|
|
1505
|
-
if (parents.length > 0) {
|
|
1506
|
-
const marks = parents.map(() => "?").join(",");
|
|
1507
|
-
const ancestors = this.open().prepare(
|
|
1508
|
-
`SELECT e.id, e.mailbox, e.uid, e.subject, e.from_name, e.from_addr, e.to_addr, e.cc,
|
|
1509
|
-
e.message_id, e.date, length(e.body_text) AS text_length,
|
|
1510
|
-
(SELECT COUNT(*) FROM attachments a WHERE a.email_id = e.id) AS attachment_count,
|
|
1511
|
-
substr(COALESCE(e.body_text, ''), 1, 240) AS snippet
|
|
1512
|
-
FROM emails e WHERE lower(e.message_id) IN (${marks}) LIMIT 100`
|
|
1513
|
-
).all(...parents);
|
|
1514
|
-
for (const row of ancestors) {
|
|
1515
|
-
const item = rowToSummary(row);
|
|
1516
|
-
if (!known.has(item.id)) {
|
|
1517
|
-
items.push(item);
|
|
1518
|
-
known.add(item.id);
|
|
1519
|
-
}
|
|
1520
|
-
}
|
|
1521
|
-
}
|
|
1522
|
-
} catch {
|
|
1523
|
-
}
|
|
1524
1509
|
for (const row of linked) {
|
|
1525
1510
|
const item = rowToSummary(row);
|
|
1526
1511
|
if (!known.has(item.id)) {
|
|
@@ -1528,12 +1513,33 @@ var MailDatabase = class {
|
|
|
1528
1513
|
known.add(item.id);
|
|
1529
1514
|
}
|
|
1530
1515
|
}
|
|
1516
|
+
const seedRow = this.open().prepare(
|
|
1517
|
+
'SELECT in_reply_to, "references", raw_eml FROM emails WHERE lower(message_id) = ? LIMIT 1'
|
|
1518
|
+
).get(seedId);
|
|
1519
|
+
const parents = `${seedRow?.references ?? ""} ${seedRow?.in_reply_to ?? ""}`.split(/\s+/).map((item) => item.replace(/^<|>$/g, "").toLowerCase()).filter((item) => item.includes("@"));
|
|
1520
|
+
if (parents.length === 0 && seedRow?.raw_eml !== void 0 && seedRow.raw_eml !== null) {
|
|
1521
|
+
for (const header of ["references", "in-reply-to"]) {
|
|
1522
|
+
for (const found of headerIds(seedRow.raw_eml, header)) parents.push(found);
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
if (parents.length > 0) {
|
|
1526
|
+
const marks = parents.map(() => "?").join(",");
|
|
1527
|
+
const ancestors = this.open().prepare(
|
|
1528
|
+
`${select}
|
|
1529
|
+
WHERE lower(e.message_id) IN (${marks}) LIMIT 100`
|
|
1530
|
+
).all(...parents);
|
|
1531
|
+
for (const row of ancestors) {
|
|
1532
|
+
const item = rowToSummary(row);
|
|
1533
|
+
if (!known.has(item.id)) {
|
|
1534
|
+
items.push(item);
|
|
1535
|
+
known.add(item.id);
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
} catch {
|
|
1531
1540
|
}
|
|
1532
|
-
return items.slice(0, limit);
|
|
1533
|
-
} catch {
|
|
1534
|
-
const page = this.list({ mailbox: "all", query: base, limit: Math.min(500, limit * 2) });
|
|
1535
|
-
return page.items.filter((item) => normalize2(item.subject) === base).slice(0, limit);
|
|
1536
1541
|
}
|
|
1542
|
+
return items.slice(0, limit);
|
|
1537
1543
|
}
|
|
1538
1544
|
/** Read (and cache) In-Reply-To / References for a set of local ids. */
|
|
1539
1545
|
referencesFor(ids) {
|
|
@@ -1792,7 +1798,117 @@ function rowToSummary(row) {
|
|
|
1792
1798
|
};
|
|
1793
1799
|
}
|
|
1794
1800
|
|
|
1801
|
+
// src/host/references.ts
|
|
1802
|
+
function extractReferences(raw, messageId) {
|
|
1803
|
+
const empty = { inReplyTo: null, references: null };
|
|
1804
|
+
if (raw === null || raw === void 0) return empty;
|
|
1805
|
+
const text2 = Buffer.from(raw.subarray(0, Math.min(raw.byteLength, 2e5))).toString("utf8");
|
|
1806
|
+
const separator = text2.search(/\r?\n\r?\n/);
|
|
1807
|
+
const head = separator >= 0 ? text2.slice(0, separator) : text2;
|
|
1808
|
+
const unfolded = head.replace(/\r?\n[ \t]+/g, " ");
|
|
1809
|
+
const value = (name2) => {
|
|
1810
|
+
const match = new RegExp(`^${name2}:\\s*(.*)$`, "im").exec(unfolded);
|
|
1811
|
+
return match === null ? "" : String(match[1] ?? "");
|
|
1812
|
+
};
|
|
1813
|
+
const ids = (value2) => value2.split(/\s+/).map((item) => item.replace(/^<|>$/g, "").toLowerCase()).filter((item) => item.includes("@"));
|
|
1814
|
+
const referenceIds = ids(value("references"));
|
|
1815
|
+
const replyTo = ids(value("in-reply-to"));
|
|
1816
|
+
const filteredRefs = referenceIds.filter((id) => id !== messageId.toLowerCase());
|
|
1817
|
+
return {
|
|
1818
|
+
inReplyTo: replyTo[0] ?? null,
|
|
1819
|
+
references: filteredRefs.length > 0 ? filteredRefs.join(" ") : null
|
|
1820
|
+
};
|
|
1821
|
+
}
|
|
1822
|
+
function backfillReferences(file, batchSize = 300) {
|
|
1823
|
+
ensureMailSchema(file);
|
|
1824
|
+
const db = new DatabaseSync3(file);
|
|
1825
|
+
try {
|
|
1826
|
+
db.exec("PRAGMA busy_timeout = 10000");
|
|
1827
|
+
const rows = db.prepare("SELECT id, message_id, raw_eml FROM emails WHERE refs_indexed = 0 ORDER BY id LIMIT ?").all(batchSize);
|
|
1828
|
+
if (rows.length === 0) return { updated: 0, remaining: 0 };
|
|
1829
|
+
const update = db.prepare(
|
|
1830
|
+
'UPDATE emails SET in_reply_to = ?, "references" = ?, refs_indexed = 1 WHERE id = ?'
|
|
1831
|
+
);
|
|
1832
|
+
db.exec("BEGIN");
|
|
1833
|
+
try {
|
|
1834
|
+
for (const row of rows) {
|
|
1835
|
+
const headers = extractReferences(row.raw_eml, String(row.message_id ?? ""));
|
|
1836
|
+
update.run(headers.inReplyTo, headers.references, Number(row.id));
|
|
1837
|
+
}
|
|
1838
|
+
db.exec("COMMIT");
|
|
1839
|
+
} catch (error) {
|
|
1840
|
+
db.exec("ROLLBACK");
|
|
1841
|
+
throw error;
|
|
1842
|
+
}
|
|
1843
|
+
const remaining = Number(
|
|
1844
|
+
db.prepare("SELECT COUNT(*) AS count FROM emails WHERE refs_indexed = 0").get()?.count ?? 0
|
|
1845
|
+
);
|
|
1846
|
+
return { updated: rows.length, remaining };
|
|
1847
|
+
} finally {
|
|
1848
|
+
db.close();
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
function optimizeDatabase(file) {
|
|
1852
|
+
ensureMailSchema(file);
|
|
1853
|
+
const db = new DatabaseSync3(file);
|
|
1854
|
+
try {
|
|
1855
|
+
db.exec("PRAGMA busy_timeout = 10000");
|
|
1856
|
+
db.exec("PRAGMA optimize");
|
|
1857
|
+
try {
|
|
1858
|
+
db.exec("INSERT INTO emails_fts(emails_fts) VALUES('optimize')");
|
|
1859
|
+
} catch {
|
|
1860
|
+
}
|
|
1861
|
+
try {
|
|
1862
|
+
db.exec("PRAGMA wal_checkpoint(TRUNCATE)");
|
|
1863
|
+
} catch {
|
|
1864
|
+
}
|
|
1865
|
+
return { ok: true };
|
|
1866
|
+
} catch (error) {
|
|
1867
|
+
return { ok: false, error: error instanceof Error ? error.message : String(error) };
|
|
1868
|
+
} finally {
|
|
1869
|
+
db.close();
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
function backfillSubjects(file, batchSize = 300) {
|
|
1873
|
+
ensureMailSchema(file);
|
|
1874
|
+
const db = new DatabaseSync3(file);
|
|
1875
|
+
try {
|
|
1876
|
+
db.exec("PRAGMA busy_timeout = 10000");
|
|
1877
|
+
const rows = db.prepare(
|
|
1878
|
+
`SELECT e.id, e.subject FROM emails e
|
|
1879
|
+
WHERE NOT EXISTS (SELECT 1 FROM email_subjects s WHERE s.email_id = e.id)
|
|
1880
|
+
ORDER BY e.id LIMIT ?`
|
|
1881
|
+
).all(batchSize);
|
|
1882
|
+
if (rows.length === 0) return { updated: 0, remaining: 0 };
|
|
1883
|
+
const insert = db.prepare("INSERT OR REPLACE INTO email_subjects (email_id, key) VALUES (?, ?)");
|
|
1884
|
+
db.exec("BEGIN");
|
|
1885
|
+
try {
|
|
1886
|
+
for (const row of rows) insert.run(Number(row.id), subjectKey(String(row.subject ?? "")));
|
|
1887
|
+
db.exec("COMMIT");
|
|
1888
|
+
} catch (error) {
|
|
1889
|
+
db.exec("ROLLBACK");
|
|
1890
|
+
throw error;
|
|
1891
|
+
}
|
|
1892
|
+
const remaining = Number(
|
|
1893
|
+
db.prepare(
|
|
1894
|
+
`SELECT COUNT(*) AS count FROM emails e
|
|
1895
|
+
WHERE NOT EXISTS (SELECT 1 FROM email_subjects s WHERE s.email_id = e.id)`
|
|
1896
|
+
).get()?.count ?? 0
|
|
1897
|
+
);
|
|
1898
|
+
return { updated: rows.length, remaining };
|
|
1899
|
+
} finally {
|
|
1900
|
+
db.close();
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1795
1904
|
// src/host/backup.ts
|
|
1905
|
+
init_address();
|
|
1906
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
1907
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
1908
|
+
import { join as join3 } from "node:path";
|
|
1909
|
+
import { ImapFlow } from "imapflow";
|
|
1910
|
+
import { simpleParser } from "mailparser";
|
|
1911
|
+
import { DatabaseSync as DatabaseSync4 } from "node:sqlite";
|
|
1796
1912
|
var BATCH = 25;
|
|
1797
1913
|
function sha256(data) {
|
|
1798
1914
|
return createHash2("sha256").update(data).digest("hex");
|
|
@@ -1874,7 +1990,7 @@ var BackupEngine = class {
|
|
|
1874
1990
|
}
|
|
1875
1991
|
openDb() {
|
|
1876
1992
|
ensureMailSchema(this.runtime.mailDb);
|
|
1877
|
-
const db = new
|
|
1993
|
+
const db = new DatabaseSync4(this.runtime.mailDb);
|
|
1878
1994
|
db.exec("PRAGMA journal_mode=WAL");
|
|
1879
1995
|
db.exec("PRAGMA busy_timeout=10000");
|
|
1880
1996
|
db.exec("PRAGMA synchronous=NORMAL");
|
|
@@ -1923,6 +2039,10 @@ var BackupEngine = class {
|
|
|
1923
2039
|
);
|
|
1924
2040
|
if (inserted.changes === 0) return 0;
|
|
1925
2041
|
const emailId = Number(inserted.lastInsertRowid);
|
|
2042
|
+
db.prepare("INSERT OR REPLACE INTO email_subjects (email_id, key) VALUES (?, ?)").run(
|
|
2043
|
+
emailId,
|
|
2044
|
+
subjectKey(subject)
|
|
2045
|
+
);
|
|
1926
2046
|
const attachments = parsed.attachments ?? [];
|
|
1927
2047
|
const insertAttachment = db.prepare(
|
|
1928
2048
|
"INSERT INTO attachments (email_id, filename, content_type, size, hash, content_id, disposition) VALUES (?, ?, ?, ?, ?, ?, ?)"
|
|
@@ -2128,7 +2248,7 @@ var BackupEngine = class {
|
|
|
2128
2248
|
};
|
|
2129
2249
|
if (!existsSync3(dbFile)) return empty;
|
|
2130
2250
|
try {
|
|
2131
|
-
const db = new
|
|
2251
|
+
const db = new DatabaseSync4(dbFile, { readOnly: true });
|
|
2132
2252
|
try {
|
|
2133
2253
|
const latest = db.prepare("SELECT * FROM backup_logs ORDER BY id DESC LIMIT 1").get();
|
|
2134
2254
|
const success = db.prepare("SELECT completed_at FROM backup_logs WHERE status = 'completed' ORDER BY id DESC LIMIT 1").get();
|
|
@@ -3004,7 +3124,7 @@ ${conversation.trim()}
|
|
|
3004
3124
|
}
|
|
3005
3125
|
|
|
3006
3126
|
// src/host/version.ts
|
|
3007
|
-
var VERSION = "0.
|
|
3127
|
+
var VERSION = "0.21.0";
|
|
3008
3128
|
|
|
3009
3129
|
// src/host/api.ts
|
|
3010
3130
|
init_smtp();
|
|
@@ -3158,6 +3278,13 @@ function buildRouter(runtime) {
|
|
|
3158
3278
|
try {
|
|
3159
3279
|
await runtime.provider(account).setFlag(detail.mailbox, detail.uid, "\\Flagged", starred);
|
|
3160
3280
|
} catch {
|
|
3281
|
+
runtime.actionQueue(account).enqueue({
|
|
3282
|
+
type: "flag",
|
|
3283
|
+
mailbox: detail.mailbox,
|
|
3284
|
+
uid: detail.uid,
|
|
3285
|
+
flag: "\\Flagged",
|
|
3286
|
+
value: starred
|
|
3287
|
+
});
|
|
3161
3288
|
}
|
|
3162
3289
|
}
|
|
3163
3290
|
return { starred };
|
|
@@ -3173,6 +3300,12 @@ function buildRouter(runtime) {
|
|
|
3173
3300
|
try {
|
|
3174
3301
|
await runtime.provider(account).move(detail.mailbox, detail.uid, target);
|
|
3175
3302
|
} catch {
|
|
3303
|
+
runtime.actionQueue(account).enqueue({
|
|
3304
|
+
type: "move",
|
|
3305
|
+
mailbox: detail.mailbox,
|
|
3306
|
+
uid: detail.uid,
|
|
3307
|
+
target
|
|
3308
|
+
});
|
|
3176
3309
|
}
|
|
3177
3310
|
}
|
|
3178
3311
|
moveLocalMessage(runtime.paths(account).mailDb, Number(params.id), target);
|
|
@@ -3198,6 +3331,13 @@ function buildRouter(runtime) {
|
|
|
3198
3331
|
try {
|
|
3199
3332
|
await runtime.provider(account).setSeen(detail.mailbox, detail.uid, action === "read");
|
|
3200
3333
|
} catch {
|
|
3334
|
+
runtime.actionQueue(account).enqueue({
|
|
3335
|
+
type: "flag",
|
|
3336
|
+
mailbox: detail.mailbox,
|
|
3337
|
+
uid: detail.uid,
|
|
3338
|
+
flag: "\\Seen",
|
|
3339
|
+
value: action === "read"
|
|
3340
|
+
});
|
|
3201
3341
|
}
|
|
3202
3342
|
}
|
|
3203
3343
|
count += 1;
|
|
@@ -3208,6 +3348,12 @@ function buildRouter(runtime) {
|
|
|
3208
3348
|
try {
|
|
3209
3349
|
await runtime.provider(account).move(detail.mailbox, detail.uid, target);
|
|
3210
3350
|
} catch {
|
|
3351
|
+
runtime.actionQueue(account).enqueue({
|
|
3352
|
+
type: "move",
|
|
3353
|
+
mailbox: detail.mailbox,
|
|
3354
|
+
uid: detail.uid,
|
|
3355
|
+
target
|
|
3356
|
+
});
|
|
3211
3357
|
}
|
|
3212
3358
|
}
|
|
3213
3359
|
moveLocalMessage(runtime.paths(account).mailDb, Number(detail.id), target);
|
|
@@ -3219,6 +3365,13 @@ function buildRouter(runtime) {
|
|
|
3219
3365
|
try {
|
|
3220
3366
|
await runtime.provider(account).setFlag(detail.mailbox, detail.uid, "\\Flagged", starred);
|
|
3221
3367
|
} catch {
|
|
3368
|
+
runtime.actionQueue(account).enqueue({
|
|
3369
|
+
type: "flag",
|
|
3370
|
+
mailbox: detail.mailbox,
|
|
3371
|
+
uid: detail.uid,
|
|
3372
|
+
flag: "\\Flagged",
|
|
3373
|
+
value: starred
|
|
3374
|
+
});
|
|
3222
3375
|
}
|
|
3223
3376
|
}
|
|
3224
3377
|
count += 1;
|
|
@@ -3226,6 +3379,10 @@ function buildRouter(runtime) {
|
|
|
3226
3379
|
}
|
|
3227
3380
|
return { ok: true, count };
|
|
3228
3381
|
});
|
|
3382
|
+
router.post("/maintenance/optimize", ({ url }) => {
|
|
3383
|
+
const { account } = accountFrom(runtime, url);
|
|
3384
|
+
return optimizeDatabase(runtime.paths(account).mailDb);
|
|
3385
|
+
});
|
|
3229
3386
|
router.get("/maintenance/references", ({ url }) => {
|
|
3230
3387
|
const { account } = accountFrom(runtime, url);
|
|
3231
3388
|
return runtime.referencesStatus(account);
|
|
@@ -3836,7 +3993,49 @@ function buildRouter(runtime) {
|
|
|
3836
3993
|
throw aiHttpError(error);
|
|
3837
3994
|
}
|
|
3838
3995
|
});
|
|
3839
|
-
router.
|
|
3996
|
+
router.post("/projects/:id/attachment", async ({ params, body }) => {
|
|
3997
|
+
const payload = await body();
|
|
3998
|
+
const account = accountFromPayload(runtime, payload);
|
|
3999
|
+
const project = runtime.projects().get(String(params.id ?? ""));
|
|
4000
|
+
if (project === void 0) throw new HttpError(404, "\u9879\u76EE\u4E0D\u5B58\u5728");
|
|
4001
|
+
const emailId = Number(payload.emailId ?? 0);
|
|
4002
|
+
const attachmentId = Number(payload.attachmentId ?? 0);
|
|
4003
|
+
const file = runtime.database(account).attachment(emailId, attachmentId);
|
|
4004
|
+
if (file === void 0) throw new HttpError(404, "\u9644\u4EF6\u4E0D\u5B58\u5728");
|
|
4005
|
+
const dir = join6(project.path, "attachments");
|
|
4006
|
+
mkdirSync5(dir, { recursive: true });
|
|
4007
|
+
const safe = basename(file.filename).replace(/[/\\:*?"<>|]/g, "_") || "attachment";
|
|
4008
|
+
let target = join6(dir, safe);
|
|
4009
|
+
let index = 1;
|
|
4010
|
+
while (existsSync6(target)) {
|
|
4011
|
+
const dot = safe.lastIndexOf(".");
|
|
4012
|
+
const stem = dot > 0 ? safe.slice(0, dot) : safe;
|
|
4013
|
+
const ext = dot > 0 ? safe.slice(dot) : "";
|
|
4014
|
+
target = join6(dir, `${stem}-${String(index)}${ext}`);
|
|
4015
|
+
index += 1;
|
|
4016
|
+
}
|
|
4017
|
+
writeFileSync5(target, Buffer.from(file.data));
|
|
4018
|
+
return { ok: true, path: target, filename: basename(target) };
|
|
4019
|
+
});
|
|
4020
|
+
router.get("/projects", () => {
|
|
4021
|
+
const counts = /* @__PURE__ */ new Map();
|
|
4022
|
+
for (const account of runtime.current.accounts) {
|
|
4023
|
+
for (const task of runtime.tasks().list({ accountId: account.id })) {
|
|
4024
|
+
const projectId = task.projectId ?? "";
|
|
4025
|
+
if (projectId === "") continue;
|
|
4026
|
+
const entry = counts.get(projectId) ?? { total: 0, done: 0 };
|
|
4027
|
+
entry.total += 1;
|
|
4028
|
+
if (task.status === "COMPLETED") entry.done += 1;
|
|
4029
|
+
counts.set(projectId, entry);
|
|
4030
|
+
}
|
|
4031
|
+
}
|
|
4032
|
+
return {
|
|
4033
|
+
items: runtime.projects().list().map((project) => {
|
|
4034
|
+
const stat = counts.get(project.id);
|
|
4035
|
+
return { ...projectView(project), taskTotal: stat?.total ?? 0, taskDone: stat?.done ?? 0 };
|
|
4036
|
+
})
|
|
4037
|
+
};
|
|
4038
|
+
});
|
|
3840
4039
|
router.get("/projects/for", ({ url }) => {
|
|
3841
4040
|
const account = accountFromPayload(runtime, { accountId: url.searchParams.get("accountId") ?? void 0 });
|
|
3842
4041
|
const key = url.searchParams.get("key") ?? "";
|
|
@@ -4276,7 +4475,7 @@ function writeAttachment(res, filename, contentType, data, inline = false) {
|
|
|
4276
4475
|
"content-disposition",
|
|
4277
4476
|
`${inline ? "inline" : "attachment"}; filename*=UTF-8''${encodeURIComponent(filename)}`
|
|
4278
4477
|
);
|
|
4279
|
-
res.setHeader("cache-control", "
|
|
4478
|
+
res.setHeader("cache-control", "private, max-age=86400");
|
|
4280
4479
|
res.setHeader("x-content-type-options", "nosniff");
|
|
4281
4480
|
res.setHeader(
|
|
4282
4481
|
"content-security-policy",
|
|
@@ -4349,7 +4548,7 @@ ${signature}`;
|
|
|
4349
4548
|
|
|
4350
4549
|
// src/host/runtime.ts
|
|
4351
4550
|
import { existsSync as existsSync8, statSync } from "node:fs";
|
|
4352
|
-
import { join as
|
|
4551
|
+
import { join as join8 } from "node:path";
|
|
4353
4552
|
|
|
4354
4553
|
// src/host/secrets.ts
|
|
4355
4554
|
import { spawnSync } from "node:child_process";
|
|
@@ -4539,7 +4738,7 @@ var CredentialManager = class {
|
|
|
4539
4738
|
};
|
|
4540
4739
|
|
|
4541
4740
|
// src/host/ai-store.ts
|
|
4542
|
-
import { existsSync as existsSync7, mkdirSync as
|
|
4741
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync6, readFileSync as readFileSync5, renameSync as renameSync4, writeFileSync as writeFileSync6 } from "node:fs";
|
|
4543
4742
|
import { dirname as dirname2 } from "node:path";
|
|
4544
4743
|
var MAX_THREADS = 50;
|
|
4545
4744
|
var MAX_CARDS_PER_THREAD = 50;
|
|
@@ -4567,9 +4766,9 @@ var AiThreadStore = class {
|
|
|
4567
4766
|
prune(data);
|
|
4568
4767
|
const text2 = `${JSON.stringify(data, null, 2)}
|
|
4569
4768
|
`;
|
|
4570
|
-
|
|
4769
|
+
mkdirSync6(dirname2(this.file), { recursive: true });
|
|
4571
4770
|
const temp = `${this.file}.${String(process.pid)}.tmp`;
|
|
4572
|
-
|
|
4771
|
+
writeFileSync6(temp, text2, { mode: 384 });
|
|
4573
4772
|
renameSync4(temp, this.file);
|
|
4574
4773
|
}
|
|
4575
4774
|
get(key) {
|
|
@@ -4679,7 +4878,7 @@ function prune(data) {
|
|
|
4679
4878
|
}
|
|
4680
4879
|
|
|
4681
4880
|
// src/host/mail-flags.ts
|
|
4682
|
-
import { mkdirSync as
|
|
4881
|
+
import { mkdirSync as mkdirSync7, readFileSync as readFileSync6, renameSync as renameSync5, writeFileSync as writeFileSync7 } from "node:fs";
|
|
4683
4882
|
import { dirname as dirname3 } from "node:path";
|
|
4684
4883
|
var MailFlagsStore = class {
|
|
4685
4884
|
file;
|
|
@@ -4699,9 +4898,9 @@ var MailFlagsStore = class {
|
|
|
4699
4898
|
return this.keys;
|
|
4700
4899
|
}
|
|
4701
4900
|
flush() {
|
|
4702
|
-
|
|
4901
|
+
mkdirSync7(dirname3(this.file), { recursive: true });
|
|
4703
4902
|
const temp = `${this.file}.${String(process.pid)}.tmp`;
|
|
4704
|
-
|
|
4903
|
+
writeFileSync7(temp, `${JSON.stringify({ starred: [...this.load()] }, null, 2)}
|
|
4705
4904
|
`, {
|
|
4706
4905
|
encoding: "utf8",
|
|
4707
4906
|
mode: 384
|
|
@@ -4730,62 +4929,87 @@ var MailFlagsStore = class {
|
|
|
4730
4929
|
}
|
|
4731
4930
|
};
|
|
4732
4931
|
|
|
4733
|
-
// src/host/
|
|
4734
|
-
import {
|
|
4735
|
-
|
|
4736
|
-
|
|
4737
|
-
|
|
4738
|
-
|
|
4739
|
-
|
|
4740
|
-
|
|
4741
|
-
|
|
4742
|
-
|
|
4743
|
-
|
|
4744
|
-
|
|
4745
|
-
};
|
|
4746
|
-
const ids = (value2) => value2.split(/\s+/).map((item) => item.replace(/^<|>$/g, "").toLowerCase()).filter((item) => item.includes("@"));
|
|
4747
|
-
const referenceIds = ids(value("references"));
|
|
4748
|
-
const replyTo = ids(value("in-reply-to"));
|
|
4749
|
-
const filteredRefs = referenceIds.filter((id) => id !== messageId.toLowerCase());
|
|
4750
|
-
return {
|
|
4751
|
-
inReplyTo: replyTo[0] ?? null,
|
|
4752
|
-
references: filteredRefs.length > 0 ? filteredRefs.join(" ") : null
|
|
4753
|
-
};
|
|
4754
|
-
}
|
|
4755
|
-
function backfillReferences(file, batchSize = 300) {
|
|
4756
|
-
ensureMailSchema(file);
|
|
4757
|
-
const db = new DatabaseSync4(file);
|
|
4758
|
-
try {
|
|
4759
|
-
db.exec("PRAGMA busy_timeout = 10000");
|
|
4760
|
-
const rows = db.prepare("SELECT id, message_id, raw_eml FROM emails WHERE refs_indexed = 0 ORDER BY id LIMIT ?").all(batchSize);
|
|
4761
|
-
if (rows.length === 0) return { updated: 0, remaining: 0 };
|
|
4762
|
-
const update = db.prepare(
|
|
4763
|
-
'UPDATE emails SET in_reply_to = ?, "references" = ?, refs_indexed = 1 WHERE id = ?'
|
|
4764
|
-
);
|
|
4765
|
-
db.exec("BEGIN");
|
|
4932
|
+
// src/host/action-queue.ts
|
|
4933
|
+
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
4934
|
+
import { mkdirSync as mkdirSync8, readFileSync as readFileSync7, renameSync as renameSync6, writeFileSync as writeFileSync8 } from "node:fs";
|
|
4935
|
+
import { dirname as dirname4 } from "node:path";
|
|
4936
|
+
var ActionQueueStore = class {
|
|
4937
|
+
file;
|
|
4938
|
+
items = null;
|
|
4939
|
+
constructor(file) {
|
|
4940
|
+
this.file = file;
|
|
4941
|
+
}
|
|
4942
|
+
load() {
|
|
4943
|
+
if (this.items !== null) return this.items;
|
|
4766
4944
|
try {
|
|
4767
|
-
|
|
4768
|
-
|
|
4769
|
-
|
|
4770
|
-
|
|
4771
|
-
db.exec("COMMIT");
|
|
4772
|
-
} catch (error) {
|
|
4773
|
-
db.exec("ROLLBACK");
|
|
4774
|
-
throw error;
|
|
4945
|
+
const parsed = JSON.parse(readFileSync7(this.file, "utf8"));
|
|
4946
|
+
this.items = Array.isArray(parsed) ? parsed.filter((item) => typeof item?.id === "string") : [];
|
|
4947
|
+
} catch {
|
|
4948
|
+
this.items = [];
|
|
4775
4949
|
}
|
|
4776
|
-
|
|
4777
|
-
|
|
4950
|
+
return this.items;
|
|
4951
|
+
}
|
|
4952
|
+
flush() {
|
|
4953
|
+
mkdirSync8(dirname4(this.file), { recursive: true });
|
|
4954
|
+
const temp = `${this.file}.${String(process.pid)}.tmp`;
|
|
4955
|
+
writeFileSync8(temp, `${JSON.stringify(this.load(), null, 2)}
|
|
4956
|
+
`, { encoding: "utf8", mode: 384 });
|
|
4957
|
+
renameSync6(temp, this.file);
|
|
4958
|
+
}
|
|
4959
|
+
list() {
|
|
4960
|
+
return [...this.load()];
|
|
4961
|
+
}
|
|
4962
|
+
enqueue(action) {
|
|
4963
|
+
const items = this.load();
|
|
4964
|
+
const filtered = items.filter(
|
|
4965
|
+
(item) => !(item.type === action.type && item.mailbox === action.mailbox && item.uid === action.uid && (action.type === "move" || item.flag === action.flag))
|
|
4778
4966
|
);
|
|
4779
|
-
|
|
4780
|
-
|
|
4781
|
-
|
|
4967
|
+
filtered.push({ ...action, id: randomUUID4(), createdAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
4968
|
+
this.items = filtered;
|
|
4969
|
+
this.flush();
|
|
4782
4970
|
}
|
|
4783
|
-
|
|
4971
|
+
remove(ids) {
|
|
4972
|
+
if (ids.length === 0) return;
|
|
4973
|
+
const set = new Set(ids);
|
|
4974
|
+
this.items = this.load().filter((item) => !set.has(item.id));
|
|
4975
|
+
this.flush();
|
|
4976
|
+
}
|
|
4977
|
+
/** Try every queued action; keep the ones the server still rejects. */
|
|
4978
|
+
async flushTo(executor) {
|
|
4979
|
+
const pending = this.list();
|
|
4980
|
+
const done = [];
|
|
4981
|
+
let failed = 0;
|
|
4982
|
+
for (const action of pending) {
|
|
4983
|
+
try {
|
|
4984
|
+
if (action.type === "flag") {
|
|
4985
|
+
await executor.setFlag(
|
|
4986
|
+
action.mailbox,
|
|
4987
|
+
action.uid,
|
|
4988
|
+
action.flag ?? "\\Flagged",
|
|
4989
|
+
action.value !== false
|
|
4990
|
+
);
|
|
4991
|
+
} else if (action.target !== void 0) {
|
|
4992
|
+
await executor.move(action.mailbox, action.uid, action.target);
|
|
4993
|
+
}
|
|
4994
|
+
done.push(action.id);
|
|
4995
|
+
} catch {
|
|
4996
|
+
failed += 1;
|
|
4997
|
+
const current = this.load().find((item) => item.id === action.id);
|
|
4998
|
+
if (current !== void 0) {
|
|
4999
|
+
current.attempts = (current.attempts ?? 0) + 1;
|
|
5000
|
+
if (current.attempts >= 5) done.push(action.id);
|
|
5001
|
+
}
|
|
5002
|
+
}
|
|
5003
|
+
}
|
|
5004
|
+
this.remove(done);
|
|
5005
|
+
return { sent: done.length, failed };
|
|
5006
|
+
}
|
|
5007
|
+
};
|
|
4784
5008
|
|
|
4785
5009
|
// src/host/tasks.ts
|
|
4786
|
-
import { randomUUID as
|
|
4787
|
-
import { mkdirSync as
|
|
4788
|
-
import { dirname as
|
|
5010
|
+
import { randomUUID as randomUUID5 } from "node:crypto";
|
|
5011
|
+
import { mkdirSync as mkdirSync9 } from "node:fs";
|
|
5012
|
+
import { dirname as dirname5 } from "node:path";
|
|
4789
5013
|
import { DatabaseSync as DatabaseSync5 } from "node:sqlite";
|
|
4790
5014
|
var STATUSES = /* @__PURE__ */ new Set(["NEEDS-ACTION", "IN-PROCESS", "COMPLETED", "CANCELLED"]);
|
|
4791
5015
|
function now() {
|
|
@@ -4834,7 +5058,7 @@ var TaskStore = class {
|
|
|
4834
5058
|
}
|
|
4835
5059
|
open() {
|
|
4836
5060
|
if (this.db !== null) return this.db;
|
|
4837
|
-
|
|
5061
|
+
mkdirSync9(dirname5(this.file), { recursive: true });
|
|
4838
5062
|
const db = new DatabaseSync5(this.file);
|
|
4839
5063
|
db.exec("PRAGMA journal_mode=WAL");
|
|
4840
5064
|
db.exec("PRAGMA busy_timeout=10000");
|
|
@@ -4911,7 +5135,7 @@ var TaskStore = class {
|
|
|
4911
5135
|
if (title === "") throw new Error("\u4EFB\u52A1\u6807\u9898\u4E0D\u80FD\u4E3A\u7A7A");
|
|
4912
5136
|
if (title.length > 300) throw new Error("\u4EFB\u52A1\u6807\u9898\u8FC7\u957F");
|
|
4913
5137
|
const status = statusOf(input.status);
|
|
4914
|
-
const id = `task_${
|
|
5138
|
+
const id = `task_${randomUUID5()}`;
|
|
4915
5139
|
const stamp3 = now();
|
|
4916
5140
|
this.open().prepare(
|
|
4917
5141
|
`INSERT INTO tasks (id, account_id, project_id, uid, title, notes, due, status, priority, owner,
|
|
@@ -4921,7 +5145,7 @@ var TaskStore = class {
|
|
|
4921
5145
|
id,
|
|
4922
5146
|
input.accountId,
|
|
4923
5147
|
input.projectId ?? null,
|
|
4924
|
-
`${
|
|
5148
|
+
`${randomUUID5()}@dsh-mailharbor`,
|
|
4925
5149
|
title,
|
|
4926
5150
|
String(input.notes ?? ""),
|
|
4927
5151
|
String(input.due ?? ""),
|
|
@@ -5017,7 +5241,7 @@ var TaskStore = class {
|
|
|
5017
5241
|
);
|
|
5018
5242
|
return this.get(String(existing.id));
|
|
5019
5243
|
}
|
|
5020
|
-
const id = `task_${
|
|
5244
|
+
const id = `task_${randomUUID5()}`;
|
|
5021
5245
|
const stamp3 = now();
|
|
5022
5246
|
db.prepare(
|
|
5023
5247
|
`INSERT INTO tasks (id, account_id, calendar_href, href, etag, uid, title, notes, due, status, priority,
|
|
@@ -5029,7 +5253,7 @@ var TaskStore = class {
|
|
|
5029
5253
|
input.calendarHref,
|
|
5030
5254
|
input.href,
|
|
5031
5255
|
input.etag,
|
|
5032
|
-
input.todo.uid || `${
|
|
5256
|
+
input.todo.uid || `${randomUUID5()}@dsh-mailharbor`,
|
|
5033
5257
|
String(input.todo.title || "(\u65E0\u6807\u9898)"),
|
|
5034
5258
|
String(input.todo.notes ?? ""),
|
|
5035
5259
|
String(input.todo.due ?? ""),
|
|
@@ -5073,9 +5297,9 @@ var TaskStore = class {
|
|
|
5073
5297
|
};
|
|
5074
5298
|
|
|
5075
5299
|
// src/host/drafts.ts
|
|
5076
|
-
import { randomUUID as
|
|
5077
|
-
import { mkdirSync as
|
|
5078
|
-
import { dirname as
|
|
5300
|
+
import { randomUUID as randomUUID6 } from "node:crypto";
|
|
5301
|
+
import { mkdirSync as mkdirSync10, readFileSync as readFileSync8, renameSync as renameSync7, writeFileSync as writeFileSync9 } from "node:fs";
|
|
5302
|
+
import { dirname as dirname6, join as join7 } from "node:path";
|
|
5079
5303
|
function text(value) {
|
|
5080
5304
|
return value === void 0 || value === null ? "" : String(value);
|
|
5081
5305
|
}
|
|
@@ -5086,7 +5310,7 @@ function isDraft(value) {
|
|
|
5086
5310
|
return value !== null && typeof value === "object" && typeof value.id === "string";
|
|
5087
5311
|
}
|
|
5088
5312
|
function draftPath(root) {
|
|
5089
|
-
return
|
|
5313
|
+
return join7(root, "drafts.json");
|
|
5090
5314
|
}
|
|
5091
5315
|
var DraftStore = class {
|
|
5092
5316
|
file;
|
|
@@ -5099,18 +5323,18 @@ var DraftStore = class {
|
|
|
5099
5323
|
}
|
|
5100
5324
|
load() {
|
|
5101
5325
|
try {
|
|
5102
|
-
const parsed = JSON.parse(
|
|
5326
|
+
const parsed = JSON.parse(readFileSync8(this.file, "utf8"));
|
|
5103
5327
|
this.items = Array.isArray(parsed) ? parsed.filter(isDraft) : [];
|
|
5104
5328
|
} catch {
|
|
5105
5329
|
this.items = [];
|
|
5106
5330
|
}
|
|
5107
5331
|
}
|
|
5108
5332
|
flush() {
|
|
5109
|
-
|
|
5333
|
+
mkdirSync10(dirname6(this.file), { recursive: true });
|
|
5110
5334
|
const temp = `${this.file}.${String(process.pid)}.${String(Date.now())}.tmp`;
|
|
5111
|
-
|
|
5335
|
+
writeFileSync9(temp, `${JSON.stringify(this.items, null, 2)}
|
|
5112
5336
|
`, { encoding: "utf8", mode: 384 });
|
|
5113
|
-
|
|
5337
|
+
renameSync7(temp, this.file);
|
|
5114
5338
|
}
|
|
5115
5339
|
list() {
|
|
5116
5340
|
return [...this.items].sort((left, right) => right.updatedAt.localeCompare(left.updatedAt)).map((draft) => ({ ...draft }));
|
|
@@ -5144,7 +5368,7 @@ var DraftStore = class {
|
|
|
5144
5368
|
return { ...existing };
|
|
5145
5369
|
}
|
|
5146
5370
|
const draft = {
|
|
5147
|
-
id:
|
|
5371
|
+
id: randomUUID6(),
|
|
5148
5372
|
accountId: this.accountId,
|
|
5149
5373
|
...fields,
|
|
5150
5374
|
updatedAt: now2
|
|
@@ -5169,8 +5393,8 @@ var DraftStore = class {
|
|
|
5169
5393
|
init_imap();
|
|
5170
5394
|
|
|
5171
5395
|
// src/host/read-state.ts
|
|
5172
|
-
import { mkdirSync as
|
|
5173
|
-
import { dirname as
|
|
5396
|
+
import { mkdirSync as mkdirSync11, readFileSync as readFileSync9, renameSync as renameSync8, writeFileSync as writeFileSync10 } from "node:fs";
|
|
5397
|
+
import { dirname as dirname7 } from "node:path";
|
|
5174
5398
|
var ReadStateStore = class {
|
|
5175
5399
|
file;
|
|
5176
5400
|
keys = null;
|
|
@@ -5180,7 +5404,7 @@ var ReadStateStore = class {
|
|
|
5180
5404
|
load() {
|
|
5181
5405
|
if (this.keys !== null) return this.keys;
|
|
5182
5406
|
try {
|
|
5183
|
-
const parsed = JSON.parse(
|
|
5407
|
+
const parsed = JSON.parse(readFileSync9(this.file, "utf8"));
|
|
5184
5408
|
const list = Array.isArray(parsed) ? parsed : Array.isArray(parsed?.unread) ? parsed.unread : [];
|
|
5185
5409
|
this.keys = new Set(list.filter((key) => typeof key === "string" && key !== ""));
|
|
5186
5410
|
} catch {
|
|
@@ -5189,14 +5413,14 @@ var ReadStateStore = class {
|
|
|
5189
5413
|
return this.keys;
|
|
5190
5414
|
}
|
|
5191
5415
|
flush() {
|
|
5192
|
-
|
|
5416
|
+
mkdirSync11(dirname7(this.file), { recursive: true });
|
|
5193
5417
|
const temp = `${this.file}.${String(process.pid)}.tmp`;
|
|
5194
|
-
|
|
5418
|
+
writeFileSync10(temp, `${JSON.stringify({ unread: [...this.load()] }, null, 2)}
|
|
5195
5419
|
`, {
|
|
5196
5420
|
encoding: "utf8",
|
|
5197
5421
|
mode: 384
|
|
5198
5422
|
});
|
|
5199
|
-
|
|
5423
|
+
renameSync8(temp, this.file);
|
|
5200
5424
|
}
|
|
5201
5425
|
isUnread(key) {
|
|
5202
5426
|
return key !== "" && this.load().has(key);
|
|
@@ -5261,6 +5485,7 @@ var MailHarborRuntime = class {
|
|
|
5261
5485
|
projectStore = null;
|
|
5262
5486
|
taskStore = null;
|
|
5263
5487
|
flagStores = /* @__PURE__ */ new Map();
|
|
5488
|
+
actionQueues = /* @__PURE__ */ new Map();
|
|
5264
5489
|
referenceJobs = /* @__PURE__ */ new Map();
|
|
5265
5490
|
disposed = false;
|
|
5266
5491
|
constructor(options = {}) {
|
|
@@ -5275,13 +5500,13 @@ var MailHarborRuntime = class {
|
|
|
5275
5500
|
}
|
|
5276
5501
|
projects() {
|
|
5277
5502
|
if (this.projectStore === null) {
|
|
5278
|
-
this.projectStore = new ProjectStore(
|
|
5503
|
+
this.projectStore = new ProjectStore(join8(this.config.dataDir, "projects.json"));
|
|
5279
5504
|
}
|
|
5280
5505
|
return this.projectStore;
|
|
5281
5506
|
}
|
|
5282
5507
|
tasks() {
|
|
5283
5508
|
if (this.taskStore === null) {
|
|
5284
|
-
this.taskStore = new TaskStore(
|
|
5509
|
+
this.taskStore = new TaskStore(join8(this.config.dataDir, "state", "state.db"));
|
|
5285
5510
|
}
|
|
5286
5511
|
return this.taskStore;
|
|
5287
5512
|
}
|
|
@@ -5361,6 +5586,22 @@ var MailHarborRuntime = class {
|
|
|
5361
5586
|
paths(account) {
|
|
5362
5587
|
return accountRuntime(account);
|
|
5363
5588
|
}
|
|
5589
|
+
actionQueue(account) {
|
|
5590
|
+
const file = join8(this.paths(account).root, "actions-queue.json");
|
|
5591
|
+
const existing = this.actionQueues.get(account.id);
|
|
5592
|
+
if (existing !== void 0 && existing.signature === file) return existing.value;
|
|
5593
|
+
const created = new ActionQueueStore(file);
|
|
5594
|
+
this.actionQueues.set(account.id, { signature: file, value: created });
|
|
5595
|
+
return created;
|
|
5596
|
+
}
|
|
5597
|
+
async flushActions(account) {
|
|
5598
|
+
if (!this.hasCredentials(account)) return { sent: 0, failed: 0 };
|
|
5599
|
+
try {
|
|
5600
|
+
return await this.actionQueue(account).flushTo(this.provider(account));
|
|
5601
|
+
} catch {
|
|
5602
|
+
return { sent: 0, failed: 0 };
|
|
5603
|
+
}
|
|
5604
|
+
}
|
|
5364
5605
|
referencesStatus(account) {
|
|
5365
5606
|
return this.referenceJobs.get(account.id) ?? {
|
|
5366
5607
|
running: false,
|
|
@@ -5391,6 +5632,13 @@ var MailHarborRuntime = class {
|
|
|
5391
5632
|
if (result.remaining <= 0 || result.updated === 0) break;
|
|
5392
5633
|
await new Promise((resolve) => setTimeout(resolve, 40));
|
|
5393
5634
|
}
|
|
5635
|
+
for (; ; ) {
|
|
5636
|
+
const result = backfillSubjects(dbFile, 300);
|
|
5637
|
+
status.indexed += result.updated;
|
|
5638
|
+
status.remaining = result.remaining;
|
|
5639
|
+
if (result.remaining <= 0 || result.updated === 0) break;
|
|
5640
|
+
await new Promise((resolve) => setTimeout(resolve, 40));
|
|
5641
|
+
}
|
|
5394
5642
|
} catch (error) {
|
|
5395
5643
|
status.error = error instanceof Error ? error.message : String(error);
|
|
5396
5644
|
} finally {
|
|
@@ -5443,7 +5691,7 @@ var MailHarborRuntime = class {
|
|
|
5443
5691
|
return created;
|
|
5444
5692
|
}
|
|
5445
5693
|
aiThreads(account) {
|
|
5446
|
-
const file =
|
|
5694
|
+
const file = join8(this.paths(account).root, "ai-threads.json");
|
|
5447
5695
|
const existing = this.aiStores.get(account.id);
|
|
5448
5696
|
if (existing !== void 0 && existing.signature === file) return existing.value;
|
|
5449
5697
|
const created = new AiThreadStore(file);
|
|
@@ -5451,7 +5699,7 @@ var MailHarborRuntime = class {
|
|
|
5451
5699
|
return created;
|
|
5452
5700
|
}
|
|
5453
5701
|
flags(account) {
|
|
5454
|
-
const file =
|
|
5702
|
+
const file = join8(this.paths(account).root, "starred.json");
|
|
5455
5703
|
const existing = this.flagStores.get(account.id);
|
|
5456
5704
|
if (existing !== void 0 && existing.signature === file) return existing.value;
|
|
5457
5705
|
const created = new MailFlagsStore(file);
|
|
@@ -5459,7 +5707,7 @@ var MailHarborRuntime = class {
|
|
|
5459
5707
|
return created;
|
|
5460
5708
|
}
|
|
5461
5709
|
readState(account) {
|
|
5462
|
-
const file =
|
|
5710
|
+
const file = join8(this.paths(account).root, "read-state.json");
|
|
5463
5711
|
const existing = this.readStores.get(account.id);
|
|
5464
5712
|
if (existing !== void 0 && existing.signature === file) return existing.value;
|
|
5465
5713
|
const created = new ReadStateStore(file);
|
|
@@ -5525,6 +5773,7 @@ var MailHarborRuntime = class {
|
|
|
5525
5773
|
if (status.exitCode === 0) status.lastSuccessAt = status.finishedAt;
|
|
5526
5774
|
this.databases.get(account.id)?.value.close();
|
|
5527
5775
|
this.databases.delete(account.id);
|
|
5776
|
+
if (status.exitCode === 0) void this.flushActions(account).catch(() => void 0);
|
|
5528
5777
|
}
|
|
5529
5778
|
})();
|
|
5530
5779
|
this.backups.set(account.id, job);
|
|
@@ -5969,6 +6218,7 @@ function apply(ctx) {
|
|
|
5969
6218
|
const disposers = [];
|
|
5970
6219
|
if (activeAccount2 !== void 0) {
|
|
5971
6220
|
setTimeout(() => runtime.startReferencesBackfill(activeAccount2), 3e3);
|
|
6221
|
+
setTimeout(() => void runtime.flushActions(activeAccount2).catch(() => void 0), 6e3);
|
|
5972
6222
|
}
|
|
5973
6223
|
const start = () => {
|
|
5974
6224
|
disposers.push(ctx.webServer.register({ kind: "prefix", path: ROUTE, handler }));
|