@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/bin/mailx.js +37 -7
- package/client/app.js +359 -32
- package/client/components/address-book.js +199 -0
- package/client/components/calendar.js +217 -0
- package/client/components/folder-tree.js +62 -16
- package/client/components/message-list.js +16 -2
- package/client/components/message-viewer.js +41 -8
- package/client/components/outbox-view.js +104 -0
- package/client/components/tasks.js +256 -0
- package/client/compose/compose.html +2 -2
- package/client/compose/compose.js +83 -43
- package/client/compose/editor.js +67 -0
- package/client/index.html +8 -6
- package/client/lib/api-client.js +18 -0
- package/client/lib/mailxapi.js +14 -0
- package/client/styles/components.css +354 -0
- package/package.json +3 -3
- package/packages/mailx-imap/index.js +19 -2
- package/packages/mailx-service/index.d.ts +23 -0
- package/packages/mailx-service/index.js +123 -14
- package/packages/mailx-service/jsonrpc.js +18 -1
- package/packages/mailx-settings/index.js +18 -3
- package/packages/mailx-store/db.d.ts +17 -0
- package/packages/mailx-store/db.js +122 -4
- package/packages/mailx-types/index.d.ts +1 -0
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
|
|
80
|
-
<button class="rail-btn" id="rail-calendar" title="Calendar
|
|
81
|
-
<button class="rail-btn" id="rail-tasks" title="Tasks
|
|
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="
|
|
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">
|
package/client/lib/api-client.js
CHANGED
|
@@ -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
|
}
|
package/client/lib/mailxapi.js
CHANGED
|
@@ -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.
|
|
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
|
+
"@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.
|
|
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
|
-
|
|
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.
|