@zaclinley/dsh-mailharbor 0.12.1 → 0.12.2
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 +14 -0
- package/lib/client.js +52 -8
- package/lib/client.js.map +2 -2
- package/lib/index.js +129 -18
- package/lib/index.js.map +3 -3
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -1101,6 +1101,23 @@ var MailDatabase = class {
|
|
|
1101
1101
|
where.push("e.date >= ?");
|
|
1102
1102
|
params.push(query.since);
|
|
1103
1103
|
}
|
|
1104
|
+
if (query.unread !== void 0) {
|
|
1105
|
+
const mids = query.unread.mids.slice(0, 500);
|
|
1106
|
+
const ids = query.unread.ids.slice(0, 500);
|
|
1107
|
+
if (mids.length === 0 && ids.length === 0) {
|
|
1108
|
+
return { items: [], total: 0, offset, limit };
|
|
1109
|
+
}
|
|
1110
|
+
const parts = [];
|
|
1111
|
+
if (mids.length > 0) {
|
|
1112
|
+
parts.push(`e.message_id IN (${mids.map(() => "?").join(",")})`);
|
|
1113
|
+
params.push(...mids);
|
|
1114
|
+
}
|
|
1115
|
+
if (ids.length > 0) {
|
|
1116
|
+
parts.push(`e.id IN (${ids.map(() => "?").join(",")})`);
|
|
1117
|
+
params.push(...ids);
|
|
1118
|
+
}
|
|
1119
|
+
where.push(`(${parts.join(" OR ")})`);
|
|
1120
|
+
}
|
|
1104
1121
|
const text2 = String(query.query ?? "").trim();
|
|
1105
1122
|
if (text2 !== "") {
|
|
1106
1123
|
const tokens = text2.split(/\s+/).filter(Boolean);
|
|
@@ -1187,6 +1204,28 @@ var MailDatabase = class {
|
|
|
1187
1204
|
const rows = db.prepare(`${columns} FROM emails e ${clause} ORDER BY e.date DESC, e.id DESC LIMIT ?`).all(...params, limit);
|
|
1188
1205
|
return { items: rows.map((row) => rowToSummary(row)), total, offset: 0, limit };
|
|
1189
1206
|
}
|
|
1207
|
+
/** Number of unread keys that still exist in this backup. */
|
|
1208
|
+
countUnread(keys) {
|
|
1209
|
+
const mids = keys.mids.slice(0, 500);
|
|
1210
|
+
const ids = keys.ids.slice(0, 500);
|
|
1211
|
+
if (mids.length === 0 && ids.length === 0) return 0;
|
|
1212
|
+
const parts = [];
|
|
1213
|
+
const params = [];
|
|
1214
|
+
if (mids.length > 0) {
|
|
1215
|
+
parts.push(`message_id IN (${mids.map(() => "?").join(",")})`);
|
|
1216
|
+
params.push(...mids);
|
|
1217
|
+
}
|
|
1218
|
+
if (ids.length > 0) {
|
|
1219
|
+
parts.push(`id IN (${ids.map(() => "?").join(",")})`);
|
|
1220
|
+
params.push(...ids);
|
|
1221
|
+
}
|
|
1222
|
+
try {
|
|
1223
|
+
const row = this.open().prepare(`SELECT COUNT(*) AS count FROM emails WHERE ${parts.join(" OR ")}`).get(...params);
|
|
1224
|
+
return Number(row?.count ?? 0);
|
|
1225
|
+
} catch {
|
|
1226
|
+
return 0;
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1190
1229
|
get(localId) {
|
|
1191
1230
|
const db = this.open();
|
|
1192
1231
|
const row = db.prepare(
|
|
@@ -1366,10 +1405,57 @@ var BackupEngine = class {
|
|
|
1366
1405
|
runtime;
|
|
1367
1406
|
credentials;
|
|
1368
1407
|
batchSize;
|
|
1408
|
+
onFlags;
|
|
1369
1409
|
constructor(runtime, credentials, options = {}) {
|
|
1370
1410
|
this.runtime = runtime;
|
|
1371
1411
|
this.credentials = credentials;
|
|
1372
1412
|
this.batchSize = options.batchSize ?? BATCH;
|
|
1413
|
+
this.onFlags = options.onFlags;
|
|
1414
|
+
}
|
|
1415
|
+
/** Mirror IMAP \Seen flags into the local unread store. */
|
|
1416
|
+
async reconcileReadState(client) {
|
|
1417
|
+
if (this.onFlags === void 0) return;
|
|
1418
|
+
const exists = Number(client.mailbox?.exists ?? 0);
|
|
1419
|
+
if (exists === 0) return;
|
|
1420
|
+
const entries = /* @__PURE__ */ new Map();
|
|
1421
|
+
const add = (messageId, unread) => {
|
|
1422
|
+
if (messageId !== "") entries.set(`mid:${messageId}`, unread);
|
|
1423
|
+
};
|
|
1424
|
+
const flagsOf2 = (flags) => [...flags ?? []].map((flag) => String(flag).toLowerCase());
|
|
1425
|
+
const start = Math.max(1, exists - 299);
|
|
1426
|
+
try {
|
|
1427
|
+
for await (const message of client.fetch(`${String(start)}:${String(exists)}`, {
|
|
1428
|
+
uid: true,
|
|
1429
|
+
envelope: true,
|
|
1430
|
+
flags: true
|
|
1431
|
+
})) {
|
|
1432
|
+
const messageId = normalizeId(message.envelope?.messageId);
|
|
1433
|
+
add(messageId, !flagsOf2(message.flags).includes("\\seen"));
|
|
1434
|
+
}
|
|
1435
|
+
} catch {
|
|
1436
|
+
}
|
|
1437
|
+
try {
|
|
1438
|
+
const unseen = await client.search({ seen: false }, { uid: true });
|
|
1439
|
+
const uids = (Array.isArray(unseen) ? unseen : []).map(Number).filter((uid) => Number.isFinite(uid));
|
|
1440
|
+
for (let index = 0; index < uids.length; index += 200) {
|
|
1441
|
+
const range = uids.slice(index, index + 200).join(",");
|
|
1442
|
+
if (range === "") continue;
|
|
1443
|
+
for await (const message of client.fetch(
|
|
1444
|
+
range,
|
|
1445
|
+
{ uid: true, envelope: true, flags: true },
|
|
1446
|
+
{ uid: true }
|
|
1447
|
+
)) {
|
|
1448
|
+
const messageId = normalizeId(
|
|
1449
|
+
message.envelope?.messageId
|
|
1450
|
+
);
|
|
1451
|
+
add(messageId, true);
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
} catch {
|
|
1455
|
+
}
|
|
1456
|
+
if (entries.size > 0) {
|
|
1457
|
+
this.onFlags([...entries.entries()].map(([key, unread]) => ({ key, unread })));
|
|
1458
|
+
}
|
|
1373
1459
|
}
|
|
1374
1460
|
attachmentsDir() {
|
|
1375
1461
|
return this.runtime.attachments;
|
|
@@ -1518,6 +1604,7 @@ var BackupEngine = class {
|
|
|
1518
1604
|
abortIfNeeded(signal);
|
|
1519
1605
|
const batch = uids.slice(offset, offset + this.batchSize);
|
|
1520
1606
|
db.exec("BEGIN");
|
|
1607
|
+
const flagEntries = [];
|
|
1521
1608
|
try {
|
|
1522
1609
|
let batchLastUid = committedUid;
|
|
1523
1610
|
for await (const message of client.fetch(
|
|
@@ -1538,6 +1625,13 @@ var BackupEngine = class {
|
|
|
1538
1625
|
boxAdded += added;
|
|
1539
1626
|
newCount += added;
|
|
1540
1627
|
batchLastUid = Math.max(batchLastUid, Number(message.uid));
|
|
1628
|
+
const messageId = normalizeId(parsed.messageId ?? "");
|
|
1629
|
+
if (messageId !== "") {
|
|
1630
|
+
const flags = [...message.flags ?? []].map(
|
|
1631
|
+
(flag) => String(flag).toLowerCase()
|
|
1632
|
+
);
|
|
1633
|
+
flagEntries.push({ key: `mid:${messageId}`, unread: !flags.includes("\\seen") });
|
|
1634
|
+
}
|
|
1541
1635
|
} catch (error) {
|
|
1542
1636
|
failCount += 1;
|
|
1543
1637
|
emit({
|
|
@@ -1548,6 +1642,7 @@ var BackupEngine = class {
|
|
|
1548
1642
|
}
|
|
1549
1643
|
this.saveState(db, path, uidValidity, batchLastUid);
|
|
1550
1644
|
db.exec("COMMIT");
|
|
1645
|
+
if (flagEntries.length > 0) this.onFlags?.(flagEntries);
|
|
1551
1646
|
committedUid = batchLastUid;
|
|
1552
1647
|
emit({ mailbox: path, added: boxAdded, message: `${path}: \u5DF2\u65B0\u589E ${String(boxAdded)} \u5C01` });
|
|
1553
1648
|
} catch (error) {
|
|
@@ -1555,6 +1650,7 @@ var BackupEngine = class {
|
|
|
1555
1650
|
throw error;
|
|
1556
1651
|
}
|
|
1557
1652
|
}
|
|
1653
|
+
if (path.toUpperCase() === "INBOX") await this.reconcileReadState(client);
|
|
1558
1654
|
mailboxes.push({ name: path, added: boxAdded });
|
|
1559
1655
|
emit({ mailbox: path, added: boxAdded, message: `${path}: \u65B0\u589E ${String(boxAdded)} \u5C01` });
|
|
1560
1656
|
} finally {
|
|
@@ -2494,7 +2590,7 @@ ${conversation.trim()}
|
|
|
2494
2590
|
}
|
|
2495
2591
|
|
|
2496
2592
|
// src/host/version.ts
|
|
2497
|
-
var VERSION = "0.12.
|
|
2593
|
+
var VERSION = "0.12.2";
|
|
2498
2594
|
|
|
2499
2595
|
// src/host/api.ts
|
|
2500
2596
|
init_smtp();
|
|
@@ -2597,7 +2693,14 @@ function buildRouter(runtime) {
|
|
|
2597
2693
|
const items = await listServer(runtime, account, mailbox, query, limit, offset);
|
|
2598
2694
|
return { ...items, offset, limit };
|
|
2599
2695
|
}
|
|
2600
|
-
const
|
|
2696
|
+
const unreadOnly = url.searchParams.get("unread") === "1";
|
|
2697
|
+
const page = runtime.database(account).list({
|
|
2698
|
+
mailbox: mailbox === "" ? "all" : mailbox,
|
|
2699
|
+
query,
|
|
2700
|
+
offset,
|
|
2701
|
+
limit,
|
|
2702
|
+
...unreadOnly ? { unread: unreadKeys(runtime, account) } : {}
|
|
2703
|
+
});
|
|
2601
2704
|
return { ...page, items: page.items.map((item) => localSummary(runtime, account, item)) };
|
|
2602
2705
|
});
|
|
2603
2706
|
router.get("/messages/lookup", ({ url }) => {
|
|
@@ -2693,21 +2796,13 @@ function buildRouter(runtime) {
|
|
|
2693
2796
|
router.get("/unread", async ({ url }) => {
|
|
2694
2797
|
const requested = url.searchParams.get("accountId");
|
|
2695
2798
|
const accounts = requested === null || requested === "" || requested === "all" ? runtime.current.accounts.filter((item) => item.enabled !== false) : [accountById(runtime, requested)];
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
server += result.count;
|
|
2704
|
-
answered += 1;
|
|
2705
|
-
} catch {
|
|
2706
|
-
}
|
|
2707
|
-
})
|
|
2708
|
-
);
|
|
2709
|
-
if (answered > 0) return { unread: server, source: "server" };
|
|
2710
|
-
const local = accounts.reduce((sum, account) => sum + runtime.readState(account).count(), 0);
|
|
2799
|
+
const local = accounts.reduce((sum, account) => {
|
|
2800
|
+
try {
|
|
2801
|
+
return sum + runtime.database(account).countUnread(unreadKeys(runtime, account));
|
|
2802
|
+
} catch {
|
|
2803
|
+
return sum;
|
|
2804
|
+
}
|
|
2805
|
+
}, 0);
|
|
2711
2806
|
return { unread: local, source: "local" };
|
|
2712
2807
|
});
|
|
2713
2808
|
router.get("/accounts", () => ({ accounts: runtime.current.accounts }));
|
|
@@ -3400,6 +3495,18 @@ function projectView(project) {
|
|
|
3400
3495
|
updatedAt: project.updatedAt
|
|
3401
3496
|
};
|
|
3402
3497
|
}
|
|
3498
|
+
function unreadKeys(runtime, account) {
|
|
3499
|
+
const mids = [];
|
|
3500
|
+
const ids = [];
|
|
3501
|
+
for (const key of runtime.readState(account).list()) {
|
|
3502
|
+
if (key.startsWith("mid:")) mids.push(key.slice(4));
|
|
3503
|
+
else if (key.startsWith("local:")) {
|
|
3504
|
+
const id = Number(key.slice(6));
|
|
3505
|
+
if (Number.isFinite(id)) ids.push(id);
|
|
3506
|
+
}
|
|
3507
|
+
}
|
|
3508
|
+
return { mids, ids };
|
|
3509
|
+
}
|
|
3403
3510
|
function localSummary(runtime, account, row) {
|
|
3404
3511
|
const store = runtime.readState(account);
|
|
3405
3512
|
const key = row.messageId !== void 0 && row.messageId !== "" ? `mid:${row.messageId}` : `local:${row.id}`;
|
|
@@ -4542,7 +4649,11 @@ var MailHarborRuntime = class {
|
|
|
4542
4649
|
exitCode: null
|
|
4543
4650
|
};
|
|
4544
4651
|
const job = { status, controller, promise: Promise.resolve() };
|
|
4545
|
-
const engine = new BackupEngine(accountRuntime(account), this.credentialsFor(account)
|
|
4652
|
+
const engine = new BackupEngine(accountRuntime(account), this.credentialsFor(account), {
|
|
4653
|
+
onFlags: (entries) => {
|
|
4654
|
+
this.readState(account).sync(entries);
|
|
4655
|
+
}
|
|
4656
|
+
});
|
|
4546
4657
|
job.promise = (async () => {
|
|
4547
4658
|
try {
|
|
4548
4659
|
const result = await engine.run({
|