@bobfrankston/mailx 1.0.167 → 1.0.169
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 +1 -1
- package/client/app.js +3 -5
- package/client/components/message-list.js +2 -0
- package/client/lib/api-client.js +47 -35
- package/client/lib/mailxapi.js +2 -0
- package/package.json +3 -3
- package/packages/mailx-core/index.d.ts +1 -1
- package/packages/mailx-core/index.js +2 -2
- package/packages/mailx-imap/index.d.ts +1 -1
- package/packages/mailx-imap/index.js +23 -13
- package/packages/mailx-service/index.d.ts +1 -1
- package/packages/mailx-service/index.js +4 -4
- package/packages/mailx-service/jsonrpc.js +5 -0
- package/packages/mailx-settings/cloud.js +1 -1
- package/packages/mailx-settings/index.d.ts +5 -4
- package/packages/mailx-settings/index.js +42 -6
package/bin/mailx.js
CHANGED
|
@@ -201,7 +201,7 @@ if (importMode) {
|
|
|
201
201
|
const wrapper = { accounts: merged };
|
|
202
202
|
if (data?.name)
|
|
203
203
|
wrapper.name = data.name;
|
|
204
|
-
saveAccounts(merged);
|
|
204
|
+
await saveAccounts(merged);
|
|
205
205
|
console.log(`Saved ${merged.length} account(s). Run 'mailx' to start.`);
|
|
206
206
|
process.exit(0);
|
|
207
207
|
}
|
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.169",
|
|
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.219",
|
|
28
28
|
"@capacitor/android": "^8.3.0",
|
|
29
29
|
"@capacitor/cli": "^8.3.0",
|
|
30
30
|
"@capacitor/core": "^8.3.0",
|
|
@@ -113,7 +113,7 @@ export declare function getSyncPending(): {
|
|
|
113
113
|
export declare function allowRemoteContent(params: {
|
|
114
114
|
type: string;
|
|
115
115
|
value: string;
|
|
116
|
-
}): void
|
|
116
|
+
}): Promise<void>;
|
|
117
117
|
export declare function getSettings(): import("@bobfrankston/mailx-types").MailxSettings;
|
|
118
118
|
export declare function saveSettingsData(data: any): void;
|
|
119
119
|
export declare function rebuildSearchIndex(): number;
|
|
@@ -312,7 +312,7 @@ export async function syncAll() {
|
|
|
312
312
|
export function getSyncPending() {
|
|
313
313
|
return { pending: db.getTotalPendingSyncCount() };
|
|
314
314
|
}
|
|
315
|
-
export function allowRemoteContent(params) {
|
|
315
|
+
export async function allowRemoteContent(params) {
|
|
316
316
|
const list = loadAllowlist();
|
|
317
317
|
if (params.type === "sender" && !list.senders.includes(params.value))
|
|
318
318
|
list.senders.push(params.value);
|
|
@@ -324,7 +324,7 @@ export function allowRemoteContent(params) {
|
|
|
324
324
|
if (!list.recipients.includes(params.value))
|
|
325
325
|
list.recipients.push(params.value);
|
|
326
326
|
}
|
|
327
|
-
saveAllowlist(list);
|
|
327
|
+
await saveAllowlist(list);
|
|
328
328
|
}
|
|
329
329
|
export function getSettings() {
|
|
330
330
|
return loadSettings();
|
|
@@ -94,7 +94,7 @@ export declare class ImapManager extends EventEmitter {
|
|
|
94
94
|
* If message count changed, triggers inbox sync for that account. */
|
|
95
95
|
private lastInboxCounts;
|
|
96
96
|
private quickCheckRunning;
|
|
97
|
-
/** Check a single account's inbox */
|
|
97
|
+
/** Check a single account's inbox — uses its own connection, never blocked by sync */
|
|
98
98
|
quickInboxCheckAccount(accountId: string): Promise<void>;
|
|
99
99
|
/** Check all accounts (used by legacy callers) */
|
|
100
100
|
quickInboxCheck(): Promise<void>;
|
|
@@ -323,7 +323,7 @@ export class ImapManager extends EventEmitter {
|
|
|
323
323
|
if (!fs.existsSync(credPath)) {
|
|
324
324
|
try {
|
|
325
325
|
const pkgDir = path.dirname(import.meta.resolve("@bobfrankston/iflow-direct").replace("file:///", "").replace("file://", ""));
|
|
326
|
-
for (const name of ["
|
|
326
|
+
for (const name of ["iflow-credentials.json"]) {
|
|
327
327
|
const p = path.join(pkgDir, name);
|
|
328
328
|
if (fs.existsSync(p)) {
|
|
329
329
|
credPath = p;
|
|
@@ -338,6 +338,7 @@ export class ImapManager extends EventEmitter {
|
|
|
338
338
|
const result = await authenticateOAuth(credPath, {
|
|
339
339
|
scope: "https://mail.google.com/ https://www.googleapis.com/auth/contacts.readonly",
|
|
340
340
|
tokenDirectory: tokenDir,
|
|
341
|
+
credentialsKey: "installed",
|
|
341
342
|
loginHint: account.imap.user,
|
|
342
343
|
});
|
|
343
344
|
return result?.access_token || "";
|
|
@@ -825,31 +826,39 @@ export class ImapManager extends EventEmitter {
|
|
|
825
826
|
* If message count changed, triggers inbox sync for that account. */
|
|
826
827
|
lastInboxCounts = new Map();
|
|
827
828
|
quickCheckRunning = new Set(); // per-account guard
|
|
828
|
-
/** Check a single account's inbox */
|
|
829
|
+
/** Check a single account's inbox — uses its own connection, never blocked by sync */
|
|
829
830
|
async quickInboxCheckAccount(accountId) {
|
|
830
|
-
if (this.quickCheckRunning.has(accountId)
|
|
831
|
+
if (this.quickCheckRunning.has(accountId))
|
|
831
832
|
return;
|
|
832
833
|
if (this.reauthenticating.has(accountId))
|
|
833
834
|
return;
|
|
834
835
|
this.quickCheckRunning.add(accountId);
|
|
836
|
+
let client = null;
|
|
835
837
|
try {
|
|
836
838
|
const inbox = this.db.getFolders(accountId).find(f => f.specialUse === "inbox");
|
|
837
839
|
if (!inbox)
|
|
838
840
|
return;
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
841
|
+
client = this.newClient(accountId);
|
|
842
|
+
const count = await client.getMessagesCount("INBOX");
|
|
843
|
+
const prev = this.lastInboxCounts.get(accountId) ?? count;
|
|
844
|
+
this.lastInboxCounts.set(accountId, count);
|
|
845
|
+
if (count !== prev) {
|
|
846
|
+
console.log(` [check] ${accountId} INBOX: ${prev} → ${count}`);
|
|
847
|
+
await this.syncFolder(accountId, inbox.id, client);
|
|
848
|
+
}
|
|
849
|
+
await client.logout();
|
|
850
|
+
client = null;
|
|
848
851
|
}
|
|
849
852
|
catch {
|
|
850
853
|
// Lightweight check — silently ignore errors
|
|
851
854
|
}
|
|
852
855
|
finally {
|
|
856
|
+
if (client) {
|
|
857
|
+
try {
|
|
858
|
+
await client.logout();
|
|
859
|
+
}
|
|
860
|
+
catch { /* */ }
|
|
861
|
+
}
|
|
853
862
|
this.quickCheckRunning.delete(accountId);
|
|
854
863
|
}
|
|
855
864
|
}
|
|
@@ -867,7 +876,8 @@ export class ImapManager extends EventEmitter {
|
|
|
867
876
|
// Password (Dovecot etc): every 60s — conservative, 20-connection limit
|
|
868
877
|
// IDLE gives instant notification when working; STATUS is the fallback.
|
|
869
878
|
for (const [accountId] of this.configs) {
|
|
870
|
-
const
|
|
879
|
+
const QUICK_CHECK_INTERVAL = 2500; // STATUS INBOX is one command, very cheap
|
|
880
|
+
const interval = QUICK_CHECK_INTERVAL;
|
|
871
881
|
const timer = setInterval(() => {
|
|
872
882
|
this.quickInboxCheckAccount(accountId).catch(() => { });
|
|
873
883
|
}, interval);
|
|
@@ -20,7 +20,7 @@ export declare class MailxService {
|
|
|
20
20
|
getMessages(accountId: string, folderId: number, page?: number, pageSize?: number, sort?: string, sortDir?: string, search?: string): any;
|
|
21
21
|
getMessage(accountId: string, uid: number, allowRemote?: boolean, folderId?: number): Promise<any>;
|
|
22
22
|
updateFlags(accountId: string, uid: number, flags: string[]): Promise<void>;
|
|
23
|
-
allowRemoteContent(type: "sender" | "domain" | "recipient", value: string): void
|
|
23
|
+
allowRemoteContent(type: "sender" | "domain" | "recipient", value: string): Promise<void>;
|
|
24
24
|
search(q: string, page?: number, pageSize?: number, scope?: string, accountId?: string, folderId?: number): Promise<any>;
|
|
25
25
|
rebuildSearchIndex(): number;
|
|
26
26
|
getSyncPending(): {
|
|
@@ -213,7 +213,7 @@ export class MailxService {
|
|
|
213
213
|
await this.imapManager.updateFlagsLocal(accountId, uid, envelope?.folderId || 0, flags);
|
|
214
214
|
}
|
|
215
215
|
// ── Remote content allow-list ──
|
|
216
|
-
allowRemoteContent(type, value) {
|
|
216
|
+
async allowRemoteContent(type, value) {
|
|
217
217
|
const list = loadAllowlist();
|
|
218
218
|
if (type === "sender" && !list.senders.includes(value))
|
|
219
219
|
list.senders.push(value);
|
|
@@ -225,7 +225,7 @@ export class MailxService {
|
|
|
225
225
|
if (!list.recipients.includes(value))
|
|
226
226
|
list.recipients.push(value);
|
|
227
227
|
}
|
|
228
|
-
saveAllowlist(list);
|
|
228
|
+
await saveAllowlist(list);
|
|
229
229
|
console.log(` [allow] Added ${type}: ${value}`);
|
|
230
230
|
}
|
|
231
231
|
// ── Search ──
|
|
@@ -571,7 +571,7 @@ export class MailxService {
|
|
|
571
571
|
account.smtp = { host: detected.smtpHost, port: 587, tls: true, auth: detected.auth, user: email };
|
|
572
572
|
}
|
|
573
573
|
account.id = domain.split(".")[0] || "account";
|
|
574
|
-
saveAccounts([account]);
|
|
574
|
+
await saveAccounts([account]);
|
|
575
575
|
// Re-read normalized settings and register
|
|
576
576
|
const settings = loadSettings();
|
|
577
577
|
for (const acct of settings.accounts) {
|
|
@@ -603,7 +603,7 @@ export class MailxService {
|
|
|
603
603
|
if (restored.length === 0) {
|
|
604
604
|
return { ok: false, error: "Could not parse cached account configs" };
|
|
605
605
|
}
|
|
606
|
-
saveAccounts(restored);
|
|
606
|
+
await saveAccounts(restored);
|
|
607
607
|
for (const acct of restored) {
|
|
608
608
|
try {
|
|
609
609
|
await this.imapManager.addAccount(acct);
|
|
@@ -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);
|
|
@@ -31,7 +31,7 @@ function findGoogleCredentials() {
|
|
|
31
31
|
let dir = import.meta.dirname;
|
|
32
32
|
for (let i = 0; i < 5; i++) {
|
|
33
33
|
for (const pkg of ["iflow-direct", "iflow"]) {
|
|
34
|
-
for (const name of ["iflow-credentials.json"
|
|
34
|
+
for (const name of ["iflow-credentials.json"]) {
|
|
35
35
|
const p = path.join(dir, "node_modules", "@bobfrankston", pkg, name);
|
|
36
36
|
if (fs.existsSync(p))
|
|
37
37
|
return p;
|
|
@@ -65,7 +65,8 @@ export declare function loadAccounts(): AccountConfig[];
|
|
|
65
65
|
/** Load accounts with cloud API fallback (async — use when cloud settings may not be mounted) */
|
|
66
66
|
export declare function loadAccountsAsync(): Promise<AccountConfig[]>;
|
|
67
67
|
/** Save account configs */
|
|
68
|
-
|
|
68
|
+
/** Save accounts — merges with cloud copy by email (multi-client safe) */
|
|
69
|
+
export declare function saveAccounts(accounts: AccountConfig[]): Promise<void>;
|
|
69
70
|
/** Load preferences (shared + local overrides, with legacy fallback) */
|
|
70
71
|
export declare function loadPreferences(): typeof DEFAULT_PREFERENCES;
|
|
71
72
|
/** Save preferences */
|
|
@@ -76,12 +77,12 @@ export declare function loadAutocomplete(): AutocompleteSettings;
|
|
|
76
77
|
export declare function saveAutocomplete(settings: AutocompleteSettings): void;
|
|
77
78
|
/** Load remote content allow-list */
|
|
78
79
|
export declare function loadAllowlist(): typeof DEFAULT_ALLOWLIST;
|
|
79
|
-
/** Save allow-list */
|
|
80
|
-
export declare function saveAllowlist(list: typeof DEFAULT_ALLOWLIST): void
|
|
80
|
+
/** Save allow-list — merges with existing cloud copy (multi-client safe) */
|
|
81
|
+
export declare function saveAllowlist(list: typeof DEFAULT_ALLOWLIST): Promise<void>;
|
|
81
82
|
/** Load settings — unified view combining all files (backward compatible) */
|
|
82
83
|
export declare function loadSettings(): MailxSettings;
|
|
83
84
|
/** Save settings — writes to split files */
|
|
84
|
-
export declare function saveSettings(settings: MailxSettings): void
|
|
85
|
+
export declare function saveSettings(settings: MailxSettings): Promise<void>;
|
|
85
86
|
/** Get the local store base path */
|
|
86
87
|
export declare function getStorePath(): string;
|
|
87
88
|
/** Get the local data directory (DB, store, etc.) */
|
|
@@ -454,7 +454,26 @@ export async function loadAccountsAsync() {
|
|
|
454
454
|
return [];
|
|
455
455
|
}
|
|
456
456
|
/** Save account configs */
|
|
457
|
-
|
|
457
|
+
/** Save accounts — merges with cloud copy by email (multi-client safe) */
|
|
458
|
+
export async function saveAccounts(accounts) {
|
|
459
|
+
// Merge with cloud: keep all accounts, deduplicate by normalized email
|
|
460
|
+
try {
|
|
461
|
+
const cloudContent = await cloudRead("accounts.jsonc");
|
|
462
|
+
if (cloudContent) {
|
|
463
|
+
const cloud = parseJsonc(cloudContent);
|
|
464
|
+
const cloudAccts = cloud?.accounts || (Array.isArray(cloud) ? cloud : []);
|
|
465
|
+
if (cloudAccts.length > 0) {
|
|
466
|
+
const seen = new Set(accounts.map(a => normalizeEmail(a.email)));
|
|
467
|
+
for (const ca of cloudAccts) {
|
|
468
|
+
if (ca.email && !seen.has(normalizeEmail(ca.email))) {
|
|
469
|
+
accounts.push(ca);
|
|
470
|
+
seen.add(normalizeEmail(ca.email));
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
catch { /* cloud read failed — save local version */ }
|
|
458
477
|
saveFile("accounts.jsonc", { accounts });
|
|
459
478
|
}
|
|
460
479
|
/** Load preferences (shared + local overrides, with legacy fallback) */
|
|
@@ -496,9 +515,26 @@ export function saveAutocomplete(settings) {
|
|
|
496
515
|
export function loadAllowlist() {
|
|
497
516
|
return loadFile("allowlist.jsonc", DEFAULT_ALLOWLIST);
|
|
498
517
|
}
|
|
499
|
-
/** Save allow-list */
|
|
500
|
-
export function saveAllowlist(list) {
|
|
501
|
-
|
|
518
|
+
/** Save allow-list — merges with existing cloud copy (multi-client safe) */
|
|
519
|
+
export async function saveAllowlist(list) {
|
|
520
|
+
// Read current cloud version and merge (other clients may have added entries)
|
|
521
|
+
let merged = { ...list };
|
|
522
|
+
try {
|
|
523
|
+
const cloudContent = await cloudRead("allowlist.jsonc");
|
|
524
|
+
if (cloudContent) {
|
|
525
|
+
const cloud = parseJsonc(cloudContent);
|
|
526
|
+
if (cloud) {
|
|
527
|
+
const mergeArrays = (local, remote) => [...new Set([...local, ...remote])];
|
|
528
|
+
merged = {
|
|
529
|
+
senders: mergeArrays(list.senders || [], cloud.senders || []),
|
|
530
|
+
domains: mergeArrays(list.domains || [], cloud.domains || []),
|
|
531
|
+
recipients: mergeArrays(list.recipients || [], cloud.recipients || []),
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
catch { /* cloud read failed — save local version */ }
|
|
537
|
+
saveFile("allowlist.jsonc", merged);
|
|
502
538
|
}
|
|
503
539
|
// ── Legacy compatibility ──
|
|
504
540
|
function loadLegacySettings() {
|
|
@@ -529,8 +565,8 @@ export function loadSettings() {
|
|
|
529
565
|
};
|
|
530
566
|
}
|
|
531
567
|
/** Save settings — writes to split files */
|
|
532
|
-
export function saveSettings(settings) {
|
|
533
|
-
saveAccounts(settings.accounts);
|
|
568
|
+
export async function saveSettings(settings) {
|
|
569
|
+
await saveAccounts(settings.accounts);
|
|
534
570
|
savePreferences({ ui: settings.ui, sync: settings.sync });
|
|
535
571
|
}
|
|
536
572
|
/** Get the local store base path */
|