Skip to content

Instantly share code, notes, and snippets.

@monperrus
Last active August 25, 2026 17:48
Show Gist options
  • Select an option

  • Save monperrus/31b116c72bbc0deec28a674db99055d7 to your computer and use it in GitHub Desktop.

Select an option

Save monperrus/31b116c72bbc0deec28a674db99055d7 to your computer and use it in GitHub Desktop.
Martin's Super Webapp tab: Email
{
"1": {
"tab": {
"id": "jmap-email",
"label": "Email",
"html": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"UTF-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n<title>JMAP Email</title>\n<style>\n html, body { margin: 0; height: 100%; background: #0d1117; color: #c9d1d9;\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; }\n * { box-sizing: border-box; }\n .app { display: flex; flex-direction: column; height: 100%; max-width: 600px; margin: 0 auto; }\n .header { padding: 12px 16px; border-bottom: 1px solid #30363d; background: #161b22;\n display: flex; align-items: center; gap: 8px; min-height: 52px; }\n .header h1 { margin: 0; font-size: 18px; font-weight: 600; flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;\n cursor: pointer; -webkit-user-select: none; user-select: none; }\n .header h1:active { opacity: .7; }\n .header .back-btn { background: none; border: none; color: #58a6ff; font-size: 20px;\n cursor: pointer; padding: 8px; min-width: 44px; min-height: 44px; display: flex; align-items: center; justify-content: center; }\n .header .header-btn { background: none; border: 1px solid #30363d; \n padding: 6px 12px; border-radius: 6px; cursor: pointer; font-size: 14px; min-height: 36px;\n display: flex; align-items: center; gap: 4px; white-space: nowrap; }\n .header .refresh-btn, .header .compose-btn { color: #58a6ff; }\n .header .logout-btn { color: #f85149; }\n .header .send-header-btn { background: #238636; color: #fff; border: none; \n padding: 6px 14px; border-radius: 6px; cursor: pointer; font-size: 14px; font-weight: 600; min-height: 36px;\n display: flex; align-items: center; gap: 4px; white-space: nowrap; }\n .header .send-header-btn:disabled { opacity: .5; cursor: not-allowed; }\n .content { flex: 1; overflow-y: auto; padding: 16px; }\n .content::-webkit-scrollbar { width: 6px; }\n .content::-webkit-scrollbar-thumb { background: #30363d; border-radius: 3px; }\n .content.no-pad { padding: 0; }\n .login-form { max-width: 400px; margin: 60px auto 0; padding: 24px; background: #161b22;\n border: 1px solid #30363d; border-radius: 12px; }\n .login-form h2 { margin: 0 0 20px; font-size: 20px; text-align: center; }\n .login-form label { display: block; margin-bottom: 6px; color: #8b949e; font-size: 14px; }\n .login-form input { width: 100%; padding: 12px 14px; margin-bottom: 16px; min-height: 44px;\n background: #0d1117; border: 1px solid #30363d; border-radius: 8px; color: #c9d1d9;\n font-size: 15px; outline: none; }\n .login-form input:focus { border-color: #58a6ff; }\n .login-form .btn-primary { width: 100%; padding: 12px; min-height: 44px; font-size: 15px;\n background: #238636; color: #fff; border: none; border-radius: 8px; cursor: pointer; font-weight: 600; }\n .login-form .btn-primary:disabled { opacity: .5; cursor: not-allowed; }\n .login-form .error { color: #f85149; font-size: 13px; padding: 8px; background: rgba(248,81,73,0.1); border-radius: 6px; display: none; }\n .mailbox-item { display: flex; align-items: center; padding: 14px 16px; min-height: 52px;\n border-bottom: 1px solid #21262d; cursor: pointer; gap: 12px; }\n .mailbox-item:hover { background: #161b22; }\n .mailbox-icon { font-size: 20px; width: 36px; text-align: center; }\n .mailbox-info { flex: 1; }\n .mailbox-name { font-size: 15px; font-weight: 500; }\n .mailbox-stats { font-size: 12px; color: #8b949e; margin-top: 2px; }\n .mailbox-unread { background: #238636; color: #fff; border-radius: 12px; padding: 2px 10px; font-size: 12px; font-weight: 600; }\n .email-list { position: relative; }\n .email-item { padding: 14px 16px; min-height: 60px; border-bottom: 1px solid #21262d; cursor: pointer;\n position: relative; background: #0d1117; transition: transform .2s ease; }\n .email-item:hover { background: #161b22; }\n .email-item.unread { border-left: 3px solid #58a6ff; }\n .email-item.swiping { transition: none; }\n .email-item .delete-bg { position: absolute; top: 0; right: 0; bottom: 0; background: #f85149;\n display: flex; align-items: center; padding: 0 20px; font-size: 14px; font-weight: 600; color: #fff;\n border-radius: 0 0 0 0; z-index: 0; }\n .email-item .email-content { position: relative; z-index: 1; background: #0d1117;\n transition: transform .2s ease; padding: 0; }\n .email-item .email-content.swiping { transition: none; }\n .email-sender { font-size: 14px; font-weight: 600; color: #e6edf3; margin-bottom: 4px; }\n .email-subject { font-size: 14px; color: #c9d1d9; margin-bottom: 4px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }\n .email-preview { font-size: 13px; color: #8b949e; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }\n .email-date { font-size: 11px; color: #8b949e; margin-top: 4px; }\n .email-item.unread .email-subject { font-weight: 600; color: #e6edf3; }\n .email-detail { position: relative; }\n .email-detail.swiping { transition: none; }\n .email-detail-header { padding: 16px; border-bottom: 1px solid #30363d; }\n .email-detail-subject { font-size: 18px; font-weight: 600; margin-bottom: 12px; }\n .email-detail-from { font-size: 14px; color: #8b949e; margin-bottom: 4px; }\n .email-detail-date { font-size: 13px; color: #8b949e; }\n .email-detail-body { padding: 16px; line-height: 1.6; font-size: 14px; word-wrap: break-word; }\n .email-detail-actions { padding: 12px 16px; border-top: 1px solid #30363d; display: flex; gap: 8px; }\n .email-detail-actions .action-btn { flex: 1; padding: 10px; min-height: 44px; border: 1px solid #30363d;\n border-radius: 8px; background: none; color: #c9d1d9; font-size: 14px; cursor: pointer; }\n .email-detail-actions .action-btn:active { background: #161b22; }\n .email-detail-actions .reply-btn { color: #58a6ff; }\n .compose-form { display: flex; flex-direction: column; height: 100%; }\n .compose-fields { padding: 16px; display: flex; flex-direction: column; height: 100%; }\n .compose-fields label { display: block; margin-bottom: 4px; color: #8b949e; font-size: 13px; font-weight: 500; }\n .compose-fields input, .compose-fields textarea { padding: 10px 12px; min-height: 44px;\n background: #0d1117; border: 1px solid #30363d; border-radius: 8px; color: #c9d1d9;\n font-size: 15px; outline: none; font-family: inherit; }\n .compose-row { display: flex; align-items: center; gap: 8px; margin-bottom: 10px; }\n .compose-row label { flex-shrink: 0; width: auto; min-width: 52px; margin-bottom: 0; }\n .compose-row input { flex: 1; min-width: 0; width: auto; }\n .compose-fields textarea { min-height: 200px; resize: vertical; }\n .compose-fields .send-btn { width: 100%; padding: 12px; min-height: 44px; font-size: 15px;\n background: #238636; color: #fff; border: none; border-radius: 8px; cursor: pointer; font-weight: 600; }\n .compose-fields .send-btn:disabled { opacity: .5; cursor: not-allowed; }\n .compose-fields .compose-error { color: #f85149; font-size: 13px; padding: 8px;\n background: rgba(248,81,73,0.1); border-radius: 6px; display: none; margin-bottom: 12px; }\n .compose-fields.compose-expanded { padding: 0; }\n .compose-fields.compose-expanded .compose-hdrs, .compose-fields.compose-expanded .compose-from-bar { display: none; }\n .compose-fields.compose-expanded .compose-error { margin: 8px 8px 0; }\n .compose-fields.compose-expanded #composeBody { min-height: 0; flex: 1; margin-bottom: 0; border-radius: 0; \n border-left: none; border-right: none; border-top: 1px solid #30363d; resize: none; padding: 16px; font-size: 16px; }\n .compose-bar { display: flex; gap: 8px; padding: 10px 12px; border-top: 1px solid #30363d; background: #161b22; }\n .compose-bar .send-btn { flex: 1; padding: 12px; min-height: 48px; font-size: 15px;\n background: #238636; color: #fff; border: none; border-radius: 8px; cursor: pointer; font-weight: 600; }\n .compose-bar .send-btn:disabled { opacity: .5; cursor: not-allowed; }\n .compose-bar .reduce-btn { min-width: 80px; padding: 12px; min-height: 48px; font-size: 14px;\n background: none; color: #8b949e; border: 1px solid #30363d; border-radius: 8px; cursor: pointer; }\n .loading { text-align: center; padding: 40px 16px; color: #8b949e; }\n .loading .spinner { display: inline-block; width: 24px; height: 24px; border: 3px solid #30363d;\n border-top-color: #58a6ff; border-radius: 50%; animation: spin .8s linear infinite; margin-bottom: 12px; }\n @keyframes spin { to { transform: rotate(360deg); } }\n .empty-state { text-align: center; padding: 60px 20px; color: #8b949e; }\n .empty-state .icon { font-size: 48px; margin-bottom: 16px; }\n .empty-state h3 { font-size: 18px; margin: 0 0 8px; color: #c9d1d9; }\n .status-bar { padding: 8px 16px; background: #161b22; border-top: 1px solid #30363d;\n font-size: 12px; color: #8b949e; display: flex; justify-content: space-between; min-height: 36px; }\n .status-bar.hidden { display: none; }\n .toast { position: fixed; bottom: 80px; left: 50%; transform: translateX(-50%); \n background: #161b22; border: 1px solid #30363d; padding: 10px 20px; border-radius: 8px;\n font-size: 14px; color: #c9d1d9; z-index: 100; display: none; max-width: 90%; text-align: center; }\n .toast.error { border-color: #f85149; background: rgba(248,81,73,0.15); }\n .toast.success { border-color: #238636; background: rgba(35,134,54,0.15); }\n .ptr-indicator { position: absolute; top: 0; left: 0; width: 100%; \n color: #8b949e; font-size: 14px; text-align: center; padding: 12px 0; \n pointer-events: none; z-index: 5; display: none; }\n .ptr-indicator.visible { display: block; }\n .ptr-indicator .hint { color: #58a6ff; font-size: 12px; }\n @media (max-width: 480px) {\n .login-form { margin: 30px 8px 0; padding: 20px 16px; }\n .content { padding: 0; }\n .header { padding: 10px 12px; gap: 6px; }\n .header .header-btn { padding: 6px 8px; font-size: 13px; }\n .compose-fields { padding: 12px; }\n .compose-fields.compose-expanded { padding: 0; }\n }\n</style>\n</head>\n<body>\n<div class=\"app\" id=\"app\">\n <div class=\"header\" id=\"header\">\n <button class=\"back-btn\" id=\"backBtn\" style=\"display:none\">←</button>\n <h1 id=\"title\">πŸ“§ Email</h1>\n <button class=\"header-btn compose-btn\" id=\"composeBtn\" style=\"display:none\">✏️</button>\n <button class=\"header-btn refresh-btn\" id=\"refreshBtn\" style=\"display:none\">πŸ”„</button>\n <button class=\"send-header-btn\" id=\"sendHeaderBtn\" style=\"display:none\">πŸ“€ Send</button>\n <button class=\"header-btn logout-btn\" id=\"logoutBtn\" style=\"display:none\">Logout</button>\n </div>\n <div class=\"content\" id=\"content\"><div class=\"ptr-indicator\" id=\"ptrIndicator\"><span id=\"ptrText\">⬇️ Hold & pull down to refresh</span></div></div>\n <div class=\"status-bar\" id=\"statusBar\"><span id=\"statusLeft\">Not connected</span><span id=\"statusRight\">jmap.server</span></div>\n</div>\n<div class=\"toast\" id=\"toast\"></div>\n<script>\nconst STORAGE_KEY='msw_tab_jmap_email';\nlet state={serverUrl:'https://jmap.gakoy.com',credentials:null,session:null,apiUrl:null,downloadUrlTpl:null,accountId:null,submissionAccountId:null,identities:[],mailboxes:[],mailboxMap:{},sendMailboxId:null,trashMailboxId:null,currentView:'login',currentMailboxId:null,currentMailboxName:'',emails:[],currentEmail:null,composeData:{to:'',cc:'',subject:'',body:'',replyToEmailId:null},composeExpanded:false,loading:false};\nconst $=id=>document.getElementById(id),content=$('content'),title=$('title'),backBtn=$('backBtn'),composeBtn=$('composeBtn'),refreshBtn=$('refreshBtn'),logoutBtn=$('logoutBtn'),sendHeaderBtn=$('sendHeaderBtn'),statusBar=$('statusBar'),statusLeft=$('statusLeft'),statusRight=$('statusRight'),toast=$('toast');\nlet toastTimer=null;\nfunction showToast(msg,type=''){toast.textContent=msg;toast.className='toast '+type;toast.style.display='block';clearTimeout(toastTimer);toastTimer=setTimeout(()=>{toast.style.display='none'},4000)}\nfunction saveState(){try{localStorage.setItem(STORAGE_KEY,JSON.stringify({serverUrl:state.serverUrl,credentials:state.credentials,session:state.session,apiUrl:state.apiUrl,accountId:state.accountId,submissionAccountId:state.submissionAccountId}))}catch(e){}}\nfunction loadState(){try{const r=localStorage.getItem(STORAGE_KEY);if(r){const s=JSON.parse(r);if(s.serverUrl)state.serverUrl=s.serverUrl;if(s.credentials)state.credentials=s.credentials;if(s.session)state.session=s.session;if(s.apiUrl)state.apiUrl=s.apiUrl;if(s.accountId)state.accountId=s.accountId;if(s.submissionAccountId)state.submissionAccountId=s.submissionAccountId}}catch(e){}}\nasync function jmapFetch(path,options={}){const url=path.startsWith('http')?path:state.serverUrl+path;const headers={'Content-Type':'application/json','Accept':'application/json',...options.headers};if(state.credentials){if(state.credentials.token)headers['Authorization']='Bearer '+state.credentials.token;else if(state.credentials.username&&state.credentials.password)headers['Authorization']='Basic '+btoa(state.credentials.username+':'+state.credentials.password)}const res=await fetch(url,{method:options.method||'GET',headers,body:options.body?JSON.stringify(options.body):undefined});if(!res.ok){let detail='';try{const e=await res.json();detail=e.detail||e.type||JSON.stringify(e)}catch(e){detail=res.statusText}throw new Error('HTTP '+res.status+': '+detail)}const text=await res.text();return text?JSON.parse(text):null}\nasync function jmapApiCall(method,params={}){if(!state.apiUrl)throw new Error('No API URL');const result=await jmapFetch(state.apiUrl,{method:'POST',body:{using:['urn:ietf:params:jmap:core','urn:ietf:params:jmap:mail','urn:ietf:params:jmap:submission'],methodCalls:[[method,params,'0']]}});return result}\nasync function jmapMultiCall(methodCalls){if(!state.apiUrl)throw new Error('No API URL');const result=await jmapFetch(state.apiUrl,{method:'POST',body:{using:['urn:ietf:params:jmap:core','urn:ietf:params:jmap:mail','urn:ietf:params:jmap:submission'],methodCalls}});return result}\nasync function loadIdentities(){try{const sub=state.submissionAccountId||state.accountId;const r=await jmapApiCall('Identity/get',{accountId:sub});state.identities=r.methodResponses[0][1].list||[]}catch(e){state.identities=[]}}\n\n// ---- Custom long-press pull-to-refresh ----\nlet ptrTouchStartY=0, ptrLongPress=false, ptrPulling=false, ptrTimer=null, ptrDy=0;\nconst ptrEl=$('ptrIndicator'), ptrText=$('ptrText');\nconst LONG_PRESS_MS=400, PTR_THRESHOLD=60;\nfunction setupPtr(){\n content.addEventListener('touchstart',onPtrTouchStart,{passive:true});\n content.addEventListener('touchmove',onPtrTouchMove,{passive:true});\n content.addEventListener('touchend',onPtrTouchEnd,{passive:true});\n content.addEventListener('touchcancel',onPtrTouchEnd,{passive:true});\n}\nfunction onPtrTouchStart(e){\n if(state.currentView!=='mailboxes'&&state.currentView!=='emails')return;\n if(content.scrollTop>0)return;\n ptrTouchStartY=e.changedTouches[0].screenY;\n ptrLongPress=false; ptrPulling=false; ptrDy=0;\n ptrEl.classList.remove('visible');\n clearTimeout(ptrTimer);\n ptrTimer=setTimeout(()=>{ptrLongPress=true;ptrText.textContent='⬇️ Now pull down to refresh';ptrEl.classList.add('visible');},LONG_PRESS_MS);\n}\nfunction onPtrTouchMove(e){\n if(state.currentView!=='mailboxes'&&state.currentView!=='emails')return;\n const dy=e.changedTouches[0].screenY-ptrTouchStartY;\n if(dy<0){clearTimeout(ptrTimer);ptrLongPress=false;ptrEl.classList.remove('visible');return;}\n if(!ptrLongPress){if(dy>15)clearTimeout(ptrTimer);return;}\n ptrDy=Math.min(dy,120);\n ptrPulling=ptrDy>PTR_THRESHOLD;\n ptrText.textContent=ptrPulling?'↻ Release to refresh':'⬇️ Pull further...';\n ptrEl.style.transform='translateY('+(ptrDy-30)+'px)';\n}\nfunction onPtrTouchEnd(){\n clearTimeout(ptrTimer);\n if(ptrPulling&&ptrLongPress){ptrText.textContent='πŸ”„ Refreshing...';doPtrRefresh();}\n else{ptrEl.classList.remove('visible');ptrEl.style.transform='';}\n ptrLongPress=false;ptrPulling=false;ptrDy=0;\n}\nasync function doPtrRefresh(){\n try{if(state.currentView==='mailboxes'){await loadMailboxes();}else if(state.currentView==='emails'){await loadEmails();}}catch(e){}\n ptrEl.classList.remove('visible');ptrEl.style.transform='';showToast('βœ… Refreshed','success');\n}\n// ---- End custom pull-to-refresh ----\n\nfunction render(){content.className='content'+(state.currentView==='compose'?' no-pad':'');switch(state.currentView){case'login':renderLogin();break;case'mailboxes':renderMailboxes();break;case'emails':renderEmails();break;case'email-detail':renderEmailDetail();break;case'compose':renderCompose();break}updateHeader();updateStatusBar();}\nfunction goToFolders(){if(state.currentView==='emails'||state.currentView==='email-detail'){state.currentView='mailboxes';render()}}\nfunction goBackToList(){if(state.currentView==='email-detail'){state.currentView='emails';render()}}\nfunction updateHeader(){const logged=state.currentView!=='login';backBtn.style.display=state.currentView==='mailboxes'||state.currentView==='compose'?'none':'flex';composeBtn.style.display=logged&&state.currentView!=='compose'?'flex':'none';refreshBtn.style.display=logged&&state.currentView!=='compose'?'flex':'none';logoutBtn.style.display=logged&&state.currentView!=='compose'?'flex':'none';sendHeaderBtn.style.display=state.currentView==='compose'?'flex':'none';switch(state.currentView){case'login':title.textContent='πŸ“§ Email';break;case'mailboxes':title.textContent='πŸ“‚ Folders';break;case'emails':title.textContent='πŸ“¨ Messages';break;case'email-detail':title.textContent='πŸ’¬ Message';break;case'compose':const t=state.composeData.to;title.textContent=t?'βœ‰οΈ '+t.split(',')[0].trim():'✏️ New Message';break}}\nfunction updateStatusBar(){if(state.currentView==='compose'){statusBar.classList.add('hidden')}else{statusBar.classList.remove('hidden');if(state.credentials&&state.session){statusLeft.textContent=state.credentials.username||'Logged in';statusRight.textContent=new Date().toLocaleTimeString([],{hour:'2-digit',minute:'2-digit'})}else{statusLeft.textContent='Not connected';statusRight.textContent=state.serverUrl.replace('https://','')}}}\nasync function handleRefresh(){if(state.loading)return;showToast('πŸ”„ Refreshing...','');switch(state.currentView){case'mailboxes':await loadMailboxes();break;case'emails':await loadEmails();break;case'email-detail':if(state.currentEmail)await loadEmailDetail(state.currentEmail.id);break}}\nfunction renderLogin(){content.innerHTML='<div class=\"login-form\"><h2>πŸ“§ JMAP Email</h2><label for=\"serverUrl\">Server</label><input type=\"text\" id=\"serverUrl\" placeholder=\"https://jmap.example.com\" value=\"'+(state.serverUrl||'https://jmap.gakoy.com')+'\"><label for=\"username\">Username</label><input type=\"text\" id=\"username\" placeholder=\"your@email.com\" autocomplete=\"username\" value=\"'+(state.credentials?.username||'')+'\"><label for=\"password\">Password</label><input type=\"password\" id=\"password\" placeholder=\"β€’β€’β€’β€’β€’β€’β€’β€’\" autocomplete=\"current-password\"><button class=\"btn-primary\" id=\"loginBtn\">Sign In</button><div class=\"error\" id=\"loginError\"></div></div>';$('loginBtn').addEventListener('click',handleLogin);$('serverUrl').addEventListener('keydown',e=>{if(e.key==='Enter')$('username').focus()});$('username').addEventListener('keydown',e=>{if(e.key==='Enter')$('password').focus()});$('password').addEventListener('keydown',e=>{if(e.key==='Enter')handleLogin()});if(state.credentials?.username)$('password').focus();else $('serverUrl').focus();}\nasync function handleLogin(){const serverUrl=$('serverUrl').value.trim()||'https://jmap.gakoy.com',username=$('username').value.trim(),password=$('password').value,errorEl=$('loginError'),btn=$('loginBtn');if(!serverUrl||!username||!password){errorEl.textContent='Please enter server, username and password.';errorEl.style.display='block';return}errorEl.style.display='none';btn.disabled=true;btn.textContent='Signing in...';try{state.serverUrl=serverUrl;state.credentials={username,password};const session=await jmapFetch('/.well-known/jmap');state.session=session;state.apiUrl=session.apiUrl;state.downloadUrlTpl=session.downloadUrl;state.accountId=session.primaryAccounts['urn:ietf:params:jmap:mail'];state.submissionAccountId=session.primaryAccounts['urn:ietf:params:jmap:submission'];saveState();state.currentView='mailboxes';showToast('Signed in successfully!','success');await loadIdentities();await loadMailboxes()}catch(e){state.credentials=null;errorEl.textContent='Login failed: '+e.message;errorEl.style.display='block';btn.disabled=false;btn.textContent='Sign In'}}\nasync function loadMailboxes(){state.loading=true;renderMailboxes();try{const r=await jmapApiCall('Mailbox/get',{accountId:state.accountId});state.mailboxes=r.methodResponses[0][1].list||[];state.mailboxMap={};let o=null,s=null,d=null,tr=null,f=null;for(const mb of state.mailboxes){state.mailboxMap[mb.id]=mb.name;const n=(mb.name||'').toLowerCase(),ro=(mb.role||'').toLowerCase();if(n==='outbox'||ro==='outbox')o=mb.id;if(n==='sent'||ro==='sent')s=mb.id;if(n==='drafts'||ro==='drafts')d=mb.id;if(n==='trash'||n==='deleted messages'||n==='bin'||ro==='trash')tr=mb.id;if(!f)f=mb.id}state.sendMailboxId=o||s||d||f;state.trashMailboxId=tr;state.loading=false;renderMailboxes()}catch(e){state.loading=false;showToast('Failed to load mailboxes: '+e.message,'error');renderMailboxes()}}\nfunction renderMailboxes(){if(state.loading){content.innerHTML='<div class=\"loading\"><div class=\"spinner\"></div><p>Loading mailboxes...</p></div>';return}if(state.mailboxes.length===0){content.innerHTML='<div class=\"empty-state\"><div class=\"icon\">πŸ“­</div><h3>No mailboxes found</h3><button class=\"btn-primary\" style=\"margin-top:16px;padding:10px 24px;background:#238636;color:#fff;border:none;border-radius:8px;cursor:pointer;font-size:14px;min-height:44px\" id=\"refreshBtnEmpty\">πŸ”„ Refresh</button></div>';$('refreshBtnEmpty')?.addEventListener('click',loadMailboxes);return}const icons={'inbox':'πŸ“₯','sent':'πŸ“€','drafts':'πŸ“','trash':'πŸ—‘οΈ','spam':'⚠️','archive':'πŸ“¦','junk':'⚠️','outbox':'πŸ“€'};const sorted=[...state.mailboxes].sort((a,b)=>(a.sortOrder||0)-(b.sortOrder||0));let html='<div class=\"ptr-indicator\" id=\"ptrIndicator\"><span id=\"ptrText\">⬇️ Hold & pull down to refresh</span></div>';for(const mb of sorted){const name=mb.name||'Unnamed',total=mb.totalEmails||mb.totalMessages||0,unread=mb.unreadEmails||mb.unreadMessages||0,icon=Object.entries(icons).find(([k])=>name.toLowerCase().includes(k))?.[1]||'πŸ“';html+='<div class=\"mailbox-item\" data-id=\"'+mb.id+'\"><div class=\"mailbox-icon\">'+icon+'</div><div class=\"mailbox-info\"><div class=\"mailbox-name\">'+name+'</div><div class=\"mailbox-stats\">'+total+' messages</div></div>'+(unread>0?'<div class=\"mailbox-unread\">'+unread+'</div>':'')+'</div>'}content.innerHTML=html;content.querySelectorAll('.mailbox-item').forEach(el=>{el.addEventListener('click',()=>{const id=el.dataset.id,mb=state.mailboxes.find(m=>m.id===id);if(mb){state.currentMailboxId=id;state.currentMailboxName=mb.name;state.currentView='emails';render();loadEmails()}})})}\nasync function loadEmails(){state.loading=true;renderEmails();try{const q=await jmapApiCall('Email/query',{accountId:state.accountId,filter:{inMailbox:state.currentMailboxId},sort:[{property:'receivedAt',isAscending:false}],limit:50});const ids=q.methodResponses[0][1].ids;if(!ids||ids.length===0){state.emails=[];state.loading=false;renderEmails();return}const g=await jmapApiCall('Email/get',{accountId:state.accountId,ids,properties:['id','subject','from','to','receivedAt','hasAttachment','preview','size','keywords']});state.emails=g.methodResponses[0][1].list||[];state.loading=false;renderEmails()}catch(e){state.loading=false;showToast('Failed to load emails: '+e.message,'error');renderEmails()}}\nfunction renderEmails(){updateHeader();if(state.loading){content.innerHTML='<div class=\"loading\"><div class=\"spinner\"></div><p>Loading emails...</p></div>';return}if(state.emails.length===0){content.innerHTML='<div class=\"empty-state\"><div class=\"icon\">πŸ“¨</div><h3>No emails</h3><p>This mailbox is empty.</p></div>';return}let html='<div class=\"ptr-indicator\" id=\"ptrIndicator\"><span id=\"ptrText\">⬇️ Hold & pull down to refresh</span></div><div class=\"email-list\">';for(const e of state.emails){const from=e.from?.[0]?.name||e.from?.[0]?.email||'Unknown',unread=!e.keywords?.$seen,date=new Date(e.receivedAt),ds=date.toLocaleDateString([],{month:'short',day:'numeric'}),ts=date.toLocaleTimeString([],{hour:'2-digit',minute:'2-digit'}),today=new Date().toDateString()===date.toDateString();html+='<div class=\"email-item'+(unread?' unread':'')+'\" data-id=\"'+e.id+'\"><div class=\"delete-bg\">πŸ—‘οΈ Delete</div><div class=\"email-content\"><div class=\"email-sender\">'+escapeHtml(from)+'</div><div class=\"email-subject\">'+escapeHtml(e.subject||'(No subject)')+'</div><div class=\"email-preview\">'+escapeHtml((e.preview||'').substring(0,100))+'</div><div class=\"email-date\">'+(today?ts:ds)+'</div></div></div>'}html+='</div>';content.innerHTML=html;content.querySelectorAll('.email-item').forEach(el=>{const contentDiv=el.querySelector('.email-content');let startX=0,currentX=0,isDragging=false,swiped=false;const deleteBg=el.querySelector('.delete-bg');deleteBg.style.opacity='0';const onStart=function(x){startX=x;currentX=0;isDragging=true;swiped=false;contentDiv.classList.add('swiping');el.classList.add('swiping')};const onMove=function(x){if(!isDragging)return;const dx=x-startX;if(dx<0){currentX=0;contentDiv.style.transform='translateX(0px)';return}currentX=Math.min(dx,120);contentDiv.style.transform='translateX('+currentX+'px)';deleteBg.style.opacity=Math.min(currentX/80,1)};const onEnd=function(){isDragging=false;contentDiv.classList.remove('swiping');el.classList.remove('swiping');if(currentX>60){swiped=true;deleteEmail(el.dataset.id,el)}else{contentDiv.style.transform='translateX(0px)';deleteBg.style.opacity='0'}};el.addEventListener('touchstart',e=>{const t=e.changedTouches[0];onStart(t.screenX)},{passive:true});el.addEventListener('touchmove',e=>{const t=e.changedTouches[0];onMove(t.screenX)},{passive:true});el.addEventListener('touchend',e=>{onEnd()},{passive:true});el.addEventListener('mousedown',e=>{onStart(e.screenX)});el.addEventListener('mousemove',e=>{onMove(e.screenX)});el.addEventListener('mouseup',()=>{onEnd()});el.addEventListener('mouseleave',()=>{if(isDragging)onEnd()});el.addEventListener('click',e=>{if(swiped){e.stopPropagation();swiped=false;return}const id=el.dataset.id,email=state.emails.find(e=>e.id===id);if(email){state.currentEmail=email;state.currentView='email-detail';loadEmailDetail(id)}})})}\nfunction removeEmailEl(emailId){const el=content.querySelector('.email-item[data-id=\"'+emailId+'\"]');if(el)el.style.display='none';}\nasync function deleteEmail(emailId,emailEl){const email=state.emails.find(e=>e.id===emailId);if(!email)return;state.emails=state.emails.filter(e=>e.id!==emailId);removeEmailEl(emailId);showToast('πŸ—‘οΈ Deleting...','');const emailBackup=JSON.parse(JSON.stringify(email));const isTrash=state.currentMailboxId===state.trashMailboxId;try{if(!state.trashMailboxId){showToast('πŸ—‘οΈ Removed','success');return}if(isTrash){await jmapApiCall('Email/set',{accountId:state.accountId,destroy:[emailId]});showToast('πŸ—‘οΈ Permanently deleted','success')}else{const fullR=await jmapApiCall('Email/get',{accountId:state.accountId,ids:[emailId],bodyProperties:['textBody','htmlBody'],fetchTextBodyValues:true,fetchHTMLBodyValues:true});const fullEmail=fullR.methodResponses[0][1].list?.[0];if(!fullEmail)throw new Error('Could not fetch email data');let bodyText='';const textBody=fullEmail.textBody?.[0];const htmlBody=fullEmail.htmlBody?.[0];if(textBody&&fullEmail.bodyValues&&fullEmail.bodyValues[textBody.partId])bodyText=fullEmail.bodyValues[textBody.partId].value;if(!bodyText&&htmlBody&&fullEmail.bodyValues&&fullEmail.bodyValues[htmlBody.partId])bodyText=fullEmail.bodyValues[htmlBody.partId].value;await jmapApiCall('Email/set',{accountId:state.accountId,destroy:[emailId]});const reimportData={mailboxIds:{[state.trashMailboxId]:true},from:fullEmail.from||email.from,to:fullEmail.to||email.to,cc:fullEmail.cc||email.cc,bcc:fullEmail.bcc||email.bcc,subject:fullEmail.subject||email.subject||'(no subject)',receivedAt:fullEmail.receivedAt||email.receivedAt,keywords:fullEmail.keywords||email.keywords||{},textBody:bodyText?[{partId:'body-0',type:'text/plain'}]:undefined,bodyValues:{}};if(bodyText)reimportData.bodyValues['body-0']={value:bodyText,charset:'UTF-8'};if(!reimportData.cc)delete reimportData.cc;if(!reimportData.bcc)delete reimportData.bcc;if(!reimportData.textBody)delete reimportData.textBody;if(Object.keys(reimportData.bodyValues).length===0)delete reimportData.bodyValues;const cR=await jmapApiCall('Email/set',{accountId:state.accountId,create:{'reimported':reimportData}});if(!cR.methodResponses[0][1].created?.['reimported']){const err=cR.methodResponses[0][1].notCreated?.['reimported'];showToast('Email destroyed but failed to re-import: '+(err?.description||'unknown'),'error')}else{showToast('πŸ—‘οΈ Moved to trash','success')}}}catch(e){state.emails.push(emailBackup);state.emails.sort((a,b)=>new Date(b.receivedAt)-new Date(a.receivedAt));if(state.currentView==='emails')renderEmails();showToast('Failed to delete: '+e.message,'error')}}\nasync function loadEmailDetail(emailId){state.loading=true;renderEmailDetail();try{const r=await jmapApiCall('Email/get',{accountId:state.accountId,ids:[emailId],bodyProperties:['textBody','htmlBody'],fetchTextBodyValues:true,fetchHTMLBodyValues:true,maxBodyValueLength:10485760});const email=r.methodResponses[0][1].list?.[0];if(email){state.currentEmail=email;const textBody=email.textBody?.[0];const htmlBody=email.htmlBody?.[0];let fullBody=null;if(email.bodyValues){if(textBody&&email.bodyValues[textBody.partId]){fullBody=email.bodyValues[textBody.partId].value}if(!fullBody&&htmlBody&&email.bodyValues[htmlBody.partId]){fullBody=email.bodyValues[htmlBody.partId].value}}if(!fullBody&&textBody&&textBody.blobId&&state.downloadUrlTpl){try{const dlUrl=state.downloadUrlTpl.replace('{accountId}',encodeURIComponent(state.accountId)).replace('{blobId}',textBody.blobId).replace('{name}','body.txt').replace('?type={type}','?type=text/plain');const dlRes=await fetch(dlUrl,{headers:{'Authorization':'Basic '+btoa(state.credentials.username+':'+state.credentials.password)}});if(dlRes.ok){fullBody=await dlRes.text()}}catch(e){}}if(!fullBody&&htmlBody&&htmlBody.blobId&&state.downloadUrlTpl){try{const dlUrl=state.downloadUrlTpl.replace('{accountId}',encodeURIComponent(state.accountId)).replace('{blobId}',htmlBody.blobId).replace('{name}','body.html').replace('?type={type}','?type=text/html');const dlRes=await fetch(dlUrl,{headers:{'Authorization':'Basic '+btoa(state.credentials.username+':'+state.credentials.password)}});if(dlRes.ok){fullBody=await dlRes.text()}}catch(e){}}if(fullBody){state.currentEmail._fullBody=fullBody}}state.loading=false;renderEmailDetail();try{await jmapApiCall('Email/set',{accountId:state.accountId,update:{[emailId]:{keywords:{$seen:true}}}})}catch(e){}}catch(e){state.loading=false;showToast('Failed to load email: '+e.message,'error');renderEmailDetail()}}\nfunction renderEmailDetail(){if(state.loading){content.innerHTML='<div class=\"loading\"><div class=\"spinner\"></div><p>Loading message...</p></div>';return}const email=state.currentEmail;if(!email){content.innerHTML='<div class=\"empty-state\"><div class=\"icon\">πŸ’¬</div><h3>No message</h3></div>';return}const from=email.from?.map(f=>f.name||f.email).join(', ')||'Unknown',to=email.to?.map(t=>t.name||t.email).join(', ')||'Unknown',date=email.receivedAt?new Date(email.receivedAt).toLocaleString():'Unknown',subject=email.subject||'(No subject)';let bodyHtml='';const htmlBody=email.htmlBody?.[0],textBody=email.textBody?.[0];const fullBody=email._fullBody;if(fullBody){bodyHtml=escapeHtml(fullBody).replace(/\\n/g,'<br>')}else if(htmlBody?.value){bodyHtml=htmlBody.value}else if(textBody?.value){bodyHtml=escapeHtml(textBody.value).replace(/\\n/g,'<br>')}else if(email.preview){bodyHtml=escapeHtml(email.preview)}else{bodyHtml='<p style=\"color:#8b949e\">(No content)</p>'}content.innerHTML='<div class=\"email-detail\"><div class=\"email-detail-header\"><div class=\"email-detail-subject\">'+escapeHtml(subject)+'</div><div class=\"email-detail-from\">πŸ“¬ <strong>From:</strong> '+escapeHtml(from)+'</div><div class=\"email-detail-from\">πŸ“¨ <strong>To:</strong> '+escapeHtml(to)+'</div><div class=\"email-detail-date\">πŸ• '+escapeHtml(date)+'</div>'+(email.hasAttachment?'<div style=\"margin-top:8px;color:#d29922\">πŸ“Ž Has attachments</div>':'')+'</div><div class=\"email-detail-body\">'+bodyHtml+'</div><div class=\"email-detail-actions\"><button class=\"action-btn reply-btn\" id=\"replyBtn\">↩️ Reply</button><button class=\"action-btn\" id=\"replyAllBtn\">β†ͺ️ Reply All</button><button class=\"action-btn\" id=\"forwardBtn\">➑️ Forward</button></div></div>';$('replyBtn')?.addEventListener('click',()=>openReply(email));$('replyAllBtn')?.addEventListener('click',()=>openReplyAll(email));$('forwardBtn')?.addEventListener('click',()=>openForward(email));const detailEl=content.querySelector('.email-detail');if(detailEl){let sx=0,ex=0,swiping=false;detailEl.addEventListener('touchstart',e=>{sx=e.changedTouches[0].screenX;swiping=true},{passive:true});detailEl.addEventListener('touchmove',e=>{if(!swiping)return;ex=e.changedTouches[0].screenX;const dx=ex-sx;if(dx<0)detailEl.style.transform='translateX('+dx+'px)';detailEl.classList.add('swiping')},{passive:true});detailEl.addEventListener('touchend',()=>{swiping=false;detailEl.classList.remove('swiping');detailEl.style.transform='';const dx=ex-sx;if(dx<-80)goBackToList()},{passive:true});}}\nfunction openCompose(prefill={}){state.composeData={to:prefill.to||'',cc:prefill.cc||'',subject:prefill.subject||'',body:prefill.body||'',replyToEmailId:prefill.replyToEmailId||null};state.composeExpanded=false;state.currentView='compose';render()}\nfunction openReply(email){openCompose({to:email.from?.[0]?.email||'',subject:email.subject?'Re: '+email.subject.replace(/^Re:\\s*/i,''):'Re: ',body:'\\n\\n\\n--- Original message ---\\n'+(email.preview||''),replyToEmailId:email.id})}\nfunction openReplyAll(email){const to=email.from?.map(f=>f.email).join(', ')||'',cc=email.to?.map(t=>t.email).filter(e=>e!==state.credentials?.username).join(', ')||'';openCompose({to,cc,subject:email.subject?'Re: '+email.subject.replace(/^Re:\\s*/i,''):'Re: ',body:'\\n\\n\\n--- Original message ---\\n'+(email.preview||''),replyToEmailId:email.id})}\nfunction openForward(email){openCompose({subject:email.subject?'Fwd: '+email.subject.replace(/^Fwd:\\s*/i,''):'Fwd: ',body:'\\n\\n\\n--- Forwarded message ---\\n'+(email.preview||''),replyToEmailId:email.id})}\nfunction renderCompose(){const d=state.composeData,userEmail=state.credentials?.username||'',identityName=state.identities.length>0?state.identities[0].name||state.identities[0].email||userEmail:userEmail,expanded=state.composeExpanded;if(expanded){content.innerHTML='<div class=\"compose-form\"><div class=\"compose-fields compose-expanded\"><div class=\"compose-error\" id=\"composeError\"></div><textarea id=\"composeBody\" placeholder=\"Write your message...\">'+escapeHtml(d.body)+'</textarea></div><div class=\"compose-bar\"><button class=\"reduce-btn\" id=\"reduceBtn\">β–½ Reduce</button></div></div>';$('reduceBtn').addEventListener('click',()=>{state.composeData.body=$('composeBody').value;state.composeExpanded=false;render()});$('composeBody').addEventListener('keydown',e=>{if((e.ctrlKey||e.metaKey)&&e.key==='Enter')handleSend()});$('composeBody').focus()}else{content.innerHTML='<div class=\"compose-form\"><div class=\"compose-fields\"><div class=\"compose-error\" id=\"composeError\"></div><div class=\"compose-hdrs\"><div class=\"compose-row\"><label for=\"composeTo\">To</label><input type=\"text\" id=\"composeTo\" placeholder=\"recipient@example.com\" value=\"'+escapeHtml(d.to)+'\"></div><div class=\"compose-row\"><label for=\"composeCc\">CC</label><input type=\"text\" id=\"composeCc\" placeholder=\"cc@example.com\" value=\"'+escapeHtml(d.cc)+'\"></div><div class=\"compose-row\"><label for=\"composeSubject\">Subj</label><input type=\"text\" id=\"composeSubject\" placeholder=\"Subject\" value=\"'+escapeHtml(d.subject)+'\"></div></div><textarea id=\"composeBody\" placeholder=\"Write your message...\">'+escapeHtml(d.body)+'</textarea></div></div>';$('composeBody').addEventListener('focus',()=>{state.composeData.to=$('composeTo').value;state.composeData.cc=$('composeCc').value;state.composeData.subject=$('composeSubject').value;state.composeData.body=$('composeBody').value;state.composeExpanded=true;render()});$('composeBody').addEventListener('keydown',e=>{if((e.ctrlKey||e.metaKey)&&e.key==='Enter')handleSend()});if(!d.to)$('composeTo').focus();else if(!d.subject)$('composeSubject').focus();else $('composeBody').focus()}}\nasync function handleSend(){let toRaw,ccRaw,subject,body;if(state.composeExpanded){toRaw=state.composeData.to;ccRaw=state.composeData.cc;subject=state.composeData.subject;body=$('composeBody').value}else{toRaw=$('composeTo').value.trim();ccRaw=$('composeCc').value.trim();subject=$('composeSubject').value.trim();body=$('composeBody').value}const errorEl=$('composeError'),btn=$('sendBtn')||sendHeaderBtn;if(!toRaw){errorEl.textContent='Please enter at least one recipient.';errorEl.style.display='block';return}errorEl.style.display='none';btn.disabled=true;btn.textContent='πŸ“€ Sending...';sendHeaderBtn.disabled=true;sendHeaderBtn.textContent='πŸ“€ Sending...';try{const toAddresses=toRaw.split(',').map(s=>s.trim()).filter(s=>s).map(email=>({email})),ccAddresses=ccRaw.split(',').map(s=>s.trim()).filter(s=>s).map(email=>({email})),userEmail=state.credentials.username;if(state.identities.length===0)await loadIdentities();let identity=state.identities.find(i=>i.email===userEmail);if(!identity&&state.identities.length>0)identity=state.identities[0];if(!identity)throw new Error('No identity found on the server.');if(!state.sendMailboxId)throw new Error('No mailbox available.');const to=toAddresses,cc=ccAddresses.length>0?ccAddresses:undefined;const emailCreate={mailboxIds:{[state.sendMailboxId]:true},keywords:{$seen:false},from:[{email:identity.email||userEmail}],to,subject:subject||'(no subject)',textBody:[{partId:'body-0',type:'text/plain'}],bodyValues:{'body-0':{value:body,charset:'UTF-8'}}};if(cc)emailCreate.cc=cc;if(state.composeData.replyToEmailId)emailCreate.inReplyTo=state.composeData.replyToEmailId;const submissionAccountId=state.submissionAccountId||state.accountId,allRcpt=[...toAddresses];if(ccAddresses.length>0)allRcpt.push(...ccAddresses);const multiResult=await jmapMultiCall([['Email/set',{accountId:state.accountId,create:{'new-email':emailCreate}},'0'],['EmailSubmission/set',{accountId:submissionAccountId,create:{'new-submission':{emailId:'#new-email',identityId:identity.id,envelope:{mailFrom:{email:identity.email||userEmail},rcptTo:allRcpt}}}},'1']]);const emailSetResult=multiResult.methodResponses[0][1],created=emailSetResult.created?.['new-email'];if(!created){const err=emailSetResult.notCreated?.['new-email'];throw new Error(err?.description||'Failed to create email')}const submissionResult=multiResult.methodResponses[1][1],submitted=submissionResult.created?.['new-submission'];if(!submitted){const err=submissionResult.notCreated?.['new-submission'],errDetail=err?(err.description||err.type||JSON.stringify(err)):'Unknown error';if(state.composeData.replyToEmailId&&(errDetail.includes('oldEmail')||errDetail.includes('inReplyTo'))){delete emailCreate.inReplyTo;const retryResult=await jmapMultiCall([['Email/set',{accountId:state.accountId,create:{'new-email':emailCreate}},'0'],['EmailSubmission/set',{accountId:submissionAccountId,create:{'new-submission':{emailId:'#new-email',identityId:identity.id,envelope:{mailFrom:{email:identity.email||userEmail},rcptTo:allRcpt}}}},'1']]);const retrySubmitted=retryResult.methodResponses[1][1].created?.['new-submission'];if(retrySubmitted){showToast('βœ… Email sent successfully!','success');state.currentView='mailboxes';state.composeExpanded=false;render();return}throw new Error('SMTP rejected: '+(retryResult.methodResponses[1][1].notCreated?.['new-submission']?.description||errDetail))}throw new Error('SMTP rejected: '+errDetail)}showToast('βœ… Email sent successfully!','success');state.currentView='mailboxes';state.composeExpanded=false;render()}catch(e){errorEl.textContent='Failed to send: '+e.message;errorEl.style.display='block';btn.disabled=false;btn.textContent='πŸ“€ Send';sendHeaderBtn.disabled=false;sendHeaderBtn.textContent='πŸ“€ Send'}btn.disabled=false;btn.textContent='πŸ“€ Send';sendHeaderBtn.disabled=false;sendHeaderBtn.textContent='πŸ“€ Send'}\nfunction escapeHtml(str){if(!str)return'';const d=document.createElement('div');d.textContent=str;return d.innerHTML}\ntitle.addEventListener('click',()=>{goToFolders()});\nbackBtn.addEventListener('click',()=>{if(state.currentView==='email-detail'){state.currentView='emails';render()}else if(state.currentView==='emails'){state.currentView='mailboxes';render()}});\ncomposeBtn.addEventListener('click',()=>openCompose({}));\nrefreshBtn.addEventListener('click',handleRefresh);\nsendHeaderBtn.addEventListener('click',handleSend);\nlogoutBtn.addEventListener('click',()=>{state.credentials=null;state.session=null;state.apiUrl=null;state.downloadUrlTpl=null;state.accountId=null;state.submissionAccountId=null;state.identities=[];state.mailboxes=[];state.mailboxMap={};state.emails=[];state.currentEmail=null;state.sendMailboxId=null;state.trashMailboxId=null;state.composeExpanded=false;state.currentView='login';localStorage.removeItem(STORAGE_KEY);render();showToast('Logged out','success')});\nasync function init(){loadState();if(state.credentials&&state.session&&state.apiUrl&&state.accountId){state.currentView='mailboxes';render();await loadIdentities();await loadMailboxes()}else{state.currentView='login';render()}setupPtr();}\ninit();\n</script>\n</body>\n</html>"
},
"currentHash": "08724e129ad34d60bbd8ad5aa8ce065bde0ca8d7732ed3fcc4125d0af8c1d86b",
"previousHash": "c6f172fe9e5317be45405fad0d04f93b4b8f1518ce5397a3b0b083ab4426f9f1",
"prompt": "email: don't hard code server, add it as as a field before user and password, prefilled with JMAP"
},
"2": {
"tab": {
"id": "jmap-email",
"label": "Email",
"html": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"UTF-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, viewport-fit=cover\">\n<title>JMAP Email</title>\n<style>\nhtml,body{margin:0;height:100%;background:#0d1117;color:#c9d1d9;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif}\n*{box-sizing:border-box}\n.app{display:flex;flex-direction:column;height:100%;max-width:600px;margin:0 auto}\n.header{padding:10px 12px;border-bottom:1px solid #30363d;background:#161b22;display:flex;align-items:center;gap:6px;min-height:52px}\n.header h1{margin:0;font-size:17px;font-weight:600;flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;cursor:pointer;-webkit-user-select:none;user-select:none}\n.header h1:active{opacity:.7}\n.header .back-btn{background:none;border:none;color:#58a6ff;font-size:20px;cursor:pointer;padding:8px;min-width:44px;min-height:44px;display:flex;align-items:center;justify-content:center}\n.header .hdr-btn{background:none;border:1px solid #30363d;padding:6px 10px;border-radius:6px;cursor:pointer;font-size:13px;min-height:36px;display:flex;align-items:center;gap:3px;white-space:nowrap}\n.header .hdr-btn.primary{background:#238636;color:#fff;border:none;font-weight:600}\n.header .hdr-btn:disabled{opacity:.5;cursor:not-allowed}\n.header .hdr-btn.danger{color:#f85149}\n.header .hdr-btn.acct{color:#58a6ff;max-width:100px;overflow:hidden;text-overflow:ellipsis}\n.content{flex:1;overflow-y:auto;padding:16px}\n.content::-webkit-scrollbar{width:6px}\n.content::-webkit-scrollbar-thumb{background:#30363d;border-radius:3px}\n.content.nopad{padding:0}\n.form-card{max-width:400px;margin:40px auto 0;padding:20px;background:#161b22;border:1px solid #30363d;border-radius:12px}\n.form-card h2{margin:0 0 16px;font-size:18px;text-align:center}\n.form-card label{display:block;margin-bottom:4px;color:#8b949e;font-size:13px}\n.form-card input{width:100%;padding:12px 14px;margin-bottom:14px;min-height:44px;background:#0d1117;border:1px solid #30363d;border-radius:8px;color:#c9d1d9;font-size:15px;outline:none}\n.form-card input:focus{border-color:#58a6ff}\n.form-card .btn-primary{width:100%;padding:12px;min-height:44px;font-size:15px;background:#238636;color:#fff;border:none;border-radius:8px;cursor:pointer;font-weight:600}\n.form-card .btn-primary:disabled{opacity:.5;cursor:not-allowed}\n.form-card .error{color:#f85149;font-size:13px;padding:8px;background:rgba(248,81,73,0.1);border-radius:6px;display:none}\n.acct-item{display:flex;align-items:center;padding:14px 16px;min-height:52px;border-bottom:1px solid #21262d;cursor:pointer;gap:12px}\n.acct-item:hover{background:#161b22}\n.acct-item .acct-icon{font-size:22px;width:40px;text-align:center}\n.acct-item .acct-info{flex:1}\n.acct-item .acct-name{font-size:15px;font-weight:500}\n.acct-item .acct-email{font-size:12px;color:#8b949e;margin-top:2px}\n.acct-item .acct-badge{background:#238636;color:#fff;border-radius:10px;padding:2px 8px;font-size:11px;font-weight:600}\n.acct-item .acct-del{background:none;border:none;color:#f85149;font-size:18px;cursor:pointer;padding:8px;min-width:44px;min-height:44px;display:flex;align-items:center;justify-content:center}\n.mailbox-item{display:flex;align-items:center;padding:14px 16px;min-height:52px;border-bottom:1px solid #21262d;cursor:pointer;gap:12px}\n.mailbox-item:hover{background:#161b22}\n.mailbox-icon{font-size:20px;width:36px;text-align:center}\n.mailbox-info{flex:1}\n.mailbox-name{font-size:15px;font-weight:500}\n.mailbox-stats{font-size:12px;color:#8b949e;margin-top:2px}\n.mailbox-unread{background:#238636;color:#fff;border-radius:12px;padding:2px 10px;font-size:12px;font-weight:600}\n.email-item{padding:14px 16px;min-height:60px;border-bottom:1px solid #21262d;cursor:pointer;position:relative;background:#0d1117}\n.email-item:hover{background:#161b22}\n.email-item.unread{border-left:3px solid #58a6ff}\n.email-item .del-bg{position:absolute;top:0;right:0;bottom:0;background:#f85149;display:flex;align-items:center;padding:0 20px;font-size:14px;font-weight:600;color:#fff;z-index:0}\n.email-item .ec{position:relative;z-index:1;background:#0d1117;transition:transform .2s ease;padding:0}\n.email-sender{font-size:14px;font-weight:600;color:#e6edf3;margin-bottom:4px}\n.email-subject{font-size:14px;color:#c9d1d9;margin-bottom:4px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n.email-preview{font-size:13px;color:#8b949e;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n.email-date{font-size:11px;color:#8b949e;margin-top:4px}\n.email-item.unread .email-subject{font-weight:600;color:#e6edf3}\n.email-detail{padding:16px}\n.email-detail-subject{font-size:17px;font-weight:600;margin-bottom:10px}\n.email-detail-from{font-size:13px;color:#8b949e;margin-bottom:3px}\n.email-detail-body{padding:12px 0;line-height:1.6;font-size:14px;word-wrap:break-word}\n.email-detail-actions{display:flex;gap:8px;padding:8px 0;border-top:1px solid #30363d}\n.email-detail-actions .act-btn{flex:1;padding:10px;min-height:44px;border:1px solid #30363d;border-radius:8px;background:none;color:#c9d1d9;font-size:14px;cursor:pointer}\n.email-detail-actions .act-btn:active{background:#161b22}\n.email-detail-actions .reply{color:#58a6ff}\n.compose-form{display:flex;flex-direction:column;height:100%}\n.compose-fields{padding:14px;display:flex;flex-direction:column;height:100%}\n.compose-fields label{display:block;margin-bottom:4px;color:#8b949e;font-size:13px;font-weight:500}\n.compose-fields input,.compose-fields textarea{padding:10px 12px;min-height:44px;background:#0d1117;border:1px solid #30363d;border-radius:8px;color:#c9d1d9;font-size:15px;outline:none;font-family:inherit}\n.compose-row{display:flex;align-items:center;gap:8px;margin-bottom:10px}\n.compose-row label{flex-shrink:0;width:auto;min-width:48px;margin-bottom:0}\n.compose-row input{flex:1;min-width:0}\n.compose-fields textarea{min-height:180px;resize:vertical}\n.compose-fields .send-btn{width:100%;padding:12px;min-height:44px;font-size:15px;background:#238636;color:#fff;border:none;border-radius:8px;cursor:pointer;font-weight:600}\n.compose-fields .send-btn:disabled{opacity:.5;cursor:not-allowed}\n.compose-fields .ce{color:#f85149;font-size:13px;padding:8px;background:rgba(248,81,73,0.1);border-radius:6px;display:none;margin-bottom:10px}\n.compose-fields.expanded{padding:0}\n.compose-fields.expanded .ch,.compose-fields.expanded .cfb{display:none}\n.compose-fields.expanded .ce{margin:8px 8px 0}\n.compose-fields.expanded #cb{min-height:0;flex:1;margin-bottom:0;border-radius:0;border-left:none;border-right:none;border-top:1px solid #30363d;resize:none;padding:16px;font-size:16px}\n.compose-bar{display:flex;gap:8px;padding:10px 12px;border-top:1px solid #30363d;background:#161b22}\n.compose-bar .send-btn{flex:1;padding:12px;min-height:48px;font-size:15px;background:#238636;color:#fff;border:none;border-radius:8px;cursor:pointer;font-weight:600}\n.compose-bar .send-btn:disabled{opacity:.5;cursor:not-allowed}\n.compose-bar .reduce-btn{min-width:80px;padding:12px;min-height:48px;font-size:14px;background:none;color:#8b949e;border:1px solid #30363d;border-radius:8px;cursor:pointer}\n.loading{text-align:center;padding:40px 16px;color:#8b949e}\n.loading .spinner{display:inline-block;width:24px;height:24px;border:3px solid #30363d;border-top-color:#58a6ff;border-radius:50%;animation:spin .8s linear infinite;margin-bottom:12px}\n@keyframes spin{to{transform:rotate(360deg)}}\n.empty-state{text-align:center;padding:60px 20px;color:#8b949e}\n.empty-state .icon{font-size:48px;margin-bottom:16px}\n.empty-state h3{font-size:18px;margin:0 0 8px;color:#c9d1d9}\n.status-bar{padding:6px 12px;background:#161b22;border-top:1px solid #30363d;font-size:11px;color:#8b949e;display:flex;justify-content:space-between;min-height:32px;align-items:center}\n.status-bar.hidden{display:none}\n.toast{position:fixed;bottom:80px;left:50%;transform:translateX(-50%);background:#161b22;border:1px solid #30363d;padding:10px 20px;border-radius:8px;font-size:14px;color:#c9d1d9;z-index:100;display:none;max-width:90%;text-align:center}\n.toast.error{border-color:#f85149;background:rgba(248,81,73,0.15)}\n.toast.success{border-color:#238636;background:rgba(35,134,54,0.15)}\n.ptr-indicator{position:absolute;top:0;left:0;width:100%;color:#8b949e;font-size:14px;text-align:center;padding:12px 0;pointer-events:none;z-index:5;display:none}\n.ptr-indicator.visible{display:block}\n@media(max-width:480px){.form-card{margin:20px 8px 0;padding:16px}.content{padding:0}.header{padding:8px 10px;gap:4px}.header .hdr-btn{padding:5px 7px;font-size:12px}.compose-fields{padding:10px}.compose-fields.expanded{padding:0}}\n</style>\n</head>\n<body>\n<div class=\"app\" id=\"app\">\n <div class=\"header\" id=\"header\">\n <button class=\"back-btn\" id=\"backBtn\" style=\"display:none\">←</button>\n <h1 id=\"title\">πŸ“§ Email</h1>\n <button class=\"hdr-btn acct\" id=\"acctBtn\" style=\"display:none\">πŸ‘€</button>\n <button class=\"hdr-btn\" id=\"addAcctHdrBtn\" style=\"display:none\">βž•</button>\n <button class=\"hdr-btn\" id=\"composeBtn\" style=\"display:none\">✏️</button>\n <button class=\"hdr-btn\" id=\"refreshBtn\" style=\"display:none\">πŸ”„</button>\n <button class=\"hdr-btn primary\" id=\"sendHdrBtn\" style=\"display:none\">πŸ“€ Send</button>\n <button class=\"hdr-btn danger\" id=\"logoutBtn\" style=\"display:none\">Exit</button>\n </div>\n <div class=\"content\" id=\"content\"><div class=\"ptr-indicator\" id=\"ptrI\"><span id=\"ptrT\">⬇️ Hold & pull down to refresh</span></div></div>\n <div class=\"status-bar\" id=\"statusBar\"><span id=\"stLeft\">Not connected</span><span id=\"stRight\">jmap</span></div>\n</div>\n<div class=\"toast\" id=\"toast\"></div>\n<script>\nconst SK='msw_tab_jmap_email';\nlet st={accounts:[],activeId:null,view:'accounts',mboxId:null,mboxName:'',emails:[],curEmail:null,comp:{to:'',cc:'',subj:'',body:'',replyId:null},compExp:false,loading:false};\nconst $=id=>document.getElementById(id),co=$('content'),ti=$('title'),bb=$('backBtn'),ab=$('acctBtn'),ab2=$('addAcctHdrBtn'),cb=$('composeBtn'),rb=$('refreshBtn'),lb=$('logoutBtn'),sh=$('sendHdrBtn'),sb=$('statusBar'),sl=$('stLeft'),sr=$('stRight'),to=$('toast');\nlet tt=null;\nfunction toast(msg,type){to.textContent=msg;to.className='toast '+(type||'');to.style.display='block';clearTimeout(tt);tt=setTimeout(()=>{to.style.display='none'},4000)}\nfunction acct(){return st.accounts.find(a=>a.id===st.activeId)}\nfunction save(){try{localStorage.setItem(SK,JSON.stringify({accounts:st.accounts.map(a=>{const{mailboxes,identities,mailboxMap,sendMailboxId,trashMailboxId,...rest}=a;return{...rest,mailboxes:mailboxes.map(m=>({id:m.id,name:m.name,totalEmails:m.totalEmails||m.totalMessages||0,unreadEmails:m.unreadEmails||m.unreadMessages||0,sortOrder:m.sortOrder||0})),identities:identities.map(i=>({id:i.id,email:i.email,name:i.name})),mailboxMap,smb:sendMailboxId,tmb:trashMailboxId}}),activeId:st.activeId}))}catch(e){}}\nfunction load(){try{const r=localStorage.getItem(SK);if(!r){st.accounts=[];st.activeId=null;return}const d=JSON.parse(r);if(!d||typeof d!=='object'||!Array.isArray(d.accounts)){st.accounts=[];st.activeId=null;return}st.accounts=d.accounts.map(a=>{a.mailboxMap=a.mailboxMap||{};a.sendMailboxId=a.smb||null;a.trashMailboxId=a.tmb||null;delete a.smb;delete a.tmb;return a});st.activeId=(d.activeId&&st.accounts.find(a=>a.id===d.activeId))?d.activeId:st.accounts.length>0?st.accounts[0].id:null}catch(e){st.accounts=[];st.activeId=null}}\nfunction actv(){return acct()||(st.accounts[0]||null)}\nfunction selAcct(id){st.activeId=id;save();st.view='mailboxes';render();loadMailboxes()}\nfunction goAccts(){st.view='accounts';render()}\nfunction goFolders(){if(st.view==='emails'||st.view==='edetail'){st.view='mailboxes';render()}}\nfunction goBack(){if(st.view==='edetail'){st.view='emails';render()}else if(st.view==='emails'||st.view==='compose'){st.view='mailboxes';render()}else if(st.view==='mailboxes'){st.view='accounts';render()}}\nfunction upHdr(){const notAccts=st.view!=='accounts';bb.style.display=(st.view==='accounts'||st.view==='compose'||st.view==='login')?'none':'flex';ab.style.display=notAccts?'flex':'none';ab2.style.display=notAccts?'flex':'none';cb.style.display=notAccts&&st.view!=='compose'?'flex':'none';rb.style.display=notAccts&&st.view!=='compose'?'flex':'none';lb.style.display=notAccts?'flex':'none';sh.style.display=st.view==='compose'?'flex':'none';const a=actv();ab.textContent=a?'πŸ‘€ '+((a.credentials?.username||a.label||'').substring(0,8)):'πŸ‘€';switch(st.view){case'accounts':ti.textContent='πŸ“§ Accounts';break;case'login':ti.textContent='βž• Add Account';break;case'mailboxes':ti.textContent='πŸ“‚ '+((a||{}).credentials?.username||a?.label||'Folders');break;case'emails':ti.textContent='πŸ“¨ '+st.mboxName;break;case'edetail':ti.textContent='πŸ’¬ Message';break;case'compose':const t=st.comp.to;ti.textContent=t?'βœ‰οΈ '+t.split(',')[0].trim():'✏️ New';break}}\nfunction upSb(){if(st.view==='compose'){sb.classList.add('hidden')}else{sb.classList.remove('hidden');const a=actv();if(a){sl.textContent=a.credentials?.username||a.label||'Logged in';sr.textContent=new Date().toLocaleTimeString([],{hour:'2-digit',minute:'2-digit'})}else{sl.textContent='No account';sr.textContent='jmap'}}}\nfunction render(){co.className='content'+(st.view==='compose'?' nopad':'');switch(st.view){case'accounts':renderAccts();break;case'login':renderLogin();break;case'mailboxes':renderMboxes();break;case'emails':renderEmails();break;case'edetail':renderDetail();break;case'compose':renderComp();break}upHdr();upSb()}\nfunction escape(str){if(!str)return'';const d=document.createElement('div');d.textContent=str;return d.innerHTML}\nti.addEventListener('click',goFolders);bb.addEventListener('click',goBack);\nlb.addEventListener('click',()=>{st.activeId=null;st.view='accounts';st.curEmail=null;st.emails=[];render();toast('Switched to account list','success')});\nab.addEventListener('click',goAccts);\nab2.addEventListener('click',()=>{st.view='login';render()});\ncb.addEventListener('click',()=>openComp({}));\nrb.addEventListener('click',async()=>{if(st.loading)return;toast('πŸ”„ Refreshing...','');if(st.view==='mailboxes'){await loadMailboxes()}else if(st.view==='emails'){await loadEmails()}else if(st.view==='edetail'&&st.curEmail){await loadDetail(st.curEmail.id)}});\nsh.addEventListener('click',handleSend);\nasync function jmapFetch(path,opts){opts=opts||{};const a=actv();if(!a)throw new Error('No active account');const url=path.startsWith('http')?path:a.serverUrl+path;const h={'Content-Type':'application/json','Accept':'application/json',...opts.headers};if(a.credentials){if(a.credentials.token)h['Authorization']='Bearer '+a.credentials.token;else if(a.credentials.username&&a.credentials.password)h['Authorization']='Basic '+btoa(a.credentials.username+':'+a.credentials.password)}const res=await fetch(url,{method:opts.method||'GET',headers:h,body:opts.body?JSON.stringify(opts.body):undefined});if(!res.ok){let d='';try{const e=await res.json();d=e.detail||e.type||JSON.stringify(e)}catch(e){d=res.statusText}throw new Error('HTTP '+res.status+': '+d)}const t=await res.text();return t?JSON.parse(t):null}\nasync function japi(method,params){params=params||{};const a=actv();if(!a.apiUrl)throw new Error('No API URL');const r=await jmapFetch(a.apiUrl,{method:'POST',body:{using:['urn:ietf:params:jmap:core','urn:ietf:params:jmap:mail','urn:ietf:params:jmap:submission'],methodCalls:[[method,params,'0']]}});return r}\nasync function jmulti(calls){const a=actv();if(!a.apiUrl)throw new Error('No API URL');const r=await jmapFetch(a.apiUrl,{method:'POST',body:{using:['urn:ietf:params:jmap:core','urn:ietf:params:jmap:mail','urn:ietf:params:jmap:submission'],methodCalls}});return r}\nfunction renderAccts(){if(st.accounts.length===0){co.innerHTML='<div class=\"empty-state\"><div class=\"icon\">πŸ“§</div><h3>No email accounts</h3><p>Add an account to get started.</p><button class=\"btn-primary\" style=\"margin-top:16px;padding:12px 24px;background:#238636;color:#fff;border:none;border-radius:8px;cursor:pointer;font-size:15px;min-height:44px\" id=\"addFirstBtn\">βž• Add Account</button></div>';$('addFirstBtn')?.addEventListener('click',()=>{st.view='login';render()});return}let html='';for(const a of st.accounts){const isActive=a.id===st.activeId;const badge=isActive?'<span class=\"acct-badge\">Active</span>':'';const email=a.credentials?.username||'';html+='<div class=\"acct-item\" data-id=\"'+a.id+'\"><div class=\"acct-icon\">πŸ“§</div><div class=\"acct-info\"><div class=\"acct-name\">'+escape(email)+'</div></div>'+badge+'<button class=\"acct-del\" data-del=\"'+a.id+'\">βœ•</button></div>'}html+='<div class=\"acct-item\" id=\"addAcctBtn\" style=\"color:#58a6ff;justify-content:center;font-weight:500;gap:6px;border-bottom:none\"><span style=\"font-size:20px\">βž•</span> Add Account</div>';co.innerHTML=html;co.querySelectorAll('.acct-item[data-id]').forEach(el=>{el.addEventListener('click',e=>{if(e.target.closest('.acct-del'))return;selAcct(el.dataset.id)})});co.querySelectorAll('.acct-del').forEach(el=>{el.addEventListener('click',e=>{e.stopPropagation();const id=el.dataset.del;if(st.accounts.length<=1){st.accounts=[];st.activeId=null;save();render();return}if(!confirm('Remove \"'+(st.accounts.find(a=>a.id===id)?.credentials?.username||'')+'\"?'))return;st.accounts=st.accounts.filter(a=>a.id!==id);if(st.activeId===id)st.activeId=st.accounts[0]?.id||null;save();render()})});$('addAcctBtn')?.addEventListener('click',()=>{st.view='login';render()})}\nfunction renderLogin(){co.innerHTML='<div class=\"form-card\"><h2>βž• Add Account</h2><label for=\"srvUrl\">JMAP Server</label><input type=\"text\" id=\"srvUrl\" placeholder=\"https://jmap.example.com\" value=\"https://jmap.gakoy.com\"><label for=\"uname\">Username (Email)</label><input type=\"text\" id=\"uname\" placeholder=\"your@email.com\" autocomplete=\"username\"><label for=\"pwd\">Password</label><input type=\"password\" id=\"pwd\" placeholder=\"β€’β€’β€’β€’β€’β€’β€’β€’\" autocomplete=\"current-password\"><button class=\"btn-primary\" id=\"loginBtn\">Add Account & Sign In</button><div class=\"error\" id=\"loginErr\"></div></div>';$('loginBtn').addEventListener('click',handleLogin);$('srvUrl').addEventListener('keydown',e=>{if(e.key==='Enter')$('uname').focus()});$('uname').addEventListener('keydown',e=>{if(e.key==='Enter')$('pwd').focus()});$('pwd').addEventListener('keydown',e=>{if(e.key==='Enter')handleLogin()});$('srvUrl').focus()}\nasync function handleLogin(){const srv=$('srvUrl').value.trim()||'https://jmap.gakoy.com',uname=$('uname').value.trim(),pwd=$('pwd').value,err=$('loginErr'),btn=$('loginBtn');if(!srv||!uname||!pwd){err.textContent='Please fill in all fields.';err.style.display='block';return}err.style.display='none';btn.disabled=true;btn.textContent='Signing in...';try{const id='acc_'+Date.now();const creds={username:uname,password:pwd};const session=await jmapFetchFrom(srv,creds,'/.well-known/jmap');const a={id,label:uname,serverUrl:srv,credentials:creds,session,apiUrl:session.apiUrl,downloadUrlTpl:session.downloadUrl,accountId:session.primaryAccounts['urn:ietf:params:jmap:mail'],submissionAccountId:session.primaryAccounts['urn:ietf:params:jmap:submission'],identities:[],mailboxes:[],mailboxMap:{},sendMailboxId:null,trashMailboxId:null};st.accounts.push(a);st.activeId=a.id;save();st.view='mailboxes';render();toast('βœ… Account added!','success');await loadIdentities();await loadMailboxes()}catch(e){err.textContent='Failed: '+e.message;err.style.display='block';btn.disabled=false;btn.textContent='Add Account & Sign In'}}\nasync function jmapFetchFrom(srv,creds,path){const url=srv+path;const h={'Content-Type':'application/json','Accept':'application/json'};if(creds.token)h['Authorization']='Bearer '+creds.token;else if(creds.username&&creds.password)h['Authorization']='Basic '+btoa(creds.username+':'+creds.password);const res=await fetch(url,{headers:h});if(!res.ok){let d='';try{const e=await res.json();d=e.detail||e.type||JSON.stringify(e)}catch(e){d=res.statusText}throw new Error('HTTP '+res.status+': '+d)}const t=await res.text();return t?JSON.parse(t):null}\nasync function loadIdentities(){const a=actv();if(!a)return;try{const sub=a.submissionAccountId||a.accountId;const r=await japi('Identity/get',{accountId:sub});a.identities=r.methodResponses[0][1].list||[];save()}catch(e){a.identities=[]}}\nasync function loadMailboxes(){st.loading=true;renderMboxes();const a=actv();if(!a){st.loading=false;render();return}try{const r=await japi('Mailbox/get',{accountId:a.accountId});a.mailboxes=r.methodResponses[0][1].list||[];a.mailboxMap={};let o=null,s=null,d=null,tr=null,f=null;for(const mb of a.mailboxes){a.mailboxMap[mb.id]=mb.name;const n=(mb.name||'').toLowerCase(),ro=(mb.role||'').toLowerCase();if(n==='outbox'||ro==='outbox')o=mb.id;if(n==='sent'||ro==='sent')s=mb.id;if(n==='drafts'||ro==='drafts')d=mb.id;if(n==='trash'||n==='deleted messages'||n==='bin'||ro==='trash')tr=mb.id;if(!f)f=mb.id}a.sendMailboxId=o||s||d||f;a.trashMailboxId=tr;save();st.loading=false;renderMboxes()}catch(e){st.loading=false;toast('Failed to load mailboxes: '+e.message,'error');renderMboxes()}}\nfunction renderMboxes(){if(st.loading){co.innerHTML='<div class=\"loading\"><div class=\"spinner\"></div><p>Loading mailboxes...</p></div>';return}const a=actv();if(!a||a.mailboxes.length===0){co.innerHTML='<div class=\"empty-state\"><div class=\"icon\">πŸ“­</div><h3>No mailboxes</h3></div>';return}const icons={'inbox':'πŸ“₯','sent':'πŸ“€','drafts':'πŸ“','trash':'πŸ—‘οΈ','spam':'⚠️','archive':'πŸ“¦','junk':'⚠️','outbox':'πŸ“€'};const sorted=[...a.mailboxes].sort((x,y)=>(x.sortOrder||0)-(y.sortOrder||0));let html='';for(const mb of sorted){const name=mb.name||'Unnamed',total=mb.totalEmails||mb.totalMessages||0,unread=mb.unreadEmails||mb.unreadMessages||0,icon=Object.entries(icons).find(([k])=>name.toLowerCase().includes(k))?.[1]||'πŸ“';html+='<div class=\"mailbox-item\" data-id=\"'+mb.id+'\"><div class=\"mailbox-icon\">'+icon+'</div><div class=\"mailbox-info\"><div class=\"mailbox-name\">'+escape(name)+'</div><div class=\"mailbox-stats\">'+total+' messages</div></div>'+(unread>0?'<div class=\"mailbox-unread\">'+unread+'</div>':'')+'</div>'}co.innerHTML=html;co.querySelectorAll('.mailbox-item').forEach(el=>{el.addEventListener('click',()=>{const id=el.dataset.id,mb=a.mailboxes.find(m=>m.id===id);if(mb){st.mboxId=id;st.mboxName=mb.name;st.view='emails';render();loadEmails()}})})}\nasync function loadEmails(){st.loading=true;renderEmails();const a=actv();if(!a){st.loading=false;render();return}try{const q=await japi('Email/query',{accountId:a.accountId,filter:{inMailbox:st.mboxId},sort:[{property:'receivedAt',isAscending:false}],limit:50});const ids=q.methodResponses[0][1].ids;if(!ids||ids.length===0){st.emails=[];st.loading=false;renderEmails();return}const g=await japi('Email/get',{accountId:a.accountId,ids,properties:['id','subject','from','to','receivedAt','hasAttachment','preview','size','keywords']});st.emails=g.methodResponses[0][1].list||[];st.loading=false;renderEmails()}catch(e){st.loading=false;toast('Failed to load emails: '+e.message,'error');renderEmails()}}\nfunction renderEmails(){if(st.loading){co.innerHTML='<div class=\"loading\"><div class=\"spinner\"></div><p>Loading emails...</p></div>';return}if(st.emails.length===0){co.innerHTML='<div class=\"empty-state\"><div class=\"icon\">πŸ“¨</div><h3>No emails</h3><p>This mailbox is empty.</p></div>';return}let html='<div class=\"email-list\">';for(const e of st.emails){const from=e.from?.[0]?.name||e.from?.[0]?.email||'Unknown',unread=!e.keywords?.$seen,date=new Date(e.receivedAt),ds=date.toLocaleDateString([],{month:'short',day:'numeric'}),ts=date.toLocaleTimeString([],{hour:'2-digit',minute:'2-digit'}),today=new Date().toDateString()===date.toDateString();html+='<div class=\"email-item'+(unread?' unread':'')+'\" data-id=\"'+e.id+'\"><div class=\"del-bg\">πŸ—‘οΈ Delete</div><div class=\"ec\"><div class=\"email-sender\">'+escape(from)+'</div><div class=\"email-subject\">'+escape(e.subject||'(No subject)')+'</div><div class=\"email-preview\">'+escape((e.preview||'').substring(0,100))+'</div><div class=\"email-date\">'+(today?ts:ds)+'</div></div></div>'}html+='</div>';co.innerHTML=html;co.querySelectorAll('.email-item').forEach(el=>{const ec=el.querySelector('.ec');let sx=0,cx=0,dr=false,sw=false;const dbg=el.querySelector('.del-bg');dbg.style.opacity='0';const onS=function(x){sx=x;cx=0;dr=true;sw=false;ec.classList.add('swiping');el.classList.add('swiping')};const onM=function(x){if(!dr)return;const dx=x-sx;if(dx<0){cx=0;ec.style.transform='translateX(0px)';return}cx=Math.min(dx,120);ec.style.transform='translateX('+cx+'px)';dbg.style.opacity=Math.min(cx/80,1)};const onE=function(){dr=false;ec.classList.remove('swiping');el.classList.remove('swiping');if(cx>60){sw=true;delEmail(el.dataset.id,el)}else{ec.style.transform='translateX(0px)';dbg.style.opacity='0'}};el.addEventListener('touchstart',e=>{onS(e.changedTouches[0].screenX)},{passive:true});el.addEventListener('touchmove',e=>{onM(e.changedTouches[0].screenX)},{passive:true});el.addEventListener('touchend',()=>{onE()},{passive:true});el.addEventListener('mousedown',e=>{onS(e.screenX)});el.addEventListener('mousemove',e=>{onM(e.screenX)});el.addEventListener('mouseup',()=>{onE()});el.addEventListener('mouseleave',()=>{if(dr)onE()});el.addEventListener('click',e=>{if(sw){e.stopPropagation();sw=false;return}const id=el.dataset.id,email=st.emails.find(e=>e.id===id);if(email){st.curEmail=email;st.view='edetail';loadDetail(id)}})})}\nfunction removeEmailEl(id){const el=co.querySelector('.email-item[data-id=\"'+id+'\"]');if(el)el.style.display='none'}\nasync function delEmail(id,el){const email=st.emails.find(e=>e.id===id);if(!email)return;st.emails=st.emails.filter(e=>e.id!==id);removeEmailEl(id);toast('πŸ—‘οΈ Deleting...','');const a=actv();if(!a)return;const backup=JSON.parse(JSON.stringify(email));const isTrash=st.mboxId===a.trashMailboxId;try{if(!a.trashMailboxId){toast('πŸ—‘οΈ Removed','success');return}if(isTrash){await japi('Email/set',{accountId:a.accountId,destroy:[id]});toast('πŸ—‘οΈ Permanently deleted','success')}else{const fr=await japi('Email/get',{accountId:a.accountId,ids:[id],bodyProperties:['textBody','htmlBody'],fetchTextBodyValues:true,fetchHTMLBodyValues:true});const fe=fr.methodResponses[0][1].list?.[0];if(!fe)throw new Error('Could not fetch email data');let bt='';const tb=fe.textBody?.[0];const hb=fe.htmlBody?.[0];if(tb&&fe.bodyValues&&fe.bodyValues[tb.partId])bt=fe.bodyValues[tb.partId].value;if(!bt&&hb&&fe.bodyValues&&fe.bodyValues[hb.partId])bt=fe.bodyValues[hb.partId].value;await japi('Email/set',{accountId:a.accountId,destroy:[id]});const rd={mailboxIds:{[a.trashMailboxId]:true},from:fe.from||email.from,to:fe.to||email.to,cc:fe.cc||email.cc,bcc:fe.bcc||email.bcc,subject:fe.subject||email.subject||'(no subject)',receivedAt:fe.receivedAt||email.receivedAt,keywords:fe.keywords||email.keywords||{},textBody:bt?[{partId:'body-0',type:'text/plain'}]:undefined,bodyValues:{}};if(bt)rd.bodyValues['body-0']={value:bt,charset:'UTF-8'};if(!rd.cc)delete rd.cc;if(!rd.bcc)delete rd.bcc;if(!rd.textBody)delete rd.textBody;if(Object.keys(rd.bodyValues).length===0)delete rd.bodyValues;const cr=await japi('Email/set',{accountId:a.accountId,create:{'ri':rd}});if(!cr.methodResponses[0][1].created?.['ri']){const err=cr.methodResponses[0][1].notCreated?.['ri'];toast('Email destroyed but re-import failed: '+(err?.description||'unknown'),'error')}else{toast('πŸ—‘οΈ Moved to trash','success')}}}catch(e){st.emails.push(backup);st.emails.sort((a,b)=>new Date(b.receivedAt)-new Date(a.receivedAt));if(st.view==='emails')renderEmails();toast('Failed to delete: '+e.message,'error')}}\nasync function loadDetail(id){st.loading=true;renderDetail();const a=actv();if(!a){st.loading=false;render();return}try{const r=await japi('Email/get',{accountId:a.accountId,ids:[id],bodyProperties:['textBody','htmlBody'],fetchTextBodyValues:true,fetchHTMLBodyValues:true,maxBodyValueLength:10485760});const email=r.methodResponses[0][1].list?.[0];if(email){st.curEmail=email;const tb=email.textBody?.[0];const hb=email.htmlBody?.[0];let fb=null;if(email.bodyValues){if(tb&&email.bodyValues[tb.partId]){fb=email.bodyValues[tb.partId].value}if(!fb&&hb&&email.bodyValues[hb.partId]){fb=email.bodyValues[hb.partId].value}}if(!fb&&tb&&tb.blobId&&a.downloadUrlTpl){try{const dl=a.downloadUrlTpl.replace('{accountId}',encodeURIComponent(a.accountId)).replace('{blobId}',tb.blobId).replace('{name}','body.txt').replace('?type={type}','?type=text/plain');const dr=await fetch(dl,{headers:{'Authorization':'Basic '+btoa(a.credentials.username+':'+a.credentials.password)}});if(dr.ok)fb=await dr.text()}catch(e){}}if(!fb&&hb&&hb.blobId&&a.downloadUrlTpl){try{const dl=a.downloadUrlTpl.replace('{accountId}',encodeURIComponent(a.accountId)).replace('{blobId}',hb.blobId).replace('{name}','body.html').replace('?type={type}','?type=text/html');const dr=await fetch(dl,{headers:{'Authorization':'Basic '+btoa(a.credentials.username+':'+a.credentials.password)}});if(dr.ok)fb=await dr.text()}catch(e){}}if(fb)st.curEmail._fb=fb}st.loading=false;renderDetail();try{await japi('Email/set',{accountId:a.accountId,update:{[id]:{keywords:{$seen:true}}}})}catch(e){}}catch(e){st.loading=false;toast('Failed to load: '+e.message,'error');renderDetail()}}\nfunction renderDetail(){if(st.loading){co.innerHTML='<div class=\"loading\"><div class=\"spinner\"></div><p>Loading message...</p></div>';return}const email=st.curEmail;if(!email){co.innerHTML='<div class=\"empty-state\"><div class=\"icon\">πŸ’¬</div><h3>No message</h3></div>';return}const from=email.from?.map(f=>f.name||f.email).join(', ')||'Unknown',to=email.to?.map(t=>t.name||t.email).join(', ')||'Unknown',date=email.receivedAt?new Date(email.receivedAt).toLocaleString():'Unknown',subj=email.subject||'(No subject)';let body='';const fb=email._fb;if(fb){body=escape(fb).replace(/\\n/g,'<br>')}else if(email.htmlBody?.[0]?.value){body=email.htmlBody[0].value}else if(email.textBody?.[0]?.value){body=escape(email.textBody[0].value).replace(/\\n/g,'<br>')}else if(email.preview){body=escape(email.preview)}else{body='<p style=\"color:#8b949e\">(No content)</p>'}co.innerHTML='<div class=\"email-detail\"><div class=\"email-detail-subject\">'+escape(subj)+'</div><div class=\"email-detail-from\">πŸ“¬ <strong>From:</strong> '+escape(from)+'</div><div class=\"email-detail-from\">πŸ“¨ <strong>To:</strong> '+escape(to)+'</div><div class=\"email-detail-from\">πŸ• '+escape(date)+'</div>'+(email.hasAttachment?'<div style=\"margin-top:6px;color:#d29922\">πŸ“Ž Has attachments</div>':'')+'<div class=\"email-detail-body\">'+body+'</div><div class=\"email-detail-actions\"><button class=\"act-btn reply\" id=\"rpBtn\">↩️ Reply</button><button class=\"act-btn\" id=\"raBtn\">β†ͺ️ Reply All</button><button class=\"act-btn\" id=\"fwBtn\">➑️ Forward</button></div></div>';$('rpBtn')?.addEventListener('click',()=>openReply(email));$('raBtn')?.addEventListener('click',()=>openReplyAll(email));$('fwBtn')?.addEventListener('click',()=>openForward(email))}\nfunction openComp(pref){st.comp={to:pref.to||'',cc:pref.cc||'',subj:pref.subj||'',body:pref.body||'',replyId:pref.replyId||null};st.compExp=false;st.view='compose';render()}\nfunction openReply(email){openComp({to:email.from?.[0]?.email||'',subj:email.subject?'Re: '+email.subject.replace(/^Re:\\s*/i,''):'Re: ',body:'\\n\\n\\n--- Original message ---\\n'+(email.preview||''),replyId:email.id})}\nfunction openReplyAll(email){const to=email.from?.map(f=>f.email).join(', ')||'',cc=email.to?.map(t=>t.email).filter(e=>e!==(actv()?.credentials?.username||'')).join(', ')||'';openComp({to,cc,subj:email.subject?'Re: '+email.subject.replace(/^Re:\\s*/i,''):'Re: ',body:'\\n\\n\\n--- Original message ---\\n'+(email.preview||''),replyId:email.id})}\nfunction openForward(email){openComp({subj:email.subject?'Fwd: '+email.subject.replace(/^Fwd:\\s*/i,''):'Fwd: ',body:'\\n\\n\\n--- Forwarded message ---\\n'+(email.preview||''),replyId:email.id})}\nfunction renderComp(){const d=st.comp,a=actv();const ue=a?.credentials?.username||'',idn=a?.identities?.[0]?.name||a?.identities?.[0]?.email||ue;if(st.compExp){co.innerHTML='<div class=\"compose-form\"><div class=\"compose-fields expanded\"><div class=\"ce\" id=\"ce\"></div><textarea id=\"cb\" placeholder=\"Write your message...\">'+escape(d.body)+'</textarea></div><div class=\"compose-bar\"><button class=\"reduce-btn\" id=\"rdBtn\">β–½ Reduce</button></div></div>';$('rdBtn').addEventListener('click',()=>{st.comp.body=$('cb').value;st.compExp=false;render()});$('cb').addEventListener('keydown',e=>{if((e.ctrlKey||e.metaKey)&&e.key==='Enter')handleSend()});$('cb').focus()}else{co.innerHTML='<div class=\"compose-form\"><div class=\"compose-fields\"><div class=\"ce\" id=\"ce\"></div><div class=\"ch\"><div class=\"compose-row\"><label for=\"ct\">To</label><input type=\"text\" id=\"ct\" placeholder=\"recipient@example.com\" value=\"'+escape(d.to)+'\"></div><div class=\"compose-row\"><label for=\"cc\">CC</label><input type=\"text\" id=\"cc\" placeholder=\"cc@example.com\" value=\"'+escape(d.cc)+'\"></div><div class=\"compose-row\"><label for=\"cs\">Subj</label><input type=\"text\" id=\"cs\" placeholder=\"Subject\" value=\"'+escape(d.subj)+'\"></div></div><textarea id=\"cb\" placeholder=\"Write your message...\">'+escape(d.body)+'</textarea></div></div>';$('cb').addEventListener('focus',()=>{st.comp.to=$('ct').value;st.comp.cc=$('cc').value;st.comp.subj=$('cs').value;st.comp.body=$('cb').value;st.compExp=true;render()});$('cb').addEventListener('keydown',e=>{if((e.ctrlKey||e.metaKey)&&e.key==='Enter')handleSend()});if(!d.to)$('ct').focus();else if(!d.subj)$('cs').focus();else $('cb').focus()}}\nasync function handleSend(){let toRaw,ccRaw,subj,body;const a=actv();if(st.compExp){toRaw=st.comp.to;ccRaw=st.comp.cc;subj=st.comp.subj;body=$('cb').value}else{toRaw=$('ct').value.trim();ccRaw=$('cc').value.trim();subj=$('cs').value.trim();body=$('cb').value}const err=$('ce'),btn=sh;if(!toRaw){err.textContent='Please enter at least one recipient.';err.style.display='block';return}err.style.display='none';btn.disabled=true;btn.textContent='πŸ“€ Sending...';sh.disabled=true;sh.textContent='πŸ“€ Sending...';try{const toA=toRaw.split(',').map(s=>s.trim()).filter(s=>s).map(e=>({email:e})),ccA=ccRaw.split(',').map(s=>s.trim()).filter(s=>s).map(e=>({email:e})),ue=a.credentials.username;if(a.identities.length===0)await loadIdentities();let idn=a.identities.find(i=>i.email===ue);if(!idn&&a.identities.length>0)idn=a.identities[0];if(!idn)throw new Error('No identity found on server.');if(!a.sendMailboxId)throw new Error('No mailbox available.');const ec={mailboxIds:{[a.sendMailboxId]:true},keywords:{$seen:false},from:[{email:idn.email||ue}],to:toA,subject:subj||'(no subject)',textBody:[{partId:'body-0',type:'text/plain'}],bodyValues:{'body-0':{value:body,charset:'UTF-8'}}};if(ccA.length>0)ec.cc=ccA;if(st.comp.replyId)ec.inReplyTo=st.comp.replyId;const sa=a.submissionAccountId||a.accountId,allR=[...toA];if(ccA.length>0)allR.push(...ccA);const mr=await jmulti([['Email/set',{accountId:a.accountId,create:{'ne':ec}},'0'],['EmailSubmission/set',{accountId:sa,create:{'ns':{emailId:'#ne',identityId:idn.id,envelope:{mailFrom:{email:idn.email||ue},rcptTo:allR}}}},'1']]);const esr=mr.methodResponses[0][1],cr=esr.created?.['ne'];if(!cr){const er=esr.notCreated?.['ne'];throw new Error(er?.description||'Failed to create email')}const sr=mr.methodResponses[1][1],sub=sr.created?.['ns'];if(!sub){const er=sr.notCreated?.['ns'],ed=er?(er.description||er.type||JSON.stringify(er)):'Unknown';if(st.comp.replyId&&(ed.includes('oldEmail')||ed.includes('inReplyTo'))){delete ec.inReplyTo;const rr=await jmulti([['Email/set',{accountId:a.accountId,create:{'ne':ec}},'0'],['EmailSubmission/set',{accountId:sa,create:{'ns':{emailId:'#ne',identityId:idn.id,envelope:{mailFrom:{email:idn.email||ue},rcptTo:allR}}}},'1']]);const rs=rr.methodResponses[1][1].created?.['ns'];if(rs){toast('βœ… Email sent!','success');st.view='mailboxes';st.compExp=false;render();return}throw new Error('SMTP rejected: '+(rr.methodResponses[1][1].notCreated?.['ns']?.description||ed))}throw new Error('SMTP rejected: '+ed)}toast('βœ… Email sent!','success');st.view='mailboxes';st.compExp=false;render()}catch(e){err.textContent='Failed to send: '+e.message;err.style.display='block';btn.disabled=false;btn.textContent='πŸ“€ Send';sh.disabled=false;sh.textContent='πŸ“€ Send'}btn.disabled=false;btn.textContent='πŸ“€ Send';sh.disabled=false;sh.textContent='πŸ“€ Send'}\nasync function init(){load();if(st.accounts.length>0&&st.activeId){const a=actv();if(a){st.view='mailboxes';render();await loadIdentities();await loadMailboxes()}else{st.view='accounts';render()}}else{st.view='accounts';render()}}\ninit();\n</script>\n</body>\n</html>"
},
"currentHash": "4043515350a389903fbdfc783b07d30f92110a9ac405ef14ae94f3a713280c5c",
"previousHash": "08724e129ad34d60bbd8ad5aa8ce065bde0ca8d7732ed3fcc4125d0af8c1d86b",
"prompt": "remove account label, we use the username as label."
},
"3": {
"tab": {
"id": "jmap-email",
"label": "Email",
"html": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"UTF-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, viewport-fit=cover\">\n<title>JMAP Email</title>\n<style>\nhtml,body{margin:0;height:100%;background:#0d1117;color:#c9d1d9;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif}\n*{box-sizing:border-box}\n.app{display:flex;flex-direction:column;height:100%;max-width:600px;margin:0 auto}\n.header{padding:10px 12px;border-bottom:1px solid #30363d;background:#161b22;display:flex;align-items:center;gap:6px;min-height:52px}\n.header h1{margin:0;font-size:17px;font-weight:600;flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;cursor:pointer;-webkit-user-select:none;user-select:none}\n.header h1:active{opacity:.7}\n.header .back-btn{background:none;border:none;color:#58a6ff;font-size:20px;cursor:pointer;padding:8px;min-width:44px;min-height:44px;display:flex;align-items:center;justify-content:center}\n.header .hdr-btn{background:none;border:1px solid #30363d;padding:6px 10px;border-radius:6px;cursor:pointer;font-size:13px;min-height:36px;display:flex;align-items:center;gap:3px;white-space:nowrap}\n.header .hdr-btn.primary{background:#238636;color:#fff;border:none;font-weight:600}\n.header .hdr-btn:disabled{opacity:.5;cursor:not-allowed}\n.header .hdr-btn.danger{color:#f85149}\n.header .hdr-btn.acct{color:#58a6ff;max-width:100px;overflow:hidden;text-overflow:ellipsis}\n.content{flex:1;overflow-y:auto;padding:16px}\n.content::-webkit-scrollbar{width:6px}\n.content::-webkit-scrollbar-thumb{background:#30363d;border-radius:3px}\n.content.nopad{padding:0}\n.form-card{max-width:400px;margin:40px auto 0;padding:20px;background:#161b22;border:1px solid #30363d;border-radius:12px}\n.form-card h2{margin:0 0 16px;font-size:18px;text-align:center}\n.form-card label{display:block;margin-bottom:4px;color:#8b949e;font-size:13px}\n.form-card input{width:100%;padding:12px 14px;margin-bottom:14px;min-height:44px;background:#0d1117;border:1px solid #30363d;border-radius:8px;color:#c9d1d9;font-size:15px;outline:none}\n.form-card input:focus{border-color:#58a6ff}\n.form-card .btn-primary{width:100%;padding:12px;min-height:44px;font-size:15px;background:#238636;color:#fff;border:none;border-radius:8px;cursor:pointer;font-weight:600}\n.form-card .btn-primary:disabled{opacity:.5;cursor:not-allowed}\n.form-card .error{color:#f85149;font-size:13px;padding:8px;background:rgba(248,81,73,0.1);border-radius:6px;display:none}\n.acct-item{display:flex;align-items:center;padding:14px 16px;min-height:52px;border-bottom:1px solid #21262d;cursor:pointer;gap:12px}\n.acct-item:hover{background:#161b22}\n.acct-item .acct-icon{font-size:22px;width:40px;text-align:center}\n.acct-item .acct-info{flex:1}\n.acct-item .acct-name{font-size:15px;font-weight:500}\n.acct-item .acct-email{font-size:12px;color:#8b949e;margin-top:2px}\n.acct-item .acct-badge{background:#238636;color:#fff;border-radius:10px;padding:2px 8px;font-size:11px;font-weight:600}\n.acct-item .acct-del{background:none;border:none;color:#f85149;font-size:18px;cursor:pointer;padding:8px;min-width:44px;min-height:44px;display:flex;align-items:center;justify-content:center}\n.mailbox-item{display:flex;align-items:center;padding:14px 16px;min-height:52px;border-bottom:1px solid #21262d;cursor:pointer;gap:12px}\n.mailbox-item:hover{background:#161b22}\n.mailbox-icon{font-size:20px;width:36px;text-align:center}\n.mailbox-info{flex:1}\n.mailbox-name{font-size:15px;font-weight:500}\n.mailbox-stats{font-size:12px;color:#8b949e;margin-top:2px}\n.mailbox-unread{background:#238636;color:#fff;border-radius:12px;padding:2px 10px;font-size:12px;font-weight:600}\n.email-item{padding:14px 16px;min-height:60px;border-bottom:1px solid #21262d;cursor:pointer;position:relative;background:#0d1117}\n.email-item:hover{background:#161b22}\n.email-item.unread{border-left:3px solid #58a6ff}\n.email-item .del-bg{position:absolute;top:0;right:0;bottom:0;background:#f85149;display:flex;align-items:center;padding:0 20px;font-size:14px;font-weight:600;color:#fff;z-index:0}\n.email-item .ec{position:relative;z-index:1;background:#0d1117;transition:transform .2s ease;padding:0}\n.email-sender{font-size:14px;font-weight:600;color:#e6edf3;margin-bottom:4px}\n.email-subject{font-size:14px;color:#c9d1d9;margin-bottom:4px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n.email-preview{font-size:13px;color:#8b949e;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n.email-date{font-size:11px;color:#8b949e;margin-top:4px}\n.email-item.unread .email-subject{font-weight:600;color:#e6edf3}\n.email-detail{padding:16px}\n.email-detail-subject{font-size:17px;font-weight:600;margin-bottom:10px}\n.email-detail-from{font-size:13px;color:#8b949e;margin-bottom:3px}\n.email-detail-body{padding:12px 0;line-height:1.6;font-size:14px;word-wrap:break-word}\n.email-detail-actions{display:flex;gap:8px;padding:8px 0;border-top:1px solid #30363d}\n.email-detail-actions .act-btn{flex:1;padding:10px;min-height:44px;border:1px solid #30363d;border-radius:8px;background:none;color:#c9d1d9;font-size:14px;cursor:pointer}\n.email-detail-actions .act-btn:active{background:#161b22}\n.email-detail-actions .reply{color:#58a6ff}\n.compose-form{display:flex;flex-direction:column;height:100%}\n.compose-fields{padding:14px;display:flex;flex-direction:column;height:100%}\n.compose-fields label{display:block;margin-bottom:4px;color:#8b949e;font-size:13px;font-weight:500}\n.compose-fields input,.compose-fields textarea{padding:10px 12px;min-height:44px;background:#0d1117;border:1px solid #30363d;border-radius:8px;color:#c9d1d9;font-size:15px;outline:none;font-family:inherit}\n.compose-row{display:flex;align-items:center;gap:8px;margin-bottom:10px}\n.compose-row label{flex-shrink:0;width:auto;min-width:48px;margin-bottom:0}\n.compose-row input{flex:1;min-width:0}\n.compose-fields textarea{min-height:180px;resize:vertical}\n.compose-fields .send-btn{width:100%;padding:12px;min-height:44px;font-size:15px;background:#238636;color:#fff;border:none;border-radius:8px;cursor:pointer;font-weight:600}\n.compose-fields .send-btn:disabled{opacity:.5;cursor:not-allowed}\n.compose-fields .ce{color:#f85149;font-size:13px;padding:8px;background:rgba(248,81,73,0.1);border-radius:6px;display:none;margin-bottom:10px}\n.compose-fields.expanded{padding:0}\n.compose-fields.expanded .ch,.compose-fields.expanded .cfb{display:none}\n.compose-fields.expanded .ce{margin:8px 8px 0}\n.compose-fields.expanded #cb{min-height:0;flex:1;margin-bottom:0;border-radius:0;border-left:none;border-right:none;border-top:1px solid #30363d;resize:none;padding:16px;font-size:16px}\n.compose-bar{display:flex;gap:8px;padding:10px 12px;border-top:1px solid #30363d;background:#161b22}\n.compose-bar .send-btn{flex:1;padding:12px;min-height:48px;font-size:15px;background:#238636;color:#fff;border:none;border-radius:8px;cursor:pointer;font-weight:600}\n.compose-bar .send-btn:disabled{opacity:.5;cursor:not-allowed}\n.compose-bar .reduce-btn{min-width:80px;padding:12px;min-height:48px;font-size:14px;background:none;color:#8b949e;border:1px solid #30363d;border-radius:8px;cursor:pointer}\n.loading{text-align:center;padding:40px 16px;color:#8b949e}\n.loading .spinner{display:inline-block;width:24px;height:24px;border:3px solid #30363d;border-top-color:#58a6ff;border-radius:50%;animation:spin .8s linear infinite;margin-bottom:12px}\n@keyframes spin{to{transform:rotate(360deg)}}\n.empty-state{text-align:center;padding:60px 20px;color:#8b949e}\n.empty-state .icon{font-size:48px;margin-bottom:16px}\n.empty-state h3{font-size:18px;margin:0 0 8px;color:#c9d1d9}\n.status-bar{padding:6px 12px;background:#161b22;border-top:1px solid #30363d;font-size:11px;color:#8b949e;display:flex;justify-content:space-between;min-height:32px;align-items:center}\n.status-bar.hidden{display:none}\n.toast{position:fixed;bottom:80px;left:50%;transform:translateX(-50%);background:#161b22;border:1px solid #30363d;padding:10px 20px;border-radius:8px;font-size:14px;color:#c9d1d9;z-index:100;display:none;max-width:90%;text-align:center}\n.toast.error{border-color:#f85149;background:rgba(248,81,73,0.15)}\n.toast.success{border-color:#238636;background:rgba(35,134,54,0.15)}\n.ptr-indicator{position:absolute;top:0;left:0;width:100%;color:#8b949e;font-size:14px;text-align:center;padding:12px 0;pointer-events:none;z-index:5;display:none}\n.ptr-indicator.visible{display:block}\n@media(max-width:480px){.form-card{margin:20px 8px 0;padding:16px}.content{padding:0}.header{padding:8px 10px;gap:4px}.header .hdr-btn{padding:5px 7px;font-size:12px}.compose-fields{padding:10px}.compose-fields.expanded{padding:0}}\n</style>\n</head>\n<body>\n<div class=\"app\" id=\"app\">\n <div class=\"header\" id=\"header\">\n <button class=\"back-btn\" id=\"backBtn\" style=\"display:none\">←</button>\n <h1 id=\"title\">πŸ“§ Email</h1>\n <button class=\"hdr-btn acct\" id=\"acctBtn\" style=\"display:none\">πŸ‘€</button>\n <button class=\"hdr-btn\" id=\"addAcctHdrBtn\" style=\"display:none\">βž•</button>\n <button class=\"hdr-btn\" id=\"composeBtn\" style=\"display:none\">✏️</button>\n <button class=\"hdr-btn\" id=\"refreshBtn\" style=\"display:none\">πŸ”„</button>\n <button class=\"hdr-btn primary\" id=\"sendHdrBtn\" style=\"display:none\">πŸ“€ Send</button>\n <button class=\"hdr-btn danger\" id=\"logoutBtn\" style=\"display:none\">Exit</button>\n </div>\n <div class=\"content\" id=\"content\"><div class=\"ptr-indicator\" id=\"ptrI\"><span id=\"ptrT\">⬇️ Hold & pull down to refresh</span></div></div>\n <div class=\"status-bar\" id=\"statusBar\"><span id=\"stLeft\">Not connected</span><span id=\"stRight\">jmap</span></div>\n</div>\n<div class=\"toast\" id=\"toast\"></div>\n<script>\nconst SK='msw_tab_jmap_email';\nlet st={accounts:[],activeId:null,view:'accounts',mboxId:null,mboxName:'',emails:[],curEmail:null,comp:{to:'',cc:'',subj:'',body:'',replyId:null},compExp:false,loading:false};\nconst $=id=>document.getElementById(id),co=$('content'),ti=$('title'),bb=$('backBtn'),ab=$('acctBtn'),ab2=$('addAcctHdrBtn'),cb=$('composeBtn'),rb=$('refreshBtn'),lb=$('logoutBtn'),sh=$('sendHdrBtn'),sb=$('statusBar'),sl=$('stLeft'),sr=$('stRight'),to=$('toast');\nlet tt=null;\nfunction toast(msg,type){to.textContent=msg;to.className='toast '+(type||'');to.style.display='block';clearTimeout(tt);tt=setTimeout(()=>{to.style.display='none'},4000)}\nfunction acct(){return st.accounts.find(a=>a.id===st.activeId)}\nfunction save(){try{localStorage.setItem(SK,JSON.stringify({accounts:st.accounts.map(a=>{const{mailboxes,identities,mailboxMap,sendMailboxId,trashMailboxId,...rest}=a;return{...rest,mailboxes:mailboxes.map(m=>({id:m.id,name:m.name,totalEmails:m.totalEmails||m.totalMessages||0,unreadEmails:m.unreadEmails||m.unreadMessages||0,sortOrder:m.sortOrder||0})),identities:identities.map(i=>({id:i.id,email:i.email,name:i.name})),mailboxMap,smb:sendMailboxId,tmb:trashMailboxId}}),activeId:st.activeId}))}catch(e){}}\nfunction load(){try{const r=localStorage.getItem(SK);if(!r){st.accounts=[];st.activeId=null;return}const d=JSON.parse(r);if(!d||typeof d!=='object'||!Array.isArray(d.accounts)){st.accounts=[];st.activeId=null;return}st.accounts=d.accounts.map(a=>{a.mailboxMap=a.mailboxMap||{};a.sendMailboxId=a.smb||null;a.trashMailboxId=a.tmb||null;delete a.smb;delete a.tmb;return a});st.activeId=(d.activeId&&st.accounts.find(a=>a.id===d.activeId))?d.activeId:st.accounts.length>0?st.accounts[0].id:null}catch(e){st.accounts=[];st.activeId=null}}\nfunction actv(){return acct()||(st.accounts[0]||null)}\nfunction selAcct(id){st.activeId=id;save();st.view='mailboxes';render();loadMailboxes()}\nfunction goAccts(){st.view='accounts';render()}\nfunction goFolders(){if(st.view==='emails'||st.view==='edetail'){st.view='mailboxes';render()}}\nfunction goBack(){if(st.view==='edetail'){st.view='emails';render()}else if(st.view==='emails'||st.view==='compose'){st.view='mailboxes';render()}else if(st.view==='mailboxes'){st.view='accounts';render()}}\nfunction upHdr(){const notAccts=st.view!=='accounts';bb.style.display=(st.view==='accounts'||st.view==='compose'||st.view==='login')?'none':'flex';ab.style.display=notAccts?'flex':'none';ab2.style.display=notAccts?'flex':'none';cb.style.display=notAccts&&st.view!=='compose'?'flex':'none';rb.style.display=notAccts&&st.view!=='compose'?'flex':'none';lb.style.display=notAccts?'flex':'none';sh.style.display=st.view==='compose'?'flex':'none';const a=actv();ab.textContent=a?'πŸ‘€ '+((a.credentials?.username||a.label||'').substring(0,8)):'πŸ‘€';switch(st.view){case'accounts':ti.textContent='πŸ“§ Accounts';break;case'login':ti.textContent='βž• Add Account';break;case'mailboxes':ti.textContent='πŸ“‚ '+((a||{}).credentials?.username||a?.label||'Folders');break;case'emails':ti.textContent='πŸ“¨ '+st.mboxName;break;case'edetail':ti.textContent='πŸ’¬ Message';break;case'compose':const t=st.comp.to;ti.textContent=t?'βœ‰οΈ '+t.split(',')[0].trim():'✏️ New';break}}\nfunction upSb(){if(st.view==='compose'){sb.classList.add('hidden')}else{sb.classList.remove('hidden');const a=actv();if(a){sl.textContent=a.credentials?.username||a.label||'Logged in';sr.textContent=new Date().toLocaleTimeString([],{hour:'2-digit',minute:'2-digit'})}else{sl.textContent='No account';sr.textContent='jmap'}}}\nfunction render(){co.className='content'+(st.view==='compose'?' nopad':'');switch(st.view){case'accounts':renderAccts();break;case'login':renderLogin();break;case'mailboxes':renderMboxes();break;case'emails':renderEmails();break;case'edetail':renderDetail();break;case'compose':renderComp();break}upHdr();upSb()}\nfunction escape(str){if(!str)return'';const d=document.createElement('div');d.textContent=str;return d.innerHTML}\nti.addEventListener('click',goFolders);bb.addEventListener('click',goBack);\nlb.addEventListener('click',()=>{st.activeId=null;st.view='accounts';st.curEmail=null;st.emails=[];render();toast('Switched to account list','success')});\nab.addEventListener('click',goAccts);\nab2.addEventListener('click',()=>{st.view='login';render()});\ncb.addEventListener('click',()=>openComp({}));\nrb.addEventListener('click',async()=>{if(st.loading)return;toast('πŸ”„ Refreshing...','');if(st.view==='mailboxes'){await loadMailboxes()}else if(st.view==='emails'){await loadEmails()}else if(st.view==='edetail'&&st.curEmail){await loadDetail(st.curEmail.id)}});\nsh.addEventListener('click',handleSend);\nasync function jmapFetch(path,opts){opts=opts||{};const a=actv();if(!a)throw new Error('No active account');const url=path.startsWith('http')?path:a.serverUrl+path;const h={'Content-Type':'application/json','Accept':'application/json',...opts.headers};if(a.credentials){if(a.credentials.token)h['Authorization']='Bearer '+a.credentials.token;else if(a.credentials.username&&a.credentials.password)h['Authorization']='Basic '+btoa(a.credentials.username+':'+a.credentials.password)}const res=await fetch(url,{method:opts.method||'GET',headers:h,body:opts.body?JSON.stringify(opts.body):undefined});if(!res.ok){let d='';try{const e=await res.json();d=e.detail||e.type||JSON.stringify(e)}catch(e){d=res.statusText}throw new Error('HTTP '+res.status+': '+d)}const t=await res.text();return t?JSON.parse(t):null}\nasync function japi(method,params){params=params||{};const a=actv();if(!a.apiUrl)throw new Error('No API URL');const r=await jmapFetch(a.apiUrl,{method:'POST',body:{using:['urn:ietf:params:jmap:core','urn:ietf:params:jmap:mail','urn:ietf:params:jmap:submission'],methodCalls:[[method,params,'0']]}});return r}\nasync function jmulti(calls){const a=actv();if(!a.apiUrl)throw new Error('No API URL');const r=await jmapFetch(a.apiUrl,{method:'POST',body:{using:['urn:ietf:params:jmap:core','urn:ietf:params:jmap:mail','urn:ietf:params:jmap:submission'],methodCalls}});return r}\nfunction renderAccts(){if(st.accounts.length===0){co.innerHTML='<div class=\"empty-state\"><div class=\"icon\">πŸ“§</div><h3>No email accounts</h3><p>Add an account to get started.</p><button class=\"btn-primary\" style=\"margin-top:16px;padding:12px 24px;background:#238636;color:#fff;border:none;border-radius:8px;cursor:pointer;font-size:15px;min-height:44px\" id=\"addFirstBtn\">βž• Add Account</button></div>';$('addFirstBtn')?.addEventListener('click',()=>{st.view='login';render()});return}let html='';for(const a of st.accounts){const isActive=a.id===st.activeId;const badge=isActive?'<span class=\"acct-badge\">Active</span>':'';const email=a.credentials?.username||'';html+='<div class=\"acct-item\" data-id=\"'+a.id+'\"><div class=\"acct-icon\">πŸ“§</div><div class=\"acct-info\"><div class=\"acct-name\">'+escape(email)+'</div></div>'+badge+'<button class=\"acct-del\" data-del=\"'+a.id+'\">βœ•</button></div>'}html+='<div class=\"acct-item\" id=\"addAcctBtn\" style=\"color:#58a6ff;justify-content:center;font-weight:500;gap:6px;border-bottom:none\"><span style=\"font-size:20px\">βž•</span> Add Account</div>';co.innerHTML=html;co.querySelectorAll('.acct-item[data-id]').forEach(el=>{el.addEventListener('click',e=>{if(e.target.closest('.acct-del'))return;selAcct(el.dataset.id)})});co.querySelectorAll('.acct-del').forEach(el=>{el.addEventListener('click',e=>{e.stopPropagation();const id=el.dataset.del;if(st.accounts.length<=1){st.accounts=[];st.activeId=null;save();render();return}if(!confirm('Remove \"'+(st.accounts.find(a=>a.id===id)?.credentials?.username||'')+'\"?'))return;st.accounts=st.accounts.filter(a=>a.id!==id);if(st.activeId===id)st.activeId=st.accounts[0]?.id||null;save();render()})});$('addAcctBtn')?.addEventListener('click',()=>{st.view='login';render()})}\nfunction renderLogin(){co.innerHTML='<div class=\"form-card\"><h2>βž• Add Account</h2><label for=\"srvUrl\">JMAP Server</label><input type=\"text\" id=\"srvUrl\" placeholder=\"https://jmap.example.com\" value=\"https://jmap.gakoy.com\"><label for=\"uname\">Username (Email)</label><input type=\"text\" id=\"uname\" placeholder=\"your@email.com\" autocomplete=\"username\"><label for=\"pwd\">Password</label><input type=\"password\" id=\"pwd\" placeholder=\"β€’β€’β€’β€’β€’β€’β€’β€’\" autocomplete=\"current-password\"><button class=\"btn-primary\" id=\"loginBtn\">Add Account & Sign In</button><div class=\"error\" id=\"loginErr\"></div></div>';$('loginBtn').addEventListener('click',handleLogin);$('srvUrl').addEventListener('keydown',e=>{if(e.key==='Enter')$('uname').focus()});$('uname').addEventListener('keydown',e=>{if(e.key==='Enter')$('pwd').focus()});$('pwd').addEventListener('keydown',e=>{if(e.key==='Enter')handleLogin()});$('srvUrl').focus()}\nasync function handleLogin(){const srv=$('srvUrl').value.trim()||'https://jmap.gakoy.com',uname=$('uname').value.trim(),pwd=$('pwd').value,err=$('loginErr'),btn=$('loginBtn');if(!srv||!uname||!pwd){err.textContent='Please fill in all fields.';err.style.display='block';return}err.style.display='none';btn.disabled=true;btn.textContent='Signing in...';try{const id='acc_'+Date.now();const creds={username:uname,password:pwd};const session=await jmapFetchFrom(srv,creds,'/.well-known/jmap');const a={id,label:uname,serverUrl:srv,credentials:creds,session,apiUrl:session.apiUrl,downloadUrlTpl:session.downloadUrl,accountId:session.primaryAccounts['urn:ietf:params:jmap:mail'],submissionAccountId:session.primaryAccounts['urn:ietf:params:jmap:submission'],identities:[],mailboxes:[],mailboxMap:{},sendMailboxId:null,trashMailboxId:null};st.accounts.push(a);st.activeId=a.id;save();st.view='mailboxes';render();toast('βœ… Account added!','success');await loadIdentities();await loadMailboxes()}catch(e){err.textContent='Failed: '+e.message;err.style.display='block';btn.disabled=false;btn.textContent='Add Account & Sign In'}}\nasync function jmapFetchFrom(srv,creds,path){const url=srv+path;const h={'Content-Type':'application/json','Accept':'application/json'};if(creds.token)h['Authorization']='Bearer '+creds.token;else if(creds.username&&creds.password)h['Authorization']='Basic '+btoa(creds.username+':'+creds.password);const res=await fetch(url,{headers:h});if(!res.ok){let d='';try{const e=await res.json();d=e.detail||e.type||JSON.stringify(e)}catch(e){d=res.statusText}throw new Error('HTTP '+res.status+': '+d)}const t=await res.text();return t?JSON.parse(t):null}\nasync function loadIdentities(){const a=actv();if(!a)return;try{const sub=a.submissionAccountId||a.accountId;const r=await japi('Identity/get',{accountId:sub});a.identities=r.methodResponses[0][1].list||[];save()}catch(e){a.identities=[]}}\nasync function loadMailboxes(){st.loading=true;renderMboxes();const a=actv();if(!a){st.loading=false;render();return}try{const r=await japi('Mailbox/get',{accountId:a.accountId});a.mailboxes=r.methodResponses[0][1].list||[];a.mailboxMap={};let o=null,s=null,d=null,tr=null,f=null;for(const mb of a.mailboxes){a.mailboxMap[mb.id]=mb.name;const n=(mb.name||'').toLowerCase(),ro=(mb.role||'').toLowerCase();if(n==='outbox'||ro==='outbox')o=mb.id;if(n==='sent'||ro==='sent')s=mb.id;if(n==='drafts'||ro==='drafts')d=mb.id;if(n==='trash'||n==='deleted messages'||n==='bin'||ro==='trash')tr=mb.id;if(!f)f=mb.id}a.sendMailboxId=o||s||d||f;a.trashMailboxId=tr;save();st.loading=false;renderMboxes()}catch(e){st.loading=false;toast('Failed to load mailboxes: '+e.message,'error');renderMboxes()}}\nfunction renderMboxes(){if(st.loading){co.innerHTML='<div class=\"loading\"><div class=\"spinner\"></div><p>Loading mailboxes...</p></div>';return}const a=actv();if(!a||a.mailboxes.length===0){co.innerHTML='<div class=\"empty-state\"><div class=\"icon\">πŸ“­</div><h3>No mailboxes</h3></div>';return}const icons={'inbox':'πŸ“₯','sent':'πŸ“€','drafts':'πŸ“','trash':'πŸ—‘οΈ','spam':'⚠️','archive':'πŸ“¦','junk':'⚠️','outbox':'πŸ“€'};const sorted=[...a.mailboxes].sort((x,y)=>(x.sortOrder||0)-(y.sortOrder||0));let html='';for(const mb of sorted){const name=mb.name||'Unnamed',total=mb.totalEmails||mb.totalMessages||0,unread=mb.unreadEmails||mb.unreadMessages||0,icon=Object.entries(icons).find(([k])=>name.toLowerCase().includes(k))?.[1]||'πŸ“';html+='<div class=\"mailbox-item\" data-id=\"'+mb.id+'\"><div class=\"mailbox-icon\">'+icon+'</div><div class=\"mailbox-info\"><div class=\"mailbox-name\">'+escape(name)+'</div><div class=\"mailbox-stats\">'+total+' messages</div></div>'+(unread>0?'<div class=\"mailbox-unread\">'+unread+'</div>':'')+'</div>'}co.innerHTML=html;co.querySelectorAll('.mailbox-item').forEach(el=>{el.addEventListener('click',()=>{const id=el.dataset.id,mb=a.mailboxes.find(m=>m.id===id);if(mb){st.mboxId=id;st.mboxName=mb.name;st.view='emails';render();loadEmails()}})})}\nasync function loadEmails(){st.loading=true;renderEmails();const a=actv();if(!a){st.loading=false;render();return}try{const q=await japi('Email/query',{accountId:a.accountId,filter:{inMailbox:st.mboxId},sort:[{property:'receivedAt',isAscending:false}],limit:50});const ids=q.methodResponses[0][1].ids;if(!ids||ids.length===0){st.emails=[];st.loading=false;renderEmails();return}const g=await japi('Email/get',{accountId:a.accountId,ids,properties:['id','subject','from','to','receivedAt','hasAttachment','preview','size','keywords']});st.emails=g.methodResponses[0][1].list||[];st.loading=false;renderEmails()}catch(e){st.loading=false;toast('Failed to load emails: '+e.message,'error');renderEmails()}}\nfunction renderEmails(){if(st.loading){co.innerHTML='<div class=\"loading\"><div class=\"spinner\"></div><p>Loading emails...</p></div>';return}if(st.emails.length===0){co.innerHTML='<div class=\"empty-state\"><div class=\"icon\">πŸ“¨</div><h3>No emails</h3><p>This mailbox is empty.</p></div>';return}let html='<div class=\"email-list\">';for(const e of st.emails){const from=e.from?.[0]?.name||e.from?.[0]?.email||'Unknown',unread=!e.keywords?.$seen,date=new Date(e.receivedAt),ds=date.toLocaleDateString([],{month:'short',day:'numeric'}),ts=date.toLocaleTimeString([],{hour:'2-digit',minute:'2-digit'}),today=new Date().toDateString()===date.toDateString();html+='<div class=\"email-item'+(unread?' unread':'')+'\" data-id=\"'+e.id+'\"><div class=\"del-bg\">πŸ—‘οΈ Delete</div><div class=\"ec\"><div class=\"email-sender\">'+escape(from)+'</div><div class=\"email-subject\">'+escape(e.subject||'(No subject)')+'</div><div class=\"email-preview\">'+escape((e.preview||'').substring(0,100))+'</div><div class=\"email-date\">'+(today?ts:ds)+'</div></div></div>'}html+='</div>';co.innerHTML=html;co.querySelectorAll('.email-item').forEach(el=>{const ec=el.querySelector('.ec');let sx=0,cx=0,dr=false,sw=false;const dbg=el.querySelector('.del-bg');dbg.style.opacity='0';const onS=function(x){sx=x;cx=0;dr=true;sw=false;ec.classList.add('swiping');el.classList.add('swiping')};const onM=function(x){if(!dr)return;const dx=x-sx;if(dx<0){cx=0;ec.style.transform='translateX(0px)';return}cx=Math.min(dx,120);ec.style.transform='translateX('+cx+'px)';dbg.style.opacity=Math.min(cx/80,1)};const onE=function(){dr=false;ec.classList.remove('swiping');el.classList.remove('swiping');if(cx>60){sw=true;delEmail(el.dataset.id,el)}else{ec.style.transform='translateX(0px)';dbg.style.opacity='0'}};el.addEventListener('touchstart',e=>{onS(e.changedTouches[0].screenX)},{passive:true});el.addEventListener('touchmove',e=>{onM(e.changedTouches[0].screenX)},{passive:true});el.addEventListener('touchend',()=>{onE()},{passive:true});el.addEventListener('mousedown',e=>{onS(e.screenX)});el.addEventListener('mousemove',e=>{onM(e.screenX)});el.addEventListener('mouseup',()=>{onE()});el.addEventListener('mouseleave',()=>{if(dr)onE()});el.addEventListener('click',e=>{if(sw){e.stopPropagation();sw=false;return}const id=el.dataset.id,email=st.emails.find(e=>e.id===id);if(email){st.curEmail=email;st.view='edetail';loadDetail(id)}})})}\nfunction removeEmailEl(id){const el=co.querySelector('.email-item[data-id=\"'+id+'\"]');if(el)el.style.display='none'}\nasync function delEmail(id,el){const email=st.emails.find(e=>e.id===id);if(!email)return;st.emails=st.emails.filter(e=>e.id!==id);removeEmailEl(id);toast('πŸ—‘οΈ Deleting...','');const a=actv();if(!a)return;const backup=JSON.parse(JSON.stringify(email));const isTrash=st.mboxId===a.trashMailboxId;try{if(!a.trashMailboxId){toast('πŸ—‘οΈ Removed','success');return}if(isTrash){await japi('Email/set',{accountId:a.accountId,destroy:[id]});toast('πŸ—‘οΈ Permanently deleted','success')}else{const fr=await japi('Email/get',{accountId:a.accountId,ids:[id],bodyProperties:['textBody','htmlBody'],fetchTextBodyValues:true,fetchHTMLBodyValues:true});const fe=fr.methodResponses[0][1].list?.[0];if(!fe)throw new Error('Could not fetch email data');let bt='';const tb=fe.textBody?.[0];const hb=fe.htmlBody?.[0];if(tb&&fe.bodyValues&&fe.bodyValues[tb.partId])bt=fe.bodyValues[tb.partId].value;if(!bt&&hb&&fe.bodyValues&&fe.bodyValues[hb.partId])bt=fe.bodyValues[hb.partId].value;await japi('Email/set',{accountId:a.accountId,destroy:[id]});const rd={mailboxIds:{[a.trashMailboxId]:true},from:fe.from||email.from,to:fe.to||email.to,cc:fe.cc||email.cc,bcc:fe.bcc||email.bcc,subject:fe.subject||email.subject||'(no subject)',receivedAt:fe.receivedAt||email.receivedAt,keywords:fe.keywords||email.keywords||{},textBody:bt?[{partId:'body-0',type:'text/plain'}]:undefined,bodyValues:{}};if(bt)rd.bodyValues['body-0']={value:bt,charset:'UTF-8'};if(!rd.cc)delete rd.cc;if(!rd.bcc)delete rd.bcc;if(!rd.textBody)delete rd.textBody;if(Object.keys(rd.bodyValues).length===0)delete rd.bodyValues;const cr=await japi('Email/set',{accountId:a.accountId,create:{'ri':rd}});if(!cr.methodResponses[0][1].created?.['ri']){const err=cr.methodResponses[0][1].notCreated?.['ri'];toast('Email destroyed but re-import failed: '+(err?.description||'unknown'),'error')}else{toast('πŸ—‘οΈ Moved to trash','success')}}}catch(e){st.emails.push(backup);st.emails.sort((a,b)=>new Date(b.receivedAt)-new Date(a.receivedAt));if(st.view==='emails')renderEmails();toast('Failed to delete: '+e.message,'error')}}\nasync function loadDetail(id){st.loading=true;renderDetail();const a=actv();if(!a){st.loading=false;render();return}try{const r=await japi('Email/get',{accountId:a.accountId,ids:[id],bodyProperties:['textBody','htmlBody'],fetchTextBodyValues:true,fetchHTMLBodyValues:true,maxBodyValueLength:10485760});const email=r.methodResponses[0][1].list?.[0];if(email){st.curEmail=email;const tb=email.textBody?.[0];const hb=email.htmlBody?.[0];let fb=null;if(email.bodyValues){if(tb&&email.bodyValues[tb.partId]){fb=email.bodyValues[tb.partId].value}if(!fb&&hb&&email.bodyValues[hb.partId]){fb=email.bodyValues[hb.partId].value}}if(!fb&&tb&&tb.blobId&&a.downloadUrlTpl){try{const dl=a.downloadUrlTpl.replace('{accountId}',encodeURIComponent(a.accountId)).replace('{blobId}',tb.blobId).replace('{name}','body.txt').replace('?type={type}','?type=text/plain');const dr=await fetch(dl,{headers:{'Authorization':'Basic '+btoa(a.credentials.username+':'+a.credentials.password)}});if(dr.ok)fb=await dr.text()}catch(e){}}if(!fb&&hb&&hb.blobId&&a.downloadUrlTpl){try{const dl=a.downloadUrlTpl.replace('{accountId}',encodeURIComponent(a.accountId)).replace('{blobId}',hb.blobId).replace('{name}','body.html').replace('?type={type}','?type=text/html');const dr=await fetch(dl,{headers:{'Authorization':'Basic '+btoa(a.credentials.username+':'+a.credentials.password)}});if(dr.ok)fb=await dr.text()}catch(e){}}if(fb)st.curEmail._fb=fb}st.loading=false;renderDetail();try{await japi('Email/set',{accountId:a.accountId,update:{[id]:{keywords:{$seen:true}}}})}catch(e){}}catch(e){st.loading=false;toast('Failed to load: '+e.message,'error');renderDetail()}}\nfunction renderDetail(){if(st.loading){co.innerHTML='<div class=\"loading\"><div class=\"spinner\"></div><p>Loading message...</p></div>';return}const email=st.curEmail;if(!email){co.innerHTML='<div class=\"empty-state\"><div class=\"icon\">πŸ’¬</div><h3>No message</h3></div>';return}const from=email.from?.map(f=>f.name||f.email).join(', ')||'Unknown',to=email.to?.map(t=>t.name||t.email).join(', ')||'Unknown',date=email.receivedAt?new Date(email.receivedAt).toLocaleString():'Unknown',subj=email.subject||'(No subject)';let body='';const fb=email._fb;if(fb){body=escape(fb).replace(/\\n/g,'<br>')}else if(email.htmlBody?.[0]?.value){body=email.htmlBody[0].value}else if(email.textBody?.[0]?.value){body=escape(email.textBody[0].value).replace(/\\n/g,'<br>')}else if(email.preview){body=escape(email.preview)}else{body='<p style=\"color:#8b949e\">(No content)</p>'}co.innerHTML='<div class=\"email-detail\"><div class=\"email-detail-subject\">'+escape(subj)+'</div><div class=\"email-detail-from\">πŸ“¬ <strong>From:</strong> '+escape(from)+'</div><div class=\"email-detail-from\">πŸ“¨ <strong>To:</strong> '+escape(to)+'</div><div class=\"email-detail-from\">πŸ• '+escape(date)+'</div>'+(email.hasAttachment?'<div style=\"margin-top:6px;color:#d29922\">πŸ“Ž Has attachments</div>':'')+'<div class=\"email-detail-body\">'+body+'</div><div class=\"email-detail-actions\"><button class=\"act-btn reply\" id=\"rpBtn\">↩️ Reply</button><button class=\"act-btn\" id=\"raBtn\">β†ͺ️ Reply All</button><button class=\"act-btn\" id=\"fwBtn\">➑️ Forward</button></div></div>';$('rpBtn')?.addEventListener('click',()=>openReply(email));$('raBtn')?.addEventListener('click',()=>openReplyAll(email));$('fwBtn')?.addEventListener('click',()=>openForward(email))}\nfunction openComp(pref){st.comp={to:pref.to||'',cc:pref.cc||'',subj:pref.subj||'',body:pref.body||'',replyId:pref.replyId||null};st.compExp=false;st.view='compose';render()}\nfunction openReply(email){openComp({to:email.from?.[0]?.email||'',subj:email.subject?'Re: '+email.subject.replace(/^Re:\\s*/i,''):'Re: ',body:'\\n\\n\\n--- Original message ---\\n'+(email.preview||''),replyId:email.id})}\nfunction openReplyAll(email){const to=email.from?.map(f=>f.email).join(', ')||'',cc=email.to?.map(t=>t.email).filter(e=>e!==(actv()?.credentials?.username||'')).join(', ')||'';openComp({to,cc,subj:email.subject?'Re: '+email.subject.replace(/^Re:\\s*/i,''):'Re: ',body:'\\n\\n\\n--- Original message ---\\n'+(email.preview||''),replyId:email.id})}\nfunction openForward(email){openComp({subj:email.subject?'Fwd: '+email.subject.replace(/^Fwd:\\s*/i,''):'Fwd: ',body:'\\n\\n\\n--- Forwarded message ---\\n'+(email.preview||''),replyId:email.id})}\nfunction renderComp(){const d=st.comp,a=actv();const ue=a?.credentials?.username||'',idn=a?.identities?.[0]?.name||a?.identities?.[0]?.email||ue;if(st.compExp){co.innerHTML='<div class=\"compose-form\"><div class=\"compose-fields expanded\"><div class=\"ce\" id=\"ce\"></div><textarea id=\"cb\" placeholder=\"Write your message...\">'+escape(d.body)+'</textarea></div><div class=\"compose-bar\"><button class=\"reduce-btn\" id=\"rdBtn\">β–½ Reduce</button></div></div>';$('rdBtn').addEventListener('click',()=>{st.comp.body=$('cb').value;st.compExp=false;render()});$('cb').addEventListener('keydown',e=>{if((e.ctrlKey||e.metaKey)&&e.key==='Enter')handleSend()});$('cb').focus()}else{co.innerHTML='<div class=\"compose-form\"><div class=\"compose-fields\"><div class=\"ce\" id=\"ce\"></div><div class=\"ch\"><div class=\"compose-row\"><label for=\"ct\">To</label><input type=\"text\" id=\"ct\" placeholder=\"recipient@example.com\" value=\"'+escape(d.to)+'\"></div><div class=\"compose-row\"><label for=\"cc\">CC</label><input type=\"text\" id=\"cc\" placeholder=\"cc@example.com\" value=\"'+escape(d.cc)+'\"></div><div class=\"compose-row\"><label for=\"cs\">Subj</label><input type=\"text\" id=\"cs\" placeholder=\"Subject\" value=\"'+escape(d.subj)+'\"></div></div><textarea id=\"cb\" placeholder=\"Write your message...\">'+escape(d.body)+'</textarea></div></div>';$('cb').addEventListener('focus',()=>{st.comp.to=$('ct').value;st.comp.cc=$('cc').value;st.comp.subj=$('cs').value;st.comp.body=$('cb').value;st.compExp=true;render()});$('cb').addEventListener('keydown',e=>{if((e.ctrlKey||e.metaKey)&&e.key==='Enter')handleSend()});if(!d.to)$('ct').focus();else if(!d.subj)$('cs').focus();else $('cb').focus()}}\nasync function handleSend(){let toRaw,ccRaw,subj,body;const a=actv();if(st.compExp){toRaw=st.comp.to;ccRaw=st.comp.cc;subj=st.comp.subj;body=$('cb').value}else{toRaw=$('ct').value.trim();ccRaw=$('cc').value.trim();subj=$('cs').value.trim();body=$('cb').value}const err=$('ce'),btn=sh;if(!toRaw){err.textContent='Please enter at least one recipient.';err.style.display='block';return}err.style.display='none';btn.disabled=true;btn.textContent='πŸ“€ Sending...';sh.disabled=true;sh.textContent='πŸ“€ Sending...';try{const toA=toRaw.split(',').map(s=>s.trim()).filter(s=>s).map(e=>({email:e})),ccA=ccRaw.split(',').map(s=>s.trim()).filter(s=>s).map(e=>({email:e})),ue=a.credentials.username;if(a.identities.length===0)await loadIdentities();let idn=a.identities.find(i=>i.email===ue);if(!idn&&a.identities.length>0)idn=a.identities[0];if(!idn)throw new Error('No identity found on server.');if(!a.sendMailboxId)throw new Error('No mailbox available.');const ec={mailboxIds:{[a.sendMailboxId]:true},keywords:{$seen:false},from:[{email:idn.email||ue}],to:toA,subject:subj||'(no subject)',textBody:[{partId:'body-0',type:'text/plain'}],bodyValues:{'body-0':{value:body,charset:'UTF-8'}}};if(ccA.length>0)ec.cc=ccA;if(st.comp.replyId)ec.inReplyTo=st.comp.replyId;const sa=a.submissionAccountId||a.accountId,allR=[...toA];if(ccA.length>0)allR.push(...ccA);const mr=await jmulti([['Email/set',{accountId:a.accountId,create:{'ne':ec}},'0'],['EmailSubmission/set',{accountId:sa,create:{'ns':{emailId:'#ne',identityId:idn.id,envelope:{mailFrom:{email:idn.email||ue},rcptTo:allR}}}},'1']]);const esr=mr.methodResponses[0][1],cr=esr.created?.['ne'];if(!cr){const er=esr.notCreated?.['ne'];throw new Error(er?.description||'Failed to create email')}const sr=mr.methodResponses[1][1],sub=sr.created?.['ns'];if(!sub){const er=sr.notCreated?.['ns'],ed=er?(er.description||er.type||JSON.stringify(er)):'Unknown';if(st.comp.replyId&&(ed.includes('oldEmail')||ed.includes('inReplyTo'))){delete ec.inReplyTo;const rr=await jmulti([['Email/set',{accountId:a.accountId,create:{'ne':ec}},'0'],['EmailSubmission/set',{accountId:sa,create:{'ns':{emailId:'#ne',identityId:idn.id,envelope:{mailFrom:{email:idn.email||ue},rcptTo:allR}}}},'1']]);const rs=rr.methodResponses[1][1].created?.['ns'];if(rs){toast('βœ… Email sent!','success');st.view='mailboxes';st.compExp=false;render();return}throw new Error('SMTP rejected: '+(rr.methodResponses[1][1].notCreated?.['ns']?.description||ed))}throw new Error('SMTP rejected: '+ed)}toast('βœ… Email sent!','success');st.view='mailboxes';st.compExp=false;render()}catch(e){err.textContent='Failed to send: '+e.message;err.style.display='block';btn.disabled=false;btn.textContent='πŸ“€ Send';sh.disabled=false;sh.textContent='πŸ“€ Send'}btn.disabled=false;btn.textContent='πŸ“€ Send';sh.disabled=false;sh.textContent='πŸ“€ Send'}\nasync function init(){load();if(st.accounts.length>0&&st.activeId){const a=actv();if(a){st.view='mailboxes';render();await loadIdentities();await loadMailboxes()}else{st.view='accounts';render()}}else{st.view='accounts';render()}}\ninit();\n</script>\n</body>\n</html>"
},
"currentHash": "67dc659403d11726955c4eb9bd5f8548eb56a8ea24c4080b4b52ef803297ad62",
"previousHash": "4043515350a389903fbdfc783b07d30f92110a9ac405ef14ae94f3a713280c5c",
"prompt": "Published shared tab",
"publishedAt": "2026-08-25T17:48:34.861Z",
"appId": "dfda7e57-b0ee-4ab3-aa45-5005bd0d4af3"
},
"appId": "dfda7e57-b0ee-4ab3-aa45-5005bd0d4af3"
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment