@bobfrankston/mailx 1.0.306 → 1.0.313

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/README.md CHANGED
@@ -123,11 +123,16 @@ Gmail OAuth requires a one-time Google Cloud setup:
123
123
 
124
124
  ## Usage
125
125
 
126
+ ### Layout
127
+
128
+ A vertical **icon rail** sits on the far left (Thunderbird / Dovecot style) with one-click access to Compose, Inbox, All Inboxes, and Settings; Calendar / Tasks / Contacts slots are placeholders until those features land. The rail is always visible on wide and medium screens; it folds into the hamburger menu on narrow ones.
129
+
126
130
  ### Reading Mail
127
131
 
128
132
  - Click a **folder** to see its messages
129
133
  - Click a **message** to read it in the preview pane
130
134
  - **All Inboxes** combines inboxes from all accounts (appears with 2+ accounts)
135
+ - A small filled teal dot before the date means the message body is downloaded locally (offline-ready); a hollow circle means not yet prefetched. Prefetch runs as a background task every ~60 s independent of folder sync.
131
136
  - Unread counts show on folders; sub-folder counts bubble up to collapsed parents
132
137
  - Use the **folder search** box to find folders by name
133
138
 
@@ -150,15 +155,18 @@ Gmail OAuth requires a one-time Google Cloud setup:
150
155
 
151
156
  ### Managing Messages
152
157
 
153
- - **Delete** or **Ctrl+D** -- Delete selected messages (moves to Trash)
158
+ - **Delete** or **Ctrl+D** -- Delete selected messages (moves to Trash; second delete in Trash is a hard delete + EXPUNGE)
154
159
  - **Ctrl+Z** -- Undo the last **delete or move** (whichever came last, 60s window)
155
160
  - **Ctrl+A** -- Select all messages in the list
156
161
  - **Drag and drop** -- Move messages to a folder by dragging them
162
+ - **Right-click a message → Move to folder…** -- Searchable folder picker; useful on narrow layouts where the folder tree is hidden
157
163
  - Click the **star** column to flag/unflag a message
158
- - **Unsubscribe** button appears when the message has a List-Unsubscribe header (one-click)
164
+ - **Unsubscribe** button appears when the message has a List-Unsubscribe header. One-click (RFC 8058) when the sender advertises `List-Unsubscribe-Post: List-Unsubscribe=One-Click`; otherwise opens the URL or a pre-filled compose for `mailto:` lists.
159
165
  - **Right-click on a From/To/Cc address** -- Copy name, Copy address, Copy both, Add to contacts, or Reply/Reply All/Forward
166
+ - **Right-click in the message body → Translate** (opt-in) -- Uses the configured AI provider; select text first to translate just the selection. Off by default; enable under **Settings → AI translate**.
160
167
  - **Preview pane zoom** -- Ctrl+wheel, Ctrl+= / Ctrl+- / Ctrl+0, or right-click menu (Zoom in/out/reset, Copy, Select all). Persisted across messages.
161
168
  - **Cross-folder search results** show the folder name for each hit
169
+ - **Empty Trash / Empty Junk** -- Right-click the folder in the tree → Empty (confirmation prompt)
162
170
 
163
171
  ### Searching
164
172
 
@@ -191,6 +199,10 @@ Under **View** in the toolbar:
191
199
  Under **Settings** in the toolbar:
192
200
  - **Editor** -- Choose between Quill (default) and tiptap for compose
193
201
  - **AI autocomplete** -- Enable LLM-powered writing suggestions (Ollama, Claude, or OpenAI)
202
+ - **AI translate** -- Enable the right-click Translate item in the message viewer (off by default; uses the same provider as autocomplete)
203
+ - **AI proofread** -- Enable the proofread path (off by default; provider method available, UI wiring in progress)
204
+
205
+ All AI features are opt-in. Provider + API key live in the autocomplete settings; toggling a feature only controls whether that specific capability calls out to the provider.
194
206
 
195
207
  ### Keyboard Shortcuts
196
208
 
@@ -402,6 +414,7 @@ Use `mailx --verbose` to see logs in the terminal instead.
402
414
  ## Architecture
403
415
 
404
416
  - **IPC-first** -- Default mode uses msger (Rust/WebView2) with bidirectional IPC. No TCP server, no CLOSE_WAIT zombies.
417
+ - **Host abstraction** -- mailx talks to the WebView host through `@bobfrankston/mailx-host`, which picks msger (default) or msgview (Electron, planned for Mac and niche Linux) at runtime. Override with `MAILX_HOST=msger|msgview`.
405
418
  - **HTTP fallback** -- `--server` flag starts Express for browser/remote access.
406
419
  - **Local-first** -- Changes update local DB immediately; background worker syncs to IMAP.
407
420
  - **Offline reading** -- Full message bodies cached as .eml files.
package/bin/mailx.js CHANGED
@@ -21,8 +21,9 @@ import path from "node:path";
21
21
  import os from "node:os";
22
22
  import net from "node:net";
23
23
  import { ports } from "@bobfrankston/miscinfo";
24
- import { showMessageBox, showService, setAppName } from "@bobfrankston/msger";
24
+ import { showMessageBox, showService, setAppName, setAppIcon } from "@bobfrankston/mailx-host";
25
25
  setAppName("mailx");
26
+ setAppIcon(path.resolve(import.meta.dirname, "..", "client", "icon.png"));
26
27
  const PORT = ports.mailx;
27
28
  const args = process.argv.slice(2);
28
29
  // Normalize: accept both -flag and --flag
@@ -146,6 +147,46 @@ for (const arg of args) {
146
147
  }
147
148
  function log(...msg) { if (verbose)
148
149
  console.log("[mailx]", ...msg); }
150
+ /** Detect whether we're running with administrator / root privileges.
151
+ * Windows: `net session` requires admin — succeeds silently when elevated,
152
+ * errors "Access is denied" otherwise. Linux/Mac: check process uid.
153
+ * Returns true only when positively detected as elevated; on ambiguity
154
+ * (e.g. child_process spawn failed for non-privilege reasons), returns
155
+ * false so we don't block users on false positives. */
156
+ function isElevated() {
157
+ try {
158
+ if (process.platform === "win32") {
159
+ const { execSync } = require("node:child_process");
160
+ execSync("net session >nul 2>&1", { stdio: "ignore", windowsHide: true });
161
+ return true;
162
+ }
163
+ if (typeof process.getuid === "function") {
164
+ return process.getuid() === 0;
165
+ }
166
+ }
167
+ catch { /* non-admin → net session fails */ }
168
+ return false;
169
+ }
170
+ /** Put up a blocking warning dialog via showMessageBox. Returns the label
171
+ * the user clicked. The default (Quit) is first so Enter dismisses to
172
+ * safety. Caller decides what to do with "Continue anyway". */
173
+ async function warnElevated() {
174
+ const res = await showMessageBox({
175
+ title: "mailx — elevated run not recommended",
176
+ message: "mailx is running with Administrator privileges.\n\n" +
177
+ "This can corrupt the per-user WebView2 profile at\n" +
178
+ "%LOCALAPPDATA%\\msger\\webview2\\ and create admin-owned files\n" +
179
+ "under ~/.mailx/ that later non-admin runs can't write to\n" +
180
+ "(SQLite db, tokens, config).\n\n" +
181
+ "Quit, relaunch from a normal shell, and only use admin if\n" +
182
+ "you specifically know you need it. To bypass this warning\n" +
183
+ "(for scripted admin use), pass --allow-elevated.",
184
+ buttons: ["Quit", "Continue anyway"],
185
+ size: { width: 540, height: 340 },
186
+ escapeCloses: true,
187
+ });
188
+ return res.button;
189
+ }
149
190
  // Kill any running mailx server
150
191
  if (hasFlag("kill")) {
151
192
  log("Killing mailx processes...");
@@ -731,6 +772,20 @@ async function main() {
731
772
  log(`Platform: ${process.platform} ${process.arch}`);
732
773
  log(`Node: ${process.version}`);
733
774
  log(`Mode: ${setupMode ? "setup" : "auto"}`);
775
+ // Refuse to run elevated unless explicitly opted in. An elevated mailx
776
+ // can poison %LOCALAPPDATA%\msger\webview2\ (see msger/notes.md WebView2
777
+ // profile playbook) and create admin-owned files under ~/.mailx/ that
778
+ // later non-admin runs can't write to. `net session` requires admin on
779
+ // Windows; succeeds → admin, fails → non-admin. Linux/Mac use process
780
+ // uid (0 = root). --allow-elevated bypasses for scripted admin use.
781
+ if (!hasFlag("allow-elevated") && !isDaemon && isElevated()) {
782
+ const button = await warnElevated();
783
+ if (button !== "Continue anyway") {
784
+ log("User chose Quit on elevated-run warning. Exiting.");
785
+ process.exit(0);
786
+ }
787
+ log("User chose Continue anyway on elevated-run warning. Proceeding (will likely poison local state).");
788
+ }
734
789
  // Test connectivity
735
790
  if (testMode) {
736
791
  await runTest();