@bobfrankston/rmfmail 1.0.570 → 1.0.590

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 (54) hide show
  1. package/TODO.md +18 -3
  2. package/bin/build-rmfmailto-exe.js +83 -0
  3. package/bin/mailto.js +130 -0
  4. package/bin/mailto.js.map +1 -0
  5. package/bin/mailto.ts +122 -0
  6. package/bin/mailx.js +199 -3
  7. package/bin/mailx.js.map +1 -1
  8. package/bin/mailx.ts +188 -3
  9. package/bin/rmfmailto-src/Cargo.lock +92 -0
  10. package/bin/rmfmailto-src/Cargo.toml +20 -0
  11. package/bin/rmfmailto-src/build.rs +27 -0
  12. package/bin/rmfmailto-src/src/main.rs +143 -0
  13. package/bin/rmfmailto.exe +0 -0
  14. package/client/app.js +157 -0
  15. package/client/app.js.map +1 -1
  16. package/client/app.ts +157 -0
  17. package/client/components/alarms.js +5 -2
  18. package/client/components/alarms.js.map +1 -1
  19. package/client/components/alarms.ts +4 -2
  20. package/client/components/folder-tree.js +43 -2
  21. package/client/components/folder-tree.js.map +1 -1
  22. package/client/components/folder-tree.ts +40 -2
  23. package/client/lib/api-client.js +7 -0
  24. package/client/lib/api-client.js.map +1 -1
  25. package/client/lib/api-client.ts +11 -0
  26. package/client/lib/mailxapi.js +3 -0
  27. package/client/styles/components.css +40 -0
  28. package/package.json +14 -14
  29. package/packages/mailx-imap/index.d.ts +46 -14
  30. package/packages/mailx-imap/index.d.ts.map +1 -1
  31. package/packages/mailx-imap/index.js +527 -187
  32. package/packages/mailx-imap/index.js.map +1 -1
  33. package/packages/mailx-imap/index.ts +518 -172
  34. package/packages/mailx-imap/package-lock.json +2 -2
  35. package/packages/mailx-imap/package.json +1 -1
  36. package/packages/mailx-server/index.js +4 -4
  37. package/packages/mailx-server/index.js.map +1 -1
  38. package/packages/mailx-server/index.ts +4 -4
  39. package/packages/mailx-service/index.d.ts +14 -0
  40. package/packages/mailx-service/index.d.ts.map +1 -1
  41. package/packages/mailx-service/index.js +61 -8
  42. package/packages/mailx-service/index.js.map +1 -1
  43. package/packages/mailx-service/index.ts +59 -8
  44. package/packages/mailx-service/jsonrpc.js +2 -0
  45. package/packages/mailx-service/jsonrpc.js.map +1 -1
  46. package/packages/mailx-service/jsonrpc.ts +2 -0
  47. package/packages/mailx-service/reconciler.d.ts.map +1 -1
  48. package/packages/mailx-service/reconciler.js +16 -0
  49. package/packages/mailx-service/reconciler.js.map +1 -1
  50. package/packages/mailx-service/reconciler.ts +15 -0
  51. package/packages/mailx-store/db.js +2 -2
  52. package/packages/mailx-store/db.js.map +1 -1
  53. package/packages/mailx-store/db.ts +2 -2
  54. package/packages/mailx-store/package.json +1 -1
@@ -0,0 +1,92 @@
1
+ # This file is automatically @generated by Cargo.
2
+ # It is not intended for manual editing.
3
+ version = 4
4
+
5
+ [[package]]
6
+ name = "proc-macro2"
7
+ version = "1.0.106"
8
+ source = "registry+https://github.com/rust-lang/crates.io-index"
9
+ checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
10
+ dependencies = [
11
+ "unicode-ident",
12
+ ]
13
+
14
+ [[package]]
15
+ name = "quote"
16
+ version = "1.0.45"
17
+ source = "registry+https://github.com/rust-lang/crates.io-index"
18
+ checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
19
+ dependencies = [
20
+ "proc-macro2",
21
+ ]
22
+
23
+ [[package]]
24
+ name = "rmfmailto"
25
+ version = "1.0.0"
26
+ dependencies = [
27
+ "winres",
28
+ ]
29
+
30
+ [[package]]
31
+ name = "serde"
32
+ version = "1.0.228"
33
+ source = "registry+https://github.com/rust-lang/crates.io-index"
34
+ checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
35
+ dependencies = [
36
+ "serde_core",
37
+ ]
38
+
39
+ [[package]]
40
+ name = "serde_core"
41
+ version = "1.0.228"
42
+ source = "registry+https://github.com/rust-lang/crates.io-index"
43
+ checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
44
+ dependencies = [
45
+ "serde_derive",
46
+ ]
47
+
48
+ [[package]]
49
+ name = "serde_derive"
50
+ version = "1.0.228"
51
+ source = "registry+https://github.com/rust-lang/crates.io-index"
52
+ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
53
+ dependencies = [
54
+ "proc-macro2",
55
+ "quote",
56
+ "syn",
57
+ ]
58
+
59
+ [[package]]
60
+ name = "syn"
61
+ version = "2.0.117"
62
+ source = "registry+https://github.com/rust-lang/crates.io-index"
63
+ checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
64
+ dependencies = [
65
+ "proc-macro2",
66
+ "quote",
67
+ "unicode-ident",
68
+ ]
69
+
70
+ [[package]]
71
+ name = "toml"
72
+ version = "0.5.11"
73
+ source = "registry+https://github.com/rust-lang/crates.io-index"
74
+ checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234"
75
+ dependencies = [
76
+ "serde",
77
+ ]
78
+
79
+ [[package]]
80
+ name = "unicode-ident"
81
+ version = "1.0.24"
82
+ source = "registry+https://github.com/rust-lang/crates.io-index"
83
+ checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
84
+
85
+ [[package]]
86
+ name = "winres"
87
+ version = "0.1.12"
88
+ source = "registry+https://github.com/rust-lang/crates.io-index"
89
+ checksum = "b68db261ef59e9e52806f688020631e987592bd83619edccda9c47d42cde4f6c"
90
+ dependencies = [
91
+ "toml",
92
+ ]
@@ -0,0 +1,20 @@
1
+ [package]
2
+ name = "rmfmailto"
3
+ version = "1.0.0"
4
+ edition = "2021"
5
+ description = "rmfmail mailto: handler launcher"
6
+ authors = ["Bob Frankston"]
7
+
8
+ [[bin]]
9
+ name = "rmfmailto"
10
+ path = "src/main.rs"
11
+
12
+ [build-dependencies]
13
+ winres = "0.1"
14
+
15
+ [profile.release]
16
+ opt-level = "z"
17
+ lto = true
18
+ codegen-units = 1
19
+ panic = "abort"
20
+ strip = true
@@ -0,0 +1,27 @@
1
+ // Embed Win32 VERSIONINFO so the Win11 "Select an app" picker shows the
2
+ // app as "rmfmail" instead of falling through to the host EXE's PE
3
+ // FileDescription. Keys here populate the picker's display.
4
+
5
+ #[cfg(windows)]
6
+ fn main() {
7
+ let mut res = winres::WindowsResource::new();
8
+ res.set("ProductName", "rmfmail");
9
+ res.set("FileDescription", "rmfmail");
10
+ res.set("CompanyName", "Bob Frankston");
11
+ res.set("LegalCopyright", "MIT");
12
+ res.set("OriginalFilename", "rmfmailto.exe");
13
+ res.set("InternalName", "rmfmailto");
14
+ // ICO can be picked up at registration time via DefaultIcon — embedding
15
+ // here gets it on the picker tile too. The path resolves relative to
16
+ // the crate root at build time.
17
+ let ico = std::path::Path::new("../../client/icon.ico");
18
+ if ico.exists() {
19
+ res.set_icon(ico.to_str().unwrap());
20
+ }
21
+ if let Err(e) = res.compile() {
22
+ eprintln!("winres compile failed: {e}");
23
+ }
24
+ }
25
+
26
+ #[cfg(not(windows))]
27
+ fn main() {}
@@ -0,0 +1,143 @@
1
+ // rmfmailto — Win32 launcher for the OS-level mailto: handler (P115).
2
+ //
3
+ // Exists for ONE reason: Win11's "Select an app to open this mailto" picker
4
+ // reads the EXE's PE FileDescription as the display name. If we register
5
+ // `node.exe ... mailx.js --mailto "%1"` the picker shows "Node.js
6
+ // JavaScript Runtime" — confusing. With this binary registered instead,
7
+ // the picker shows "rmfmail" (set in build.rs's VERSIONINFO).
8
+ //
9
+ // The runtime job is trivial: receive argv (the mailto URL), find node + the
10
+ // installed mailx.js, spawn `node mailx.js --mailto <url>`, exit. Detached
11
+ // so we don't hold a handle on a console window.
12
+ //
13
+ // Lookup order for node + mailx.js:
14
+ // 1. NODE_EXE / RMFMAIL_JS env vars (test override / unusual installs).
15
+ // 2. %APPDATA%\npm\node_modules\@bobfrankston\rmfmail\bin\mailx.js
16
+ // (standard `npm install -g @bobfrankston/rmfmail` location).
17
+ // 3. PATH lookup for `node.exe`.
18
+ // Failing any of those, surface a single MessageBox so the user gets a
19
+ // readable error instead of a silent dead click.
20
+
21
+ #![windows_subsystem = "windows"]
22
+
23
+ use std::env;
24
+ use std::path::PathBuf;
25
+ use std::process::Command;
26
+
27
+ #[cfg(windows)]
28
+ fn show_error(msg: &str) {
29
+ use std::ffi::OsStr;
30
+ use std::iter::once;
31
+ use std::os::windows::ffi::OsStrExt;
32
+ #[link(name = "user32")]
33
+ extern "system" {
34
+ fn MessageBoxW(hwnd: usize, text: *const u16, caption: *const u16, ty: u32) -> i32;
35
+ }
36
+ let to_wide = |s: &str| -> Vec<u16> {
37
+ OsStr::new(s).encode_wide().chain(once(0)).collect()
38
+ };
39
+ let text = to_wide(msg);
40
+ let caption = to_wide("rmfmail mailto handler");
41
+ unsafe { MessageBoxW(0, text.as_ptr(), caption.as_ptr(), 0x10); }
42
+ }
43
+
44
+ #[cfg(not(windows))]
45
+ fn show_error(msg: &str) { eprintln!("{msg}"); }
46
+
47
+ fn find_node() -> Option<PathBuf> {
48
+ if let Ok(p) = env::var("NODE_EXE") {
49
+ let pb = PathBuf::from(p);
50
+ if pb.is_file() { return Some(pb); }
51
+ }
52
+ // Look up node on PATH. Windows's where.exe finds it but isn't always
53
+ // available; instead walk PATH manually.
54
+ let path = env::var_os("PATH")?;
55
+ for dir in env::split_paths(&path) {
56
+ for name in &["node.exe", "node"] {
57
+ let candidate = dir.join(name);
58
+ if candidate.is_file() { return Some(candidate); }
59
+ }
60
+ }
61
+ None
62
+ }
63
+
64
+ fn find_mailx_js() -> Option<PathBuf> {
65
+ if let Ok(p) = env::var("RMFMAIL_JS") {
66
+ let pb = PathBuf::from(p);
67
+ if pb.is_file() { return Some(pb); }
68
+ }
69
+ // Standard global-install location.
70
+ if let Ok(appdata) = env::var("APPDATA") {
71
+ let pb = PathBuf::from(appdata)
72
+ .join("npm").join("node_modules")
73
+ .join("@bobfrankston").join("rmfmail")
74
+ .join("bin").join("mailx.js");
75
+ if pb.is_file() { return Some(pb); }
76
+ }
77
+ // Fallback: relative to this binary, walking up to find `bin/mailx.js`.
78
+ // Lets a developer-mode build (rmfmailto.exe sitting next to mailx.js
79
+ // in `app/bin/`) work without env-var fiddling.
80
+ if let Ok(exe) = env::current_exe() {
81
+ let mut p = exe.clone();
82
+ for _ in 0..5 {
83
+ if let Some(parent) = p.parent() {
84
+ let candidate = parent.join("mailx.js");
85
+ if candidate.is_file() { return Some(candidate); }
86
+ let candidate = parent.join("bin").join("mailx.js");
87
+ if candidate.is_file() { return Some(candidate); }
88
+ p = parent.to_path_buf();
89
+ } else {
90
+ break;
91
+ }
92
+ }
93
+ }
94
+ None
95
+ }
96
+
97
+ fn main() {
98
+ // argv[0] is the exe path itself; argv[1] should be the mailto URL.
99
+ let mailto_url: Option<String> = env::args().nth(1);
100
+ let mailto_url = match mailto_url {
101
+ Some(u) if !u.is_empty() => u,
102
+ _ => {
103
+ show_error("rmfmailto invoked without a mailto URL.\n\nUsage: rmfmailto.exe <mailto:...>");
104
+ std::process::exit(1);
105
+ }
106
+ };
107
+
108
+ let node = match find_node() {
109
+ Some(p) => p,
110
+ None => {
111
+ show_error("Node.js was not found on PATH.\n\nrmfmail requires Node.js to be installed. See https://nodejs.org");
112
+ std::process::exit(2);
113
+ }
114
+ };
115
+ let mailx_js = match find_mailx_js() {
116
+ Some(p) => p,
117
+ None => {
118
+ show_error("rmfmail's mailx.js was not located.\n\nExpected at:\n %APPDATA%\\npm\\node_modules\\@bobfrankston\\rmfmail\\bin\\mailx.js\n\nSet RMFMAIL_JS to override.");
119
+ std::process::exit(3);
120
+ }
121
+ };
122
+
123
+ // Spawn detached so the picker doesn't get a "is this still running?"
124
+ // console window. CREATE_NO_WINDOW (0x08000000) + DETACHED_PROCESS
125
+ // (0x00000008) on Windows; nothing special needed elsewhere.
126
+ let mut cmd = Command::new(&node);
127
+ cmd.arg(&mailx_js).arg("--mailto").arg(&mailto_url);
128
+ #[cfg(windows)]
129
+ {
130
+ use std::os::windows::process::CommandExt;
131
+ cmd.creation_flags(0x08000000 | 0x00000008);
132
+ }
133
+ match cmd.spawn() {
134
+ Ok(_) => {}
135
+ Err(e) => {
136
+ show_error(&format!(
137
+ "Failed to launch rmfmail.\n\nNode: {}\nScript: {}\nError: {e}",
138
+ node.display(), mailx_js.display(),
139
+ ));
140
+ std::process::exit(4);
141
+ }
142
+ }
143
+ }
Binary file
package/client/app.js CHANGED
@@ -2294,8 +2294,51 @@ onWsEvent((event) => {
2294
2294
  }
2295
2295
  break;
2296
2296
  }
2297
+ case "openMailto": {
2298
+ // P115: OS-level mailto: handler. CLI dropped a pending file; the
2299
+ // daemon picked it up and forwarded the parsed fields. Open compose
2300
+ // pre-populated. Treat as a fresh New compose, not a reply — the
2301
+ // browser doesn't know about a parent message.
2302
+ openComposeFromMailto({
2303
+ to: Array.isArray(event.to) ? event.to : [],
2304
+ cc: Array.isArray(event.cc) ? event.cc : [],
2305
+ bcc: Array.isArray(event.bcc) ? event.bcc : [],
2306
+ subject: event.subject || "",
2307
+ body: event.body || "",
2308
+ inReplyTo: event.inReplyTo || "",
2309
+ }).catch((e) => console.error("openComposeFromMailto failed:", e?.message || e));
2310
+ break;
2311
+ }
2297
2312
  }
2298
2313
  });
2314
+ /** Open a compose window pre-filled from a mailto: URL. Same plumbing as
2315
+ * openCompose("new") but the init shape comes from the parsed URL instead
2316
+ * of the currently-selected message. The body arrives as plain text from
2317
+ * the URL — we wrap it in a single <p> so Quill renders it as paragraphs
2318
+ * rather than a single line, escaping `<` so embedded angle brackets in a
2319
+ * signature/template don't get interpreted as tags. */
2320
+ async function openComposeFromMailto(m) {
2321
+ const accounts = await getAccounts();
2322
+ const accountId = accounts[0]?.id || "";
2323
+ const escape = (s) => s.replace(/[&<>]/g, c => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;" }[c]));
2324
+ const bodyHtml = m.body
2325
+ ? "<p>" + escape(m.body).replace(/\r?\n/g, "</p><p>") + "</p>"
2326
+ : "";
2327
+ const init = {
2328
+ mode: "new",
2329
+ accountId,
2330
+ to: m.to.map(addr => ({ name: "", address: addr })),
2331
+ cc: m.cc.map(addr => ({ name: "", address: addr })),
2332
+ bcc: m.bcc.map(addr => ({ name: "", address: addr })),
2333
+ subject: m.subject,
2334
+ bodyHtml,
2335
+ inReplyTo: m.inReplyTo,
2336
+ references: m.inReplyTo ? [m.inReplyTo] : [],
2337
+ accounts: accounts.map((a) => ({ id: a.id, name: a.name, email: a.email, signature: a.signature })),
2338
+ };
2339
+ sessionStorage.setItem("composeInit", JSON.stringify(init));
2340
+ showComposeOverlay(m.subject ? `Compose: ${m.subject}` : "Compose");
2341
+ }
2299
2342
  // ── Keyboard shortcuts ──
2300
2343
  // Capture-phase pre-handler: intercept WebView accelerator keys that would
2301
2344
  // otherwise trigger a browser action (Ctrl+R / Ctrl+Shift+R = reload). Call
@@ -2797,6 +2840,120 @@ function applyThreadFilter() {
2797
2840
  console.error("alarm poller init failed:", e?.message || e);
2798
2841
  }
2799
2842
  })();
2843
+ // Pull-to-refresh: touch-drag down at the top of the message list fires a
2844
+ // sync, same as F5 / btn-sync. Standard Android / Thunderbird / mobile
2845
+ // pattern. The indicator slides down with the drag and rotates the chevron
2846
+ // once the user crosses the trigger threshold; releasing past the threshold
2847
+ // fires triggerSync(). Below threshold = snap back without action.
2848
+ //
2849
+ // Touch-only by design — desktop has the F5 / sync button. Pointer events
2850
+ // would also catch mouse-drag, which is more annoying than useful (people
2851
+ // drag the scrollbar). The `e.touches.length === 1` check skips two-finger
2852
+ // gestures (pinch-zoom, two-finger scroll).
2853
+ (() => {
2854
+ const PTR_THRESHOLD_PX = 80; // pull this far to arm the trigger
2855
+ const PTR_MAX_PX = 120; // cap the visual translate
2856
+ const mlBody = document.getElementById("ml-body");
2857
+ if (!mlBody)
2858
+ return;
2859
+ const indicator = document.createElement("div");
2860
+ indicator.className = "ptr-indicator";
2861
+ indicator.innerHTML = `<span class="ptr-chev"></span><span class="ptr-label">Pull to refresh</span>`;
2862
+ mlBody.parentElement?.insertBefore(indicator, mlBody);
2863
+ const labelEl = indicator.querySelector(".ptr-label");
2864
+ let startY = 0;
2865
+ let pullY = 0;
2866
+ let active = false;
2867
+ let refreshing = false;
2868
+ const setPull = (px, armed) => {
2869
+ const clamped = Math.min(PTR_MAX_PX, Math.max(0, px));
2870
+ indicator.classList.toggle("armed", armed);
2871
+ indicator.style.transform = `translateY(calc(-100% + ${clamped}px))`;
2872
+ };
2873
+ const reset = () => {
2874
+ indicator.classList.remove("dragging", "armed");
2875
+ indicator.style.transform = "";
2876
+ };
2877
+ mlBody.addEventListener("touchstart", (e) => {
2878
+ if (refreshing)
2879
+ return;
2880
+ if (e.touches.length !== 1)
2881
+ return;
2882
+ if (mlBody.scrollTop > 0)
2883
+ return;
2884
+ startY = e.touches[0].clientY;
2885
+ pullY = 0;
2886
+ active = true;
2887
+ indicator.classList.add("dragging");
2888
+ labelEl.textContent = "Pull to refresh";
2889
+ }, { passive: true });
2890
+ mlBody.addEventListener("touchmove", (e) => {
2891
+ if (!active || refreshing)
2892
+ return;
2893
+ const dy = e.touches[0].clientY - startY;
2894
+ if (dy <= 0) {
2895
+ // User scrolled up past the start — abandon, let normal scroll
2896
+ // take over without reverting (the touchend handler will reset).
2897
+ pullY = 0;
2898
+ setPull(0, false);
2899
+ return;
2900
+ }
2901
+ // Resist past 0 — pull "stretches" rather than scrolls. preventDefault
2902
+ // is necessary to keep the browser from interpreting the drag as
2903
+ // overscroll / browser-level pull-to-refresh.
2904
+ if (e.cancelable)
2905
+ e.preventDefault();
2906
+ pullY = dy * 0.5; // 0.5 = rubber-band damping
2907
+ const armed = pullY >= PTR_THRESHOLD_PX;
2908
+ labelEl.textContent = armed ? "Release to refresh" : "Pull to refresh";
2909
+ setPull(pullY, armed);
2910
+ }, { passive: false });
2911
+ const finish = async () => {
2912
+ if (!active)
2913
+ return;
2914
+ active = false;
2915
+ if (pullY < PTR_THRESHOLD_PX) {
2916
+ reset();
2917
+ return;
2918
+ }
2919
+ // Armed — show spinner, fire sync, clear when complete.
2920
+ refreshing = true;
2921
+ indicator.classList.remove("armed", "dragging");
2922
+ indicator.classList.add("refreshing");
2923
+ indicator.style.transform = `translateY(calc(-100% + 60px))`;
2924
+ labelEl.textContent = "Refreshing...";
2925
+ try {
2926
+ const { triggerSync } = await import("./lib/api-client.js");
2927
+ await triggerSync();
2928
+ }
2929
+ catch (e) {
2930
+ console.error("pull-to-refresh failed:", e?.message || e);
2931
+ }
2932
+ refreshing = false;
2933
+ indicator.classList.remove("refreshing");
2934
+ reset();
2935
+ };
2936
+ mlBody.addEventListener("touchend", () => { finish(); }, { passive: true });
2937
+ mlBody.addEventListener("touchcancel", () => { active = false; reset(); }, { passive: true });
2938
+ })();
2939
+ // P115: pick up a pending mailto: drop file dropped by the CLI invocation
2940
+ // that spawned us (or by an earlier --mailto run that the user hasn't yet
2941
+ // acknowledged). The IPC call deletes the file as part of reading it, so a
2942
+ // race with the daemon's fs.watch is harmless — whichever fires first wins
2943
+ // and the other gets null. Subsequent live clicks during this session
2944
+ // arrive via the `openMailto` event handled in onEvent above.
2945
+ (async () => {
2946
+ try {
2947
+ const { consumePendingMailto } = await import("./lib/api-client.js");
2948
+ const data = await consumePendingMailto();
2949
+ if (data && (data.to.length || data.subject || data.body)) {
2950
+ await openComposeFromMailto(data);
2951
+ }
2952
+ }
2953
+ catch (e) {
2954
+ console.error("pending mailto pickup failed:", e?.message || e);
2955
+ }
2956
+ })();
2800
2957
  // Two-line toggle
2801
2958
  optTwoLine?.addEventListener("change", () => {
2802
2959
  const list = document.getElementById("message-list");