@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.
@@ -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"
@@ -1,4 +0,0 @@
1
- @echo off
2
- call "C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Auxiliary\Build\vcvars64.bat" >nul 2>&1
3
- cd /d "%~dp0"
4
- cargo build %*
package/launcher/build.rs DELETED
@@ -1,8 +0,0 @@
1
- fn main() {
2
- #[cfg(target_os = "windows")]
3
- {
4
- let mut res = winres::WindowsResource::new();
5
- res.set_icon("mailx.ico");
6
- res.compile().expect("Failed to compile Windows resources");
7
- }
8
- }
Binary file
@@ -1,4 +0,0 @@
1
- @echo off
2
- call "C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Auxiliary\Build\vcvars64.bat" >nul 2>&1
3
- cd /d "%~dp0"
4
- cargo build --release %*
@@ -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
@@ -1,2 +0,0 @@
1
- msger mailx.json
2
- : npm start
package/mailx.db DELETED
Binary file
package/mailx.db-shm DELETED
Binary file
package/mailx.db-wal DELETED
Binary file