@bobfrankston/mailx 1.0.227 → 1.0.229
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 +94 -0
- package/client/components/message-list.js +65 -5
- package/client/index.html +2 -0
- package/client/lib/api-client.js +9 -0
- package/client/lib/mailxapi.js +9 -0
- package/client/styles/components.css +144 -0
- package/package.json +3 -3
- package/packages/mailx-service/index.d.ts +8 -0
- package/packages/mailx-service/index.js +31 -0
- package/packages/mailx-service/jsonrpc.js +7 -0
package/client/app.js
CHANGED
|
@@ -1132,6 +1132,100 @@ optSnippet?.addEventListener("change", () => {
|
|
|
1132
1132
|
}
|
|
1133
1133
|
localStorage.setItem("mailx-snippet", String(optSnippet.checked));
|
|
1134
1134
|
});
|
|
1135
|
+
// ── JSONC config file editor ──
|
|
1136
|
+
document.getElementById("btn-edit-jsonc")?.addEventListener("click", async () => {
|
|
1137
|
+
const settingsDropdown = document.getElementById("settings-dropdown");
|
|
1138
|
+
if (settingsDropdown)
|
|
1139
|
+
settingsDropdown.hidden = true;
|
|
1140
|
+
await openJsoncEditor("accounts.jsonc");
|
|
1141
|
+
});
|
|
1142
|
+
async function openJsoncEditor(initialFile) {
|
|
1143
|
+
const { readJsoncFile, writeJsoncFile } = await import("./lib/api-client.js");
|
|
1144
|
+
const backdrop = document.createElement("div");
|
|
1145
|
+
backdrop.className = "mailx-modal-backdrop";
|
|
1146
|
+
const panel = document.createElement("div");
|
|
1147
|
+
panel.className = "mailx-modal mailx-modal-wide";
|
|
1148
|
+
panel.innerHTML = `
|
|
1149
|
+
<div class="mailx-modal-title">Edit config file</div>
|
|
1150
|
+
<label class="mailx-modal-label">File
|
|
1151
|
+
<select class="mailx-modal-input" id="jsonc-file">
|
|
1152
|
+
<option value="accounts.jsonc">accounts.jsonc</option>
|
|
1153
|
+
<option value="allowlist.jsonc">allowlist.jsonc</option>
|
|
1154
|
+
<option value="clients.jsonc">clients.jsonc</option>
|
|
1155
|
+
</select>
|
|
1156
|
+
</label>
|
|
1157
|
+
<label class="mailx-modal-label">Contents (JSONC — comments and trailing commas allowed)
|
|
1158
|
+
<textarea class="mailx-modal-input mailx-modal-textarea" id="jsonc-content" spellcheck="false"></textarea>
|
|
1159
|
+
</label>
|
|
1160
|
+
<div class="mailx-modal-error" id="jsonc-error" hidden></div>
|
|
1161
|
+
<div class="mailx-modal-buttons">
|
|
1162
|
+
<span class="mailx-modal-spacer"></span>
|
|
1163
|
+
<button type="button" class="mailx-modal-btn" data-action="cancel">Cancel</button>
|
|
1164
|
+
<button type="button" class="mailx-modal-btn mailx-modal-btn-primary" data-action="save">Save</button>
|
|
1165
|
+
</div>`;
|
|
1166
|
+
backdrop.appendChild(panel);
|
|
1167
|
+
document.body.appendChild(backdrop);
|
|
1168
|
+
const fileSelect = panel.querySelector("#jsonc-file");
|
|
1169
|
+
const textarea = panel.querySelector("#jsonc-content");
|
|
1170
|
+
const errorEl = panel.querySelector("#jsonc-error");
|
|
1171
|
+
fileSelect.value = initialFile;
|
|
1172
|
+
const loadFile = async () => {
|
|
1173
|
+
textarea.value = "Loading...";
|
|
1174
|
+
errorEl.hidden = true;
|
|
1175
|
+
try {
|
|
1176
|
+
const r = await readJsoncFile(fileSelect.value);
|
|
1177
|
+
textarea.value = r?.content || "";
|
|
1178
|
+
}
|
|
1179
|
+
catch (e) {
|
|
1180
|
+
textarea.value = "";
|
|
1181
|
+
errorEl.textContent = `Failed to load: ${e.message}`;
|
|
1182
|
+
errorEl.hidden = false;
|
|
1183
|
+
}
|
|
1184
|
+
};
|
|
1185
|
+
await loadFile();
|
|
1186
|
+
fileSelect.addEventListener("change", loadFile);
|
|
1187
|
+
const close = () => {
|
|
1188
|
+
backdrop.remove();
|
|
1189
|
+
document.removeEventListener("keydown", onKey, true);
|
|
1190
|
+
};
|
|
1191
|
+
const onKey = (e) => {
|
|
1192
|
+
if (e.key === "Escape") {
|
|
1193
|
+
e.stopPropagation();
|
|
1194
|
+
e.preventDefault();
|
|
1195
|
+
close();
|
|
1196
|
+
}
|
|
1197
|
+
};
|
|
1198
|
+
document.addEventListener("keydown", onKey, true);
|
|
1199
|
+
panel.querySelectorAll(".mailx-modal-btn").forEach(btn => {
|
|
1200
|
+
btn.addEventListener("click", async () => {
|
|
1201
|
+
const action = btn.dataset.action;
|
|
1202
|
+
if (action === "cancel") {
|
|
1203
|
+
close();
|
|
1204
|
+
return;
|
|
1205
|
+
}
|
|
1206
|
+
if (action === "save") {
|
|
1207
|
+
errorEl.hidden = true;
|
|
1208
|
+
btn.disabled = true;
|
|
1209
|
+
btn.textContent = "Saving...";
|
|
1210
|
+
try {
|
|
1211
|
+
await writeJsoncFile(fileSelect.value, textarea.value);
|
|
1212
|
+
close();
|
|
1213
|
+
const statusSync = document.getElementById("status-sync");
|
|
1214
|
+
if (statusSync)
|
|
1215
|
+
statusSync.textContent = `Saved ${fileSelect.value} — restart mailx to apply`;
|
|
1216
|
+
}
|
|
1217
|
+
catch (e) {
|
|
1218
|
+
errorEl.textContent = `${e.message}`;
|
|
1219
|
+
errorEl.hidden = false;
|
|
1220
|
+
btn.disabled = false;
|
|
1221
|
+
btn.textContent = "Save";
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
});
|
|
1225
|
+
});
|
|
1226
|
+
backdrop.addEventListener("mousedown", (e) => { if (e.target === backdrop)
|
|
1227
|
+
close(); });
|
|
1228
|
+
}
|
|
1135
1229
|
// Threaded view toggle
|
|
1136
1230
|
optThreaded?.addEventListener("change", () => {
|
|
1137
1231
|
const body = document.getElementById("ml-body");
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Message list component — renders paginated message rows.
|
|
3
3
|
* Reads from message-state; operations mutate state, list reacts.
|
|
4
4
|
*/
|
|
5
|
-
import { getMessages as apiGetMessages, getUnifiedInbox as apiGetUnifiedInbox, searchMessages, updateFlags } from "../lib/api-client.js";
|
|
5
|
+
import { getMessages as apiGetMessages, getUnifiedInbox as apiGetUnifiedInbox, searchMessages, updateFlags, getThreadMessages } from "../lib/api-client.js";
|
|
6
6
|
import * as state from "../lib/message-state.js";
|
|
7
7
|
import { showContextMenu } from "./context-menu.js";
|
|
8
8
|
let onMessageSelect;
|
|
@@ -301,6 +301,61 @@ function restoreSelection(body, savedUid) {
|
|
|
301
301
|
row.classList.add("selected");
|
|
302
302
|
}
|
|
303
303
|
}
|
|
304
|
+
/** Show a floating list of all messages in a thread when the pill is clicked.
|
|
305
|
+
* Each entry in the popup selects that message in the viewer when clicked.
|
|
306
|
+
* This is simpler than inline expansion and avoids duplicating the row builder. */
|
|
307
|
+
async function showThreadPopup(pillEl, headMsg) {
|
|
308
|
+
// Remove any existing popup
|
|
309
|
+
document.querySelectorAll(".ml-thread-popup").forEach(el => el.remove());
|
|
310
|
+
let thread = [];
|
|
311
|
+
try {
|
|
312
|
+
thread = await getThreadMessages(headMsg.accountId, headMsg.threadId);
|
|
313
|
+
}
|
|
314
|
+
catch { /* ignore */ }
|
|
315
|
+
if (!thread || thread.length === 0)
|
|
316
|
+
return;
|
|
317
|
+
thread.sort((a, b) => (a.date || 0) - (b.date || 0));
|
|
318
|
+
const popup = document.createElement("div");
|
|
319
|
+
popup.className = "ml-thread-popup";
|
|
320
|
+
for (const msg of thread) {
|
|
321
|
+
const item = document.createElement("div");
|
|
322
|
+
item.className = "ml-thread-popup-item";
|
|
323
|
+
if (!msg.flags.includes("\\Seen"))
|
|
324
|
+
item.classList.add("unread");
|
|
325
|
+
const from = document.createElement("span");
|
|
326
|
+
from.className = "ml-thread-popup-from";
|
|
327
|
+
from.textContent = msg.from?.name || msg.from?.address || "?";
|
|
328
|
+
const date = document.createElement("span");
|
|
329
|
+
date.className = "ml-thread-popup-date";
|
|
330
|
+
date.textContent = formatDate(msg.date);
|
|
331
|
+
const subject = document.createElement("span");
|
|
332
|
+
subject.className = "ml-thread-popup-subject";
|
|
333
|
+
subject.textContent = msg.subject || "(no subject)";
|
|
334
|
+
item.appendChild(from);
|
|
335
|
+
item.appendChild(date);
|
|
336
|
+
item.appendChild(subject);
|
|
337
|
+
item.addEventListener("click", async () => {
|
|
338
|
+
state.select({ accountId: msg.accountId, uid: msg.uid, folderId: msg.folderId, subject: msg.subject, from: msg.from, to: msg.to, cc: msg.cc, date: msg.date, flags: msg.flags, size: msg.size, preview: msg.preview, hasAttachments: msg.hasAttachments });
|
|
339
|
+
onMessageSelect(msg.accountId, msg.uid, msg.folderId);
|
|
340
|
+
popup.remove();
|
|
341
|
+
});
|
|
342
|
+
popup.appendChild(item);
|
|
343
|
+
}
|
|
344
|
+
document.body.appendChild(popup);
|
|
345
|
+
const rect = pillEl.getBoundingClientRect();
|
|
346
|
+
popup.style.left = `${rect.left}px`;
|
|
347
|
+
popup.style.top = `${rect.bottom + 4}px`;
|
|
348
|
+
// Dismiss on outside click
|
|
349
|
+
setTimeout(() => {
|
|
350
|
+
const dismiss = (e) => {
|
|
351
|
+
if (!popup.contains(e.target)) {
|
|
352
|
+
popup.remove();
|
|
353
|
+
document.removeEventListener("mousedown", dismiss, true);
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
document.addEventListener("mousedown", dismiss, true);
|
|
357
|
+
}, 0);
|
|
358
|
+
}
|
|
304
359
|
function appendMessages(body, accountId, items) {
|
|
305
360
|
// Thread grouping: when the list has the "threaded" class, collapse messages
|
|
306
361
|
// sharing the same threadId to a single row showing the most recent message,
|
|
@@ -373,15 +428,20 @@ function appendMessages(body, accountId, items) {
|
|
|
373
428
|
const subject = document.createElement("span");
|
|
374
429
|
subject.className = "ml-subject";
|
|
375
430
|
subject.innerHTML = escapeHtml(msg.subject);
|
|
376
|
-
// Thread size pill:
|
|
377
|
-
// represents a collapsed thread with multiple messages.
|
|
431
|
+
// Thread size pill: click to show a popup list of the thread's messages.
|
|
378
432
|
if (threadSize) {
|
|
379
433
|
const n = threadSize.get(msg) || 1;
|
|
380
|
-
if (n > 1) {
|
|
434
|
+
if (n > 1 && msg.threadId) {
|
|
435
|
+
row.classList.add("thread-head");
|
|
436
|
+
row.dataset.threadId = msg.threadId;
|
|
381
437
|
const threadPill = document.createElement("span");
|
|
382
438
|
threadPill.className = "ml-thread-pill";
|
|
383
439
|
threadPill.textContent = String(n);
|
|
384
|
-
threadPill.title = `${n} messages in this thread`;
|
|
440
|
+
threadPill.title = `${n} messages in this thread — click to see list`;
|
|
441
|
+
threadPill.addEventListener("click", async (e) => {
|
|
442
|
+
e.stopPropagation();
|
|
443
|
+
await showThreadPopup(threadPill, msg);
|
|
444
|
+
});
|
|
385
445
|
subject.prepend(threadPill);
|
|
386
446
|
}
|
|
387
447
|
}
|
package/client/index.html
CHANGED
|
@@ -38,6 +38,8 @@
|
|
|
38
38
|
<label class="tb-menu-item"><input type="radio" name="opt-editor" value="tiptap" id="opt-editor-tiptap"> tiptap</label>
|
|
39
39
|
<hr class="tb-menu-sep">
|
|
40
40
|
<label class="tb-menu-item"><input type="checkbox" id="opt-autocomplete"> AI autocomplete</label>
|
|
41
|
+
<hr class="tb-menu-sep">
|
|
42
|
+
<button class="tb-menu-item" id="btn-edit-jsonc" title="Edit accounts.jsonc / allowlist.jsonc">Edit config files...</button>
|
|
41
43
|
</div>
|
|
42
44
|
</div>
|
|
43
45
|
<span id="app-version" class="app-version">mailx</span>
|
package/client/lib/api-client.js
CHANGED
|
@@ -149,6 +149,15 @@ export function deleteDraft(accountId, draftUid, draftId) {
|
|
|
149
149
|
export function addContact(name, email) {
|
|
150
150
|
return ipc().addContact?.(name, email);
|
|
151
151
|
}
|
|
152
|
+
export function getThreadMessages(accountId, threadId) {
|
|
153
|
+
return ipc().getThreadMessages?.(accountId, threadId);
|
|
154
|
+
}
|
|
155
|
+
export function readJsoncFile(name) {
|
|
156
|
+
return ipc().readJsoncFile?.(name);
|
|
157
|
+
}
|
|
158
|
+
export function writeJsoncFile(name, content) {
|
|
159
|
+
return ipc().writeJsoncFile?.(name, content);
|
|
160
|
+
}
|
|
152
161
|
export function setupAccount(name, email, password) {
|
|
153
162
|
return ipc().setupAccount?.(name, email, password);
|
|
154
163
|
}
|
package/client/lib/mailxapi.js
CHANGED
|
@@ -100,6 +100,15 @@
|
|
|
100
100
|
addContact: function(name, email) {
|
|
101
101
|
return callNode("addContact", { name: name, email: email });
|
|
102
102
|
},
|
|
103
|
+
getThreadMessages: function(accountId, threadId) {
|
|
104
|
+
return callNode("getThreadMessages", { accountId: accountId, threadId: threadId });
|
|
105
|
+
},
|
|
106
|
+
readJsoncFile: function(name) {
|
|
107
|
+
return callNode("readJsoncFile", { name: name });
|
|
108
|
+
},
|
|
109
|
+
writeJsoncFile: function(name, content) {
|
|
110
|
+
return callNode("writeJsoncFile", { name: name, content: content });
|
|
111
|
+
},
|
|
103
112
|
searchContacts: function(query) {
|
|
104
113
|
return callNode("searchContacts", { query: query });
|
|
105
114
|
},
|
|
@@ -411,7 +411,151 @@ button.tb-menu-item { background: none; border: none; color: inherit; width: 100
|
|
|
411
411
|
vertical-align: baseline;
|
|
412
412
|
min-width: 1.5em;
|
|
413
413
|
text-align: center;
|
|
414
|
+
cursor: pointer;
|
|
415
|
+
}
|
|
416
|
+
.ml-thread-pill:hover { filter: brightness(1.15); }
|
|
417
|
+
|
|
418
|
+
/* Popup that lists the messages in a thread, anchored below the clicked pill. */
|
|
419
|
+
.ml-thread-popup {
|
|
420
|
+
position: fixed;
|
|
421
|
+
min-width: 320px;
|
|
422
|
+
max-width: 600px;
|
|
423
|
+
max-height: 60vh;
|
|
424
|
+
overflow-y: auto;
|
|
425
|
+
background: var(--color-bg);
|
|
426
|
+
color: var(--color-text);
|
|
427
|
+
border: 1px solid var(--color-border);
|
|
428
|
+
border-radius: var(--radius-md);
|
|
429
|
+
box-shadow: 0 6px 24px rgba(0, 0, 0, 0.35);
|
|
430
|
+
padding: var(--gap-xs);
|
|
431
|
+
z-index: 1500;
|
|
432
|
+
font-family: var(--font-ui);
|
|
433
|
+
font-size: var(--font-size-sm);
|
|
434
|
+
}
|
|
435
|
+
.ml-thread-popup-item {
|
|
436
|
+
display: grid;
|
|
437
|
+
grid-template-columns: minmax(0, 1fr) auto;
|
|
438
|
+
gap: 4px var(--gap-sm);
|
|
439
|
+
padding: 6px 10px;
|
|
440
|
+
border-radius: var(--radius-sm);
|
|
441
|
+
cursor: pointer;
|
|
442
|
+
}
|
|
443
|
+
.ml-thread-popup-item:hover { background: var(--color-bg-hover); }
|
|
444
|
+
.ml-thread-popup-item.unread .ml-thread-popup-from,
|
|
445
|
+
.ml-thread-popup-item.unread .ml-thread-popup-subject { font-weight: 600; }
|
|
446
|
+
.ml-thread-popup-from {
|
|
447
|
+
grid-column: 1;
|
|
448
|
+
grid-row: 1;
|
|
449
|
+
overflow: hidden;
|
|
450
|
+
text-overflow: ellipsis;
|
|
451
|
+
white-space: nowrap;
|
|
452
|
+
}
|
|
453
|
+
.ml-thread-popup-date {
|
|
454
|
+
grid-column: 2;
|
|
455
|
+
grid-row: 1;
|
|
456
|
+
color: var(--color-text-muted);
|
|
457
|
+
font-variant-numeric: tabular-nums;
|
|
458
|
+
}
|
|
459
|
+
.ml-thread-popup-subject {
|
|
460
|
+
grid-column: 1 / -1;
|
|
461
|
+
grid-row: 2;
|
|
462
|
+
color: var(--color-text-muted);
|
|
463
|
+
overflow: hidden;
|
|
464
|
+
text-overflow: ellipsis;
|
|
465
|
+
white-space: nowrap;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/* Generic modal — used by the JSONC config editor launched from Settings */
|
|
469
|
+
.mailx-modal-backdrop {
|
|
470
|
+
position: fixed;
|
|
471
|
+
inset: 0;
|
|
472
|
+
background: rgba(0, 0, 0, 0.4);
|
|
473
|
+
display: flex;
|
|
474
|
+
align-items: center;
|
|
475
|
+
justify-content: center;
|
|
476
|
+
z-index: 2000;
|
|
477
|
+
}
|
|
478
|
+
.mailx-modal {
|
|
479
|
+
background: var(--color-bg);
|
|
480
|
+
color: var(--color-text);
|
|
481
|
+
padding: var(--gap-lg);
|
|
482
|
+
border-radius: var(--radius-md);
|
|
483
|
+
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
|
|
484
|
+
min-width: 420px;
|
|
485
|
+
max-width: 90vw;
|
|
486
|
+
display: flex;
|
|
487
|
+
flex-direction: column;
|
|
488
|
+
gap: var(--gap-md);
|
|
489
|
+
font-family: var(--font-ui);
|
|
490
|
+
}
|
|
491
|
+
.mailx-modal-wide {
|
|
492
|
+
width: 80vw;
|
|
493
|
+
max-width: 900px;
|
|
494
|
+
max-height: 85vh;
|
|
495
|
+
}
|
|
496
|
+
.mailx-modal-title {
|
|
497
|
+
font-size: var(--font-size-lg);
|
|
498
|
+
font-weight: 600;
|
|
499
|
+
}
|
|
500
|
+
.mailx-modal-label {
|
|
501
|
+
display: flex;
|
|
502
|
+
flex-direction: column;
|
|
503
|
+
gap: 4px;
|
|
504
|
+
font-size: var(--font-size-sm);
|
|
505
|
+
color: var(--color-text-muted);
|
|
506
|
+
}
|
|
507
|
+
.mailx-modal-input {
|
|
508
|
+
padding: 6px 8px;
|
|
509
|
+
border: 1px solid var(--color-border);
|
|
510
|
+
border-radius: var(--radius-sm);
|
|
511
|
+
background: var(--color-bg-surface);
|
|
512
|
+
color: var(--color-text);
|
|
513
|
+
font-family: var(--font-ui);
|
|
514
|
+
font-size: var(--font-size-base);
|
|
515
|
+
}
|
|
516
|
+
.mailx-modal-textarea {
|
|
517
|
+
min-height: 50vh;
|
|
518
|
+
resize: vertical;
|
|
519
|
+
font-family: var(--font-mono);
|
|
520
|
+
font-size: 13px;
|
|
521
|
+
white-space: pre;
|
|
522
|
+
tab-size: 2;
|
|
523
|
+
}
|
|
524
|
+
.mailx-modal-input:focus {
|
|
525
|
+
outline: 2px solid var(--color-accent);
|
|
526
|
+
outline-offset: -1px;
|
|
527
|
+
}
|
|
528
|
+
.mailx-modal-error {
|
|
529
|
+
color: oklch(0.65 0.2 25);
|
|
530
|
+
font-size: var(--font-size-sm);
|
|
531
|
+
background: color-mix(in oklch, oklch(0.65 0.2 25) 10%, transparent);
|
|
532
|
+
padding: 6px 10px;
|
|
533
|
+
border-radius: var(--radius-sm);
|
|
534
|
+
white-space: pre-wrap;
|
|
535
|
+
}
|
|
536
|
+
.mailx-modal-buttons {
|
|
537
|
+
display: flex;
|
|
538
|
+
gap: var(--gap-sm);
|
|
539
|
+
align-items: center;
|
|
540
|
+
}
|
|
541
|
+
.mailx-modal-spacer { flex: 1; }
|
|
542
|
+
.mailx-modal-btn {
|
|
543
|
+
padding: 6px 14px;
|
|
544
|
+
border: 1px solid var(--color-border);
|
|
545
|
+
border-radius: var(--radius-sm);
|
|
546
|
+
background: var(--color-bg-surface);
|
|
547
|
+
color: var(--color-text);
|
|
548
|
+
cursor: pointer;
|
|
549
|
+
font-size: var(--font-size-sm);
|
|
550
|
+
}
|
|
551
|
+
.mailx-modal-btn:hover { background: var(--color-bg-hover); }
|
|
552
|
+
.mailx-modal-btn-primary {
|
|
553
|
+
background: var(--color-accent);
|
|
554
|
+
color: #fff;
|
|
555
|
+
border-color: transparent;
|
|
556
|
+
font-weight: 500;
|
|
414
557
|
}
|
|
558
|
+
.mailx-modal-btn-primary:hover { filter: brightness(1.1); }
|
|
415
559
|
.ml-subject {
|
|
416
560
|
overflow: hidden;
|
|
417
561
|
text-overflow: ellipsis;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bobfrankston/mailx",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.229",
|
|
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.
|
|
27
|
+
"@bobfrankston/msger": "^0.1.291",
|
|
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.
|
|
81
|
+
"@bobfrankston/msger": "^0.1.291",
|
|
82
82
|
"@capacitor/android": "^8.3.0",
|
|
83
83
|
"@capacitor/cli": "^8.3.0",
|
|
84
84
|
"@capacitor/core": "^8.3.0",
|
|
@@ -59,6 +59,14 @@ export declare class MailxService {
|
|
|
59
59
|
* action on From/To/Cc addresses in the message viewer. Just calls the same
|
|
60
60
|
* validated upsert path as recordSentAddress. */
|
|
61
61
|
addContact(name: string, email: string): boolean;
|
|
62
|
+
/** Get all messages in a thread (across folders) for an account. */
|
|
63
|
+
getThreadMessages(accountId: string, threadId: string): any;
|
|
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. */
|
|
66
|
+
readJsoncFile(name: string): Promise<string | null>;
|
|
67
|
+
/** Write a JSONC config file. Validates that the content parses as JSONC
|
|
68
|
+
* (loosely — strips comments/trailing commas) before writing. */
|
|
69
|
+
writeJsoncFile(name: string, content: string): Promise<void>;
|
|
62
70
|
getSettings(): any;
|
|
63
71
|
saveSettings(settings: any): void;
|
|
64
72
|
getStorageInfo(): {
|
|
@@ -659,6 +659,37 @@ export class MailxService {
|
|
|
659
659
|
this.db.recordSentAddress(name || "", email);
|
|
660
660
|
return true;
|
|
661
661
|
}
|
|
662
|
+
/** Get all messages in a thread (across folders) for an account. */
|
|
663
|
+
getThreadMessages(accountId, threadId) {
|
|
664
|
+
return this.db.getThreadMessages(accountId, threadId);
|
|
665
|
+
}
|
|
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. */
|
|
668
|
+
async readJsoncFile(name) {
|
|
669
|
+
const whitelist = ["accounts.jsonc", "allowlist.jsonc", "clients.jsonc"];
|
|
670
|
+
if (!whitelist.includes(name))
|
|
671
|
+
throw new Error(`File not allowed: ${name}`);
|
|
672
|
+
const { cloudRead } = await import("@bobfrankston/mailx-settings");
|
|
673
|
+
return cloudRead(name);
|
|
674
|
+
}
|
|
675
|
+
/** Write a JSONC config file. Validates that the content parses as JSONC
|
|
676
|
+
* (loosely — strips comments/trailing commas) before writing. */
|
|
677
|
+
async writeJsoncFile(name, content) {
|
|
678
|
+
const whitelist = ["accounts.jsonc", "allowlist.jsonc", "clients.jsonc"];
|
|
679
|
+
if (!whitelist.includes(name))
|
|
680
|
+
throw new Error(`File not allowed: ${name}`);
|
|
681
|
+
// Validate the content parses before writing
|
|
682
|
+
const { parse: parseJsonc } = await import("jsonc-parser");
|
|
683
|
+
const errors = [];
|
|
684
|
+
parseJsonc(content, errors, { allowTrailingComma: true });
|
|
685
|
+
if (errors.length) {
|
|
686
|
+
throw new Error(`JSONC parse error: ${errors.map(e => e.error).join(", ")}`);
|
|
687
|
+
}
|
|
688
|
+
const { cloudWrite } = await import("@bobfrankston/mailx-settings");
|
|
689
|
+
const ok = await cloudWrite(name, content);
|
|
690
|
+
if (!ok)
|
|
691
|
+
throw new Error(`Failed to write ${name}`);
|
|
692
|
+
}
|
|
662
693
|
// ── Settings ──
|
|
663
694
|
getSettings() {
|
|
664
695
|
return loadSettings();
|
|
@@ -96,6 +96,13 @@ async function dispatchAction(svc, action, p) {
|
|
|
96
96
|
return svc.searchContacts(p.query);
|
|
97
97
|
case "addContact":
|
|
98
98
|
return { ok: svc.addContact(p.name, p.email) };
|
|
99
|
+
case "getThreadMessages":
|
|
100
|
+
return svc.getThreadMessages(p.accountId, p.threadId);
|
|
101
|
+
case "readJsoncFile":
|
|
102
|
+
return { content: await svc.readJsoncFile(p.name) };
|
|
103
|
+
case "writeJsoncFile":
|
|
104
|
+
await svc.writeJsoncFile(p.name, p.content);
|
|
105
|
+
return { ok: true };
|
|
99
106
|
// Settings
|
|
100
107
|
case "getSettings":
|
|
101
108
|
return svc.getSettings();
|