@bobfrankston/mailx 1.0.230 → 1.0.232

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
@@ -1154,9 +1154,10 @@ async function openJsoncEditor(initialFile) {
1154
1154
  <div class="mailx-modal-title">Edit config file</div>
1155
1155
  <label class="mailx-modal-label">File
1156
1156
  <select class="mailx-modal-input" id="jsonc-file">
1157
- <option value="accounts.jsonc">accounts.jsonc</option>
1158
- <option value="allowlist.jsonc">allowlist.jsonc</option>
1159
- <option value="clients.jsonc">clients.jsonc</option>
1157
+ <option value="accounts.jsonc">accounts.jsonc — accounts (shared via Google Drive)</option>
1158
+ <option value="allowlist.jsonc">allowlist.jsonc — remote-content allowlist (shared)</option>
1159
+ <option value="clients.jsonc">clients.jsonc — per-device registrations (shared)</option>
1160
+ <option value="config.jsonc">config.jsonc — local per-machine overrides (not synced)</option>
1160
1161
  </select>
1161
1162
  </label>
1162
1163
  <label class="mailx-modal-label">Contents (JSONC — comments and trailing commas allowed)
@@ -211,9 +211,12 @@ export async function showMessage(accountId, uid, folderId, specialUse, isRetry
211
211
  });
212
212
  }
213
213
  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 });
214
- // Unsubscribe button (upper right of header)
214
+ // Unsubscribe button (upper right of header).
215
+ // - mailto: URLs open a pre-filled compose window (so the unsubscribe
216
+ // reply gets sent from the correct mailx account, not the OS default
217
+ // mail handler)
218
+ // - https: URLs open a new tab
215
219
  const unsubBtn = document.getElementById("mv-unsubscribe");
216
- // listUnsubscribe is now a clean URL (https:// or mailto:) from the server
217
220
  const unsubUrl = msg.listUnsubscribe || "";
218
221
  if (unsubBtn) {
219
222
  if (unsubUrl) {
@@ -223,7 +226,30 @@ export async function showMessage(accountId, uid, folderId, specialUse, isRetry
223
226
  unsubBtn.href = "#";
224
227
  unsubBtn.onclick = (e) => {
225
228
  e.preventDefault();
226
- window.open(unsubUrl, "_blank");
229
+ if (/^mailto:/i.test(unsubUrl)) {
230
+ // Parse mailto:addr?subject=... and pre-fill compose
231
+ const m = unsubUrl.match(/^mailto:([^?]*)(?:\?(.*))?$/i);
232
+ const to = m?.[1] ? decodeURIComponent(m[1]) : "";
233
+ const qs = new URLSearchParams(m?.[2] || "");
234
+ const subject = qs.get("subject") || "Unsubscribe";
235
+ const body = qs.get("body") || "";
236
+ const init = {
237
+ mode: "new",
238
+ accountId: currentAccountId,
239
+ to: to ? [{ name: "", address: to }] : [],
240
+ cc: [],
241
+ subject,
242
+ bodyHtml: body ? `<p>${body}</p>` : "",
243
+ inReplyTo: "",
244
+ references: [],
245
+ accounts: [],
246
+ };
247
+ sessionStorage.setItem("composeInit", JSON.stringify(init));
248
+ document.dispatchEvent(new CustomEvent("mailx-compose", { detail: { mode: "new" } }));
249
+ }
250
+ else {
251
+ window.open(unsubUrl, "_blank");
252
+ }
227
253
  };
228
254
  }
229
255
  else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/mailx",
3
- "version": "1.0.230",
3
+ "version": "1.0.232",
4
4
  "description": "Local-first email client with IMAP sync and standalone native app",
5
5
  "type": "module",
6
6
  "main": "bin/mailx.js",
@@ -24,7 +24,7 @@
24
24
  "@bobfrankston/iflow-node": "^0.1.2",
25
25
  "@bobfrankston/miscinfo": "^1.0.8",
26
26
  "@bobfrankston/oauthsupport": "^1.0.22",
27
- "@bobfrankston/msger": "^0.1.292",
27
+ "@bobfrankston/msger": "^0.1.294",
28
28
  "@capacitor/android": "^8.3.0",
29
29
  "@capacitor/cli": "^8.3.0",
30
30
  "@capacitor/core": "^8.3.0",
@@ -78,7 +78,7 @@
78
78
  "@bobfrankston/iflow-node": "^0.1.2",
79
79
  "@bobfrankston/miscinfo": "^1.0.8",
80
80
  "@bobfrankston/oauthsupport": "^1.0.22",
81
- "@bobfrankston/msger": "^0.1.292",
81
+ "@bobfrankston/msger": "^0.1.294",
82
82
  "@capacitor/android": "^8.3.0",
83
83
  "@capacitor/cli": "^8.3.0",
84
84
  "@capacitor/core": "^8.3.0",
@@ -62,7 +62,8 @@ export declare class MailxService {
62
62
  /** Get all messages in a thread (across folders) for an account. */
63
63
  getThreadMessages(accountId: string, threadId: string): any;
64
64
  /** Read a JSONC config file from the shared cloud dir or local ~/.mailx.
65
- * Names are whitelisted so the UI can't read arbitrary files. */
65
+ * Names are whitelisted so the UI can't read arbitrary files.
66
+ * `config.jsonc` is the local per-machine config (not cloud-synced). */
66
67
  readJsoncFile(name: string): Promise<string | null>;
67
68
  /** Write a JSONC config file. Validates that the content parses as JSONC
68
69
  * (loosely — strips comments/trailing commas) before writing. */
@@ -664,19 +664,29 @@ export class MailxService {
664
664
  return this.db.getThreadMessages(accountId, threadId);
665
665
  }
666
666
  /** Read a JSONC config file from the shared cloud dir or local ~/.mailx.
667
- * Names are whitelisted so the UI can't read arbitrary files. */
667
+ * Names are whitelisted so the UI can't read arbitrary files.
668
+ * `config.jsonc` is the local per-machine config (not cloud-synced). */
668
669
  async readJsoncFile(name) {
669
- const whitelist = ["accounts.jsonc", "allowlist.jsonc", "clients.jsonc"];
670
- if (!whitelist.includes(name))
670
+ const WHITELIST = ["accounts.jsonc", "allowlist.jsonc", "clients.jsonc", "config.jsonc"];
671
+ if (!WHITELIST.includes(name))
671
672
  throw new Error(`File not allowed: ${name}`);
673
+ if (name === "config.jsonc") {
674
+ const configPath = path.join(getConfigDir(), "config.jsonc");
675
+ try {
676
+ return fs.readFileSync(configPath, "utf-8");
677
+ }
678
+ catch {
679
+ return null;
680
+ }
681
+ }
672
682
  const { cloudRead } = await import("@bobfrankston/mailx-settings");
673
683
  return cloudRead(name);
674
684
  }
675
685
  /** Write a JSONC config file. Validates that the content parses as JSONC
676
686
  * (loosely — strips comments/trailing commas) before writing. */
677
687
  async writeJsoncFile(name, content) {
678
- const whitelist = ["accounts.jsonc", "allowlist.jsonc", "clients.jsonc"];
679
- if (!whitelist.includes(name))
688
+ const WHITELIST = ["accounts.jsonc", "allowlist.jsonc", "clients.jsonc", "config.jsonc"];
689
+ if (!WHITELIST.includes(name))
680
690
  throw new Error(`File not allowed: ${name}`);
681
691
  // Validate the content parses before writing
682
692
  const { parse: parseJsonc } = await import("jsonc-parser");
@@ -685,6 +695,11 @@ export class MailxService {
685
695
  if (errors.length) {
686
696
  throw new Error(`JSONC parse error: ${errors.map(e => e.error).join(", ")}`);
687
697
  }
698
+ if (name === "config.jsonc") {
699
+ const configPath = path.join(getConfigDir(), "config.jsonc");
700
+ fs.writeFileSync(configPath, content);
701
+ return;
702
+ }
688
703
  const { cloudWrite } = await import("@bobfrankston/mailx-settings");
689
704
  const ok = await cloudWrite(name, content);
690
705
  if (!ok)