@bobfrankston/rmfmail 1.0.567 → 1.0.589
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 +18 -3
- package/bin/build-rmfmailto-exe.js +83 -0
- package/bin/mailto.js +130 -0
- package/bin/mailto.js.map +1 -0
- package/bin/mailto.ts +122 -0
- package/bin/mailx.js +199 -3
- package/bin/mailx.js.map +1 -1
- package/bin/mailx.ts +188 -3
- package/bin/rmfmailto-src/Cargo.lock +92 -0
- package/bin/rmfmailto-src/Cargo.toml +20 -0
- package/bin/rmfmailto-src/build.rs +27 -0
- package/bin/rmfmailto-src/src/main.rs +143 -0
- package/bin/rmfmailto.exe +0 -0
- package/client/app.js +170 -5
- package/client/app.js.map +1 -1
- package/client/app.ts +169 -4
- package/client/components/alarms.js +5 -2
- package/client/components/alarms.js.map +1 -1
- package/client/components/alarms.ts +4 -2
- package/client/components/folder-tree.js +54 -2
- package/client/components/folder-tree.js.map +1 -1
- package/client/components/folder-tree.ts +48 -2
- package/client/components/message-list.js +6 -3
- package/client/components/message-list.js.map +1 -1
- package/client/components/message-list.ts +6 -3
- package/client/components/message-viewer.js +58 -5
- package/client/components/message-viewer.js.map +1 -1
- package/client/components/message-viewer.ts +58 -5
- package/client/index.html +1 -0
- package/client/lib/api-client.js +9 -2
- package/client/lib/api-client.js.map +1 -1
- package/client/lib/api-client.ts +13 -2
- package/client/lib/mailxapi.js +5 -2
- package/client/styles/components.css +53 -0
- package/package.json +14 -14
- package/packages/mailx-imap/index.d.ts +46 -14
- package/packages/mailx-imap/index.d.ts.map +1 -1
- package/packages/mailx-imap/index.js +527 -187
- package/packages/mailx-imap/index.js.map +1 -1
- package/packages/mailx-imap/index.ts +518 -172
- package/packages/mailx-imap/package-lock.json +2 -2
- package/packages/mailx-imap/package.json +1 -1
- package/packages/mailx-server/index.js +4 -4
- package/packages/mailx-server/index.js.map +1 -1
- package/packages/mailx-server/index.ts +4 -4
- package/packages/mailx-service/index.d.ts +15 -1
- package/packages/mailx-service/index.d.ts.map +1 -1
- package/packages/mailx-service/index.js +66 -11
- package/packages/mailx-service/index.js.map +1 -1
- package/packages/mailx-service/index.ts +64 -11
- package/packages/mailx-service/jsonrpc.js +3 -1
- package/packages/mailx-service/jsonrpc.js.map +1 -1
- package/packages/mailx-service/jsonrpc.ts +3 -1
- package/packages/mailx-service/local-store.d.ts +1 -1
- package/packages/mailx-service/local-store.d.ts.map +1 -1
- package/packages/mailx-service/local-store.js +2 -2
- package/packages/mailx-service/local-store.js.map +1 -1
- package/packages/mailx-service/local-store.ts +2 -2
- package/packages/mailx-service/reconciler.d.ts.map +1 -1
- package/packages/mailx-service/reconciler.js +16 -0
- package/packages/mailx-service/reconciler.js.map +1 -1
- package/packages/mailx-service/reconciler.ts +15 -0
- package/packages/mailx-store/db.d.ts +1 -1
- package/packages/mailx-store/db.d.ts.map +1 -1
- package/packages/mailx-store/db.js +15 -4
- package/packages/mailx-store/db.js.map +1 -1
- package/packages/mailx-store/db.ts +15 -4
- 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
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { initFolderTree, refreshFolderTree, updateFolderCounts, setFolderSynced, getFolderSynced, setOutboxTotal } from "./components/folder-tree.js";
|
|
6
6
|
import { initMessageList, loadMessages, loadUnifiedInbox, loadSearchResults, reloadCurrentFolder, clearSearchMode, getSelectedMessages, markBodiesCached, getCurrentFocused, releaseFocus, removeMessagesAndReconcile, setRowFlagged, scrollFocusedIntoView, refreshPriorityIndex } from "./components/message-list.js";
|
|
7
|
-
import {
|
|
7
|
+
import { getCurrentMessage, initViewer, popOutCurrentMessage, toggleFullscreenPreview, showPreviewBodyMenu } from "./components/message-viewer.js";
|
|
8
8
|
import { connectWebSocket, onWsEvent, triggerSync, syncAccount, reauthenticate, getAccounts, getFolders, deleteMessages, undeleteMessage, restartServer, getSyncPending, getVersion, getSettings, saveSettings, getAutocompleteSettings, saveAutocompleteSettings, repairAccounts, updateFlags, markAsSpamMessages, logClientEvent, sendMessage as apiSendMessage } from "./lib/api-client.js";
|
|
9
9
|
import * as messageState from "./lib/message-state.js";
|
|
10
10
|
// ── New message badge (favicon + title) ──
|
|
@@ -324,9 +324,14 @@ initFolderTree(folderTree, (accountId, folderId, folderName, specialUse) => {
|
|
|
324
324
|
setTitle(`${APP_NAME} - All Inboxes`);
|
|
325
325
|
setNarrowFolderTitle("All Inboxes");
|
|
326
326
|
});
|
|
327
|
-
initMessageList((
|
|
328
|
-
|
|
329
|
-
//
|
|
327
|
+
initMessageList((_accountId, _uid, _folderId) => {
|
|
328
|
+
// The list row's setFocus() already drove the viewer with its own
|
|
329
|
+
// envelope (header + preview paint synchronously, body fetches in
|
|
330
|
+
// background). Calling showMessage here a SECOND time without the
|
|
331
|
+
// envelope wiped the good render and forced a bare "Loading…"
|
|
332
|
+
// placeholder until the body IPC returned — making every click feel
|
|
333
|
+
// like a network round-trip. This callback now only handles the
|
|
334
|
+
// narrow-screen layout switch.
|
|
330
335
|
if (window.innerWidth <= 768) {
|
|
331
336
|
document.getElementById("message-viewer")?.classList.add("narrow-active");
|
|
332
337
|
document.getElementById("message-list")?.classList.add("narrow-hidden");
|
|
@@ -1450,8 +1455,10 @@ function doSearch(immediate = false) {
|
|
|
1450
1455
|
// "server" which spans all folders on all accounts. Local scope dropdown
|
|
1451
1456
|
// is unchanged (all/current) for the local-only case.
|
|
1452
1457
|
const serverCheck = document.getElementById("search-server-too");
|
|
1458
|
+
const trashCheck = document.getElementById("search-include-trash");
|
|
1453
1459
|
const localScope = searchScope?.value || "all";
|
|
1454
1460
|
const effectiveScope = serverCheck?.checked ? "server" : localScope;
|
|
1461
|
+
const includeTrash = !!trashCheck?.checked;
|
|
1455
1462
|
// "This folder" scope: instant client-side filter on debounce, server search on Enter
|
|
1456
1463
|
if (effectiveScope === "current" && !immediate) {
|
|
1457
1464
|
// Client-side filter of visible rows
|
|
@@ -1465,7 +1472,7 @@ function doSearch(immediate = false) {
|
|
|
1465
1472
|
}
|
|
1466
1473
|
return;
|
|
1467
1474
|
}
|
|
1468
|
-
loadSearchResults(query, effectiveScope, currentAccountId, currentFolderId);
|
|
1475
|
+
loadSearchResults(query, effectiveScope, currentAccountId, currentFolderId, includeTrash);
|
|
1469
1476
|
setTitle(`${APP_NAME} - Search: ${query}`);
|
|
1470
1477
|
}
|
|
1471
1478
|
// Track current folder for scoped search
|
|
@@ -1523,6 +1530,7 @@ function rerunActiveSearch() {
|
|
|
1523
1530
|
}
|
|
1524
1531
|
searchScope?.addEventListener("change", rerunActiveSearch);
|
|
1525
1532
|
document.getElementById("search-server-too")?.addEventListener("change", rerunActiveSearch);
|
|
1533
|
+
document.getElementById("search-include-trash")?.addEventListener("change", rerunActiveSearch);
|
|
1526
1534
|
// Message state handles move/delete — no manual event listener needed
|
|
1527
1535
|
// ── Folder filter ──
|
|
1528
1536
|
const ftFilterInput = document.getElementById("ft-filter-input");
|
|
@@ -2286,8 +2294,51 @@ onWsEvent((event) => {
|
|
|
2286
2294
|
}
|
|
2287
2295
|
break;
|
|
2288
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
|
+
}
|
|
2289
2312
|
}
|
|
2290
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 => ({ "&": "&", "<": "<", ">": ">" }[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
|
+
}
|
|
2291
2342
|
// ── Keyboard shortcuts ──
|
|
2292
2343
|
// Capture-phase pre-handler: intercept WebView accelerator keys that would
|
|
2293
2344
|
// otherwise trigger a browser action (Ctrl+R / Ctrl+Shift+R = reload). Call
|
|
@@ -2789,6 +2840,120 @@ function applyThreadFilter() {
|
|
|
2789
2840
|
console.error("alarm poller init failed:", e?.message || e);
|
|
2790
2841
|
}
|
|
2791
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
|
+
})();
|
|
2792
2957
|
// Two-line toggle
|
|
2793
2958
|
optTwoLine?.addEventListener("change", () => {
|
|
2794
2959
|
const list = document.getElementById("message-list");
|