@bobfrankston/mailx 1.0.1 → 1.0.2
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/package.json +1 -1
- package/launcher/Cargo.lock +0 -3167
- package/launcher/Cargo.toml +0 -13
- package/launcher/build.cmd +0 -4
- package/launcher/build.rs +0 -8
- package/launcher/mailx.ico +0 -0
- package/launcher/release.cmd +0 -4
- package/launcher/src/main.rs +0 -371
- package/mailx.cmd +0 -2
- package/mailx.db +0 -0
- package/mailx.db-shm +0 -0
- package/mailx.db-wal +0 -0
package/launcher/Cargo.toml
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
[package]
|
|
2
|
-
name = "mailx-app"
|
|
3
|
-
version = "0.1.0"
|
|
4
|
-
edition = "2021"
|
|
5
|
-
description = "mailx email client — standalone WebView2 launcher"
|
|
6
|
-
|
|
7
|
-
[dependencies]
|
|
8
|
-
wry = "0.47"
|
|
9
|
-
tao = "0.30"
|
|
10
|
-
image = "0.24"
|
|
11
|
-
|
|
12
|
-
[target.'cfg(windows)'.build-dependencies]
|
|
13
|
-
winres = "0.1"
|
package/launcher/build.cmd
DELETED
package/launcher/build.rs
DELETED
package/launcher/mailx.ico
DELETED
|
Binary file
|
package/launcher/release.cmd
DELETED
package/launcher/src/main.rs
DELETED
|
@@ -1,371 +0,0 @@
|
|
|
1
|
-
/// mailx-app: Standalone WebView2 launcher for mailx email client.
|
|
2
|
-
/// Starts the Node.js server process and opens a WebView2 window.
|
|
3
|
-
///
|
|
4
|
-
/// Usage: mailx-app [-dev] [-prod] [-restart]
|
|
5
|
-
/// -dev (default) Start server with --watch for auto-restart on file changes
|
|
6
|
-
/// -prod Start server without --watch
|
|
7
|
-
/// -restart Kill existing server on port 9333 before starting
|
|
8
|
-
|
|
9
|
-
use std::process::{Command, Child, Stdio};
|
|
10
|
-
use std::time::Duration;
|
|
11
|
-
use std::thread;
|
|
12
|
-
use std::net::TcpStream;
|
|
13
|
-
use std::path::PathBuf;
|
|
14
|
-
use std::env;
|
|
15
|
-
use std::fs;
|
|
16
|
-
|
|
17
|
-
use tao::event::{Event, WindowEvent};
|
|
18
|
-
use tao::event_loop::{ControlFlow, EventLoop};
|
|
19
|
-
use tao::window::{WindowBuilder, Icon};
|
|
20
|
-
use wry::WebViewBuilder;
|
|
21
|
-
|
|
22
|
-
const PORT: u16 = 9333;
|
|
23
|
-
const URL: &str = "http://localhost:9333";
|
|
24
|
-
|
|
25
|
-
#[derive(Debug)]
|
|
26
|
-
struct WindowState {
|
|
27
|
-
x: i32,
|
|
28
|
-
y: i32,
|
|
29
|
-
width: u32,
|
|
30
|
-
height: u32,
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
impl WindowState {
|
|
34
|
-
fn default() -> Self {
|
|
35
|
-
WindowState { x: 100, y: 100, width: 1280, height: 800 }
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
fn load() -> Self {
|
|
39
|
-
let path = Self::path();
|
|
40
|
-
if !path.exists() { return Self::default(); }
|
|
41
|
-
match fs::read_to_string(&path) {
|
|
42
|
-
Ok(s) => Self::parse(&s).unwrap_or_else(Self::default),
|
|
43
|
-
Err(_) => Self::default(),
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
fn save(&self) {
|
|
48
|
-
let path = Self::path();
|
|
49
|
-
if let Some(dir) = path.parent() {
|
|
50
|
-
let _ = fs::create_dir_all(dir);
|
|
51
|
-
}
|
|
52
|
-
let json = format!("{{\"x\":{},\"y\":{},\"width\":{},\"height\":{}}}", self.x, self.y, self.width, self.height);
|
|
53
|
-
let _ = fs::write(&path, json);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
fn path() -> PathBuf {
|
|
57
|
-
let home = env::var("USERPROFILE").or_else(|_| env::var("HOME")).unwrap_or_else(|_| ".".to_string());
|
|
58
|
-
PathBuf::from(home).join(".mailx").join("window.json")
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
fn parse(s: &str) -> Option<Self> {
|
|
62
|
-
// Minimal JSON parser for {x, y, width, height}
|
|
63
|
-
let get = |key: &str| -> Option<i64> {
|
|
64
|
-
let pat = format!("\"{}\":", key);
|
|
65
|
-
let idx = s.find(&pat)? + pat.len();
|
|
66
|
-
let rest = s[idx..].trim_start();
|
|
67
|
-
let end = rest.find(|c: char| !c.is_ascii_digit() && c != '-')?;
|
|
68
|
-
rest[..end].parse().ok()
|
|
69
|
-
};
|
|
70
|
-
Some(WindowState {
|
|
71
|
-
x: get("x")? as i32,
|
|
72
|
-
y: get("y")? as i32,
|
|
73
|
-
width: get("width")? as u32,
|
|
74
|
-
height: get("height")? as u32,
|
|
75
|
-
})
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/// Find the mailx project root (parent of launcher/)
|
|
80
|
-
fn find_project_root() -> PathBuf {
|
|
81
|
-
let exe_dir = env::current_exe()
|
|
82
|
-
.expect("Cannot determine executable path")
|
|
83
|
-
.parent()
|
|
84
|
-
.expect("Cannot determine executable directory")
|
|
85
|
-
.to_path_buf();
|
|
86
|
-
|
|
87
|
-
let mut dir = exe_dir.clone();
|
|
88
|
-
for _ in 0..5 {
|
|
89
|
-
if dir.join("packages").join("mailx-server").join("index.js").exists() {
|
|
90
|
-
return dir;
|
|
91
|
-
}
|
|
92
|
-
if let Some(parent) = dir.parent() {
|
|
93
|
-
dir = parent.to_path_buf();
|
|
94
|
-
} else {
|
|
95
|
-
break;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
let cwd = env::current_dir().unwrap_or(exe_dir);
|
|
100
|
-
if cwd.join("packages").join("mailx-server").join("index.js").exists() {
|
|
101
|
-
return cwd;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
panic!("Cannot find mailx project root (looking for packages/mailx-server/index.js)");
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/// Start the Node.js server process
|
|
108
|
-
fn start_server(project_root: &PathBuf, dev_mode: bool) -> Child {
|
|
109
|
-
let server_script = project_root
|
|
110
|
-
.join("packages")
|
|
111
|
-
.join("mailx-server")
|
|
112
|
-
.join("index.js");
|
|
113
|
-
|
|
114
|
-
let mut cmd = Command::new("node");
|
|
115
|
-
if dev_mode {
|
|
116
|
-
cmd.arg("--watch");
|
|
117
|
-
}
|
|
118
|
-
cmd.arg(&server_script)
|
|
119
|
-
.current_dir(project_root)
|
|
120
|
-
.stdout(Stdio::inherit())
|
|
121
|
-
.stderr(Stdio::inherit())
|
|
122
|
-
.spawn()
|
|
123
|
-
.expect("Failed to start Node.js server. Is Node installed?")
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/// Wait for the server to be ready (accepting TCP connections)
|
|
127
|
-
fn wait_for_server(timeout_secs: u64) -> bool {
|
|
128
|
-
let addr = format!("127.0.0.1:{}", PORT);
|
|
129
|
-
let deadline = std::time::Instant::now() + Duration::from_secs(timeout_secs);
|
|
130
|
-
|
|
131
|
-
while std::time::Instant::now() < deadline {
|
|
132
|
-
if TcpStream::connect(&addr).is_ok() {
|
|
133
|
-
return true;
|
|
134
|
-
}
|
|
135
|
-
thread::sleep(Duration::from_millis(250));
|
|
136
|
-
}
|
|
137
|
-
false
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
fn server_is_running() -> bool {
|
|
141
|
-
TcpStream::connect(format!("127.0.0.1:{}", PORT)).is_ok()
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/// Open a URL in the system default browser
|
|
145
|
-
fn open_in_browser(url: &str) {
|
|
146
|
-
#[cfg(target_os = "windows")]
|
|
147
|
-
{ let _ = Command::new("cmd").args(["/c", "start", "", url]).spawn(); }
|
|
148
|
-
#[cfg(target_os = "linux")]
|
|
149
|
-
{ let _ = Command::new("xdg-open").arg(url).spawn(); }
|
|
150
|
-
#[cfg(target_os = "macos")]
|
|
151
|
-
{ let _ = Command::new("open").arg(url).spawn(); }
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/// Kill any process listening on the given port
|
|
155
|
-
fn kill_port(port: u16) {
|
|
156
|
-
#[cfg(target_os = "windows")]
|
|
157
|
-
{
|
|
158
|
-
// Use killport if available, fallback to netstat+taskkill
|
|
159
|
-
let result = Command::new("killport").arg(port.to_string()).status();
|
|
160
|
-
if result.is_err() || !result.unwrap().success() {
|
|
161
|
-
// Fallback: find PID via netstat
|
|
162
|
-
if let Ok(output) = Command::new("cmd")
|
|
163
|
-
.args(["/c", &format!("netstat -ano | findstr :{}", port)])
|
|
164
|
-
.output()
|
|
165
|
-
{
|
|
166
|
-
let text = String::from_utf8_lossy(&output.stdout);
|
|
167
|
-
for line in text.lines() {
|
|
168
|
-
if line.contains("LISTENING") {
|
|
169
|
-
if let Some(pid) = line.split_whitespace().last() {
|
|
170
|
-
let _ = Command::new("taskkill").args(["/F", "/PID", pid]).status();
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
#[cfg(not(target_os = "windows"))]
|
|
178
|
-
{
|
|
179
|
-
let _ = Command::new("fuser").args(["-k", &format!("{}/tcp", port)]).status();
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
fn lock_path() -> PathBuf {
|
|
184
|
-
let home = env::var("USERPROFILE").or_else(|_| env::var("HOME")).unwrap_or_else(|_| ".".to_string());
|
|
185
|
-
PathBuf::from(home).join(".mailx").join("mailx-app.lock")
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
fn is_app_running() -> bool {
|
|
189
|
-
let path = lock_path();
|
|
190
|
-
if !path.exists() { return false; }
|
|
191
|
-
// Check if the PID in the lock file is still alive
|
|
192
|
-
if let Ok(pid_str) = fs::read_to_string(&path) {
|
|
193
|
-
if let Ok(pid) = pid_str.trim().parse::<u32>() {
|
|
194
|
-
#[cfg(target_os = "windows")]
|
|
195
|
-
{
|
|
196
|
-
let status = Command::new("tasklist")
|
|
197
|
-
.args(["/FI", &format!("PID eq {}", pid), "/NH"])
|
|
198
|
-
.output();
|
|
199
|
-
if let Ok(output) = status {
|
|
200
|
-
let text = String::from_utf8_lossy(&output.stdout);
|
|
201
|
-
return text.contains(&pid.to_string());
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
false
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
fn write_lock() {
|
|
210
|
-
let path = lock_path();
|
|
211
|
-
if let Some(dir) = path.parent() {
|
|
212
|
-
let _ = fs::create_dir_all(dir);
|
|
213
|
-
}
|
|
214
|
-
let _ = fs::write(&path, std::process::id().to_string());
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
fn remove_lock() {
|
|
218
|
-
let _ = fs::remove_file(lock_path());
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
fn main() {
|
|
222
|
-
let args: Vec<String> = env::args().collect();
|
|
223
|
-
let dev_mode = !args.iter().any(|a| a == "-prod");
|
|
224
|
-
let restart = args.iter().any(|a| a == "-restart");
|
|
225
|
-
|
|
226
|
-
// Check if another mailx-app is already running
|
|
227
|
-
if !restart && is_app_running() {
|
|
228
|
-
println!("mailx-app is already running. Use -restart to force.");
|
|
229
|
-
// Focus existing window by opening in browser as fallback
|
|
230
|
-
if server_is_running() {
|
|
231
|
-
open_in_browser(URL);
|
|
232
|
-
}
|
|
233
|
-
std::process::exit(0);
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
let project_root = find_project_root();
|
|
237
|
-
println!("mailx project root: {}", project_root.display());
|
|
238
|
-
println!("Mode: {}", if dev_mode { "dev (--watch)" } else { "prod" });
|
|
239
|
-
|
|
240
|
-
// Kill existing server if -restart
|
|
241
|
-
if restart && server_is_running() {
|
|
242
|
-
println!("Killing existing server on port {}...", PORT);
|
|
243
|
-
kill_port(PORT);
|
|
244
|
-
remove_lock();
|
|
245
|
-
thread::sleep(Duration::from_secs(2));
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
// Write lock file
|
|
249
|
-
write_lock();
|
|
250
|
-
|
|
251
|
-
// Start the Node server if not already running
|
|
252
|
-
let server_process: Option<Child>;
|
|
253
|
-
if server_is_running() {
|
|
254
|
-
println!("Server already running on port {}", PORT);
|
|
255
|
-
server_process = None;
|
|
256
|
-
} else {
|
|
257
|
-
let proc = start_server(&project_root, dev_mode);
|
|
258
|
-
println!("Starting mailx server (port {})...", PORT);
|
|
259
|
-
server_process = Some(proc);
|
|
260
|
-
|
|
261
|
-
if !wait_for_server(30) {
|
|
262
|
-
eprintln!("Server failed to start within 30 seconds");
|
|
263
|
-
std::process::exit(1);
|
|
264
|
-
}
|
|
265
|
-
println!("Server ready");
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
// Restore window state
|
|
269
|
-
let saved = WindowState::load();
|
|
270
|
-
println!("Window: {}x{} at ({}, {})", saved.width, saved.height, saved.x, saved.y);
|
|
271
|
-
|
|
272
|
-
let event_loop = EventLoop::new();
|
|
273
|
-
|
|
274
|
-
// Load window icon
|
|
275
|
-
let icon_path = project_root.join("launcher").join("mailx.ico");
|
|
276
|
-
let window_icon = if icon_path.exists() {
|
|
277
|
-
match image::open(&icon_path) {
|
|
278
|
-
Ok(img) => {
|
|
279
|
-
let rgba = img.to_rgba8();
|
|
280
|
-
let (w, h) = rgba.dimensions();
|
|
281
|
-
Icon::from_rgba(rgba.into_raw(), w, h).ok()
|
|
282
|
-
}
|
|
283
|
-
Err(e) => { eprintln!("Icon load error: {}", e); None }
|
|
284
|
-
}
|
|
285
|
-
} else {
|
|
286
|
-
None
|
|
287
|
-
};
|
|
288
|
-
|
|
289
|
-
let mut builder = WindowBuilder::new()
|
|
290
|
-
.with_title("mailx")
|
|
291
|
-
.with_inner_size(tao::dpi::LogicalSize::new(saved.width as f64, saved.height as f64))
|
|
292
|
-
.with_position(tao::dpi::LogicalPosition::new(saved.x as f64, saved.y as f64))
|
|
293
|
-
.with_resizable(true);
|
|
294
|
-
|
|
295
|
-
if let Some(icon) = window_icon {
|
|
296
|
-
builder = builder.with_window_icon(Some(icon));
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
let window = builder
|
|
300
|
-
.build(&event_loop)
|
|
301
|
-
.expect("Failed to create window");
|
|
302
|
-
|
|
303
|
-
let _webview = WebViewBuilder::new()
|
|
304
|
-
.with_url(URL)
|
|
305
|
-
.with_devtools(dev_mode)
|
|
306
|
-
.with_initialization_script(r#"
|
|
307
|
-
window.mailxapi = {
|
|
308
|
-
isApp: true,
|
|
309
|
-
platform: 'webview2',
|
|
310
|
-
ensureServer: function() {
|
|
311
|
-
// Server is managed by the launcher — always running when app is open
|
|
312
|
-
return Promise.resolve(true);
|
|
313
|
-
},
|
|
314
|
-
openExternal: function(url) {
|
|
315
|
-
// Navigation handler intercepts, but this is explicit
|
|
316
|
-
window.open(url, '_blank');
|
|
317
|
-
}
|
|
318
|
-
};
|
|
319
|
-
"#)
|
|
320
|
-
.with_navigation_handler(|uri| {
|
|
321
|
-
// Allow localhost navigation (our app), block everything else → open in browser
|
|
322
|
-
if uri.starts_with("http://localhost") || uri.starts_with("https://localhost") || uri.starts_with("about:") {
|
|
323
|
-
true // allow
|
|
324
|
-
} else {
|
|
325
|
-
open_in_browser(&uri);
|
|
326
|
-
false // don't navigate in WebView
|
|
327
|
-
}
|
|
328
|
-
})
|
|
329
|
-
.with_new_window_req_handler(|uri| {
|
|
330
|
-
if uri.starts_with("http://localhost") || uri.starts_with("https://localhost") {
|
|
331
|
-
true // allow localhost popups (compose, etc.) in WebView
|
|
332
|
-
} else {
|
|
333
|
-
open_in_browser(&uri);
|
|
334
|
-
false // external links → system browser
|
|
335
|
-
}
|
|
336
|
-
})
|
|
337
|
-
.build(&window)
|
|
338
|
-
.expect("Failed to create WebView");
|
|
339
|
-
|
|
340
|
-
// Run the event loop
|
|
341
|
-
event_loop.run(move |event, _, control_flow| {
|
|
342
|
-
*control_flow = ControlFlow::Wait;
|
|
343
|
-
|
|
344
|
-
// Keep server_process alive so it doesn't get dropped
|
|
345
|
-
let _ = &server_process;
|
|
346
|
-
|
|
347
|
-
match event {
|
|
348
|
-
Event::WindowEvent {
|
|
349
|
-
event: WindowEvent::CloseRequested,
|
|
350
|
-
..
|
|
351
|
-
} => {
|
|
352
|
-
// Save window position and size (convert to logical pixels for DPI independence)
|
|
353
|
-
let scale = window.scale_factor();
|
|
354
|
-
let pos = window.outer_position().unwrap_or(tao::dpi::PhysicalPosition::new(100, 100));
|
|
355
|
-
let size = window.inner_size();
|
|
356
|
-
let state = WindowState {
|
|
357
|
-
x: (pos.x as f64 / scale) as i32,
|
|
358
|
-
y: (pos.y as f64 / scale) as i32,
|
|
359
|
-
width: (size.width as f64 / scale) as u32,
|
|
360
|
-
height: (size.height as f64 / scale) as u32,
|
|
361
|
-
};
|
|
362
|
-
state.save();
|
|
363
|
-
remove_lock();
|
|
364
|
-
println!("Window saved: {}x{} at ({}, {})", state.width, state.height, state.x, state.y);
|
|
365
|
-
println!("Server still running on port {}", PORT);
|
|
366
|
-
*control_flow = ControlFlow::Exit;
|
|
367
|
-
}
|
|
368
|
-
_ => {}
|
|
369
|
-
}
|
|
370
|
-
});
|
|
371
|
-
}
|
package/mailx.cmd
DELETED
package/mailx.db
DELETED
|
Binary file
|
package/mailx.db-shm
DELETED
|
Binary file
|
package/mailx.db-wal
DELETED
|
Binary file
|