@bobfrankston/mailx-imap 0.1.22 → 0.1.24
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/index.d.ts +9 -1
- package/index.js +92 -51
- package/package.json +7 -7
package/index.d.ts
CHANGED
|
@@ -239,6 +239,10 @@ export declare class ImapManager extends EventEmitter {
|
|
|
239
239
|
quickInboxCheck(): Promise<void>;
|
|
240
240
|
/** Start periodic sync */
|
|
241
241
|
startPeriodicSync(intervalMinutes: number): void;
|
|
242
|
+
/** One-shot full sync + IDLE restart. Public so callers (Reconciler,
|
|
243
|
+
* user-initiated "Sync now") can fire it on demand. The original body
|
|
244
|
+
* that lived inline in startPeriodicSync was lifted verbatim. */
|
|
245
|
+
runFullSync(): Promise<void>;
|
|
242
246
|
/** Stop periodic sync */
|
|
243
247
|
stopPeriodicSync(): void;
|
|
244
248
|
/** Check if an account is OAuth (Gmail/Outlook — generous connection limits) */
|
|
@@ -292,7 +296,11 @@ export declare class ImapManager extends EventEmitter {
|
|
|
292
296
|
private shouldSkipFolder;
|
|
293
297
|
private recordFolderError;
|
|
294
298
|
private clearFolderErrors;
|
|
295
|
-
|
|
299
|
+
/** Background body-cache backfill. Public so the Reconciler can schedule
|
|
300
|
+
* the periodic tick under its priority/back-pressure rules; existing
|
|
301
|
+
* in-method post-sync nudges (sync, fetchSince, fetchOne) call this
|
|
302
|
+
* too and the per-account `prefetchingAccounts` set deduplicates. */
|
|
303
|
+
prefetchBodies(accountId: string): Promise<void>;
|
|
296
304
|
private _prefetchBodies;
|
|
297
305
|
/** Get the body store for direct access */
|
|
298
306
|
getBodyStore(): FileMessageStore;
|
package/index.js
CHANGED
|
@@ -1825,47 +1825,30 @@ export class ImapManager extends EventEmitter {
|
|
|
1825
1825
|
}
|
|
1826
1826
|
}, 30000);
|
|
1827
1827
|
this.syncIntervals.set("actions", actionsInterval);
|
|
1828
|
-
// Body prefetch
|
|
1829
|
-
//
|
|
1830
|
-
//
|
|
1831
|
-
//
|
|
1832
|
-
//
|
|
1833
|
-
//
|
|
1834
|
-
//
|
|
1835
|
-
//
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
}
|
|
1841
|
-
};
|
|
1842
|
-
// Fire once now so the "not downloaded" dots start filling in
|
|
1843
|
-
// immediately on app start, don't make the user wait a minute.
|
|
1844
|
-
setTimeout(kickPrefetch, 2000);
|
|
1845
|
-
const prefetchInterval = setInterval(kickPrefetch, 60000);
|
|
1846
|
-
this.syncIntervals.set("prefetch", prefetchInterval);
|
|
1847
|
-
console.log(` [periodic] body prefetch every 60s (independent of sync)`);
|
|
1848
|
-
}
|
|
1849
|
-
// Tombstone prune: age out local-delete records older than 30 days.
|
|
1850
|
-
// Runs hourly — cheap (one indexed DELETE).
|
|
1851
|
-
const TOMBSTONE_RETENTION_DAYS = 30;
|
|
1852
|
-
const pruneTombstones = () => {
|
|
1853
|
-
const cutoff = Date.now() - TOMBSTONE_RETENTION_DAYS * 86400_000;
|
|
1854
|
-
const n = this.db.pruneTombstones(cutoff);
|
|
1855
|
-
if (n > 0)
|
|
1856
|
-
console.log(` [tombstones] pruned ${n} older than ${TOMBSTONE_RETENTION_DAYS} days`);
|
|
1857
|
-
};
|
|
1858
|
-
setTimeout(pruneTombstones, 30_000); // first run after startup settles
|
|
1859
|
-
this.syncIntervals.set("tombstone-prune", setInterval(pruneTombstones, 3600_000));
|
|
1860
|
-
// Full sync (all folders + IDLE restart) at configured interval
|
|
1861
|
-
const fullInterval = setInterval(async () => {
|
|
1862
|
-
console.log(` [periodic] Full sync at ${new Date().toLocaleTimeString()}`);
|
|
1863
|
-
await this.syncAll();
|
|
1864
|
-
await this.stopWatching();
|
|
1865
|
-
await this.startWatching();
|
|
1828
|
+
// Body prefetch + tombstone prune moved to Reconciler.start —
|
|
1829
|
+
// prefetch needs back-pressure from the body-fetch lane, and
|
|
1830
|
+
// tombstone prune is pure DB bookkeeping with no IMAP coupling.
|
|
1831
|
+
// Quick STATUS check (above) and actions/outbox drain (above)
|
|
1832
|
+
// stay here because they're tightly tied to IMAP connection state
|
|
1833
|
+
// and the `syncing` flag.
|
|
1834
|
+
// Full sync (all folders + IDLE restart) at configured interval.
|
|
1835
|
+
// Stays here because callers pass `intervalMinutes` directly —
|
|
1836
|
+
// moving it would mean threading the value through MailxService
|
|
1837
|
+
// → Reconciler with a separate setter, for no behavior gain.
|
|
1838
|
+
const fullInterval = setInterval(() => {
|
|
1839
|
+
this.runFullSync().catch(e => console.error(` [periodic] full sync error: ${e?.message || e}`));
|
|
1866
1840
|
}, intervalMinutes * 60000);
|
|
1867
1841
|
this.syncIntervals.set("all", fullInterval);
|
|
1868
1842
|
}
|
|
1843
|
+
/** One-shot full sync + IDLE restart. Public so callers (Reconciler,
|
|
1844
|
+
* user-initiated "Sync now") can fire it on demand. The original body
|
|
1845
|
+
* that lived inline in startPeriodicSync was lifted verbatim. */
|
|
1846
|
+
async runFullSync() {
|
|
1847
|
+
console.log(` [periodic] Full sync at ${new Date().toLocaleTimeString()}`);
|
|
1848
|
+
await this.syncAll();
|
|
1849
|
+
await this.stopWatching();
|
|
1850
|
+
await this.startWatching();
|
|
1851
|
+
}
|
|
1869
1852
|
/** Stop periodic sync */
|
|
1870
1853
|
stopPeriodicSync() {
|
|
1871
1854
|
for (const [key, interval] of this.syncIntervals) {
|
|
@@ -1888,19 +1871,70 @@ export class ImapManager extends EventEmitter {
|
|
|
1888
1871
|
// is parked in IDLE, it's unusable for any other command, so
|
|
1889
1872
|
// it can't share the ops queue. Counts against the per-host
|
|
1890
1873
|
// semaphore (one slot for the IDLE socket).
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1874
|
+
//
|
|
1875
|
+
// We watch INBOX (incoming mail) and Sent + Drafts (outgoing
|
|
1876
|
+
// changes from another client — Thunderbird, phone, web).
|
|
1877
|
+
// Without IDLE on Sent, a message just sent from another
|
|
1878
|
+
// device would only show in mailx after the next periodic
|
|
1879
|
+
// full sync.
|
|
1880
|
+
const stops = [];
|
|
1881
|
+
const clients = [];
|
|
1882
|
+
const watchOne = async (mailboxLabel, path) => {
|
|
1883
|
+
const client = await this.createClient(accountId, "idle");
|
|
1884
|
+
clients.push(client);
|
|
1885
|
+
const stop = await client.watchMailbox(path, (newCount) => {
|
|
1886
|
+
console.log(` [idle] ${accountId} ${path}: ${newCount} new message(s)`);
|
|
1887
|
+
if (mailboxLabel === "inbox") {
|
|
1888
|
+
// Fast path: incremental fetch of NEW UIDs only.
|
|
1889
|
+
// Heavy reconcile runs on the 5-minute STATUS poll.
|
|
1890
|
+
this.syncInboxNewOnly(accountId).catch(e => console.error(` [idle] inbox sync error: ${e.message}`));
|
|
1891
|
+
}
|
|
1892
|
+
else {
|
|
1893
|
+
// Sent / Drafts changed elsewhere. Use the
|
|
1894
|
+
// standard folder sync — picks up the new UID,
|
|
1895
|
+
// rebinds any optimistic local row by Message-ID.
|
|
1896
|
+
const folder = this.findFolder(accountId, mailboxLabel);
|
|
1897
|
+
if (folder) {
|
|
1898
|
+
this.syncFolder(accountId, folder.id).catch(e => console.error(` [idle] ${path} sync error: ${e.message}`));
|
|
1899
|
+
}
|
|
1900
|
+
}
|
|
1901
|
+
});
|
|
1902
|
+
stops.push(stop);
|
|
1903
|
+
};
|
|
1904
|
+
await watchOne("inbox", "INBOX");
|
|
1905
|
+
const sent = this.findFolder(accountId, "sent");
|
|
1906
|
+
if (sent) {
|
|
1907
|
+
try {
|
|
1908
|
+
await watchOne("sent", sent.path);
|
|
1909
|
+
}
|
|
1910
|
+
catch (e) {
|
|
1911
|
+
console.error(` [idle] Failed to watch ${sent.path}: ${e.message}`);
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1914
|
+
const drafts = this.findFolder(accountId, "drafts");
|
|
1915
|
+
if (drafts) {
|
|
1916
|
+
try {
|
|
1917
|
+
await watchOne("drafts", drafts.path);
|
|
1918
|
+
}
|
|
1919
|
+
catch (e) {
|
|
1920
|
+
console.error(` [idle] Failed to watch ${drafts.path}: ${e.message}`);
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1899
1923
|
this.watchers.set(accountId, async () => {
|
|
1900
|
-
|
|
1901
|
-
|
|
1924
|
+
for (const stop of stops) {
|
|
1925
|
+
try {
|
|
1926
|
+
await stop();
|
|
1927
|
+
}
|
|
1928
|
+
catch { /* ignore */ }
|
|
1929
|
+
}
|
|
1930
|
+
for (const c of clients) {
|
|
1931
|
+
try {
|
|
1932
|
+
await c.logout();
|
|
1933
|
+
}
|
|
1934
|
+
catch { /* ignore */ }
|
|
1935
|
+
}
|
|
1902
1936
|
});
|
|
1903
|
-
console.log(` [idle] Watching INBOX for ${accountId}`);
|
|
1937
|
+
console.log(` [idle] Watching INBOX${sent ? "+Sent" : ""}${drafts ? "+Drafts" : ""} for ${accountId}`);
|
|
1904
1938
|
}
|
|
1905
1939
|
catch (e) {
|
|
1906
1940
|
console.error(` [idle] Failed to watch ${accountId}: ${e.message}`);
|
|
@@ -2065,6 +2099,10 @@ export class ImapManager extends EventEmitter {
|
|
|
2065
2099
|
clearFolderErrors(accountId, folderPath) {
|
|
2066
2100
|
this.folderErrorCooldown.delete(`${accountId}:${folderPath}`);
|
|
2067
2101
|
}
|
|
2102
|
+
/** Background body-cache backfill. Public so the Reconciler can schedule
|
|
2103
|
+
* the periodic tick under its priority/back-pressure rules; existing
|
|
2104
|
+
* in-method post-sync nudges (sync, fetchSince, fetchOne) call this
|
|
2105
|
+
* too and the per-account `prefetchingAccounts` set deduplicates. */
|
|
2068
2106
|
async prefetchBodies(accountId) {
|
|
2069
2107
|
if (this.prefetchingAccounts.has(accountId))
|
|
2070
2108
|
return;
|
|
@@ -2625,9 +2663,12 @@ export class ImapManager extends EventEmitter {
|
|
|
2625
2663
|
bodyPath,
|
|
2626
2664
|
});
|
|
2627
2665
|
// Folder-tree badge refresh + message-list reload if the user
|
|
2628
|
-
// is currently on Sent — same event the sync path emits.
|
|
2666
|
+
// is currently on Sent — same event shape the sync path emits.
|
|
2667
|
+
// (Was sending {accountId,folderId} as a single arg, which the
|
|
2668
|
+
// IPC forwarder + UI handler decoded as garbage — the optimistic
|
|
2669
|
+
// row landed in the DB but never appeared in the list.)
|
|
2629
2670
|
this.db.recalcFolderCounts(sent.id);
|
|
2630
|
-
this.emit("folderCountsChanged",
|
|
2671
|
+
this.emit("folderCountsChanged", accountId, {});
|
|
2631
2672
|
console.log(` [sent-local] wrote optimistic row in Sent (uid=${synthUid}) for ${accountId}: ${envelope.subject}`);
|
|
2632
2673
|
}
|
|
2633
2674
|
catch (e) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bobfrankston/mailx-imap",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.24",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -9,9 +9,9 @@
|
|
|
9
9
|
},
|
|
10
10
|
"license": "ISC",
|
|
11
11
|
"dependencies": {
|
|
12
|
-
"@bobfrankston/mailx-types": "^0.1.
|
|
13
|
-
"@bobfrankston/mailx-settings": "^0.1.
|
|
14
|
-
"@bobfrankston/mailx-store": "^0.1.
|
|
12
|
+
"@bobfrankston/mailx-types": "^0.1.10",
|
|
13
|
+
"@bobfrankston/mailx-settings": "^0.1.13",
|
|
14
|
+
"@bobfrankston/mailx-store": "^0.1.10",
|
|
15
15
|
"@bobfrankston/iflow-direct": "^0.1.30",
|
|
16
16
|
"@bobfrankston/tcp-transport": "^0.1.5",
|
|
17
17
|
"@bobfrankston/smtp-direct": "^0.1.5",
|
|
@@ -37,9 +37,9 @@
|
|
|
37
37
|
},
|
|
38
38
|
".transformedSnapshot": {
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@bobfrankston/mailx-types": "^0.1.
|
|
41
|
-
"@bobfrankston/mailx-settings": "^0.1.
|
|
42
|
-
"@bobfrankston/mailx-store": "^0.1.
|
|
40
|
+
"@bobfrankston/mailx-types": "^0.1.10",
|
|
41
|
+
"@bobfrankston/mailx-settings": "^0.1.13",
|
|
42
|
+
"@bobfrankston/mailx-store": "^0.1.10",
|
|
43
43
|
"@bobfrankston/iflow-direct": "^0.1.30",
|
|
44
44
|
"@bobfrankston/tcp-transport": "^0.1.5",
|
|
45
45
|
"@bobfrankston/smtp-direct": "^0.1.5",
|