@bobfrankston/mailx 1.0.207 → 1.0.208

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
@@ -31,14 +31,22 @@ const isDaemon = hasFlag("daemon"); // internal: re-spawned detached process
31
31
  // Auto-detach: re-spawn as background process so terminal returns immediately
32
32
  // Skip for: --verbose (want console), --daemon (already detached),
33
33
  // and any command flags (setup, kill, test, etc.)
34
- if (!verbose && !isDaemon && !process.argv.slice(2).some(a => /^-/.test(a) && !["--no-browser"].includes(a))) {
35
- const { spawn } = await import("node:child_process");
36
- const child = spawn(process.execPath, [...process.argv.slice(1), "--daemon"], {
37
- detached: true,
38
- stdio: "ignore",
39
- windowsHide: true,
40
- });
41
- child.unref();
34
+ if (!verbose && !isDaemon && !process.argv.slice(2).some(a => /^-/.test(a))) {
35
+ const { execSync, spawn } = await import("node:child_process");
36
+ if (process.platform === "win32") {
37
+ // Use wscript to launch without any visible console window
38
+ const args = [...process.argv.slice(1), "--daemon"].map(a => `"${a}"`).join(" ");
39
+ const vbs = `CreateObject("Wscript.Shell").Run """${process.execPath}"" ${args}", 0, False`;
40
+ const tmpVbs = path.join(os.tmpdir(), "mailx-launch.vbs");
41
+ fs.writeFileSync(tmpVbs, vbs);
42
+ execSync(`wscript "${tmpVbs}"`, { stdio: "ignore" });
43
+ }
44
+ else {
45
+ const child = spawn(process.execPath, [...process.argv.slice(1), "--daemon"], {
46
+ detached: true, stdio: "ignore",
47
+ });
48
+ child.unref();
49
+ }
42
50
  process.exit(0);
43
51
  }
44
52
  const setupMode = hasFlag("setup");
@@ -607,7 +615,7 @@ async function registerClient(settings) {
607
615
  }
608
616
  }
609
617
  catch { /* ignore */ }
610
- // Read existing clients.jsonc from cloud
618
+ // Read existing clients.jsonc from cloud (may not exist yet — that's fine)
611
619
  let clients = {};
612
620
  try {
613
621
  const content = await cloudRead("clients.jsonc");
@@ -88,6 +88,10 @@ body {
88
88
  }
89
89
 
90
90
  /* Responsive: narrow OR short viewport — single panel navigation */
91
+ @media (max-width: 768px), (max-height: 600px) {
92
+ /* Hide preview snippet under message subject — save space */
93
+ .ml-preview { display: none; }
94
+ }
91
95
  @media (max-width: 768px), (max-height: 600px) {
92
96
  body {
93
97
  grid-template-columns: 1fr;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/mailx",
3
- "version": "1.0.207",
3
+ "version": "1.0.208",
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.268",
27
+ "@bobfrankston/msger": "^0.1.269",
28
28
  "@capacitor/android": "^8.3.0",
29
29
  "@capacitor/cli": "^8.3.0",
30
30
  "@capacitor/core": "^8.3.0",
@@ -420,6 +420,12 @@ async function registerDeviceInGDrive(tokenProvider, folderId, accountIds) {
420
420
  catch { /* */ }
421
421
  }
422
422
  }
423
+ // Remove stale android-* entries (from old random-UUID approach) — keep only this device
424
+ for (const key of Object.keys(clients)) {
425
+ if (key.startsWith("android-") && key !== deviceId) {
426
+ delete clients[key];
427
+ }
428
+ }
423
429
  clients[deviceId] = {
424
430
  hostname: deviceId,
425
431
  platform: "android",
@@ -232,6 +232,56 @@ const DEFAULT_ALLOWLIST = {
232
232
  domains: [],
233
233
  recipients: [],
234
234
  };
235
+ // ── JSONC parser (strips comments and trailing commas) ──
236
+ function parseJsonc(text) {
237
+ // Strip /* block comments */ and // line comments, but preserve content inside strings
238
+ let stripped = "";
239
+ let i = 0;
240
+ let inString = false;
241
+ let stringChar = "";
242
+ while (i < text.length) {
243
+ const c = text[i];
244
+ const next = text[i + 1];
245
+ if (inString) {
246
+ stripped += c;
247
+ if (c === "\\" && i + 1 < text.length) {
248
+ stripped += text[i + 1];
249
+ i += 2;
250
+ continue;
251
+ }
252
+ if (c === stringChar)
253
+ inString = false;
254
+ i++;
255
+ continue;
256
+ }
257
+ if (c === '"' || c === "'") {
258
+ inString = true;
259
+ stringChar = c;
260
+ stripped += c;
261
+ i++;
262
+ continue;
263
+ }
264
+ if (c === "/" && next === "/") {
265
+ // Line comment — skip to end of line
266
+ while (i < text.length && text[i] !== "\n")
267
+ i++;
268
+ continue;
269
+ }
270
+ if (c === "/" && next === "*") {
271
+ // Block comment — skip to */
272
+ i += 2;
273
+ while (i < text.length - 1 && !(text[i] === "*" && text[i + 1] === "/"))
274
+ i++;
275
+ i += 2;
276
+ continue;
277
+ }
278
+ stripped += c;
279
+ i++;
280
+ }
281
+ // Strip trailing commas before } or ]
282
+ stripped = stripped.replace(/,(\s*[}\]])/g, "$1");
283
+ return JSON.parse(stripped);
284
+ }
235
285
  // ── Public API ──
236
286
  /** Load accounts — first from IndexedDB cache, then GDrive */
237
287
  export async function loadAccounts() {
@@ -239,24 +289,28 @@ export async function loadAccounts() {
239
289
  const cached = await idbRead("accounts.jsonc");
240
290
  if (cached) {
241
291
  try {
242
- const data = JSON.parse(cached);
292
+ const data = parseJsonc(cached);
243
293
  const raw = data.accounts || (Array.isArray(data) ? data : []);
244
294
  if (raw.length > 0) {
245
295
  return raw.map((a) => normalizeAccount(a, data.name));
246
296
  }
247
297
  }
248
- catch { /* parse error — fall through to GDrive */ }
298
+ catch (e) {
299
+ console.warn(`[settings] Cached accounts.jsonc parse failed: ${e.message}`);
300
+ }
249
301
  }
250
302
  // Try GDrive
251
303
  const content = await gDriveRead("accounts.jsonc");
252
304
  if (content) {
253
305
  await idbWrite("accounts.jsonc", content);
254
306
  try {
255
- const data = JSON.parse(content);
307
+ const data = parseJsonc(content);
256
308
  const raw = data.accounts || (Array.isArray(data) ? data : []);
257
309
  return raw.map((a) => normalizeAccount(a, data.name));
258
310
  }
259
- catch { /* parse error */ }
311
+ catch (e) {
312
+ console.warn(`[settings] GDrive accounts.jsonc parse failed: ${e.message}`);
313
+ }
260
314
  }
261
315
  return [];
262
316
  }
@@ -266,11 +320,13 @@ export async function loadAccountsFromCloud() {
266
320
  if (content) {
267
321
  await idbWrite("accounts.jsonc", content);
268
322
  try {
269
- const data = JSON.parse(content);
323
+ const data = parseJsonc(content);
270
324
  const raw = data.accounts || (Array.isArray(data) ? data : []);
271
325
  return raw.map((a) => normalizeAccount(a, data.name));
272
326
  }
273
- catch { /* parse error */ }
327
+ catch (e) {
328
+ console.warn(`[settings] loadAccountsFromCloud parse failed: ${e.message}`);
329
+ }
274
330
  }
275
331
  return [];
276
332
  }
@@ -285,7 +341,7 @@ export async function loadPreferences() {
285
341
  const cached = await idbRead("preferences.jsonc");
286
342
  if (cached) {
287
343
  try {
288
- const data = JSON.parse(cached);
344
+ const data = parseJsonc(cached);
289
345
  return {
290
346
  ui: { ...DEFAULT_PREFERENCES.ui, ...data.ui },
291
347
  sync: { ...DEFAULT_PREFERENCES.sync, ...data.sync },
@@ -299,7 +355,7 @@ export async function loadPreferences() {
299
355
  if (content) {
300
356
  await idbWrite("preferences.jsonc", content);
301
357
  try {
302
- const data = JSON.parse(content);
358
+ const data = parseJsonc(content);
303
359
  return {
304
360
  ui: { ...DEFAULT_PREFERENCES.ui, ...data.ui },
305
361
  sync: { ...DEFAULT_PREFERENCES.sync, ...data.sync },
@@ -341,7 +397,7 @@ export async function loadAllowlist() {
341
397
  const cached = await idbRead("allowlist.jsonc");
342
398
  if (cached) {
343
399
  try {
344
- return JSON.parse(cached);
400
+ return parseJsonc(cached);
345
401
  }
346
402
  catch { /* */ }
347
403
  }
@@ -349,7 +405,7 @@ export async function loadAllowlist() {
349
405
  if (content) {
350
406
  await idbWrite("allowlist.jsonc", content);
351
407
  try {
352
- return JSON.parse(content);
408
+ return parseJsonc(content);
353
409
  }
354
410
  catch { /* */ }
355
411
  }
@@ -418,7 +474,7 @@ export async function loadDeviceState() {
418
474
  const cached = await idbRead(filename);
419
475
  if (cached) {
420
476
  try {
421
- return JSON.parse(cached);
477
+ return parseJsonc(cached);
422
478
  }
423
479
  catch { /* */ }
424
480
  }
@@ -426,7 +482,7 @@ export async function loadDeviceState() {
426
482
  if (content) {
427
483
  await idbWrite(filename, content);
428
484
  try {
429
- return JSON.parse(content);
485
+ return parseJsonc(content);
430
486
  }
431
487
  catch { /* */ }
432
488
  }