@bobfrankston/rmfmail 1.2.219 → 1.2.220

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.
@@ -3443,26 +3443,62 @@ export class MailxService implements MailxApi {
3443
3443
  if (ext) safeName += ext;
3444
3444
  }
3445
3445
  const target = path.join(dir, safeName);
3446
- fs.writeFileSync(target, att.content);
3447
- if (process.platform === "win32") {
3448
- // Mark-of-the-Web: tag the staged file as coming from the Internet
3449
- // (Zone.Identifier ADS, ZoneId 3) so Office opens it in Protected
3450
- // View and SmartScreen vets executables same treatment a browser
3451
- // download gets. Email attachments are exactly the threat model
3452
- // MotW exists for (Bob 2026-07-02: "the file should be flagged as
3453
- // being saved from an unknown source"). Best-effort: ADS needs
3454
- // NTFS; FAT/exFAT volumes throw and we still open the file.
3455
- try { fs.writeFileSync(`${target}:Zone.Identifier`, "[ZoneTransfer]\r\nZoneId=3\r\n"); } catch { /* non-NTFS */ }
3446
+ // Mark-of-the-Web: tag the staged file as coming from the Internet
3447
+ // (Zone.Identifier ADS, ZoneId 3) so Office opens it in Protected
3448
+ // View and SmartScreen vets executables same treatment a browser
3449
+ // download gets. Email attachments are exactly the threat model
3450
+ // MotW exists for (Bob 2026-07-02: "the file should be flagged as
3451
+ // being saved from an unknown source"). Best-effort: ADS needs
3452
+ // NTFS; FAT/exFAT volumes throw and we still open the file.
3453
+ const markOfTheWeb = (p: string): void => {
3454
+ if (process.platform !== "win32") return;
3455
+ try { fs.writeFileSync(`${p}:Zone.Identifier`, "[ZoneTransfer]\r\nZoneId=3\r\n"); } catch { /* non-NTFS or locked */ }
3456
+ };
3457
+ // Staging path collides with itself when the SAME attachment is opened
3458
+ // twice: the viewer launched by the first open (Acrobat, Edge) holds
3459
+ // the file with a deny-write share, so the second writeFileSync throws
3460
+ // EBUSY and the user gets a red banner instead of their PDF (Bob
3461
+ // 2026-08-06, "278 Lake Ave Newton - Bob Frankston.pdf"). Two outs:
3462
+ // 1. Byte-identical file already staged → don't rewrite it at all.
3463
+ // This is the double-click case; just re-launch. Cheapest and it
3464
+ // never touches the lock.
3465
+ // 2. Different content but the name is locked (a DIFFERENT
3466
+ // attachment sharing a filename) → stage under a unique sibling
3467
+ // so both stay openable.
3468
+ let staged = target;
3469
+ let existing: Buffer | null = null;
3470
+ try { existing = fs.readFileSync(target); } catch { existing = null; }
3471
+ if (!existing || !existing.equals(att.content)) {
3472
+ try {
3473
+ fs.writeFileSync(target, att.content);
3474
+ markOfTheWeb(target);
3475
+ } catch (e: any) {
3476
+ const locked = e?.code === "EBUSY" || e?.code === "EPERM" || e?.code === "EACCES";
3477
+ if (!locked) throw e;
3478
+ const ext = path.extname(safeName);
3479
+ const stem = safeName.slice(0, safeName.length - ext.length);
3480
+ let n = 2;
3481
+ do { staged = path.join(dir, `${stem} (${n++})${ext}`); } while (fs.existsSync(staged) && n <= 99);
3482
+ if (fs.existsSync(staged)) throw e; // 99 locked variants — something is really wrong, surface it
3483
+ console.log(` [attachment] "${safeName}" locked by another app (${e.code}) — staging as "${path.basename(staged)}"`);
3484
+ fs.writeFileSync(staged, att.content);
3485
+ markOfTheWeb(staged);
3486
+ }
3487
+ } else {
3488
+ // Re-open of an already-staged copy. Re-assert MotW in case the
3489
+ // staged file predates the tagging (or the ADS was stripped);
3490
+ // no-op when it's already there.
3491
+ markOfTheWeb(target);
3456
3492
  }
3457
3493
  const { spawn } = await import("node:child_process");
3458
3494
  if (process.platform === "win32") {
3459
- spawn("cmd", ["/c", "start", "", target], { detached: true, stdio: "ignore", windowsHide: true }).unref();
3495
+ spawn("cmd", ["/c", "start", "", staged], { detached: true, stdio: "ignore", windowsHide: true }).unref();
3460
3496
  } else if (process.platform === "darwin") {
3461
- spawn("open", [target], { detached: true, stdio: "ignore" }).unref();
3497
+ spawn("open", [staged], { detached: true, stdio: "ignore" }).unref();
3462
3498
  } else {
3463
- spawn("xdg-open", [target], { detached: true, stdio: "ignore" }).unref();
3499
+ spawn("xdg-open", [staged], { detached: true, stdio: "ignore" }).unref();
3464
3500
  }
3465
- return { ok: true, path: target };
3501
+ return { ok: true, path: staged };
3466
3502
  }
3467
3503
 
3468
3504
  /** Open an http/https/mailto URL in the OS default browser/handler from the
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/mailx-service",
3
- "version": "0.1.17",
3
+ "version": "0.1.18",
4
4
  "type": "module",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",