@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 +26 -12
- package/package.json +2 -2
- package/packages/mailx-imap/index.d.ts +1 -1
- package/packages/mailx-imap/index.js +18 -16
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
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
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
|
-
|
|
585
|
-
acctEl
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
//
|
|
427
|
-
const
|
|
428
|
-
|
|
429
|
-
|
|
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,
|