@bobfrankston/mailx-imap 0.1.23 → 0.1.25
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 +1 -26
- package/index.js +69 -70
- package/package.json +5 -5
package/index.d.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import type { TransportFactory } from "@bobfrankston/tcp-transport";
|
|
7
7
|
import { MailxDB, FileMessageStore } from "@bobfrankston/mailx-store";
|
|
8
|
-
import type { AccountConfig, MessageEnvelope,
|
|
8
|
+
import type { AccountConfig, MessageEnvelope, Folder } from "@bobfrankston/mailx-types";
|
|
9
9
|
import { EventEmitter } from "node:events";
|
|
10
10
|
/** Events emitted by the IMAP manager */
|
|
11
11
|
export interface ImapManagerEvents {
|
|
@@ -336,31 +336,6 @@ export declare class ImapManager extends EventEmitter {
|
|
|
336
336
|
processSyncActions(accountId: string): Promise<void>;
|
|
337
337
|
/** Find a folder by specialUse, case-insensitive */
|
|
338
338
|
private findFolder;
|
|
339
|
-
/** Optimistic local-first Sent insert: write a row into the local DB's
|
|
340
|
-
* Sent folder the moment the user hits Send, so the list reflects it
|
|
341
|
-
* immediately instead of waiting for SMTP + IMAP-APPEND + syncFolder
|
|
342
|
-
* (five server round-trips against a Dovecot that caps at 20 conns).
|
|
343
|
-
*
|
|
344
|
-
* Uses a synthetic negative UID so it can't collide with a real APPENDUID
|
|
345
|
-
* (which is always positive). When the real sync eventually picks the
|
|
346
|
-
* message up in Sent with the server's UID, `db.upsertMessage` spots
|
|
347
|
-
* the Message-ID match and rebinds the existing row's UID — no duplicate.
|
|
348
|
-
* Negative UID also makes the row render pink (getMessages flags uid<0
|
|
349
|
-
* as pending) so the user sees it's not-yet-reconciled.
|
|
350
|
-
*
|
|
351
|
-
* Best-effort — any failure path (no Sent folder yet, parse error, store
|
|
352
|
-
* write error) is logged and swallowed; the send itself is unaffected. */
|
|
353
|
-
insertOptimisticSentRow(accountId: string, envelope: {
|
|
354
|
-
messageId: string;
|
|
355
|
-
inReplyTo: string;
|
|
356
|
-
references: string[];
|
|
357
|
-
subject: string;
|
|
358
|
-
from: EmailAddress;
|
|
359
|
-
to: EmailAddress[];
|
|
360
|
-
cc: EmailAddress[];
|
|
361
|
-
bcc: EmailAddress[];
|
|
362
|
-
date: number;
|
|
363
|
-
}, rawMessage: string): Promise<void>;
|
|
364
339
|
/** Copy sent message to the Sent folder via IMAP APPEND */
|
|
365
340
|
copyToSent(accountId: string, rawMessage: string | Buffer): Promise<void>;
|
|
366
341
|
/** Save a draft to the Drafts folder via IMAP APPEND.
|
package/index.js
CHANGED
|
@@ -874,7 +874,9 @@ export class ImapManager extends EventEmitter {
|
|
|
874
874
|
if (!folder)
|
|
875
875
|
throw new Error(`Folder ${folderId} not found`);
|
|
876
876
|
this.emit("syncProgress", accountId, `sync:${folder.path}`, 0);
|
|
877
|
-
// Get the highest UID we already have for this folder
|
|
877
|
+
// Get the highest UID we already have for this folder. IMAP UIDs are
|
|
878
|
+
// monotonically increasing within a UIDVALIDITY (RFC 3501); a
|
|
879
|
+
// high-water mark is the right anchor for incremental fetch.
|
|
878
880
|
const highestUid = this.db.getHighestUid(accountId, folderId);
|
|
879
881
|
console.log(` [sync] ${accountId}/${folder.path}: highestUid=${highestUid}, fetching...`);
|
|
880
882
|
let messages;
|
|
@@ -1871,19 +1873,70 @@ export class ImapManager extends EventEmitter {
|
|
|
1871
1873
|
// is parked in IDLE, it's unusable for any other command, so
|
|
1872
1874
|
// it can't share the ops queue. Counts against the per-host
|
|
1873
1875
|
// semaphore (one slot for the IDLE socket).
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1876
|
+
//
|
|
1877
|
+
// We watch INBOX (incoming mail) and Sent + Drafts (outgoing
|
|
1878
|
+
// changes from another client — Thunderbird, phone, web).
|
|
1879
|
+
// Without IDLE on Sent, a message just sent from another
|
|
1880
|
+
// device would only show in mailx after the next periodic
|
|
1881
|
+
// full sync.
|
|
1882
|
+
const stops = [];
|
|
1883
|
+
const clients = [];
|
|
1884
|
+
const watchOne = async (mailboxLabel, path) => {
|
|
1885
|
+
const client = await this.createClient(accountId, "idle");
|
|
1886
|
+
clients.push(client);
|
|
1887
|
+
const stop = await client.watchMailbox(path, (newCount) => {
|
|
1888
|
+
console.log(` [idle] ${accountId} ${path}: ${newCount} new message(s)`);
|
|
1889
|
+
if (mailboxLabel === "inbox") {
|
|
1890
|
+
// Fast path: incremental fetch of NEW UIDs only.
|
|
1891
|
+
// Heavy reconcile runs on the 5-minute STATUS poll.
|
|
1892
|
+
this.syncInboxNewOnly(accountId).catch(e => console.error(` [idle] inbox sync error: ${e.message}`));
|
|
1893
|
+
}
|
|
1894
|
+
else {
|
|
1895
|
+
// Sent / Drafts changed elsewhere. Use the
|
|
1896
|
+
// standard folder sync — picks up the new UID,
|
|
1897
|
+
// rebinds any optimistic local row by Message-ID.
|
|
1898
|
+
const folder = this.findFolder(accountId, mailboxLabel);
|
|
1899
|
+
if (folder) {
|
|
1900
|
+
this.syncFolder(accountId, folder.id).catch(e => console.error(` [idle] ${path} sync error: ${e.message}`));
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
});
|
|
1904
|
+
stops.push(stop);
|
|
1905
|
+
};
|
|
1906
|
+
await watchOne("inbox", "INBOX");
|
|
1907
|
+
const sent = this.findFolder(accountId, "sent");
|
|
1908
|
+
if (sent) {
|
|
1909
|
+
try {
|
|
1910
|
+
await watchOne("sent", sent.path);
|
|
1911
|
+
}
|
|
1912
|
+
catch (e) {
|
|
1913
|
+
console.error(` [idle] Failed to watch ${sent.path}: ${e.message}`);
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1916
|
+
const drafts = this.findFolder(accountId, "drafts");
|
|
1917
|
+
if (drafts) {
|
|
1918
|
+
try {
|
|
1919
|
+
await watchOne("drafts", drafts.path);
|
|
1920
|
+
}
|
|
1921
|
+
catch (e) {
|
|
1922
|
+
console.error(` [idle] Failed to watch ${drafts.path}: ${e.message}`);
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1882
1925
|
this.watchers.set(accountId, async () => {
|
|
1883
|
-
|
|
1884
|
-
|
|
1926
|
+
for (const stop of stops) {
|
|
1927
|
+
try {
|
|
1928
|
+
await stop();
|
|
1929
|
+
}
|
|
1930
|
+
catch { /* ignore */ }
|
|
1931
|
+
}
|
|
1932
|
+
for (const c of clients) {
|
|
1933
|
+
try {
|
|
1934
|
+
await c.logout();
|
|
1935
|
+
}
|
|
1936
|
+
catch { /* ignore */ }
|
|
1937
|
+
}
|
|
1885
1938
|
});
|
|
1886
|
-
console.log(` [idle] Watching INBOX for ${accountId}`);
|
|
1939
|
+
console.log(` [idle] Watching INBOX${sent ? "+Sent" : ""}${drafts ? "+Drafts" : ""} for ${accountId}`);
|
|
1887
1940
|
}
|
|
1888
1941
|
catch (e) {
|
|
1889
1942
|
console.error(` [idle] Failed to watch ${accountId}: ${e.message}`);
|
|
@@ -2565,64 +2618,10 @@ export class ImapManager extends EventEmitter {
|
|
|
2565
2618
|
return folders.find(f => f.specialUse === specialUse ||
|
|
2566
2619
|
f.path.toLowerCase() === specialUse.toLowerCase()) || null;
|
|
2567
2620
|
}
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
*
|
|
2573
|
-
* Uses a synthetic negative UID so it can't collide with a real APPENDUID
|
|
2574
|
-
* (which is always positive). When the real sync eventually picks the
|
|
2575
|
-
* message up in Sent with the server's UID, `db.upsertMessage` spots
|
|
2576
|
-
* the Message-ID match and rebinds the existing row's UID — no duplicate.
|
|
2577
|
-
* Negative UID also makes the row render pink (getMessages flags uid<0
|
|
2578
|
-
* as pending) so the user sees it's not-yet-reconciled.
|
|
2579
|
-
*
|
|
2580
|
-
* Best-effort — any failure path (no Sent folder yet, parse error, store
|
|
2581
|
-
* write error) is logged and swallowed; the send itself is unaffected. */
|
|
2582
|
-
async insertOptimisticSentRow(accountId, envelope, rawMessage) {
|
|
2583
|
-
try {
|
|
2584
|
-
const sent = this.findFolder(accountId, "sent");
|
|
2585
|
-
if (!sent) {
|
|
2586
|
-
console.log(` [sent-local] no Sent folder for ${accountId}; skipping optimistic row`);
|
|
2587
|
-
return;
|
|
2588
|
-
}
|
|
2589
|
-
// Synthetic UID — negative ms timestamp is monotonic + won't
|
|
2590
|
-
// collide with server UIDs. When the real APPENDUID returns via
|
|
2591
|
-
// sync, upsertMessage's Message-ID rebind swaps this for the
|
|
2592
|
-
// real positive value.
|
|
2593
|
-
const synthUid = -Date.now();
|
|
2594
|
-
const bodyPath = await this.bodyStore.putMessage(accountId, sent.id, synthUid, Buffer.from(rawMessage, "utf-8"));
|
|
2595
|
-
const parsed = await extractPreview(rawMessage);
|
|
2596
|
-
this.db.upsertMessage({
|
|
2597
|
-
accountId,
|
|
2598
|
-
folderId: sent.id,
|
|
2599
|
-
uid: synthUid,
|
|
2600
|
-
messageId: envelope.messageId,
|
|
2601
|
-
inReplyTo: envelope.inReplyTo,
|
|
2602
|
-
references: envelope.references,
|
|
2603
|
-
date: envelope.date,
|
|
2604
|
-
subject: envelope.subject,
|
|
2605
|
-
from: envelope.from,
|
|
2606
|
-
to: envelope.to,
|
|
2607
|
-
cc: envelope.cc,
|
|
2608
|
-
flags: ["\\Seen"],
|
|
2609
|
-
size: rawMessage.length,
|
|
2610
|
-
hasAttachments: parsed.hasAttachments,
|
|
2611
|
-
preview: parsed.preview,
|
|
2612
|
-
bodyPath,
|
|
2613
|
-
});
|
|
2614
|
-
// Folder-tree badge refresh + message-list reload if the user
|
|
2615
|
-
// is currently on Sent — same event the sync path emits.
|
|
2616
|
-
this.db.recalcFolderCounts(sent.id);
|
|
2617
|
-
this.emit("folderCountsChanged", { accountId, folderId: sent.id });
|
|
2618
|
-
console.log(` [sent-local] wrote optimistic row in Sent (uid=${synthUid}) for ${accountId}: ${envelope.subject}`);
|
|
2619
|
-
}
|
|
2620
|
-
catch (e) {
|
|
2621
|
-
// Non-fatal — send continues, Sent folder just won't show the
|
|
2622
|
-
// row until the real APPEND-then-sync cycle completes.
|
|
2623
|
-
console.error(` [sent-local] optimistic insert failed: ${e?.message || e}`);
|
|
2624
|
-
}
|
|
2625
|
-
}
|
|
2621
|
+
// insertOptimisticSentRow removed — the synthetic-negative-UID hack
|
|
2622
|
+
// wedged Sent's high-water-mark sync (a synthetic value polluted MAX(uid)
|
|
2623
|
+
// and made every Sent fetch take a stale code path). Sent is now what
|
|
2624
|
+
// the server has, period. Pending sends live in the Outbox view.
|
|
2626
2625
|
/** Copy sent message to the Sent folder via IMAP APPEND */
|
|
2627
2626
|
async copyToSent(accountId, rawMessage) {
|
|
2628
2627
|
const sent = this.findFolder(accountId, "sent");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bobfrankston/mailx-imap",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.25",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
"license": "ISC",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@bobfrankston/mailx-types": "^0.1.10",
|
|
13
|
-
"@bobfrankston/mailx-settings": "^0.1.
|
|
14
|
-
"@bobfrankston/mailx-store": "^0.1.
|
|
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",
|
|
@@ -38,8 +38,8 @@
|
|
|
38
38
|
".transformedSnapshot": {
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"@bobfrankston/mailx-types": "^0.1.10",
|
|
41
|
-
"@bobfrankston/mailx-settings": "^0.1.
|
|
42
|
-
"@bobfrankston/mailx-store": "^0.1.
|
|
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",
|