@bobfrankston/rmfmail 1.2.173 → 1.2.175
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/TODO.md +3 -3
- package/bin/build-rmfshare-exe.js +79 -0
- package/bin/mailx.js +52 -1
- package/bin/mailx.js.map +1 -1
- package/bin/mailx.ts +51 -1
- package/bin/rmfshare-src/Program.cs +225 -0
- package/bin/rmfshare-src/rmfshare.csproj +36 -0
- package/bin/rmfshare.exe +0 -0
- package/bin/share-target.js +314 -0
- package/bin/share-target.js.map +1 -0
- package/bin/share-target.ts +331 -0
- package/client/app.bundle.js +62 -0
- package/client/app.bundle.js.map +3 -3
- package/client/app.js +69 -0
- package/client/app.js.map +1 -1
- package/client/app.ts +72 -0
- package/client/compose/compose.bundle.js +4 -0
- package/client/compose/compose.bundle.js.map +2 -2
- package/client/lib/api-client.js +7 -0
- package/client/lib/api-client.js.map +1 -1
- package/client/lib/api-client.ts +12 -0
- package/client/package.json +1 -1
- package/package.json +6 -6
- package/packages/mailx-imap/package-lock.json +2 -2
- package/packages/mailx-imap/package.json +1 -1
- package/packages/mailx-service/index.d.ts +22 -0
- package/packages/mailx-service/index.d.ts.map +1 -1
- package/packages/mailx-service/index.js +82 -0
- package/packages/mailx-service/index.js.map +1 -1
- package/packages/mailx-service/index.ts +76 -0
- package/packages/mailx-service/jsonrpc.js +2 -0
- package/packages/mailx-service/jsonrpc.js.map +1 -1
- package/packages/mailx-service/jsonrpc.ts +2 -0
- package/packages/mailx-service/package.json +1 -1
- package/packages/mailx-settings/package.json +1 -1
- package/packages/mailx-store/package.json +1 -1
- package/packages/mailx-store-web/package.json +1 -1
- package/packages/mailx-types/mailx-api.d.ts +3 -0
- package/packages/mailx-types/mailx-api.d.ts.map +1 -1
- package/packages/mailx-types/mailx-api.ts +3 -0
- package/packages/mailx-types/package.json +1 -1
- /package/packages/mailx-imap/{node_modules.npmglobalize-stash-7284 → node_modules.npmglobalize-stash-33656}/.package-lock.json +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"share-target.js","sourceRoot":"","sources":["share-target.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Windows Share-target integration (C46).
|
|
3
|
+
*
|
|
4
|
+
* Share flow when rmfmail is picked in the Windows Share sheet:
|
|
5
|
+
*
|
|
6
|
+
* 1. Windows activates rmfshare.exe (bin/rmfshare-src/, declared in the
|
|
7
|
+
* AppxManifest registered by `rmfmail -register-share`).
|
|
8
|
+
* 2. rmfshare.exe copies shared files to ~/.rmfmail/share-staging/<stamp>/,
|
|
9
|
+
* writes ~/.rmfmail/pending-share.json (atomic rename), and launches
|
|
10
|
+
* `node mailx.js` — which exits at the instance check if a daemon
|
|
11
|
+
* already runs (its fs.watch on the pending file has fired) or becomes
|
|
12
|
+
* the daemon (the client's startup poll consumes the file).
|
|
13
|
+
* 3. The daemon loads the staged files as base64 and pushes an `openShare`
|
|
14
|
+
* event; the client opens compose with the shared content attached.
|
|
15
|
+
*
|
|
16
|
+
* Same pending-file indirection as the P115 mailto handler (bin/mailto.ts) —
|
|
17
|
+
* no IPC socket needed, one share = one file write.
|
|
18
|
+
*
|
|
19
|
+
* Registration is a "loose" AppX deployment: a staged folder holding
|
|
20
|
+
* rmfshare.exe + AppxManifest.xml + Assets, registered per-user via
|
|
21
|
+
* `Add-AppxPackage -Register`. That requires Developer Mode (unsigned
|
|
22
|
+
* manifest) — the registration path checks and says so. No MSIX signing,
|
|
23
|
+
* no Store, no admin.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import fs from "node:fs";
|
|
27
|
+
import path from "node:path";
|
|
28
|
+
import { execFileSync } from "node:child_process";
|
|
29
|
+
|
|
30
|
+
export interface PendingShareFile {
|
|
31
|
+
name: string;
|
|
32
|
+
/** Absolute path inside ~/.rmfmail/share-staging/<stamp>/ */
|
|
33
|
+
path: string;
|
|
34
|
+
size: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface PendingShareData {
|
|
38
|
+
title: string;
|
|
39
|
+
text: string;
|
|
40
|
+
url: string;
|
|
41
|
+
files: PendingShareFile[];
|
|
42
|
+
writtenAt: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface ShareAttachment {
|
|
46
|
+
filename: string;
|
|
47
|
+
mimeType: string;
|
|
48
|
+
dataBase64: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Attachments larger than this are skipped (typical SMTP servers reject
|
|
52
|
+
* messages this size anyway); the skip is reported, never silent. */
|
|
53
|
+
export const SHARE_ATTACHMENT_MAX_BYTES = 25 * 1024 * 1024;
|
|
54
|
+
|
|
55
|
+
const IDENTITY_NAME = "BobFrankston.rmfmail";
|
|
56
|
+
|
|
57
|
+
function rmfmailDir(): string {
|
|
58
|
+
const home = process.env.USERPROFILE || process.env.HOME || ".";
|
|
59
|
+
return path.join(home, ".rmfmail");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function getPendingShareFile(): string {
|
|
63
|
+
return path.join(rmfmailDir(), "pending-share.json");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Read + delete the pending-share file. One-shot, same contract as
|
|
67
|
+
* consumePendingMailto: caller acts on the result, the file is gone either
|
|
68
|
+
* way, stale files (crashed handoff) are dropped. */
|
|
69
|
+
export function consumePendingShare(): PendingShareData | null {
|
|
70
|
+
const target = getPendingShareFile();
|
|
71
|
+
let raw: string;
|
|
72
|
+
try { raw = fs.readFileSync(target, "utf-8"); }
|
|
73
|
+
catch { return null; }
|
|
74
|
+
try { fs.unlinkSync(target); } catch { /* */ }
|
|
75
|
+
let parsed: PendingShareData;
|
|
76
|
+
try { parsed = JSON.parse(raw) as PendingShareData; }
|
|
77
|
+
catch { return null; }
|
|
78
|
+
const age = Date.now() - (parsed.writtenAt || 0);
|
|
79
|
+
if (age > 10 * 60_000) {
|
|
80
|
+
// Crashed handoff — user shared but nothing consumed it for 10
|
|
81
|
+
// minutes. Drop it (staging cleanup reaps the copied files).
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
parsed.title = parsed.title || "";
|
|
85
|
+
parsed.text = parsed.text || "";
|
|
86
|
+
parsed.url = parsed.url || "";
|
|
87
|
+
parsed.files = Array.isArray(parsed.files) ? parsed.files : [];
|
|
88
|
+
return parsed;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const MIME_BY_EXT: Record<string, string> = {
|
|
92
|
+
".pdf": "application/pdf",
|
|
93
|
+
".png": "image/png",
|
|
94
|
+
".jpg": "image/jpeg", ".jpeg": "image/jpeg",
|
|
95
|
+
".gif": "image/gif",
|
|
96
|
+
".webp": "image/webp",
|
|
97
|
+
".bmp": "image/bmp",
|
|
98
|
+
".svg": "image/svg+xml",
|
|
99
|
+
".heic": "image/heic",
|
|
100
|
+
".txt": "text/plain", ".log": "text/plain", ".md": "text/plain",
|
|
101
|
+
".html": "text/html", ".htm": "text/html",
|
|
102
|
+
".csv": "text/csv",
|
|
103
|
+
".json": "application/json",
|
|
104
|
+
".xml": "application/xml",
|
|
105
|
+
".zip": "application/zip",
|
|
106
|
+
".7z": "application/x-7z-compressed",
|
|
107
|
+
".doc": "application/msword",
|
|
108
|
+
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
109
|
+
".xls": "application/vnd.ms-excel",
|
|
110
|
+
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
111
|
+
".ppt": "application/vnd.ms-powerpoint",
|
|
112
|
+
".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
113
|
+
".eml": "message/rfc822",
|
|
114
|
+
".ics": "text/calendar",
|
|
115
|
+
".mp3": "audio/mpeg",
|
|
116
|
+
".mp4": "video/mp4",
|
|
117
|
+
".mov": "video/quicktime",
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
export function mimeForFilename(name: string): string {
|
|
121
|
+
return MIME_BY_EXT[path.extname(name).toLowerCase()] || "application/octet-stream";
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Load staged share files as compose-init attachments (base64). Oversized
|
|
125
|
+
* or unreadable files land in `skipped` with a reason — surfaced to the
|
|
126
|
+
* user in the compose body, never dropped silently. Consumed staging dirs
|
|
127
|
+
* are deleted afterwards; the content lives on in the compose draft. */
|
|
128
|
+
export function loadShareAttachments(files: PendingShareFile[]): {
|
|
129
|
+
attachments: ShareAttachment[];
|
|
130
|
+
skipped: { name: string; reason: string }[];
|
|
131
|
+
} {
|
|
132
|
+
const attachments: ShareAttachment[] = [];
|
|
133
|
+
const skipped: { name: string; reason: string }[] = [];
|
|
134
|
+
const dirs = new Set<string>();
|
|
135
|
+
for (const f of files) {
|
|
136
|
+
try {
|
|
137
|
+
const st = fs.statSync(f.path);
|
|
138
|
+
if (st.size > SHARE_ATTACHMENT_MAX_BYTES) {
|
|
139
|
+
skipped.push({ name: f.name, reason: `larger than ${Math.round(SHARE_ATTACHMENT_MAX_BYTES / 1024 / 1024)} MB` });
|
|
140
|
+
} else {
|
|
141
|
+
attachments.push({
|
|
142
|
+
filename: f.name,
|
|
143
|
+
mimeType: mimeForFilename(f.name),
|
|
144
|
+
dataBase64: fs.readFileSync(f.path).toString("base64"),
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
dirs.add(path.dirname(f.path));
|
|
148
|
+
} catch (e: any) {
|
|
149
|
+
skipped.push({ name: f.name, reason: e?.message || "unreadable" });
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
for (const d of dirs) {
|
|
153
|
+
// Only reap inside our own staging root — a hand-written pending
|
|
154
|
+
// file must never turn this into an arbitrary directory delete.
|
|
155
|
+
if (d.startsWith(path.join(rmfmailDir(), "share-staging") + path.sep)) {
|
|
156
|
+
try { fs.rmSync(d, { recursive: true, force: true }); } catch { /* next startup sweep */ }
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return { attachments, skipped };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Reap staging dirs older than 2 days — leftovers from shares that never
|
|
163
|
+
* got consumed (daemon crash between copy and compose). Called once at
|
|
164
|
+
* daemon startup. */
|
|
165
|
+
export function cleanupShareStaging(): void {
|
|
166
|
+
const root = path.join(rmfmailDir(), "share-staging");
|
|
167
|
+
let entries: fs.Dirent[];
|
|
168
|
+
try { entries = fs.readdirSync(root, { withFileTypes: true }); }
|
|
169
|
+
catch { return; }
|
|
170
|
+
const cutoff = Date.now() - 2 * 24 * 3600_000;
|
|
171
|
+
for (const e of entries) {
|
|
172
|
+
if (!e.isDirectory()) continue;
|
|
173
|
+
const full = path.join(root, e.name);
|
|
174
|
+
try {
|
|
175
|
+
if (fs.statSync(full).mtimeMs < cutoff) {
|
|
176
|
+
fs.rmSync(full, { recursive: true, force: true });
|
|
177
|
+
}
|
|
178
|
+
} catch { /* in use or already gone */ }
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ── Registration (Windows only) ─────────────────────────────────────────
|
|
183
|
+
|
|
184
|
+
function buildAppxManifest(version: string): string {
|
|
185
|
+
// Unsigned loose deployment: Publisher only has to be well-formed, it
|
|
186
|
+
// isn't validated against a certificate. AppListEntry="none" keeps the
|
|
187
|
+
// entry out of the Start menu — rmfshare.exe is a share sink, not an
|
|
188
|
+
// app the user launches.
|
|
189
|
+
return `<?xml version="1.0" encoding="utf-8"?>
|
|
190
|
+
<Package
|
|
191
|
+
xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
|
|
192
|
+
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
|
|
193
|
+
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
|
|
194
|
+
IgnorableNamespaces="uap rescap">
|
|
195
|
+
<Identity Name="${IDENTITY_NAME}" Publisher="CN=Bob Frankston" Version="${version}" ProcessorArchitecture="x64" />
|
|
196
|
+
<Properties>
|
|
197
|
+
<DisplayName>rmfmail</DisplayName>
|
|
198
|
+
<PublisherDisplayName>Bob Frankston</PublisherDisplayName>
|
|
199
|
+
<Logo>Assets\\logo150.png</Logo>
|
|
200
|
+
</Properties>
|
|
201
|
+
<Dependencies>
|
|
202
|
+
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.19041.0" MaxVersionTested="10.0.26100.0" />
|
|
203
|
+
</Dependencies>
|
|
204
|
+
<Resources>
|
|
205
|
+
<Resource Language="en-us" />
|
|
206
|
+
</Resources>
|
|
207
|
+
<Applications>
|
|
208
|
+
<Application Id="rmfmail" Executable="rmfshare.exe" EntryPoint="Windows.FullTrustApplication">
|
|
209
|
+
<uap:VisualElements DisplayName="rmfmail" Description="Share to rmfmail"
|
|
210
|
+
BackgroundColor="transparent"
|
|
211
|
+
Square150x150Logo="Assets\\logo150.png" Square44x44Logo="Assets\\logo44.png"
|
|
212
|
+
AppListEntry="none" />
|
|
213
|
+
<Extensions>
|
|
214
|
+
<uap:Extension Category="windows.shareTarget">
|
|
215
|
+
<uap:ShareTarget Description="Share to rmfmail">
|
|
216
|
+
<uap:SupportedFileTypes>
|
|
217
|
+
<uap:SupportsAnyFileType />
|
|
218
|
+
</uap:SupportedFileTypes>
|
|
219
|
+
<uap:DataFormat>Text</uap:DataFormat>
|
|
220
|
+
<uap:DataFormat>WebLink</uap:DataFormat>
|
|
221
|
+
<uap:DataFormat>StorageItems</uap:DataFormat>
|
|
222
|
+
</uap:ShareTarget>
|
|
223
|
+
</uap:Extension>
|
|
224
|
+
</Extensions>
|
|
225
|
+
</Application>
|
|
226
|
+
</Applications>
|
|
227
|
+
<Capabilities>
|
|
228
|
+
<rescap:Capability Name="runFullTrust" />
|
|
229
|
+
</Capabilities>
|
|
230
|
+
</Package>
|
|
231
|
+
`;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function powershell(script: string): string {
|
|
235
|
+
return execFileSync("powershell", ["-NoProfile", "-NonInteractive", "-Command", script],
|
|
236
|
+
{ stdio: ["pipe", "pipe", "pipe"] }).toString();
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function developerModeEnabled(): boolean {
|
|
240
|
+
try {
|
|
241
|
+
const out = powershell(
|
|
242
|
+
"(Get-ItemProperty 'HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\AppModelUnlock' -ErrorAction SilentlyContinue).AllowDevelopmentWithoutDevLicense");
|
|
243
|
+
return out.trim() === "1";
|
|
244
|
+
} catch { return false; }
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** Register (or unregister) rmfmail as a Windows Share target. Mirrors
|
|
248
|
+
* registerMailtoWindows in mailx.ts: returns outcome text instead of
|
|
249
|
+
* printing so the CLI and any future Settings toggle share one path. */
|
|
250
|
+
export function registerShareWindows(unregister: boolean): { ok: boolean; message: string } {
|
|
251
|
+
const lines: string[] = [];
|
|
252
|
+
|
|
253
|
+
if (unregister) {
|
|
254
|
+
try {
|
|
255
|
+
powershell(`Get-AppxPackage '${IDENTITY_NAME}' | Remove-AppxPackage`);
|
|
256
|
+
lines.push("rmfmail removed from the Windows Share menu.");
|
|
257
|
+
} catch (e: any) {
|
|
258
|
+
return { ok: false, message: `Remove-AppxPackage failed: ${e?.stderr?.toString?.() || e.message}` };
|
|
259
|
+
}
|
|
260
|
+
return { ok: true, message: lines.join("\n") };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const pkgExe = path.join(import.meta.dirname, "rmfshare.exe");
|
|
264
|
+
if (!fs.existsSync(pkgExe)) {
|
|
265
|
+
return { ok: false, message: `rmfshare.exe not found at ${pkgExe} — package was built without it. Re-run 'npm run build' on the build machine.` };
|
|
266
|
+
}
|
|
267
|
+
if (!developerModeEnabled()) {
|
|
268
|
+
return {
|
|
269
|
+
ok: false,
|
|
270
|
+
message: "Registering an unsigned Share target needs Windows Developer Mode:\n" +
|
|
271
|
+
" Settings → System → For developers → Developer Mode → On\n" +
|
|
272
|
+
"then re-run `rmfmail -register-share`.",
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Stage under %LOCALAPPDATA%\rmfmail\share-target — NOT inside
|
|
277
|
+
// node_modules: the registered package points at absolute paths, and an
|
|
278
|
+
// npm upgrade replacing node_modules would strand the registration.
|
|
279
|
+
const stageDir = path.join(process.env.LOCALAPPDATA || rmfmailDir(), "rmfmail", "share-target");
|
|
280
|
+
const assetsDir = path.join(stageDir, "Assets");
|
|
281
|
+
fs.mkdirSync(assetsDir, { recursive: true });
|
|
282
|
+
fs.copyFileSync(pkgExe, path.join(stageDir, "rmfshare.exe"));
|
|
283
|
+
|
|
284
|
+
// Manifest logos scaled from the app icon at register time (System.
|
|
285
|
+
// Drawing via PowerShell — registration is already a Windows-only,
|
|
286
|
+
// PowerShell-driven path).
|
|
287
|
+
const iconPng = path.join(import.meta.dirname, "..", "client", "icon.png");
|
|
288
|
+
try {
|
|
289
|
+
powershell(`
|
|
290
|
+
Add-Type -AssemblyName System.Drawing
|
|
291
|
+
$src = [System.Drawing.Image]::FromFile('${iconPng.replace(/'/g, "''")}')
|
|
292
|
+
foreach ($s in 44,150) {
|
|
293
|
+
$bmp = New-Object System.Drawing.Bitmap $s, $s
|
|
294
|
+
$g = [System.Drawing.Graphics]::FromImage($bmp)
|
|
295
|
+
$g.InterpolationMode = 'HighQualityBicubic'
|
|
296
|
+
$g.DrawImage($src, 0, 0, $s, $s)
|
|
297
|
+
$bmp.Save('${assetsDir.replace(/'/g, "''")}\\logo' + $s + '.png', [System.Drawing.Imaging.ImageFormat]::Png)
|
|
298
|
+
$g.Dispose(); $bmp.Dispose()
|
|
299
|
+
}
|
|
300
|
+
$src.Dispose()`);
|
|
301
|
+
} catch (e: any) {
|
|
302
|
+
return { ok: false, message: `Icon scaling failed: ${e?.stderr?.toString?.() || e.message}` };
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Manifest version tracks the package version so re-registering after an
|
|
306
|
+
// upgrade redeploys cleanly (4th part must be 0 for AppX).
|
|
307
|
+
let pkgVersion = "1.0.0";
|
|
308
|
+
try {
|
|
309
|
+
pkgVersion = JSON.parse(fs.readFileSync(path.join(import.meta.dirname, "..", "package.json"), "utf-8")).version || pkgVersion;
|
|
310
|
+
} catch { /* fall back */ }
|
|
311
|
+
const appxVersion = pkgVersion.split(".").slice(0, 3).map(p => String(parseInt(p, 10) || 0)).join(".") + ".0";
|
|
312
|
+
fs.writeFileSync(path.join(stageDir, "AppxManifest.xml"), buildAppxManifest(appxVersion));
|
|
313
|
+
|
|
314
|
+
try {
|
|
315
|
+
// Remove any prior registration first — Add-AppxPackage -Register
|
|
316
|
+
// rejects same-version re-registration, and downgrades after a
|
|
317
|
+
// rollback would fail outright.
|
|
318
|
+
powershell(`Get-AppxPackage '${IDENTITY_NAME}' | Remove-AppxPackage`);
|
|
319
|
+
} catch { /* not registered — fine */ }
|
|
320
|
+
try {
|
|
321
|
+
powershell(`Add-AppxPackage -Register '${path.join(stageDir, "AppxManifest.xml").replace(/'/g, "''")}'`);
|
|
322
|
+
} catch (e: any) {
|
|
323
|
+
return { ok: false, message: `Add-AppxPackage failed: ${e?.stderr?.toString?.() || e.message}` };
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
lines.push("rmfmail registered as a Windows Share target.");
|
|
327
|
+
lines.push("It now appears in the Share sheet (Explorer right-click → Share, browser Share, etc.).");
|
|
328
|
+
lines.push(`Staged at: ${stageDir}`);
|
|
329
|
+
lines.push("Note: re-run -register-share after major upgrades so the staged rmfshare.exe stays current.");
|
|
330
|
+
return { ok: true, message: lines.join("\n") };
|
|
331
|
+
}
|
package/client/app.bundle.js
CHANGED
|
@@ -32,6 +32,7 @@ __export(api_client_exports, {
|
|
|
32
32
|
connectEvents: () => connectEvents,
|
|
33
33
|
connectWebSocket: () => connectWebSocket,
|
|
34
34
|
consumePendingMailto: () => consumePendingMailto,
|
|
35
|
+
consumePendingShare: () => consumePendingShare,
|
|
35
36
|
consumePopoutComposeInit: () => consumePopoutComposeInit,
|
|
36
37
|
copyMessages: () => copyMessages,
|
|
37
38
|
createCalendarEvent: () => createCalendarEvent,
|
|
@@ -618,6 +619,9 @@ function showReminderPopup(opts) {
|
|
|
618
619
|
function consumePendingMailto() {
|
|
619
620
|
return ipc().consumePendingMailto?.() ?? Promise.resolve(null);
|
|
620
621
|
}
|
|
622
|
+
function consumePendingShare() {
|
|
623
|
+
return ipc().consumePendingShare?.() ?? Promise.resolve(null);
|
|
624
|
+
}
|
|
621
625
|
function aiTransform(req) {
|
|
622
626
|
return ipc().aiTransform?.(req) ?? Promise.resolve({ text: "", reason: "AI not available in this host" });
|
|
623
627
|
}
|
|
@@ -10395,6 +10399,16 @@ onWsEvent((event) => {
|
|
|
10395
10399
|
}).catch((e) => console.error("openComposeFromMailto failed:", e?.message || e));
|
|
10396
10400
|
break;
|
|
10397
10401
|
}
|
|
10402
|
+
case "openShare": {
|
|
10403
|
+
openComposeFromShare({
|
|
10404
|
+
title: event.title || "",
|
|
10405
|
+
text: event.text || "",
|
|
10406
|
+
url: event.url || "",
|
|
10407
|
+
attachments: Array.isArray(event.attachments) ? event.attachments : [],
|
|
10408
|
+
skipped: Array.isArray(event.skipped) ? event.skipped : []
|
|
10409
|
+
}).catch((e) => console.error("openComposeFromShare failed:", e?.message || e));
|
|
10410
|
+
break;
|
|
10411
|
+
}
|
|
10398
10412
|
case "debugEval": {
|
|
10399
10413
|
const { id, code } = event;
|
|
10400
10414
|
if (!id || typeof code !== "string") break;
|
|
@@ -10465,6 +10479,43 @@ async function openComposeFromMailto(m) {
|
|
|
10465
10479
|
} catch {
|
|
10466
10480
|
}
|
|
10467
10481
|
}
|
|
10482
|
+
async function openComposeFromShare(s) {
|
|
10483
|
+
const accountsP = getAccounts();
|
|
10484
|
+
let subject = s.title;
|
|
10485
|
+
if (!subject && s.attachments.length) {
|
|
10486
|
+
subject = s.attachments[0].filename + (s.attachments.length > 1 ? ` (+${s.attachments.length - 1} more)` : "");
|
|
10487
|
+
}
|
|
10488
|
+
const frame = showComposeOverlay(subject ? `Compose: ${subject}` : "Compose");
|
|
10489
|
+
const accounts = await accountsP;
|
|
10490
|
+
const escape = (str) => str.replace(/[&<>]/g, (c) => ({ "&": "&", "<": "<", ">": ">" })[c]);
|
|
10491
|
+
const paras = [];
|
|
10492
|
+
if (s.text && s.text !== s.url) {
|
|
10493
|
+
paras.push("<p>" + escape(s.text).replace(/\r?\n/g, "</p><p>") + "</p>");
|
|
10494
|
+
}
|
|
10495
|
+
if (s.url) {
|
|
10496
|
+
const u = escape(s.url);
|
|
10497
|
+
paras.push(`<p><a href="${u}">${u}</a></p>`);
|
|
10498
|
+
}
|
|
10499
|
+
for (const sk of s.skipped) {
|
|
10500
|
+
paras.push(`<p>[Attachment "${escape(sk.name)}" was not added \u2014 ${escape(sk.reason)}]</p>`);
|
|
10501
|
+
}
|
|
10502
|
+
const init = {
|
|
10503
|
+
mode: "new",
|
|
10504
|
+
accountId: accounts[0]?.id || "",
|
|
10505
|
+
to: [],
|
|
10506
|
+
cc: [],
|
|
10507
|
+
bcc: [],
|
|
10508
|
+
subject,
|
|
10509
|
+
bodyHtml: paras.join(""),
|
|
10510
|
+
attachments: s.attachments,
|
|
10511
|
+
accounts: accounts.map((a) => ({ id: a.id, name: a.name, email: a.email, signature: a.signature, sig: a.sig }))
|
|
10512
|
+
};
|
|
10513
|
+
sessionStorage.setItem("composeInit", JSON.stringify(init));
|
|
10514
|
+
try {
|
|
10515
|
+
frame?.contentWindow?.postMessage({ type: "compose-init-ready" }, "*");
|
|
10516
|
+
} catch {
|
|
10517
|
+
}
|
|
10518
|
+
}
|
|
10468
10519
|
window.addEventListener("keydown", (e) => {
|
|
10469
10520
|
if (!e.ctrlKey || e.altKey || e.metaKey) return;
|
|
10470
10521
|
if (e.key !== "r" && e.key !== "R") return;
|
|
@@ -11106,6 +11157,17 @@ function applyThreadFilter() {
|
|
|
11106
11157
|
console.error("pending mailto pickup failed:", e?.message || e);
|
|
11107
11158
|
}
|
|
11108
11159
|
})();
|
|
11160
|
+
(async () => {
|
|
11161
|
+
try {
|
|
11162
|
+
const { consumePendingShare: consumePendingShare2 } = await Promise.resolve().then(() => (init_api_client(), api_client_exports));
|
|
11163
|
+
const data = await consumePendingShare2();
|
|
11164
|
+
if (data && (data.attachments.length || data.text || data.url || data.title || data.skipped.length)) {
|
|
11165
|
+
await openComposeFromShare(data);
|
|
11166
|
+
}
|
|
11167
|
+
} catch (e) {
|
|
11168
|
+
console.error("pending share pickup failed:", e?.message || e);
|
|
11169
|
+
}
|
|
11170
|
+
})();
|
|
11109
11171
|
optTwoLine?.addEventListener("change", () => {
|
|
11110
11172
|
const list = document.getElementById("message-list");
|
|
11111
11173
|
if (optTwoLine.checked) {
|