@bobfrankston/mailx 1.0.168 → 1.0.171
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 +3 -5
- package/client/lib/api-client.js +47 -35
- package/client/lib/mailxapi.js +2 -0
- package/package.json +3 -3
- package/packages/mailx-imap/index.js +43 -20
- package/packages/mailx-service/jsonrpc.js +5 -0
- package/packages/mailx-settings/index.d.ts +3 -0
- package/packages/mailx-settings/index.js +6 -0
- package/packages/mailx-types/index.d.ts +1 -0
package/client/app.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { initFolderTree, refreshFolderTree, updateFolderCounts } from "./components/folder-tree.js";
|
|
6
6
|
import { initMessageList, loadMessages, loadUnifiedInbox, loadSearchResults, reloadCurrentFolder, getSelectedMessages } from "./components/message-list.js";
|
|
7
7
|
import { showMessage, getCurrentMessage } from "./components/message-viewer.js";
|
|
8
|
-
import { connectWebSocket, onWsEvent, triggerSync, getAccounts, getFolders, deleteMessages, undeleteMessage, restartServer, rebuildServer, getSyncPending, getVersion, getSettings, saveSettings, getAutocompleteSettings, saveAutocompleteSettings, repairAccounts } from "./lib/api-client.js";
|
|
8
|
+
import { connectWebSocket, onWsEvent, triggerSync, syncAccount, reauthenticate, getAccounts, getFolders, deleteMessages, undeleteMessage, restartServer, rebuildServer, getSyncPending, getVersion, getSettings, saveSettings, getAutocompleteSettings, saveAutocompleteSettings, repairAccounts } from "./lib/api-client.js";
|
|
9
9
|
// ── New message badge (favicon + title) ──
|
|
10
10
|
let baseTitle = "mailx";
|
|
11
11
|
let lastSeenCount = 0;
|
|
@@ -641,8 +641,7 @@ onWsEvent((event) => {
|
|
|
641
641
|
btn.disabled = true;
|
|
642
642
|
btn.textContent = "Authenticating...";
|
|
643
643
|
try {
|
|
644
|
-
const
|
|
645
|
-
const data = await res.json();
|
|
644
|
+
const data = await reauthenticate(event.accountId);
|
|
646
645
|
if (data.ok) {
|
|
647
646
|
hideAlert();
|
|
648
647
|
const acctEl = document.getElementById("status-accounts");
|
|
@@ -668,8 +667,7 @@ onWsEvent((event) => {
|
|
|
668
667
|
btn.disabled = true;
|
|
669
668
|
btn.textContent = "Syncing...";
|
|
670
669
|
try {
|
|
671
|
-
const
|
|
672
|
-
const data = await res.json();
|
|
670
|
+
const data = await syncAccount(event.accountId);
|
|
673
671
|
if (data.ok) {
|
|
674
672
|
hideAlert();
|
|
675
673
|
const acctEl = document.getElementById("status-accounts");
|
package/client/lib/api-client.js
CHANGED
|
@@ -6,7 +6,9 @@
|
|
|
6
6
|
* All server operations MUST go through these centralized methods.
|
|
7
7
|
* Never use fetch("/api/...") directly in components.
|
|
8
8
|
*/
|
|
9
|
-
|
|
9
|
+
// In popup windows (compose), mailxapi isn't injected — fall back to opener's bridge
|
|
10
|
+
const _ipc = typeof mailxapi !== "undefined" ? mailxapi : window.opener?.mailxapi;
|
|
11
|
+
const hasIPC = _ipc?.isApp === true;
|
|
10
12
|
// ── HTTP fallback ──
|
|
11
13
|
// Abort controller for message-list requests — cancel stale fetches when folder changes
|
|
12
14
|
let messageListAbort = null;
|
|
@@ -44,29 +46,29 @@ async function api(path, options) {
|
|
|
44
46
|
// ── API Methods (IPC or HTTP) ──
|
|
45
47
|
export function getAccounts() {
|
|
46
48
|
if (hasIPC)
|
|
47
|
-
return
|
|
49
|
+
return _ipc.getAccounts();
|
|
48
50
|
return api("/accounts");
|
|
49
51
|
}
|
|
50
52
|
export function getFolders(accountId) {
|
|
51
53
|
if (hasIPC)
|
|
52
|
-
return
|
|
54
|
+
return _ipc.getFolders(accountId);
|
|
53
55
|
return api(`/folders/${accountId}`);
|
|
54
56
|
}
|
|
55
57
|
export function getMessages(accountId, folderId, page = 1, pageSize = 50) {
|
|
56
58
|
if (hasIPC)
|
|
57
|
-
return
|
|
59
|
+
return _ipc.getMessages(accountId, folderId, page, pageSize);
|
|
58
60
|
const signal = newMessageListSignal();
|
|
59
61
|
return api(`/messages/${accountId}/${folderId}?page=${page}&pageSize=${pageSize}`, { signal });
|
|
60
62
|
}
|
|
61
63
|
export function getUnifiedInbox(page = 1, pageSize = 50) {
|
|
62
64
|
if (hasIPC)
|
|
63
|
-
return
|
|
65
|
+
return _ipc.getUnifiedInbox(page, pageSize);
|
|
64
66
|
const signal = newMessageListSignal();
|
|
65
67
|
return api(`/messages/unified/inbox?page=${page}&pageSize=${pageSize}`, { signal });
|
|
66
68
|
}
|
|
67
69
|
export function searchMessages(query, page = 1, pageSize = 50, scope = "all", accountId = "", folderId = 0) {
|
|
68
70
|
if (hasIPC)
|
|
69
|
-
return
|
|
71
|
+
return _ipc.searchMessages(query, page, pageSize);
|
|
70
72
|
const params = new URLSearchParams({ q: query, page: String(page), pageSize: String(pageSize), scope });
|
|
71
73
|
if (scope === "current" && accountId) {
|
|
72
74
|
params.set("accountId", accountId);
|
|
@@ -80,7 +82,7 @@ export function searchMessages(query, page = 1, pageSize = 50, scope = "all", ac
|
|
|
80
82
|
}
|
|
81
83
|
export function getMessage(accountId, uid, allowRemote = false, folderId) {
|
|
82
84
|
if (hasIPC)
|
|
83
|
-
return
|
|
85
|
+
return _ipc.getMessage(accountId, uid, allowRemote, folderId);
|
|
84
86
|
const params = new URLSearchParams();
|
|
85
87
|
if (allowRemote)
|
|
86
88
|
params.set("allowRemote", "true");
|
|
@@ -91,7 +93,7 @@ export function getMessage(accountId, uid, allowRemote = false, folderId) {
|
|
|
91
93
|
}
|
|
92
94
|
export function updateFlags(accountId, uid, flags) {
|
|
93
95
|
if (hasIPC)
|
|
94
|
-
return
|
|
96
|
+
return _ipc.updateFlags(accountId, uid, flags);
|
|
95
97
|
return api(`/message/${accountId}/${uid}/flags`, {
|
|
96
98
|
method: "PATCH",
|
|
97
99
|
body: JSON.stringify({ flags })
|
|
@@ -99,22 +101,32 @@ export function updateFlags(accountId, uid, flags) {
|
|
|
99
101
|
}
|
|
100
102
|
export function triggerSync() {
|
|
101
103
|
if (hasIPC)
|
|
102
|
-
return
|
|
104
|
+
return _ipc.syncAll();
|
|
103
105
|
return api("/sync", { method: "POST" });
|
|
104
106
|
}
|
|
107
|
+
export function syncAccount(accountId) {
|
|
108
|
+
if (hasIPC)
|
|
109
|
+
return _ipc.syncAccount(accountId);
|
|
110
|
+
return api(`/sync/${accountId}`, { method: "POST" });
|
|
111
|
+
}
|
|
112
|
+
export function reauthenticate(accountId) {
|
|
113
|
+
if (hasIPC)
|
|
114
|
+
return _ipc.reauthenticate(accountId);
|
|
115
|
+
return api(`/reauth/${accountId}`, { method: "POST" });
|
|
116
|
+
}
|
|
105
117
|
export function getSyncPending() {
|
|
106
118
|
if (hasIPC)
|
|
107
|
-
return
|
|
119
|
+
return _ipc.getSyncPending();
|
|
108
120
|
return api("/sync/pending");
|
|
109
121
|
}
|
|
110
122
|
export function searchContacts(query) {
|
|
111
123
|
if (hasIPC)
|
|
112
|
-
return
|
|
124
|
+
return _ipc.searchContacts(query);
|
|
113
125
|
return api(`/contacts?q=${encodeURIComponent(query)}`);
|
|
114
126
|
}
|
|
115
127
|
export function allowRemoteContent(type, value) {
|
|
116
128
|
if (hasIPC)
|
|
117
|
-
return
|
|
129
|
+
return _ipc.allowRemoteContent(type, value);
|
|
118
130
|
return api("/settings/allow-remote", {
|
|
119
131
|
method: "POST",
|
|
120
132
|
body: JSON.stringify({ type, value })
|
|
@@ -122,14 +134,14 @@ export function allowRemoteContent(type, value) {
|
|
|
122
134
|
}
|
|
123
135
|
export function deleteMessage(accountId, uid) {
|
|
124
136
|
if (hasIPC)
|
|
125
|
-
return
|
|
137
|
+
return _ipc.deleteMessage?.(accountId, uid);
|
|
126
138
|
return api(`/message/${accountId}/${uid}`, { method: "DELETE" });
|
|
127
139
|
}
|
|
128
140
|
export function deleteMessages(accountId, uids) {
|
|
129
141
|
if (uids.length === 1)
|
|
130
142
|
return deleteMessage(accountId, uids[0]);
|
|
131
143
|
if (hasIPC)
|
|
132
|
-
return
|
|
144
|
+
return _ipc.deleteMessages?.(accountId, uids);
|
|
133
145
|
return api("/messages/delete", {
|
|
134
146
|
method: "POST", body: JSON.stringify({ accountId, uids })
|
|
135
147
|
});
|
|
@@ -138,7 +150,7 @@ export function moveMessages(accountId, uids, targetFolderId, targetAccountId) {
|
|
|
138
150
|
if (uids.length === 1)
|
|
139
151
|
return moveMessage(accountId, uids[0], targetFolderId, targetAccountId);
|
|
140
152
|
if (hasIPC)
|
|
141
|
-
return
|
|
153
|
+
return _ipc.moveMessages?.(accountId, uids, targetFolderId, targetAccountId);
|
|
142
154
|
const body = { accountId, uids, targetFolderId };
|
|
143
155
|
if (targetAccountId)
|
|
144
156
|
body.targetAccountId = targetAccountId;
|
|
@@ -148,7 +160,7 @@ export function moveMessages(accountId, uids, targetFolderId, targetAccountId) {
|
|
|
148
160
|
}
|
|
149
161
|
export function undeleteMessage(accountId, uid, folderId) {
|
|
150
162
|
if (hasIPC)
|
|
151
|
-
return
|
|
163
|
+
return _ipc.undeleteMessage?.(accountId, uid, folderId);
|
|
152
164
|
return api(`/message/${accountId}/${uid}/undelete`, {
|
|
153
165
|
method: "POST",
|
|
154
166
|
body: JSON.stringify({ folderId })
|
|
@@ -156,7 +168,7 @@ export function undeleteMessage(accountId, uid, folderId) {
|
|
|
156
168
|
}
|
|
157
169
|
export function moveMessage(accountId, uid, targetFolderId, targetAccountId) {
|
|
158
170
|
if (hasIPC)
|
|
159
|
-
return
|
|
171
|
+
return _ipc.moveMessage?.(accountId, uid, targetFolderId, targetAccountId);
|
|
160
172
|
const body = { targetFolderId };
|
|
161
173
|
if (targetAccountId)
|
|
162
174
|
body.targetAccountId = targetAccountId;
|
|
@@ -167,7 +179,7 @@ export function moveMessage(accountId, uid, targetFolderId, targetAccountId) {
|
|
|
167
179
|
}
|
|
168
180
|
export function restartServer() {
|
|
169
181
|
if (hasIPC)
|
|
170
|
-
return
|
|
182
|
+
return _ipc.restart?.();
|
|
171
183
|
return api("/restart", { method: "POST" }).catch(() => { });
|
|
172
184
|
}
|
|
173
185
|
export function rebuildServer() {
|
|
@@ -175,12 +187,12 @@ export function rebuildServer() {
|
|
|
175
187
|
}
|
|
176
188
|
export function markFolderRead(accountId, folderId) {
|
|
177
189
|
if (hasIPC)
|
|
178
|
-
return
|
|
190
|
+
return _ipc.markFolderRead?.(accountId, folderId);
|
|
179
191
|
return api(`/folder/${accountId}/${folderId}/mark-read`, { method: "POST" });
|
|
180
192
|
}
|
|
181
193
|
export function createFolder(accountId, parentPath, name) {
|
|
182
194
|
if (hasIPC)
|
|
183
|
-
return
|
|
195
|
+
return _ipc.createFolder?.(accountId, parentPath, name);
|
|
184
196
|
return api(`/folder/${accountId}`, {
|
|
185
197
|
method: "POST",
|
|
186
198
|
body: JSON.stringify({ parentPath, name })
|
|
@@ -188,7 +200,7 @@ export function createFolder(accountId, parentPath, name) {
|
|
|
188
200
|
}
|
|
189
201
|
export function renameFolder(accountId, folderId, newName) {
|
|
190
202
|
if (hasIPC)
|
|
191
|
-
return
|
|
203
|
+
return _ipc.renameFolder?.(accountId, folderId, newName);
|
|
192
204
|
return api(`/folder/${accountId}/${folderId}/rename`, {
|
|
193
205
|
method: "POST",
|
|
194
206
|
body: JSON.stringify({ newName })
|
|
@@ -196,22 +208,22 @@ export function renameFolder(accountId, folderId, newName) {
|
|
|
196
208
|
}
|
|
197
209
|
export function deleteFolder(accountId, folderId) {
|
|
198
210
|
if (hasIPC)
|
|
199
|
-
return
|
|
211
|
+
return _ipc.deleteFolder?.(accountId, folderId);
|
|
200
212
|
return api(`/folder/${accountId}/${folderId}`, { method: "DELETE" });
|
|
201
213
|
}
|
|
202
214
|
export function emptyFolder(accountId, folderId) {
|
|
203
215
|
if (hasIPC)
|
|
204
|
-
return
|
|
216
|
+
return _ipc.emptyFolder?.(accountId, folderId);
|
|
205
217
|
return api(`/folder/${accountId}/${folderId}/empty`, { method: "POST" });
|
|
206
218
|
}
|
|
207
219
|
export function sendMessage(body) {
|
|
208
220
|
if (hasIPC)
|
|
209
|
-
return
|
|
221
|
+
return _ipc.sendMessage?.(body);
|
|
210
222
|
return api("/send", { method: "POST", body: JSON.stringify(body) });
|
|
211
223
|
}
|
|
212
224
|
export function saveDraft(body) {
|
|
213
225
|
if (hasIPC)
|
|
214
|
-
return
|
|
226
|
+
return _ipc.saveDraft?.(body);
|
|
215
227
|
return api("/draft", { method: "POST", body: JSON.stringify(body) });
|
|
216
228
|
}
|
|
217
229
|
const eventHandlers = [];
|
|
@@ -221,7 +233,7 @@ export function onEvent(handler) {
|
|
|
221
233
|
export function connectEvents() {
|
|
222
234
|
if (hasIPC) {
|
|
223
235
|
// IPC events come via mailxapi.onEvent
|
|
224
|
-
|
|
236
|
+
_ipc.onEvent((event) => {
|
|
225
237
|
for (const h of eventHandlers)
|
|
226
238
|
h(event);
|
|
227
239
|
});
|
|
@@ -246,47 +258,47 @@ export function connectEvents() {
|
|
|
246
258
|
// ── Autocomplete ──
|
|
247
259
|
export function autocomplete(body, signal) {
|
|
248
260
|
if (hasIPC)
|
|
249
|
-
return
|
|
261
|
+
return _ipc.autocomplete?.(body);
|
|
250
262
|
return api("/autocomplete", { method: "POST", body: JSON.stringify(body), signal });
|
|
251
263
|
}
|
|
252
264
|
export function getAutocompleteSettings() {
|
|
253
265
|
if (hasIPC)
|
|
254
|
-
return
|
|
266
|
+
return _ipc.getAutocompleteSettings?.();
|
|
255
267
|
return api("/autocomplete/settings");
|
|
256
268
|
}
|
|
257
269
|
export function saveAutocompleteSettings(settings) {
|
|
258
270
|
if (hasIPC)
|
|
259
|
-
return
|
|
271
|
+
return _ipc.saveAutocompleteSettings?.(settings);
|
|
260
272
|
return api("/autocomplete/settings", { method: "POST", body: JSON.stringify(settings) });
|
|
261
273
|
}
|
|
262
274
|
export function getVersion() {
|
|
263
275
|
if (hasIPC)
|
|
264
|
-
return
|
|
276
|
+
return _ipc.getVersion();
|
|
265
277
|
return api("/version");
|
|
266
278
|
}
|
|
267
279
|
export function getSettings() {
|
|
268
280
|
if (hasIPC)
|
|
269
|
-
return
|
|
281
|
+
return _ipc.getSettings();
|
|
270
282
|
return api("/settings");
|
|
271
283
|
}
|
|
272
284
|
export function saveSettings(settings) {
|
|
273
285
|
if (hasIPC)
|
|
274
|
-
return
|
|
286
|
+
return _ipc.saveSettingsData?.(settings);
|
|
275
287
|
return api("/settings", { method: "PUT", body: JSON.stringify(settings) });
|
|
276
288
|
}
|
|
277
289
|
export function repairAccounts() {
|
|
278
290
|
if (hasIPC)
|
|
279
|
-
return
|
|
291
|
+
return _ipc.repairAccounts?.();
|
|
280
292
|
return api("/repair-accounts", { method: "POST" });
|
|
281
293
|
}
|
|
282
294
|
export function deleteDraft(accountId, draftUid) {
|
|
283
295
|
if (hasIPC)
|
|
284
|
-
return
|
|
296
|
+
return _ipc.deleteDraft?.(accountId, draftUid);
|
|
285
297
|
return api("/draft", { method: "DELETE", body: JSON.stringify({ accountId, draftUid }) });
|
|
286
298
|
}
|
|
287
299
|
export function setupAccount(name, email, password) {
|
|
288
300
|
if (hasIPC)
|
|
289
|
-
return
|
|
301
|
+
return _ipc.setupAccount?.(name, email, password);
|
|
290
302
|
return api("/setup", { method: "POST", body: JSON.stringify({ name, email, password }) });
|
|
291
303
|
}
|
|
292
304
|
// Legacy exports for backward compatibility
|
package/client/lib/mailxapi.js
CHANGED
|
@@ -103,7 +103,9 @@
|
|
|
103
103
|
|
|
104
104
|
// Sync
|
|
105
105
|
syncAll: function() { return callNode("syncAll"); },
|
|
106
|
+
syncAccount: function(accountId) { return callNode("syncAccount", { accountId: accountId }); },
|
|
106
107
|
getSyncPending: function() { return callNode("getSyncPending"); },
|
|
108
|
+
reauthenticate: function(accountId) { return callNode("reauthenticate", { accountId: accountId }); },
|
|
107
109
|
|
|
108
110
|
// Bulk operations
|
|
109
111
|
deleteMessages: function(accountId, uids) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bobfrankston/mailx",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.171",
|
|
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.
|
|
23
|
+
"@bobfrankston/iflow-direct": "^0.1.3",
|
|
24
24
|
"@bobfrankston/iflow-node": "^0.1.1",
|
|
25
25
|
"@bobfrankston/miscinfo": "^1.0.7",
|
|
26
26
|
"@bobfrankston/oauthsupport": "^1.0.20",
|
|
27
|
-
"@bobfrankston/msger": "^0.1.
|
|
27
|
+
"@bobfrankston/msger": "^0.1.220",
|
|
28
28
|
"@capacitor/android": "^8.3.0",
|
|
29
29
|
"@capacitor/cli": "^8.3.0",
|
|
30
30
|
"@capacitor/core": "^8.3.0",
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import { createAutoImapConfig, CompatImapClient } from "@bobfrankston/iflow-direct";
|
|
7
7
|
import { authenticateOAuth } from "@bobfrankston/oauthsupport";
|
|
8
8
|
import { FileMessageStore } from "@bobfrankston/mailx-store";
|
|
9
|
-
import { loadSettings, getStorePath, getConfigDir, getHistoryDays } from "@bobfrankston/mailx-settings";
|
|
9
|
+
import { loadSettings, getStorePath, getConfigDir, getHistoryDays, getPrefetch } from "@bobfrankston/mailx-settings";
|
|
10
10
|
import { EventEmitter } from "node:events";
|
|
11
11
|
import * as fs from "node:fs";
|
|
12
12
|
import * as path from "node:path";
|
|
@@ -412,7 +412,7 @@ export class ImapManager extends EventEmitter {
|
|
|
412
412
|
return dbFolders;
|
|
413
413
|
}
|
|
414
414
|
/** Store a batch of messages to DB immediately — used by onChunk for incremental sync */
|
|
415
|
-
storeMessages(accountId, folderId, folder, msgs, highestUid) {
|
|
415
|
+
async storeMessages(accountId, folderId, folder, msgs, highestUid) {
|
|
416
416
|
let stored = 0;
|
|
417
417
|
this.db.beginTransaction();
|
|
418
418
|
try {
|
|
@@ -426,7 +426,14 @@ export class ImapManager extends EventEmitter {
|
|
|
426
426
|
continue; // already have it
|
|
427
427
|
const source = msg.source || "";
|
|
428
428
|
let bodyPath = "";
|
|
429
|
-
|
|
429
|
+
let preview = "";
|
|
430
|
+
let hasAttachments = false;
|
|
431
|
+
if (source) {
|
|
432
|
+
bodyPath = await this.bodyStore.putMessage(accountId, folderId, msg.uid, Buffer.from(source, "utf-8"));
|
|
433
|
+
const parsed = await extractPreview(source);
|
|
434
|
+
preview = parsed.preview;
|
|
435
|
+
hasAttachments = parsed.hasAttachments;
|
|
436
|
+
}
|
|
430
437
|
const flags = [];
|
|
431
438
|
if (msg.seen)
|
|
432
439
|
flags.push("\\Seen");
|
|
@@ -444,7 +451,7 @@ export class ImapManager extends EventEmitter {
|
|
|
444
451
|
from: toEmailAddress(msg.from?.[0] || {}),
|
|
445
452
|
to: toEmailAddresses(msg.to || []),
|
|
446
453
|
cc: toEmailAddresses(msg.cc || []),
|
|
447
|
-
flags, size: msg.size || 0, hasAttachments
|
|
454
|
+
flags, size: msg.size || 0, hasAttachments, preview, bodyPath
|
|
448
455
|
});
|
|
449
456
|
stored++;
|
|
450
457
|
}
|
|
@@ -460,6 +467,7 @@ export class ImapManager extends EventEmitter {
|
|
|
460
467
|
async syncFolder(accountId, folderId, client) {
|
|
461
468
|
if (!client)
|
|
462
469
|
client = this.createClient(accountId);
|
|
470
|
+
const prefetch = getPrefetch();
|
|
463
471
|
const folders = this.db.getFolders(accountId);
|
|
464
472
|
const folder = folders.find(f => f.id === folderId);
|
|
465
473
|
if (!folder)
|
|
@@ -477,8 +485,8 @@ export class ImapManager extends EventEmitter {
|
|
|
477
485
|
? new Date(Date.now() - effectiveDays * 86400000)
|
|
478
486
|
: new Date(0);
|
|
479
487
|
if (highestUid > 0) {
|
|
480
|
-
// Incremental: fetch new messages —
|
|
481
|
-
const fetched = await client.fetchMessagesSinceUid(folder.path, highestUid, { source:
|
|
488
|
+
// Incremental: fetch new messages — prefetch bodies for offline access
|
|
489
|
+
const fetched = await client.fetchMessagesSinceUid(folder.path, highestUid, { source: prefetch });
|
|
482
490
|
// Filter out the last known message (IMAP * always returns at least one)
|
|
483
491
|
messages = fetched.filter((m) => m.uid > highestUid);
|
|
484
492
|
// Gap detection: check for missing UIDs within the range we've already synced
|
|
@@ -499,7 +507,7 @@ export class ImapManager extends EventEmitter {
|
|
|
499
507
|
for (let i = 0; i < missingUids.length; i += chunkSize) {
|
|
500
508
|
const chunk = missingUids.slice(i, i + chunkSize);
|
|
501
509
|
const range = chunk.join(",");
|
|
502
|
-
const recovered = await client.fetchMessages(folder.path, range, { source:
|
|
510
|
+
const recovered = await client.fetchMessages(folder.path, range, { source: prefetch });
|
|
503
511
|
messages.push(...recovered);
|
|
504
512
|
}
|
|
505
513
|
}
|
|
@@ -515,7 +523,7 @@ export class ImapManager extends EventEmitter {
|
|
|
515
523
|
const oldestDate = this.db.getOldestDate(accountId, folderId);
|
|
516
524
|
if (oldestDate > 0 && startDate.getTime() < oldestDate) {
|
|
517
525
|
const existingUids = new Set(this.db.getUidsForFolder(accountId, folderId));
|
|
518
|
-
const backfill = await client.fetchMessageByDate(folder.path, startDate, new Date(oldestDate), { source:
|
|
526
|
+
const backfill = await client.fetchMessageByDate(folder.path, startDate, new Date(oldestDate), { source: prefetch });
|
|
519
527
|
const newBackfill = backfill.filter((m) => !existingUids.has(m.uid));
|
|
520
528
|
if (newBackfill.length > 0) {
|
|
521
529
|
console.log(` ${folder.path}: backfilling ${newBackfill.length} older messages`);
|
|
@@ -526,8 +534,8 @@ export class ImapManager extends EventEmitter {
|
|
|
526
534
|
else {
|
|
527
535
|
// First sync: fetch in chunks, store each chunk immediately for instant UI
|
|
528
536
|
let totalStored = 0;
|
|
529
|
-
const onChunk = (chunk) => {
|
|
530
|
-
const stored = this.storeMessages(accountId, folderId, folder, chunk, highestUid);
|
|
537
|
+
const onChunk = async (chunk) => {
|
|
538
|
+
const stored = await this.storeMessages(accountId, folderId, folder, chunk, highestUid);
|
|
531
539
|
totalStored += stored;
|
|
532
540
|
if (stored > 0) {
|
|
533
541
|
this.db.recalcFolderCounts(folderId);
|
|
@@ -535,7 +543,7 @@ export class ImapManager extends EventEmitter {
|
|
|
535
543
|
}
|
|
536
544
|
};
|
|
537
545
|
const tomorrow = new Date(Date.now() + 86400000); // IMAP BEFORE is exclusive
|
|
538
|
-
messages = await client.fetchMessageByDate(folder.path, startDate, tomorrow, { source:
|
|
546
|
+
messages = await client.fetchMessageByDate(folder.path, startDate, tomorrow, { source: prefetch }, onChunk);
|
|
539
547
|
if (totalStored > 0) {
|
|
540
548
|
console.log(` ${folder.path}: ${totalStored} messages (streamed)`);
|
|
541
549
|
this.db.recalcFolderCounts(folderId);
|
|
@@ -695,19 +703,34 @@ export class ImapManager extends EventEmitter {
|
|
|
695
703
|
const t0 = Date.now();
|
|
696
704
|
const folders = await this.syncFolders(accountId, client);
|
|
697
705
|
console.log(` [timing] ${accountId}: folder list ${Date.now() - t0}ms (${folders.length} folders)`);
|
|
698
|
-
// Step 2: Sync INBOX first
|
|
706
|
+
// Step 2: Sync INBOX first — keep retrying on failure (most important folder)
|
|
699
707
|
const inbox = folders.find(f => f.specialUse === "inbox");
|
|
700
708
|
if (inbox) {
|
|
701
709
|
console.log(` [sync] ${accountId}: starting INBOX sync (folder ${inbox.id})`);
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
710
|
+
const maxAttempts = 5;
|
|
711
|
+
let inboxDone = false;
|
|
712
|
+
for (let attempt = 1; attempt <= maxAttempts && !inboxDone; attempt++) {
|
|
713
|
+
try {
|
|
714
|
+
client = await this.getOpsClient(accountId);
|
|
715
|
+
if (attempt > 1)
|
|
716
|
+
console.log(` [sync] ${accountId}: INBOX retry #${attempt}`);
|
|
717
|
+
await this.syncFolder(accountId, inbox.id, client);
|
|
718
|
+
console.log(` [sync] ${accountId}: INBOX sync complete`);
|
|
719
|
+
inboxDone = true;
|
|
720
|
+
}
|
|
721
|
+
catch (e) {
|
|
722
|
+
console.error(` Inbox sync error for ${accountId} (attempt ${attempt}/${maxAttempts}): ${e.message}`);
|
|
723
|
+
await this.reconnectOps(accountId);
|
|
724
|
+
if (attempt < maxAttempts) {
|
|
725
|
+
const delay = Math.min(attempt * 5000, 15000);
|
|
726
|
+
console.log(` [sync] ${accountId}: waiting ${delay / 1000}s before INBOX retry`);
|
|
727
|
+
await new Promise(r => setTimeout(r, delay));
|
|
728
|
+
}
|
|
729
|
+
}
|
|
707
730
|
}
|
|
708
|
-
|
|
709
|
-
console.error(`
|
|
710
|
-
|
|
731
|
+
if (!inboxDone) {
|
|
732
|
+
console.error(` [sync] ${accountId}: INBOX failed after ${maxAttempts} attempts — will retry next sync cycle`);
|
|
733
|
+
this.emit("syncError", accountId, `INBOX sync failed after ${maxAttempts} attempts`);
|
|
711
734
|
}
|
|
712
735
|
}
|
|
713
736
|
else {
|
|
@@ -82,8 +82,13 @@ async function dispatchAction(svc, action, p) {
|
|
|
82
82
|
case "syncAll":
|
|
83
83
|
await svc.syncAll();
|
|
84
84
|
return { ok: true };
|
|
85
|
+
case "syncAccount":
|
|
86
|
+
await svc.syncAccount(p.accountId);
|
|
87
|
+
return { ok: true };
|
|
85
88
|
case "getSyncPending":
|
|
86
89
|
return svc.getSyncPending();
|
|
90
|
+
case "reauthenticate":
|
|
91
|
+
return { ok: await svc.reauthenticate(p.accountId) };
|
|
87
92
|
// Search & contacts
|
|
88
93
|
case "searchMessages":
|
|
89
94
|
return svc.search(p.query, p.page, p.pageSize, p.scope, p.accountId, p.folderId);
|
|
@@ -42,6 +42,7 @@ declare const DEFAULT_PREFERENCES: {
|
|
|
42
42
|
sync: {
|
|
43
43
|
intervalMinutes: number;
|
|
44
44
|
historyDays: number;
|
|
45
|
+
prefetch: boolean;
|
|
45
46
|
};
|
|
46
47
|
autocomplete: {
|
|
47
48
|
enabled: boolean;
|
|
@@ -99,5 +100,7 @@ export declare function initCloudConfig(provider?: "gdrive"): Promise<void>;
|
|
|
99
100
|
declare const DEFAULT_SETTINGS: MailxSettings;
|
|
100
101
|
/** Get historyDays for an account: per-account override > system override > shared default */
|
|
101
102
|
export declare function getHistoryDays(accountId?: string): number;
|
|
103
|
+
/** Get prefetch setting: download bodies during sync (default true) */
|
|
104
|
+
export declare function getPrefetch(): boolean;
|
|
102
105
|
export { DEFAULT_SETTINGS, DEFAULT_ALLOWLIST, DEFAULT_PREFERENCES, DEFAULT_AUTOCOMPLETE, LOCAL_DIR };
|
|
103
106
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -338,6 +338,7 @@ const DEFAULT_PREFERENCES = {
|
|
|
338
338
|
sync: {
|
|
339
339
|
intervalMinutes: 5,
|
|
340
340
|
historyDays: 30,
|
|
341
|
+
prefetch: true,
|
|
341
342
|
},
|
|
342
343
|
autocomplete: {
|
|
343
344
|
enabled: false,
|
|
@@ -633,5 +634,10 @@ export function getHistoryDays(accountId) {
|
|
|
633
634
|
const prefs = loadPreferences();
|
|
634
635
|
return prefs.sync.historyDays || 0;
|
|
635
636
|
}
|
|
637
|
+
/** Get prefetch setting: download bodies during sync (default true) */
|
|
638
|
+
export function getPrefetch() {
|
|
639
|
+
const prefs = loadPreferences();
|
|
640
|
+
return prefs.sync.prefetch !== false;
|
|
641
|
+
}
|
|
636
642
|
export { DEFAULT_SETTINGS, DEFAULT_ALLOWLIST, DEFAULT_PREFERENCES, DEFAULT_AUTOCOMPLETE, LOCAL_DIR };
|
|
637
643
|
//# sourceMappingURL=index.js.map
|
|
@@ -191,6 +191,7 @@ export interface MailxSettings {
|
|
|
191
191
|
sync: {
|
|
192
192
|
intervalMinutes: number;
|
|
193
193
|
historyDays: number; /** 0 = all history */
|
|
194
|
+
prefetch: boolean; /** Download message bodies during sync (default true) */
|
|
194
195
|
};
|
|
195
196
|
store: {
|
|
196
197
|
basePath: string; /** Where message bodies are stored */
|