@bobfrankston/rmfmail 1.1.239 → 1.1.240
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/package.json +1 -1
- package/packages/mailx-imap/index.d.ts.map +1 -1
- package/packages/mailx-imap/index.js +97 -99
- package/packages/mailx-imap/index.js.map +1 -1
- package/packages/mailx-imap/index.ts +106 -96
- package/packages/mailx-imap/package-lock.json +2 -2
- package/packages/mailx-imap/package.json +1 -1
|
@@ -1017,14 +1017,24 @@ export class ImapManager extends EventEmitter {
|
|
|
1017
1017
|
// worth of latency instead of the full loop's worth.
|
|
1018
1018
|
const BATCH_SIZE = 50;
|
|
1019
1019
|
let stored = 0;
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1020
|
+
// Two-phase batching. The async I/O (write .eml to disk, parse the body
|
|
1021
|
+
// for preview/FTS) runs OUTSIDE any transaction; only the synchronous
|
|
1022
|
+
// upserts run inside one. The previous version held a raw BEGIN open
|
|
1023
|
+
// across `await putMessage` / `await extractPreview`, so a concurrent
|
|
1024
|
+
// storeMessages or the contacts walker could hit BEGIN on the same
|
|
1025
|
+
// connection mid-await → "cannot start a transaction within a
|
|
1026
|
+
// transaction". That threw out the whole batch: messages stored with
|
|
1027
|
+
// empty From/Subject AND the local high-water never advanced, so the
|
|
1028
|
+
// same UIDs re-fetched forever (Bob 2026-06-11: npm notice with blank
|
|
1029
|
+
// From/Subject, its UID re-fetched 51×, sync wedged). Keeping every
|
|
1030
|
+
// `await` out of the transaction makes the collision structurally
|
|
1031
|
+
// impossible, and runInTxn() is nesting-safe besides.
|
|
1032
|
+
type PreparedRow = { upsert: Parameters<MailxDB["upsertMessage"]>[0]; ftsBody: string };
|
|
1033
|
+
for (let i = 0; i < msgs.length; i += BATCH_SIZE) {
|
|
1034
|
+
const chunk = msgs.slice(i, i + BATCH_SIZE);
|
|
1035
|
+
// Phase 1 — async prep, NO transaction held.
|
|
1036
|
+
const prepared: PreparedRow[] = [];
|
|
1037
|
+
for (const msg of chunk) {
|
|
1028
1038
|
// Debug: log subjects with non-ASCII to trace encoding issues
|
|
1029
1039
|
if (msg.subject && /[^\x00-\x7F]/.test(msg.subject)) {
|
|
1030
1040
|
const hex = Buffer.from(msg.subject, "utf-8").subarray(0, 40).toString("hex");
|
|
@@ -1070,41 +1080,43 @@ export class ImapManager extends EventEmitter {
|
|
|
1070
1080
|
if (msg.flagged) flags.push("\\Flagged");
|
|
1071
1081
|
if (msg.answered) flags.push("\\Answered");
|
|
1072
1082
|
if (msg.draft) flags.push("\\Draft");
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1083
|
+
prepared.push({
|
|
1084
|
+
upsert: {
|
|
1085
|
+
accountId, folderId, uid: msg.uid,
|
|
1086
|
+
messageId: msg.messageId || "",
|
|
1087
|
+
inReplyTo: (msg as any).inReplyTo || "",
|
|
1088
|
+
references: [],
|
|
1089
|
+
date: toFiniteDateMs(msg.date),
|
|
1090
|
+
subject: msg.subject || "",
|
|
1091
|
+
from: toEmailAddress(msg.from?.[0] || {}),
|
|
1092
|
+
to: toEmailAddresses(msg.to || []),
|
|
1093
|
+
cc: toEmailAddresses(msg.cc || []),
|
|
1094
|
+
flags, size: msg.size || 0, hasAttachments, preview, bodyPath,
|
|
1095
|
+
},
|
|
1096
|
+
ftsBody,
|
|
1084
1097
|
});
|
|
1085
|
-
// Index the full body for search. extractPreview already
|
|
1086
|
-
// parsed `source`, so the body text is free here — and
|
|
1087
|
-
// indexing it at sync time means a word buried in the body
|
|
1088
|
-
// is searchable without the user first opening the message.
|
|
1089
|
-
if (ftsRowId && ftsBody) this.db.updateFtsBody(ftsRowId, ftsBody);
|
|
1090
|
-
stored++;
|
|
1091
|
-
batchCount++;
|
|
1092
|
-
if (batchCount >= BATCH_SIZE) {
|
|
1093
|
-
commitTxn();
|
|
1094
|
-
// Hand the event loop to any waiting IPC (user clicks,
|
|
1095
|
-
// outbox drains, alarm polls). setImmediate runs AFTER
|
|
1096
|
-
// pending I/O callbacks, so a stack of getMessage IPCs
|
|
1097
|
-
// that arrived during the previous chunk gets serviced
|
|
1098
|
-
// before we start the next batch.
|
|
1099
|
-
await new Promise<void>(r => setImmediate(r));
|
|
1100
|
-
batchCount = 0;
|
|
1101
|
-
startTxn();
|
|
1102
|
-
}
|
|
1103
1098
|
}
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1099
|
+
// Phase 2 — synchronous transaction, no awaits inside. runInTxn
|
|
1100
|
+
// owns its own BEGIN/COMMIT (or joins an already-open txn) and
|
|
1101
|
+
// ROLLBACKs on any throw, so a bad row can't leak an open txn.
|
|
1102
|
+
try {
|
|
1103
|
+
this.db.runInTxn(() => {
|
|
1104
|
+
for (const r of prepared) {
|
|
1105
|
+
const ftsRowId = this.db.upsertMessage(r.upsert);
|
|
1106
|
+
// Index the full body for search. extractPreview already
|
|
1107
|
+
// parsed `source`, so the body text is free here — and
|
|
1108
|
+
// indexing it at sync time means a word buried in the
|
|
1109
|
+
// body is searchable without first opening the message.
|
|
1110
|
+
if (ftsRowId && r.ftsBody) this.db.updateFtsBody(ftsRowId, r.ftsBody);
|
|
1111
|
+
}
|
|
1112
|
+
});
|
|
1113
|
+
stored += prepared.length;
|
|
1114
|
+
} catch (e: any) {
|
|
1115
|
+
console.error(` storeMessages error: ${e.message}`);
|
|
1116
|
+
}
|
|
1117
|
+
// Hand the event loop to any waiting IPC (user clicks, outbox
|
|
1118
|
+
// drains, alarm polls) between batches.
|
|
1119
|
+
await new Promise<void>(r => setImmediate(r));
|
|
1108
1120
|
}
|
|
1109
1121
|
return stored;
|
|
1110
1122
|
}
|
|
@@ -1594,50 +1606,42 @@ export class ImapManager extends EventEmitter {
|
|
|
1594
1606
|
|
|
1595
1607
|
for (let batchStart = 0; batchStart < messages.length; batchStart += batchSize) {
|
|
1596
1608
|
const batchEnd = Math.min(batchStart + batchSize, messages.length);
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
const flags: string[] = [];
|
|
1634
|
-
if (msg.seen) flags.push("\\Seen");
|
|
1635
|
-
if (msg.flagged) flags.push("\\Flagged");
|
|
1636
|
-
if (msg.answered) flags.push("\\Answered");
|
|
1637
|
-
if (msg.draft) flags.push("\\Draft");
|
|
1638
|
-
|
|
1639
|
-
// Store metadata
|
|
1640
|
-
const ftsRowId = this.db.upsertMessage({
|
|
1609
|
+
// Phase 1 — async prep (write .eml, parse preview), NO transaction
|
|
1610
|
+
// held. The previous version kept a raw BEGIN open across
|
|
1611
|
+
// `await putMessage` / `await extractPreview`, so a concurrent
|
|
1612
|
+
// store on the same connection could hit BEGIN mid-await →
|
|
1613
|
+
// "cannot start a transaction within a transaction", wedging the
|
|
1614
|
+
// sync. Same fix + cause as storeMessages above (Bob 2026-06-11).
|
|
1615
|
+
//
|
|
1616
|
+
// NOTE: the old `if (msg.uid <= highestUid) continue` skip is
|
|
1617
|
+
// intentionally gone — gap-fills recover UIDs in the scanned range,
|
|
1618
|
+
// and upsertMessage's UNIQUE(account,folder,uid) constraint dedupes
|
|
1619
|
+
// (truly-present rows become a cheap UPDATE).
|
|
1620
|
+
const prepared: Array<{ upsert: Parameters<MailxDB["upsertMessage"]>[0]; ftsBody: string }> = [];
|
|
1621
|
+
for (let i = batchStart; i < batchEnd; i++) {
|
|
1622
|
+
const msg = messages[i];
|
|
1623
|
+
// Tombstone-skip: in-flight local delete/move awaiting server
|
|
1624
|
+
// confirmation (cleared on success or permanent failure).
|
|
1625
|
+
if (msg.messageId && this.db.hasTombstone(accountId, msg.messageId)) {
|
|
1626
|
+
console.log(` [tombstone] ${accountId}: skipping ${msg.messageId} in syncFolder (locally deleted)`);
|
|
1627
|
+
continue;
|
|
1628
|
+
}
|
|
1629
|
+
const source = msg.source || "";
|
|
1630
|
+
let bodyPath = "";
|
|
1631
|
+
if (source) {
|
|
1632
|
+
bodyPath = await this.bodyStore.putMessage(
|
|
1633
|
+
accountId, folderId, msg.uid,
|
|
1634
|
+
Buffer.from(source, "utf-8")
|
|
1635
|
+
);
|
|
1636
|
+
}
|
|
1637
|
+
const parsed = await extractPreview(source);
|
|
1638
|
+
const flags: string[] = [];
|
|
1639
|
+
if (msg.seen) flags.push("\\Seen");
|
|
1640
|
+
if (msg.flagged) flags.push("\\Flagged");
|
|
1641
|
+
if (msg.answered) flags.push("\\Answered");
|
|
1642
|
+
if (msg.draft) flags.push("\\Draft");
|
|
1643
|
+
prepared.push({
|
|
1644
|
+
upsert: {
|
|
1641
1645
|
accountId,
|
|
1642
1646
|
folderId,
|
|
1643
1647
|
uid: msg.uid,
|
|
@@ -1653,19 +1657,25 @@ export class ImapManager extends EventEmitter {
|
|
|
1653
1657
|
size: msg.size || 0,
|
|
1654
1658
|
hasAttachments: parsed.hasAttachments,
|
|
1655
1659
|
preview: parsed.preview,
|
|
1656
|
-
bodyPath
|
|
1657
|
-
}
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1660
|
+
bodyPath,
|
|
1661
|
+
},
|
|
1662
|
+
ftsBody: parsed.bodyText || "",
|
|
1663
|
+
});
|
|
1664
|
+
}
|
|
1665
|
+
// Phase 2 — synchronous transaction, no awaits inside. runInTxn
|
|
1666
|
+
// owns BEGIN/COMMIT (or joins an open txn) and ROLLBACKs on throw.
|
|
1667
|
+
try {
|
|
1668
|
+
this.db.runInTxn(() => {
|
|
1669
|
+
for (const r of prepared) {
|
|
1670
|
+
const ftsRowId = this.db.upsertMessage(r.upsert);
|
|
1671
|
+
// Full body into FTS — parse above already produced the
|
|
1672
|
+
// text, so search covers unopened messages too.
|
|
1673
|
+
if (ftsRowId && r.ftsBody) this.db.updateFtsBody(ftsRowId, r.ftsBody);
|
|
1674
|
+
}
|
|
1675
|
+
});
|
|
1676
|
+
newCount += prepared.length;
|
|
1666
1677
|
} catch (e: any) {
|
|
1667
1678
|
console.error(` transaction error: ${e.message}`);
|
|
1668
|
-
this.db.rollbackTransaction();
|
|
1669
1679
|
throw e;
|
|
1670
1680
|
}
|
|
1671
1681
|
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bobfrankston/mailx-imap",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.87",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bobfrankston/mailx-imap",
|
|
9
|
-
"version": "0.1.
|
|
9
|
+
"version": "0.1.87",
|
|
10
10
|
"license": "ISC",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@bobfrankston/iflow-direct": "^0.1.27",
|