@bobfrankston/rmfmail 1.1.230 → 1.1.231

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 (37) hide show
  1. package/client/android-bootstrap.bundle.js +4 -1
  2. package/client/android-bootstrap.bundle.js.map +2 -2
  3. package/client/app.bundle.js +25 -9
  4. package/client/app.bundle.js.map +2 -2
  5. package/client/components/message-viewer.js +30 -9
  6. package/client/components/message-viewer.js.map +1 -1
  7. package/client/components/message-viewer.ts +28 -9
  8. package/client/compose/compose.bundle.js +4 -4
  9. package/client/compose/compose.bundle.js.map +2 -2
  10. package/client/compose/compose.js +2 -2
  11. package/client/compose/compose.js.map +1 -1
  12. package/client/compose/compose.ts +2 -2
  13. package/client/lib/api-client.js +2 -2
  14. package/client/lib/api-client.js.map +1 -1
  15. package/client/lib/api-client.ts +2 -2
  16. package/package.json +5 -5
  17. package/packages/mailx-service/index.d.ts +1 -1
  18. package/packages/mailx-service/index.d.ts.map +1 -1
  19. package/packages/mailx-service/index.js +92 -5
  20. package/packages/mailx-service/index.js.map +1 -1
  21. package/packages/mailx-service/index.ts +83 -5
  22. package/packages/mailx-service/jsonrpc.js +1 -1
  23. package/packages/mailx-service/jsonrpc.js.map +1 -1
  24. package/packages/mailx-service/jsonrpc.ts +1 -1
  25. package/packages/mailx-store/db.d.ts.map +1 -1
  26. package/packages/mailx-store/db.js +30 -8
  27. package/packages/mailx-store/db.js.map +1 -1
  28. package/packages/mailx-store/db.ts +27 -8
  29. package/packages/mailx-types/index.d.ts +7 -3
  30. package/packages/mailx-types/index.d.ts.map +1 -1
  31. package/packages/mailx-types/index.js.map +1 -1
  32. package/packages/mailx-types/index.ts +7 -3
  33. package/packages/mailx-types/mailx-api.d.ts +1 -1
  34. package/packages/mailx-types/mailx-api.d.ts.map +1 -1
  35. package/packages/mailx-types/mailx-api.ts +1 -1
  36. package/tsconfig.base.json +1 -0
  37. package/packages/mailx-imap/node_modules.npmglobalize-stash-86824/.package-lock.json +0 -116
@@ -5,6 +5,7 @@
5
5
  */
6
6
  import * as dns from "node:dns/promises";
7
7
  import * as fs from "node:fs";
8
+ import * as os from "node:os";
8
9
  import * as path from "node:path";
9
10
  import { parseSerial } from "@bobfrankston/mailx-store";
10
11
  const __dirname = import.meta.dirname;
@@ -54,6 +55,74 @@ async function detectEmailProvider(domain) {
54
55
  return null;
55
56
  }
56
57
  // sanitizeHtml and encodeQuotedPrintable imported from @bobfrankston/mailx-types (shared with Android)
58
+ /** Scratch dir where the *Open* path stages an attachment before handing it to
59
+ * the OS default app. It lives under the OS temp dir — not `~/.rmfmail/` —
60
+ * because these are throwaway copies, not app state: the OS sweeps temp as a
61
+ * backstop and they don't masquerade as durable data. `start`/`open` only need
62
+ * the file to exist long enough for the launched app to read it. */
63
+ function getAttachmentStagingDir() {
64
+ return path.join(os.tmpdir(), "rmfmail", "attachments");
65
+ }
66
+ const ATTACHMENT_STAGING_MAX_AGE_MS = 24 * 60 * 60 * 1000;
67
+ /** Purge stale staged attachments (and retire the legacy
68
+ * `~/.rmfmail/attachments/` location, which never had cleanup and accumulated
69
+ * months of opened files). Best-effort: any error is swallowed — staging is
70
+ * not load-bearing. Called at startup and is cheap (one readdir). */
71
+ function cleanupAttachmentStaging(legacyDir) {
72
+ const sweep = (dir, maxAgeMs) => {
73
+ let entries;
74
+ try {
75
+ entries = fs.readdirSync(dir);
76
+ }
77
+ catch {
78
+ return;
79
+ }
80
+ const now = Date.now();
81
+ for (const name of entries) {
82
+ const p = path.join(dir, name);
83
+ try {
84
+ const st = fs.statSync(p);
85
+ if (st.isFile() && (maxAgeMs === 0 || now - st.mtimeMs > maxAgeMs)) {
86
+ fs.unlinkSync(p);
87
+ }
88
+ }
89
+ catch { /* ignore — file may have vanished or be locked */ }
90
+ }
91
+ };
92
+ sweep(getAttachmentStagingDir(), ATTACHMENT_STAGING_MAX_AGE_MS);
93
+ // Legacy location: these are all orphaned Open-staging copies, so clear
94
+ // them regardless of age (maxAge 0), then drop the now-empty dir.
95
+ sweep(legacyDir, 0);
96
+ try {
97
+ fs.rmdirSync(legacyDir);
98
+ }
99
+ catch { /* not empty / not present — fine */ }
100
+ }
101
+ /** Map a MIME type to a file extension so a saved attachment whose name lost
102
+ * its extension still dispatches to an OS handler. Covers the common types;
103
+ * unknown types return "" (the file opens without an extension as before). */
104
+ function extForMime(mime) {
105
+ const m = (mime || "").toLowerCase().split(";")[0].trim();
106
+ const map = {
107
+ "text/calendar": ".ics",
108
+ "application/ics": ".ics",
109
+ "application/pdf": ".pdf",
110
+ "text/plain": ".txt",
111
+ "text/html": ".html",
112
+ "image/png": ".png",
113
+ "image/jpeg": ".jpg",
114
+ "image/gif": ".gif",
115
+ "image/webp": ".webp",
116
+ "image/svg+xml": ".svg",
117
+ "application/zip": ".zip",
118
+ "application/json": ".json",
119
+ "application/msword": ".doc",
120
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document": ".docx",
121
+ "application/vnd.ms-excel": ".xls",
122
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": ".xlsx",
123
+ };
124
+ return map[m] || "";
125
+ }
57
126
  /** Compare a local task row to an incoming Google task projection. Used by
58
127
  * refreshTasks to skip no-op upserts that would otherwise emit `tasksUpdated`
59
128
  * on every poll, feeding back into the UI's getTasks-on-event listener and
@@ -147,6 +216,11 @@ export class MailxService {
147
216
  this.store.db.setOnContactsChanged(() => this.markContactsDirty());
148
217
  // Initial load of contacts.jsonc — fire-and-forget; missing file is fine.
149
218
  this.loadContactsConfig().catch(() => { });
219
+ // Sweep stale Open-staging copies and retire the legacy in-config dir.
220
+ try {
221
+ cleanupAttachmentStaging(path.join(getConfigDir(), "attachments"));
222
+ }
223
+ catch { /* best-effort */ }
150
224
  }
151
225
  /** Transitional getter — direct DB access from MailxService for the
152
226
  * ~50 callsites that haven't yet migrated to Store methods. Future:
@@ -2624,13 +2698,26 @@ export class MailxService {
2624
2698
  * a programmatic `<a download>` click is silently dropped inside msger's
2625
2699
  * WebView2, so the open must happen in the Node process (same pattern as
2626
2700
  * openInWord / openLocalPath). Cross-platform: start / open / xdg-open. */
2627
- async openAttachment(accountId, uid, attachmentId, folderId) {
2701
+ async openAttachment(accountId, uid, attachmentId, folderId, filename) {
2628
2702
  const att = await this.getAttachment(accountId, uid, attachmentId, folderId);
2629
- const dir = path.join(getConfigDir(), "attachments");
2703
+ const dir = getAttachmentStagingDir();
2630
2704
  fs.mkdirSync(dir, { recursive: true });
2631
- // basename() strips any path components so a crafted filename can't
2632
- // escape the temp dir; also drop newlines.
2633
- const safeName = (path.basename(att.filename || "attachment").replace(/[\r\n]/g, "_")) || "attachment";
2705
+ // Prefer the filename the UI is showing it comes from the sync-time
2706
+ // parse stored in the DB. The re-parse inside getAttachment() can lose
2707
+ // a part's name param (notably Google calendar invites: the part
2708
+ // re-parses with an empty filename), and a NAMELESS temp file gets no
2709
+ // extension, so `start` on Windows has no handler to dispatch to and
2710
+ // silently no-ops — the invite.ics that "did nothing" (2026-06-08).
2711
+ // basename() strips any path components so a crafted name can't escape
2712
+ // the temp dir; also drop newlines.
2713
+ let safeName = (path.basename(filename || att.filename || "attachment").replace(/[\r\n]/g, "_")) || "attachment";
2714
+ // Guarantee an extension so the OS has something to associate. Derive
2715
+ // it from the MIME type when the name lacks one.
2716
+ if (!path.extname(safeName)) {
2717
+ const ext = extForMime(att.contentType);
2718
+ if (ext)
2719
+ safeName += ext;
2720
+ }
2634
2721
  const target = path.join(dir, safeName);
2635
2722
  fs.writeFileSync(target, att.content);
2636
2723
  const { spawn } = await import("node:child_process");