@bobfrankston/mailx 1.0.235 → 1.0.236
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.
|
@@ -458,7 +458,14 @@ export async function showMessage(accountId, uid, folderId, specialUse, isRetry
|
|
|
458
458
|
catch (e) {
|
|
459
459
|
const err = e.message || "Unknown error";
|
|
460
460
|
console.error("showMessage error:", e);
|
|
461
|
-
//
|
|
461
|
+
// "Message was deleted from the server" — the service already dropped
|
|
462
|
+
// the local row. Remove it from the list so the UI advances to the next
|
|
463
|
+
// message instead of sitting on a stale error banner.
|
|
464
|
+
const isDeleted = /deleted from the server|isNotFound/.test(err);
|
|
465
|
+
if (isDeleted) {
|
|
466
|
+
state.removeMessages([{ accountId, uid }]);
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
462
469
|
const isNotFound = err.includes("not found") || err.includes("Not Found") || err.includes("404");
|
|
463
470
|
if (!isNotFound && retryCount < 3) {
|
|
464
471
|
retryCount++;
|
|
@@ -532,6 +539,15 @@ document.addEventListener("mouseover", e => {
|
|
|
532
539
|
const a = e.target.closest("a[href]");
|
|
533
540
|
window.parent.postMessage({ type: "linkHover", url: a ? a.href : "" }, "*");
|
|
534
541
|
});
|
|
542
|
+
// Intercept link clicks — Android WebView silently drops window.open, so forward to parent
|
|
543
|
+
document.addEventListener("click", e => {
|
|
544
|
+
const a = e.target.closest("a[href]");
|
|
545
|
+
if (!a) return;
|
|
546
|
+
const url = a.href;
|
|
547
|
+
if (!url || url.startsWith("javascript:") || url.startsWith("#")) return;
|
|
548
|
+
e.preventDefault();
|
|
549
|
+
window.parent.postMessage({ type: "linkClick", url: url }, "*");
|
|
550
|
+
}, true);
|
|
535
551
|
</script>
|
|
536
552
|
</head><body>${html}</body></html>`;
|
|
537
553
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bobfrankston/mailx",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.236",
|
|
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.298",
|
|
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.298",
|
|
82
82
|
"@capacitor/android": "^8.3.0",
|
|
83
83
|
"@capacitor/cli": "^8.3.0",
|
|
84
84
|
"@capacitor/core": "^8.3.0",
|
|
@@ -132,7 +132,10 @@ export declare class ImapManager extends EventEmitter {
|
|
|
132
132
|
/** Fetch a single message body on demand, caching in the store.
|
|
133
133
|
* Uses its own fresh connection — never blocked by background prefetch. */
|
|
134
134
|
fetchMessageBody(accountId: string, folderId: number, uid: number): Promise<Buffer | null>;
|
|
135
|
-
/** Fetch message body via Gmail/Outlook API
|
|
135
|
+
/** Fetch message body via Gmail/Outlook API.
|
|
136
|
+
* Throws `MessageNotFoundError` when the server says the message is gone
|
|
137
|
+
* (deleted from another device, for example). The caller uses that to
|
|
138
|
+
* delete the stale row locally instead of showing a generic error. */
|
|
136
139
|
private fetchMessageBodyViaApi;
|
|
137
140
|
/** Background body prefetch — download bodies for messages that don't have them */
|
|
138
141
|
private prefetchBodies;
|
|
@@ -42,6 +42,15 @@ function insertHeaderBeforeBody(raw, line) {
|
|
|
42
42
|
const nl = m[0].startsWith("\r\n") ? "\r\n" : "\n";
|
|
43
43
|
return raw.slice(0, m.index) + nl + line + raw.slice(m.index);
|
|
44
44
|
}
|
|
45
|
+
/** Error thrown when a message body can't be fetched because the server says
|
|
46
|
+
* the message is gone (deleted from another device, expunged, etc.). The
|
|
47
|
+
* caller uses this to remove the stale local row instead of showing a
|
|
48
|
+
* generic "fetch failed" error to the user. */
|
|
49
|
+
function makeNotFoundError(accountId, folderId, uid) {
|
|
50
|
+
const err = new Error(`Message ${accountId}/${folderId}/${uid} not found on server`);
|
|
51
|
+
err.isNotFound = true;
|
|
52
|
+
return err;
|
|
53
|
+
}
|
|
45
54
|
/** Extract full error detail with provenance */
|
|
46
55
|
function imapError(err) {
|
|
47
56
|
const msg = err.message || err.reason || err.code || (typeof err === "string" ? err : "");
|
|
@@ -1332,7 +1341,12 @@ export class ImapManager extends EventEmitter {
|
|
|
1332
1341
|
const msg = await client.fetchMessageByUid(folder.path, uid, { source: true });
|
|
1333
1342
|
await client.logout();
|
|
1334
1343
|
client = null;
|
|
1335
|
-
if (!msg
|
|
1344
|
+
if (!msg) {
|
|
1345
|
+
// IMAP server says the UID is gone — message was deleted
|
|
1346
|
+
// elsewhere. Raise NotFound so the caller can remove the row.
|
|
1347
|
+
throw makeNotFoundError(accountId, folderId, uid);
|
|
1348
|
+
}
|
|
1349
|
+
if (!msg.source)
|
|
1336
1350
|
return null;
|
|
1337
1351
|
const raw = Buffer.from(msg.source, "utf-8");
|
|
1338
1352
|
const bodyPath = await this.bodyStore.putMessage(accountId, folderId, uid, raw);
|
|
@@ -1340,6 +1354,15 @@ export class ImapManager extends EventEmitter {
|
|
|
1340
1354
|
return raw;
|
|
1341
1355
|
}
|
|
1342
1356
|
catch (e) {
|
|
1357
|
+
if (e?.isNotFound) {
|
|
1358
|
+
if (client) {
|
|
1359
|
+
try {
|
|
1360
|
+
await client.logout();
|
|
1361
|
+
}
|
|
1362
|
+
catch { /* */ }
|
|
1363
|
+
}
|
|
1364
|
+
throw e;
|
|
1365
|
+
}
|
|
1343
1366
|
console.error(` Body fetch error (${accountId}/${uid}): ${e.message}`);
|
|
1344
1367
|
if (client) {
|
|
1345
1368
|
try {
|
|
@@ -1350,13 +1373,20 @@ export class ImapManager extends EventEmitter {
|
|
|
1350
1373
|
return null;
|
|
1351
1374
|
}
|
|
1352
1375
|
}
|
|
1353
|
-
/** Fetch message body via Gmail/Outlook API
|
|
1376
|
+
/** Fetch message body via Gmail/Outlook API.
|
|
1377
|
+
* Throws `MessageNotFoundError` when the server says the message is gone
|
|
1378
|
+
* (deleted from another device, for example). The caller uses that to
|
|
1379
|
+
* delete the stale row locally instead of showing a generic error. */
|
|
1354
1380
|
async fetchMessageBodyViaApi(accountId, folderId, uid, folderPath) {
|
|
1355
1381
|
try {
|
|
1356
1382
|
const api = this.getGmailProvider(accountId);
|
|
1357
1383
|
const msg = await api.fetchOne(folderPath, uid, { source: true });
|
|
1358
1384
|
await api.close();
|
|
1359
|
-
if (!msg
|
|
1385
|
+
if (!msg) {
|
|
1386
|
+
// fetchOne returned null — message doesn't exist on the server anymore
|
|
1387
|
+
throw makeNotFoundError(accountId, folderId, uid);
|
|
1388
|
+
}
|
|
1389
|
+
if (!msg.source)
|
|
1360
1390
|
return null;
|
|
1361
1391
|
const raw = Buffer.from(msg.source, "utf-8");
|
|
1362
1392
|
const bodyPath = await this.bodyStore.putMessage(accountId, folderId, uid, raw);
|
|
@@ -1364,6 +1394,10 @@ export class ImapManager extends EventEmitter {
|
|
|
1364
1394
|
return raw;
|
|
1365
1395
|
}
|
|
1366
1396
|
catch (e) {
|
|
1397
|
+
// Gmail API 404 → the message was deleted on the server
|
|
1398
|
+
if (e?.isNotFound || /Gmail API 404|404|not found/i.test(e?.message || "")) {
|
|
1399
|
+
throw makeNotFoundError(accountId, folderId, uid);
|
|
1400
|
+
}
|
|
1367
1401
|
console.error(` [api] Body fetch error (${accountId}/${uid}): ${e.message}`);
|
|
1368
1402
|
return null;
|
|
1369
1403
|
}
|
|
@@ -141,13 +141,26 @@ export class MailxService {
|
|
|
141
141
|
raw = await this.imapManager.fetchMessageBody(accountId, envelope.folderId, envelope.uid);
|
|
142
142
|
}
|
|
143
143
|
catch (fetchErr) {
|
|
144
|
+
// Message was deleted from the server (another device, expunge, etc.) —
|
|
145
|
+
// drop the stale local row so the UI removes it instead of showing a
|
|
146
|
+
// confusing error. Throwing a tagged error lets the client react.
|
|
147
|
+
if (fetchErr?.isNotFound) {
|
|
148
|
+
try {
|
|
149
|
+
this.db.deleteMessage(accountId, envelope.uid);
|
|
150
|
+
this.db.recalcFolderCounts(envelope.folderId);
|
|
151
|
+
}
|
|
152
|
+
catch { /* ignore */ }
|
|
153
|
+
const err = new Error("Message was deleted from the server");
|
|
154
|
+
err.isNotFound = true;
|
|
155
|
+
throw err;
|
|
156
|
+
}
|
|
144
157
|
return {
|
|
145
|
-
...envelope, bodyHtml: "", bodyText: `[Message body unavailable: ${fetchErr.message || "
|
|
158
|
+
...envelope, bodyHtml: "", bodyText: `[Message body unavailable: ${fetchErr.message || "connection failed"}]`,
|
|
146
159
|
hasRemoteContent: false, remoteAllowed: false, attachments: [], emlPath: "", deliveredTo: "", returnPath: "", listUnsubscribe: ""
|
|
147
160
|
};
|
|
148
161
|
}
|
|
149
162
|
if (!raw) {
|
|
150
|
-
bodyText = "[Message body not
|
|
163
|
+
bodyText = "[Message body not cached locally and the server fetch returned nothing. Try re-syncing the folder.]";
|
|
151
164
|
}
|
|
152
165
|
else {
|
|
153
166
|
const parsed = await simpleParser(raw);
|