@bobfrankston/mailx 1.0.13 → 1.0.15
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/README.md +11 -7
- package/bin/mailx.js +8 -2
- package/client/app.js +39 -17
- package/client/components/message-list.js +46 -42
- package/client/components/message-viewer.js +16 -7
- package/client/index.html +20 -18
- package/client/lib/api-client.js +11 -2
- package/client/styles/components.css +20 -25
- package/launcher/bin/mailx-app-linux +0 -0
- package/launcher/bin/mailx-app.exe +0 -0
- package/launcher/builder/build-config.json +11 -0
- package/launcher/builder/postinstall.js +21 -0
- package/package.json +4 -4
- package/packages/mailx-api/index.js +84 -10
- package/packages/mailx-core/index.js +17 -6
- package/packages/mailx-imap/index.d.ts +4 -0
- package/packages/mailx-imap/index.js +37 -4
- package/packages/mailx-store/db.d.ts +1 -1
- package/packages/mailx-store/db.js +14 -4
- package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Breadcrumbs +0 -0
- package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Crashpad/metadata +0 -0
- package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Crashpad/settings.dat +0 -0
- package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Crashpad/throttle_store.dat +0 -1
- package/launcher/bin/mailx-app.exe.WebView2/EBWebView/CrashpadMetrics-active.pma +0 -0
- package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/BrowsingTopicsSiteData +0 -0
- package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/Cache/No_Vary_Search/journal.baj +0 -1
- package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/DIPS +0 -0
- package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/DashTrackerDatabase +0 -0
- package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/EdgeJourneys/EdgeJourneys.db +0 -0
- package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/Extension Rules/LOCK +0 -0
- package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/Extension Rules/LOG +0 -3
- package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/Extension Rules/MANIFEST-000001 +0 -0
- package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/Extension Scripts/LOCK +0 -0
- package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/Extension Scripts/LOG +0 -3
- package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/Extension Scripts/MANIFEST-000001 +0 -0
- package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/Extension State/LOCK +0 -0
- package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/Extension State/LOG +0 -3
- package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/Extension State/MANIFEST-000001 +0 -0
- package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/ExtensionActivityComp +0 -0
- package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/ExtensionActivityEdge +0 -0
- package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/Favicons +0 -0
- package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/History +0 -0
- package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/History-journal +0 -0
- package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/IndexedDB/devtools_devtools_0.indexeddb.leveldb/LOCK +0 -0
- package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/IndexedDB/devtools_devtools_0.indexeddb.leveldb/LOG +0 -3
- package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/IndexedDB/devtools_devtools_0.indexeddb.leveldb/MANIFEST-000001 +0 -0
- package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/Local Storage/leveldb/LOCK +0 -0
- package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/Local Storage/leveldb/LOG +0 -3
- package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/Local Storage/leveldb/MANIFEST-000001 +0 -0
- package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/Login Data +0 -0
- package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/Login Data For Account +0 -0
- package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/Network/Cookies +0 -0
- package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/Network/Reporting and NEL +0 -0
- package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/Network/Trust Tokens +0 -0
- package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/Network Action Predictor +0 -0
- package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/Preferences +0 -1
- package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/Safe Browsing Network/Safe Browsing Cookies +0 -0
- package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/ServerCertificate +0 -0
- package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/Session Storage/LOCK +0 -0
- package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/Session Storage/LOG +0 -3
- package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/Session Storage/MANIFEST-000001 +0 -0
- package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/Shared Dictionary/db +0 -0
- package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/SharedStorage +0 -0
- package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/Shortcuts +0 -0
- package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/Site Characteristics Database/LOCK +0 -0
- package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/Site Characteristics Database/LOG +0 -3
- package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/Site Characteristics Database/MANIFEST-000001 +0 -0
- package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/Sync Data/LevelDB/LOCK +0 -0
- package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/Sync Data/LevelDB/LOG +0 -3
- package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/Sync Data/LevelDB/MANIFEST-000001 +0 -0
- package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/Top Sites +0 -0
- package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/Vpn Tokens +0 -0
- package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/Web Data +0 -0
- package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/Web Data-journal +0 -0
- package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/WebStorage/QuotaManager +0 -0
- package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/WebStorage/QuotaManager-journal +0 -0
- package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/heavy_ad_intervention_opt_out.db +0 -0
- package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/shared_proto_db/LOCK +0 -0
- package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/shared_proto_db/LOG +0 -3
- package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/shared_proto_db/MANIFEST-000001 +0 -0
- package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/shared_proto_db/metadata/LOCK +0 -0
- package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/shared_proto_db/metadata/LOG +0 -3
- package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/shared_proto_db/metadata/MANIFEST-000001 +0 -0
- package/launcher/bin/mailx-app.exe.WebView2/EBWebView/DeferredBrowserMetrics/BrowserMetrics-69CAD063-BE24.pma +0 -0
- package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Local State +0 -1
- package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Variations +0 -1
- package/launcher/bin/mailx-app.old.exe +0 -0
package/README.md
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
|
-
# mailx
|
|
1
|
+
# mailx -- Email Client
|
|
2
2
|
|
|
3
3
|
A local-first email client with IMAP sync, full offline reading, and a standalone native app. Replaces Thunderbird/Outlook.
|
|
4
4
|
|
|
5
|
+
> **Disclaimer:** This is a personal project written for my own use. I provide it as-is with no promises of support, stability, or fitness for any particular purpose. Use at your own risk.
|
|
6
|
+
|
|
7
|
+
MIT License -- Copyright (c) 2026 Bob Frankston
|
|
8
|
+
|
|
5
9
|
## Quick Start
|
|
6
10
|
|
|
7
11
|
```bash
|
|
@@ -14,7 +18,7 @@ launch.ps1 -restart # Kill existing server and restart
|
|
|
14
18
|
|
|
15
19
|
## Setup
|
|
16
20
|
|
|
17
|
-
### 1. Config Pointer (~/.mailx/config.
|
|
21
|
+
### 1. Config Pointer (~/.mailx/config.jsonc)
|
|
18
22
|
|
|
19
23
|
Points to the shared settings file and local store:
|
|
20
24
|
|
|
@@ -28,7 +32,7 @@ Points to the shared settings file and local store:
|
|
|
28
32
|
- **settingsPath** — Path to shared settings (can be on OneDrive/Dropbox for multi-machine sync)
|
|
29
33
|
- **storePath** — Where cached message bodies are stored (local, not synced)
|
|
30
34
|
|
|
31
|
-
If `config.
|
|
35
|
+
If `config.jsonc` doesn't exist, settings default to `~/.mailx/settings.jsonc`.
|
|
32
36
|
|
|
33
37
|
### 2. Settings File (settings.jsonc)
|
|
34
38
|
|
|
@@ -167,7 +171,7 @@ All data lives in `~/.mailx/` (e.g., `C:\Users\You\.mailx\`):
|
|
|
167
171
|
|
|
168
172
|
| File | Shared? | Purpose |
|
|
169
173
|
|------|---------|---------|
|
|
170
|
-
| config.
|
|
174
|
+
| config.jsonc | No | Points to shared settings dir + local overrides |
|
|
171
175
|
| accounts.jsonc | Yes | IMAP/SMTP account configs |
|
|
172
176
|
| preferences.jsonc | Yes | UI, sync, font settings |
|
|
173
177
|
| allowlist.jsonc | Yes | Remote content sender/domain allow-list |
|
|
@@ -177,7 +181,7 @@ All data lives in `~/.mailx/` (e.g., `C:\Users\You\.mailx\`):
|
|
|
177
181
|
| window.json | No | Window position (per machine) |
|
|
178
182
|
| mailx-YYYY-MM-DD.log | No | Server log (auto-deleted after 7 days) |
|
|
179
183
|
|
|
180
|
-
**Shared** files can live on OneDrive/Dropbox — `config.
|
|
184
|
+
**Shared** files can live on OneDrive/Dropbox — `config.jsonc` points to the shared directory. **Local** files stay on the machine.
|
|
181
185
|
|
|
182
186
|
### Safe to Delete
|
|
183
187
|
|
|
@@ -186,7 +190,7 @@ All data lives in `~/.mailx/` (e.g., `C:\Users\You\.mailx\`):
|
|
|
186
190
|
- **mailx-*.log** — auto-cleaned after 7 days. Safe to delete anytime.
|
|
187
191
|
- **window.json** — resets window position to default 1280×800.
|
|
188
192
|
|
|
189
|
-
### config.
|
|
193
|
+
### config.jsonc
|
|
190
194
|
|
|
191
195
|
```json
|
|
192
196
|
{
|
|
@@ -200,7 +204,7 @@ All data lives in `~/.mailx/` (e.g., `C:\Users\You\.mailx\`):
|
|
|
200
204
|
- **storePath** — where cached .eml files are stored
|
|
201
205
|
- **historyDays** — per-machine override for sync history (shared default is 30)
|
|
202
206
|
|
|
203
|
-
If `config.
|
|
207
|
+
If `config.jsonc` doesn't exist, all settings default to `~/.mailx/`.
|
|
204
208
|
|
|
205
209
|
## Architecture
|
|
206
210
|
|
package/bin/mailx.js
CHANGED
|
@@ -58,9 +58,15 @@ async function main() {
|
|
|
58
58
|
}
|
|
59
59
|
} else {
|
|
60
60
|
// Default: launch native WebView app with IPC
|
|
61
|
+
// Platform-specific binary naming (matches msger pattern)
|
|
62
|
+
let binaryName;
|
|
63
|
+
if (process.platform === "win32") binaryName = "mailx-app.exe";
|
|
64
|
+
else if (process.platform === "darwin") binaryName = process.arch === "arm64" ? "mailx-app-arm64" : "mailx-app";
|
|
65
|
+
else binaryName = process.arch === "arm64" ? "mailx-app-linux-aarch64" : "mailx-app-linux";
|
|
66
|
+
|
|
61
67
|
const launcherPaths = [
|
|
62
|
-
path.join(import.meta.dirname, "..", "launcher", "bin",
|
|
63
|
-
path.join(import.meta.dirname, "..", "launcher", "target", "
|
|
68
|
+
path.join(import.meta.dirname, "..", "launcher", "bin", binaryName),
|
|
69
|
+
path.join(import.meta.dirname, "..", "launcher", "target", "release", binaryName),
|
|
64
70
|
];
|
|
65
71
|
|
|
66
72
|
let launcherPath = launcherPaths.find(p => fs.existsSync(p));
|
package/client/app.js
CHANGED
|
@@ -11,6 +11,10 @@ const folderTree = document.getElementById("folder-tree");
|
|
|
11
11
|
let currentFolderSpecialUse = "";
|
|
12
12
|
initFolderTree(folderTree, (accountId, folderId, folderName, specialUse) => {
|
|
13
13
|
currentFolderSpecialUse = specialUse;
|
|
14
|
+
currentAccountId = accountId;
|
|
15
|
+
currentFolderId = folderId;
|
|
16
|
+
if (searchInput)
|
|
17
|
+
searchInput.value = "";
|
|
14
18
|
loadMessages(accountId, folderId, 1, specialUse);
|
|
15
19
|
document.title = `mailx - ${folderName}`;
|
|
16
20
|
}, () => {
|
|
@@ -207,32 +211,50 @@ document.getElementById("btn-forward")?.addEventListener("click", () => openComp
|
|
|
207
211
|
// ── Search ──
|
|
208
212
|
let searchTimeout;
|
|
209
213
|
const searchInput = document.getElementById("search-input");
|
|
214
|
+
const searchScope = document.getElementById("search-scope");
|
|
215
|
+
function doSearch(immediate = false) {
|
|
216
|
+
const query = searchInput.value.trim();
|
|
217
|
+
if (query.length === 0) {
|
|
218
|
+
reloadCurrentFolder();
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
if (query.length < 2 && !immediate)
|
|
222
|
+
return;
|
|
223
|
+
const scope = searchScope?.value || "all";
|
|
224
|
+
// "This folder" scope: instant client-side filter on debounce, server search on Enter
|
|
225
|
+
if (scope === "current" && !immediate) {
|
|
226
|
+
// Client-side filter of visible rows
|
|
227
|
+
const body = document.getElementById("ml-body");
|
|
228
|
+
if (body) {
|
|
229
|
+
const lower = query.toLowerCase();
|
|
230
|
+
for (const row of body.querySelectorAll(".ml-row")) {
|
|
231
|
+
const text = row.textContent?.toLowerCase() || "";
|
|
232
|
+
row.classList.toggle("filter-hidden", !text.includes(lower));
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
loadSearchResults(query, scope, currentAccountId, currentFolderId);
|
|
238
|
+
document.title = `mailx - Search: ${query}`;
|
|
239
|
+
}
|
|
240
|
+
// Track current folder for scoped search
|
|
241
|
+
let currentAccountId = "";
|
|
242
|
+
let currentFolderId = 0;
|
|
210
243
|
searchInput?.addEventListener("input", () => {
|
|
211
244
|
clearTimeout(searchTimeout);
|
|
212
|
-
searchTimeout = setTimeout(() =>
|
|
213
|
-
const query = searchInput.value.trim();
|
|
214
|
-
if (query.length >= 2) {
|
|
215
|
-
loadSearchResults(query);
|
|
216
|
-
document.title = `mailx - Search: ${query}`;
|
|
217
|
-
}
|
|
218
|
-
else if (query.length === 0) {
|
|
219
|
-
// Clear search — reload current folder
|
|
220
|
-
reloadCurrentFolder();
|
|
221
|
-
}
|
|
222
|
-
}, 400);
|
|
245
|
+
searchTimeout = setTimeout(() => doSearch(false), 300);
|
|
223
246
|
});
|
|
224
|
-
// Enter triggers immediate search
|
|
225
247
|
searchInput?.addEventListener("keydown", (e) => {
|
|
226
248
|
if (e.key === "Enter") {
|
|
227
249
|
clearTimeout(searchTimeout);
|
|
228
|
-
|
|
229
|
-
if (query) {
|
|
230
|
-
loadSearchResults(query);
|
|
231
|
-
document.title = `mailx - Search: ${query}`;
|
|
232
|
-
}
|
|
250
|
+
doSearch(true);
|
|
233
251
|
}
|
|
234
252
|
if (e.key === "Escape") {
|
|
235
253
|
searchInput.value = "";
|
|
254
|
+
// Clear any client-side filters
|
|
255
|
+
const body = document.getElementById("ml-body");
|
|
256
|
+
if (body)
|
|
257
|
+
body.querySelectorAll(".filter-hidden").forEach(r => r.classList.remove("filter-hidden"));
|
|
236
258
|
reloadCurrentFolder();
|
|
237
259
|
}
|
|
238
260
|
});
|
|
@@ -3,11 +3,6 @@
|
|
|
3
3
|
* Loads more messages on scroll.
|
|
4
4
|
*/
|
|
5
5
|
import { getMessages, getUnifiedInbox, searchMessages } from "../lib/api-client.js";
|
|
6
|
-
function clearFilter() {
|
|
7
|
-
const f = document.getElementById("ml-filter-input");
|
|
8
|
-
if (f)
|
|
9
|
-
f.value = "";
|
|
10
|
-
}
|
|
11
6
|
/** Clear the message viewer when no message is selected */
|
|
12
7
|
function clearViewer() {
|
|
13
8
|
const bodyEl = document.getElementById("mv-body");
|
|
@@ -64,26 +59,6 @@ const dateFmt = { year: "numeric", month: "short", day: "numeric", hour: "2-digi
|
|
|
64
59
|
const dateFmtSameYear = { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit", hour12: false };
|
|
65
60
|
export function initMessageList(handler) {
|
|
66
61
|
onMessageSelect = handler;
|
|
67
|
-
// Client-side filter
|
|
68
|
-
const filterInput = document.getElementById("ml-filter-input");
|
|
69
|
-
if (filterInput) {
|
|
70
|
-
filterInput.addEventListener("input", () => {
|
|
71
|
-
const query = filterInput.value.toLowerCase();
|
|
72
|
-
const body = document.getElementById("ml-body");
|
|
73
|
-
if (!body)
|
|
74
|
-
return;
|
|
75
|
-
for (const row of body.querySelectorAll(".ml-row")) {
|
|
76
|
-
const text = row.textContent?.toLowerCase() || "";
|
|
77
|
-
row.classList.toggle("filter-hidden", query.length > 0 && !text.includes(query));
|
|
78
|
-
}
|
|
79
|
-
});
|
|
80
|
-
filterInput.addEventListener("keydown", (e) => {
|
|
81
|
-
if (e.key === "Escape") {
|
|
82
|
-
filterInput.value = "";
|
|
83
|
-
filterInput.dispatchEvent(new Event("input"));
|
|
84
|
-
}
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
62
|
// Infinite scroll
|
|
88
63
|
const body = document.getElementById("ml-body");
|
|
89
64
|
if (body) {
|
|
@@ -97,21 +72,20 @@ export function initMessageList(handler) {
|
|
|
97
72
|
});
|
|
98
73
|
}
|
|
99
74
|
}
|
|
100
|
-
/** Reload the currently displayed folder */
|
|
75
|
+
/** Reload the currently displayed folder (preserves current selection) */
|
|
101
76
|
export function reloadCurrentFolder() {
|
|
102
77
|
if (searchMode) {
|
|
103
78
|
loadSearchResults(currentSearchQuery);
|
|
104
79
|
}
|
|
105
80
|
else if (unifiedMode) {
|
|
106
|
-
loadUnifiedInbox();
|
|
81
|
+
loadUnifiedInbox(false);
|
|
107
82
|
}
|
|
108
83
|
else if (currentAccountId && currentFolderId) {
|
|
109
|
-
loadMessages(currentAccountId, currentFolderId);
|
|
84
|
+
loadMessages(currentAccountId, currentFolderId, 1, "", false);
|
|
110
85
|
}
|
|
111
86
|
}
|
|
112
87
|
/** Load unified inbox (all accounts) */
|
|
113
|
-
export async function loadUnifiedInbox() {
|
|
114
|
-
clearFilter();
|
|
88
|
+
export async function loadUnifiedInbox(autoSelect = true) {
|
|
115
89
|
unifiedMode = true;
|
|
116
90
|
currentPage = 1;
|
|
117
91
|
totalMessages = 0;
|
|
@@ -129,9 +103,11 @@ export async function loadUnifiedInbox() {
|
|
|
129
103
|
}
|
|
130
104
|
body.innerHTML = "";
|
|
131
105
|
appendMessages(body, "", result.items);
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
firstRow
|
|
106
|
+
if (autoSelect) {
|
|
107
|
+
const firstRow = body.querySelector(".ml-row");
|
|
108
|
+
if (firstRow)
|
|
109
|
+
firstRow.click();
|
|
110
|
+
}
|
|
135
111
|
}
|
|
136
112
|
catch (e) {
|
|
137
113
|
if (e.name === "AbortError")
|
|
@@ -140,8 +116,7 @@ export async function loadUnifiedInbox() {
|
|
|
140
116
|
}
|
|
141
117
|
}
|
|
142
118
|
/** Load search results */
|
|
143
|
-
export async function loadSearchResults(query) {
|
|
144
|
-
clearFilter();
|
|
119
|
+
export async function loadSearchResults(query, scope = "all", accountId = "", folderId = 0) {
|
|
145
120
|
searchMode = true;
|
|
146
121
|
unifiedMode = false;
|
|
147
122
|
currentSearchQuery = query;
|
|
@@ -152,7 +127,35 @@ export async function loadSearchResults(query) {
|
|
|
152
127
|
return;
|
|
153
128
|
body.innerHTML = `<div class="ml-empty">Searching...</div>`;
|
|
154
129
|
try {
|
|
155
|
-
|
|
130
|
+
// Regex search: filter client-side
|
|
131
|
+
if (query.startsWith("/") && query.endsWith("/") && query.length > 2) {
|
|
132
|
+
const pattern = query.slice(1, -1);
|
|
133
|
+
let regex;
|
|
134
|
+
try {
|
|
135
|
+
regex = new RegExp(pattern, "i");
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
body.innerHTML = `<div class="ml-empty">Invalid regex</div>`;
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
// Get all messages from current context and filter
|
|
142
|
+
const source = scope === "current" && accountId
|
|
143
|
+
? await getMessages(accountId, folderId, 1, 10000)
|
|
144
|
+
: await searchMessages("*", 1, 10000, "all");
|
|
145
|
+
const matches = source.items.filter((m) => regex.test(m.subject || "") || regex.test(m.from?.name || "") || regex.test(m.from?.address || "") || regex.test(m.preview || ""));
|
|
146
|
+
totalMessages = matches.length;
|
|
147
|
+
if (matches.length === 0) {
|
|
148
|
+
body.innerHTML = `<div class="ml-empty">No regex matches</div>`;
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
body.innerHTML = "";
|
|
152
|
+
appendMessages(body, "", matches);
|
|
153
|
+
const firstRow = body.querySelector(".ml-row");
|
|
154
|
+
if (firstRow)
|
|
155
|
+
firstRow.click();
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
const result = await searchMessages(query, 1, 50, scope, accountId, folderId);
|
|
156
159
|
totalMessages = result.total;
|
|
157
160
|
if (result.items.length === 0) {
|
|
158
161
|
body.innerHTML = `<div class="ml-empty">No results for "${query}"</div>`;
|
|
@@ -165,8 +168,7 @@ export async function loadSearchResults(query) {
|
|
|
165
168
|
body.innerHTML = `<div class="ml-empty">Search error: ${e.message}</div>`;
|
|
166
169
|
}
|
|
167
170
|
}
|
|
168
|
-
export async function loadMessages(accountId, folderId, page = 1, specialUse = "") {
|
|
169
|
-
clearFilter();
|
|
171
|
+
export async function loadMessages(accountId, folderId, page = 1, specialUse = "", autoSelect = true) {
|
|
170
172
|
searchMode = false;
|
|
171
173
|
unifiedMode = false;
|
|
172
174
|
showToInsteadOfFrom = ["sent", "drafts", "outbox"].includes(specialUse) ||
|
|
@@ -193,10 +195,12 @@ export async function loadMessages(accountId, folderId, page = 1, specialUse = "
|
|
|
193
195
|
}
|
|
194
196
|
body.innerHTML = "";
|
|
195
197
|
appendMessages(body, accountId, result.items);
|
|
196
|
-
// Auto-select first message
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
firstRow
|
|
198
|
+
// Auto-select first message only on explicit folder navigation, not sync reload
|
|
199
|
+
if (autoSelect) {
|
|
200
|
+
const firstRow = body.querySelector(".ml-row");
|
|
201
|
+
if (firstRow)
|
|
202
|
+
firstRow.click();
|
|
203
|
+
}
|
|
200
204
|
}
|
|
201
205
|
catch (e) {
|
|
202
206
|
if (e.name === "AbortError")
|
|
@@ -42,13 +42,12 @@ export async function showMessage(accountId, uid, folderId, specialUse) {
|
|
|
42
42
|
headerEl.querySelector(".mv-date").textContent = new Date(msg.date).toLocaleString(undefined, { year: "numeric", month: "short", day: "numeric", hour: "2-digit", minute: "2-digit", hour12: false });
|
|
43
43
|
// Unsubscribe button (upper right of header)
|
|
44
44
|
const unsubBtn = document.getElementById("mv-unsubscribe");
|
|
45
|
-
|
|
46
|
-
const
|
|
47
|
-
|| headerUnsub.match(/<(mailto:[^>]+)>/)?.[1] || "";
|
|
45
|
+
// listUnsubscribe is now a clean URL (https:// or mailto:) from the server
|
|
46
|
+
const unsubUrl = msg.listUnsubscribe || "";
|
|
48
47
|
if (unsubBtn) {
|
|
49
|
-
if (
|
|
48
|
+
if (unsubUrl) {
|
|
50
49
|
unsubBtn.hidden = false;
|
|
51
|
-
unsubBtn.href =
|
|
50
|
+
unsubBtn.href = unsubUrl;
|
|
52
51
|
unsubBtn.target = "_blank";
|
|
53
52
|
unsubBtn.rel = "noopener noreferrer";
|
|
54
53
|
}
|
|
@@ -202,7 +201,8 @@ export async function showMessage(accountId, uid, folderId, specialUse) {
|
|
|
202
201
|
else if (msg.bodyText) {
|
|
203
202
|
const pre = document.createElement("pre");
|
|
204
203
|
pre.style.cssText = "padding: 1rem; white-space: pre-wrap; word-break: break-word;";
|
|
205
|
-
|
|
204
|
+
// Auto-linkify URLs in plain text
|
|
205
|
+
pre.innerHTML = linkifyText(msg.bodyText);
|
|
206
206
|
bodyEl.appendChild(pre);
|
|
207
207
|
}
|
|
208
208
|
else {
|
|
@@ -221,7 +221,9 @@ export async function showMessage(accountId, uid, folderId, specialUse) {
|
|
|
221
221
|
}
|
|
222
222
|
}
|
|
223
223
|
catch (e) {
|
|
224
|
-
|
|
224
|
+
const msg = e.message || "Unknown error";
|
|
225
|
+
bodyEl.innerHTML = `<div class="mv-empty">Failed to load message: ${msg}<br><button onclick="location.reload()">Retry</button></div>`;
|
|
226
|
+
console.error("showMessage error:", e);
|
|
225
227
|
}
|
|
226
228
|
}
|
|
227
229
|
function formatAddr(addr) {
|
|
@@ -229,6 +231,13 @@ function formatAddr(addr) {
|
|
|
229
231
|
return `${addr.name} <${addr.address}>`;
|
|
230
232
|
return addr.address;
|
|
231
233
|
}
|
|
234
|
+
/** Convert plain text URLs into clickable links, escaping HTML */
|
|
235
|
+
function linkifyText(text) {
|
|
236
|
+
// Escape HTML first
|
|
237
|
+
const escaped = text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
238
|
+
// Then linkify URLs
|
|
239
|
+
return escaped.replace(/(https?:\/\/[^\s<>"')\]]+)/g, '<a href="$1" target="_blank" rel="noopener noreferrer">$1</a>');
|
|
240
|
+
}
|
|
232
241
|
function escapeText(s) {
|
|
233
242
|
const div = document.createElement("div");
|
|
234
243
|
div.textContent = s;
|
package/client/index.html
CHANGED
|
@@ -13,24 +13,24 @@
|
|
|
13
13
|
<body>
|
|
14
14
|
<header class="toolbar">
|
|
15
15
|
<div class="toolbar-left">
|
|
16
|
-
<button class="tb-btn" id="btn-compose" title="Compose">
|
|
17
|
-
<span class="tb-icon"
|
|
16
|
+
<button class="tb-btn" id="btn-compose" title="Compose (Ctrl+N)">
|
|
17
|
+
<span class="tb-icon">✏</span> Compose
|
|
18
18
|
</button>
|
|
19
|
-
<button class="tb-btn" id="btn-reply" title="Reply" disabled>
|
|
20
|
-
<span class="tb-icon"
|
|
19
|
+
<button class="tb-btn" id="btn-reply" title="Reply (Ctrl+R)" disabled>
|
|
20
|
+
<span class="tb-icon">↩</span>
|
|
21
21
|
</button>
|
|
22
|
-
<button class="tb-btn" id="btn-reply-all" title="Reply All" disabled>
|
|
23
|
-
<span class="tb-icon"
|
|
22
|
+
<button class="tb-btn" id="btn-reply-all" title="Reply All (Ctrl+Shift+R)" disabled>
|
|
23
|
+
<span class="tb-icon">↩↩</span>
|
|
24
24
|
</button>
|
|
25
25
|
<button class="tb-btn" id="btn-forward" title="Forward" disabled>
|
|
26
|
-
<span class="tb-icon"
|
|
26
|
+
<span class="tb-icon">→</span>
|
|
27
27
|
</button>
|
|
28
28
|
<span class="tb-sep"></span>
|
|
29
|
-
<button class="tb-btn" id="btn-delete" title="Delete" disabled>
|
|
30
|
-
<span class="tb-icon"
|
|
29
|
+
<button class="tb-btn" id="btn-delete" title="Delete (Del)" disabled>
|
|
30
|
+
<span class="tb-icon">🗑</span>
|
|
31
31
|
</button>
|
|
32
32
|
<button class="tb-btn" id="btn-flag" title="Flag" disabled>
|
|
33
|
-
<span class="tb-icon"
|
|
33
|
+
<span class="tb-icon">⚑</span>
|
|
34
34
|
</button>
|
|
35
35
|
</div>
|
|
36
36
|
<div class="toolbar-center">
|
|
@@ -45,14 +45,19 @@
|
|
|
45
45
|
<span id="app-version" class="app-version"></span>
|
|
46
46
|
</div>
|
|
47
47
|
<div class="toolbar-right">
|
|
48
|
-
<search>
|
|
49
|
-
<
|
|
48
|
+
<search class="search-bar">
|
|
49
|
+
<select id="search-scope" title="Search scope">
|
|
50
|
+
<option value="all">All folders</option>
|
|
51
|
+
<option value="current">This folder</option>
|
|
52
|
+
<option value="server">IMAP server</option>
|
|
53
|
+
</select>
|
|
54
|
+
<input type="search" id="search-input" placeholder="Search... (/regex/)" autocomplete="off" title="Search messages. /pattern/ for regex. Qualifiers: from: to: subject:">
|
|
50
55
|
</search>
|
|
51
56
|
<button class="tb-btn" id="btn-sync" title="Sync all folders (F5)">
|
|
52
|
-
<span class="tb-icon"
|
|
57
|
+
<span class="tb-icon">↻</span> Sync
|
|
53
58
|
</button>
|
|
54
59
|
<button class="tb-btn" id="btn-restart" title="Restart server and reload page">
|
|
55
|
-
<span class="tb-icon"
|
|
60
|
+
<span class="tb-icon">⚡</span> Restart
|
|
56
61
|
</button>
|
|
57
62
|
</div>
|
|
58
63
|
</header>
|
|
@@ -68,9 +73,6 @@
|
|
|
68
73
|
|
|
69
74
|
<main class="main-area">
|
|
70
75
|
<section class="message-list" id="message-list">
|
|
71
|
-
<div class="ml-filter">
|
|
72
|
-
<input type="text" id="ml-filter-input" placeholder="Filter..." autocomplete="off">
|
|
73
|
-
</div>
|
|
74
76
|
<div class="ml-header">
|
|
75
77
|
<span class="ml-col ml-col-flag"></span>
|
|
76
78
|
<span class="ml-col ml-col-from" data-sort="from">From</span>
|
|
@@ -92,7 +94,7 @@
|
|
|
92
94
|
<div class="mv-to"></div>
|
|
93
95
|
</div>
|
|
94
96
|
<div class="mv-header-actions">
|
|
95
|
-
<button class="mv-action mv-action-primary" id="mv-edit-draft" hidden>Edit &
|
|
97
|
+
<button class="mv-action mv-action-primary" id="mv-edit-draft" hidden>Edit & Send</button>
|
|
96
98
|
<a class="mv-unsubscribe" id="mv-unsubscribe" hidden>Unsubscribe</a>
|
|
97
99
|
<button class="mv-action" id="mv-view-source" title="View source (.eml)" hidden>Source</button>
|
|
98
100
|
</div>
|
package/client/lib/api-client.js
CHANGED
|
@@ -52,10 +52,19 @@ export function getUnifiedInbox(page = 1, pageSize = 50) {
|
|
|
52
52
|
const signal = newMessageListSignal();
|
|
53
53
|
return api(`/messages/unified/inbox?page=${page}&pageSize=${pageSize}`, { signal });
|
|
54
54
|
}
|
|
55
|
-
export function searchMessages(query, page = 1, pageSize = 50) {
|
|
55
|
+
export function searchMessages(query, page = 1, pageSize = 50, scope = "all", accountId = "", folderId = 0) {
|
|
56
56
|
if (hasIPC)
|
|
57
57
|
return mailxapi.searchMessages(query, page, pageSize);
|
|
58
|
-
|
|
58
|
+
const params = new URLSearchParams({ q: query, page: String(page), pageSize: String(pageSize), scope });
|
|
59
|
+
if (scope === "current" && accountId) {
|
|
60
|
+
params.set("accountId", accountId);
|
|
61
|
+
params.set("folderId", String(folderId));
|
|
62
|
+
}
|
|
63
|
+
if (scope === "server" && accountId) {
|
|
64
|
+
params.set("accountId", accountId);
|
|
65
|
+
params.set("folderId", String(folderId));
|
|
66
|
+
}
|
|
67
|
+
return api(`/search?${params}`);
|
|
59
68
|
}
|
|
60
69
|
export function getMessage(accountId, uid, allowRemote = false, folderId) {
|
|
61
70
|
if (hasIPC)
|
|
@@ -68,14 +68,31 @@
|
|
|
68
68
|
.tb-menu-item input[type="checkbox"] { accent-color: var(--color-accent); }
|
|
69
69
|
.tb-sep { width: 1px; height: 1.2rem; background: var(--color-border); margin: 0 var(--gap-xs); }
|
|
70
70
|
|
|
71
|
+
.search-bar {
|
|
72
|
+
display: flex;
|
|
73
|
+
align-items: center;
|
|
74
|
+
gap: 0;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
#search-scope {
|
|
78
|
+
padding: var(--gap-xs) var(--gap-xs);
|
|
79
|
+
border: 1px solid var(--color-border);
|
|
80
|
+
border-radius: var(--radius-md) 0 0 var(--radius-md);
|
|
81
|
+
background: var(--color-bg-surface);
|
|
82
|
+
color: var(--color-text);
|
|
83
|
+
font-size: var(--font-size-sm);
|
|
84
|
+
border-right: none;
|
|
85
|
+
cursor: pointer;
|
|
86
|
+
}
|
|
87
|
+
|
|
71
88
|
#search-input {
|
|
72
89
|
padding: var(--gap-xs) var(--gap-sm);
|
|
73
90
|
border: 1px solid var(--color-border);
|
|
74
|
-
border-radius: var(--radius-md);
|
|
91
|
+
border-radius: 0 var(--radius-md) var(--radius-md) 0;
|
|
75
92
|
background: var(--color-bg-surface);
|
|
76
93
|
color: var(--color-text);
|
|
77
94
|
font-size: var(--font-size-sm);
|
|
78
|
-
width:
|
|
95
|
+
width: 350px;
|
|
79
96
|
|
|
80
97
|
&::placeholder { color: var(--color-text-muted); }
|
|
81
98
|
&:focus { outline: 1px solid var(--color-accent); border-color: var(--color-accent); }
|
|
@@ -201,7 +218,7 @@
|
|
|
201
218
|
.message-list {
|
|
202
219
|
display: grid;
|
|
203
220
|
grid-template-columns: 1.2em minmax(120px, 200px) auto 1fr;
|
|
204
|
-
grid-template-rows: auto
|
|
221
|
+
grid-template-rows: auto 1fr;
|
|
205
222
|
column-gap: var(--gap-sm);
|
|
206
223
|
overflow: hidden;
|
|
207
224
|
border-right: 1px solid var(--color-border);
|
|
@@ -227,28 +244,6 @@
|
|
|
227
244
|
font-size: var(--font-size-sm);
|
|
228
245
|
}
|
|
229
246
|
|
|
230
|
-
.ml-filter {
|
|
231
|
-
grid-column: 1 / -1;
|
|
232
|
-
padding: 2px var(--gap-xs);
|
|
233
|
-
border-bottom: 1px solid var(--color-border);
|
|
234
|
-
background: var(--color-bg-surface);
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
.ml-filter input {
|
|
238
|
-
width: 100%;
|
|
239
|
-
padding: 2px var(--gap-sm);
|
|
240
|
-
border: 1px solid var(--color-border);
|
|
241
|
-
border-radius: var(--radius-sm);
|
|
242
|
-
background: var(--color-bg);
|
|
243
|
-
color: var(--color-text);
|
|
244
|
-
font-size: var(--font-size-sm);
|
|
245
|
-
outline: none;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
.ml-filter input:focus {
|
|
249
|
-
border-color: var(--color-accent);
|
|
250
|
-
}
|
|
251
|
-
|
|
252
247
|
.ml-row.filter-hidden { display: none; }
|
|
253
248
|
|
|
254
249
|
.ml-header {
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* mailx postinstall — delegates to @bobfrankston/rust-builder
|
|
4
|
+
*/
|
|
5
|
+
import { runPostinstall } from "@bobfrankston/rust-builder/postinstall";
|
|
6
|
+
import path from "path";
|
|
7
|
+
import { fileURLToPath } from "url";
|
|
8
|
+
|
|
9
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
|
|
11
|
+
runPostinstall({
|
|
12
|
+
binaryName: "mailx-app",
|
|
13
|
+
binDir: path.join(__dirname, "..", "bin"),
|
|
14
|
+
binaries: {
|
|
15
|
+
win32: "mailx-app.exe",
|
|
16
|
+
darwin: "mailx-app",
|
|
17
|
+
darwinArm64: "mailx-app-arm64",
|
|
18
|
+
linux: "mailx-app-linux",
|
|
19
|
+
linuxArm64: "mailx-app-linux-aarch64",
|
|
20
|
+
},
|
|
21
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bobfrankston/mailx",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.15",
|
|
4
4
|
"description": "Local-first email client with IMAP sync and standalone native app",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "bin/mailx.js",
|
|
@@ -17,13 +17,13 @@
|
|
|
17
17
|
"start": "node --watch packages/mailx-server/index.js",
|
|
18
18
|
"start:prod": "node packages/mailx-server/index.js",
|
|
19
19
|
"release": "npmglobalize",
|
|
20
|
-
"postinstall": "node
|
|
20
|
+
"postinstall": "node launcher/builder/postinstall.js"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"@bobfrankston/iflow": "^1.0.2",
|
|
24
24
|
"@bobfrankston/miscinfo": "^1.0.5",
|
|
25
25
|
"@bobfrankston/oauthsupport": "^1.0.10",
|
|
26
|
-
"@bobfrankston/
|
|
26
|
+
"@bobfrankston/rust-builder": "^0.1.1",
|
|
27
27
|
"mailparser": "^3.7.2",
|
|
28
28
|
"quill": "^2.0.3",
|
|
29
29
|
"express": "^4.21.0",
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"@bobfrankston/iflow": "file:../MailApps/iflow",
|
|
53
53
|
"@bobfrankston/miscinfo": "file:../../projects/npm/miscinfo",
|
|
54
54
|
"@bobfrankston/oauthsupport": "file:../../projects/oauth/oauthsupport",
|
|
55
|
-
"@bobfrankston/
|
|
55
|
+
"@bobfrankston/rust-builder": "file:../../utils/rust-builder",
|
|
56
56
|
"mailparser": "^3.7.2",
|
|
57
57
|
"quill": "^2.0.3",
|
|
58
58
|
"express": "^4.21.0",
|