@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.
- package/client/android-bootstrap.bundle.js +4 -1
- package/client/android-bootstrap.bundle.js.map +2 -2
- package/client/app.bundle.js +25 -9
- package/client/app.bundle.js.map +2 -2
- package/client/components/message-viewer.js +30 -9
- package/client/components/message-viewer.js.map +1 -1
- package/client/components/message-viewer.ts +28 -9
- package/client/compose/compose.bundle.js +4 -4
- package/client/compose/compose.bundle.js.map +2 -2
- package/client/compose/compose.js +2 -2
- package/client/compose/compose.js.map +1 -1
- package/client/compose/compose.ts +2 -2
- package/client/lib/api-client.js +2 -2
- package/client/lib/api-client.js.map +1 -1
- package/client/lib/api-client.ts +2 -2
- package/package.json +5 -5
- package/packages/mailx-service/index.d.ts +1 -1
- package/packages/mailx-service/index.d.ts.map +1 -1
- package/packages/mailx-service/index.js +92 -5
- package/packages/mailx-service/index.js.map +1 -1
- package/packages/mailx-service/index.ts +83 -5
- package/packages/mailx-service/jsonrpc.js +1 -1
- package/packages/mailx-service/jsonrpc.js.map +1 -1
- package/packages/mailx-service/jsonrpc.ts +1 -1
- package/packages/mailx-store/db.d.ts.map +1 -1
- package/packages/mailx-store/db.js +30 -8
- package/packages/mailx-store/db.js.map +1 -1
- package/packages/mailx-store/db.ts +27 -8
- package/packages/mailx-types/index.d.ts +7 -3
- package/packages/mailx-types/index.d.ts.map +1 -1
- package/packages/mailx-types/index.js.map +1 -1
- package/packages/mailx-types/index.ts +7 -3
- package/packages/mailx-types/mailx-api.d.ts +1 -1
- package/packages/mailx-types/mailx-api.d.ts.map +1 -1
- package/packages/mailx-types/mailx-api.ts +1 -1
- package/tsconfig.base.json +1 -0
- 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 =
|
|
2703
|
+
const dir = getAttachmentStagingDir();
|
|
2630
2704
|
fs.mkdirSync(dir, { recursive: true });
|
|
2631
|
-
//
|
|
2632
|
-
//
|
|
2633
|
-
|
|
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");
|