@zaclinley/dsh-mailharbor 0.19.1 → 0.20.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 +18 -0
- package/lib/client.js +100 -21
- package/lib/client.js.map +3 -3
- package/lib/index.js +191 -113
- package/lib/index.js.map +4 -4
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -1027,20 +1027,21 @@ async function draftMime(account, draft) {
|
|
|
1027
1027
|
return Buffer.from(info.message);
|
|
1028
1028
|
}
|
|
1029
1029
|
|
|
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";
|
|
1030
|
+
// src/host/references.ts
|
|
1037
1031
|
import { DatabaseSync as DatabaseSync3 } from "node:sqlite";
|
|
1038
1032
|
|
|
1039
1033
|
// src/host/mail-db.ts
|
|
1040
1034
|
init_address();
|
|
1041
|
-
init_inline_images();
|
|
1042
1035
|
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
|
|
1043
1036
|
import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
|
|
1037
|
+
|
|
1038
|
+
// src/host/subject-key.ts
|
|
1039
|
+
function subjectKey(subject) {
|
|
1040
|
+
return subject.replace(/^(\s*(re|fw|fwd|回复|答复|转发)\s*[::]\s*)+/gi, "").replace(/\s+/g, " ").trim().toLowerCase();
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
// src/host/mail-db.ts
|
|
1044
|
+
init_inline_images();
|
|
1044
1045
|
var SCHEMA = `
|
|
1045
1046
|
CREATE TABLE IF NOT EXISTS emails (
|
|
1046
1047
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
@@ -1125,6 +1126,8 @@ function ensureMailSchema(file) {
|
|
|
1125
1126
|
db.exec("ALTER TABLE emails ADD COLUMN refs_indexed INTEGER DEFAULT 0");
|
|
1126
1127
|
}
|
|
1127
1128
|
try {
|
|
1129
|
+
db.exec("CREATE TABLE IF NOT EXISTS email_subjects (email_id INTEGER PRIMARY KEY, key TEXT NOT NULL)");
|
|
1130
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_email_subjects_key ON email_subjects(key)");
|
|
1128
1131
|
db.exec("CREATE INDEX IF NOT EXISTS idx_emails_in_reply ON emails(in_reply_to)");
|
|
1129
1132
|
db.exec("CREATE INDEX IF NOT EXISTS idx_emails_message_id_lower ON emails(lower(message_id))");
|
|
1130
1133
|
} catch {
|
|
@@ -1463,64 +1466,45 @@ var MailDatabase = class {
|
|
|
1463
1466
|
this.threadCache.set(cacheKey, { at: Date.now(), items: result });
|
|
1464
1467
|
return result;
|
|
1465
1468
|
}
|
|
1466
|
-
/** Subject candidates
|
|
1469
|
+
/** Subject candidates from a normalized-key table, then FTS, then LIKE. */
|
|
1467
1470
|
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
1471
|
const select = `SELECT e.id, e.mailbox, e.uid, e.subject, e.from_name, e.from_addr, e.to_addr, e.cc,
|
|
1472
1472
|
e.message_id, e.date, length(e.body_text) AS text_length,
|
|
1473
1473
|
(SELECT COUNT(*) FROM attachments a WHERE a.email_id = e.id) AS attachment_count,
|
|
1474
1474
|
substr(COALESCE(e.body_text, ''), 1, 240) AS snippet
|
|
1475
1475
|
FROM emails e`;
|
|
1476
|
+
let items = [];
|
|
1476
1477
|
try {
|
|
1477
|
-
const
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
WHERE emails_fts MATCH ?
|
|
1478
|
+
const byKey = this.open().prepare(
|
|
1479
|
+
`${select} JOIN email_subjects s ON s.email_id = e.id
|
|
1480
|
+
WHERE s.key = ?
|
|
1481
1481
|
ORDER BY e.date DESC LIMIT 500`
|
|
1482
|
-
).all(
|
|
1483
|
-
|
|
1484
|
-
|
|
1482
|
+
).all(base);
|
|
1483
|
+
if (byKey.length > 0) {
|
|
1484
|
+
items = byKey.map((row) => rowToSummary(row));
|
|
1485
|
+
} else {
|
|
1486
|
+
const words = base.split(/[^\p{L}\p{N}]+/u).filter((word) => word.length >= 3);
|
|
1487
|
+
const token = words.sort((left, right) => right.length - left.length)[0] ?? base.slice(0, 8);
|
|
1488
|
+
const ftsQuery = `subject:"${token.replace(/"/g, '""')}"`;
|
|
1489
|
+
const rows = this.open().prepare(
|
|
1490
|
+
`${select} JOIN emails_fts f ON f.rowid = e.id
|
|
1491
|
+
WHERE emails_fts MATCH ?
|
|
1492
|
+
ORDER BY e.date DESC LIMIT 500`
|
|
1493
|
+
).all(ftsQuery);
|
|
1494
|
+
items = rows.map((row) => rowToSummary(row)).filter((item) => subjectKey(item.subject) === base);
|
|
1495
|
+
}
|
|
1496
|
+
} catch {
|
|
1497
|
+
const page = this.list({ mailbox: "all", query: base, limit: Math.min(500, limit * 2) });
|
|
1498
|
+
return page.items.filter((item) => subjectKey(item.subject) === base).slice(0, limit);
|
|
1499
|
+
}
|
|
1500
|
+
if (seedId !== "" && this.hasRefColumns()) {
|
|
1501
|
+
try {
|
|
1485
1502
|
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
|
|
1503
|
+
`${select}
|
|
1491
1504
|
WHERE e.in_reply_to = ?
|
|
1492
1505
|
ORDER BY e.date DESC LIMIT 100`
|
|
1493
1506
|
).all(seedId);
|
|
1494
1507
|
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
1508
|
for (const row of linked) {
|
|
1525
1509
|
const item = rowToSummary(row);
|
|
1526
1510
|
if (!known.has(item.id)) {
|
|
@@ -1528,12 +1512,33 @@ var MailDatabase = class {
|
|
|
1528
1512
|
known.add(item.id);
|
|
1529
1513
|
}
|
|
1530
1514
|
}
|
|
1515
|
+
const seedRow = this.open().prepare(
|
|
1516
|
+
'SELECT in_reply_to, "references", raw_eml FROM emails WHERE lower(message_id) = ? LIMIT 1'
|
|
1517
|
+
).get(seedId);
|
|
1518
|
+
const parents = `${seedRow?.references ?? ""} ${seedRow?.in_reply_to ?? ""}`.split(/\s+/).map((item) => item.replace(/^<|>$/g, "").toLowerCase()).filter((item) => item.includes("@"));
|
|
1519
|
+
if (parents.length === 0 && seedRow?.raw_eml !== void 0 && seedRow.raw_eml !== null) {
|
|
1520
|
+
for (const header of ["references", "in-reply-to"]) {
|
|
1521
|
+
for (const found of headerIds(seedRow.raw_eml, header)) parents.push(found);
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
if (parents.length > 0) {
|
|
1525
|
+
const marks = parents.map(() => "?").join(",");
|
|
1526
|
+
const ancestors = this.open().prepare(
|
|
1527
|
+
`${select}
|
|
1528
|
+
WHERE lower(e.message_id) IN (${marks}) LIMIT 100`
|
|
1529
|
+
).all(...parents);
|
|
1530
|
+
for (const row of ancestors) {
|
|
1531
|
+
const item = rowToSummary(row);
|
|
1532
|
+
if (!known.has(item.id)) {
|
|
1533
|
+
items.push(item);
|
|
1534
|
+
known.add(item.id);
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
} catch {
|
|
1531
1539
|
}
|
|
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
1540
|
}
|
|
1541
|
+
return items.slice(0, limit);
|
|
1537
1542
|
}
|
|
1538
1543
|
/** Read (and cache) In-Reply-To / References for a set of local ids. */
|
|
1539
1544
|
referencesFor(ids) {
|
|
@@ -1792,7 +1797,117 @@ function rowToSummary(row) {
|
|
|
1792
1797
|
};
|
|
1793
1798
|
}
|
|
1794
1799
|
|
|
1800
|
+
// src/host/references.ts
|
|
1801
|
+
function extractReferences(raw, messageId) {
|
|
1802
|
+
const empty = { inReplyTo: null, references: null };
|
|
1803
|
+
if (raw === null || raw === void 0) return empty;
|
|
1804
|
+
const text2 = Buffer.from(raw.subarray(0, Math.min(raw.byteLength, 2e5))).toString("utf8");
|
|
1805
|
+
const separator = text2.search(/\r?\n\r?\n/);
|
|
1806
|
+
const head = separator >= 0 ? text2.slice(0, separator) : text2;
|
|
1807
|
+
const unfolded = head.replace(/\r?\n[ \t]+/g, " ");
|
|
1808
|
+
const value = (name2) => {
|
|
1809
|
+
const match = new RegExp(`^${name2}:\\s*(.*)$`, "im").exec(unfolded);
|
|
1810
|
+
return match === null ? "" : String(match[1] ?? "");
|
|
1811
|
+
};
|
|
1812
|
+
const ids = (value2) => value2.split(/\s+/).map((item) => item.replace(/^<|>$/g, "").toLowerCase()).filter((item) => item.includes("@"));
|
|
1813
|
+
const referenceIds = ids(value("references"));
|
|
1814
|
+
const replyTo = ids(value("in-reply-to"));
|
|
1815
|
+
const filteredRefs = referenceIds.filter((id) => id !== messageId.toLowerCase());
|
|
1816
|
+
return {
|
|
1817
|
+
inReplyTo: replyTo[0] ?? null,
|
|
1818
|
+
references: filteredRefs.length > 0 ? filteredRefs.join(" ") : null
|
|
1819
|
+
};
|
|
1820
|
+
}
|
|
1821
|
+
function backfillReferences(file, batchSize = 300) {
|
|
1822
|
+
ensureMailSchema(file);
|
|
1823
|
+
const db = new DatabaseSync3(file);
|
|
1824
|
+
try {
|
|
1825
|
+
db.exec("PRAGMA busy_timeout = 10000");
|
|
1826
|
+
const rows = db.prepare("SELECT id, message_id, raw_eml FROM emails WHERE refs_indexed = 0 ORDER BY id LIMIT ?").all(batchSize);
|
|
1827
|
+
if (rows.length === 0) return { updated: 0, remaining: 0 };
|
|
1828
|
+
const update = db.prepare(
|
|
1829
|
+
'UPDATE emails SET in_reply_to = ?, "references" = ?, refs_indexed = 1 WHERE id = ?'
|
|
1830
|
+
);
|
|
1831
|
+
db.exec("BEGIN");
|
|
1832
|
+
try {
|
|
1833
|
+
for (const row of rows) {
|
|
1834
|
+
const headers = extractReferences(row.raw_eml, String(row.message_id ?? ""));
|
|
1835
|
+
update.run(headers.inReplyTo, headers.references, Number(row.id));
|
|
1836
|
+
}
|
|
1837
|
+
db.exec("COMMIT");
|
|
1838
|
+
} catch (error) {
|
|
1839
|
+
db.exec("ROLLBACK");
|
|
1840
|
+
throw error;
|
|
1841
|
+
}
|
|
1842
|
+
const remaining = Number(
|
|
1843
|
+
db.prepare("SELECT COUNT(*) AS count FROM emails WHERE refs_indexed = 0").get()?.count ?? 0
|
|
1844
|
+
);
|
|
1845
|
+
return { updated: rows.length, remaining };
|
|
1846
|
+
} finally {
|
|
1847
|
+
db.close();
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1850
|
+
function optimizeDatabase(file) {
|
|
1851
|
+
ensureMailSchema(file);
|
|
1852
|
+
const db = new DatabaseSync3(file);
|
|
1853
|
+
try {
|
|
1854
|
+
db.exec("PRAGMA busy_timeout = 10000");
|
|
1855
|
+
db.exec("PRAGMA optimize");
|
|
1856
|
+
try {
|
|
1857
|
+
db.exec("INSERT INTO emails_fts(emails_fts) VALUES('optimize')");
|
|
1858
|
+
} catch {
|
|
1859
|
+
}
|
|
1860
|
+
try {
|
|
1861
|
+
db.exec("PRAGMA wal_checkpoint(TRUNCATE)");
|
|
1862
|
+
} catch {
|
|
1863
|
+
}
|
|
1864
|
+
return { ok: true };
|
|
1865
|
+
} catch (error) {
|
|
1866
|
+
return { ok: false, error: error instanceof Error ? error.message : String(error) };
|
|
1867
|
+
} finally {
|
|
1868
|
+
db.close();
|
|
1869
|
+
}
|
|
1870
|
+
}
|
|
1871
|
+
function backfillSubjects(file, batchSize = 300) {
|
|
1872
|
+
ensureMailSchema(file);
|
|
1873
|
+
const db = new DatabaseSync3(file);
|
|
1874
|
+
try {
|
|
1875
|
+
db.exec("PRAGMA busy_timeout = 10000");
|
|
1876
|
+
const rows = db.prepare(
|
|
1877
|
+
`SELECT e.id, e.subject FROM emails e
|
|
1878
|
+
WHERE NOT EXISTS (SELECT 1 FROM email_subjects s WHERE s.email_id = e.id)
|
|
1879
|
+
ORDER BY e.id LIMIT ?`
|
|
1880
|
+
).all(batchSize);
|
|
1881
|
+
if (rows.length === 0) return { updated: 0, remaining: 0 };
|
|
1882
|
+
const insert = db.prepare("INSERT OR REPLACE INTO email_subjects (email_id, key) VALUES (?, ?)");
|
|
1883
|
+
db.exec("BEGIN");
|
|
1884
|
+
try {
|
|
1885
|
+
for (const row of rows) insert.run(Number(row.id), subjectKey(String(row.subject ?? "")));
|
|
1886
|
+
db.exec("COMMIT");
|
|
1887
|
+
} catch (error) {
|
|
1888
|
+
db.exec("ROLLBACK");
|
|
1889
|
+
throw error;
|
|
1890
|
+
}
|
|
1891
|
+
const remaining = Number(
|
|
1892
|
+
db.prepare(
|
|
1893
|
+
`SELECT COUNT(*) AS count FROM emails e
|
|
1894
|
+
WHERE NOT EXISTS (SELECT 1 FROM email_subjects s WHERE s.email_id = e.id)`
|
|
1895
|
+
).get()?.count ?? 0
|
|
1896
|
+
);
|
|
1897
|
+
return { updated: rows.length, remaining };
|
|
1898
|
+
} finally {
|
|
1899
|
+
db.close();
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1795
1903
|
// src/host/backup.ts
|
|
1904
|
+
init_address();
|
|
1905
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
1906
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
1907
|
+
import { join as join3 } from "node:path";
|
|
1908
|
+
import { ImapFlow } from "imapflow";
|
|
1909
|
+
import { simpleParser } from "mailparser";
|
|
1910
|
+
import { DatabaseSync as DatabaseSync4 } from "node:sqlite";
|
|
1796
1911
|
var BATCH = 25;
|
|
1797
1912
|
function sha256(data) {
|
|
1798
1913
|
return createHash2("sha256").update(data).digest("hex");
|
|
@@ -1874,7 +1989,7 @@ var BackupEngine = class {
|
|
|
1874
1989
|
}
|
|
1875
1990
|
openDb() {
|
|
1876
1991
|
ensureMailSchema(this.runtime.mailDb);
|
|
1877
|
-
const db = new
|
|
1992
|
+
const db = new DatabaseSync4(this.runtime.mailDb);
|
|
1878
1993
|
db.exec("PRAGMA journal_mode=WAL");
|
|
1879
1994
|
db.exec("PRAGMA busy_timeout=10000");
|
|
1880
1995
|
db.exec("PRAGMA synchronous=NORMAL");
|
|
@@ -1923,6 +2038,10 @@ var BackupEngine = class {
|
|
|
1923
2038
|
);
|
|
1924
2039
|
if (inserted.changes === 0) return 0;
|
|
1925
2040
|
const emailId = Number(inserted.lastInsertRowid);
|
|
2041
|
+
db.prepare("INSERT OR REPLACE INTO email_subjects (email_id, key) VALUES (?, ?)").run(
|
|
2042
|
+
emailId,
|
|
2043
|
+
subjectKey(subject)
|
|
2044
|
+
);
|
|
1926
2045
|
const attachments = parsed.attachments ?? [];
|
|
1927
2046
|
const insertAttachment = db.prepare(
|
|
1928
2047
|
"INSERT INTO attachments (email_id, filename, content_type, size, hash, content_id, disposition) VALUES (?, ?, ?, ?, ?, ?, ?)"
|
|
@@ -2128,7 +2247,7 @@ var BackupEngine = class {
|
|
|
2128
2247
|
};
|
|
2129
2248
|
if (!existsSync3(dbFile)) return empty;
|
|
2130
2249
|
try {
|
|
2131
|
-
const db = new
|
|
2250
|
+
const db = new DatabaseSync4(dbFile, { readOnly: true });
|
|
2132
2251
|
try {
|
|
2133
2252
|
const latest = db.prepare("SELECT * FROM backup_logs ORDER BY id DESC LIMIT 1").get();
|
|
2134
2253
|
const success = db.prepare("SELECT completed_at FROM backup_logs WHERE status = 'completed' ORDER BY id DESC LIMIT 1").get();
|
|
@@ -3004,7 +3123,7 @@ ${conversation.trim()}
|
|
|
3004
3123
|
}
|
|
3005
3124
|
|
|
3006
3125
|
// src/host/version.ts
|
|
3007
|
-
var VERSION = "0.
|
|
3126
|
+
var VERSION = "0.20.0";
|
|
3008
3127
|
|
|
3009
3128
|
// src/host/api.ts
|
|
3010
3129
|
init_smtp();
|
|
@@ -3226,6 +3345,10 @@ function buildRouter(runtime) {
|
|
|
3226
3345
|
}
|
|
3227
3346
|
return { ok: true, count };
|
|
3228
3347
|
});
|
|
3348
|
+
router.post("/maintenance/optimize", ({ url }) => {
|
|
3349
|
+
const { account } = accountFrom(runtime, url);
|
|
3350
|
+
return optimizeDatabase(runtime.paths(account).mailDb);
|
|
3351
|
+
});
|
|
3229
3352
|
router.get("/maintenance/references", ({ url }) => {
|
|
3230
3353
|
const { account } = accountFrom(runtime, url);
|
|
3231
3354
|
return runtime.referencesStatus(account);
|
|
@@ -4276,7 +4399,7 @@ function writeAttachment(res, filename, contentType, data, inline = false) {
|
|
|
4276
4399
|
"content-disposition",
|
|
4277
4400
|
`${inline ? "inline" : "attachment"}; filename*=UTF-8''${encodeURIComponent(filename)}`
|
|
4278
4401
|
);
|
|
4279
|
-
res.setHeader("cache-control", "
|
|
4402
|
+
res.setHeader("cache-control", "private, max-age=86400");
|
|
4280
4403
|
res.setHeader("x-content-type-options", "nosniff");
|
|
4281
4404
|
res.setHeader(
|
|
4282
4405
|
"content-security-policy",
|
|
@@ -4730,58 +4853,6 @@ var MailFlagsStore = class {
|
|
|
4730
4853
|
}
|
|
4731
4854
|
};
|
|
4732
4855
|
|
|
4733
|
-
// src/host/references.ts
|
|
4734
|
-
import { DatabaseSync as DatabaseSync4 } from "node:sqlite";
|
|
4735
|
-
function extractReferences(raw, messageId) {
|
|
4736
|
-
const empty = { inReplyTo: null, references: null };
|
|
4737
|
-
if (raw === null || raw === void 0) return empty;
|
|
4738
|
-
const text2 = Buffer.from(raw.subarray(0, Math.min(raw.byteLength, 2e5))).toString("utf8");
|
|
4739
|
-
const separator = text2.search(/\r?\n\r?\n/);
|
|
4740
|
-
const head = separator >= 0 ? text2.slice(0, separator) : text2;
|
|
4741
|
-
const unfolded = head.replace(/\r?\n[ \t]+/g, " ");
|
|
4742
|
-
const value = (name2) => {
|
|
4743
|
-
const match = new RegExp(`^${name2}:\\s*(.*)$`, "im").exec(unfolded);
|
|
4744
|
-
return match === null ? "" : String(match[1] ?? "");
|
|
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");
|
|
4766
|
-
try {
|
|
4767
|
-
for (const row of rows) {
|
|
4768
|
-
const headers = extractReferences(row.raw_eml, String(row.message_id ?? ""));
|
|
4769
|
-
update.run(headers.inReplyTo, headers.references, Number(row.id));
|
|
4770
|
-
}
|
|
4771
|
-
db.exec("COMMIT");
|
|
4772
|
-
} catch (error) {
|
|
4773
|
-
db.exec("ROLLBACK");
|
|
4774
|
-
throw error;
|
|
4775
|
-
}
|
|
4776
|
-
const remaining = Number(
|
|
4777
|
-
db.prepare("SELECT COUNT(*) AS count FROM emails WHERE refs_indexed = 0").get()?.count ?? 0
|
|
4778
|
-
);
|
|
4779
|
-
return { updated: rows.length, remaining };
|
|
4780
|
-
} finally {
|
|
4781
|
-
db.close();
|
|
4782
|
-
}
|
|
4783
|
-
}
|
|
4784
|
-
|
|
4785
4856
|
// src/host/tasks.ts
|
|
4786
4857
|
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
4787
4858
|
import { mkdirSync as mkdirSync7 } from "node:fs";
|
|
@@ -5391,6 +5462,13 @@ var MailHarborRuntime = class {
|
|
|
5391
5462
|
if (result.remaining <= 0 || result.updated === 0) break;
|
|
5392
5463
|
await new Promise((resolve) => setTimeout(resolve, 40));
|
|
5393
5464
|
}
|
|
5465
|
+
for (; ; ) {
|
|
5466
|
+
const result = backfillSubjects(dbFile, 300);
|
|
5467
|
+
status.indexed += result.updated;
|
|
5468
|
+
status.remaining = result.remaining;
|
|
5469
|
+
if (result.remaining <= 0 || result.updated === 0) break;
|
|
5470
|
+
await new Promise((resolve) => setTimeout(resolve, 40));
|
|
5471
|
+
}
|
|
5394
5472
|
} catch (error) {
|
|
5395
5473
|
status.error = error instanceof Error ? error.message : String(error);
|
|
5396
5474
|
} finally {
|