@bobfrankston/mailx 1.0.223 → 1.0.225

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
@@ -137,18 +137,28 @@ Gmail OAuth requires a one-time Google Cloud setup:
137
137
  - **Ctrl+R** -- Reply
138
138
  - **Ctrl+Shift+R** -- Reply All
139
139
  - **Ctrl+F** -- Forward
140
- - From dropdown lets you pick which account to send from
140
+ - From dropdown lets you pick which account to send from; reply auto-detects which identity to reply from based on which of your addresses the mail was sent to
141
141
  - Contact autocomplete searches Google Contacts as you type in To/Cc/Bcc
142
- - Drafts auto-save every 5 seconds to your Drafts folder
142
+ - **Cc / Bcc** are hidden by default — click the toggle buttons next to To to show them
143
+ - **Attach** opens a file picker; attachments show as chips with remove buttons
144
+ - Drafts auto-save 1.5s after you stop typing, plus a 5s safety-net interval, plus on window close
145
+ - Compose window close asks Save / Discard / Cancel if there's content
146
+ - Address validation (`local@domain.tld`) runs on To/Cc/Bcc/From before sending — invalid addresses are refused
147
+ - **Editor shortcuts**: Ctrl+K insert link, Ctrl+Shift+K remove link, Ctrl+Shift+X strikethrough, Ctrl+Shift+7/8 ordered/bullet list, Ctrl+]/[ indent/outdent, Ctrl+Shift+C color, Ctrl+\ clear formatting. Native spell-check via WebView2 (right-click to add to dictionary).
148
+ - **Link editor modal**: Ctrl+K opens a two-field dialog (text + URL) with Remove-link button; hovering any link in the editor shows a floating URL preview
149
+ - **Paste URL** auto-links: paste a bare URL over a selection and it wraps it, or paste into empty space to insert as a link
143
150
 
144
151
  ### Managing Messages
145
152
 
146
153
  - **Delete** or **Ctrl+D** -- Delete selected messages (moves to Trash)
147
- - **Ctrl+Z** -- Undo last delete
154
+ - **Ctrl+Z** -- Undo the last **delete or move** (whichever came last, 60s window)
148
155
  - **Ctrl+A** -- Select all messages in the list
149
156
  - **Drag and drop** -- Move messages to a folder by dragging them
150
157
  - Click the **star** column to flag/unflag a message
151
- - **Unsubscribe** button appears when the message has a List-Unsubscribe header
158
+ - **Unsubscribe** button appears when the message has a List-Unsubscribe header (one-click)
159
+ - **Right-click on a From/To/Cc address** -- Copy name, Copy address, Copy both, Add to contacts, or Reply/Reply All/Forward
160
+ - **Preview pane zoom** -- Ctrl+wheel, Ctrl+= / Ctrl+- / Ctrl+0, or right-click menu (Zoom in/out/reset, Copy, Select all). Persisted across messages.
161
+ - **Cross-folder search results** show the folder name for each hit
152
162
 
153
163
  ### Searching
154
164
 
@@ -191,11 +201,35 @@ Under **Settings** in the toolbar:
191
201
  | Ctrl+Shift+R | Reply All |
192
202
  | Ctrl+F | Forward |
193
203
  | Delete / Ctrl+D | Delete |
194
- | Ctrl+Z | Undo delete |
204
+ | Ctrl+Z | Undo last delete or move |
195
205
  | Ctrl+A | Select all |
196
206
  | F5 | Sync all folders |
197
207
  | Escape | Clear search / close menus |
198
208
 
209
+ **In the compose editor:**
210
+
211
+ | Key | Action |
212
+ |-----|--------|
213
+ | Ctrl+K | Insert / edit link (opens dialog with text + URL fields) |
214
+ | Ctrl+Shift+K | Remove link |
215
+ | Ctrl+B / Ctrl+I / Ctrl+U | Bold / Italic / Underline |
216
+ | Ctrl+Shift+X | Strikethrough |
217
+ | Ctrl+Shift+7 / 8 | Ordered / Bullet list |
218
+ | Ctrl+] / Ctrl+[ | Indent / Outdent |
219
+ | Ctrl+Shift+C | Set text color |
220
+ | Ctrl+\ | Clear formatting |
221
+ | Ctrl+Enter | Send |
222
+ | Escape | Close (prompts Save / Discard / Cancel) |
223
+
224
+ **In the preview pane:**
225
+
226
+ | Key | Action |
227
+ |-----|--------|
228
+ | Ctrl+wheel | Zoom in/out |
229
+ | Ctrl+= / Ctrl+- | Zoom in / out |
230
+ | Ctrl+0 | Reset zoom |
231
+ | Delete | Delete message (also works with focus in preview) |
232
+
199
233
  ## Command Line
200
234
 
201
235
  ```
@@ -324,6 +324,29 @@ body {
324
324
  }
325
325
  .compose-att-chip button:hover { color: oklch(0.65 0.2 25); }
326
326
 
327
+ /* Cc/Bcc toggle buttons in the To row */
328
+ .compose-recipient-toggle {
329
+ display: inline-flex;
330
+ gap: 4px;
331
+ margin-left: var(--gap-xs);
332
+ }
333
+ .compose-toggle-btn {
334
+ background: transparent;
335
+ border: 1px solid var(--color-border);
336
+ border-radius: var(--radius-sm);
337
+ color: var(--color-text-muted);
338
+ padding: 1px 8px;
339
+ font-size: var(--font-size-sm);
340
+ cursor: pointer;
341
+ font-family: inherit;
342
+ }
343
+ .compose-toggle-btn:hover { background: var(--color-bg-hover); color: var(--color-text); }
344
+ .compose-toggle-btn.active {
345
+ background: var(--color-accent);
346
+ color: #fff;
347
+ border-color: transparent;
348
+ }
349
+
327
350
  /* Link editor modal (Ctrl+K / toolbar link button) */
328
351
  .mailx-modal-backdrop {
329
352
  position: fixed;
@@ -20,12 +20,16 @@
20
20
  <div class="compose-field">
21
21
  <label for="compose-to">To</label>
22
22
  <input type="text" id="compose-to" autocomplete="off">
23
+ <span class="compose-recipient-toggle">
24
+ <button type="button" class="compose-toggle-btn" id="btn-toggle-cc" title="Show/hide Cc">Cc</button>
25
+ <button type="button" class="compose-toggle-btn" id="btn-toggle-bcc" title="Show/hide Bcc">Bcc</button>
26
+ </span>
23
27
  </div>
24
- <div class="compose-field">
28
+ <div class="compose-field" id="compose-cc-row" hidden>
25
29
  <label for="compose-cc">Cc</label>
26
30
  <input type="text" id="compose-cc" autocomplete="off">
27
31
  </div>
28
- <div class="compose-field">
32
+ <div class="compose-field" id="compose-bcc-row" hidden>
29
33
  <label for="compose-bcc">Bcc</label>
30
34
  <input type="text" id="compose-bcc" autocomplete="off">
31
35
  </div>
@@ -275,6 +275,15 @@ function applyInit(init) {
275
275
  toInput.value = formatAddrs(init.to);
276
276
  ccInput.value = formatAddrs(init.cc);
277
277
  subjectInput.value = init.subject;
278
+ // Auto-expand Cc row if the init already has Cc content (reply-all, draft-with-cc)
279
+ if (ccInput.value.trim()) {
280
+ const ccRowEl = document.getElementById("compose-cc-row");
281
+ const ccBtn = document.getElementById("btn-toggle-cc");
282
+ if (ccRowEl)
283
+ ccRowEl.hidden = false;
284
+ if (ccBtn)
285
+ ccBtn.classList.add("active");
286
+ }
278
287
  if (init.bodyHtml) {
279
288
  editor.setHtml(init.bodyHtml);
280
289
  editor.setCursor(0);
@@ -497,6 +506,29 @@ async function handleCloseRequest() {
497
506
  document.getElementById("btn-discard")?.addEventListener("click", () => {
498
507
  handleCloseRequest();
499
508
  });
509
+ // ── Cc / Bcc toggle ──
510
+ const ccRow = document.getElementById("compose-cc-row");
511
+ const bccRow = document.getElementById("compose-bcc-row");
512
+ const toggleCcBtn = document.getElementById("btn-toggle-cc");
513
+ const toggleBccBtn = document.getElementById("btn-toggle-bcc");
514
+ function setCcVisible(visible) {
515
+ ccRow.hidden = !visible;
516
+ toggleCcBtn.classList.toggle("active", visible);
517
+ if (visible)
518
+ ccInput.focus();
519
+ else
520
+ ccInput.value = "";
521
+ }
522
+ function setBccVisible(visible) {
523
+ bccRow.hidden = !visible;
524
+ toggleBccBtn.classList.toggle("active", visible);
525
+ if (visible)
526
+ bccInput.focus();
527
+ else
528
+ bccInput.value = "";
529
+ }
530
+ toggleCcBtn?.addEventListener("click", () => setCcVisible(ccRow.hidden));
531
+ toggleBccBtn?.addEventListener("click", () => setBccVisible(bccRow.hidden));
500
532
  // ── Attachments ──
501
533
  const fileInput = document.getElementById("compose-file");
502
534
  const attEl = document.getElementById("compose-attachments");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/mailx",
3
- "version": "1.0.223",
3
+ "version": "1.0.225",
4
4
  "description": "Local-first email client with IMAP sync and standalone native app",
5
5
  "type": "module",
6
6
  "main": "bin/mailx.js",
@@ -20,11 +20,11 @@
20
20
  "postinstall": "node bin/postinstall.js"
21
21
  },
22
22
  "dependencies": {
23
- "@bobfrankston/iflow-direct": "^0.1.9",
23
+ "@bobfrankston/iflow-direct": "^0.1.10",
24
24
  "@bobfrankston/iflow-node": "^0.1.2",
25
25
  "@bobfrankston/miscinfo": "^1.0.8",
26
26
  "@bobfrankston/oauthsupport": "^1.0.22",
27
- "@bobfrankston/msger": "^0.1.285",
27
+ "@bobfrankston/msger": "^0.1.287",
28
28
  "@capacitor/android": "^8.3.0",
29
29
  "@capacitor/cli": "^8.3.0",
30
30
  "@capacitor/core": "^8.3.0",
@@ -74,11 +74,11 @@
74
74
  },
75
75
  ".transformedSnapshot": {
76
76
  "dependencies": {
77
- "@bobfrankston/iflow-direct": "^0.1.9",
77
+ "@bobfrankston/iflow-direct": "^0.1.10",
78
78
  "@bobfrankston/iflow-node": "^0.1.2",
79
79
  "@bobfrankston/miscinfo": "^1.0.8",
80
80
  "@bobfrankston/oauthsupport": "^1.0.22",
81
- "@bobfrankston/msger": "^0.1.285",
81
+ "@bobfrankston/msger": "^0.1.287",
82
82
  "@capacitor/android": "^8.3.0",
83
83
  "@capacitor/cli": "^8.3.0",
84
84
  "@capacitor/core": "^8.3.0",
@@ -371,24 +371,35 @@ export class ImapManager extends EventEmitter {
371
371
  }
372
372
  const tokenDir = path.join(getConfigDir(), "tokens", account.imap.user.replace(/[@.]/g, "_"));
373
373
  tokenProvider = async () => {
374
- const result = await authenticateOAuth(credPath, {
374
+ // Wrap authenticateOAuth with a 30s wall-clock timeout. Without this,
375
+ // a hung OAuth server could block the entire sync thread indefinitely.
376
+ const TOKEN_FETCH_TIMEOUT_MS = 30000;
377
+ const authPromise = authenticateOAuth(credPath, {
375
378
  scope: "https://mail.google.com/ https://www.googleapis.com/auth/contacts.readonly https://www.googleapis.com/auth/calendar",
376
379
  tokenDirectory: tokenDir,
377
380
  credentialsKey: "installed",
378
381
  loginHint: account.imap.user,
379
382
  });
383
+ const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error(`OAuth token fetch timeout (${TOKEN_FETCH_TIMEOUT_MS / 1000}s)`)), TOKEN_FETCH_TIMEOUT_MS));
384
+ const result = await Promise.race([authPromise, timeoutPromise]);
380
385
  return result?.access_token || "";
381
386
  };
382
387
  }
388
+ // Non-Gmail accounts (typically Dovecot / generic IMAP) get a smaller
389
+ // fetch chunk size (10 vs 25) and longer inactivity timeout (300s) so
390
+ // multi-body FETCH batches don't trip the connection-dead detector on
391
+ // slow servers. Gmail stays at the defaults since it's fast and has
392
+ // its own rate limits to respect.
393
+ const isGmail = account.imap.host?.includes("gmail") || account.email.endsWith("@gmail.com");
383
394
  const config = createAutoImapConfig({
384
395
  server: account.imap.host,
385
396
  port: account.imap.port,
386
397
  username: account.imap.user,
387
398
  password: account.imap.password,
388
399
  tokenProvider,
389
- // Slow Dovecot servers (e.g. iecc.com) can stall >60s during multi-body FETCH.
390
- // Raise the inactivity timeout so the connection isn't dropped mid-stream.
391
- inactivityTimeout: 180000,
400
+ inactivityTimeout: isGmail ? 60000 : 300000,
401
+ fetchChunkSize: isGmail ? 25 : 10,
402
+ fetchChunkSizeMax: isGmail ? 500 : 100,
392
403
  });
393
404
  this.configs.set(account.id, config);
394
405
  // Register account in DB