@bobfrankston/mailx 1.0.244 → 1.0.246
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/.msger-window.json +1 -1
- package/client/app.js +6 -2
- package/client/components/message-viewer.js +9 -1
- package/package.json +7 -7
- package/packages/mailx-imap/index.d.ts +10 -5
- package/packages/mailx-imap/index.js +58 -49
- package/packages/mailx-imap/providers/gmail-api.js +24 -5
- package/packages/mailx-store-web/android-bootstrap.js +7 -0
- package/packages/mailx-store-web/imap-web-provider.d.ts +9 -0
- package/packages/mailx-store-web/imap-web-provider.js +45 -8
|
@@ -1 +1 @@
|
|
|
1
|
-
{"height":1344,"width":2151,"x":
|
|
1
|
+
{"height":1344,"width":2151,"x":736,"y":147}
|
package/client/app.js
CHANGED
|
@@ -221,9 +221,12 @@ document.getElementById("btn-back")?.addEventListener("click", () => {
|
|
|
221
221
|
document.getElementById("message-list")?.classList.remove("narrow-hidden");
|
|
222
222
|
});
|
|
223
223
|
// Close folder panel when a folder is selected (narrow mode)
|
|
224
|
+
// Also reset narrow navigation: show message list, hide viewer
|
|
224
225
|
document.getElementById("folder-tree")?.addEventListener("click", (e) => {
|
|
225
226
|
if (window.innerWidth <= 768 && e.target.closest(".ft-folder")) {
|
|
226
227
|
document.querySelector(".folder-panel")?.classList.remove("open");
|
|
228
|
+
document.getElementById("message-viewer")?.classList.remove("narrow-active");
|
|
229
|
+
document.getElementById("message-list")?.classList.remove("narrow-hidden");
|
|
227
230
|
}
|
|
228
231
|
});
|
|
229
232
|
// Close folder overlay when user clicks outside it (narrow mode OR
|
|
@@ -755,10 +758,11 @@ window.addEventListener("message", (e) => {
|
|
|
755
758
|
if (e.data?.type === "linkClick" && e.data.url) {
|
|
756
759
|
const url = e.data.url;
|
|
757
760
|
if (window.mailxapi?.platform === "android") {
|
|
758
|
-
// Android: use
|
|
761
|
+
// Android: use mailxapi:// bridge scheme — OnNavigating intercepts it
|
|
762
|
+
// and opens in system browser. Raw http:// in sub-frames doesn't trigger OnNavigating.
|
|
759
763
|
const f = document.createElement("iframe");
|
|
760
764
|
f.style.display = "none";
|
|
761
|
-
f.src = url
|
|
765
|
+
f.src = `mailxapi://openurl?url=${encodeURIComponent(url)}`;
|
|
762
766
|
document.body.appendChild(f);
|
|
763
767
|
setTimeout(() => f.remove(), 500);
|
|
764
768
|
}
|
|
@@ -540,13 +540,21 @@ document.addEventListener("mouseover", e => {
|
|
|
540
540
|
window.parent.postMessage({ type: "linkHover", url: a ? a.href : "" }, "*");
|
|
541
541
|
});
|
|
542
542
|
// Intercept link clicks — Android WebView silently drops window.open, so forward to parent
|
|
543
|
-
|
|
543
|
+
// Listen for both click and touchend since click may not fire on some Android WebViews
|
|
544
|
+
function handleLinkTap(e) {
|
|
544
545
|
const a = e.target.closest("a[href]");
|
|
545
546
|
if (!a) return;
|
|
546
547
|
const url = a.href;
|
|
547
548
|
if (!url || url.startsWith("javascript:") || url.startsWith("#")) return;
|
|
548
549
|
e.preventDefault();
|
|
549
550
|
window.parent.postMessage({ type: "linkClick", url: url }, "*");
|
|
551
|
+
}
|
|
552
|
+
document.addEventListener("click", handleLinkTap, true);
|
|
553
|
+
let lastTouchTarget = null;
|
|
554
|
+
document.addEventListener("touchstart", e => { lastTouchTarget = e.target; }, true);
|
|
555
|
+
document.addEventListener("touchend", e => {
|
|
556
|
+
if (lastTouchTarget && lastTouchTarget === e.target) handleLinkTap(e);
|
|
557
|
+
lastTouchTarget = null;
|
|
550
558
|
}, true);
|
|
551
559
|
</script>
|
|
552
560
|
</head><body>${html}</body></html>`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bobfrankston/mailx",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.246",
|
|
4
4
|
"description": "Local-first email client with IMAP sync and standalone native app",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "bin/mailx.js",
|
|
@@ -20,11 +20,11 @@
|
|
|
20
20
|
"postinstall": "node bin/postinstall.js"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@bobfrankston/iflow-direct": "^0.1.
|
|
24
|
-
"@bobfrankston/iflow-node": "^0.1.
|
|
23
|
+
"@bobfrankston/iflow-direct": "^0.1.13",
|
|
24
|
+
"@bobfrankston/iflow-node": "^0.1.3",
|
|
25
25
|
"@bobfrankston/miscinfo": "^1.0.8",
|
|
26
26
|
"@bobfrankston/oauthsupport": "^1.0.22",
|
|
27
|
-
"@bobfrankston/msger": "^0.1.
|
|
27
|
+
"@bobfrankston/msger": "^0.1.307",
|
|
28
28
|
"@capacitor/android": "^8.3.0",
|
|
29
29
|
"@capacitor/cli": "^8.3.0",
|
|
30
30
|
"@capacitor/core": "^8.3.0",
|
|
@@ -74,11 +74,11 @@
|
|
|
74
74
|
},
|
|
75
75
|
".transformedSnapshot": {
|
|
76
76
|
"dependencies": {
|
|
77
|
-
"@bobfrankston/iflow-direct": "^0.1.
|
|
78
|
-
"@bobfrankston/iflow-node": "^0.1.
|
|
77
|
+
"@bobfrankston/iflow-direct": "^0.1.13",
|
|
78
|
+
"@bobfrankston/iflow-node": "^0.1.3",
|
|
79
79
|
"@bobfrankston/miscinfo": "^1.0.8",
|
|
80
80
|
"@bobfrankston/oauthsupport": "^1.0.22",
|
|
81
|
-
"@bobfrankston/msger": "^0.1.
|
|
81
|
+
"@bobfrankston/msger": "^0.1.307",
|
|
82
82
|
"@capacitor/android": "^8.3.0",
|
|
83
83
|
"@capacitor/cli": "^8.3.0",
|
|
84
84
|
"@capacitor/core": "^8.3.0",
|
|
@@ -103,14 +103,19 @@ export declare class ImapManager extends EventEmitter {
|
|
|
103
103
|
private handleSyncError;
|
|
104
104
|
/** Sync just INBOX for each account (fast check for new mail) */
|
|
105
105
|
syncInbox(): Promise<void>;
|
|
106
|
-
/** Quick inbox check —
|
|
107
|
-
* If
|
|
108
|
-
|
|
106
|
+
/** Quick inbox check — per-account lightweight probe.
|
|
107
|
+
* If the probe value changed since last time, triggers an inbox sync.
|
|
108
|
+
* The marker is only advanced after a successful sync so that a failed
|
|
109
|
+
* sync doesn't eat the "new mail" signal and make us stop retrying. */
|
|
110
|
+
private lastInboxMarker;
|
|
109
111
|
private quickCheckRunning;
|
|
112
|
+
/** Shared quick-check skeleton: probe → compare → sync-if-changed → advance marker.
|
|
113
|
+
* `probe` returns the current marker value; `sync` runs only when it differs
|
|
114
|
+
* from the previously stored value. Marker is advanced only after sync resolves. */
|
|
115
|
+
private quickCheck;
|
|
110
116
|
/** Check a single account's inbox — uses its own connection, never blocked by sync */
|
|
111
117
|
quickInboxCheckAccount(accountId: string): Promise<void>;
|
|
112
|
-
|
|
113
|
-
private lastGmailInboxTop;
|
|
118
|
+
private quickImapCheck;
|
|
114
119
|
private quickGmailCheck;
|
|
115
120
|
/** Check all accounts (used by legacy callers) */
|
|
116
121
|
quickInboxCheck(): Promise<void>;
|
|
@@ -1139,39 +1139,61 @@ export class ImapManager extends EventEmitter {
|
|
|
1139
1139
|
this.inboxSyncing = false;
|
|
1140
1140
|
}
|
|
1141
1141
|
}
|
|
1142
|
-
/** Quick inbox check —
|
|
1143
|
-
* If
|
|
1144
|
-
|
|
1142
|
+
/** Quick inbox check — per-account lightweight probe.
|
|
1143
|
+
* If the probe value changed since last time, triggers an inbox sync.
|
|
1144
|
+
* The marker is only advanced after a successful sync so that a failed
|
|
1145
|
+
* sync doesn't eat the "new mail" signal and make us stop retrying. */
|
|
1146
|
+
lastInboxMarker = new Map();
|
|
1145
1147
|
quickCheckRunning = new Set(); // per-account guard
|
|
1146
|
-
/**
|
|
1147
|
-
|
|
1148
|
+
/** Shared quick-check skeleton: probe → compare → sync-if-changed → advance marker.
|
|
1149
|
+
* `probe` returns the current marker value; `sync` runs only when it differs
|
|
1150
|
+
* from the previously stored value. Marker is advanced only after sync resolves. */
|
|
1151
|
+
async quickCheck(accountId, probe, sync) {
|
|
1148
1152
|
if (this.quickCheckRunning.has(accountId))
|
|
1149
1153
|
return;
|
|
1150
1154
|
if (this.reauthenticating.has(accountId))
|
|
1151
1155
|
return;
|
|
1152
|
-
if (this.isGmailAccount(accountId)) {
|
|
1153
|
-
return this.quickGmailCheck(accountId);
|
|
1154
|
-
}
|
|
1155
1156
|
this.quickCheckRunning.add(accountId);
|
|
1156
|
-
let client = null;
|
|
1157
1157
|
try {
|
|
1158
|
-
const
|
|
1159
|
-
if (
|
|
1158
|
+
const current = await probe();
|
|
1159
|
+
if (current === null || current === "")
|
|
1160
1160
|
return;
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
this.lastInboxCounts.set(accountId, count);
|
|
1165
|
-
if (count !== prev) {
|
|
1166
|
-
console.log(` [check] ${accountId} INBOX: ${prev} → ${count}`);
|
|
1167
|
-
await this.syncFolder(accountId, inbox.id, client);
|
|
1161
|
+
const prev = this.lastInboxMarker.get(accountId);
|
|
1162
|
+
if (prev === undefined || current !== prev) {
|
|
1163
|
+
await sync(current, prev);
|
|
1168
1164
|
}
|
|
1169
|
-
|
|
1170
|
-
|
|
1165
|
+
// Only advance after sync succeeds — a thrown error skips this line
|
|
1166
|
+
// and the next tick will see the same delta and retry.
|
|
1167
|
+
this.lastInboxMarker.set(accountId, current);
|
|
1171
1168
|
}
|
|
1172
1169
|
catch {
|
|
1173
1170
|
// Lightweight check — silently ignore errors
|
|
1174
1171
|
}
|
|
1172
|
+
finally {
|
|
1173
|
+
this.quickCheckRunning.delete(accountId);
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
/** Check a single account's inbox — uses its own connection, never blocked by sync */
|
|
1177
|
+
async quickInboxCheckAccount(accountId) {
|
|
1178
|
+
if (this.isGmailAccount(accountId))
|
|
1179
|
+
return this.quickGmailCheck(accountId);
|
|
1180
|
+
return this.quickImapCheck(accountId);
|
|
1181
|
+
}
|
|
1182
|
+
async quickImapCheck(accountId) {
|
|
1183
|
+
const inbox = this.db.getFolders(accountId).find(f => f.specialUse === "inbox");
|
|
1184
|
+
if (!inbox)
|
|
1185
|
+
return;
|
|
1186
|
+
let client = null;
|
|
1187
|
+
try {
|
|
1188
|
+
await this.quickCheck(accountId, async () => {
|
|
1189
|
+
client = this.newClient(accountId);
|
|
1190
|
+
return await client.getMessagesCount("INBOX");
|
|
1191
|
+
}, async (count, prev) => {
|
|
1192
|
+
if (prev !== undefined)
|
|
1193
|
+
console.log(` [check] ${accountId} INBOX: ${prev} → ${count}`);
|
|
1194
|
+
await this.syncFolder(accountId, inbox.id, client);
|
|
1195
|
+
});
|
|
1196
|
+
}
|
|
1175
1197
|
finally {
|
|
1176
1198
|
if (client) {
|
|
1177
1199
|
try {
|
|
@@ -1179,44 +1201,31 @@ export class ImapManager extends EventEmitter {
|
|
|
1179
1201
|
}
|
|
1180
1202
|
catch { /* */ }
|
|
1181
1203
|
}
|
|
1182
|
-
this.quickCheckRunning.delete(accountId);
|
|
1183
1204
|
}
|
|
1184
1205
|
}
|
|
1185
|
-
/** Quick Gmail inbox check — one lightweight API call to check for new messages */
|
|
1186
|
-
lastGmailInboxTop = new Map();
|
|
1187
1206
|
async quickGmailCheck(accountId) {
|
|
1188
|
-
|
|
1207
|
+
const config = this.configs.get(accountId);
|
|
1208
|
+
if (!config?.tokenProvider)
|
|
1189
1209
|
return;
|
|
1190
|
-
this.
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
return;
|
|
1210
|
+
const inbox = this.db.getFolders(accountId).find(f => f.specialUse === "inbox");
|
|
1211
|
+
if (!inbox)
|
|
1212
|
+
return;
|
|
1213
|
+
await this.quickCheck(accountId, async () => {
|
|
1195
1214
|
const token = await config.tokenProvider();
|
|
1196
|
-
// Single API call: get just the first message ID
|
|
1197
1215
|
const res = await globalThis.fetch(`https://gmail.googleapis.com/gmail/v1/users/me/messages?q=in:inbox&maxResults=1`, { headers: { "Authorization": `Bearer ${token}` } });
|
|
1198
1216
|
if (!res.ok)
|
|
1199
|
-
return;
|
|
1217
|
+
return null;
|
|
1200
1218
|
const data = await res.json();
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
if (topId && topId !== prev) {
|
|
1219
|
+
return data.messages?.[0]?.id || null;
|
|
1220
|
+
}, async (_topId, prev) => {
|
|
1221
|
+
if (prev !== undefined)
|
|
1205
1222
|
console.log(` [check] ${accountId} INBOX: new message detected`);
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
await api.close();
|
|
1213
|
-
}
|
|
1214
|
-
}
|
|
1215
|
-
}
|
|
1216
|
-
catch { /* lightweight — ignore errors */ }
|
|
1217
|
-
finally {
|
|
1218
|
-
this.quickCheckRunning.delete(accountId);
|
|
1219
|
-
}
|
|
1223
|
+
const api = this.getGmailProvider(accountId);
|
|
1224
|
+
await this.syncFolderViaApi(accountId, inbox, api);
|
|
1225
|
+
this.db.recalcFolderCounts(inbox.id);
|
|
1226
|
+
this.emit("folderCountsChanged", accountId, {});
|
|
1227
|
+
await api.close();
|
|
1228
|
+
});
|
|
1220
1229
|
}
|
|
1221
1230
|
/** Check all accounts (used by legacy callers) */
|
|
1222
1231
|
async quickInboxCheck() {
|
|
@@ -45,7 +45,11 @@ export class GmailApiProvider {
|
|
|
45
45
|
}
|
|
46
46
|
async fetch(path, options = {}) {
|
|
47
47
|
const token = await this.tokenProvider();
|
|
48
|
-
|
|
48
|
+
const maxAttempts = 6;
|
|
49
|
+
const baseDelayMs = 1000;
|
|
50
|
+
const maxDelayMs = 60_000;
|
|
51
|
+
let lastStatus = 0;
|
|
52
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
49
53
|
const res = await globalThis.fetch(`${API}${path}`, {
|
|
50
54
|
...options,
|
|
51
55
|
headers: {
|
|
@@ -55,9 +59,24 @@ export class GmailApiProvider {
|
|
|
55
59
|
},
|
|
56
60
|
});
|
|
57
61
|
if (res.status === 429 || res.status >= 500) {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
62
|
+
lastStatus = res.status;
|
|
63
|
+
// Honor Retry-After if present (seconds or HTTP-date)
|
|
64
|
+
const retryAfter = res.headers.get("Retry-After");
|
|
65
|
+
let delay = baseDelayMs * Math.pow(2, attempt);
|
|
66
|
+
if (retryAfter) {
|
|
67
|
+
const asInt = parseInt(retryAfter, 10);
|
|
68
|
+
if (!isNaN(asInt))
|
|
69
|
+
delay = asInt * 1000;
|
|
70
|
+
else {
|
|
71
|
+
const when = Date.parse(retryAfter);
|
|
72
|
+
if (!isNaN(when))
|
|
73
|
+
delay = Math.max(0, when - Date.now());
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// Full jitter to avoid synchronized retries
|
|
77
|
+
delay = Math.min(maxDelayMs, delay);
|
|
78
|
+
delay = Math.floor(delay * (0.5 + Math.random() * 0.5));
|
|
79
|
+
console.log(` [gmail] ${res.status} (attempt ${attempt + 1}/${maxAttempts}), waiting ${(delay / 1000).toFixed(1)}s${retryAfter ? ` (Retry-After: ${retryAfter})` : ""}...`);
|
|
61
80
|
await new Promise(r => setTimeout(r, delay));
|
|
62
81
|
continue;
|
|
63
82
|
}
|
|
@@ -67,7 +86,7 @@ export class GmailApiProvider {
|
|
|
67
86
|
}
|
|
68
87
|
return res.json();
|
|
69
88
|
}
|
|
70
|
-
throw new Error(
|
|
89
|
+
throw new Error(`Gmail API: failed after ${maxAttempts} retries (last status ${lastStatus})`);
|
|
71
90
|
}
|
|
72
91
|
async listFolders() {
|
|
73
92
|
const data = await this.fetch("/labels");
|
|
@@ -618,6 +618,13 @@ export async function initAndroid() {
|
|
|
618
618
|
setTimeout(() => {
|
|
619
619
|
syncManager.syncAll().catch(e => console.error(`[android] Sync error: ${e.message}`));
|
|
620
620
|
}, 1000);
|
|
621
|
+
// Periodic re-sync every 2 minutes (no IDLE on Android, so poll)
|
|
622
|
+
const SYNC_INTERVAL_MS = 2 * 60 * 1000;
|
|
623
|
+
setInterval(() => {
|
|
624
|
+
console.log("[sync] periodic poll");
|
|
625
|
+
vlog("periodic sync poll");
|
|
626
|
+
syncManager.syncAll().catch(e => console.error(`[android] Periodic sync error: ${e.message}`));
|
|
627
|
+
}, SYNC_INTERVAL_MS);
|
|
621
628
|
console.log("[android] Initialization complete");
|
|
622
629
|
emitEvent({ type: "connected" });
|
|
623
630
|
}
|
|
@@ -5,14 +5,23 @@
|
|
|
5
5
|
* The native shell (MAUI) exposes TCP via window._nativeBridge.tcp.*; we alias it
|
|
6
6
|
* to window.msgapi in initAndroid() because iflow-direct's BridgeTransport expects
|
|
7
7
|
* that global name.
|
|
8
|
+
*
|
|
9
|
+
* Includes automatic retry on broken pipe / connection errors: if an operation fails
|
|
10
|
+
* with a connection-related error, we create a fresh client and retry once.
|
|
8
11
|
*/
|
|
9
12
|
import { type ImapClientConfig } from "@bobfrankston/iflow-direct";
|
|
10
13
|
import type { MailProvider, ProviderFolder, ProviderMessage, FetchOptions } from "./provider-types.js";
|
|
11
14
|
export declare class ImapWebProvider implements MailProvider {
|
|
12
15
|
private client;
|
|
16
|
+
private config;
|
|
17
|
+
private transportFactory;
|
|
13
18
|
private specialFolders;
|
|
14
19
|
private folderListCache;
|
|
15
20
|
constructor(config: ImapClientConfig);
|
|
21
|
+
/** Create a fresh client (after broken pipe / connection error) */
|
|
22
|
+
private reconnect;
|
|
23
|
+
/** Run an operation with one retry on connection error */
|
|
24
|
+
private withRetry;
|
|
16
25
|
listFolders(): Promise<ProviderFolder[]>;
|
|
17
26
|
fetchSince(folder: string, sinceUid: number, options?: FetchOptions): Promise<ProviderMessage[]>;
|
|
18
27
|
fetchByDate(folder: string, since: Date, before: Date, options?: FetchOptions, onChunk?: (msgs: ProviderMessage[]) => void): Promise<ProviderMessage[]>;
|
|
@@ -5,6 +5,9 @@
|
|
|
5
5
|
* The native shell (MAUI) exposes TCP via window._nativeBridge.tcp.*; we alias it
|
|
6
6
|
* to window.msgapi in initAndroid() because iflow-direct's BridgeTransport expects
|
|
7
7
|
* that global name.
|
|
8
|
+
*
|
|
9
|
+
* Includes automatic retry on broken pipe / connection errors: if an operation fails
|
|
10
|
+
* with a connection-related error, we create a fresh client and retry once.
|
|
8
11
|
*/
|
|
9
12
|
import { CompatImapClient, BridgeTransport } from "@bobfrankston/iflow-direct";
|
|
10
13
|
/**
|
|
@@ -54,16 +57,50 @@ function toProviderMessage(m) {
|
|
|
54
57
|
source: m.source || "",
|
|
55
58
|
};
|
|
56
59
|
}
|
|
60
|
+
/** Check if an error is a connection/broken-pipe error worth retrying */
|
|
61
|
+
function isConnectionError(e) {
|
|
62
|
+
const msg = (e?.message || "").toLowerCase();
|
|
63
|
+
return msg.includes("broken pipe") || msg.includes("not connected") ||
|
|
64
|
+
msg.includes("connection") || msg.includes("socket") ||
|
|
65
|
+
msg.includes("timeout") || msg.includes("econnreset") ||
|
|
66
|
+
msg.includes("epipe") || msg.includes("closed");
|
|
67
|
+
}
|
|
57
68
|
export class ImapWebProvider {
|
|
58
69
|
client;
|
|
70
|
+
config;
|
|
71
|
+
transportFactory;
|
|
59
72
|
specialFolders = {};
|
|
60
73
|
folderListCache = null;
|
|
61
74
|
constructor(config) {
|
|
62
|
-
|
|
63
|
-
this.
|
|
75
|
+
this.config = config;
|
|
76
|
+
this.transportFactory = () => new BridgeTransport();
|
|
77
|
+
this.client = new CompatImapClient(config, this.transportFactory);
|
|
78
|
+
}
|
|
79
|
+
/** Create a fresh client (after broken pipe / connection error) */
|
|
80
|
+
reconnect() {
|
|
81
|
+
console.log("[imap-web] reconnecting after connection error");
|
|
82
|
+
try {
|
|
83
|
+
this.client.logout();
|
|
84
|
+
}
|
|
85
|
+
catch { /* ignore */ }
|
|
86
|
+
this.client = new CompatImapClient(this.config, this.transportFactory);
|
|
87
|
+
}
|
|
88
|
+
/** Run an operation with one retry on connection error */
|
|
89
|
+
async withRetry(op, label) {
|
|
90
|
+
try {
|
|
91
|
+
return await op();
|
|
92
|
+
}
|
|
93
|
+
catch (e) {
|
|
94
|
+
if (isConnectionError(e)) {
|
|
95
|
+
console.warn(`[imap-web] ${label}: ${e.message} — reconnecting and retrying`);
|
|
96
|
+
this.reconnect();
|
|
97
|
+
return await op();
|
|
98
|
+
}
|
|
99
|
+
throw e;
|
|
100
|
+
}
|
|
64
101
|
}
|
|
65
102
|
async listFolders() {
|
|
66
|
-
const native = await this.client.getFolderList();
|
|
103
|
+
const native = await this.withRetry(() => this.client.getFolderList(), "listFolders");
|
|
67
104
|
const special = this.client.getSpecialFolders(native);
|
|
68
105
|
this.specialFolders = special;
|
|
69
106
|
const result = native.map(f => toProviderFolder(f, this.specialFolders));
|
|
@@ -71,27 +108,27 @@ export class ImapWebProvider {
|
|
|
71
108
|
return result;
|
|
72
109
|
}
|
|
73
110
|
async fetchSince(folder, sinceUid, options) {
|
|
74
|
-
const msgs = await this.client.fetchMessagesSinceUid(folder, sinceUid, { source: !!options?.source });
|
|
111
|
+
const msgs = await this.withRetry(() => this.client.fetchMessagesSinceUid(folder, sinceUid, { source: !!options?.source }), `fetchSince(${folder})`);
|
|
75
112
|
return msgs.map(toProviderMessage);
|
|
76
113
|
}
|
|
77
114
|
async fetchByDate(folder, since, before, options, onChunk) {
|
|
78
115
|
const wrappedChunk = onChunk ? (raw) => onChunk(raw.map(toProviderMessage)) : undefined;
|
|
79
|
-
const msgs = await this.client.fetchMessageByDate(folder, since, before, { source: !!options?.source }, wrappedChunk);
|
|
116
|
+
const msgs = await this.withRetry(() => this.client.fetchMessageByDate(folder, since, before, { source: !!options?.source }, wrappedChunk), `fetchByDate(${folder})`);
|
|
80
117
|
return msgs.map(toProviderMessage);
|
|
81
118
|
}
|
|
82
119
|
async fetchByUids(folder, uids, options) {
|
|
83
120
|
if (!uids.length)
|
|
84
121
|
return [];
|
|
85
122
|
const range = uids.join(",");
|
|
86
|
-
const msgs = await this.client.fetchMessages(folder, range, { source: !!options?.source });
|
|
123
|
+
const msgs = await this.withRetry(() => this.client.fetchMessages(folder, range, { source: !!options?.source }), `fetchByUids(${folder})`);
|
|
87
124
|
return msgs.map(toProviderMessage);
|
|
88
125
|
}
|
|
89
126
|
async fetchOne(folder, uid, options) {
|
|
90
|
-
const msg = await this.client.fetchMessageByUid(folder, uid, { source: !!options?.source });
|
|
127
|
+
const msg = await this.withRetry(() => this.client.fetchMessageByUid(folder, uid, { source: !!options?.source }), `fetchOne(${folder}/${uid})`);
|
|
91
128
|
return msg ? toProviderMessage(msg) : null;
|
|
92
129
|
}
|
|
93
130
|
async getUids(folder) {
|
|
94
|
-
return this.client.getUids(folder);
|
|
131
|
+
return this.withRetry(() => this.client.getUids(folder), `getUids(${folder})`);
|
|
95
132
|
}
|
|
96
133
|
async close() {
|
|
97
134
|
try {
|