@bobfrankston/mailx 1.0.44 → 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/bin/mailx.js CHANGED
@@ -27,9 +27,10 @@ const verbose = hasFlag("verbose");
27
27
 
28
28
  const setupMode = hasFlag("setup");
29
29
  const addMode = hasFlag("add");
30
+ const testMode = hasFlag("test");
30
31
 
31
32
  // Validate arguments
32
- const knownFlags = ["server", "no-browser", "verbose", "external", "kill", "v", "version", "setup", "add"];
33
+ const knownFlags = ["server", "no-browser", "verbose", "external", "kill", "v", "version", "setup", "add", "test"];
33
34
  for (const arg of args) {
34
35
  const flag = arg.replace(/^--?/, "");
35
36
  if (arg.startsWith("-") && !knownFlags.includes(flag)) {
@@ -291,21 +292,60 @@ async function runSetup() {
291
292
  sync: { intervalMinutes: 5, historyDays: 0 },
292
293
  };
293
294
 
294
- // Detect mounted cloud drive to save settings to
295
+ const domain = account.email.split("@")[1]?.toLowerCase() || "";
296
+ const isGmail = domain === "gmail.com" || domain === "googlemail.com";
297
+
298
+ // Ask where to store settings
295
299
  const mountedDrive = findMountedDrive();
300
+ let storageChoice = "local";
301
+
296
302
  if (mountedDrive) {
303
+ const useCloud = await prompt(`Store settings on ${mountedDrive.provider} (syncs across machines)? [Y/n]: `);
304
+ if (!useCloud || useCloud.toLowerCase() !== "n") storageChoice = mountedDrive.provider;
305
+ } else if (isGmail) {
306
+ const useGDrive = await prompt("Store settings on Google Drive (syncs across machines, editable via drive.google.com)? [Y/n]: ");
307
+ if (!useGDrive || useGDrive.toLowerCase() !== "n") storageChoice = "gdrive-api";
308
+ }
309
+
310
+ fs.mkdirSync(mailxDir, { recursive: true });
311
+
312
+ if (storageChoice === "gdrive-api") {
313
+ // Save to Google Drive via API (not mounted)
314
+ console.log("\nSaving settings to Google Drive via API...");
315
+ try {
316
+ const { getCloudProvider } = await import("../packages/mailx-settings/cloud.js");
317
+ const gdrive = getCloudProvider("gdrive");
318
+ if (gdrive) {
319
+ const content = JSON.stringify(settings, null, 2);
320
+ const ok = await gdrive.write("home/.mailx/settings.jsonc", content);
321
+ if (ok) {
322
+ console.log("Settings saved to Google Drive: home/.mailx/settings.jsonc");
323
+ const config = { sharedDir: { provider: "gdrive", path: "home/.mailx" } };
324
+ fs.writeFileSync(path.join(mailxDir, "config.jsonc"), JSON.stringify(config, null, 2));
325
+ console.log("Local config created pointing to Google Drive.");
326
+ } else {
327
+ console.log("Google Drive write failed — saving locally instead.");
328
+ fs.writeFileSync(path.join(mailxDir, "settings.jsonc"), JSON.stringify(settings, null, 2));
329
+ }
330
+ } else {
331
+ console.log("Google Drive API not available — saving locally.");
332
+ fs.writeFileSync(path.join(mailxDir, "settings.jsonc"), JSON.stringify(settings, null, 2));
333
+ }
334
+ } catch (e) {
335
+ console.log(`Google Drive error: ${e.message} — saving locally.`);
336
+ fs.writeFileSync(path.join(mailxDir, "settings.jsonc"), JSON.stringify(settings, null, 2));
337
+ }
338
+ } else if (storageChoice !== "local" && mountedDrive) {
339
+ // Save to mounted cloud drive
297
340
  console.log(`\nSaving settings to ${mountedDrive.provider} at ${mountedDrive.dir}...`);
298
341
  fs.mkdirSync(mountedDrive.dir, { recursive: true });
299
342
  fs.writeFileSync(path.join(mountedDrive.dir, "settings.jsonc"), JSON.stringify(settings, null, 2));
300
- // Create local config pointing to cloud
301
- fs.mkdirSync(mailxDir, { recursive: true });
302
343
  const config = { sharedDir: { provider: mountedDrive.provider, path: "home/.mailx" } };
303
344
  fs.writeFileSync(path.join(mailxDir, "config.jsonc"), JSON.stringify(config, null, 2));
304
345
  console.log("Settings saved to cloud drive + local config created.");
305
346
  } else {
306
347
  // Save locally
307
348
  console.log(`\nSaving settings to ${mailxDir}...`);
308
- fs.mkdirSync(mailxDir, { recursive: true });
309
349
  fs.writeFileSync(path.join(mailxDir, "settings.jsonc"), JSON.stringify(settings, null, 2));
310
350
  }
311
351
 
@@ -330,11 +370,108 @@ function findMountedDrive() {
330
370
  return null;
331
371
  }
332
372
 
373
+ /** Test account connectivity — IMAP connect, SMTP send, sync round-trip */
374
+ async function runTest() {
375
+ console.log("\nmailx — connection test\n");
376
+
377
+ // Start server in-process to access settings
378
+ console.log("Loading settings...");
379
+ const { loadSettings, getSharedDir } = await import("../packages/mailx-settings/index.js");
380
+ const { initLocalConfig } = await import("../packages/mailx-settings/index.js");
381
+ initLocalConfig();
382
+ const settings = loadSettings();
383
+
384
+ if (settings.accounts.length === 0) {
385
+ console.log("No accounts configured. Run: mailx -setup");
386
+ process.exit(1);
387
+ }
388
+
389
+ console.log(`Shared dir: ${getSharedDir()}`);
390
+ console.log(`Accounts: ${settings.accounts.map(a => `${a.label || a.name} <${a.email}>`).join(", ")}\n`);
391
+
392
+ for (const account of settings.accounts) {
393
+ if (!account.enabled) { console.log(` ${account.label || account.id}: SKIPPED (disabled)\n`); continue; }
394
+
395
+ console.log(`Testing ${account.label || account.id} (${account.email}):`);
396
+
397
+ // Test IMAP
398
+ try {
399
+ const { ImapClient, createAutoImapConfig } = await import("@bobfrankston/iflow");
400
+ const config = createAutoImapConfig({
401
+ server: account.imap.host,
402
+ port: account.imap.port,
403
+ username: account.imap.user,
404
+ password: account.imap.password
405
+ });
406
+ const client = new ImapClient(config);
407
+ const folders = await client.getFolderList();
408
+ await client.logout();
409
+ console.log(` IMAP: OK (${folders.length} folders)`);
410
+ } catch (e) {
411
+ console.log(` IMAP: FAILED — ${e.message}`);
412
+ }
413
+
414
+ // Test SMTP
415
+ try {
416
+ const { createTransport } = await import("nodemailer");
417
+ let smtpAuth;
418
+ if (account.smtp.auth === "password") {
419
+ smtpAuth = { user: account.smtp.user, pass: account.smtp.password };
420
+ } else if (account.smtp.auth === "oauth2") {
421
+ // Try to get OAuth token
422
+ const { ImapClient, createAutoImapConfig } = await import("@bobfrankston/iflow");
423
+ const config = createAutoImapConfig({
424
+ server: account.imap.host,
425
+ port: account.imap.port,
426
+ username: account.imap.user,
427
+ });
428
+ if (config.tokenProvider) {
429
+ const accessToken = await config.tokenProvider();
430
+ smtpAuth = { type: "OAuth2", user: account.smtp.user, accessToken };
431
+ }
432
+ }
433
+ const transport = createTransport({
434
+ host: account.smtp.host,
435
+ port: account.smtp.port,
436
+ secure: account.smtp.port === 465,
437
+ auth: smtpAuth,
438
+ tls: { rejectUnauthorized: false },
439
+ });
440
+ await transport.verify();
441
+ console.log(` SMTP: OK`);
442
+
443
+ // Send test message to self
444
+ const testSubject = `mailx test — ${new Date().toLocaleString()}`;
445
+ await transport.sendMail({
446
+ from: `${account.name} <${account.email}>`,
447
+ to: account.email,
448
+ subject: testSubject,
449
+ text: `This is a test message from mailx -test.\nSent: ${new Date().toISOString()}\nAccount: ${account.id}`,
450
+ });
451
+ console.log(` SEND: OK — test message sent to ${account.email}`);
452
+ console.log(` Subject: "${testSubject}"`);
453
+ } catch (e) {
454
+ console.log(` SMTP: FAILED — ${e.message}`);
455
+ }
456
+
457
+ console.log();
458
+ }
459
+
460
+ console.log("Test complete. Check your inbox for the test message(s).");
461
+ process.exit(0);
462
+ }
463
+
333
464
  async function main() {
334
465
  log(`Platform: ${process.platform} ${process.arch}`);
335
466
  log(`Node: ${process.version}`);
336
467
  log(`Mode: ${serverMode ? "server" : setupMode ? "setup" : "auto-detect"}`);
337
468
 
469
+ // Test connectivity
470
+ if (testMode) {
471
+ await runTest();
472
+ return;
473
+ }
474
+
338
475
  // Add account to existing config
339
476
  if (addMode) {
340
477
  const account = await promptForAccount();
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
  }
@@ -17,6 +17,8 @@ const editor = new Quill("#compose-editor", {
17
17
  ]
18
18
  }
19
19
  });
20
+ // Make toolbar buttons non-tabbable so Tab goes straight to editor body
21
+ document.querySelectorAll(".ql-toolbar button, .ql-toolbar select, .ql-toolbar .ql-picker-label").forEach(el => el.setAttribute("tabindex", "-1"));
20
22
  // ── Populate from init data ──
21
23
  const fromSelect = document.getElementById("compose-from-select");
22
24
  const fromCustom = document.getElementById("compose-from-custom");
@@ -259,7 +261,7 @@ async function saveDraft() {
259
261
  return; // empty
260
262
  lastDraftContent = content;
261
263
  try {
262
- const res = await fetch("/api/draft", {
264
+ const data = await fetch("/api/draft", {
263
265
  method: "POST",
264
266
  headers: { "Content-Type": "application/json" },
265
267
  body: JSON.stringify({
@@ -271,12 +273,9 @@ async function saveDraft() {
271
273
  cc: ccInput.value,
272
274
  previousDraftUid: draftUid,
273
275
  }),
274
- });
275
- if (res.ok) {
276
- const data = await res.json();
277
- if (data.draftUid)
278
- draftUid = data.draftUid;
279
- }
276
+ }).then(r => r.ok ? r.json() : null);
277
+ if (data?.draftUid)
278
+ draftUid = data.draftUid;
280
279
  }
281
280
  catch { /* ignore draft save errors */ }
282
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.44",
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.24",
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",