@bobfrankston/mailx 1.0.118 → 1.0.119
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 +31 -1
- package/package.json +1 -1
- package/packages/mailx-api/index.js +42 -0
- package/packages/mailx-imap/index.d.ts +2 -0
- package/packages/mailx-imap/index.js +2 -0
- package/packages/mailx-server/index.js +7 -1
- package/packages/mailx-store/db.d.ts +6 -0
- package/packages/mailx-store/db.js +3 -0
package/client/app.js
CHANGED
|
@@ -844,7 +844,37 @@ fetch("/api/version").then(r => r.json()).then(d => {
|
|
|
844
844
|
: "";
|
|
845
845
|
if (el)
|
|
846
846
|
el.textContent = `mailx v${d.version}${storageLabel}${isApp ? "" : " [browser]"}`;
|
|
847
|
-
if (
|
|
847
|
+
if (d.settingsError) {
|
|
848
|
+
showAlert(d.settingsError, "settings-error");
|
|
849
|
+
// Add repair button to the banner
|
|
850
|
+
const banner = document.getElementById("alert-banner");
|
|
851
|
+
if (banner && !banner.querySelector(".repair-btn")) {
|
|
852
|
+
const btn = document.createElement("button");
|
|
853
|
+
btn.className = "repair-btn status-action";
|
|
854
|
+
btn.textContent = "Repair: restore accounts from cache";
|
|
855
|
+
btn.style.cssText = "margin-left:1rem;padding:0.25rem 0.75rem;background:#a6e3a1;color:#1e1e2e;border:none;border-radius:4px;cursor:pointer;font-weight:bold";
|
|
856
|
+
btn.onclick = async () => {
|
|
857
|
+
btn.textContent = "Restoring...";
|
|
858
|
+
btn.disabled = true;
|
|
859
|
+
try {
|
|
860
|
+
const r = await fetch("/api/repair-accounts", { method: "POST" });
|
|
861
|
+
const data = await r.json();
|
|
862
|
+
if (data.ok) {
|
|
863
|
+
hideAlert();
|
|
864
|
+
setTimeout(() => location.reload(), 1000);
|
|
865
|
+
}
|
|
866
|
+
else {
|
|
867
|
+
btn.textContent = `Failed: ${data.error}`;
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
catch (e) {
|
|
871
|
+
btn.textContent = `Error: ${e.message}`;
|
|
872
|
+
}
|
|
873
|
+
};
|
|
874
|
+
banner.querySelector("#alert-text")?.after(btn);
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
else if (storage.cloudError) {
|
|
848
878
|
showAlert(`Cloud storage error: ${storage.cloudError}`, "cloud-error");
|
|
849
879
|
}
|
|
850
880
|
}).catch(async () => {
|
package/package.json
CHANGED
|
@@ -158,6 +158,48 @@ export function createApiRouter(db, imapManager) {
|
|
|
158
158
|
res.status(500).json({ error: e.message });
|
|
159
159
|
}
|
|
160
160
|
});
|
|
161
|
+
// ── Repair: restore accounts from DB cache to settings, re-register IMAP ──
|
|
162
|
+
router.post("/repair-accounts", async (req, res) => {
|
|
163
|
+
try {
|
|
164
|
+
// Get accounts from DB (stale but present)
|
|
165
|
+
const dbAccounts = db.getAccountConfigs();
|
|
166
|
+
if (dbAccounts.length === 0) {
|
|
167
|
+
res.json({ ok: false, error: "No cached accounts in database" });
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
// Rebuild account configs from DB's stored config_json
|
|
171
|
+
const restored = [];
|
|
172
|
+
for (const a of dbAccounts) {
|
|
173
|
+
try {
|
|
174
|
+
const cfg = JSON.parse(a.configJson);
|
|
175
|
+
restored.push(cfg);
|
|
176
|
+
}
|
|
177
|
+
catch { /* skip corrupt entries */ }
|
|
178
|
+
}
|
|
179
|
+
if (restored.length === 0) {
|
|
180
|
+
res.json({ ok: false, error: "Could not parse cached account configs" });
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
// Save back to shared dir (and cloud API if active)
|
|
184
|
+
saveAccounts(restored);
|
|
185
|
+
// Re-register in IMAP manager
|
|
186
|
+
for (const acct of restored) {
|
|
187
|
+
try {
|
|
188
|
+
await imapManager.addAccount(acct);
|
|
189
|
+
console.log(` [repair] Re-registered account: ${acct.name} (${acct.id})`);
|
|
190
|
+
}
|
|
191
|
+
catch (e) {
|
|
192
|
+
console.error(` [repair] Failed to register ${acct.id}: ${e.message}`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
// Start sync
|
|
196
|
+
imapManager.syncAll().catch(() => { });
|
|
197
|
+
res.json({ ok: true, message: `Restored ${restored.length} account(s) and started sync.` });
|
|
198
|
+
}
|
|
199
|
+
catch (e) {
|
|
200
|
+
res.status(500).json({ error: e.message });
|
|
201
|
+
}
|
|
202
|
+
});
|
|
161
203
|
// ── Send ──
|
|
162
204
|
router.post("/send", async (req, res) => {
|
|
163
205
|
try {
|
|
@@ -56,6 +56,8 @@ export declare class ImapManager extends EventEmitter {
|
|
|
56
56
|
private createClient;
|
|
57
57
|
/** Track client logout for connection counting */
|
|
58
58
|
private trackLogout;
|
|
59
|
+
/** Number of registered IMAP accounts */
|
|
60
|
+
getAccountCount(): number;
|
|
59
61
|
/** Register an account */
|
|
60
62
|
addAccount(account: AccountConfig): Promise<void>;
|
|
61
63
|
/** Sync folder list for an account */
|
|
@@ -214,6 +214,8 @@ export class ImapManager extends EventEmitter {
|
|
|
214
214
|
this.activeConnections.set(accountId, count);
|
|
215
215
|
console.log(` [conn] ${accountId}: -1 (${count} active)`);
|
|
216
216
|
}
|
|
217
|
+
/** Number of registered IMAP accounts */
|
|
218
|
+
getAccountCount() { return this.configs.size; }
|
|
217
219
|
/** Register an account */
|
|
218
220
|
async addAccount(account) {
|
|
219
221
|
if (this.configs.has(account.id))
|
|
@@ -94,7 +94,13 @@ const apiRouter = createApiRouter(db, imapManager);
|
|
|
94
94
|
app.use("/api", apiRouter);
|
|
95
95
|
app.get("/api/version", (req, res) => {
|
|
96
96
|
const storage = getStorageInfo();
|
|
97
|
-
|
|
97
|
+
const imapAccounts = imapManager.getAccountCount();
|
|
98
|
+
const dbAccounts = db.getAccounts().length;
|
|
99
|
+
// Warn if DB has accounts but IMAP has none — stale DB, settings missing
|
|
100
|
+
const settingsError = (dbAccounts > 0 && imapAccounts === 0)
|
|
101
|
+
? "No accounts loaded from settings — showing stale data. Check accounts.jsonc on your cloud drive."
|
|
102
|
+
: undefined;
|
|
103
|
+
res.json({ version: SERVER_VERSION, theme: settings.ui?.theme || "system", storage, imapAccounts, settingsError });
|
|
98
104
|
});
|
|
99
105
|
app.all("/info", (req, res) => {
|
|
100
106
|
res.json({ version: SERVER_VERSION, uptime: Math.round(process.uptime()), port: PORT, imap: imapManager.useNativeClient ? "native" : "imapflow" });
|
|
@@ -15,6 +15,12 @@ export declare class MailxDB {
|
|
|
15
15
|
email: string;
|
|
16
16
|
lastSync: number;
|
|
17
17
|
}[];
|
|
18
|
+
getAccountConfigs(): {
|
|
19
|
+
id: string;
|
|
20
|
+
name: string;
|
|
21
|
+
email: string;
|
|
22
|
+
configJson: string;
|
|
23
|
+
}[];
|
|
18
24
|
updateLastSync(accountId: string, timestamp: number): void;
|
|
19
25
|
upsertFolder(accountId: string, folderPath: string, name: string, specialUse: string, delimiter: string): number;
|
|
20
26
|
getFolders(accountId: string): Folder[];
|
|
@@ -137,6 +137,9 @@ export class MailxDB {
|
|
|
137
137
|
getAccounts() {
|
|
138
138
|
return this.db.prepare("SELECT id, name, email, last_sync as lastSync FROM accounts").all();
|
|
139
139
|
}
|
|
140
|
+
getAccountConfigs() {
|
|
141
|
+
return this.db.prepare("SELECT id, name, email, config_json as configJson FROM accounts").all();
|
|
142
|
+
}
|
|
140
143
|
updateLastSync(accountId, timestamp) {
|
|
141
144
|
this.db.prepare("UPDATE accounts SET last_sync = ? WHERE id = ?").run(timestamp, accountId);
|
|
142
145
|
}
|