@bobfrankston/mailx 1.0.45 → 1.0.46

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/app.js CHANGED
@@ -5,7 +5,7 @@
5
5
  import { initFolderTree, refreshFolderTree } from "./components/folder-tree.js";
6
6
  import { initMessageList, loadMessages, loadUnifiedInbox, loadSearchResults, reloadCurrentFolder } from "./components/message-list.js";
7
7
  import { showMessage, getCurrentMessage } from "./components/message-viewer.js";
8
- import { connectWebSocket, onWsEvent, triggerSync, getAccounts, getFolders } from "./lib/api-client.js";
8
+ import { connectWebSocket, onWsEvent, triggerSync, getAccounts, getFolders, deleteMessage, undeleteMessage, restartServer, getSyncPending } from "./lib/api-client.js";
9
9
  // ── New message badge (favicon + title) ──
10
10
  let baseTitle = "mailx";
11
11
  let lastSeenCount = 0;
@@ -180,7 +180,7 @@ document.getElementById("btn-restart")?.addEventListener("click", async () => {
180
180
  if (statusSync)
181
181
  statusSync.textContent = "Restarting...";
182
182
  try {
183
- await fetch("/api/restart", { method: "POST" });
183
+ await restartServer();
184
184
  }
185
185
  catch { /* server is shutting down */ }
186
186
  // Server broadcasts reload event; if missed, WebSocket reconnect will trigger page reload
@@ -247,11 +247,7 @@ async function deleteCurrentMessage() {
247
247
  return;
248
248
  const { accountId, message } = current;
249
249
  try {
250
- const res = await fetch(`/api/message/${accountId}/${message.uid}`, { method: "DELETE" });
251
- if (!res.ok) {
252
- const err = await res.json().catch(() => ({ error: res.statusText }));
253
- throw new Error(err.error || res.statusText);
254
- }
250
+ await deleteMessage(accountId, message.uid);
255
251
  lastDeleted = { accountId, uid: message.uid, folderId: message.folderId, subject: message.subject };
256
252
  // Show undo notification in status bar
257
253
  const statusSync = document.getElementById("status-sync");
@@ -296,15 +292,7 @@ async function undoDelete() {
296
292
  return;
297
293
  const { accountId, uid, folderId } = lastDeleted;
298
294
  try {
299
- const res = await fetch(`/api/message/${accountId}/${uid}/undelete`, {
300
- method: "POST",
301
- headers: { "Content-Type": "application/json" },
302
- body: JSON.stringify({ folderId }),
303
- });
304
- if (!res.ok) {
305
- const err = await res.json().catch(() => ({ error: res.statusText }));
306
- throw new Error(err.error || res.statusText);
307
- }
295
+ await undeleteMessage(accountId, uid, folderId);
308
296
  const statusSync = document.getElementById("status-sync");
309
297
  if (statusSync)
310
298
  statusSync.textContent = "Message restored";
@@ -638,10 +626,7 @@ fetch("/api/version").then(r => r.json()).then(d => {
638
626
  let serverDown = false;
639
627
  setInterval(async () => {
640
628
  try {
641
- const res = await fetch("/api/sync/pending");
642
- if (!res.ok)
643
- return;
644
- const data = await res.json();
629
+ const data = await getSyncPending();
645
630
  const el = document.getElementById("status-pending");
646
631
  if (el) {
647
632
  el.textContent = data.pending > 0 ? `↻ ${data.pending} pending` : "";
@@ -2,7 +2,7 @@
2
2
  * Folder tree component -- renders account folders with hierarchy,
3
3
  * expand/collapse, and optional unified inbox.
4
4
  */
5
- import { getAccounts, getFolders } from "../lib/api-client.js";
5
+ import { getAccounts, getFolders, moveMessage, markFolderRead, createFolder, renameFolder, deleteFolder, emptyFolder } from "../lib/api-client.js";
6
6
  import { showContextMenu } from "./context-menu.js";
7
7
  let onFolderSelect;
8
8
  let onUnifiedInbox = null;
@@ -166,7 +166,7 @@ function renderNode(node, container, depth) {
166
166
  const items = [
167
167
  { label: "Mark all read", action: async () => {
168
168
  try {
169
- await fetch(`/api/folder/${node.accountId}/${node.id}/mark-read`, { method: "POST" });
169
+ await markFolderRead(node.accountId, node.id);
170
170
  const treeContainer = document.getElementById("folder-tree");
171
171
  if (treeContainer)
172
172
  loadFolderTree(treeContainer);
@@ -179,11 +179,7 @@ function renderNode(node, container, depth) {
179
179
  if (!name)
180
180
  return;
181
181
  try {
182
- await fetch(`/api/folder/${node.accountId}`, {
183
- method: "POST",
184
- headers: { "Content-Type": "application/json" },
185
- body: JSON.stringify({ parentPath: node.path, name }),
186
- });
182
+ await createFolder(node.accountId, node.path, name);
187
183
  const treeContainer = document.getElementById("folder-tree");
188
184
  if (treeContainer)
189
185
  loadFolderTree(treeContainer);
@@ -197,11 +193,7 @@ function renderNode(node, container, depth) {
197
193
  if (!newName || newName === node.name)
198
194
  return;
199
195
  try {
200
- await fetch(`/api/folder/${node.accountId}/${node.id}/rename`, {
201
- method: "POST",
202
- headers: { "Content-Type": "application/json" },
203
- body: JSON.stringify({ newName }),
204
- });
196
+ await renameFolder(node.accountId, node.id, newName);
205
197
  const treeContainer = document.getElementById("folder-tree");
206
198
  if (treeContainer)
207
199
  loadFolderTree(treeContainer);
@@ -214,7 +206,7 @@ function renderNode(node, container, depth) {
214
206
  if (!confirm(`Delete folder "${node.name}"? Messages will be moved to Trash.`))
215
207
  return;
216
208
  try {
217
- await fetch(`/api/folder/${node.accountId}/${node.id}`, { method: "DELETE" });
209
+ await deleteFolder(node.accountId, node.id);
218
210
  const treeContainer = document.getElementById("folder-tree");
219
211
  if (treeContainer)
220
212
  loadFolderTree(treeContainer);
@@ -230,7 +222,7 @@ function renderNode(node, container, depth) {
230
222
  if (!confirm(`Permanently delete all messages in "${node.name}"?`))
231
223
  return;
232
224
  try {
233
- await fetch(`/api/folder/${node.accountId}/${node.id}/empty`, { method: "POST" });
225
+ await emptyFolder(node.accountId, node.id);
234
226
  const treeContainer = document.getElementById("folder-tree");
235
227
  if (treeContainer)
236
228
  loadFolderTree(treeContainer);
@@ -267,18 +259,8 @@ function renderNode(node, container, depth) {
267
259
  try {
268
260
  let moved = 0;
269
261
  for (const msg of toMove) {
270
- const body = { targetFolderId: node.id };
271
- if (msg.accountId !== node.accountId)
272
- body.targetAccountId = node.accountId;
273
- const res = await fetch(`/api/message/${msg.accountId}/${msg.uid}/move`, {
274
- method: "POST",
275
- headers: { "Content-Type": "application/json" },
276
- body: JSON.stringify(body),
277
- });
278
- if (!res.ok) {
279
- const err = await res.json().catch(() => ({ error: res.statusText }));
280
- throw new Error(err.error);
281
- }
262
+ const targetAccountId = msg.accountId !== node.accountId ? node.accountId : undefined;
263
+ await moveMessage(msg.accountId, msg.uid, node.id, targetAccountId);
282
264
  moved++;
283
265
  }
284
266
  if (statusEl)
@@ -2,7 +2,7 @@
2
2
  * Message list component -- renders paginated message rows.
3
3
  * Loads more messages on scroll.
4
4
  */
5
- import { getMessages, getUnifiedInbox, searchMessages } from "../lib/api-client.js";
5
+ import { getMessages, getUnifiedInbox, searchMessages, updateFlags } from "../lib/api-client.js";
6
6
  /** Clear the message viewer when no message is selected */
7
7
  function clearViewer() {
8
8
  const bodyEl = document.getElementById("mv-body");
@@ -302,11 +302,7 @@ function appendMessages(body, accountId, items) {
302
302
  ? currentFlags.filter((f) => f !== "\\Flagged")
303
303
  : [...currentFlags, "\\Flagged"];
304
304
  try {
305
- await fetch(`/api/message/${msgAccountId}/${msg.uid}/flags`, {
306
- method: "PATCH",
307
- headers: { "Content-Type": "application/json" },
308
- body: JSON.stringify({ flags: newFlags }),
309
- });
305
+ await updateFlags(msgAccountId, msg.uid, newFlags);
310
306
  msg.flags = newFlags;
311
307
  row.classList.toggle("flagged");
312
308
  flag.textContent = row.classList.contains("flagged") ? "\u2605" : "\u2606";
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Message viewer component -- displays full message in sandboxed iframe.
3
3
  */
4
- import { getMessage, updateFlags } from "../lib/api-client.js";
4
+ import { getMessage, updateFlags, allowRemoteContent } from "../lib/api-client.js";
5
5
  /** Currently displayed message (for reply/forward) */
6
6
  let currentMessage = null;
7
7
  let currentAccountId = "";
@@ -189,30 +189,18 @@ export async function showMessage(accountId, uid, folderId, specialUse, isRetry
189
189
  };
190
190
  banner.querySelector("#btn-load-remote").addEventListener("click", loadRemote);
191
191
  banner.querySelector("#btn-allow-sender")?.addEventListener("click", async () => {
192
- await fetch("/api/settings/allow-remote", {
193
- method: "POST",
194
- headers: { "Content-Type": "application/json" },
195
- body: JSON.stringify({ type: "sender", value: senderAddr }),
196
- });
192
+ await allowRemoteContent("sender", senderAddr);
197
193
  loadRemote();
198
194
  });
199
195
  banner.querySelector("#btn-allow-domain")?.addEventListener("click", async () => {
200
- await fetch("/api/settings/allow-remote", {
201
- method: "POST",
202
- headers: { "Content-Type": "application/json" },
203
- body: JSON.stringify({ type: "domain", value: senderDomain }),
204
- });
196
+ await allowRemoteContent("domain", senderDomain);
205
197
  loadRemote();
206
198
  });
207
199
  banner.querySelector("#btn-allow-to")?.addEventListener("click", async () => {
208
200
  const addr = deliveredTo || toAddr;
209
201
  if (!addr)
210
202
  return;
211
- await fetch("/api/settings/allow-remote", {
212
- method: "POST",
213
- headers: { "Content-Type": "application/json" },
214
- body: JSON.stringify({ type: "recipient", value: addr }),
215
- });
203
+ await allowRemoteContent("recipient", addr);
216
204
  loadRemote();
217
205
  });
218
206
  }
@@ -261,7 +261,7 @@ async function saveDraft() {
261
261
  return; // empty
262
262
  lastDraftContent = content;
263
263
  try {
264
- const res = await fetch("/api/draft", {
264
+ const data = await fetch("/api/draft", {
265
265
  method: "POST",
266
266
  headers: { "Content-Type": "application/json" },
267
267
  body: JSON.stringify({
@@ -273,12 +273,9 @@ async function saveDraft() {
273
273
  cc: ccInput.value,
274
274
  previousDraftUid: draftUid,
275
275
  }),
276
- });
277
- if (res.ok) {
278
- const data = await res.json();
279
- if (data.draftUid)
280
- draftUid = data.draftUid;
281
- }
276
+ }).then(r => r.ok ? r.json() : null);
277
+ if (data?.draftUid)
278
+ draftUid = data.draftUid;
282
279
  }
283
280
  catch { /* ignore draft save errors */ }
284
281
  }
@@ -122,6 +122,81 @@ export function allowRemoteContent(type, value) {
122
122
  body: JSON.stringify({ type, value })
123
123
  });
124
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.
128
+ export function deleteMessage(accountId, uid) {
129
+ if (hasIPC)
130
+ return mailxapi.deleteMessage(accountId, uid);
131
+ return api(`/message/${accountId}/${uid}`, { method: "DELETE" });
132
+ }
133
+ export function undeleteMessage(accountId, uid, folderId) {
134
+ if (hasIPC)
135
+ return mailxapi.undeleteMessage(accountId, uid, folderId);
136
+ return api(`/message/${accountId}/${uid}/undelete`, {
137
+ method: "POST",
138
+ body: JSON.stringify({ folderId })
139
+ });
140
+ }
141
+ export function moveMessage(accountId, uid, targetFolderId, targetAccountId) {
142
+ if (hasIPC)
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
+ });
151
+ }
152
+ export function restartServer() {
153
+ if (hasIPC)
154
+ return mailxapi.restart?.();
155
+ return api("/restart", { method: "POST" }).catch(() => { });
156
+ }
157
+ // ── Folder management ──
158
+ export function markFolderRead(accountId, folderId) {
159
+ if (hasIPC)
160
+ return mailxapi.markFolderRead?.(accountId, folderId);
161
+ return api(`/folder/${accountId}/${folderId}/mark-read`, { method: "POST" });
162
+ }
163
+ export function createFolder(accountId, parentPath, name) {
164
+ if (hasIPC)
165
+ return mailxapi.createFolder?.(accountId, parentPath, name);
166
+ return api(`/folder/${accountId}`, {
167
+ method: "POST",
168
+ body: JSON.stringify({ parentPath, name })
169
+ });
170
+ }
171
+ export function renameFolder(accountId, folderId, newName) {
172
+ if (hasIPC)
173
+ return mailxapi.renameFolder?.(accountId, folderId, newName);
174
+ return api(`/folder/${accountId}/${folderId}/rename`, {
175
+ method: "POST",
176
+ body: JSON.stringify({ newName })
177
+ });
178
+ }
179
+ export function deleteFolder(accountId, folderId) {
180
+ if (hasIPC)
181
+ return mailxapi.deleteFolder?.(accountId, folderId);
182
+ return api(`/folder/${accountId}/${folderId}`, { method: "DELETE" });
183
+ }
184
+ export function emptyFolder(accountId, folderId) {
185
+ if (hasIPC)
186
+ return mailxapi.emptyFolder?.(accountId, folderId);
187
+ return api(`/folder/${accountId}/${folderId}/empty`, { method: "POST" });
188
+ }
189
+ // ── Compose ──
190
+ export function sendMessage(body) {
191
+ if (hasIPC)
192
+ return mailxapi.sendMessage?.(body);
193
+ return api("/send", { method: "POST", body: JSON.stringify(body) });
194
+ }
195
+ export function saveDraft(body) {
196
+ if (hasIPC)
197
+ return mailxapi.saveDraft?.(body);
198
+ return api("/draft", { method: "POST", body: JSON.stringify(body) });
199
+ }
125
200
  const eventHandlers = [];
126
201
  export function onEvent(handler) {
127
202
  eventHandlers.push(handler);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/mailx",
3
- "version": "1.0.45",
3
+ "version": "1.0.46",
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,7 +20,7 @@
20
20
  "postinstall": "node launcher/builder/postinstall.js"
21
21
  },
22
22
  "dependencies": {
23
- "@bobfrankston/iflow": "^1.0.25",
23
+ "@bobfrankston/iflow": "^1.0.26",
24
24
  "@bobfrankston/miscinfo": "^1.0.6",
25
25
  "@bobfrankston/oauthsupport": "^1.0.11",
26
26
  "@bobfrankston/rust-builder": "^0.1.2",