@bobfrankston/mailx-imap 0.1.66 → 0.1.68
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 +101 -9
- package/package.json +5 -5
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
|
@@ -1238,7 +1238,7 @@ export class ImapManager extends EventEmitter {
|
|
|
1238
1238
|
for (const uid of qr.vanishedUids) {
|
|
1239
1239
|
const env = this.db.getMessageByUid(accountId, uid, folderId);
|
|
1240
1240
|
if (env) {
|
|
1241
|
-
this.db.deleteMessage(accountId, uid, "server VANISHED via QRESYNC", "mailx-imap syncFolder/qresync");
|
|
1241
|
+
this.db.deleteMessage(accountId, folderId, uid, "server VANISHED via QRESYNC", "mailx-imap syncFolder/qresync");
|
|
1242
1242
|
vanishedApplied++;
|
|
1243
1243
|
}
|
|
1244
1244
|
}
|
|
@@ -2078,7 +2078,7 @@ export class ImapManager extends EventEmitter {
|
|
|
2078
2078
|
const tag = env ? `msgid=${env.messageId || "?"} subj="${(env.subject || "").slice(0, 60)}"` : "unknown";
|
|
2079
2079
|
console.log(` [reconcile-delete] ${accountId}/${folder.path} uid=${uid} ${tag}`);
|
|
2080
2080
|
this.unlinkBodyFile(accountId, uid, folder.id).catch(() => { });
|
|
2081
|
-
this.db.deleteMessage(accountId, uid, "Gmail-API reconcile: server list missing this UID", `mailx-imap Gmail reconcile (${folder.path})`);
|
|
2081
|
+
this.db.deleteMessage(accountId, folder.id, uid, "Gmail-API reconcile: server list missing this UID", `mailx-imap Gmail reconcile (${folder.path})`);
|
|
2082
2082
|
}
|
|
2083
2083
|
if (toDelete.length > 0)
|
|
2084
2084
|
console.log(` [api] ${accountId}/${folder.path}: ${toDelete.length} deleted`);
|
|
@@ -2632,7 +2632,7 @@ export class ImapManager extends EventEmitter {
|
|
|
2632
2632
|
const tag = env.messageId ? `msgid=${env.messageId} subj="${(env.subject || "").slice(0, 60)}"` : "no-msgid";
|
|
2633
2633
|
console.log(` [reconcile-delete] ${accountId}/${folderPath} uid=${uid} ${tag} (legacy path)`);
|
|
2634
2634
|
this.unlinkBodyFile(accountId, uid, folderId).catch(() => { });
|
|
2635
|
-
this.db.deleteMessage(accountId, uid, "reconcile: server missing this UID after grace (legacy)", `mailx-imap deferred reconcile (${folderPath})`);
|
|
2635
|
+
this.db.deleteMessage(accountId, folderId, uid, "reconcile: server missing this UID after grace (legacy)", `mailx-imap deferred reconcile (${folderPath})`);
|
|
2636
2636
|
}
|
|
2637
2637
|
this.db.recalcFolderCounts(folderId);
|
|
2638
2638
|
this.emit("folderCountsChanged", accountId, {});
|
|
@@ -3214,7 +3214,7 @@ export class ImapManager extends EventEmitter {
|
|
|
3214
3214
|
if (!msg)
|
|
3215
3215
|
throw new Error(`Message UID ${uid} not found in ${fromFolder.path}`);
|
|
3216
3216
|
await sourceClient.moveMessageToServer(msg, fromFolder.path, targetClient, toFolder.path);
|
|
3217
|
-
this.db.deleteMessage(fromAccountId, uid, `cross-account move to ${toAccountId}/${toFolder.path}`, "mailx-imap moveBetweenAccounts");
|
|
3217
|
+
this.db.deleteMessage(fromAccountId, fromFolder.id, uid, `cross-account move to ${toAccountId}/${toFolder.path}`, "mailx-imap moveBetweenAccounts");
|
|
3218
3218
|
console.log(` Cross-account move: ${fromAccountId}/${fromFolder.path} UID ${uid} → ${toAccountId}/${toFolder.path}`);
|
|
3219
3219
|
});
|
|
3220
3220
|
});
|
|
@@ -3561,7 +3561,7 @@ export class ImapManager extends EventEmitter {
|
|
|
3561
3561
|
const existing = this.db.getMessageByUid(accountId, draftUid, drafts.id);
|
|
3562
3562
|
if (existing) {
|
|
3563
3563
|
this.unlinkBodyFile(accountId, draftUid, drafts.id).catch(() => { });
|
|
3564
|
-
this.db.deleteMessage(accountId, draftUid, "user sent the message (draft cleanup)", "mailx-imap deleteDraft (local)");
|
|
3564
|
+
this.db.deleteMessage(accountId, drafts.id, draftUid, "user sent the message (draft cleanup)", "mailx-imap deleteDraft (local)");
|
|
3565
3565
|
localDeletedUid = draftUid;
|
|
3566
3566
|
}
|
|
3567
3567
|
}
|
|
@@ -3827,6 +3827,7 @@ export class ImapManager extends EventEmitter {
|
|
|
3827
3827
|
try {
|
|
3828
3828
|
const dir = path.join(getConfigDir(), "sending", accountId, label);
|
|
3829
3829
|
fs.mkdirSync(dir, { recursive: true });
|
|
3830
|
+
this.ensureRequeueScripts(dir);
|
|
3830
3831
|
const now = new Date();
|
|
3831
3832
|
const pad2 = (n) => String(n).padStart(2, "0");
|
|
3832
3833
|
const ts = `${now.getFullYear()}${pad2(now.getMonth() + 1)}${pad2(now.getDate())}_${pad2(now.getHours())}${pad2(now.getMinutes())}${pad2(now.getSeconds())}`;
|
|
@@ -3837,6 +3838,68 @@ export class ImapManager extends EventEmitter {
|
|
|
3837
3838
|
console.error(` [sending] Failed to save copy: ${e.message}`);
|
|
3838
3839
|
}
|
|
3839
3840
|
}
|
|
3841
|
+
/** Drop a requeue.cmd / requeue.sh into a sending/<acct>/<label>/ directory
|
|
3842
|
+
* so the user can re-queue a saved .eml/.ltr without remembering the
|
|
3843
|
+
* rmfmail -send syntax. Idempotent: only writes when missing.
|
|
3844
|
+
* The word is "requeue" — it puts the file back into the outbox queue;
|
|
3845
|
+
* it does NOT send a fresh copy bypassing the queue. */
|
|
3846
|
+
ensureRequeueScripts(dir) {
|
|
3847
|
+
const cmdPath = path.join(dir, "requeue.cmd");
|
|
3848
|
+
if (!fs.existsSync(cmdPath)) {
|
|
3849
|
+
const cmd = "@echo off\r\n" +
|
|
3850
|
+
"rem Re-queue a saved .eml / .ltr for sending. Pass the filename:\r\n" +
|
|
3851
|
+
"rem requeue 20260527_140527-6599.ltr\r\n" +
|
|
3852
|
+
"rem With no arg, lists files in this directory.\r\n" +
|
|
3853
|
+
"if \"%~1\"==\"\" (\r\n" +
|
|
3854
|
+
" dir /b *.eml *.ltr 2>nul\r\n" +
|
|
3855
|
+
" exit /b 0\r\n" +
|
|
3856
|
+
")\r\n" +
|
|
3857
|
+
"rmfmail -send \"%~dp0%~1\"\r\n";
|
|
3858
|
+
fs.writeFileSync(cmdPath, cmd);
|
|
3859
|
+
}
|
|
3860
|
+
const shPath = path.join(dir, "requeue.sh");
|
|
3861
|
+
if (!fs.existsSync(shPath)) {
|
|
3862
|
+
const sh = "#!/bin/sh\n" +
|
|
3863
|
+
"# Re-queue a saved .eml / .ltr for sending. Pass the filename:\n" +
|
|
3864
|
+
"# ./requeue.sh 20260527_140527-6599.ltr\n" +
|
|
3865
|
+
"# With no arg, lists files in this directory.\n" +
|
|
3866
|
+
"DIR=\"$(cd \"$(dirname \"$0\")\" && pwd)\"\n" +
|
|
3867
|
+
"if [ -z \"$1\" ]; then\n" +
|
|
3868
|
+
" ls \"$DIR\"/*.eml \"$DIR\"/*.ltr 2>/dev/null | xargs -n1 basename 2>/dev/null\n" +
|
|
3869
|
+
" exit 0\n" +
|
|
3870
|
+
"fi\n" +
|
|
3871
|
+
"rmfmail -send \"$DIR/$1\"\n";
|
|
3872
|
+
fs.writeFileSync(shPath, sh);
|
|
3873
|
+
try {
|
|
3874
|
+
fs.chmodSync(shPath, 0o755);
|
|
3875
|
+
}
|
|
3876
|
+
catch { /* */ }
|
|
3877
|
+
}
|
|
3878
|
+
}
|
|
3879
|
+
/** Walk the sending tree at startup and drop a requeue.cmd/.sh into every
|
|
3880
|
+
* existing sending/<acct>/<label>/ directory. Without this the scripts
|
|
3881
|
+
* only appear on the next outgoing message, which doesn't help a user
|
|
3882
|
+
* who needs to re-queue a stuck letter from before the upgrade. */
|
|
3883
|
+
seedRequeueScripts() {
|
|
3884
|
+
try {
|
|
3885
|
+
const root = path.join(getConfigDir(), "sending");
|
|
3886
|
+
if (!fs.existsSync(root))
|
|
3887
|
+
return;
|
|
3888
|
+
for (const acct of fs.readdirSync(root, { withFileTypes: true })) {
|
|
3889
|
+
if (!acct.isDirectory())
|
|
3890
|
+
continue;
|
|
3891
|
+
const acctDir = path.join(root, acct.name);
|
|
3892
|
+
for (const label of fs.readdirSync(acctDir, { withFileTypes: true })) {
|
|
3893
|
+
if (!label.isDirectory())
|
|
3894
|
+
continue;
|
|
3895
|
+
this.ensureRequeueScripts(path.join(acctDir, label.name));
|
|
3896
|
+
}
|
|
3897
|
+
}
|
|
3898
|
+
}
|
|
3899
|
+
catch (e) {
|
|
3900
|
+
console.error(` [sending] seedRequeueScripts failed: ${e?.message || e}`);
|
|
3901
|
+
}
|
|
3902
|
+
}
|
|
3840
3903
|
/** Queue a message for sending. Tries IMAP Outbox, falls back to local file. */
|
|
3841
3904
|
async queueOutgoing(accountId, rawMessage) {
|
|
3842
3905
|
// IMPORTANT: do NOT save a "debug copy" to sending/<acct>/queued/ here.
|
|
@@ -3848,13 +3911,38 @@ export class ImapManager extends EventEmitter {
|
|
|
3848
3911
|
// - ~/.mailx/outbox/<acct>/*.ltr (fallback when IMAP is unreachable)
|
|
3849
3912
|
try {
|
|
3850
3913
|
const outboxPath = await this.ensureOutbox(accountId);
|
|
3914
|
+
const outboxFolder = this.findFolder(accountId, "outbox");
|
|
3915
|
+
// Capture APPENDUID. The Sent folder path (processOutbox at line
|
|
3916
|
+
// ~4150) does this and then calls insertLocalRowFromSource so the
|
|
3917
|
+
// local Sent view is instantly populated without a sync round-trip.
|
|
3918
|
+
// The Outbox path used to ONLY trigger fire-and-forget syncFolder,
|
|
3919
|
+
// which on a wedged slow-lane (Bob 2026-05-27 14:11: syncAll held
|
|
3920
|
+
// the lane for the whole afternoon) left the queued message
|
|
3921
|
+
// invisible in the local Outbox view until next daemon restart.
|
|
3922
|
+
let appendedUid = null;
|
|
3923
|
+
const sourceStr = typeof rawMessage === "string"
|
|
3924
|
+
? rawMessage
|
|
3925
|
+
: rawMessage.toString("utf-8");
|
|
3851
3926
|
await this.withConnection(accountId, async (client) => {
|
|
3852
|
-
await client.appendMessage(outboxPath, rawMessage, ["\\Seen"]);
|
|
3853
|
-
console.log(` [outbox] Queued message in ${outboxPath}`);
|
|
3927
|
+
appendedUid = await client.appendMessage(outboxPath, rawMessage, ["\\Seen"]);
|
|
3928
|
+
console.log(` [outbox] Queued message in ${outboxPath}${appendedUid != null ? ` (UID ${appendedUid})` : ""}`);
|
|
3854
3929
|
});
|
|
3855
|
-
const outboxFolder = this.findFolder(accountId, "outbox");
|
|
3856
3930
|
if (outboxFolder) {
|
|
3857
|
-
|
|
3931
|
+
if (appendedUid != null) {
|
|
3932
|
+
// Inserts the row from the source bytes we already have +
|
|
3933
|
+
// the UID the server assigned. No sync, no slow-lane
|
|
3934
|
+
// dependency. The row shows up in the Outbox view instantly.
|
|
3935
|
+
await this.insertLocalRowFromSource(accountId, outboxFolder, appendedUid, sourceStr, ["\\Seen"])
|
|
3936
|
+
.catch((e) => {
|
|
3937
|
+
console.error(` [outbox] Local Outbox row insert failed: ${e?.message || e} — falling back to broad sync`);
|
|
3938
|
+
this.syncFolder(accountId, outboxFolder.id).catch(() => { });
|
|
3939
|
+
});
|
|
3940
|
+
}
|
|
3941
|
+
else {
|
|
3942
|
+
// No APPENDUID (server doesn't support UIDPLUS, or APPEND
|
|
3943
|
+
// returned without one). Fall back to a broad sync.
|
|
3944
|
+
this.syncFolder(accountId, outboxFolder.id).catch(() => { });
|
|
3945
|
+
}
|
|
3858
3946
|
}
|
|
3859
3947
|
return;
|
|
3860
3948
|
}
|
|
@@ -4410,6 +4498,10 @@ export class ImapManager extends EventEmitter {
|
|
|
4410
4498
|
startOutboxWorker() {
|
|
4411
4499
|
if (this.outboxInterval)
|
|
4412
4500
|
return;
|
|
4501
|
+
// Seed requeue.cmd/.sh into every existing sending/<acct>/<label>/ dir
|
|
4502
|
+
// so a user staring at a stuck .ltr can re-queue it without knowing
|
|
4503
|
+
// the rmfmail -send syntax. Cheap, idempotent.
|
|
4504
|
+
this.seedRequeueScripts();
|
|
4413
4505
|
const processAll = async () => {
|
|
4414
4506
|
const now = Date.now();
|
|
4415
4507
|
// 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.68",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -11,12 +11,12 @@
|
|
|
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",
|
|
18
18
|
"@bobfrankston/mailx-sync": "^0.1.19",
|
|
19
|
-
"@bobfrankston/oauthsupport": "^1.0.
|
|
19
|
+
"@bobfrankston/oauthsupport": "^1.0.29"
|
|
20
20
|
},
|
|
21
21
|
"repository": {
|
|
22
22
|
"type": "git",
|
|
@@ -39,12 +39,12 @@
|
|
|
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",
|
|
46
46
|
"@bobfrankston/mailx-sync": "^0.1.19",
|
|
47
|
-
"@bobfrankston/oauthsupport": "^1.0.
|
|
47
|
+
"@bobfrankston/oauthsupport": "^1.0.29"
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
}
|