@bobfrankston/mailx 1.0.178 → 1.0.180

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.
@@ -0,0 +1,156 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>mailx</title>
7
+ <link rel="icon" type="image/svg+xml" href="favicon.svg">
8
+ <link rel="stylesheet" href="styles/variables.css">
9
+ <link rel="stylesheet" href="styles/layout.css">
10
+ <link rel="stylesheet" href="styles/components.css">
11
+ <!-- Import map for Android — resolves @bobfrankston packages to bundled assets -->
12
+ <script type="importmap">
13
+ {
14
+ "imports": {
15
+ "@bobfrankston/mailx-store-web": "../packages/mailx-store-web/index.js",
16
+ "@bobfrankston/mailx-store-web/": "../packages/mailx-store-web/",
17
+ "@bobfrankston/mailx-types": "../packages/mailx-types/index.js",
18
+ "sql.js": "../node_modules/sql.js/dist/sql-wasm.js"
19
+ }
20
+ }
21
+ </script>
22
+ <!-- Android: load bootstrap first (installs window.mailxapi), then app -->
23
+ <script type="module">
24
+ import { initAndroid } from "@bobfrankston/mailx-store-web/android-bootstrap.js";
25
+ await initAndroid();
26
+ // Now load the main app (mailxapi is ready)
27
+ await import("./app.js");
28
+ </script>
29
+ </head>
30
+ <body>
31
+ <header class="toolbar">
32
+ <div class="toolbar-left">
33
+ <button class="tb-btn" id="btn-menu" title="Folders" hidden>☰</button>
34
+ <button class="tb-btn" id="btn-compose" title="Compose (Ctrl+N)">
35
+ <span class="tb-icon">✏</span> Compose
36
+ </button>
37
+ </div>
38
+ <div class="toolbar-center">
39
+ <div class="tb-menu" id="view-menu">
40
+ <button class="tb-btn" id="btn-view">View</button>
41
+ <div class="tb-menu-dropdown" id="view-dropdown" hidden>
42
+ <label class="tb-menu-item"><input type="checkbox" id="opt-two-line"> Two-line view</label>
43
+ <label class="tb-menu-item"><input type="checkbox" id="opt-preview" checked> Preview pane</label>
44
+ <label class="tb-menu-item"><input type="checkbox" id="opt-snippet" checked> Preview snippets</label>
45
+ <label class="tb-menu-item"><input type="checkbox" id="opt-flagged"> ★ Flagged only</label>
46
+ <label class="tb-menu-item"><input type="checkbox" id="opt-folder-counts"> Folder counts</label>
47
+ </div>
48
+ </div>
49
+ <div class="tb-menu" id="settings-menu">
50
+ <button class="tb-btn" id="btn-settings">Settings</button>
51
+ <div class="tb-menu-dropdown" id="settings-dropdown" hidden>
52
+ <span class="tb-menu-label">Editor</span>
53
+ <label class="tb-menu-item"><input type="radio" name="opt-editor" value="quill" id="opt-editor-quill" checked> Quill</label>
54
+ <label class="tb-menu-item"><input type="radio" name="opt-editor" value="tiptap" id="opt-editor-tiptap"> tiptap</label>
55
+ <hr class="tb-menu-sep">
56
+ <label class="tb-menu-item"><input type="checkbox" id="opt-autocomplete"> AI autocomplete</label>
57
+ </div>
58
+ </div>
59
+ <span id="app-version" class="app-version">mailx</span>
60
+ </div>
61
+ <div class="toolbar-right">
62
+ <button class="tb-btn" id="btn-sync" title="Sync all folders (F5)">
63
+ <span class="tb-icon">↻</span> Sync
64
+ </button>
65
+ <div class="tb-menu" id="restart-menu">
66
+ <button class="tb-btn" id="btn-restart" title="Reset">
67
+ <span class="tb-icon">⚡</span> Reset ▾
68
+ </button>
69
+ <div class="tb-menu-dropdown" id="restart-dropdown" hidden>
70
+ <button class="tb-menu-item" id="btn-restart-quick" title="Reload the page">Reload</button>
71
+ <button class="tb-menu-item" id="btn-rebuild" title="Wipe local cache and re-sync">Reset local store</button>
72
+ </div>
73
+ </div>
74
+ </div>
75
+ </header>
76
+
77
+ <div class="alert-banner" id="alert-banner" hidden>
78
+ <span id="alert-text"></span>
79
+ <button class="alert-dismiss" id="alert-dismiss" title="Dismiss">&times;</button>
80
+ </div>
81
+
82
+ <div class="folder-panel">
83
+ <div class="ft-filter">
84
+ <input type="text" id="ft-filter-input" placeholder="Find folder..." autocomplete="off">
85
+ </div>
86
+ <nav class="folder-tree" id="folder-tree">
87
+ <div class="folder-loading">Loading accounts...</div>
88
+ </nav>
89
+ </div>
90
+
91
+ <main class="main-area">
92
+ <section class="message-list" id="message-list">
93
+ <search class="search-bar ml-search">
94
+ <select id="search-scope" title="Search scope">
95
+ <option value="all">All folders</option>
96
+ <option value="current">This folder</option>
97
+ </select>
98
+ <input type="search" id="search-input" placeholder="Search..." autocomplete="off" title="Search messages">
99
+ </search>
100
+ <div class="ml-header">
101
+ <span class="ml-col ml-col-flag"></span>
102
+ <span class="ml-col ml-col-from" data-sort="from">From</span>
103
+ <span class="ml-col ml-col-date" data-sort="date">Date</span>
104
+ <span class="ml-col ml-col-subject">Subject</span>
105
+ </div>
106
+ <div class="ml-body" id="ml-body">
107
+ <div class="ml-empty">Select a folder to view messages</div>
108
+ </div>
109
+ </section>
110
+
111
+ <div class="splitter" id="splitter-h"></div>
112
+
113
+ <section class="message-viewer" id="message-viewer">
114
+ <div class="mv-header" id="mv-header" hidden>
115
+ <div class="mv-toolbar">
116
+ <button class="tb-btn" id="btn-back" title="Back to list" hidden>←</button>
117
+ <button class="tb-btn" id="btn-reply" title="Reply">↩</button>
118
+ <button class="tb-btn" id="btn-reply-all" title="Reply All">↩↩</button>
119
+ <button class="tb-btn" id="btn-forward" title="Forward">→</button>
120
+ <button class="tb-btn" id="btn-delete" title="Delete">🗑</button>
121
+ <button class="tb-btn" id="btn-flag" title="Flag">⚑</button>
122
+ <span style="flex:1"></span>
123
+ <button class="mv-action mv-action-primary" id="mv-edit-draft" hidden>Edit & Send</button>
124
+ <a class="mv-unsubscribe" id="mv-unsubscribe" hidden>Unsubscribe</a>
125
+ <button class="mv-action" id="mv-toggle-details" title="Show/hide extra headers">Details</button>
126
+ </div>
127
+ <div class="mv-header-info">
128
+ <div class="mv-from"></div>
129
+ <div class="mv-to"></div>
130
+ </div>
131
+ <div class="mv-subject"></div>
132
+ <div class="mv-date"></div>
133
+ <div class="mv-details" id="mv-details" hidden></div>
134
+ </div>
135
+ <div class="mv-body" id="mv-body">
136
+ <div class="mv-empty">Select a message to read</div>
137
+ </div>
138
+ <div class="mv-attachments" id="mv-attachments" hidden></div>
139
+ </section>
140
+ </main>
141
+
142
+ <footer class="status-bar" id="status-bar">
143
+ <span id="status-accounts"></span>
144
+ <span id="status-sync">Initializing...</span>
145
+ <span id="status-pending"></span>
146
+ <span id="status-queue"></span>
147
+ </footer>
148
+
149
+ <div id="startup-overlay" class="startup-overlay">
150
+ <div class="startup-content">
151
+ <div class="startup-spinner"></div>
152
+ <div id="startup-status">Initializing mailx...</div>
153
+ </div>
154
+ </div>
155
+ </body>
156
+ </html>
@@ -81,11 +81,13 @@ export async function showMessage(accountId, uid, folderId, specialUse, isRetry
81
81
  if (unsubBtn) {
82
82
  if (unsubUrl) {
83
83
  unsubBtn.hidden = false;
84
- unsubBtn.href = unsubUrl;
85
84
  unsubBtn.textContent = "Unsubscribe";
86
85
  unsubBtn.title = unsubUrl;
87
- unsubBtn.target = "_blank";
88
- unsubBtn.rel = "noopener noreferrer";
86
+ unsubBtn.href = "#";
87
+ unsubBtn.onclick = (e) => {
88
+ e.preventDefault();
89
+ window.open(unsubUrl, "_blank");
90
+ };
89
91
  }
90
92
  else {
91
93
  unsubBtn.hidden = true;
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Stub — the real android bootstrap is in packages/mailx-store-web/android-bootstrap.ts.
3
+ * This file exists only because it was created during development. The Android HTML
4
+ * (client/android.html) loads the bootstrap from the package via import map.
5
+ *
6
+ * This file is excluded from the desktop build (not imported by anything).
7
+ */
8
+ export {};
9
+ //# sourceMappingURL=android-bootstrap.js.map
@@ -6,9 +6,17 @@
6
6
  * All server operations MUST go through these centralized methods.
7
7
  * Never use fetch("/api/...") directly in components.
8
8
  */
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;
9
+ // Lazy IPC detection checked on each call, not at module load time.
10
+ // Handles: desktop (initScript before page load), Android (bootstrap before app import),
11
+ // and popup windows (opener's bridge).
12
+ function getIpc() {
13
+ if (typeof mailxapi !== "undefined" && mailxapi?.isApp)
14
+ return mailxapi;
15
+ if (window.opener?.mailxapi?.isApp)
16
+ return window.opener.mailxapi;
17
+ return null;
18
+ }
19
+ function hasIPC() { return getIpc() !== null; }
12
20
  // ── HTTP fallback ──
13
21
  // Abort controller for message-list requests — cancel stale fetches when folder changes
14
22
  let messageListAbort = null;
@@ -45,30 +53,30 @@ async function api(path, options) {
45
53
  }
46
54
  // ── API Methods (IPC or HTTP) ──
47
55
  export function getAccounts() {
48
- if (hasIPC)
49
- return _ipc.getAccounts();
56
+ if (hasIPC())
57
+ return getIpc().getAccounts();
50
58
  return api("/accounts");
51
59
  }
52
60
  export function getFolders(accountId) {
53
- if (hasIPC)
54
- return _ipc.getFolders(accountId);
61
+ if (hasIPC())
62
+ return getIpc().getFolders(accountId);
55
63
  return api(`/folders/${accountId}`);
56
64
  }
57
65
  export function getMessages(accountId, folderId, page = 1, pageSize = 50) {
58
- if (hasIPC)
59
- return _ipc.getMessages(accountId, folderId, page, pageSize);
66
+ if (hasIPC())
67
+ return getIpc().getMessages(accountId, folderId, page, pageSize);
60
68
  const signal = newMessageListSignal();
61
69
  return api(`/messages/${accountId}/${folderId}?page=${page}&pageSize=${pageSize}`, { signal });
62
70
  }
63
71
  export function getUnifiedInbox(page = 1, pageSize = 50) {
64
- if (hasIPC)
65
- return _ipc.getUnifiedInbox(page, pageSize);
72
+ if (hasIPC())
73
+ return getIpc().getUnifiedInbox(page, pageSize);
66
74
  const signal = newMessageListSignal();
67
75
  return api(`/messages/unified/inbox?page=${page}&pageSize=${pageSize}`, { signal });
68
76
  }
69
77
  export function searchMessages(query, page = 1, pageSize = 50, scope = "all", accountId = "", folderId = 0) {
70
- if (hasIPC)
71
- return _ipc.searchMessages(query, page, pageSize);
78
+ if (hasIPC())
79
+ return getIpc().searchMessages(query, page, pageSize);
72
80
  const params = new URLSearchParams({ q: query, page: String(page), pageSize: String(pageSize), scope });
73
81
  if (scope === "current" && accountId) {
74
82
  params.set("accountId", accountId);
@@ -81,8 +89,8 @@ export function searchMessages(query, page = 1, pageSize = 50, scope = "all", ac
81
89
  return api(`/search?${params}`);
82
90
  }
83
91
  export function getMessage(accountId, uid, allowRemote = false, folderId) {
84
- if (hasIPC)
85
- return _ipc.getMessage(accountId, uid, allowRemote, folderId);
92
+ if (hasIPC())
93
+ return getIpc().getMessage(accountId, uid, allowRemote, folderId);
86
94
  const params = new URLSearchParams();
87
95
  if (allowRemote)
88
96
  params.set("allowRemote", "true");
@@ -92,56 +100,56 @@ export function getMessage(accountId, uid, allowRemote = false, folderId) {
92
100
  return api(`/message/${accountId}/${uid}${q}`);
93
101
  }
94
102
  export function updateFlags(accountId, uid, flags) {
95
- if (hasIPC)
96
- return _ipc.updateFlags(accountId, uid, flags);
103
+ if (hasIPC())
104
+ return getIpc().updateFlags(accountId, uid, flags);
97
105
  return api(`/message/${accountId}/${uid}/flags`, {
98
106
  method: "PATCH",
99
107
  body: JSON.stringify({ flags })
100
108
  });
101
109
  }
102
110
  export function triggerSync() {
103
- if (hasIPC)
104
- return _ipc.syncAll();
111
+ if (hasIPC())
112
+ return getIpc().syncAll();
105
113
  return api("/sync", { method: "POST" });
106
114
  }
107
115
  export function syncAccount(accountId) {
108
- if (hasIPC)
109
- return _ipc.syncAccount(accountId);
116
+ if (hasIPC())
117
+ return getIpc().syncAccount(accountId);
110
118
  return api(`/sync/${accountId}`, { method: "POST" });
111
119
  }
112
120
  export function reauthenticate(accountId) {
113
- if (hasIPC)
114
- return _ipc.reauthenticate(accountId);
121
+ if (hasIPC())
122
+ return getIpc().reauthenticate(accountId);
115
123
  return api(`/reauth/${accountId}`, { method: "POST" });
116
124
  }
117
125
  export function getSyncPending() {
118
- if (hasIPC)
119
- return _ipc.getSyncPending();
126
+ if (hasIPC())
127
+ return getIpc().getSyncPending();
120
128
  return api("/sync/pending");
121
129
  }
122
130
  export function searchContacts(query) {
123
- if (hasIPC)
124
- return _ipc.searchContacts(query);
131
+ if (hasIPC())
132
+ return getIpc().searchContacts(query);
125
133
  return api(`/contacts?q=${encodeURIComponent(query)}`);
126
134
  }
127
135
  export function allowRemoteContent(type, value) {
128
- if (hasIPC)
129
- return _ipc.allowRemoteContent(type, value);
136
+ if (hasIPC())
137
+ return getIpc().allowRemoteContent(type, value);
130
138
  return api("/settings/allow-remote", {
131
139
  method: "POST",
132
140
  body: JSON.stringify({ type, value })
133
141
  });
134
142
  }
135
143
  export function deleteMessage(accountId, uid) {
136
- if (hasIPC)
137
- return _ipc.deleteMessage?.(accountId, uid);
144
+ if (hasIPC())
145
+ return getIpc().deleteMessage?.(accountId, uid);
138
146
  return api(`/message/${accountId}/${uid}`, { method: "DELETE" });
139
147
  }
140
148
  export function deleteMessages(accountId, uids) {
141
149
  if (uids.length === 1)
142
150
  return deleteMessage(accountId, uids[0]);
143
- if (hasIPC)
144
- return _ipc.deleteMessages?.(accountId, uids);
151
+ if (hasIPC())
152
+ return getIpc().deleteMessages?.(accountId, uids);
145
153
  return api("/messages/delete", {
146
154
  method: "POST", body: JSON.stringify({ accountId, uids })
147
155
  });
@@ -149,8 +157,8 @@ export function deleteMessages(accountId, uids) {
149
157
  export function moveMessages(accountId, uids, targetFolderId, targetAccountId) {
150
158
  if (uids.length === 1)
151
159
  return moveMessage(accountId, uids[0], targetFolderId, targetAccountId);
152
- if (hasIPC)
153
- return _ipc.moveMessages?.(accountId, uids, targetFolderId, targetAccountId);
160
+ if (hasIPC())
161
+ return getIpc().moveMessages?.(accountId, uids, targetFolderId, targetAccountId);
154
162
  const body = { accountId, uids, targetFolderId };
155
163
  if (targetAccountId)
156
164
  body.targetAccountId = targetAccountId;
@@ -159,16 +167,16 @@ export function moveMessages(accountId, uids, targetFolderId, targetAccountId) {
159
167
  });
160
168
  }
161
169
  export function undeleteMessage(accountId, uid, folderId) {
162
- if (hasIPC)
163
- return _ipc.undeleteMessage?.(accountId, uid, folderId);
170
+ if (hasIPC())
171
+ return getIpc().undeleteMessage?.(accountId, uid, folderId);
164
172
  return api(`/message/${accountId}/${uid}/undelete`, {
165
173
  method: "POST",
166
174
  body: JSON.stringify({ folderId })
167
175
  });
168
176
  }
169
177
  export function moveMessage(accountId, uid, targetFolderId, targetAccountId) {
170
- if (hasIPC)
171
- return _ipc.moveMessage?.(accountId, uid, targetFolderId, targetAccountId);
178
+ if (hasIPC())
179
+ return getIpc().moveMessage?.(accountId, uid, targetFolderId, targetAccountId);
172
180
  const body = { targetFolderId };
173
181
  if (targetAccountId)
174
182
  body.targetAccountId = targetAccountId;
@@ -178,52 +186,52 @@ export function moveMessage(accountId, uid, targetFolderId, targetAccountId) {
178
186
  });
179
187
  }
180
188
  export function restartServer() {
181
- if (hasIPC)
182
- return _ipc.restart?.();
189
+ if (hasIPC())
190
+ return getIpc().restart?.();
183
191
  return api("/restart", { method: "POST" }).catch(() => { });
184
192
  }
185
193
  export function rebuildServer() {
186
194
  return api("/rebuild", { method: "POST" }).catch(() => { });
187
195
  }
188
196
  export function markFolderRead(accountId, folderId) {
189
- if (hasIPC)
190
- return _ipc.markFolderRead?.(accountId, folderId);
197
+ if (hasIPC())
198
+ return getIpc().markFolderRead?.(accountId, folderId);
191
199
  return api(`/folder/${accountId}/${folderId}/mark-read`, { method: "POST" });
192
200
  }
193
201
  export function createFolder(accountId, parentPath, name) {
194
- if (hasIPC)
195
- return _ipc.createFolder?.(accountId, parentPath, name);
202
+ if (hasIPC())
203
+ return getIpc().createFolder?.(accountId, parentPath, name);
196
204
  return api(`/folder/${accountId}`, {
197
205
  method: "POST",
198
206
  body: JSON.stringify({ parentPath, name })
199
207
  });
200
208
  }
201
209
  export function renameFolder(accountId, folderId, newName) {
202
- if (hasIPC)
203
- return _ipc.renameFolder?.(accountId, folderId, newName);
210
+ if (hasIPC())
211
+ return getIpc().renameFolder?.(accountId, folderId, newName);
204
212
  return api(`/folder/${accountId}/${folderId}/rename`, {
205
213
  method: "POST",
206
214
  body: JSON.stringify({ newName })
207
215
  });
208
216
  }
209
217
  export function deleteFolder(accountId, folderId) {
210
- if (hasIPC)
211
- return _ipc.deleteFolder?.(accountId, folderId);
218
+ if (hasIPC())
219
+ return getIpc().deleteFolder?.(accountId, folderId);
212
220
  return api(`/folder/${accountId}/${folderId}`, { method: "DELETE" });
213
221
  }
214
222
  export function emptyFolder(accountId, folderId) {
215
- if (hasIPC)
216
- return _ipc.emptyFolder?.(accountId, folderId);
223
+ if (hasIPC())
224
+ return getIpc().emptyFolder?.(accountId, folderId);
217
225
  return api(`/folder/${accountId}/${folderId}/empty`, { method: "POST" });
218
226
  }
219
227
  export function sendMessage(body) {
220
- if (hasIPC)
221
- return _ipc.sendMessage?.(body);
228
+ if (hasIPC())
229
+ return getIpc().sendMessage?.(body);
222
230
  return api("/send", { method: "POST", body: JSON.stringify(body) });
223
231
  }
224
232
  export function saveDraft(body) {
225
- if (hasIPC)
226
- return _ipc.saveDraft?.(body);
233
+ if (hasIPC())
234
+ return getIpc().saveDraft?.(body);
227
235
  return api("/draft", { method: "POST", body: JSON.stringify(body) });
228
236
  }
229
237
  const eventHandlers = [];
@@ -231,9 +239,9 @@ export function onEvent(handler) {
231
239
  eventHandlers.push(handler);
232
240
  }
233
241
  export function connectEvents() {
234
- if (hasIPC) {
242
+ if (hasIPC()) {
235
243
  // IPC events come via mailxapi.onEvent
236
- _ipc.onEvent((event) => {
244
+ getIpc().onEvent((event) => {
237
245
  for (const h of eventHandlers)
238
246
  h(event);
239
247
  });
@@ -257,48 +265,48 @@ export function connectEvents() {
257
265
  }
258
266
  // ── Autocomplete ──
259
267
  export function autocomplete(body, signal) {
260
- if (hasIPC)
261
- return _ipc.autocomplete?.(body);
268
+ if (hasIPC())
269
+ return getIpc().autocomplete?.(body);
262
270
  return api("/autocomplete", { method: "POST", body: JSON.stringify(body), signal });
263
271
  }
264
272
  export function getAutocompleteSettings() {
265
- if (hasIPC)
266
- return _ipc.getAutocompleteSettings?.();
273
+ if (hasIPC())
274
+ return getIpc().getAutocompleteSettings?.();
267
275
  return api("/autocomplete/settings");
268
276
  }
269
277
  export function saveAutocompleteSettings(settings) {
270
- if (hasIPC)
271
- return _ipc.saveAutocompleteSettings?.(settings);
278
+ if (hasIPC())
279
+ return getIpc().saveAutocompleteSettings?.(settings);
272
280
  return api("/autocomplete/settings", { method: "POST", body: JSON.stringify(settings) });
273
281
  }
274
282
  export function getVersion() {
275
- if (hasIPC)
276
- return _ipc.getVersion();
283
+ if (hasIPC())
284
+ return getIpc().getVersion();
277
285
  return api("/version");
278
286
  }
279
287
  export function getSettings() {
280
- if (hasIPC)
281
- return _ipc.getSettings();
288
+ if (hasIPC())
289
+ return getIpc().getSettings();
282
290
  return api("/settings");
283
291
  }
284
292
  export function saveSettings(settings) {
285
- if (hasIPC)
286
- return _ipc.saveSettingsData?.(settings);
293
+ if (hasIPC())
294
+ return getIpc().saveSettingsData?.(settings);
287
295
  return api("/settings", { method: "PUT", body: JSON.stringify(settings) });
288
296
  }
289
297
  export function repairAccounts() {
290
- if (hasIPC)
291
- return _ipc.repairAccounts?.();
298
+ if (hasIPC())
299
+ return getIpc().repairAccounts?.();
292
300
  return api("/repair-accounts", { method: "POST" });
293
301
  }
294
302
  export function deleteDraft(accountId, draftUid) {
295
- if (hasIPC)
296
- return _ipc.deleteDraft?.(accountId, draftUid);
303
+ if (hasIPC())
304
+ return getIpc().deleteDraft?.(accountId, draftUid);
297
305
  return api("/draft", { method: "DELETE", body: JSON.stringify({ accountId, draftUid }) });
298
306
  }
299
307
  export function setupAccount(name, email, password) {
300
- if (hasIPC)
301
- return _ipc.setupAccount?.(name, email, password);
308
+ if (hasIPC())
309
+ return getIpc().setupAccount?.(name, email, password);
302
310
  return api("/setup", { method: "POST", body: JSON.stringify({ name, email, password }) });
303
311
  }
304
312
  // Legacy exports for backward compatibility
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/mailx",
3
- "version": "1.0.178",
3
+ "version": "1.0.180",
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.21",
27
- "@bobfrankston/msger": "^0.1.227",
27
+ "@bobfrankston/msger": "^0.1.230",
28
28
  "@capacitor/android": "^8.3.0",
29
29
  "@capacitor/cli": "^8.3.0",
30
30
  "@capacitor/core": "^8.3.0",
@@ -33,7 +33,8 @@
33
33
  "mailparser": "^3.7.2",
34
34
  "nodemailer": "^7.0.0",
35
35
  "quill": "^2.0.3",
36
- "ws": "^8.18.0"
36
+ "ws": "^8.18.0",
37
+ "sql.js": "^1.14.1"
37
38
  },
38
39
  "devDependencies": {
39
40
  "@types/mailparser": "^3.4.6"
@@ -68,6 +69,7 @@
68
69
  "mailparser": "^3.7.2",
69
70
  "nodemailer": "^7.0.0",
70
71
  "quill": "^2.0.3",
71
- "ws": "^8.18.0"
72
+ "ws": "^8.18.0",
73
+ "sql.js": "^1.14.1"
72
74
  }
73
75
  }
@@ -1245,24 +1245,32 @@ export class ImapManager extends EventEmitter {
1245
1245
  }
1246
1246
  /** Background body prefetch — download bodies for messages that don't have them */
1247
1247
  async prefetchBodies(accountId) {
1248
- const missing = this.db.getMessagesWithoutBody(accountId, 25);
1249
- if (missing.length === 0)
1250
- return;
1251
- console.log(` [prefetch] ${accountId}: ${missing.length} bodies to fetch`);
1252
- let fetched = 0;
1253
- for (const msg of missing) {
1254
- try {
1255
- const result = await this.fetchMessageBody(accountId, msg.folderId, msg.uid);
1256
- if (result)
1257
- fetched++;
1258
- }
1259
- catch (e) {
1260
- console.error(` [prefetch] ${accountId}/${msg.uid}: ${e.message}`);
1261
- break; // Stop on error — don't hammer a broken connection
1248
+ // Fetch ALL missing bodies in one pass — don't wait for next sync cycle
1249
+ let totalFetched = 0;
1250
+ let errors = 0;
1251
+ while (true) {
1252
+ const missing = this.db.getMessagesWithoutBody(accountId, 100);
1253
+ if (missing.length === 0)
1254
+ break;
1255
+ if (totalFetched === 0)
1256
+ console.log(` [prefetch] ${accountId}: ${missing.length}+ bodies to fetch`);
1257
+ for (const msg of missing) {
1258
+ try {
1259
+ const result = await this.fetchMessageBody(accountId, msg.folderId, msg.uid);
1260
+ if (result)
1261
+ totalFetched++;
1262
+ }
1263
+ catch (e) {
1264
+ errors++;
1265
+ if (errors >= 3) {
1266
+ console.error(` [prefetch] ${accountId}: stopping after ${errors} errors (${totalFetched} cached)`);
1267
+ return;
1268
+ }
1269
+ }
1262
1270
  }
1263
1271
  }
1264
- if (fetched > 0)
1265
- console.log(` [prefetch] ${accountId}: ${fetched} bodies cached`);
1272
+ if (totalFetched > 0)
1273
+ console.log(` [prefetch] ${accountId}: ${totalFetched} bodies cached (done)`);
1266
1274
  }
1267
1275
  /** Get the body store for direct access */
1268
1276
  getBodyStore() {
@@ -54,10 +54,10 @@ export class GmailApiProvider {
54
54
  ...options.headers,
55
55
  },
56
56
  });
57
- if (res.status === 429) {
58
- // Rate limited — back off and retry
57
+ if (res.status === 429 || res.status >= 500) {
58
+ // Rate limited or server error — back off and retry
59
59
  const delay = (attempt + 1) * 2000;
60
- console.log(` [gmail] Rate limited, waiting ${delay / 1000}s...`);
60
+ console.log(` [gmail] ${res.status} error, waiting ${delay / 1000}s...`);
61
61
  await new Promise(r => setTimeout(r, delay));
62
62
  continue;
63
63
  }
@@ -67,7 +67,7 @@ export class GmailApiProvider {
67
67
  }
68
68
  return res.json();
69
69
  }
70
- throw new Error("Gmail API: rate limited after 3 retries");
70
+ throw new Error("Gmail API: failed after 3 retries");
71
71
  }
72
72
  async listFolders() {
73
73
  const data = await this.fetch("/labels");
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Android bootstrap — wires WebMailxDB + WebMessageStore + GmailApiWebProvider + WebMailxService
3
+ * into the mailxapi bridge. This replaces Node.js backend for Android WebView.
4
+ *
5
+ * On Android, everything runs in the same JavaScript context:
6
+ * - wa-sqlite for metadata (via WebMailxDB)
7
+ * - IndexedDB for message bodies (via WebMessageStore)
8
+ * - Gmail/Outlook sync via REST APIs (plain fetch — no native bridge needed)
9
+ * - IMAP accounts use BridgeTransport (via MAUI TCP bridge) — not yet implemented
10
+ *
11
+ * The existing client UI (app.ts, components/) is completely unchanged —
12
+ * it calls window.mailxapi.* which this module provides.
13
+ */
14
+ export declare function initAndroid(): Promise<void>;
15
+ export declare function resetStore(): Promise<void>;
16
+ //# sourceMappingURL=android-bootstrap.d.ts.map