@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.
- package/client/android.html +156 -0
- package/client/components/message-viewer.js +5 -3
- package/client/lib/android-bootstrap.js +9 -0
- package/client/lib/api-client.js +83 -75
- package/package.json +6 -4
- package/packages/mailx-imap/index.js +24 -16
- package/packages/mailx-imap/providers/gmail-api.js +4 -4
- package/packages/mailx-store-web/android-bootstrap.d.ts +16 -0
- package/packages/mailx-store-web/android-bootstrap.js +340 -0
- package/packages/mailx-store-web/db.d.ts +112 -0
- package/packages/mailx-store-web/db.js +508 -0
- package/packages/mailx-store-web/gmail-api-web.d.ts +28 -0
- package/packages/mailx-store-web/gmail-api-web.js +231 -0
- package/packages/mailx-store-web/index.d.ts +10 -0
- package/packages/mailx-store-web/index.js +10 -0
- package/packages/mailx-store-web/package.json +19 -0
- package/packages/mailx-store-web/provider-types.d.ts +50 -0
- package/packages/mailx-store-web/provider-types.js +7 -0
- package/packages/mailx-store-web/sql.js.d.ts +29 -0
- package/packages/mailx-store-web/web-jsonrpc.d.ts +20 -0
- package/packages/mailx-store-web/web-jsonrpc.js +94 -0
- package/packages/mailx-store-web/web-message-store.d.ts +16 -0
- package/packages/mailx-store-web/web-message-store.js +89 -0
- package/packages/mailx-store-web/web-service.d.ts +92 -0
- package/packages/mailx-store-web/web-service.js +481 -0
- package/packages/mailx-store-web/web-settings.d.ts +81 -0
- package/packages/mailx-store-web/web-settings.js +421 -0
|
@@ -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">×</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.
|
|
88
|
-
unsubBtn.
|
|
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
|
package/client/lib/api-client.js
CHANGED
|
@@ -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
|
-
//
|
|
10
|
-
|
|
11
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
283
|
+
if (hasIPC())
|
|
284
|
+
return getIpc().getVersion();
|
|
277
285
|
return api("/version");
|
|
278
286
|
}
|
|
279
287
|
export function getSettings() {
|
|
280
|
-
if (hasIPC)
|
|
281
|
-
return
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
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 (
|
|
1265
|
-
console.log(` [prefetch] ${accountId}: ${
|
|
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]
|
|
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:
|
|
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
|