@bobfrankston/mailx 1.0.64 → 1.0.69
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/android/app/build/.npmkeep +0 -0
- package/android/app/build.gradle +54 -0
- package/android/app/capacitor.build.gradle +19 -0
- package/android/app/proguard-rules.pro +21 -0
- package/android/app/src/androidTest/java/com/getcapacitor/myapp/ExampleInstrumentedTest.java +26 -0
- package/android/app/src/main/AndroidManifest.xml +41 -0
- package/android/app/src/main/java/com/frankston/mailx/MainActivity.java +5 -0
- package/android/app/src/main/res/drawable/ic_launcher_background.xml +170 -0
- package/android/app/src/main/res/drawable/splash.png +0 -0
- package/android/app/src/main/res/drawable-land-hdpi/splash.png +0 -0
- package/android/app/src/main/res/drawable-land-mdpi/splash.png +0 -0
- package/android/app/src/main/res/drawable-land-xhdpi/splash.png +0 -0
- package/android/app/src/main/res/drawable-land-xxhdpi/splash.png +0 -0
- package/android/app/src/main/res/drawable-land-xxxhdpi/splash.png +0 -0
- package/android/app/src/main/res/drawable-port-hdpi/splash.png +0 -0
- package/android/app/src/main/res/drawable-port-mdpi/splash.png +0 -0
- package/android/app/src/main/res/drawable-port-xhdpi/splash.png +0 -0
- package/android/app/src/main/res/drawable-port-xxhdpi/splash.png +0 -0
- package/android/app/src/main/res/drawable-port-xxxhdpi/splash.png +0 -0
- package/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml +34 -0
- package/android/app/src/main/res/layout/activity_main.xml +12 -0
- package/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +5 -0
- package/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +5 -0
- package/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
- package/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png +0 -0
- package/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png +0 -0
- package/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
- package/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png +0 -0
- package/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png +0 -0
- package/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
- package/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png +0 -0
- package/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png +0 -0
- package/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
- package/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png +0 -0
- package/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png +0 -0
- package/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
- package/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png +0 -0
- package/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png +0 -0
- package/android/app/src/main/res/values/ic_launcher_background.xml +4 -0
- package/android/app/src/main/res/values/strings.xml +7 -0
- package/android/app/src/main/res/values/styles.xml +22 -0
- package/android/app/src/main/res/xml/file_paths.xml +5 -0
- package/android/app/src/test/java/com/getcapacitor/myapp/ExampleUnitTest.java +18 -0
- package/android/build.gradle +29 -0
- package/android/capacitor.settings.gradle +3 -0
- package/android/gradle/wrapper/gradle-wrapper.jar +0 -0
- package/android/gradle/wrapper/gradle-wrapper.properties +7 -0
- package/android/gradle.properties +23 -0
- package/android/gradlew +251 -0
- package/android/gradlew.bat +94 -0
- package/android/settings.gradle +5 -0
- package/android/variables.gradle +16 -0
- package/client/app.js +90 -32
- package/client/lib/api-client.js +203 -164
- package/client/styles/layout.css +31 -0
- package/download/apks/mailx-debug.apk +0 -0
- package/download/index.html +118 -0
- package/download/versions.json +19 -0
- package/package.json +17 -11
- package/packages/mailx-imap/index.d.ts +2 -0
- package/packages/mailx-imap/index.js +39 -19
package/client/lib/api-client.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* API client —
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* API client — transport-agnostic.
|
|
3
|
+
* Automatically selects the right transport:
|
|
4
|
+
* - HttpTransport: browser mode (REST + WebSocket)
|
|
5
|
+
* - IPC: injected by native launcher (mailxapi global)
|
|
6
|
+
* - BridgeTransport: Android (capacitor-nodejs bridge) — future
|
|
7
|
+
*
|
|
8
|
+
* All server operations MUST go through these centralized methods.
|
|
9
|
+
* Never use fetch("/api/...") directly in components.
|
|
5
10
|
*/
|
|
6
|
-
|
|
7
|
-
// ── HTTP fallback ──
|
|
8
|
-
// Abort controller for message-list requests — cancel stale fetches when folder changes
|
|
11
|
+
// ── HTTP Transport (browser mode) ──
|
|
9
12
|
let messageListAbort = null;
|
|
10
13
|
export function abortMessageListRequests() {
|
|
11
14
|
if (messageListAbort) {
|
|
@@ -18,216 +21,252 @@ function newMessageListSignal() {
|
|
|
18
21
|
messageListAbort = new AbortController();
|
|
19
22
|
return messageListAbort.signal;
|
|
20
23
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
/** Server base URL — empty for same-origin, set by remote config for mobile */
|
|
25
|
+
let serverUrl = localStorage.getItem("mailx-server-url") || "";
|
|
26
|
+
/** Set the server URL (for mobile/remote mode) */
|
|
27
|
+
export function setServerUrl(url) {
|
|
28
|
+
serverUrl = url.replace(/\/$/, "");
|
|
29
|
+
localStorage.setItem("mailx-server-url", serverUrl);
|
|
30
|
+
}
|
|
31
|
+
/** Get the configured server URL */
|
|
32
|
+
export function getServerUrl() { return serverUrl; }
|
|
33
|
+
class HttpTransport {
|
|
34
|
+
handlers = [];
|
|
35
|
+
async call(method, params) {
|
|
36
|
+
const route = httpRoutes[method];
|
|
37
|
+
if (!route)
|
|
38
|
+
throw new Error(`Unknown method: ${method}`);
|
|
39
|
+
const { path, options } = route(params);
|
|
40
|
+
const base = serverUrl ? serverUrl : "";
|
|
41
|
+
let res;
|
|
42
|
+
try {
|
|
43
|
+
res = await fetch(`${base}/api${path}`, {
|
|
44
|
+
headers: { "Content-Type": "application/json" },
|
|
45
|
+
...options,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
catch (e) {
|
|
49
|
+
if (e.name === "AbortError")
|
|
50
|
+
throw e;
|
|
51
|
+
throw new Error("Server offline — check connection or run: mailx -server");
|
|
52
|
+
}
|
|
53
|
+
// Detect HTML error responses (server not reachable, returns error page)
|
|
54
|
+
const ct = res.headers.get("content-type") || "";
|
|
55
|
+
if (ct.includes("text/html")) {
|
|
56
|
+
throw new Error("Server returned HTML instead of JSON — check server URL");
|
|
57
|
+
}
|
|
58
|
+
if (!res.ok) {
|
|
59
|
+
const err = await res.json().catch(() => ({ error: res.statusText }));
|
|
60
|
+
throw new Error(err.error || res.statusText);
|
|
61
|
+
}
|
|
62
|
+
return res.json();
|
|
63
|
+
}
|
|
64
|
+
onEvent(handler) {
|
|
65
|
+
this.handlers.push(handler);
|
|
66
|
+
}
|
|
67
|
+
connect() {
|
|
68
|
+
let wsUrl;
|
|
69
|
+
if (serverUrl) {
|
|
70
|
+
// Remote server — derive WS URL from HTTP URL
|
|
71
|
+
const u = new URL(serverUrl);
|
|
72
|
+
wsUrl = `${u.protocol === "https:" ? "wss:" : "ws:"}//${u.host}`;
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
wsUrl = `${location.protocol === "https:" ? "wss:" : "ws:"}//${location.host}`;
|
|
76
|
+
}
|
|
77
|
+
const ws = new WebSocket(wsUrl);
|
|
78
|
+
ws.onmessage = (ev) => {
|
|
79
|
+
try {
|
|
80
|
+
const event = JSON.parse(ev.data);
|
|
81
|
+
for (const h of this.handlers)
|
|
82
|
+
h(event);
|
|
83
|
+
}
|
|
84
|
+
catch { /* ignore */ }
|
|
85
|
+
};
|
|
86
|
+
ws.onclose = () => { setTimeout(() => this.connect(), 3000); };
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
class IpcTransport {
|
|
90
|
+
async call(method, params) {
|
|
91
|
+
// IPC methods are direct function calls on the injected mailxapi object
|
|
92
|
+
const p = params || {};
|
|
93
|
+
switch (method) {
|
|
94
|
+
case "getAccounts": return mailxapi.getAccounts();
|
|
95
|
+
case "getFolders": return mailxapi.getFolders(p.accountId);
|
|
96
|
+
case "getMessages": return mailxapi.getMessages(p.accountId, p.folderId, p.page, p.pageSize);
|
|
97
|
+
case "getUnifiedInbox": return mailxapi.getUnifiedInbox(p.page, p.pageSize);
|
|
98
|
+
case "searchMessages": return mailxapi.searchMessages(p.query, p.page, p.pageSize);
|
|
99
|
+
case "getMessage": return mailxapi.getMessage(p.accountId, p.uid, p.allowRemote, p.folderId);
|
|
100
|
+
case "updateFlags": return mailxapi.updateFlags(p.accountId, p.uid, p.flags);
|
|
101
|
+
case "triggerSync": return mailxapi.syncAll();
|
|
102
|
+
case "getSyncPending": return mailxapi.getSyncPending();
|
|
103
|
+
case "searchContacts": return mailxapi.searchContacts(p.query);
|
|
104
|
+
case "allowRemoteContent": return mailxapi.allowRemoteContent(p.type, p.value);
|
|
105
|
+
case "deleteMessage": return mailxapi.deleteMessage(p.accountId, p.uid);
|
|
106
|
+
case "undeleteMessage": return mailxapi.undeleteMessage(p.accountId, p.uid, p.folderId);
|
|
107
|
+
case "moveMessage": return mailxapi.moveMessage(p.accountId, p.uid, p.targetFolderId, p.targetAccountId);
|
|
108
|
+
case "restartServer": return mailxapi.restart?.();
|
|
109
|
+
case "markFolderRead": return mailxapi.markFolderRead?.(p.accountId, p.folderId);
|
|
110
|
+
case "createFolder": return mailxapi.createFolder?.(p.accountId, p.parentPath, p.name);
|
|
111
|
+
case "renameFolder": return mailxapi.renameFolder?.(p.accountId, p.folderId, p.newName);
|
|
112
|
+
case "deleteFolder": return mailxapi.deleteFolder?.(p.accountId, p.folderId);
|
|
113
|
+
case "emptyFolder": return mailxapi.emptyFolder?.(p.accountId, p.folderId);
|
|
114
|
+
case "sendMessage": return mailxapi.sendMessage?.(p.body);
|
|
115
|
+
case "saveDraft": return mailxapi.saveDraft?.(p.body);
|
|
116
|
+
default: throw new Error(`Unknown IPC method: ${method}`);
|
|
117
|
+
}
|
|
28
118
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
if (e.name === "AbortError")
|
|
32
|
-
throw e;
|
|
33
|
-
throw new Error("Server offline — run: mailx -server");
|
|
119
|
+
onEvent(handler) {
|
|
120
|
+
mailxapi.onEvent(handler);
|
|
34
121
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
throw new Error(err.error || res.statusText);
|
|
122
|
+
connect() {
|
|
123
|
+
// IPC events are push-based — no connection needed
|
|
38
124
|
}
|
|
39
|
-
return res.json();
|
|
40
125
|
}
|
|
41
|
-
|
|
126
|
+
const httpRoutes = {
|
|
127
|
+
getAccounts: () => ({ path: "/accounts" }),
|
|
128
|
+
getFolders: (p) => ({ path: `/folders/${p.accountId}` }),
|
|
129
|
+
getMessages: (p) => ({
|
|
130
|
+
path: `/messages/${p.accountId}/${p.folderId}?page=${p.page || 1}&pageSize=${p.pageSize || 50}`,
|
|
131
|
+
options: { signal: newMessageListSignal() }
|
|
132
|
+
}),
|
|
133
|
+
getUnifiedInbox: (p) => ({
|
|
134
|
+
path: `/messages/unified/inbox?page=${p.page || 1}&pageSize=${p.pageSize || 50}`,
|
|
135
|
+
options: { signal: newMessageListSignal() }
|
|
136
|
+
}),
|
|
137
|
+
searchMessages: (p) => {
|
|
138
|
+
const params = new URLSearchParams({ q: p.query, page: String(p.page || 1), pageSize: String(p.pageSize || 50), scope: p.scope || "all" });
|
|
139
|
+
if ((p.scope === "current" || p.scope === "server") && p.accountId) {
|
|
140
|
+
params.set("accountId", p.accountId);
|
|
141
|
+
params.set("folderId", String(p.folderId));
|
|
142
|
+
}
|
|
143
|
+
return { path: `/search?${params}` };
|
|
144
|
+
},
|
|
145
|
+
getMessage: (p) => {
|
|
146
|
+
const params = new URLSearchParams();
|
|
147
|
+
if (p.allowRemote)
|
|
148
|
+
params.set("allowRemote", "true");
|
|
149
|
+
if (p.folderId != null)
|
|
150
|
+
params.set("folderId", String(p.folderId));
|
|
151
|
+
const q = params.toString() ? `?${params}` : "";
|
|
152
|
+
return { path: `/message/${p.accountId}/${p.uid}${q}` };
|
|
153
|
+
},
|
|
154
|
+
updateFlags: (p) => ({
|
|
155
|
+
path: `/message/${p.accountId}/${p.uid}/flags`,
|
|
156
|
+
options: { method: "PATCH", body: JSON.stringify({ flags: p.flags }) }
|
|
157
|
+
}),
|
|
158
|
+
triggerSync: () => ({ path: "/sync", options: { method: "POST" } }),
|
|
159
|
+
getSyncPending: () => ({ path: "/sync/pending" }),
|
|
160
|
+
searchContacts: (p) => ({ path: `/contacts?q=${encodeURIComponent(p.query)}` }),
|
|
161
|
+
allowRemoteContent: (p) => ({
|
|
162
|
+
path: "/settings/allow-remote",
|
|
163
|
+
options: { method: "POST", body: JSON.stringify({ type: p.type, value: p.value }) }
|
|
164
|
+
}),
|
|
165
|
+
deleteMessage: (p) => ({ path: `/message/${p.accountId}/${p.uid}`, options: { method: "DELETE" } }),
|
|
166
|
+
undeleteMessage: (p) => ({
|
|
167
|
+
path: `/message/${p.accountId}/${p.uid}/undelete`,
|
|
168
|
+
options: { method: "POST", body: JSON.stringify({ folderId: p.folderId }) }
|
|
169
|
+
}),
|
|
170
|
+
moveMessage: (p) => {
|
|
171
|
+
const body = { targetFolderId: p.targetFolderId };
|
|
172
|
+
if (p.targetAccountId)
|
|
173
|
+
body.targetAccountId = p.targetAccountId;
|
|
174
|
+
return { path: `/message/${p.accountId}/${p.uid}/move`, options: { method: "POST", body: JSON.stringify(body) } };
|
|
175
|
+
},
|
|
176
|
+
restartServer: () => ({ path: "/restart", options: { method: "POST" } }),
|
|
177
|
+
rebuildServer: () => ({ path: "/rebuild", options: { method: "POST" } }),
|
|
178
|
+
markFolderRead: (p) => ({ path: `/folder/${p.accountId}/${p.folderId}/mark-read`, options: { method: "POST" } }),
|
|
179
|
+
createFolder: (p) => ({
|
|
180
|
+
path: `/folder/${p.accountId}`,
|
|
181
|
+
options: { method: "POST", body: JSON.stringify({ parentPath: p.parentPath, name: p.name }) }
|
|
182
|
+
}),
|
|
183
|
+
renameFolder: (p) => ({
|
|
184
|
+
path: `/folder/${p.accountId}/${p.folderId}/rename`,
|
|
185
|
+
options: { method: "POST", body: JSON.stringify({ newName: p.newName }) }
|
|
186
|
+
}),
|
|
187
|
+
deleteFolder: (p) => ({ path: `/folder/${p.accountId}/${p.folderId}`, options: { method: "DELETE" } }),
|
|
188
|
+
emptyFolder: (p) => ({ path: `/folder/${p.accountId}/${p.folderId}/empty`, options: { method: "POST" } }),
|
|
189
|
+
sendMessage: (p) => ({ path: "/send", options: { method: "POST", body: JSON.stringify(p.body) } }),
|
|
190
|
+
saveDraft: (p) => ({ path: "/draft", options: { method: "POST", body: JSON.stringify(p.body) } }),
|
|
191
|
+
};
|
|
192
|
+
// ── Select transport ──
|
|
193
|
+
const hasIPC = typeof mailxapi !== "undefined" && mailxapi?.isApp;
|
|
194
|
+
const transport = hasIPC ? new IpcTransport() : new HttpTransport();
|
|
195
|
+
// ── Public API (unchanged signatures for all callers) ──
|
|
42
196
|
export function getAccounts() {
|
|
43
|
-
|
|
44
|
-
return mailxapi.getAccounts();
|
|
45
|
-
return api("/accounts");
|
|
197
|
+
return transport.call("getAccounts");
|
|
46
198
|
}
|
|
47
199
|
export function getFolders(accountId) {
|
|
48
|
-
|
|
49
|
-
return mailxapi.getFolders(accountId);
|
|
50
|
-
return api(`/folders/${accountId}`);
|
|
200
|
+
return transport.call("getFolders", { accountId });
|
|
51
201
|
}
|
|
52
202
|
export function getMessages(accountId, folderId, page = 1, pageSize = 50) {
|
|
53
|
-
|
|
54
|
-
return mailxapi.getMessages(accountId, folderId, page, pageSize);
|
|
55
|
-
const signal = newMessageListSignal();
|
|
56
|
-
return api(`/messages/${accountId}/${folderId}?page=${page}&pageSize=${pageSize}`, { signal });
|
|
203
|
+
return transport.call("getMessages", { accountId, folderId, page, pageSize });
|
|
57
204
|
}
|
|
58
205
|
export function getUnifiedInbox(page = 1, pageSize = 50) {
|
|
59
|
-
|
|
60
|
-
return mailxapi.getUnifiedInbox(page, pageSize);
|
|
61
|
-
const signal = newMessageListSignal();
|
|
62
|
-
return api(`/messages/unified/inbox?page=${page}&pageSize=${pageSize}`, { signal });
|
|
206
|
+
return transport.call("getUnifiedInbox", { page, pageSize });
|
|
63
207
|
}
|
|
64
208
|
export function searchMessages(query, page = 1, pageSize = 50, scope = "all", accountId = "", folderId = 0) {
|
|
65
|
-
|
|
66
|
-
return mailxapi.searchMessages(query, page, pageSize);
|
|
67
|
-
const params = new URLSearchParams({ q: query, page: String(page), pageSize: String(pageSize), scope });
|
|
68
|
-
if (scope === "current" && accountId) {
|
|
69
|
-
params.set("accountId", accountId);
|
|
70
|
-
params.set("folderId", String(folderId));
|
|
71
|
-
}
|
|
72
|
-
if (scope === "server" && accountId) {
|
|
73
|
-
params.set("accountId", accountId);
|
|
74
|
-
params.set("folderId", String(folderId));
|
|
75
|
-
}
|
|
76
|
-
return api(`/search?${params}`);
|
|
209
|
+
return transport.call("searchMessages", { query, page, pageSize, scope, accountId, folderId });
|
|
77
210
|
}
|
|
78
211
|
export function getMessage(accountId, uid, allowRemote = false, folderId) {
|
|
79
|
-
|
|
80
|
-
return mailxapi.getMessage(accountId, uid, allowRemote, folderId);
|
|
81
|
-
const params = new URLSearchParams();
|
|
82
|
-
if (allowRemote)
|
|
83
|
-
params.set("allowRemote", "true");
|
|
84
|
-
if (folderId != null)
|
|
85
|
-
params.set("folderId", String(folderId));
|
|
86
|
-
const q = params.toString() ? `?${params}` : "";
|
|
87
|
-
return api(`/message/${accountId}/${uid}${q}`);
|
|
212
|
+
return transport.call("getMessage", { accountId, uid, allowRemote, folderId });
|
|
88
213
|
}
|
|
89
214
|
export function updateFlags(accountId, uid, flags) {
|
|
90
|
-
|
|
91
|
-
return mailxapi.updateFlags(accountId, uid, flags);
|
|
92
|
-
return api(`/message/${accountId}/${uid}/flags`, {
|
|
93
|
-
method: "PATCH",
|
|
94
|
-
body: JSON.stringify({ flags })
|
|
95
|
-
});
|
|
215
|
+
return transport.call("updateFlags", { accountId, uid, flags });
|
|
96
216
|
}
|
|
97
217
|
export function triggerSync() {
|
|
98
|
-
|
|
99
|
-
return mailxapi.syncAll();
|
|
100
|
-
return api("/sync", { method: "POST" });
|
|
101
|
-
}
|
|
102
|
-
export function getVersion() {
|
|
103
|
-
if (hasIPC)
|
|
104
|
-
return mailxapi.getVersion();
|
|
105
|
-
return api("/version");
|
|
218
|
+
return transport.call("triggerSync");
|
|
106
219
|
}
|
|
107
220
|
export function getSyncPending() {
|
|
108
|
-
|
|
109
|
-
return mailxapi.getSyncPending();
|
|
110
|
-
return api("/sync/pending");
|
|
221
|
+
return transport.call("getSyncPending");
|
|
111
222
|
}
|
|
112
223
|
export function searchContacts(query) {
|
|
113
|
-
|
|
114
|
-
return mailxapi.searchContacts(query);
|
|
115
|
-
return api(`/contacts?q=${encodeURIComponent(query)}`);
|
|
224
|
+
return transport.call("searchContacts", { query });
|
|
116
225
|
}
|
|
117
226
|
export function allowRemoteContent(type, value) {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
return api("/settings/allow-remote", {
|
|
121
|
-
method: "POST",
|
|
122
|
-
body: JSON.stringify({ type, value })
|
|
123
|
-
});
|
|
124
|
-
}
|
|
125
|
-
// ── Message actions ──
|
|
126
|
-
// IMPORTANT: All server operations MUST go through these centralized methods
|
|
127
|
-
// so IPC mode works. Never use fetch("/api/...") directly in components.
|
|
227
|
+
return transport.call("allowRemoteContent", { type, value });
|
|
228
|
+
}
|
|
128
229
|
export function deleteMessage(accountId, uid) {
|
|
129
|
-
|
|
130
|
-
return mailxapi.deleteMessage(accountId, uid);
|
|
131
|
-
return api(`/message/${accountId}/${uid}`, { method: "DELETE" });
|
|
230
|
+
return transport.call("deleteMessage", { accountId, uid });
|
|
132
231
|
}
|
|
133
232
|
export function undeleteMessage(accountId, uid, folderId) {
|
|
134
|
-
|
|
135
|
-
return mailxapi.undeleteMessage(accountId, uid, folderId);
|
|
136
|
-
return api(`/message/${accountId}/${uid}/undelete`, {
|
|
137
|
-
method: "POST",
|
|
138
|
-
body: JSON.stringify({ folderId })
|
|
139
|
-
});
|
|
233
|
+
return transport.call("undeleteMessage", { accountId, uid, folderId });
|
|
140
234
|
}
|
|
141
235
|
export function moveMessage(accountId, uid, targetFolderId, targetAccountId) {
|
|
142
|
-
|
|
143
|
-
return mailxapi.moveMessage(accountId, uid, targetFolderId, targetAccountId);
|
|
144
|
-
const body = { targetFolderId };
|
|
145
|
-
if (targetAccountId)
|
|
146
|
-
body.targetAccountId = targetAccountId;
|
|
147
|
-
return api(`/message/${accountId}/${uid}/move`, {
|
|
148
|
-
method: "POST",
|
|
149
|
-
body: JSON.stringify(body)
|
|
150
|
-
});
|
|
236
|
+
return transport.call("moveMessage", { accountId, uid, targetFolderId, targetAccountId });
|
|
151
237
|
}
|
|
152
238
|
export function restartServer() {
|
|
153
|
-
|
|
154
|
-
return mailxapi.restart?.();
|
|
155
|
-
return api("/restart", { method: "POST" }).catch(() => { });
|
|
239
|
+
return transport.call("restartServer").catch(() => { });
|
|
156
240
|
}
|
|
157
241
|
export function rebuildServer() {
|
|
158
|
-
return
|
|
242
|
+
return transport.call("rebuildServer").catch(() => { });
|
|
159
243
|
}
|
|
160
|
-
// ── Folder management ──
|
|
161
244
|
export function markFolderRead(accountId, folderId) {
|
|
162
|
-
|
|
163
|
-
return mailxapi.markFolderRead?.(accountId, folderId);
|
|
164
|
-
return api(`/folder/${accountId}/${folderId}/mark-read`, { method: "POST" });
|
|
245
|
+
return transport.call("markFolderRead", { accountId, folderId });
|
|
165
246
|
}
|
|
166
247
|
export function createFolder(accountId, parentPath, name) {
|
|
167
|
-
|
|
168
|
-
return mailxapi.createFolder?.(accountId, parentPath, name);
|
|
169
|
-
return api(`/folder/${accountId}`, {
|
|
170
|
-
method: "POST",
|
|
171
|
-
body: JSON.stringify({ parentPath, name })
|
|
172
|
-
});
|
|
248
|
+
return transport.call("createFolder", { accountId, parentPath, name });
|
|
173
249
|
}
|
|
174
250
|
export function renameFolder(accountId, folderId, newName) {
|
|
175
|
-
|
|
176
|
-
return mailxapi.renameFolder?.(accountId, folderId, newName);
|
|
177
|
-
return api(`/folder/${accountId}/${folderId}/rename`, {
|
|
178
|
-
method: "POST",
|
|
179
|
-
body: JSON.stringify({ newName })
|
|
180
|
-
});
|
|
251
|
+
return transport.call("renameFolder", { accountId, folderId, newName });
|
|
181
252
|
}
|
|
182
253
|
export function deleteFolder(accountId, folderId) {
|
|
183
|
-
|
|
184
|
-
return mailxapi.deleteFolder?.(accountId, folderId);
|
|
185
|
-
return api(`/folder/${accountId}/${folderId}`, { method: "DELETE" });
|
|
254
|
+
return transport.call("deleteFolder", { accountId, folderId });
|
|
186
255
|
}
|
|
187
256
|
export function emptyFolder(accountId, folderId) {
|
|
188
|
-
|
|
189
|
-
return mailxapi.emptyFolder?.(accountId, folderId);
|
|
190
|
-
return api(`/folder/${accountId}/${folderId}/empty`, { method: "POST" });
|
|
257
|
+
return transport.call("emptyFolder", { accountId, folderId });
|
|
191
258
|
}
|
|
192
|
-
// ── Compose ──
|
|
193
259
|
export function sendMessage(body) {
|
|
194
|
-
|
|
195
|
-
return mailxapi.sendMessage?.(body);
|
|
196
|
-
return api("/send", { method: "POST", body: JSON.stringify(body) });
|
|
260
|
+
return transport.call("sendMessage", { body });
|
|
197
261
|
}
|
|
198
262
|
export function saveDraft(body) {
|
|
199
|
-
|
|
200
|
-
return mailxapi.saveDraft?.(body);
|
|
201
|
-
return api("/draft", { method: "POST", body: JSON.stringify(body) });
|
|
263
|
+
return transport.call("saveDraft", { body });
|
|
202
264
|
}
|
|
203
|
-
const eventHandlers = [];
|
|
204
265
|
export function onEvent(handler) {
|
|
205
|
-
|
|
266
|
+
transport.onEvent(handler);
|
|
206
267
|
}
|
|
207
268
|
export function connectEvents() {
|
|
208
|
-
|
|
209
|
-
// IPC events come via mailxapi.onEvent
|
|
210
|
-
mailxapi.onEvent((event) => {
|
|
211
|
-
for (const h of eventHandlers)
|
|
212
|
-
h(event);
|
|
213
|
-
});
|
|
214
|
-
}
|
|
215
|
-
else {
|
|
216
|
-
// WebSocket for HTTP mode
|
|
217
|
-
const protocol = location.protocol === "https:" ? "wss:" : "ws:";
|
|
218
|
-
const ws = new WebSocket(`${protocol}//${location.host}`);
|
|
219
|
-
ws.onmessage = (ev) => {
|
|
220
|
-
try {
|
|
221
|
-
const event = JSON.parse(ev.data);
|
|
222
|
-
for (const h of eventHandlers)
|
|
223
|
-
h(event);
|
|
224
|
-
}
|
|
225
|
-
catch { /* ignore */ }
|
|
226
|
-
};
|
|
227
|
-
ws.onclose = () => {
|
|
228
|
-
setTimeout(connectEvents, 3000);
|
|
229
|
-
};
|
|
230
|
-
}
|
|
269
|
+
transport.connect();
|
|
231
270
|
}
|
|
232
271
|
// Legacy exports for backward compatibility
|
|
233
272
|
export const connectWebSocket = connectEvents;
|
package/client/styles/layout.css
CHANGED
|
@@ -56,6 +56,37 @@ body {
|
|
|
56
56
|
background: var(--color-accent);
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
/* Responsive: mid-width (tablets, foldables) — hide folders, keep list + preview */
|
|
60
|
+
@media (max-width: 1100px) and (min-width: 769px) {
|
|
61
|
+
body {
|
|
62
|
+
grid-template-columns: 1fr;
|
|
63
|
+
grid-template-rows: var(--toolbar-height) auto 1fr var(--statusbar-height);
|
|
64
|
+
grid-template-areas:
|
|
65
|
+
"toolbar"
|
|
66
|
+
"alert"
|
|
67
|
+
"main"
|
|
68
|
+
"status";
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/* Folder panel: overlay slide-in from left (same as narrow) */
|
|
72
|
+
.folder-panel {
|
|
73
|
+
position: fixed;
|
|
74
|
+
left: -280px;
|
|
75
|
+
top: var(--toolbar-height);
|
|
76
|
+
bottom: var(--statusbar-height);
|
|
77
|
+
width: 280px;
|
|
78
|
+
z-index: 50;
|
|
79
|
+
transition: left 0.2s ease;
|
|
80
|
+
background: var(--color-bg);
|
|
81
|
+
border-right: 1px solid var(--color-border);
|
|
82
|
+
box-shadow: 2px 0 8px rgba(0,0,0,0.3);
|
|
83
|
+
}
|
|
84
|
+
.folder-panel.open { left: 0; }
|
|
85
|
+
|
|
86
|
+
/* Show hamburger */
|
|
87
|
+
#btn-menu { display: inline-flex !important; }
|
|
88
|
+
}
|
|
89
|
+
|
|
59
90
|
/* Responsive: narrow viewport — single panel navigation */
|
|
60
91
|
@media (max-width: 768px) {
|
|
61
92
|
body {
|
|
Binary file
|
|
@@ -0,0 +1,118 @@
|
|
|
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 - Download</title>
|
|
7
|
+
<link rel="icon" type="image/svg+xml" href="../client/favicon.svg">
|
|
8
|
+
<style>
|
|
9
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
10
|
+
body { font-family: system-ui, sans-serif; background: #1e1e2e; color: #cdd6f4; min-height: 100vh; padding: 2rem; }
|
|
11
|
+
.container { max-width: 640px; margin: 0 auto; }
|
|
12
|
+
h1 { font-size: 1.8rem; margin-bottom: 0.5rem; }
|
|
13
|
+
h1 .version { font-size: 0.9rem; color: #89b4fa; font-weight: normal; }
|
|
14
|
+
.subtitle { color: #a6adc8; margin-bottom: 2rem; }
|
|
15
|
+
.section { background: #313244; border-radius: 12px; padding: 1.5rem; margin-bottom: 1.5rem; }
|
|
16
|
+
.section h2 { font-size: 1.1rem; margin-bottom: 1rem; color: #89b4fa; }
|
|
17
|
+
.dl-row { display: flex; align-items: center; justify-content: space-between; padding: 0.75rem 0; border-bottom: 1px solid #45475a; }
|
|
18
|
+
.dl-row:last-child { border-bottom: none; }
|
|
19
|
+
.dl-info { flex: 1; }
|
|
20
|
+
.dl-name { font-weight: 600; }
|
|
21
|
+
.dl-meta { font-size: 0.85rem; color: #a6adc8; }
|
|
22
|
+
.btn { display: inline-block; padding: 0.5rem 1.2rem; border-radius: 8px; text-decoration: none; font-weight: 600; font-size: 0.9rem; }
|
|
23
|
+
.btn-primary { background: #89b4fa; color: #1e1e2e; }
|
|
24
|
+
.btn-primary:hover { background: #b4d0fb; }
|
|
25
|
+
.btn-secondary { background: #45475a; color: #cdd6f4; }
|
|
26
|
+
.btn-secondary:hover { background: #585b70; }
|
|
27
|
+
.btn-disabled { background: #45475a; color: #6c7086; pointer-events: none; }
|
|
28
|
+
code { background: #45475a; padding: 2px 6px; border-radius: 4px; font-size: 0.9em; }
|
|
29
|
+
.install-cmd { display: block; background: #11111b; padding: 0.75rem 1rem; border-radius: 8px; margin: 0.5rem 0; font-family: monospace; font-size: 0.9rem; color: #a6e3a1; }
|
|
30
|
+
details { margin-top: 1.5rem; }
|
|
31
|
+
summary { cursor: pointer; color: #89b4fa; font-weight: 600; }
|
|
32
|
+
details ol { padding-left: 1.5rem; margin-top: 0.5rem; color: #a6adc8; }
|
|
33
|
+
details li { margin: 0.5rem 0; }
|
|
34
|
+
footer { text-align: center; margin-top: 2rem; font-size: 0.8rem; color: #6c7086; }
|
|
35
|
+
footer a { color: #89b4fa; text-decoration: none; }
|
|
36
|
+
</style>
|
|
37
|
+
</head>
|
|
38
|
+
<body>
|
|
39
|
+
<div class="container">
|
|
40
|
+
<h1>mailx <span class="version" id="version"></span></h1>
|
|
41
|
+
<p class="subtitle">Local-first email client with IMAP sync. Replaces Thunderbird/Outlook.</p>
|
|
42
|
+
|
|
43
|
+
<div class="section">
|
|
44
|
+
<h2>Android</h2>
|
|
45
|
+
<div id="apk-list">
|
|
46
|
+
<div class="dl-row">
|
|
47
|
+
<div class="dl-info">
|
|
48
|
+
<span class="dl-name">Loading...</span>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
<div class="section">
|
|
55
|
+
<h2>Desktop (Windows / Linux / Mac)</h2>
|
|
56
|
+
<div class="dl-row">
|
|
57
|
+
<div class="dl-info">
|
|
58
|
+
<span class="dl-name">Install via npm</span>
|
|
59
|
+
<span class="dl-meta">Requires Node.js 22+</span>
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
<code class="install-cmd">npm install -g @bobfrankston/mailx && mailx</code>
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
<details>
|
|
66
|
+
<summary>Android installation help</summary>
|
|
67
|
+
<ol>
|
|
68
|
+
<li>Tap Download → tap the notification when done → Install</li>
|
|
69
|
+
<li>If blocked: Settings → Apps → Install unknown apps → allow your browser</li>
|
|
70
|
+
<li>The app needs a mailx server running on your network (desktop or server)</li>
|
|
71
|
+
<li>On first launch, enter your server address (e.g. <code>http://192.168.1.x:9333</code>)</li>
|
|
72
|
+
</ol>
|
|
73
|
+
</details>
|
|
74
|
+
|
|
75
|
+
<footer>
|
|
76
|
+
<a href="https://github.com/BobFrankston/mailx">GitHub</a> ·
|
|
77
|
+
<a href="https://www.npmjs.com/package/@bobfrankston/mailx">npm</a>
|
|
78
|
+
</footer>
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
<script>
|
|
82
|
+
async function load() {
|
|
83
|
+
try {
|
|
84
|
+
const resp = await fetch('versions.json', { cache: 'no-cache' });
|
|
85
|
+
if (!resp.ok) return;
|
|
86
|
+
const v = await resp.json();
|
|
87
|
+
|
|
88
|
+
document.getElementById('version').textContent = 'v' + v.version;
|
|
89
|
+
|
|
90
|
+
const list = document.getElementById('apk-list');
|
|
91
|
+
list.innerHTML = v.apks.map(apk => {
|
|
92
|
+
const size = apk.sizeMB ? apk.sizeMB + ' MB' : '';
|
|
93
|
+
const date = apk.built ? new Date(apk.built).toLocaleDateString() : '';
|
|
94
|
+
return '<div class="dl-row">' +
|
|
95
|
+
'<div class="dl-info">' +
|
|
96
|
+
'<span class="dl-name">' + apk.name + '</span><br>' +
|
|
97
|
+
'<span class="dl-meta">' + apk.description + ' · ' + size + ' · ' + date + '</span>' +
|
|
98
|
+
'</div>' +
|
|
99
|
+
'<a href="' + apk.file + '" class="btn btn-primary" id="dl-' + apk.name + '">Download APK</a>' +
|
|
100
|
+
'</div>';
|
|
101
|
+
}).join('');
|
|
102
|
+
|
|
103
|
+
// Check file exists
|
|
104
|
+
v.apks.forEach(async apk => {
|
|
105
|
+
try {
|
|
106
|
+
const r = await fetch(apk.file, { method: 'HEAD' });
|
|
107
|
+
if (!r.ok) {
|
|
108
|
+
const dl = document.getElementById('dl-' + apk.name);
|
|
109
|
+
if (dl) { dl.className = 'btn btn-disabled'; dl.removeAttribute('href'); dl.textContent = 'N/A'; }
|
|
110
|
+
}
|
|
111
|
+
} catch { }
|
|
112
|
+
});
|
|
113
|
+
} catch { }
|
|
114
|
+
}
|
|
115
|
+
load();
|
|
116
|
+
</script>
|
|
117
|
+
</body>
|
|
118
|
+
</html>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "1.0.68",
|
|
3
|
+
"buildDate": "2026-04-02T21:39:00-04:00",
|
|
4
|
+
"apks": [
|
|
5
|
+
{
|
|
6
|
+
"name": "mailx",
|
|
7
|
+
"description": "mailx email client (connects to your mailx server)",
|
|
8
|
+
"file": "apks/mailx-debug.apk",
|
|
9
|
+
"sizeMB": 4.1,
|
|
10
|
+
"built": "2026-04-02T20:46:00-04:00",
|
|
11
|
+
"default": true
|
|
12
|
+
}
|
|
13
|
+
],
|
|
14
|
+
"downloads": {
|
|
15
|
+
"npm": "npm install -g @bobfrankston/mailx",
|
|
16
|
+
"windows": "npm install -g @bobfrankston/mailx && mailx",
|
|
17
|
+
"linux": "npm install -g @bobfrankston/mailx && mailx -server"
|
|
18
|
+
}
|
|
19
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bobfrankston/mailx",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.69",
|
|
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,16 +20,19 @@
|
|
|
20
20
|
"postinstall": "node launcher/builder/postinstall.js"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@bobfrankston/iflow": "^1.0.
|
|
23
|
+
"@bobfrankston/iflow": "^1.0.32",
|
|
24
24
|
"@bobfrankston/miscinfo": "^1.0.6",
|
|
25
|
-
"@bobfrankston/oauthsupport": "^1.0.
|
|
25
|
+
"@bobfrankston/oauthsupport": "^1.0.17",
|
|
26
26
|
"@bobfrankston/rust-builder": "^0.1.2",
|
|
27
|
-
"
|
|
28
|
-
"
|
|
27
|
+
"@capacitor/android": "^8.3.0",
|
|
28
|
+
"@capacitor/cli": "^8.3.0",
|
|
29
|
+
"@capacitor/core": "^8.3.0",
|
|
29
30
|
"express": "^4.21.0",
|
|
31
|
+
"jsonc-parser": "^3.3.1",
|
|
32
|
+
"mailparser": "^3.7.2",
|
|
30
33
|
"nodemailer": "^7.0.0",
|
|
31
|
-
"
|
|
32
|
-
"
|
|
34
|
+
"quill": "^2.0.3",
|
|
35
|
+
"ws": "^8.18.0"
|
|
33
36
|
},
|
|
34
37
|
"devDependencies": {
|
|
35
38
|
"@types/mailparser": "^3.4.6"
|
|
@@ -55,11 +58,14 @@
|
|
|
55
58
|
"@bobfrankston/miscinfo": "file:../../projects/npm/miscinfo",
|
|
56
59
|
"@bobfrankston/oauthsupport": "file:../../projects/oauth/oauthsupport",
|
|
57
60
|
"@bobfrankston/rust-builder": "file:../../utils/rust-builder",
|
|
58
|
-
"
|
|
59
|
-
"
|
|
61
|
+
"@capacitor/android": "^8.3.0",
|
|
62
|
+
"@capacitor/cli": "^8.3.0",
|
|
63
|
+
"@capacitor/core": "^8.3.0",
|
|
60
64
|
"express": "^4.21.0",
|
|
65
|
+
"jsonc-parser": "^3.3.1",
|
|
66
|
+
"mailparser": "^3.7.2",
|
|
61
67
|
"nodemailer": "^7.0.0",
|
|
62
|
-
"
|
|
63
|
-
"
|
|
68
|
+
"quill": "^2.0.3",
|
|
69
|
+
"ws": "^8.18.0"
|
|
64
70
|
}
|
|
65
71
|
}
|