@bobfrankston/mailx-imap 0.1.67 → 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.
Files changed (3) hide show
  1. package/index.d.ts +11 -0
  2. package/index.js +96 -4
  3. 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
@@ -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
- this.syncFolder(accountId, outboxFolder.id).catch(() => { });
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.67",
3
+ "version": "0.1.68",
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.39",
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.39",
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",