@bobfrankston/mailx 1.0.229 → 1.0.231
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
|
@@ -775,6 +775,9 @@ async function main() {
|
|
|
775
775
|
imapManager.on("accountError", (accountId, error, hint, isOAuth) => {
|
|
776
776
|
handle.send({ _event: "accountError", type: "accountError", accountId, error, hint, isOAuth });
|
|
777
777
|
});
|
|
778
|
+
imapManager.on("configChanged", (filename) => {
|
|
779
|
+
handle.send({ _event: "configChanged", type: "configChanged", filename });
|
|
780
|
+
});
|
|
778
781
|
// Brief pause for WebView2 to initialize before starting IMAP (avoids stdin writes during init)
|
|
779
782
|
await new Promise(r => setTimeout(r, 500));
|
|
780
783
|
// Register all accounts (OAuth may open browser for Gmail — event loop stays free for IPC)
|
|
@@ -797,6 +800,7 @@ async function main() {
|
|
|
797
800
|
}
|
|
798
801
|
imapManager.startPeriodicSync(settings.sync.intervalMinutes);
|
|
799
802
|
imapManager.startOutboxWorker();
|
|
803
|
+
imapManager.watchConfigFiles();
|
|
800
804
|
// Graceful shutdown — close IMAP connections, stop timers, close DB
|
|
801
805
|
let shuttingDown = false;
|
|
802
806
|
async function gracefulShutdown(reason) {
|
package/client/app.js
CHANGED
|
@@ -866,6 +866,11 @@ onWsEvent((event) => {
|
|
|
866
866
|
case "reload":
|
|
867
867
|
location.reload();
|
|
868
868
|
break;
|
|
869
|
+
case "configChanged":
|
|
870
|
+
// A watched config file (accounts.jsonc etc.) was modified externally.
|
|
871
|
+
// Show a banner telling the user to restart for changes to take effect.
|
|
872
|
+
showAlert(`${event.filename} was updated — restart mailx to apply changes`, `config-${event.filename}`);
|
|
873
|
+
break;
|
|
869
874
|
case "error":
|
|
870
875
|
if (statusSync)
|
|
871
876
|
statusSync.textContent = `Error: ${event.message}`;
|
|
@@ -211,9 +211,12 @@ export async function showMessage(accountId, uid, folderId, specialUse, isRetry
|
|
|
211
211
|
});
|
|
212
212
|
}
|
|
213
213
|
headerEl.querySelector(".mv-date").textContent = new Date(msg.date).toLocaleString(undefined, { year: "numeric", month: "short", day: "numeric", hour: "2-digit", minute: "2-digit", hour12: false });
|
|
214
|
-
// Unsubscribe button (upper right of header)
|
|
214
|
+
// Unsubscribe button (upper right of header).
|
|
215
|
+
// - mailto: URLs open a pre-filled compose window (so the unsubscribe
|
|
216
|
+
// reply gets sent from the correct mailx account, not the OS default
|
|
217
|
+
// mail handler)
|
|
218
|
+
// - https: URLs open a new tab
|
|
215
219
|
const unsubBtn = document.getElementById("mv-unsubscribe");
|
|
216
|
-
// listUnsubscribe is now a clean URL (https:// or mailto:) from the server
|
|
217
220
|
const unsubUrl = msg.listUnsubscribe || "";
|
|
218
221
|
if (unsubBtn) {
|
|
219
222
|
if (unsubUrl) {
|
|
@@ -223,7 +226,30 @@ export async function showMessage(accountId, uid, folderId, specialUse, isRetry
|
|
|
223
226
|
unsubBtn.href = "#";
|
|
224
227
|
unsubBtn.onclick = (e) => {
|
|
225
228
|
e.preventDefault();
|
|
226
|
-
|
|
229
|
+
if (/^mailto:/i.test(unsubUrl)) {
|
|
230
|
+
// Parse mailto:addr?subject=... and pre-fill compose
|
|
231
|
+
const m = unsubUrl.match(/^mailto:([^?]*)(?:\?(.*))?$/i);
|
|
232
|
+
const to = m?.[1] ? decodeURIComponent(m[1]) : "";
|
|
233
|
+
const qs = new URLSearchParams(m?.[2] || "");
|
|
234
|
+
const subject = qs.get("subject") || "Unsubscribe";
|
|
235
|
+
const body = qs.get("body") || "";
|
|
236
|
+
const init = {
|
|
237
|
+
mode: "new",
|
|
238
|
+
accountId: currentAccountId,
|
|
239
|
+
to: to ? [{ name: "", address: to }] : [],
|
|
240
|
+
cc: [],
|
|
241
|
+
subject,
|
|
242
|
+
bodyHtml: body ? `<p>${body}</p>` : "",
|
|
243
|
+
inReplyTo: "",
|
|
244
|
+
references: [],
|
|
245
|
+
accounts: [],
|
|
246
|
+
};
|
|
247
|
+
sessionStorage.setItem("composeInit", JSON.stringify(init));
|
|
248
|
+
document.dispatchEvent(new CustomEvent("mailx-compose", { detail: { mode: "new" } }));
|
|
249
|
+
}
|
|
250
|
+
else {
|
|
251
|
+
window.open(unsubUrl, "_blank");
|
|
252
|
+
}
|
|
227
253
|
};
|
|
228
254
|
}
|
|
229
255
|
else {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bobfrankston/mailx",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.231",
|
|
4
4
|
"description": "Local-first email client with IMAP sync and standalone native app",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "bin/mailx.js",
|
|
@@ -24,7 +24,7 @@
|
|
|
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.
|
|
27
|
+
"@bobfrankston/msger": "^0.1.293",
|
|
28
28
|
"@capacitor/android": "^8.3.0",
|
|
29
29
|
"@capacitor/cli": "^8.3.0",
|
|
30
30
|
"@capacitor/core": "^8.3.0",
|
|
@@ -78,7 +78,7 @@
|
|
|
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.
|
|
81
|
+
"@bobfrankston/msger": "^0.1.293",
|
|
82
82
|
"@capacitor/android": "^8.3.0",
|
|
83
83
|
"@capacitor/cli": "^8.3.0",
|
|
84
84
|
"@capacitor/core": "^8.3.0",
|
|
@@ -18,6 +18,7 @@ export interface ImapManagerEvents {
|
|
|
18
18
|
unread: number;
|
|
19
19
|
}>) => void;
|
|
20
20
|
accountError: (accountId: string, error: string, hint: string, isOAuth: boolean) => void;
|
|
21
|
+
configChanged: (filename: string) => void;
|
|
21
22
|
}
|
|
22
23
|
export declare class ImapManager extends EventEmitter {
|
|
23
24
|
private configs;
|
|
@@ -205,6 +206,13 @@ export declare class ImapManager extends EventEmitter {
|
|
|
205
206
|
startOutboxWorker(): void;
|
|
206
207
|
/** Stop Outbox worker */
|
|
207
208
|
stopOutboxWorker(): void;
|
|
209
|
+
private configWatchers;
|
|
210
|
+
/** Watch the local config files for external changes. On change, emit
|
|
211
|
+
* configChanged so the UI can show a "restart to apply" banner. Uses
|
|
212
|
+
* a debounce to coalesce rapid writes from save tools. */
|
|
213
|
+
watchConfigFiles(): void;
|
|
214
|
+
/** Stop all config file watchers */
|
|
215
|
+
stopWatchingConfig(): void;
|
|
208
216
|
private contactsSyncToken;
|
|
209
217
|
/** Get an OAuth token for Google APIs (contacts, calendar, etc.)
|
|
210
218
|
* Uses the SAME token as IMAP — scopes are combined in one grant */
|
|
@@ -2116,6 +2116,47 @@ export class ImapManager extends EventEmitter {
|
|
|
2116
2116
|
this.outboxInterval = null;
|
|
2117
2117
|
}
|
|
2118
2118
|
}
|
|
2119
|
+
// ── Config file watcher ──
|
|
2120
|
+
configWatchers = [];
|
|
2121
|
+
/** Watch the local config files for external changes. On change, emit
|
|
2122
|
+
* configChanged so the UI can show a "restart to apply" banner. Uses
|
|
2123
|
+
* a debounce to coalesce rapid writes from save tools. */
|
|
2124
|
+
watchConfigFiles() {
|
|
2125
|
+
const files = ["accounts.jsonc", "allowlist.jsonc", "clients.jsonc", "config.jsonc"];
|
|
2126
|
+
const configDir = getConfigDir();
|
|
2127
|
+
const debounce = new Map();
|
|
2128
|
+
for (const filename of files) {
|
|
2129
|
+
const full = path.join(configDir, filename);
|
|
2130
|
+
if (!fs.existsSync(full))
|
|
2131
|
+
continue;
|
|
2132
|
+
try {
|
|
2133
|
+
const watcher = fs.watch(full, () => {
|
|
2134
|
+
const prev = debounce.get(filename);
|
|
2135
|
+
if (prev)
|
|
2136
|
+
clearTimeout(prev);
|
|
2137
|
+
debounce.set(filename, setTimeout(() => {
|
|
2138
|
+
debounce.delete(filename);
|
|
2139
|
+
console.log(` [watch] ${filename} changed`);
|
|
2140
|
+
this.emit("configChanged", filename);
|
|
2141
|
+
}, 500));
|
|
2142
|
+
});
|
|
2143
|
+
this.configWatchers.push(watcher);
|
|
2144
|
+
}
|
|
2145
|
+
catch (e) {
|
|
2146
|
+
console.error(` [watch] Failed to watch ${filename}: ${e.message}`);
|
|
2147
|
+
}
|
|
2148
|
+
}
|
|
2149
|
+
}
|
|
2150
|
+
/** Stop all config file watchers */
|
|
2151
|
+
stopWatchingConfig() {
|
|
2152
|
+
for (const w of this.configWatchers) {
|
|
2153
|
+
try {
|
|
2154
|
+
w.close();
|
|
2155
|
+
}
|
|
2156
|
+
catch { /* ignore */ }
|
|
2157
|
+
}
|
|
2158
|
+
this.configWatchers = [];
|
|
2159
|
+
}
|
|
2119
2160
|
// ── Google Contacts Sync ──
|
|
2120
2161
|
contactsSyncToken = null;
|
|
2121
2162
|
/** Get an OAuth token for Google APIs (contacts, calendar, etc.)
|