@bobfrankston/mailx 1.0.340 → 1.0.349

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/index.html CHANGED
@@ -42,6 +42,8 @@
42
42
  <label class="tb-menu-item" title="Right-click in compose editor → Proofread (when wired)"><input type="checkbox" id="opt-ai-proofread"> AI proofread (off by default)</label>
43
43
  <hr class="tb-menu-sep">
44
44
  <button class="tb-menu-item" id="btn-edit-jsonc" title="Edit accounts.jsonc / allowlist.jsonc">Edit config files...</button>
45
+ <button class="tb-menu-item" id="btn-open-mailx-dir" title="Open ~/.mailx in file explorer">Open mailx folder...</button>
46
+ <button class="tb-menu-item" id="btn-open-log" title="Open today's log file">Open log...</button>
45
47
  <button class="tb-menu-item" id="btn-about" title="Show version and build info">About mailx...</button>
46
48
  </div>
47
49
  </div>
@@ -76,9 +78,9 @@
76
78
  <button class="rail-btn" id="rail-compose" title="Compose (Ctrl+N)" aria-label="Compose">✏</button>
77
79
  <button class="rail-btn" id="rail-inbox" title="Inbox" aria-label="Inbox" data-active="true">✉</button>
78
80
  <button class="rail-btn" id="rail-unified" title="All Inboxes" aria-label="All Inboxes">⌘</button>
79
- <button class="rail-btn" id="rail-contacts" title="Contacts (coming soon)" aria-label="Contacts" disabled>👤</button>
80
- <button class="rail-btn" id="rail-calendar" title="Calendar (Phase 4)" aria-label="Calendar" disabled>📅</button>
81
- <button class="rail-btn" id="rail-tasks" title="Tasks (Phase 4)" aria-label="Tasks" disabled>☑</button>
81
+ <button class="rail-btn" id="rail-contacts" title="Contacts / Address book" aria-label="Contacts">👤</button>
82
+ <button class="rail-btn" id="rail-calendar" title="Calendar" aria-label="Calendar">📅</button>
83
+ <button class="rail-btn" id="rail-tasks" title="Tasks" aria-label="Tasks">☑</button>
82
84
  </div>
83
85
  <div class="rail-bottom">
84
86
  <button class="rail-btn" id="rail-settings" title="Settings" aria-label="Settings">⚙</button>
@@ -99,12 +101,12 @@
99
101
  <main class="main-area">
100
102
  <section class="message-list" id="message-list">
101
103
  <search class="search-bar ml-search">
102
- <select id="search-scope" title="Search scope">
104
+ <select id="search-scope" title="Local search scope">
103
105
  <option value="all">All folders</option>
104
106
  <option value="current">This folder</option>
105
- <option value="server">IMAP server</option>
106
107
  </select>
107
- <input type="search" id="search-input" placeholder="Search... (/regex/)" autocomplete="off" title="Search messages. /pattern/ for regex. Qualifiers: from: to: subject:">
108
+ <input type="search" id="search-input" placeholder="Search... (/regex/)" autocomplete="off" title="Search messages. /pattern/ for regex. Qualifiers: from: to: subject: date: after: before: has:attachment is:flagged|unread|answered|draft folder:">
109
+ <label class="search-server-check" title="Also search the IMAP server (slower; spans all folders on all accounts)"><input type="checkbox" id="search-server-too"> Server</label>
108
110
  </search>
109
111
  <div class="ml-folder-title" id="ml-folder-title"></div>
110
112
  <div class="ml-header">
@@ -65,9 +65,27 @@ export function getSyncPending() {
65
65
  export function getOutboxStatus() {
66
66
  return ipc().getOutboxStatus();
67
67
  }
68
+ export function listQueuedOutgoing() {
69
+ return ipc().listQueuedOutgoing();
70
+ }
71
+ export function cancelQueuedOutgoing(p) {
72
+ return ipc().cancelQueuedOutgoing(p);
73
+ }
68
74
  export function searchContacts(query) {
69
75
  return ipc().searchContacts(query);
70
76
  }
77
+ export function listContacts(query, page = 1, pageSize = 100) {
78
+ return ipc().listContacts(query, page, pageSize);
79
+ }
80
+ export function upsertContact(name, email) {
81
+ return ipc().upsertContact(name, email);
82
+ }
83
+ export function deleteContact(email) {
84
+ return ipc().deleteContact(email);
85
+ }
86
+ export function openLocalPath(which) {
87
+ return ipc().openLocalPath(which);
88
+ }
71
89
  export function allowRemoteContent(type, value) {
72
90
  return ipc().allowRemoteContent(type, value);
73
91
  }
@@ -135,12 +135,26 @@
135
135
  searchContacts: function(query) {
136
136
  return callNode("searchContacts", { query: query });
137
137
  },
138
+ listContacts: function(query, page, pageSize) {
139
+ return callNode("listContacts", { query: query || "", page: page || 1, pageSize: pageSize || 100 });
140
+ },
141
+ upsertContact: function(name, email) {
142
+ return callNode("upsertContact", { name: name, email: email });
143
+ },
144
+ deleteContact: function(email) {
145
+ return callNode("deleteContact", { email: email });
146
+ },
147
+ openLocalPath: function(which) {
148
+ return callNode("openLocalPath", { which: which });
149
+ },
138
150
 
139
151
  // Sync
140
152
  syncAll: function() { return callNode("syncAll"); },
141
153
  syncAccount: function(accountId) { return callNode("syncAccount", { accountId: accountId }); },
142
154
  getSyncPending: function() { return callNode("getSyncPending"); },
143
155
  getOutboxStatus: function() { return callNode("getOutboxStatus"); },
156
+ listQueuedOutgoing: function() { return callNode("listQueuedOutgoing"); },
157
+ cancelQueuedOutgoing: function(p) { return callNode("cancelQueuedOutgoing", { path: p }); },
144
158
  reauthenticate: function(accountId) { return callNode("reauthenticate", { accountId: accountId }); },
145
159
 
146
160
  // Bulk operations
@@ -924,6 +924,29 @@ button.tb-menu-item { background: none; border: none; color: inherit; width: 100
924
924
  color: var(--color-text);
925
925
  font-weight: 600;
926
926
  }
927
+ .mv-details-row {
928
+ display: flex;
929
+ align-items: center;
930
+ gap: 6px;
931
+ padding: 2px 0;
932
+ }
933
+ .mv-details-value {
934
+ flex: 1;
935
+ font-family: var(--font-mono);
936
+ font-size: 0.85em;
937
+ word-break: break-all;
938
+ }
939
+ .mv-details-copy {
940
+ background: none;
941
+ border: none;
942
+ cursor: pointer;
943
+ color: var(--color-text-muted);
944
+ font-size: 1rem;
945
+ padding: 0 4px;
946
+ border-radius: 3px;
947
+
948
+ &:hover { background: var(--color-bg-hover); color: var(--color-text); }
949
+ }
927
950
  .mv-action-primary {
928
951
  background: var(--color-brand-dark) !important;
929
952
  color: white !important;
@@ -1062,6 +1085,337 @@ button.tb-menu-item { background: none; border: none; color: inherit; width: 100
1062
1085
  .status-action:hover { background: oklch(0.65 0.15 25); color: #fff; }
1063
1086
  .status-action:disabled { opacity: 0.5; cursor: default; }
1064
1087
 
1088
+ /* ── Address Book ── */
1089
+ .ab-toolbar {
1090
+ display: flex;
1091
+ gap: var(--gap-sm);
1092
+ align-items: center;
1093
+ }
1094
+ .ab-toolbar input {
1095
+ flex: 1;
1096
+ }
1097
+ .ab-count {
1098
+ font-size: var(--font-size-sm);
1099
+ color: var(--color-text-muted);
1100
+ }
1101
+ .ab-list {
1102
+ flex: 1;
1103
+ overflow: auto;
1104
+ border: 1px solid var(--color-border);
1105
+ border-radius: var(--radius-sm);
1106
+ min-height: 240px;
1107
+ }
1108
+ .ab-row {
1109
+ display: grid;
1110
+ grid-template-columns: 180px minmax(220px, 1fr) 80px 50px 80px 96px;
1111
+ gap: var(--gap-sm);
1112
+ padding: 4px 8px;
1113
+ align-items: center;
1114
+ border-bottom: 1px solid var(--color-border-faint, rgba(0,0,0,0.05));
1115
+ font-size: var(--font-size-sm);
1116
+
1117
+ &:hover { background: var(--color-bg-hover); }
1118
+ &.ab-header {
1119
+ font-weight: 600;
1120
+ background: var(--color-bg-surface);
1121
+ position: sticky;
1122
+ top: 0;
1123
+ z-index: 1;
1124
+ }
1125
+ }
1126
+ .ab-name, .ab-email { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1127
+ .ab-name-input { width: 100%; box-sizing: border-box; }
1128
+ .ab-source { font-size: 0.75rem; color: var(--color-text-muted); text-transform: uppercase; }
1129
+ .ab-count-cell { text-align: right; color: var(--color-text-muted); }
1130
+ .ab-last { color: var(--color-text-muted); font-size: 0.85em; }
1131
+ .ab-actions { display: flex; gap: 2px; justify-content: flex-end; }
1132
+ .ab-actions button {
1133
+ background: none;
1134
+ border: none;
1135
+ cursor: pointer;
1136
+ padding: 2px 6px;
1137
+ border-radius: 3px;
1138
+ font-size: 0.9rem;
1139
+
1140
+ &:hover { background: var(--color-bg-hover); }
1141
+ }
1142
+ .ab-empty {
1143
+ padding: 24px;
1144
+ text-align: center;
1145
+ color: var(--color-text-muted);
1146
+ }
1147
+
1148
+ /* ── Calendar ── */
1149
+ .cal-grid {
1150
+ display: grid;
1151
+ grid-template-columns: 380px 1fr;
1152
+ gap: var(--gap-md);
1153
+ flex: 1;
1154
+ min-height: 0;
1155
+ }
1156
+ .cal-month, .cal-upcoming {
1157
+ display: flex;
1158
+ flex-direction: column;
1159
+ min-height: 0;
1160
+ }
1161
+ .cal-upcoming { overflow: auto; }
1162
+ .cal-nav {
1163
+ display: flex;
1164
+ align-items: center;
1165
+ gap: var(--gap-sm);
1166
+ padding: 4px 0;
1167
+ }
1168
+ .cal-nav-title { flex: 1; font-weight: 600; }
1169
+ .cal-nav-btn {
1170
+ background: none;
1171
+ border: 1px solid var(--color-border);
1172
+ border-radius: var(--radius-sm);
1173
+ padding: 2px 8px;
1174
+ cursor: pointer;
1175
+ font-size: 0.9rem;
1176
+
1177
+ &:hover { background: var(--color-bg-hover); }
1178
+ }
1179
+ .cal-month-grid {
1180
+ display: grid;
1181
+ grid-template-columns: repeat(7, 1fr);
1182
+ gap: 2px;
1183
+ }
1184
+ .cal-dow {
1185
+ text-align: center;
1186
+ font-size: 0.75rem;
1187
+ color: var(--color-text-muted);
1188
+ padding: 4px 0;
1189
+ }
1190
+ .cal-day {
1191
+ aspect-ratio: 1;
1192
+ border: 1px solid transparent;
1193
+ background: none;
1194
+ cursor: pointer;
1195
+ border-radius: var(--radius-sm);
1196
+ font-size: 0.85rem;
1197
+ color: var(--color-text);
1198
+
1199
+ &.cal-day-blank { visibility: hidden; cursor: default; }
1200
+ &:hover:not(.cal-day-blank) { background: var(--color-bg-hover); }
1201
+ &.cal-day-today { font-weight: 700; color: var(--color-accent); }
1202
+ &.cal-day-selected { background: var(--color-accent); color: #fff; }
1203
+ &.cal-day-has-event::after {
1204
+ content: "•";
1205
+ display: block;
1206
+ margin-top: -4px;
1207
+ font-size: 0.6rem;
1208
+ }
1209
+ }
1210
+ .cal-section-title {
1211
+ font-weight: 600;
1212
+ margin-bottom: var(--gap-sm);
1213
+ color: var(--color-text-muted);
1214
+ }
1215
+ .cal-event {
1216
+ border-left: 3px solid var(--color-accent);
1217
+ padding: 6px 8px;
1218
+ margin-bottom: 6px;
1219
+ background: var(--color-bg-surface);
1220
+ border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
1221
+ position: relative;
1222
+ }
1223
+ .cal-event-time { font-size: 0.75rem; color: var(--color-text-muted); }
1224
+ .cal-event-title { font-weight: 500; }
1225
+ .cal-event-loc { font-size: 0.85rem; color: var(--color-text-muted); }
1226
+ .cal-event-del {
1227
+ position: absolute;
1228
+ right: 4px;
1229
+ top: 4px;
1230
+ background: none;
1231
+ border: none;
1232
+ cursor: pointer;
1233
+ color: var(--color-text-muted);
1234
+ font-size: 1.1rem;
1235
+
1236
+ &:hover { color: oklch(0.65 0.2 25); }
1237
+ }
1238
+ .cal-empty { color: var(--color-text-muted); padding: 12px 0; }
1239
+ .cal-source-note { font-size: 0.75rem; color: var(--color-text-muted); }
1240
+
1241
+ /* ── Tasks ── */
1242
+ .tk-toolbar { padding-bottom: var(--gap-sm); }
1243
+ .tk-toolbar input { width: 100%; box-sizing: border-box; }
1244
+ .tk-filters {
1245
+ display: flex;
1246
+ gap: 4px;
1247
+ align-items: center;
1248
+ padding-bottom: var(--gap-sm);
1249
+ border-bottom: 1px solid var(--color-border);
1250
+ }
1251
+ .tk-filter-btn {
1252
+ background: none;
1253
+ border: 1px solid transparent;
1254
+ padding: 4px 10px;
1255
+ cursor: pointer;
1256
+ border-radius: var(--radius-sm);
1257
+ color: var(--color-text-muted);
1258
+ font-size: var(--font-size-sm);
1259
+
1260
+ &:hover { background: var(--color-bg-hover); }
1261
+ &.tk-filter-active {
1262
+ background: var(--color-bg-surface);
1263
+ border-color: var(--color-border);
1264
+ color: var(--color-text);
1265
+ font-weight: 500;
1266
+ }
1267
+ }
1268
+ .tk-spacer { flex: 1; }
1269
+ .tk-count { font-size: var(--font-size-sm); color: var(--color-text-muted); }
1270
+ .tk-list {
1271
+ flex: 1;
1272
+ overflow: auto;
1273
+ min-height: 240px;
1274
+ }
1275
+ .tk-row {
1276
+ display: grid;
1277
+ grid-template-columns: 24px 24px minmax(180px, 1fr) auto 80px 28px 28px;
1278
+ gap: var(--gap-sm);
1279
+ padding: 6px 4px;
1280
+ align-items: center;
1281
+ border-bottom: 1px solid var(--color-border-faint, rgba(0,0,0,0.05));
1282
+
1283
+ &:hover { background: var(--color-bg-hover); }
1284
+ &.tk-done .tk-title { text-decoration: line-through; color: var(--color-text-muted); }
1285
+ }
1286
+ .tk-prio {
1287
+ text-align: center;
1288
+ font-weight: 700;
1289
+
1290
+ &.tk-prio-3 { color: oklch(0.65 0.2 25); }
1291
+ &.tk-prio-2 { color: oklch(0.7 0.18 60); }
1292
+ &.tk-prio-1 { color: var(--color-text-muted); }
1293
+ }
1294
+ .tk-title {
1295
+ outline: none;
1296
+ padding: 2px 4px;
1297
+ border-radius: 3px;
1298
+
1299
+ &:focus { background: var(--color-bg-surface); box-shadow: inset 0 0 0 1px var(--color-accent); }
1300
+ }
1301
+ .tk-tags {
1302
+ color: var(--color-accent);
1303
+ font-size: 0.85em;
1304
+ }
1305
+ .tk-due {
1306
+ text-align: right;
1307
+ color: var(--color-text-muted);
1308
+ font-size: 0.85em;
1309
+
1310
+ &.tk-overdue { color: oklch(0.65 0.2 25); font-weight: 500; }
1311
+ }
1312
+ .tk-snooze, .tk-del {
1313
+ background: none;
1314
+ border: none;
1315
+ cursor: pointer;
1316
+ color: var(--color-text-muted);
1317
+ padding: 2px 4px;
1318
+ border-radius: 3px;
1319
+
1320
+ &:hover { background: var(--color-bg-hover); color: var(--color-text); }
1321
+ }
1322
+ .tk-empty {
1323
+ padding: 24px;
1324
+ text-align: center;
1325
+ color: var(--color-text-muted);
1326
+ }
1327
+
1328
+ /* Server-search orthogonal checkbox */
1329
+ .search-server-check {
1330
+ display: inline-flex;
1331
+ align-items: center;
1332
+ gap: 4px;
1333
+ margin-left: 6px;
1334
+ padding: 2px 6px;
1335
+ font-size: 0.85rem;
1336
+ color: var(--color-text-muted);
1337
+ cursor: pointer;
1338
+ border-radius: var(--radius-sm);
1339
+
1340
+ &:has(input:checked) {
1341
+ color: var(--color-accent);
1342
+ font-weight: 500;
1343
+ }
1344
+ }
1345
+
1346
+ /* ── Outbox / Pink-row view ── */
1347
+ .ob-info {
1348
+ color: var(--color-text-muted);
1349
+ font-size: var(--font-size-sm);
1350
+ padding-bottom: var(--gap-sm);
1351
+ }
1352
+ .ob-list {
1353
+ flex: 1;
1354
+ overflow: auto;
1355
+ min-height: 200px;
1356
+ }
1357
+ .ob-row {
1358
+ border: 1px solid var(--color-border);
1359
+ border-radius: var(--radius-sm);
1360
+ padding: 8px 12px;
1361
+ margin-bottom: 8px;
1362
+ position: relative;
1363
+ font-size: 0.9rem;
1364
+ }
1365
+ .ob-pink {
1366
+ /* Visible-reconciliation-state: local-only not yet on the server. */
1367
+ background: color-mix(in oklch, oklch(0.75 0.15 350) 15%, var(--color-bg-surface));
1368
+ border-color: oklch(0.7 0.15 350);
1369
+ }
1370
+ .ob-row-hdr {
1371
+ display: flex;
1372
+ gap: 8px;
1373
+ align-items: baseline;
1374
+ flex-wrap: wrap;
1375
+ margin-bottom: 4px;
1376
+ }
1377
+ .ob-acct {
1378
+ font-size: 0.75rem;
1379
+ color: var(--color-text-muted);
1380
+ background: var(--color-bg);
1381
+ padding: 1px 6px;
1382
+ border-radius: 3px;
1383
+ text-transform: uppercase;
1384
+ }
1385
+ .ob-subject { font-weight: 600; flex: 1; }
1386
+ .ob-created { color: var(--color-text-muted); font-size: 0.8rem; }
1387
+ .ob-badge {
1388
+ font-size: 0.75rem;
1389
+ padding: 1px 6px;
1390
+ border-radius: 10px;
1391
+ background: var(--color-bg);
1392
+ color: var(--color-text-muted);
1393
+ }
1394
+ .ob-claimed { background: oklch(0.7 0.15 200); color: #fff; }
1395
+ .ob-retry { background: oklch(0.75 0.15 60); color: #fff; }
1396
+ .ob-row-meta { font-size: 0.85rem; color: var(--color-text-muted); }
1397
+ .ob-row-path {
1398
+ font-family: var(--font-mono);
1399
+ font-size: 0.75rem;
1400
+ color: var(--color-text-muted);
1401
+ margin-top: 4px;
1402
+ word-break: break-all;
1403
+ }
1404
+ .ob-row-actions { margin-top: 6px; }
1405
+ .ob-cancel {
1406
+ background: none;
1407
+ border: 1px solid oklch(0.65 0.2 25);
1408
+ color: oklch(0.65 0.2 25);
1409
+ padding: 3px 10px;
1410
+ border-radius: var(--radius-sm);
1411
+ cursor: pointer;
1412
+ font-size: 0.85rem;
1413
+
1414
+ &:hover:not(:disabled) { background: oklch(0.65 0.2 25); color: #fff; }
1415
+ &:disabled { opacity: 0.5; cursor: not-allowed; }
1416
+ }
1417
+ .ob-empty { padding: 24px; text-align: center; color: var(--color-text-muted); }
1418
+
1065
1419
  /* ── Startup Overlay ── */
1066
1420
 
1067
1421
  .startup-overlay {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/mailx",
3
- "version": "1.0.340",
3
+ "version": "1.0.349",
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 bin/postinstall.js"
21
21
  },
22
22
  "dependencies": {
23
- "@bobfrankston/iflow-direct": "^0.1.23",
23
+ "@bobfrankston/iflow-direct": "^0.1.24",
24
24
  "@bobfrankston/iflow-node": "^0.1.7",
25
25
  "@bobfrankston/miscinfo": "^1.0.9",
26
26
  "@bobfrankston/oauthsupport": "^1.0.24",
@@ -84,7 +84,7 @@
84
84
  },
85
85
  ".transformedSnapshot": {
86
86
  "dependencies": {
87
- "@bobfrankston/iflow-direct": "^0.1.23",
87
+ "@bobfrankston/iflow-direct": "^0.1.24",
88
88
  "@bobfrankston/iflow-node": "^0.1.7",
89
89
  "@bobfrankston/miscinfo": "^1.0.9",
90
90
  "@bobfrankston/oauthsupport": "^1.0.24",
@@ -303,8 +303,25 @@ export class ImapManager extends EventEmitter {
303
303
  * logout() is wrapped as a no-op so legacy callers don't close it. */
304
304
  async getOpsClient(accountId) {
305
305
  let client = this.opsClients.get(accountId);
306
- if (client)
307
- return client;
306
+ if (client) {
307
+ // C38: health-check the cached client before returning. If the
308
+ // underlying socket is dead (Dovecot silently dropped IDLE after
309
+ // the inactivity timeout, or we lost connectivity), the next
310
+ // command would fail with "Not connected" — and nothing would
311
+ // recover it until an explicit reconnectOps was called from the
312
+ // catch handler. Cheap pre-check here catches it earlier.
313
+ const sock = client?.native?.transport?.socket;
314
+ const dead = sock?.destroyed || sock?.readyState === "closed" || client?._dead;
315
+ if (!dead)
316
+ return client;
317
+ try {
318
+ await (client._realLogout || client.logout)();
319
+ }
320
+ catch { /* */ }
321
+ this.opsClients.delete(accountId);
322
+ console.log(` [conn] ${accountId}: stale ops client detected in getOpsClient — reconnecting`);
323
+ client = undefined;
324
+ }
308
325
  client = this.newClient(accountId, "ops");
309
326
  // Wrap logout as no-op — this is a persistent connection. The
310
327
  // newClient wrapper's close-counter runs on `_realLogout`.
@@ -36,6 +36,13 @@ export declare class MailxService {
36
36
  };
37
37
  /** Outbox queue depth + retry status for the UI status bar. Cheap to call. */
38
38
  getOutboxStatus(): any;
39
+ /** List queued outgoing messages with parsed envelope headers so the UI
40
+ * can render a pink-row "pending" view before IMAP APPEND succeeds. */
41
+ listQueuedOutgoing(): any[];
42
+ /** Manually drop a queued message (not yet sent). Removes the .ltr file. */
43
+ cancelQueuedOutgoing(filePath: string): {
44
+ ok: true;
45
+ };
39
46
  syncAll(): Promise<void>;
40
47
  syncAccount(accountId: string): Promise<void>;
41
48
  /** Force re-authentication for an account (deletes token, opens browser consent) */
@@ -75,6 +82,22 @@ export declare class MailxService {
75
82
  * action on From/To/Cc addresses in the message viewer. Just calls the same
76
83
  * validated upsert path as recordSentAddress. */
77
84
  addContact(name: string, email: string): boolean;
85
+ /** Address-book listing — paginated, filterable. */
86
+ listContacts(query: string, page?: number, pageSize?: number): any;
87
+ /** Upsert a contact from the address book UI (edit name). */
88
+ upsertContact(name: string, email: string): {
89
+ ok: true;
90
+ };
91
+ /** Delete a contact from the address book. */
92
+ deleteContact(email: string): {
93
+ ok: true;
94
+ };
95
+ /** Open a configured local path in the OS file explorer. Whitelisted to
96
+ * avoid the UI poking at arbitrary paths. */
97
+ openLocalPath(which: "config" | "log"): Promise<{
98
+ ok: true;
99
+ path: string;
100
+ }>;
78
101
  /** Get all messages in a thread (across folders) for an account. */
79
102
  getThreadMessages(accountId: string, threadId: string): any;
80
103
  /** Read a JSONC config file from the shared cloud dir or local ~/.mailx.