@bobfrankston/mailx 1.0.61 → 1.0.64

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/client/app.js CHANGED
@@ -562,15 +562,14 @@ onWsEvent((event) => {
562
562
  showAlert(event.message, "ws-error");
563
563
  break;
564
564
  case "accountError": {
565
- const acctEl = document.getElementById("status-accounts");
566
- if (acctEl) {
567
- acctEl.innerHTML = "";
568
- acctEl.style.color = "oklch(0.65 0.2 25)";
569
- const text = document.createElement("span");
570
- text.textContent = `${event.accountId}: ${event.hint} `;
571
- acctEl.appendChild(text);
572
- // Add re-auth button for auth errors
573
- if (event.error.includes("token") || event.error.includes("auth") || event.error.includes("Command failed")) {
565
+ // Show in alert banner with re-auth button
566
+ const msg = `${event.accountId}: ${event.hint}`;
567
+ showAlert(msg, `acct-${event.accountId}`);
568
+ // Add re-auth button to the banner
569
+ const bannerText = document.getElementById("alert-text");
570
+ if (bannerText && bannerText.textContent === msg) {
571
+ const existing = bannerText.parentElement?.querySelector(".status-action");
572
+ if (!existing) {
574
573
  const btn = document.createElement("button");
575
574
  btn.textContent = "Re-authenticate";
576
575
  btn.className = "status-action";
@@ -581,8 +580,12 @@ onWsEvent((event) => {
581
580
  const res = await fetch(`/api/reauth/${event.accountId}`, { method: "POST" });
582
581
  const data = await res.json();
583
582
  if (data.ok) {
584
- acctEl.textContent = `${event.accountId}: reconnected`;
585
- acctEl.style.color = "";
583
+ hideAlert();
584
+ const acctEl = document.getElementById("status-accounts");
585
+ if (acctEl) {
586
+ acctEl.textContent = `${event.accountId}: reconnected`;
587
+ acctEl.style.color = "";
588
+ }
586
589
  }
587
590
  else {
588
591
  btn.textContent = "Re-authenticate";
@@ -594,9 +597,15 @@ onWsEvent((event) => {
594
597
  btn.disabled = false;
595
598
  }
596
599
  });
597
- acctEl.appendChild(btn);
600
+ bannerText.parentElement?.insertBefore(btn, document.getElementById("alert-dismiss"));
598
601
  }
599
602
  }
603
+ // Also show in status bar
604
+ const acctEl = document.getElementById("status-accounts");
605
+ if (acctEl) {
606
+ acctEl.textContent = `${event.accountId}: ${event.hint}`;
607
+ acctEl.style.color = "oklch(0.65 0.2 25)";
608
+ }
600
609
  break;
601
610
  }
602
611
  }
@@ -608,6 +617,11 @@ document.addEventListener("keydown", (e) => {
608
617
  e.preventDefault();
609
618
  openCompose("new");
610
619
  }
620
+ // Ctrl+F = Forward
621
+ if (e.ctrlKey && e.key === "f") {
622
+ e.preventDefault();
623
+ openCompose("forward");
624
+ }
611
625
  // Ctrl+R = Reply
612
626
  if (e.ctrlKey && e.key === "r" && !e.shiftKey) {
613
627
  e.preventDefault();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/mailx",
3
- "version": "1.0.61",
3
+ "version": "1.0.64",
4
4
  "description": "Local-first email client with IMAP sync and standalone native app",
5
5
  "type": "module",
6
6
  "main": "bin/mailx.js",
@@ -22,7 +22,7 @@
22
22
  "dependencies": {
23
23
  "@bobfrankston/iflow": "^1.0.30",
24
24
  "@bobfrankston/miscinfo": "^1.0.6",
25
- "@bobfrankston/oauthsupport": "^1.0.13",
25
+ "@bobfrankston/oauthsupport": "^1.0.14",
26
26
  "@bobfrankston/rust-builder": "^0.1.2",
27
27
  "mailparser": "^3.7.2",
28
28
  "quill": "^2.0.3",
@@ -30,7 +30,7 @@ export declare class ImapManager extends EventEmitter {
30
30
  constructor(db: MailxDB);
31
31
  /** Get OAuth access token for an account (for SMTP auth) */
32
32
  getOAuthToken(accountId: string): Promise<string | null>;
33
- /** Force re-authentication for an OAuth account — deletes cached token, triggers browser consent */
33
+ /** Force re-authentication for an OAuth account — deletes cached IMAP token, triggers browser consent */
34
34
  reauthenticate(accountId: string): Promise<boolean>;
35
35
  /** Delete a message directly on the IMAP server (for stuck outbox messages not in local DB) */
36
36
  deleteOnServer(accountId: string, folderPath: string, uid: number): Promise<void>;
@@ -72,21 +72,19 @@ export class ImapManager extends EventEmitter {
72
72
  return null;
73
73
  return config.tokenProvider();
74
74
  }
75
- /** Force re-authentication for an OAuth account — deletes cached token, triggers browser consent */
75
+ /** Force re-authentication for an OAuth account — deletes cached IMAP token, triggers browser consent */
76
76
  async reauthenticate(accountId) {
77
77
  const settings = loadSettings();
78
78
  const account = settings.accounts.find(a => a.id === accountId);
79
79
  if (!account)
80
80
  return false;
81
- // Delete cached tokens
81
+ // Delete only the IMAP token (not contacts — separate scope, separate consent)
82
82
  const accountDir = account.imap.user.replace(/[@.]/g, "_");
83
83
  const tokenDir = path.join(getConfigDir(), "tokens", accountDir);
84
- for (const f of ["token.json", "contacts-token.json"]) {
85
- const p = path.join(tokenDir, f);
86
- if (fs.existsSync(p)) {
87
- fs.unlinkSync(p);
88
- console.log(` [reauth] Deleted ${p}`);
89
- }
84
+ const tokenPath = path.join(tokenDir, "token.json");
85
+ if (fs.existsSync(tokenPath)) {
86
+ fs.unlinkSync(tokenPath);
87
+ console.log(` [reauth] Deleted ${tokenPath}`);
90
88
  }
91
89
  // Re-register the account to get a fresh config with new tokenProvider
92
90
  this.configs.delete(accountId);
@@ -423,14 +421,11 @@ export class ImapManager extends EventEmitter {
423
421
  catch (e) {
424
422
  this.emit("syncError", accountId, e.message);
425
423
  console.error(`Sync error for ${accountId}: ${e.message}`);
426
- // Classify error and emit user-facing hint
427
- const msg = e.message || "";
428
- if (msg.includes("token") || msg.includes("auth") || msg.includes("Command failed")) {
429
- this.emit("accountError", accountId, msg, "Re-authenticate: mailx -setup or check credentials");
430
- }
431
- else if (msg.includes("timeout")) {
432
- this.emit("accountError", accountId, msg, "Server slow or unreachable — will retry");
433
- }
424
+ // Emit user-facing error always offer re-auth for OAuth accounts
425
+ const config = this.configs.get(accountId);
426
+ const isOAuth = !!config?.tokenProvider;
427
+ const hint = isOAuth ? "Authentication may have expired" : "Check server connectivity";
428
+ this.emit("accountError", accountId, e.message || "unknown", hint);
434
429
  }
435
430
  finally {
436
431
  if (client)
@@ -1188,6 +1183,13 @@ export class ImapManager extends EventEmitter {
1188
1183
  }
1189
1184
  const accountDir = account.imap.user.replace(/[@.]/g, "_");
1190
1185
  const tokenDir = path.join(getConfigDir(), "tokens", accountDir);
1186
+ const tokenPath = path.join(tokenDir, "contacts-token.json");
1187
+ // If no cached token exists, skip — don't open interactive browser consent for contacts
1188
+ // The user can trigger contacts sync manually or it will be set up during mailx -setup
1189
+ if (!fs.existsSync(tokenPath)) {
1190
+ console.log(` [contacts] No token for ${accountId} — skipping (run mailx -setup to authorize)`);
1191
+ return null;
1192
+ }
1191
1193
  const token = await authenticateOAuth(credentialsPath, {
1192
1194
  scope: "https://www.googleapis.com/auth/contacts.readonly",
1193
1195
  tokenDirectory: tokenDir,