@bobfrankston/mailx 1.0.289 → 1.0.291
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/bin/mailx.js
CHANGED
|
@@ -939,6 +939,9 @@ async function main() {
|
|
|
939
939
|
imapManager.on("syncComplete", (accountId) => {
|
|
940
940
|
handle.send({ _event: "syncComplete", type: "syncComplete", accountId });
|
|
941
941
|
});
|
|
942
|
+
imapManager.on("syncActionFailed", (accountId, action, uid, error) => {
|
|
943
|
+
handle.send({ _event: "syncActionFailed", type: "syncActionFailed", accountId, action, uid, error });
|
|
944
|
+
});
|
|
942
945
|
// Cloud-write/read failures from mailx-settings → push to UI as a banner so
|
|
943
946
|
// silent fall-back-to-local can no longer swallow Drive errors.
|
|
944
947
|
const { onCloudError } = await import("@bobfrankston/mailx-settings");
|
package/client/app.js
CHANGED
|
@@ -608,12 +608,12 @@ async function deleteSelectedMessages() {
|
|
|
608
608
|
if (selected.length === 1) {
|
|
609
609
|
lastDeleted = { ...selected[0], subject: "" };
|
|
610
610
|
if (statusSync)
|
|
611
|
-
statusSync.textContent = `
|
|
611
|
+
statusSync.textContent = `Trashed 1 message (syncing) — Ctrl+Z to undo`;
|
|
612
612
|
}
|
|
613
613
|
else {
|
|
614
614
|
lastDeleted = null;
|
|
615
615
|
if (statusSync)
|
|
616
|
-
statusSync.textContent = `
|
|
616
|
+
statusSync.textContent = `Trashed ${selected.length} messages (syncing)`;
|
|
617
617
|
}
|
|
618
618
|
if (undoTimeout)
|
|
619
619
|
clearTimeout(undoTimeout);
|
|
@@ -711,7 +711,7 @@ async function spamSelectedMessages() {
|
|
|
711
711
|
await markAsSpamMessages(accountId, uids);
|
|
712
712
|
}
|
|
713
713
|
if (statusSync)
|
|
714
|
-
statusSync.textContent = `
|
|
714
|
+
statusSync.textContent = `Spam: ${selected.length} queued — pending server sync`;
|
|
715
715
|
messageState.removeMessages(selected);
|
|
716
716
|
}
|
|
717
717
|
catch (e) {
|
|
@@ -1019,7 +1019,11 @@ onWsEvent((event) => {
|
|
|
1019
1019
|
}
|
|
1020
1020
|
break;
|
|
1021
1021
|
case "folderCountsChanged": {
|
|
1022
|
-
// Update folder badges +
|
|
1022
|
+
// Update folder badges + full tree refresh (not just counts) so
|
|
1023
|
+
// newly-synced folders appear. Without this, the first sync after
|
|
1024
|
+
// setup creates DB folders but the tree never picks them up because
|
|
1025
|
+
// syncComplete fires during page reload and gets lost.
|
|
1026
|
+
refreshFolderTree();
|
|
1023
1027
|
updateFolderCounts();
|
|
1024
1028
|
updateNewMessageCount();
|
|
1025
1029
|
// Debounced silent reload — preserves scroll position, selection, and viewer
|
|
@@ -1040,6 +1044,14 @@ onWsEvent((event) => {
|
|
|
1040
1044
|
statusSync.textContent = `Synced ${new Date().toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit", hour12: false })}`;
|
|
1041
1045
|
break;
|
|
1042
1046
|
}
|
|
1047
|
+
case "syncActionFailed": {
|
|
1048
|
+
// Surface sync failures (move/delete/flag not applied on server)
|
|
1049
|
+
// so the user knows local-first actions haven't propagated yet.
|
|
1050
|
+
const action = event.action === "move" ? "Move" : event.action === "delete" ? "Delete" : event.action;
|
|
1051
|
+
if (statusSync)
|
|
1052
|
+
statusSync.textContent = `Sync failed: ${action} — ${event.error}`;
|
|
1053
|
+
break;
|
|
1054
|
+
}
|
|
1043
1055
|
case "reload":
|
|
1044
1056
|
location.reload();
|
|
1045
1057
|
break;
|
|
@@ -482,22 +482,34 @@ async function loadFolderTree(container) {
|
|
|
482
482
|
"aol.com": "Use an app password: AOL Settings → Account Security → Generate app password",
|
|
483
483
|
"icloud.com": "Use an app-specific password: appleid.apple.com → Sign-In and Security → App-Specific Passwords",
|
|
484
484
|
};
|
|
485
|
+
let oauthAutoFired = false;
|
|
485
486
|
emailInput?.addEventListener("input", () => {
|
|
486
487
|
const email = emailInput.value.trim();
|
|
487
488
|
const domain = email.split("@")[1]?.toLowerCase() || "";
|
|
488
489
|
const hasAt = email.includes("@") && domain.length > 0;
|
|
489
490
|
const isOAuth = ["gmail.com", "googlemail.com", "outlook.com", "hotmail.com", "live.com"].includes(domain);
|
|
490
|
-
|
|
491
|
-
//
|
|
491
|
+
const isGmailLike = ["gmail.com", "googlemail.com"].includes(domain);
|
|
492
|
+
// OAuth providers: auto-fire setup immediately once domain
|
|
493
|
+
// is recognized — don't show name/password (name is auto-
|
|
494
|
+
// detected from Google profile, no password needed). This
|
|
495
|
+
// eliminates the "form flash" where fields briefly appear
|
|
496
|
+
// before the page reloads.
|
|
497
|
+
if (hasAt && isOAuth && !oauthAutoFired && !setupTriggered) {
|
|
498
|
+
oauthAutoFired = true;
|
|
499
|
+
statusEl.textContent = `Connecting to ${isGmailLike ? "Gmail" : "Outlook"}...`;
|
|
500
|
+
trySetup();
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
// Non-OAuth: progressive reveal of name + password + submit.
|
|
492
504
|
const nameRow = document.getElementById("setup-name-row");
|
|
493
505
|
const pwRow = document.getElementById("setup-password-row");
|
|
494
506
|
const submitBtn = document.getElementById("setup-submit");
|
|
495
507
|
if (nameRow)
|
|
496
|
-
nameRow.style.display = hasAt ? "block" : "none";
|
|
508
|
+
nameRow.style.display = hasAt && !isOAuth ? "block" : "none";
|
|
497
509
|
if (pwRow)
|
|
498
510
|
pwRow.style.display = hasAt && !isOAuth ? "block" : "none";
|
|
499
511
|
if (submitBtn)
|
|
500
|
-
submitBtn.style.display = hasAt ? "block" : "none";
|
|
512
|
+
submitBtn.style.display = hasAt && !isOAuth ? "block" : "none";
|
|
501
513
|
const helpEl = document.getElementById("setup-app-password-help");
|
|
502
514
|
if (helpEl) {
|
|
503
515
|
const help = APP_PASSWORD_HELP[domain];
|
package/package.json
CHANGED
|
@@ -22,6 +22,7 @@ export interface ImapManagerEvents {
|
|
|
22
22
|
/** Fired after a message body has been written to the local store — lets
|
|
23
23
|
* the UI flip a row's "not-downloaded" indicator without re-rendering. */
|
|
24
24
|
bodyCached: (accountId: string, uid: number) => void;
|
|
25
|
+
syncActionFailed: (accountId: string, action: string, uid: number, error: string) => void;
|
|
25
26
|
}
|
|
26
27
|
export declare class ImapManager extends EventEmitter {
|
|
27
28
|
private configs;
|
|
@@ -2078,9 +2078,11 @@ export class ImapManager extends EventEmitter {
|
|
|
2078
2078
|
catch (e) {
|
|
2079
2079
|
console.error(` [sync] Failed action ${action.action} UID ${action.uid}: ${e.message}`);
|
|
2080
2080
|
this.db.failSyncAction(action.id, e.message);
|
|
2081
|
+
this.emit("syncActionFailed", accountId, action.action, action.uid, e.message);
|
|
2081
2082
|
if (action.attempts >= 5) {
|
|
2082
2083
|
console.error(` [sync] Giving up on action ${action.id} after 5 attempts`);
|
|
2083
2084
|
this.db.completeSyncAction(action.id);
|
|
2085
|
+
this.emit("syncActionFailed", accountId, action.action, action.uid, `Gave up after 5 attempts: ${e.message}`);
|
|
2084
2086
|
}
|
|
2085
2087
|
}
|
|
2086
2088
|
}
|