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.
@@ -1098,101 +1098,579 @@ function refreshPlan(){
1098
1098
  }
1099
1099
 
1100
1100
  // ---- EMAILS ----
1101
- function renderEmails(el){
1102
- if(!dashLoaded.emails){el.innerHTML=loadingHTML('emails');return}
1103
- var e=dash.emails;
1104
- if(e.length===0){el.innerHTML='<div class="card" style="text-align:center;color:var(--dim);padding:30px">Inbox zero - no emails</div>';return}
1105
- var unreadCount=e.filter(function(x){return x.isUnread}).length;
1106
- var h='<div style="display:flex;gap:8px;margin-bottom:10px;align-items:center">';
1107
- h+='<span style="font-size:12px;color:var(--dim)">'+e.length+' emails'+(unreadCount>0?' ('+unreadCount+' unread)':'')+'</span>';
1108
- if(unreadCount>0)h+='<button class="btn btn--secondary" style="font-size:10px;padding:4px 10px" onclick="markAllEmailsRead()">Mark all read</button>';
1109
- h+='</div>';
1110
- e.forEach(function(x){
1111
- var unreadStyle=x.isUnread?'border-left:3px solid var(--green);font-weight:700':'border-left:3px solid transparent;opacity:0.7';
1112
- h+='<div class="card email" style="cursor:pointer;'+unreadStyle+'" onclick="openEmail(\\x27'+esc(x.id)+'\\x27)"><div class="email__header"><span class="email__from">'+esc(x.from)+'</span><span class="email__date">'+esc(x.date)+(x.isUnread?' <span style="color:var(--green);font-size:9px">NEW</span>':'')+'</span></div><div class="email__subject">'+esc(x.subject)+'</div><div class="email__snippet" style="font-weight:400">'+esc((x.snippet||'').slice(0,150))+'</div></div>';
1113
- });
1114
- // Load More button
1115
- if(dash._emailHasMore!==false){
1116
- h+='<button id="loadMoreEmails" onclick="loadMoreEmails()" style="width:100%;padding:12px;margin-top:8px;background:var(--bg3);border:1px solid var(--border);border-radius:var(--r);color:var(--cyan);font-family:var(--font);font-size:12px;cursor:pointer;font-weight:700">Load More Emails</button>';
1117
- }
1118
- el.innerHTML=h;
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
- var emailPage=0;
1121
- function loadMoreEmails(){
1122
- var btn=document.getElementById('loadMoreEmails');
1123
- if(btn){btn.textContent='Loading...';btn.disabled=true;}
1124
- emailPage++;
1125
- apiGet('/api/emails?page='+emailPage+'&pageSize=25').then(function(r){
1126
- if(r&&r.emails){
1127
- for(var i=0;i<r.emails.length;i++){
1128
- if(!dash.emails.find(function(e){return e.id===r.emails[i].id})){
1129
- dash.emails.push(r.emails[i]);
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
- dash._emailHasMore=r.hasMore;
1133
- updateBadges();
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
- function markAllEmailsRead(){
1139
- apiPost('/api/email/mark-all-read',{}).then(function(r){
1140
- if(r&&r.ok){
1141
- dash.emails.forEach(function(e){e.isUnread=false});
1142
- updateBadges();
1143
- renderEmails(document.getElementById('content'));
1144
- showToast('success','All Read','Marked '+( r.count||0)+' emails as read');
1145
- }else{
1146
- showToast('error','Error',r&&r.error||'Failed');
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:'&#9993;'},{id:'SENT',name:'Sent',icon:'&#8594;'},{id:'DRAFTS',name:'Drafts',icon:'&#128196;'},{id:'SPAM',name:'Spam',icon:'&#128737;'},{id:'TRASH',name:'Trash',icon:'&#128465;'}];
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">&#9998;</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
- var openEmailId=null;
1151
- function openEmail(id){
1152
- openEmailId=id;
1153
- // Mark as read locally + on server
1154
- var emailObj=dash.emails.find(function(e){return e.id===id});
1155
- if(emailObj&&emailObj.isUnread){
1156
- emailObj.isUnread=false;
1157
- updateBadges();
1158
- apiPost('/api/email/mark-read',{messageId:id}).catch(function(){});
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 el=document.getElementById('content');
1161
- el.innerHTML='<div style="text-align:center;padding:40px"><div class="spinner"></div><div style="color:var(--dim)">Loading email...</div></div>';
1162
- apiPost('/api/email/read',{messageId:id}).then(function(r){
1163
- if(!r||r.error){el.innerHTML='<div class="card" style="color:var(--red);padding:20px">Error: '+(r&&r.error||'Failed to load email')+'</div>';return}
1164
- var m=r.message||r;
1165
- var h='<div style="margin-bottom:12px"><button class="btn btn--secondary" onclick="switchView(\\x27emails\\x27)" style="font-size:11px">&larr; Back to inbox</button></div>';
1166
- h+='<div class="card" style="padding:20px">';
1167
- h+='<div style="margin-bottom:12px;padding-bottom:12px;border-bottom:1px solid var(--border)">';
1168
- h+='<div style="font-size:16px;font-weight:700;color:var(--bright);margin-bottom:8px">'+esc(m.subject||'(no subject)')+'</div>';
1169
- h+='<div style="font-size:12px;color:var(--dim);margin-bottom:4px"><strong style="color:var(--text)">From:</strong> '+esc(m.from||'')+'</div>';
1170
- h+='<div style="font-size:12px;color:var(--dim);margin-bottom:4px"><strong style="color:var(--text)">To:</strong> '+esc(m.to||'')+'</div>';
1171
- h+='<div style="font-size:12px;color:var(--dim)"><strong style="color:var(--text)">Date:</strong> '+esc(m.date||'')+'</div>';
1172
- h+='</div>';
1173
- h+='<div style="font-size:13px;line-height:1.7;color:var(--text);white-space:pre-wrap;word-wrap:break-word">'+esc(m.body||m.snippet||'(no content)')+'</div>';
1174
- h+='</div>';
1175
- // Action buttons
1176
- h+='<div style="display:flex;gap:8px;margin-top:12px">';
1177
- h+='<button class="btn btn--primary" onclick="replyToEmail(\\x27'+esc(id)+'\\x27)" style="font-size:12px">Reply</button>';
1178
- h+='<button class="btn btn--secondary" onclick="askAgentAboutEmail(\\x27'+esc(id)+'\\x27)" style="font-size:12px">Ask SABER to scan</button>';
1179
- h+='</div>';
1180
- el.innerHTML=h;
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">&#128465;</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 ? '&#9733;' : '&#9734;') + '</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">&#128206; ' + 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, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;') + '"></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
- function replyToEmail(id){
1184
- switchView('chat');
1185
- setTimeout(function(){
1186
- var inp=document.getElementById('chatInput');
1187
- if(inp){inp.value='Reply to email '+id+': ';inp.focus()}
1188
- },200);
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
- function askAgentAboutEmail(id){
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='Scan email '+id+' for phishing or security threats';inp.focus()}
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:600px;margin:0 auto">' +
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) + ' &middot; ' + 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 ? ' &middot; 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() {