nothumanallowed 13.5.113 → 13.5.115
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/package.json +4 -1
- package/src/commands/ui.mjs +409 -4
- package/src/constants.mjs +1 -1
- package/src/services/email-db.mjs +688 -0
- package/src/services/email-imap.mjs +421 -0
- package/src/services/email-smtp.mjs +135 -0
- package/src/services/llm.mjs +7 -1
- package/src/services/tool-executor.mjs +95 -1
- package/src/services/web-ui.mjs +701 -83
package/src/services/web-ui.mjs
CHANGED
|
@@ -1098,101 +1098,579 @@ function refreshPlan(){
|
|
|
1098
1098
|
}
|
|
1099
1099
|
|
|
1100
1100
|
// ---- EMAILS ----
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1101
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1102
|
+
// EMAIL CLIENT — full 3-pane client (folders / message list / reading pane)
|
|
1103
|
+
// Supports Google (existing) + IMAP custom accounts via dropdown
|
|
1104
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1105
|
+
|
|
1106
|
+
var emailState = {
|
|
1107
|
+
accountId: null, // 'google' | imap account id
|
|
1108
|
+
accountType: 'google',
|
|
1109
|
+
labelId: null,
|
|
1110
|
+
labelName: 'Inbox',
|
|
1111
|
+
search: '',
|
|
1112
|
+
offset: 0,
|
|
1113
|
+
limit: 50,
|
|
1114
|
+
messages: [],
|
|
1115
|
+
total: 0,
|
|
1116
|
+
activeMessageId: null,
|
|
1117
|
+
labels: [],
|
|
1118
|
+
accounts: [],
|
|
1119
|
+
composing: false,
|
|
1120
|
+
composeData: null, // {to, subject, inReplyTo, references, replyType}
|
|
1121
|
+
quillEditor: null,
|
|
1122
|
+
};
|
|
1123
|
+
|
|
1124
|
+
function renderEmails(el) {
|
|
1125
|
+
el.innerHTML =
|
|
1126
|
+
// Load Quill CSS once
|
|
1127
|
+
'<link rel="stylesheet" href="https://cdn.quilljs.com/1.3.7/quill.snow.css">' +
|
|
1128
|
+
'<div id="emailClientRoot" style="display:flex;height:calc(100vh - 120px);min-height:500px;gap:0;background:var(--bg2);border-radius:10px;border:1px solid var(--border);overflow:hidden">' +
|
|
1129
|
+
// Col 1: sidebar
|
|
1130
|
+
'<div id="emailSidebar" style="width:200px;min-width:160px;background:var(--bg3);border-right:1px solid var(--border);display:flex;flex-direction:column;overflow-y:auto"></div>' +
|
|
1131
|
+
// Col 2: message list
|
|
1132
|
+
'<div id="emailList" style="width:280px;min-width:220px;border-right:1px solid var(--border);display:flex;flex-direction:column;overflow:hidden">' +
|
|
1133
|
+
'<div id="emailListHeader" style="padding:10px 12px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:6px;flex-shrink:0">' +
|
|
1134
|
+
'<input id="emailSearch" type="text" placeholder="Search..." onkeydown="if(event.key===String.fromCharCode(13)){emailSearch()}" style="flex:1;padding:5px 10px;font-size:11px;background:var(--bg);color:var(--fg);border:1px solid var(--border2);border-radius:5px">' +
|
|
1135
|
+
'<button onclick="emailSearch()" style="padding:5px 8px;font-size:10px;background:var(--bg);color:var(--dim);border:1px solid var(--border);border-radius:4px;cursor:pointer">Go</button>' +
|
|
1136
|
+
'</div>' +
|
|
1137
|
+
'<div id="emailListBody" style="flex:1;overflow-y:auto"></div>' +
|
|
1138
|
+
'<div id="emailListFooter" style="padding:6px 10px;border-top:1px solid var(--border);font-size:10px;color:var(--dim);display:flex;gap:8px;align-items:center;flex-shrink:0">' +
|
|
1139
|
+
'<span id="emailListCount"></span>' +
|
|
1140
|
+
'<button id="emailLoadMore" onclick="emailLoadMore()" style="display:none;padding:3px 10px;font-size:10px;background:var(--bg);color:var(--cyan);border:1px solid var(--cyan);border-radius:4px;cursor:pointer">Load more</button>' +
|
|
1141
|
+
'</div>' +
|
|
1142
|
+
'</div>' +
|
|
1143
|
+
// Col 3: reading pane / compose
|
|
1144
|
+
'<div id="emailPane" style="flex:1;display:flex;flex-direction:column;overflow:hidden"><div style="padding:40px;text-align:center;color:var(--dim);font-size:12px">Select a message</div></div>' +
|
|
1145
|
+
'</div>';
|
|
1146
|
+
|
|
1147
|
+
// Load accounts then render sidebar
|
|
1148
|
+
emailLoadAccounts();
|
|
1119
1149
|
}
|
|
1120
|
-
|
|
1121
|
-
function
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
if(
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1150
|
+
|
|
1151
|
+
function emailLoadAccounts() {
|
|
1152
|
+
apiGet('/api/imap/accounts').then(function(r) {
|
|
1153
|
+
emailState.accounts = r.accounts || [];
|
|
1154
|
+
emailRenderSidebar();
|
|
1155
|
+
// Auto-select: prefer first IMAP account if no Google
|
|
1156
|
+
if (emailState.accountId === null) {
|
|
1157
|
+
if (emailState.accounts.length > 0) {
|
|
1158
|
+
emailSelectAccount(emailState.accounts[0].id, 'imap');
|
|
1159
|
+
} else if (dashLoaded.emails) {
|
|
1160
|
+
emailSelectAccount('google', 'google');
|
|
1131
1161
|
}
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
render();
|
|
1162
|
+
} else {
|
|
1163
|
+
emailRenderSidebar();
|
|
1135
1164
|
}
|
|
1165
|
+
}).catch(function() {
|
|
1166
|
+
if (dashLoaded.emails) emailSelectAccount('google', 'google');
|
|
1167
|
+
else emailRenderSidebar();
|
|
1136
1168
|
});
|
|
1137
1169
|
}
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1170
|
+
|
|
1171
|
+
function emailRenderSidebar() {
|
|
1172
|
+
var sb = document.getElementById('emailSidebar');
|
|
1173
|
+
if (!sb) return;
|
|
1174
|
+
var h = '';
|
|
1175
|
+
|
|
1176
|
+
// Account dropdown
|
|
1177
|
+
h += '<div style="padding:10px 10px 6px;border-bottom:1px solid var(--border)">';
|
|
1178
|
+
h += '<div style="font-size:9px;color:var(--dim);text-transform:uppercase;letter-spacing:1px;margin-bottom:5px">Account</div>';
|
|
1179
|
+
h += '<select id="emailAccountSelect" onchange="emailOnAccountChange(this.value)" style="width:100%;padding:5px 8px;font-size:11px;background:var(--bg);color:var(--fg);border:1px solid var(--border2);border-radius:4px">';
|
|
1180
|
+
if (dashLoaded.emails) {
|
|
1181
|
+
h += '<option value="google"' + (emailState.accountType === 'google' ? ' selected' : '') + '>Google (Gmail)</option>';
|
|
1182
|
+
}
|
|
1183
|
+
for (var i = 0; i < emailState.accounts.length; i++) {
|
|
1184
|
+
var a = emailState.accounts[i];
|
|
1185
|
+
h += '<option value="imap:' + esc(a.id) + '"' + (emailState.accountId === a.id ? ' selected' : '') + '>' + esc(a.display_name || a.email_address) + '</option>';
|
|
1186
|
+
}
|
|
1187
|
+
if (!dashLoaded.emails && emailState.accounts.length === 0) {
|
|
1188
|
+
h += '<option value="">No accounts — add in Settings</option>';
|
|
1189
|
+
}
|
|
1190
|
+
h += '</select></div>';
|
|
1191
|
+
|
|
1192
|
+
// Compose button
|
|
1193
|
+
h += '<div style="padding:8px 10px;border-bottom:1px solid var(--border)">';
|
|
1194
|
+
h += '<button onclick="emailOpenCompose(null)" style="width:100%;padding:7px;background:var(--green3);color:var(--bg);border:none;border-radius:5px;font-size:12px;font-weight:700;cursor:pointer">+ Compose</button>';
|
|
1195
|
+
h += '</div>';
|
|
1196
|
+
|
|
1197
|
+
// Labels / folders
|
|
1198
|
+
h += '<div style="flex:1;overflow-y:auto;padding:6px 0">';
|
|
1199
|
+
if (emailState.accountType === 'google') {
|
|
1200
|
+
var googleFolders = [{id:'INBOX',name:'Inbox',icon:'✉'},{id:'SENT',name:'Sent',icon:'→'},{id:'DRAFTS',name:'Drafts',icon:'📄'},{id:'SPAM',name:'Spam',icon:'🛡'},{id:'TRASH',name:'Trash',icon:'🗑'}];
|
|
1201
|
+
for (var gi = 0; gi < googleFolders.length; gi++) {
|
|
1202
|
+
var gf = googleFolders[gi];
|
|
1203
|
+
var sel = emailState.labelId === gf.id;
|
|
1204
|
+
h += '<div onclick="emailSelectGoogleFolder(\\x27' + gf.id + '\\x27,\\x27' + gf.name + '\\x27)" style="padding:7px 14px;cursor:pointer;font-size:12px;' + (sel ? 'background:var(--green3);color:var(--bg);font-weight:700' : 'color:var(--fg)') + '">' + gf.icon + ' ' + esc(gf.name) + '</div>';
|
|
1205
|
+
}
|
|
1206
|
+
} else {
|
|
1207
|
+
// IMAP labels
|
|
1208
|
+
h += '<div style="padding:4px 10px;display:flex;align-items:center;justify-content:space-between">';
|
|
1209
|
+
h += '<span style="font-size:9px;color:var(--dim);text-transform:uppercase;letter-spacing:1px">Labels</span>';
|
|
1210
|
+
h += '<button onclick="emailShowNewLabel()" style="font-size:10px;background:none;border:none;color:var(--cyan);cursor:pointer">+</button>';
|
|
1211
|
+
h += '</div>';
|
|
1212
|
+
for (var li = 0; li < emailState.labels.length; li++) {
|
|
1213
|
+
var lbl = emailState.labels[li];
|
|
1214
|
+
var lsel = emailState.labelId === lbl.id;
|
|
1215
|
+
var lcolor = lbl.color || 'var(--dim)';
|
|
1216
|
+
var unread = lbl.unread_count > 0 ? '<span style="margin-left:auto;background:var(--green3);color:var(--bg);border-radius:10px;padding:0 5px;font-size:9px;font-weight:700">' + lbl.unread_count + '</span>' : '';
|
|
1217
|
+
h += '<div onclick="emailSelectLabel(\\x27' + lbl.id + '\\x27,\\x27' + esc(lbl.name) + '\\x27)" style="padding:6px 14px;cursor:pointer;font-size:12px;display:flex;align-items:center;gap:6px;' + (lsel ? 'background:var(--green3);color:var(--bg);font-weight:700;border-radius:4px;margin:0 4px' : 'color:var(--fg)') + '">';
|
|
1218
|
+
h += '<span style="width:8px;height:8px;border-radius:50%;background:' + lcolor + ';flex-shrink:0;display:inline-block"></span>';
|
|
1219
|
+
h += esc(lbl.name);
|
|
1220
|
+
if (!lbl.is_system) h += '<span onclick="event.stopPropagation();emailEditLabel(\\x27' + lbl.id + '\\x27,\\x27' + esc(lbl.name) + '\\x27,\\x27' + (lbl.color||'') + '\\x27)" style="margin-left:auto;font-size:9px;color:var(--dim);cursor:pointer">✎</span>';
|
|
1221
|
+
else h += unread;
|
|
1222
|
+
h += '</div>';
|
|
1147
1223
|
}
|
|
1224
|
+
}
|
|
1225
|
+
h += '</div>';
|
|
1226
|
+
|
|
1227
|
+
// Sync / settings shortcuts
|
|
1228
|
+
if (emailState.accountType === 'imap' && emailState.accountId) {
|
|
1229
|
+
h += '<div style="padding:8px 10px;border-top:1px solid var(--border)">';
|
|
1230
|
+
h += '<button onclick="emailSyncCurrent()" style="width:100%;padding:5px;background:var(--bg);color:var(--cyan);border:1px solid var(--cyan);border-radius:4px;font-size:10px;cursor:pointer">Sync now</button>';
|
|
1231
|
+
h += '</div>';
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
sb.innerHTML = h;
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
function emailOnAccountChange(val) {
|
|
1238
|
+
if (val === 'google') {
|
|
1239
|
+
emailSelectAccount('google', 'google');
|
|
1240
|
+
} else if (val.startsWith('imap:')) {
|
|
1241
|
+
emailSelectAccount(val.slice(5), 'imap');
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
function emailSelectAccount(accountId, type) {
|
|
1246
|
+
emailState.accountId = accountId;
|
|
1247
|
+
emailState.accountType = type;
|
|
1248
|
+
emailState.labelId = null;
|
|
1249
|
+
emailState.labelName = 'Inbox';
|
|
1250
|
+
emailState.offset = 0;
|
|
1251
|
+
emailState.messages = [];
|
|
1252
|
+
emailState.activeMessageId = null;
|
|
1253
|
+
if (type === 'imap') {
|
|
1254
|
+
apiGet('/api/imap/labels?accountId=' + accountId).then(function(r) {
|
|
1255
|
+
emailState.labels = r.labels || [];
|
|
1256
|
+
// Default to inbox system label
|
|
1257
|
+
var inbox = emailState.labels.find(function(l) { return l.system_type === 'inbox'; });
|
|
1258
|
+
if (inbox) { emailState.labelId = inbox.id; emailState.labelName = 'Inbox'; }
|
|
1259
|
+
emailRenderSidebar();
|
|
1260
|
+
emailLoadMessages();
|
|
1261
|
+
});
|
|
1262
|
+
} else {
|
|
1263
|
+
emailState.labels = [];
|
|
1264
|
+
emailState.labelId = 'INBOX';
|
|
1265
|
+
emailRenderSidebar();
|
|
1266
|
+
emailLoadGoogleMessages();
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
function emailSelectLabel(labelId, labelName) {
|
|
1271
|
+
emailState.labelId = labelId;
|
|
1272
|
+
emailState.labelName = labelName;
|
|
1273
|
+
emailState.offset = 0;
|
|
1274
|
+
emailState.messages = [];
|
|
1275
|
+
emailState.activeMessageId = null;
|
|
1276
|
+
emailRenderSidebar();
|
|
1277
|
+
emailLoadMessages();
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
function emailSelectGoogleFolder(folderId, folderName) {
|
|
1281
|
+
emailState.labelId = folderId;
|
|
1282
|
+
emailState.labelName = folderName;
|
|
1283
|
+
emailState.offset = 0;
|
|
1284
|
+
emailState.messages = [];
|
|
1285
|
+
emailRenderSidebar();
|
|
1286
|
+
emailLoadGoogleMessages();
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
function emailSearch() {
|
|
1290
|
+
var inp = document.getElementById('emailSearch');
|
|
1291
|
+
emailState.search = inp ? inp.value.trim() : '';
|
|
1292
|
+
emailState.offset = 0;
|
|
1293
|
+
emailState.messages = [];
|
|
1294
|
+
if (emailState.accountType === 'imap') emailLoadMessages();
|
|
1295
|
+
else emailLoadGoogleMessages();
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
function emailLoadMessages() {
|
|
1299
|
+
var listBody = document.getElementById('emailListBody');
|
|
1300
|
+
if (listBody) listBody.innerHTML = '<div style="padding:20px;text-align:center"><div class="spinner"></div></div>';
|
|
1301
|
+
var qs = '/api/imap/messages?accountId=' + encodeURIComponent(emailState.accountId) +
|
|
1302
|
+
'&limit=' + emailState.limit + '&offset=' + emailState.offset;
|
|
1303
|
+
if (emailState.labelId) qs += '&labelId=' + encodeURIComponent(emailState.labelId);
|
|
1304
|
+
if (emailState.search) qs += '&search=' + encodeURIComponent(emailState.search);
|
|
1305
|
+
apiGet(qs).then(function(r) {
|
|
1306
|
+
if (emailState.offset === 0) emailState.messages = r.messages || [];
|
|
1307
|
+
else emailState.messages = emailState.messages.concat(r.messages || []);
|
|
1308
|
+
emailState.total = r.total || 0;
|
|
1309
|
+
emailRenderMessageList();
|
|
1310
|
+
}).catch(function(e) {
|
|
1311
|
+
if (listBody) listBody.innerHTML = '<div style="padding:20px;color:var(--red);font-size:12px">' + esc(e.message) + '</div>';
|
|
1148
1312
|
});
|
|
1149
1313
|
}
|
|
1150
|
-
|
|
1151
|
-
function
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
var
|
|
1155
|
-
if(
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1314
|
+
|
|
1315
|
+
function emailLoadGoogleMessages() {
|
|
1316
|
+
var listBody = document.getElementById('emailListBody');
|
|
1317
|
+
if (listBody) listBody.innerHTML = '<div style="padding:20px;text-align:center"><div class="spinner"></div></div>';
|
|
1318
|
+
var q = emailState.labelId === 'INBOX' ? 'in:inbox' : 'in:' + emailState.labelId.toLowerCase();
|
|
1319
|
+
if (emailState.search) q = emailState.search;
|
|
1320
|
+
import('../services/google-gmail.mjs').then(function(gm) {
|
|
1321
|
+
gm.listMessages(config, q, 50).then(function(refs) {
|
|
1322
|
+
if (!refs || !refs.length) { emailState.messages = []; emailRenderMessageList(); return; }
|
|
1323
|
+
var page = refs.slice(0, 50);
|
|
1324
|
+
return Promise.all(page.slice(0, 20).map(function(r) { return gm.getMessage(config, r.id); }));
|
|
1325
|
+
}).then(function(msgs) {
|
|
1326
|
+
emailState.messages = (msgs || []).map(function(m) {
|
|
1327
|
+
return { id: m.id, subject: m.subject, from_name: m.from, from_address: m.from, internal_date: m.date, body_preview: m.snippet, is_read: !m.isUnread, is_starred: false, has_attachments: false, _google: true };
|
|
1328
|
+
});
|
|
1329
|
+
emailRenderMessageList();
|
|
1330
|
+
});
|
|
1331
|
+
}).catch(function() {
|
|
1332
|
+
// Fallback to dash emails
|
|
1333
|
+
emailState.messages = (dash.emails || []).map(function(m) {
|
|
1334
|
+
return { id: m.id, subject: m.subject, from_name: m.from, from_address: m.from, internal_date: m.date, body_preview: m.snippet, is_read: !m.isUnread, is_starred: false, has_attachments: false, _google: true };
|
|
1335
|
+
});
|
|
1336
|
+
emailRenderMessageList();
|
|
1337
|
+
});
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
function emailRenderMessageList() {
|
|
1341
|
+
var listBody = document.getElementById('emailListBody');
|
|
1342
|
+
var listCount = document.getElementById('emailListCount');
|
|
1343
|
+
var loadMoreBtn = document.getElementById('emailLoadMore');
|
|
1344
|
+
if (!listBody) return;
|
|
1345
|
+
if (listCount) listCount.textContent = emailState.total + ' messages';
|
|
1346
|
+
if (loadMoreBtn) loadMoreBtn.style.display = (emailState.messages.length < emailState.total && emailState.accountType === 'imap') ? 'inline-block' : 'none';
|
|
1347
|
+
if (!emailState.messages.length) {
|
|
1348
|
+
listBody.innerHTML = '<div style="padding:24px;text-align:center;color:var(--dim);font-size:12px">No messages</div>';
|
|
1349
|
+
return;
|
|
1159
1350
|
}
|
|
1160
|
-
var
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
var m
|
|
1165
|
-
var
|
|
1166
|
-
|
|
1167
|
-
h+='<div style="
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1351
|
+
var h = '';
|
|
1352
|
+
for (var i = 0; i < emailState.messages.length; i++) {
|
|
1353
|
+
var m = emailState.messages[i];
|
|
1354
|
+
var active = m.id === emailState.activeMessageId;
|
|
1355
|
+
var unread = !m.is_read;
|
|
1356
|
+
var date = m.internal_date ? m.internal_date.slice(0, 10) : '';
|
|
1357
|
+
var from = esc(m.from_name || m.from_address || '');
|
|
1358
|
+
h += '<div onclick="emailOpenMessage(\\x27' + esc(m.id) + '\\x27)" style="padding:10px 12px;cursor:pointer;border-bottom:1px solid var(--border);' +
|
|
1359
|
+
(active ? 'background:var(--green3);' : 'background:' + (unread ? 'var(--bg2)' : 'var(--bg3)') + ';') +
|
|
1360
|
+
(unread ? 'border-left:3px solid var(--green);' : 'border-left:3px solid transparent;') + '">' +
|
|
1361
|
+
'<div style="display:flex;justify-content:space-between;margin-bottom:2px">' +
|
|
1362
|
+
'<span style="font-size:12px;font-weight:' + (unread ? '700' : '500') + ';color:' + (active ? 'var(--bg)' : 'var(--bright)') + ';max-width:150px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' + from + '</span>' +
|
|
1363
|
+
'<span style="font-size:10px;color:' + (active ? 'var(--bg)' : 'var(--dim)') + '">' + esc(date) + '</span>' +
|
|
1364
|
+
'</div>' +
|
|
1365
|
+
'<div style="font-size:11px;font-weight:' + (unread ? '600' : '400') + ';color:' + (active ? 'var(--bg)' : 'var(--text)') + ';white-space:nowrap;overflow:hidden;text-overflow:ellipsis">' + esc(m.subject || '(no subject)') + '</div>' +
|
|
1366
|
+
'<div style="font-size:10px;color:' + (active ? 'rgba(0,0,0,0.6)' : 'var(--dim)') + ';white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-top:2px">' + esc((m.body_preview || '').slice(0, 80)) + '</div>' +
|
|
1367
|
+
'</div>';
|
|
1368
|
+
}
|
|
1369
|
+
listBody.innerHTML = h;
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
function emailLoadMore() {
|
|
1373
|
+
emailState.offset += emailState.limit;
|
|
1374
|
+
emailLoadMessages();
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
function emailOpenMessage(id) {
|
|
1378
|
+
emailState.activeMessageId = id;
|
|
1379
|
+
emailRenderMessageList();
|
|
1380
|
+
var pane = document.getElementById('emailPane');
|
|
1381
|
+
if (!pane) return;
|
|
1382
|
+
pane.innerHTML = '<div style="padding:30px;text-align:center"><div class="spinner"></div></div>';
|
|
1383
|
+
|
|
1384
|
+
if (emailState.accountType === 'google') {
|
|
1385
|
+
apiPost('/api/email/read', { messageId: id }).then(function(r) {
|
|
1386
|
+
var m = r.message || r;
|
|
1387
|
+
emailRenderReadingPane(pane, {
|
|
1388
|
+
id: id, subject: m.subject, from_address: m.from, from_name: m.from,
|
|
1389
|
+
to_addresses: m.to, internal_date: m.date,
|
|
1390
|
+
body_html: m.bodyHtml || null, body_text: m.body || m.snippet || '',
|
|
1391
|
+
attachments: [], labels: [], is_read: true, is_starred: false, _google: true,
|
|
1392
|
+
});
|
|
1393
|
+
apiPost('/api/email/mark-read', { messageId: id }).catch(function() {});
|
|
1394
|
+
});
|
|
1395
|
+
} else {
|
|
1396
|
+
apiGet('/api/imap/message?id=' + encodeURIComponent(id)).then(function(r) {
|
|
1397
|
+
emailRenderReadingPane(pane, r.message);
|
|
1398
|
+
// Update read state in local list
|
|
1399
|
+
var msg = emailState.messages.find(function(m) { return m.id === id; });
|
|
1400
|
+
if (msg) msg.is_read = true;
|
|
1401
|
+
emailRenderMessageList();
|
|
1402
|
+
}).catch(function(e) {
|
|
1403
|
+
pane.innerHTML = '<div style="padding:20px;color:var(--red);font-size:12px">' + esc(e.message) + '</div>';
|
|
1404
|
+
});
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
function emailRenderReadingPane(pane, m) {
|
|
1409
|
+
if (!m) { pane.innerHTML = '<div style="padding:20px;color:var(--red);font-size:12px">Could not load message</div>'; return; }
|
|
1410
|
+
|
|
1411
|
+
var toStr = '';
|
|
1412
|
+
try {
|
|
1413
|
+
var toArr = typeof m.to_addresses === 'string' ? JSON.parse(m.to_addresses) : (m.to_addresses || []);
|
|
1414
|
+
toStr = toArr.map(function(a) { return a.name ? a.name + ' <' + a.address + '>' : (a.address || a); }).join(', ');
|
|
1415
|
+
} catch(e) { toStr = m.to_addresses || ''; }
|
|
1416
|
+
|
|
1417
|
+
var h = '<div style="display:flex;align-items:center;justify-content:space-between;padding:10px 16px;border-bottom:1px solid var(--border);flex-shrink:0;background:var(--bg3)">';
|
|
1418
|
+
h += '<div style="display:flex;gap:6px">';
|
|
1419
|
+
h += '<button onclick="emailOpenCompose({replyTo:\\x27' + esc(m.id) + '\\x27,type:\\x27reply\\x27,subject:\\x27Re: ' + esc(m.subject||'') + '\\x27,to:\\x27' + esc(m.from_address||'') + '\\x27,inReplyTo:\\x27' + esc(m.message_id||m.id) + '\\x27})" style="padding:5px 12px;font-size:11px;background:var(--green3);color:var(--bg);border:none;border-radius:4px;cursor:pointer;font-weight:700">Reply</button>';
|
|
1420
|
+
h += '<button onclick="emailOpenCompose({replyTo:\\x27' + esc(m.id) + '\\x27,type:\\x27forward\\x27,subject:\\x27Fwd: ' + esc(m.subject||'') + '\\x27})" style="padding:5px 12px;font-size:11px;background:var(--bg);color:var(--dim);border:1px solid var(--border);border-radius:4px;cursor:pointer">Forward</button>';
|
|
1421
|
+
if (!m._google) {
|
|
1422
|
+
h += '<button onclick="emailTrash(\\x27' + esc(m.id) + '\\x27)" style="padding:5px 10px;font-size:11px;background:var(--bg);color:var(--red);border:1px solid var(--border);border-radius:4px;cursor:pointer">🗑</button>';
|
|
1423
|
+
h += '<button onclick="emailToggleStar(\\x27' + esc(m.id) + '\\x27,' + (m.is_starred ? 'false' : 'true') + ')" style="padding:5px 10px;font-size:11px;background:var(--bg);color:var(--amber,#F59E0B);border:1px solid var(--border);border-radius:4px;cursor:pointer">' + (m.is_starred ? '★' : '☆') + '</button>';
|
|
1424
|
+
// Label assign
|
|
1425
|
+
h += '<select onchange="emailAssignLabel(\\x27' + esc(m.id) + '\\x27,this.value);this.value=\\x27\\x27" style="font-size:10px;padding:4px;background:var(--bg);color:var(--dim);border:1px solid var(--border);border-radius:4px"><option value="">+ Label</option>';
|
|
1426
|
+
for (var li = 0; li < emailState.labels.length; li++) {
|
|
1427
|
+
var lbl = emailState.labels[li];
|
|
1428
|
+
h += '<option value="' + esc(lbl.id) + '">' + esc(lbl.name) + '</option>';
|
|
1429
|
+
}
|
|
1430
|
+
h += '</select>';
|
|
1431
|
+
}
|
|
1432
|
+
h += '<button onclick="emailAskAgent(\\x27' + esc(m.id) + '\\x27)" style="padding:5px 10px;font-size:11px;background:var(--bg);color:var(--cyan);border:1px solid var(--cyan);border-radius:4px;cursor:pointer">Ask AI</button>';
|
|
1433
|
+
h += '</div></div>';
|
|
1434
|
+
|
|
1435
|
+
// Header
|
|
1436
|
+
h += '<div style="padding:14px 16px;border-bottom:1px solid var(--border);flex-shrink:0">';
|
|
1437
|
+
h += '<div style="font-size:15px;font-weight:700;color:var(--bright);margin-bottom:8px">' + esc(m.subject || '(no subject)') + '</div>';
|
|
1438
|
+
h += '<div style="font-size:11px;color:var(--dim)"><strong style="color:var(--text)">From:</strong> ' + esc((m.from_name && m.from_name !== m.from_address ? m.from_name + ' <' + m.from_address + '>' : m.from_address) || '') + '</div>';
|
|
1439
|
+
h += '<div style="font-size:11px;color:var(--dim)"><strong style="color:var(--text)">To:</strong> ' + esc(toStr) + '</div>';
|
|
1440
|
+
h += '<div style="font-size:11px;color:var(--dim)"><strong style="color:var(--text)">Date:</strong> ' + esc(m.internal_date || '') + '</div>';
|
|
1441
|
+
|
|
1442
|
+
// Attachments
|
|
1443
|
+
if (m.attachments && m.attachments.length) {
|
|
1444
|
+
h += '<div style="margin-top:8px;display:flex;flex-wrap:wrap;gap:6px">';
|
|
1445
|
+
for (var ai = 0; ai < m.attachments.length; ai++) {
|
|
1446
|
+
var att = m.attachments[ai];
|
|
1447
|
+
var attUrl = '/api/imap/attachment?messageId=' + encodeURIComponent(m.id) + '&partId=' + encodeURIComponent(att.part_id || '') + '&accountId=' + encodeURIComponent(emailState.accountId);
|
|
1448
|
+
h += '<a href="' + attUrl + '" download="' + esc(att.filename || 'attachment') + '" style="font-size:10px;padding:4px 10px;background:var(--bg3);border:1px solid var(--border);border-radius:4px;color:var(--cyan);text-decoration:none">📎 ' + esc(att.filename || 'attachment') + ' (' + Math.round((att.size_bytes||0)/1024) + 'KB)</a>';
|
|
1449
|
+
}
|
|
1450
|
+
h += '</div>';
|
|
1451
|
+
}
|
|
1452
|
+
h += '</div>';
|
|
1453
|
+
|
|
1454
|
+
// Body
|
|
1455
|
+
h += '<div style="flex:1;overflow-y:auto;padding:16px">';
|
|
1456
|
+
if (m.body_html) {
|
|
1457
|
+
h += '<iframe id="emailBodyFrame" sandbox="allow-same-origin" style="width:100%;min-height:400px;border:none;background:#fff" srcdoc="' + m.body_html.replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>') + '"></iframe>';
|
|
1458
|
+
} else {
|
|
1459
|
+
h += '<pre style="white-space:pre-wrap;word-wrap:break-word;font-size:13px;line-height:1.7;color:var(--text);font-family:inherit">' + esc(m.body_text || m.body_preview || '(empty)') + '</pre>';
|
|
1460
|
+
}
|
|
1461
|
+
h += '</div>';
|
|
1462
|
+
|
|
1463
|
+
pane.innerHTML = '<div style="display:flex;flex-direction:column;height:100%">' + h + '</div>';
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
function emailOpenCompose(opts) {
|
|
1467
|
+
var pane = document.getElementById('emailPane');
|
|
1468
|
+
if (!pane) return;
|
|
1469
|
+
opts = opts || {};
|
|
1470
|
+
|
|
1471
|
+
var h = '<div style="display:flex;flex-direction:column;height:100%;padding:14px;gap:10px">';
|
|
1472
|
+
h += '<div style="font-size:13px;font-weight:700;color:var(--bright);border-bottom:1px solid var(--border);padding-bottom:8px">Compose</div>';
|
|
1473
|
+
h += '<input id="compTo" type="text" placeholder="To" value="' + esc(opts.to || '') + '" style="padding:7px 10px;font-size:12px;background:var(--bg);color:var(--fg);border:1px solid var(--border2);border-radius:5px">';
|
|
1474
|
+
h += '<input id="compCc" type="text" placeholder="Cc" style="padding:7px 10px;font-size:12px;background:var(--bg);color:var(--fg);border:1px solid var(--border2);border-radius:5px">';
|
|
1475
|
+
h += '<input id="compSubject" type="text" placeholder="Subject" value="' + esc(opts.subject || '') + '" style="padding:7px 10px;font-size:12px;background:var(--bg);color:var(--fg);border:1px solid var(--border2);border-radius:5px">';
|
|
1476
|
+
h += '<input type="hidden" id="compInReplyTo" value="' + esc(opts.inReplyTo || '') + '">';
|
|
1477
|
+
h += '<input type="hidden" id="compReplyToId" value="' + esc(opts.replyTo || '') + '">';
|
|
1478
|
+
// Quill editor container
|
|
1479
|
+
h += '<div id="quillContainer" style="flex:1;min-height:200px;background:#fff;border-radius:5px;overflow:auto"></div>';
|
|
1480
|
+
// Toolbar
|
|
1481
|
+
h += '<div style="display:flex;gap:8px;flex-wrap:wrap">';
|
|
1482
|
+
h += '<button onclick="emailSend()" style="padding:7px 18px;background:var(--green3);color:var(--bg);border:none;border-radius:5px;font-size:12px;font-weight:700;cursor:pointer">Send</button>';
|
|
1483
|
+
h += '<button onclick="emailSaveDraft()" style="padding:7px 14px;background:var(--bg);color:var(--dim);border:1px solid var(--border);border-radius:5px;font-size:12px;cursor:pointer">Save Draft</button>';
|
|
1484
|
+
h += '<button onclick="emailCloseCompose()" style="padding:7px 14px;background:var(--bg);color:var(--dim);border:1px solid var(--border);border-radius:5px;font-size:12px;cursor:pointer">Discard</button>';
|
|
1485
|
+
h += '<div id="composeStatus" style="font-size:11px;color:var(--dim);align-self:center"></div>';
|
|
1486
|
+
h += '</div></div>';
|
|
1487
|
+
|
|
1488
|
+
pane.innerHTML = h;
|
|
1489
|
+
emailState.composeData = opts;
|
|
1490
|
+
|
|
1491
|
+
// Init Quill
|
|
1492
|
+
if (typeof Quill === 'undefined') {
|
|
1493
|
+
var script = document.createElement('script');
|
|
1494
|
+
script.src = 'https://cdn.quilljs.com/1.3.7/quill.min.js';
|
|
1495
|
+
script.onload = function() { emailInitQuill(opts); };
|
|
1496
|
+
document.head.appendChild(script);
|
|
1497
|
+
} else {
|
|
1498
|
+
emailInitQuill(opts);
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
function emailInitQuill(opts) {
|
|
1503
|
+
emailState.quillEditor = new Quill('#quillContainer', {
|
|
1504
|
+
theme: 'snow',
|
|
1505
|
+
modules: { toolbar: [
|
|
1506
|
+
['bold', 'italic', 'underline', 'strike'],
|
|
1507
|
+
[{ list: 'ordered' }, { list: 'bullet' }],
|
|
1508
|
+
['link', 'blockquote', 'code-block'],
|
|
1509
|
+
[{ color: [] }, { background: [] }],
|
|
1510
|
+
['clean'],
|
|
1511
|
+
]},
|
|
1512
|
+
placeholder: 'Write your message...',
|
|
1513
|
+
});
|
|
1514
|
+
// Pre-fill for forward
|
|
1515
|
+
if (opts && opts.type === 'forward' && opts.body) {
|
|
1516
|
+
emailState.quillEditor.clipboard.dangerouslyPasteHTML('<br><br><hr><p>---------- Forwarded message ----------</p>' + (opts.body || ''));
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
function emailSend() {
|
|
1521
|
+
var status = document.getElementById('composeStatus');
|
|
1522
|
+
var to = document.getElementById('compTo');
|
|
1523
|
+
var cc = document.getElementById('compCc');
|
|
1524
|
+
var subject = document.getElementById('compSubject');
|
|
1525
|
+
var inReplyTo = document.getElementById('compInReplyTo');
|
|
1526
|
+
if (!to || !to.value.trim()) { if (status) { status.textContent = 'Recipient required'; status.style.color = 'var(--red)'; } return; }
|
|
1527
|
+
|
|
1528
|
+
var bodyHtml = '';
|
|
1529
|
+
var bodyText = '';
|
|
1530
|
+
if (emailState.quillEditor) {
|
|
1531
|
+
bodyHtml = emailState.quillEditor.root.innerHTML;
|
|
1532
|
+
bodyText = emailState.quillEditor.getText();
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
var accountId = emailState.accountType === 'imap' ? emailState.accountId : null;
|
|
1536
|
+
if (!accountId) {
|
|
1537
|
+
// Google send via existing tool
|
|
1538
|
+
if (status) { status.textContent = 'Sending via Google...'; status.style.color = 'var(--dim)'; }
|
|
1539
|
+
apiPost('/api/email/send', { to: to.value.trim(), subject: subject.value.trim(), body: bodyText }).then(function(r) {
|
|
1540
|
+
if (r.ok || r.id) { if (status) { status.textContent = 'Sent!'; status.style.color = 'var(--green)'; } setTimeout(emailCloseCompose, 800); }
|
|
1541
|
+
else { if (status) { status.textContent = r.error || 'Error'; status.style.color = 'var(--red)'; } }
|
|
1542
|
+
}).catch(function(e) { if (status) { status.textContent = e.message; status.style.color = 'var(--red)'; } });
|
|
1543
|
+
return;
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
if (status) { status.textContent = 'Sending...'; status.style.color = 'var(--dim)'; }
|
|
1547
|
+
apiPost('/api/imap/send', {
|
|
1548
|
+
accountId: accountId,
|
|
1549
|
+
to: to.value.trim(),
|
|
1550
|
+
cc: cc && cc.value.trim() ? cc.value.trim() : undefined,
|
|
1551
|
+
subject: subject.value.trim(),
|
|
1552
|
+
bodyHtml: bodyHtml,
|
|
1553
|
+
bodyText: bodyText,
|
|
1554
|
+
inReplyTo: inReplyTo && inReplyTo.value ? inReplyTo.value : undefined,
|
|
1555
|
+
}).then(function(r) {
|
|
1556
|
+
if (r.ok) {
|
|
1557
|
+
if (status) { status.textContent = 'Sent!'; status.style.color = 'var(--green)'; }
|
|
1558
|
+
setTimeout(emailCloseCompose, 800);
|
|
1559
|
+
} else {
|
|
1560
|
+
if (status) { status.textContent = r.error || 'Error'; status.style.color = 'var(--red)'; }
|
|
1561
|
+
}
|
|
1562
|
+
}).catch(function(e) {
|
|
1563
|
+
if (status) { status.textContent = e.message; status.style.color = 'var(--red)'; }
|
|
1181
1564
|
});
|
|
1182
1565
|
}
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1566
|
+
|
|
1567
|
+
function emailSaveDraft() {
|
|
1568
|
+
var accountId = emailState.accountType === 'imap' ? emailState.accountId : null;
|
|
1569
|
+
if (!accountId) return;
|
|
1570
|
+
var to = document.getElementById('compTo');
|
|
1571
|
+
var subject = document.getElementById('compSubject');
|
|
1572
|
+
var bodyHtml = emailState.quillEditor ? emailState.quillEditor.root.innerHTML : '';
|
|
1573
|
+
apiPost('/api/imap/drafts/save', {
|
|
1574
|
+
accountId: accountId,
|
|
1575
|
+
to: to ? [{ address: to.value.trim() }] : [],
|
|
1576
|
+
subject: subject ? subject.value.trim() : '',
|
|
1577
|
+
body_html: bodyHtml,
|
|
1578
|
+
}).then(function() {
|
|
1579
|
+
var s = document.getElementById('composeStatus');
|
|
1580
|
+
if (s) { s.textContent = 'Draft saved'; s.style.color = 'var(--green)'; }
|
|
1581
|
+
});
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
function emailCloseCompose() {
|
|
1585
|
+
emailState.quillEditor = null;
|
|
1586
|
+
var pane = document.getElementById('emailPane');
|
|
1587
|
+
if (pane) pane.innerHTML = '<div style="padding:40px;text-align:center;color:var(--dim);font-size:12px">Select a message</div>';
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
function emailTrash(id) {
|
|
1591
|
+
if (!confirm('Move to trash? Email stays on the server — only removed from local view.')) return;
|
|
1592
|
+
apiPost('/api/imap/trash', { messageId: id }).then(function() {
|
|
1593
|
+
emailState.messages = emailState.messages.filter(function(m) { return m.id !== id; });
|
|
1594
|
+
emailRenderMessageList();
|
|
1595
|
+
var pane = document.getElementById('emailPane');
|
|
1596
|
+
if (pane) pane.innerHTML = '<div style="padding:40px;text-align:center;color:var(--dim);font-size:12px">Message moved to trash</div>';
|
|
1597
|
+
showToast('success', 'Trash', 'Moved to trash (server untouched)');
|
|
1598
|
+
});
|
|
1189
1599
|
}
|
|
1190
|
-
|
|
1600
|
+
|
|
1601
|
+
function emailToggleStar(id, isStarred) {
|
|
1602
|
+
apiPost('/api/imap/mark-starred', { messageId: id, isStarred: isStarred }).then(function() {
|
|
1603
|
+
showToast('success', isStarred ? 'Starred' : 'Unstarred', '');
|
|
1604
|
+
emailOpenMessage(id);
|
|
1605
|
+
});
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
function emailAssignLabel(messageId, labelId) {
|
|
1609
|
+
if (!labelId) return;
|
|
1610
|
+
apiPost('/api/imap/labels/assign', { messageId: messageId, labelId: labelId }).then(function() {
|
|
1611
|
+
showToast('success', 'Label', 'Label assigned');
|
|
1612
|
+
});
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
function emailAskAgent(id) {
|
|
1191
1616
|
switchView('chat');
|
|
1192
|
-
setTimeout(function(){
|
|
1193
|
-
var inp=document.getElementById('chatInput');
|
|
1194
|
-
if(inp){inp.value='
|
|
1195
|
-
},200);
|
|
1617
|
+
setTimeout(function() {
|
|
1618
|
+
var inp = document.getElementById('chatInput');
|
|
1619
|
+
if (inp) { inp.value = 'Analyze email ' + id + ' — extract key information, action items, and summarize.'; inp.focus(); }
|
|
1620
|
+
}, 200);
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
function emailSyncCurrent() {
|
|
1624
|
+
if (!emailState.accountId || emailState.accountType !== 'imap') return;
|
|
1625
|
+
apiPost('/api/imap/sync', { accountId: emailState.accountId }).then(function() {
|
|
1626
|
+
showToast('success', 'Sync', 'Sync started — messages will appear shortly');
|
|
1627
|
+
setTimeout(function() { emailLoadMessages(); }, 5000);
|
|
1628
|
+
});
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
function emailShowNewLabel() {
|
|
1632
|
+
var name = prompt('New label name:');
|
|
1633
|
+
if (!name) return;
|
|
1634
|
+
var color = prompt('Color (hex, e.g. #3B82F6) or leave empty:', '');
|
|
1635
|
+
apiPost('/api/imap/labels/create', { accountId: emailState.accountId, name: name, color: color || null }).then(function(r) {
|
|
1636
|
+
if (r.ok) {
|
|
1637
|
+
apiGet('/api/imap/labels?accountId=' + emailState.accountId).then(function(r2) {
|
|
1638
|
+
emailState.labels = r2.labels || [];
|
|
1639
|
+
emailRenderSidebar();
|
|
1640
|
+
});
|
|
1641
|
+
}
|
|
1642
|
+
});
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
function emailEditLabel(id, name, color) {
|
|
1646
|
+
var newName = prompt('Label name:', name);
|
|
1647
|
+
if (newName === null) return;
|
|
1648
|
+
var newColor = prompt('Color (hex):', color);
|
|
1649
|
+
apiPost('/api/imap/labels/update', { id: id, name: newName, color: newColor || null }).then(function() {
|
|
1650
|
+
apiGet('/api/imap/labels?accountId=' + emailState.accountId).then(function(r) {
|
|
1651
|
+
emailState.labels = r.labels || [];
|
|
1652
|
+
emailRenderSidebar();
|
|
1653
|
+
});
|
|
1654
|
+
});
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
// Legacy compat for Google (existing code paths)
|
|
1658
|
+
var openEmailId = null;
|
|
1659
|
+
function openEmail(id) { emailOpenMessage(id); }
|
|
1660
|
+
function replyToEmail(id) { emailOpenCompose({ replyTo: id }); }
|
|
1661
|
+
function askAgentAboutEmail(id) { emailAskAgent(id); }
|
|
1662
|
+
function markAllEmailsRead() {
|
|
1663
|
+
if (emailState.accountType === 'imap' && emailState.accountId) {
|
|
1664
|
+
apiPost('/api/imap/mark-all-read', { accountId: emailState.accountId, labelId: emailState.labelId || null }).then(function(r) {
|
|
1665
|
+
emailState.messages.forEach(function(m) { m.is_read = true; });
|
|
1666
|
+
emailRenderMessageList();
|
|
1667
|
+
showToast('success', 'All Read', 'Marked ' + (r.count || 0) + ' emails as read');
|
|
1668
|
+
});
|
|
1669
|
+
} else {
|
|
1670
|
+
apiPost('/api/email/mark-all-read', {}).then(function(r) {
|
|
1671
|
+
if (r && r.ok) { dash.emails.forEach(function(e) { e.isUnread = false; }); updateBadges(); showToast('success', 'All Read', 'Marked ' + (r.count || 0) + ' emails as read'); }
|
|
1672
|
+
});
|
|
1673
|
+
}
|
|
1196
1674
|
}
|
|
1197
1675
|
|
|
1198
1676
|
// ---- CALENDAR (monthly grid + day detail modal) ----
|
|
@@ -2605,7 +3083,7 @@ function renderSettings(el) {
|
|
|
2605
3083
|
return;
|
|
2606
3084
|
}
|
|
2607
3085
|
|
|
2608
|
-
el.innerHTML = '<div style="max-width:
|
|
3086
|
+
el.innerHTML = '<div style="max-width:620px;margin:0 auto">' +
|
|
2609
3087
|
settingsSection('profile', 'User Profile', 'Agents use this when you say "my home", "my city", etc.', [
|
|
2610
3088
|
['name', 'Name', 'e.g. John Smith'],
|
|
2611
3089
|
['email', 'Email', 'e.g. john@example.com'],
|
|
@@ -2646,7 +3124,147 @@ function renderSettings(el) {
|
|
|
2646
3124
|
'</div>' +
|
|
2647
3125
|
'<div id="googleStatus" style="margin-top:8px;font-size:10px;color:var(--dim)"></div>' +
|
|
2648
3126
|
'</div>' +
|
|
3127
|
+
renderImapAccountsSettings() +
|
|
2649
3128
|
'</div>';
|
|
3129
|
+
setTimeout(loadImapAccounts, 100);
|
|
3130
|
+
}
|
|
3131
|
+
|
|
3132
|
+
function renderImapAccountsSettings() {
|
|
3133
|
+
return '<div class="card" id="imapAccountsCard" style="margin-top:16px">' +
|
|
3134
|
+
'<div class="card__title" style="display:flex;align-items:center;justify-content:space-between">' +
|
|
3135
|
+
'<span>Email Accounts (IMAP/SMTP)</span>' +
|
|
3136
|
+
'<button onclick="showAddImapAccount()" style="background:var(--green3);color:var(--bg);padding:5px 14px;border-radius:var(--r);font-weight:700;font-size:11px;cursor:pointer;border:none">+ Add Account</button>' +
|
|
3137
|
+
'</div>' +
|
|
3138
|
+
'<div style="font-size:11px;color:var(--dim);margin-bottom:10px">IMAP is read-only — no emails are ever deleted or moved on the server.</div>' +
|
|
3139
|
+
'<div id="imapAccountsList">Loading...</div>' +
|
|
3140
|
+
'<div id="imapAccountForm" style="display:none;margin-top:14px;padding:14px;background:var(--bg3);border-radius:8px;border:1px solid var(--border)">' +
|
|
3141
|
+
'<div style="font-size:12px;font-weight:700;color:var(--green);margin-bottom:10px" id="imapFormTitle">Add IMAP Account</div>' +
|
|
3142
|
+
'<input type="hidden" id="imapEditId" value="">' +
|
|
3143
|
+
imapField('imapDisplayName','Display Name','e.g. Work Email') +
|
|
3144
|
+
imapField('imapEmail','Email Address','user@example.com') +
|
|
3145
|
+
imapField('imapFromName','From Name','e.g. John Smith') +
|
|
3146
|
+
imapField('imapImapHost','IMAP Host','e.g. imap.gmail.com') +
|
|
3147
|
+
imapField('imapImapPort','IMAP Port','993') +
|
|
3148
|
+
imapField('imapSmtpHost','SMTP Host','e.g. smtp.gmail.com') +
|
|
3149
|
+
imapField('imapSmtpPort','SMTP Port','587') +
|
|
3150
|
+
imapField('imapUsername','Username','e.g. user@example.com') +
|
|
3151
|
+
imapField('imapPassword','Password','App password or IMAP password', true) +
|
|
3152
|
+
'<div style="display:flex;gap:8px;margin-top:12px">' +
|
|
3153
|
+
'<button onclick="saveImapAccount()" style="background:var(--green3);color:var(--bg);padding:7px 18px;border-radius:var(--r);font-weight:700;font-size:12px;cursor:pointer;border:none">Save</button>' +
|
|
3154
|
+
'<button onclick="document.getElementById(\\x27imapAccountForm\\x27).style.display=\\x27none\\x27" style="background:var(--bg);color:var(--dim);padding:7px 14px;border-radius:var(--r);font-size:12px;cursor:pointer;border:1px solid var(--border)">Cancel</button>' +
|
|
3155
|
+
'</div>' +
|
|
3156
|
+
'<div id="imapFormStatus" style="margin-top:8px;font-size:11px;color:var(--dim)"></div>' +
|
|
3157
|
+
'</div>' +
|
|
3158
|
+
'</div>';
|
|
3159
|
+
}
|
|
3160
|
+
|
|
3161
|
+
function imapField(id, label, placeholder, isPassword) {
|
|
3162
|
+
return '<div style="margin-bottom:8px">' +
|
|
3163
|
+
'<label style="display:block;font-size:10px;color:var(--dim);margin-bottom:3px">' + label + '</label>' +
|
|
3164
|
+
'<input id="' + id + '" type="' + (isPassword ? 'password' : 'text') + '" placeholder="' + placeholder + '" style="width:100%;padding:7px 10px;font-size:12px;background:var(--bg);color:var(--fg);border:1px solid var(--border2);border-radius:6px;box-sizing:border-box">' +
|
|
3165
|
+
'</div>';
|
|
3166
|
+
}
|
|
3167
|
+
|
|
3168
|
+
function loadImapAccounts() {
|
|
3169
|
+
apiGet('/api/imap/accounts').then(function(r) {
|
|
3170
|
+
var el = document.getElementById('imapAccountsList');
|
|
3171
|
+
if (!el) return;
|
|
3172
|
+
var accounts = r.accounts || [];
|
|
3173
|
+
if (!accounts.length) { el.innerHTML = '<div style="color:var(--dim);font-size:11px">No IMAP accounts configured yet.</div>'; return; }
|
|
3174
|
+
var h = '';
|
|
3175
|
+
for (var i = 0; i < accounts.length; i++) {
|
|
3176
|
+
var a = accounts[i];
|
|
3177
|
+
h += '<div style="display:flex;align-items:center;justify-content:space-between;padding:8px 10px;margin-bottom:6px;background:var(--bg3);border-radius:6px;border:1px solid var(--border)">' +
|
|
3178
|
+
'<div>' +
|
|
3179
|
+
'<div style="font-size:12px;font-weight:700;color:var(--bright)">' + esc(a.display_name) + '</div>' +
|
|
3180
|
+
'<div style="font-size:10px;color:var(--dim)">' + esc(a.email_address) + ' · ' + esc(a.imap_host || '') + '</div>' +
|
|
3181
|
+
'<div style="font-size:10px;color:' + (a.sync_status === 'idle' ? 'var(--green)' : a.sync_status === 'syncing' ? 'var(--cyan)' : 'var(--red)') + '">' + esc(a.sync_status) + (a.last_sync_at ? ' · last: ' + esc(a.last_sync_at.slice(0,16)) : '') + '</div>' +
|
|
3182
|
+
'</div>' +
|
|
3183
|
+
'<div style="display:flex;gap:6px">' +
|
|
3184
|
+
'<button onclick="imapSync(\\x27' + a.id + '\\x27)" style="padding:4px 10px;font-size:10px;background:var(--cyan3,#0e4f5e);color:var(--cyan);border:1px solid var(--cyan);border-radius:4px;cursor:pointer">Sync</button>' +
|
|
3185
|
+
'<button onclick="editImapAccount(\\x27' + a.id + '\\x27)" style="padding:4px 10px;font-size:10px;background:var(--bg);color:var(--dim);border:1px solid var(--border);border-radius:4px;cursor:pointer">Edit</button>' +
|
|
3186
|
+
'<button onclick="deleteImapAccount(\\x27' + a.id + '\\x27)" style="padding:4px 10px;font-size:10px;background:var(--red3,#7f1d1d);color:#fca5a5;border:1px solid var(--red,#ef4444);border-radius:4px;cursor:pointer">Delete</button>' +
|
|
3187
|
+
'</div></div>';
|
|
3188
|
+
}
|
|
3189
|
+
el.innerHTML = h;
|
|
3190
|
+
}).catch(function() {
|
|
3191
|
+
var el = document.getElementById('imapAccountsList');
|
|
3192
|
+
if (el) el.innerHTML = '<div style="color:var(--dim);font-size:11px">Could not load accounts.</div>';
|
|
3193
|
+
});
|
|
3194
|
+
}
|
|
3195
|
+
|
|
3196
|
+
function showAddImapAccount() {
|
|
3197
|
+
document.getElementById('imapEditId').value = '';
|
|
3198
|
+
document.getElementById('imapFormTitle').textContent = 'Add IMAP Account';
|
|
3199
|
+
var fields = ['DisplayName','Email','FromName','ImapHost','ImapPort','SmtpHost','SmtpPort','Username','Password'];
|
|
3200
|
+
var defaults = ['','','','993','587','','','',''];
|
|
3201
|
+
for (var i = 0; i < fields.length; i++) {
|
|
3202
|
+
var el = document.getElementById('imap' + fields[i]);
|
|
3203
|
+
if (el) el.value = defaults[i] || '';
|
|
3204
|
+
}
|
|
3205
|
+
document.getElementById('imapAccountForm').style.display = 'block';
|
|
3206
|
+
}
|
|
3207
|
+
|
|
3208
|
+
function editImapAccount(id) {
|
|
3209
|
+
apiGet('/api/imap/accounts').then(function(r) {
|
|
3210
|
+
var a = (r.accounts || []).find(function(x) { return x.id === id; });
|
|
3211
|
+
if (!a) return;
|
|
3212
|
+
document.getElementById('imapEditId').value = id;
|
|
3213
|
+
document.getElementById('imapFormTitle').textContent = 'Edit Account';
|
|
3214
|
+
document.getElementById('imapDisplayName').value = a.display_name || '';
|
|
3215
|
+
document.getElementById('imapEmail').value = a.email_address || '';
|
|
3216
|
+
document.getElementById('imapFromName').value = a.from_name || '';
|
|
3217
|
+
document.getElementById('imapImapHost').value = a.imap_host || '';
|
|
3218
|
+
document.getElementById('imapImapPort').value = a.imap_port || 993;
|
|
3219
|
+
document.getElementById('imapSmtpHost').value = a.smtp_host || '';
|
|
3220
|
+
document.getElementById('imapSmtpPort').value = a.smtp_port || 587;
|
|
3221
|
+
document.getElementById('imapUsername').value = a.username || '';
|
|
3222
|
+
document.getElementById('imapPassword').value = '';
|
|
3223
|
+
document.getElementById('imapAccountForm').style.display = 'block';
|
|
3224
|
+
});
|
|
3225
|
+
}
|
|
3226
|
+
|
|
3227
|
+
function saveImapAccount() {
|
|
3228
|
+
var id = document.getElementById('imapEditId').value;
|
|
3229
|
+
var data = {
|
|
3230
|
+
display_name: document.getElementById('imapDisplayName').value.trim(),
|
|
3231
|
+
email_address: document.getElementById('imapEmail').value.trim(),
|
|
3232
|
+
from_name: document.getElementById('imapFromName').value.trim(),
|
|
3233
|
+
imap_host: document.getElementById('imapImapHost').value.trim(),
|
|
3234
|
+
imap_port: parseInt(document.getElementById('imapImapPort').value || '993', 10),
|
|
3235
|
+
smtp_host: document.getElementById('imapSmtpHost').value.trim(),
|
|
3236
|
+
smtp_port: parseInt(document.getElementById('imapSmtpPort').value || '587', 10),
|
|
3237
|
+
username: document.getElementById('imapUsername').value.trim(),
|
|
3238
|
+
};
|
|
3239
|
+
var pwd = document.getElementById('imapPassword').value;
|
|
3240
|
+
if (pwd) data.password = pwd;
|
|
3241
|
+
var status = document.getElementById('imapFormStatus');
|
|
3242
|
+
if (status) { status.textContent = 'Saving...'; status.style.color = 'var(--dim)'; }
|
|
3243
|
+
var url2 = id ? '/api/imap/accounts/update' : '/api/imap/accounts';
|
|
3244
|
+
if (id) data.id = id;
|
|
3245
|
+
apiPost(url2, data).then(function(r) {
|
|
3246
|
+
if (r.ok || r.id) {
|
|
3247
|
+
if (status) { status.textContent = 'Saved.'; status.style.color = 'var(--green)'; }
|
|
3248
|
+
document.getElementById('imapAccountForm').style.display = 'none';
|
|
3249
|
+
loadImapAccounts();
|
|
3250
|
+
} else {
|
|
3251
|
+
if (status) { status.textContent = r.error || 'Error'; status.style.color = 'var(--red)'; }
|
|
3252
|
+
}
|
|
3253
|
+
}).catch(function(e) {
|
|
3254
|
+
if (status) { status.textContent = e.message; status.style.color = 'var(--red)'; }
|
|
3255
|
+
});
|
|
3256
|
+
}
|
|
3257
|
+
|
|
3258
|
+
function deleteImapAccount(id) {
|
|
3259
|
+
if (!confirm('Delete this email account? All synced emails will be removed locally.')) return;
|
|
3260
|
+
apiPost('/api/imap/accounts/delete', { id: id }).then(function() { loadImapAccounts(); });
|
|
3261
|
+
}
|
|
3262
|
+
|
|
3263
|
+
function imapSync(accountId) {
|
|
3264
|
+
apiPost('/api/imap/sync', { accountId: accountId }).then(function() {
|
|
3265
|
+
showToast('success', 'Sync', 'Sync started in background');
|
|
3266
|
+
setTimeout(loadImapAccounts, 3000);
|
|
3267
|
+
}).catch(function(e) { showToast('error', 'Sync', e.message); });
|
|
2650
3268
|
}
|
|
2651
3269
|
|
|
2652
3270
|
function connectGoogle() {
|