@bobfrankston/mailx-imap 0.1.67 → 0.1.69
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 +11 -0
- package/index.js +111 -5
- package/package.json +3 -3
package/index.d.ts
CHANGED
|
@@ -452,6 +452,17 @@ export declare class ImapManager extends EventEmitter {
|
|
|
452
452
|
private ensureOutbox;
|
|
453
453
|
/** Save a copy of outgoing mail — label is a subdirectory (editing/queued/sent) */
|
|
454
454
|
private saveSendingCopy;
|
|
455
|
+
/** Drop a requeue.cmd / requeue.sh into a sending/<acct>/<label>/ directory
|
|
456
|
+
* so the user can re-queue a saved .eml/.ltr without remembering the
|
|
457
|
+
* rmfmail -send syntax. Idempotent: only writes when missing.
|
|
458
|
+
* The word is "requeue" — it puts the file back into the outbox queue;
|
|
459
|
+
* it does NOT send a fresh copy bypassing the queue. */
|
|
460
|
+
private ensureRequeueScripts;
|
|
461
|
+
/** Walk the sending tree at startup and drop a requeue.cmd/.sh into every
|
|
462
|
+
* existing sending/<acct>/<label>/ directory. Without this the scripts
|
|
463
|
+
* only appear on the next outgoing message, which doesn't help a user
|
|
464
|
+
* who needs to re-queue a stuck letter from before the upgrade. */
|
|
465
|
+
private seedRequeueScripts;
|
|
455
466
|
/** Queue a message for sending. Tries IMAP Outbox, falls back to local file. */
|
|
456
467
|
queueOutgoing(accountId: string, rawMessage: string | Buffer): Promise<void>;
|
|
457
468
|
/** Process local file queue — scan both outbox/ (IMAP-unreachable fallback)
|
package/index.js
CHANGED
|
@@ -1250,7 +1250,7 @@ export class ImapManager extends EventEmitter {
|
|
|
1250
1250
|
for (const m of qr.changedMessages) {
|
|
1251
1251
|
try {
|
|
1252
1252
|
const flagsArr = Array.from(m.flags || []).map(f => String(f));
|
|
1253
|
-
this.db.updateMessageFlags(accountId, m.uid, flagsArr);
|
|
1253
|
+
this.db.updateMessageFlags(accountId, folderId, m.uid, flagsArr);
|
|
1254
1254
|
}
|
|
1255
1255
|
catch { /* row may have just been VANISHED */ }
|
|
1256
1256
|
}
|
|
@@ -3467,6 +3467,12 @@ export class ImapManager extends EventEmitter {
|
|
|
3467
3467
|
// Delete previous draft — try UID first (fast path), and ALWAYS also try
|
|
3468
3468
|
// searchByHeader(X-Mailx-Draft-ID) as a safety net. Running both catches
|
|
3469
3469
|
// orphans from a crash-mid-save or a UID delete that failed silently.
|
|
3470
|
+
//
|
|
3471
|
+
// CRITICAL: also delete the LOCAL DB row for the superseded UID. Without
|
|
3472
|
+
// that, every checkpoint save left a stale row behind — IMAP had only
|
|
3473
|
+
// the latest copy but the local Drafts view rendered every past UID as
|
|
3474
|
+
// its own row, producing the "Drafts is flooded with droppings" symptom
|
|
3475
|
+
// (Bob 2026-05-27 — 8+ rows for a single in-progress reply).
|
|
3470
3476
|
if (previousDraftUid) {
|
|
3471
3477
|
try {
|
|
3472
3478
|
await client.deleteMessageByUid(drafts.path, previousDraftUid);
|
|
@@ -3474,6 +3480,10 @@ export class ImapManager extends EventEmitter {
|
|
|
3474
3480
|
catch (e) {
|
|
3475
3481
|
console.error(` [drafts] Delete prev UID ${previousDraftUid} failed: ${e.message}`);
|
|
3476
3482
|
}
|
|
3483
|
+
try {
|
|
3484
|
+
this.db.deleteMessage(accountId, drafts.id, previousDraftUid, "previous draft superseded", "saveDraft");
|
|
3485
|
+
}
|
|
3486
|
+
catch { /* */ }
|
|
3477
3487
|
}
|
|
3478
3488
|
if (draftId) {
|
|
3479
3489
|
try {
|
|
@@ -3483,6 +3493,10 @@ export class ImapManager extends EventEmitter {
|
|
|
3483
3493
|
await client.deleteMessageByUid(drafts.path, uid);
|
|
3484
3494
|
}
|
|
3485
3495
|
catch { /* next */ }
|
|
3496
|
+
try {
|
|
3497
|
+
this.db.deleteMessage(accountId, drafts.id, uid, `draft superseded by newer save (id=${draftId})`, "saveDraft");
|
|
3498
|
+
}
|
|
3499
|
+
catch { /* */ }
|
|
3486
3500
|
}
|
|
3487
3501
|
if (uids.length > 0)
|
|
3488
3502
|
console.log(` [drafts] Deleted ${uids.length} stale draft(s) by ID ${draftId}`);
|
|
@@ -3827,6 +3841,7 @@ export class ImapManager extends EventEmitter {
|
|
|
3827
3841
|
try {
|
|
3828
3842
|
const dir = path.join(getConfigDir(), "sending", accountId, label);
|
|
3829
3843
|
fs.mkdirSync(dir, { recursive: true });
|
|
3844
|
+
this.ensureRequeueScripts(dir);
|
|
3830
3845
|
const now = new Date();
|
|
3831
3846
|
const pad2 = (n) => String(n).padStart(2, "0");
|
|
3832
3847
|
const ts = `${now.getFullYear()}${pad2(now.getMonth() + 1)}${pad2(now.getDate())}_${pad2(now.getHours())}${pad2(now.getMinutes())}${pad2(now.getSeconds())}`;
|
|
@@ -3837,6 +3852,68 @@ export class ImapManager extends EventEmitter {
|
|
|
3837
3852
|
console.error(` [sending] Failed to save copy: ${e.message}`);
|
|
3838
3853
|
}
|
|
3839
3854
|
}
|
|
3855
|
+
/** Drop a requeue.cmd / requeue.sh into a sending/<acct>/<label>/ directory
|
|
3856
|
+
* so the user can re-queue a saved .eml/.ltr without remembering the
|
|
3857
|
+
* rmfmail -send syntax. Idempotent: only writes when missing.
|
|
3858
|
+
* The word is "requeue" — it puts the file back into the outbox queue;
|
|
3859
|
+
* it does NOT send a fresh copy bypassing the queue. */
|
|
3860
|
+
ensureRequeueScripts(dir) {
|
|
3861
|
+
const cmdPath = path.join(dir, "requeue.cmd");
|
|
3862
|
+
if (!fs.existsSync(cmdPath)) {
|
|
3863
|
+
const cmd = "@echo off\r\n" +
|
|
3864
|
+
"rem Re-queue a saved .eml / .ltr for sending. Pass the filename:\r\n" +
|
|
3865
|
+
"rem requeue 20260527_140527-6599.ltr\r\n" +
|
|
3866
|
+
"rem With no arg, lists files in this directory.\r\n" +
|
|
3867
|
+
"if \"%~1\"==\"\" (\r\n" +
|
|
3868
|
+
" dir /b *.eml *.ltr 2>nul\r\n" +
|
|
3869
|
+
" exit /b 0\r\n" +
|
|
3870
|
+
")\r\n" +
|
|
3871
|
+
"rmfmail -send \"%~dp0%~1\"\r\n";
|
|
3872
|
+
fs.writeFileSync(cmdPath, cmd);
|
|
3873
|
+
}
|
|
3874
|
+
const shPath = path.join(dir, "requeue.sh");
|
|
3875
|
+
if (!fs.existsSync(shPath)) {
|
|
3876
|
+
const sh = "#!/bin/sh\n" +
|
|
3877
|
+
"# Re-queue a saved .eml / .ltr for sending. Pass the filename:\n" +
|
|
3878
|
+
"# ./requeue.sh 20260527_140527-6599.ltr\n" +
|
|
3879
|
+
"# With no arg, lists files in this directory.\n" +
|
|
3880
|
+
"DIR=\"$(cd \"$(dirname \"$0\")\" && pwd)\"\n" +
|
|
3881
|
+
"if [ -z \"$1\" ]; then\n" +
|
|
3882
|
+
" ls \"$DIR\"/*.eml \"$DIR\"/*.ltr 2>/dev/null | xargs -n1 basename 2>/dev/null\n" +
|
|
3883
|
+
" exit 0\n" +
|
|
3884
|
+
"fi\n" +
|
|
3885
|
+
"rmfmail -send \"$DIR/$1\"\n";
|
|
3886
|
+
fs.writeFileSync(shPath, sh);
|
|
3887
|
+
try {
|
|
3888
|
+
fs.chmodSync(shPath, 0o755);
|
|
3889
|
+
}
|
|
3890
|
+
catch { /* */ }
|
|
3891
|
+
}
|
|
3892
|
+
}
|
|
3893
|
+
/** Walk the sending tree at startup and drop a requeue.cmd/.sh into every
|
|
3894
|
+
* existing sending/<acct>/<label>/ directory. Without this the scripts
|
|
3895
|
+
* only appear on the next outgoing message, which doesn't help a user
|
|
3896
|
+
* who needs to re-queue a stuck letter from before the upgrade. */
|
|
3897
|
+
seedRequeueScripts() {
|
|
3898
|
+
try {
|
|
3899
|
+
const root = path.join(getConfigDir(), "sending");
|
|
3900
|
+
if (!fs.existsSync(root))
|
|
3901
|
+
return;
|
|
3902
|
+
for (const acct of fs.readdirSync(root, { withFileTypes: true })) {
|
|
3903
|
+
if (!acct.isDirectory())
|
|
3904
|
+
continue;
|
|
3905
|
+
const acctDir = path.join(root, acct.name);
|
|
3906
|
+
for (const label of fs.readdirSync(acctDir, { withFileTypes: true })) {
|
|
3907
|
+
if (!label.isDirectory())
|
|
3908
|
+
continue;
|
|
3909
|
+
this.ensureRequeueScripts(path.join(acctDir, label.name));
|
|
3910
|
+
}
|
|
3911
|
+
}
|
|
3912
|
+
}
|
|
3913
|
+
catch (e) {
|
|
3914
|
+
console.error(` [sending] seedRequeueScripts failed: ${e?.message || e}`);
|
|
3915
|
+
}
|
|
3916
|
+
}
|
|
3840
3917
|
/** Queue a message for sending. Tries IMAP Outbox, falls back to local file. */
|
|
3841
3918
|
async queueOutgoing(accountId, rawMessage) {
|
|
3842
3919
|
// IMPORTANT: do NOT save a "debug copy" to sending/<acct>/queued/ here.
|
|
@@ -3848,13 +3925,38 @@ export class ImapManager extends EventEmitter {
|
|
|
3848
3925
|
// - ~/.mailx/outbox/<acct>/*.ltr (fallback when IMAP is unreachable)
|
|
3849
3926
|
try {
|
|
3850
3927
|
const outboxPath = await this.ensureOutbox(accountId);
|
|
3928
|
+
const outboxFolder = this.findFolder(accountId, "outbox");
|
|
3929
|
+
// Capture APPENDUID. The Sent folder path (processOutbox at line
|
|
3930
|
+
// ~4150) does this and then calls insertLocalRowFromSource so the
|
|
3931
|
+
// local Sent view is instantly populated without a sync round-trip.
|
|
3932
|
+
// The Outbox path used to ONLY trigger fire-and-forget syncFolder,
|
|
3933
|
+
// which on a wedged slow-lane (Bob 2026-05-27 14:11: syncAll held
|
|
3934
|
+
// the lane for the whole afternoon) left the queued message
|
|
3935
|
+
// invisible in the local Outbox view until next daemon restart.
|
|
3936
|
+
let appendedUid = null;
|
|
3937
|
+
const sourceStr = typeof rawMessage === "string"
|
|
3938
|
+
? rawMessage
|
|
3939
|
+
: rawMessage.toString("utf-8");
|
|
3851
3940
|
await this.withConnection(accountId, async (client) => {
|
|
3852
|
-
await client.appendMessage(outboxPath, rawMessage, ["\\Seen"]);
|
|
3853
|
-
console.log(` [outbox] Queued message in ${outboxPath}`);
|
|
3941
|
+
appendedUid = await client.appendMessage(outboxPath, rawMessage, ["\\Seen"]);
|
|
3942
|
+
console.log(` [outbox] Queued message in ${outboxPath}${appendedUid != null ? ` (UID ${appendedUid})` : ""}`);
|
|
3854
3943
|
});
|
|
3855
|
-
const outboxFolder = this.findFolder(accountId, "outbox");
|
|
3856
3944
|
if (outboxFolder) {
|
|
3857
|
-
|
|
3945
|
+
if (appendedUid != null) {
|
|
3946
|
+
// Inserts the row from the source bytes we already have +
|
|
3947
|
+
// the UID the server assigned. No sync, no slow-lane
|
|
3948
|
+
// dependency. The row shows up in the Outbox view instantly.
|
|
3949
|
+
await this.insertLocalRowFromSource(accountId, outboxFolder, appendedUid, sourceStr, ["\\Seen"])
|
|
3950
|
+
.catch((e) => {
|
|
3951
|
+
console.error(` [outbox] Local Outbox row insert failed: ${e?.message || e} — falling back to broad sync`);
|
|
3952
|
+
this.syncFolder(accountId, outboxFolder.id).catch(() => { });
|
|
3953
|
+
});
|
|
3954
|
+
}
|
|
3955
|
+
else {
|
|
3956
|
+
// No APPENDUID (server doesn't support UIDPLUS, or APPEND
|
|
3957
|
+
// returned without one). Fall back to a broad sync.
|
|
3958
|
+
this.syncFolder(accountId, outboxFolder.id).catch(() => { });
|
|
3959
|
+
}
|
|
3858
3960
|
}
|
|
3859
3961
|
return;
|
|
3860
3962
|
}
|
|
@@ -4410,6 +4512,10 @@ export class ImapManager extends EventEmitter {
|
|
|
4410
4512
|
startOutboxWorker() {
|
|
4411
4513
|
if (this.outboxInterval)
|
|
4412
4514
|
return;
|
|
4515
|
+
// Seed requeue.cmd/.sh into every existing sending/<acct>/<label>/ dir
|
|
4516
|
+
// so a user staring at a stuck .ltr can re-queue it without knowing
|
|
4517
|
+
// the rmfmail -send syntax. Cheap, idempotent.
|
|
4518
|
+
this.seedRequeueScripts();
|
|
4413
4519
|
const processAll = async () => {
|
|
4414
4520
|
const now = Date.now();
|
|
4415
4521
|
// Auto-route any files dropped into the general (acct-agnostic)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bobfrankston/mailx-imap",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.69",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@bobfrankston/mailx-types": "^0.1.18",
|
|
13
13
|
"@bobfrankston/mailx-settings": "^0.1.25",
|
|
14
|
-
"@bobfrankston/mailx-store": "^0.1.
|
|
14
|
+
"@bobfrankston/mailx-store": "^0.1.40",
|
|
15
15
|
"@bobfrankston/iflow-direct": "^0.1.51",
|
|
16
16
|
"@bobfrankston/tcp-transport": "^0.1.6",
|
|
17
17
|
"@bobfrankston/smtp-direct": "^0.1.8",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"@bobfrankston/mailx-types": "^0.1.18",
|
|
41
41
|
"@bobfrankston/mailx-settings": "^0.1.25",
|
|
42
|
-
"@bobfrankston/mailx-store": "^0.1.
|
|
42
|
+
"@bobfrankston/mailx-store": "^0.1.40",
|
|
43
43
|
"@bobfrankston/iflow-direct": "^0.1.51",
|
|
44
44
|
"@bobfrankston/tcp-transport": "^0.1.6",
|
|
45
45
|
"@bobfrankston/smtp-direct": "^0.1.8",
|