first commit
This commit is contained in:
@@ -0,0 +1,203 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>GoMail</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<style>
|
||||
body{background:#0f172a;color:#e2e8f0;font-family:system-ui,-apple-system,sans-serif;margin:0}
|
||||
.sidebar{width:220px;background:#1e293b;border-right:1px solid #334155;min-height:100vh;position:fixed;top:0;left:0;bottom:0}
|
||||
.main{margin-left:220px;display:flex;min-height:100vh}
|
||||
.msg-list{width:340px;border-right:1px solid #334155;overflow-y:auto}
|
||||
.msg-view{flex:1;padding:24px;overflow-y:auto}
|
||||
.nav-item{padding:9px 16px;cursor:pointer;font-size:13px;color:#94a3b8;border-radius:8px;margin:2px 8px}
|
||||
.nav-item:hover{background:#334155}
|
||||
.nav-item.active{background:#7c3aed22;color:#a78bfa}
|
||||
.msg-row{padding:12px 16px;border-bottom:1px solid #1e293b;cursor:pointer;font-size:13px}
|
||||
.msg-row:hover{background:#1e293b80}
|
||||
.msg-row.unread{font-weight:600}
|
||||
.btn{padding:7px 14px;border-radius:7px;font-size:13px;font-weight:500;cursor:pointer;border:none}
|
||||
.btn-primary{background:#7c3aed;color:#fff}
|
||||
.btn-ghost{background:transparent;color:#94a3b8;border:1px solid #334155}
|
||||
.inp{background:#0f172a;border:1px solid #334155;border-radius:7px;padding:8px 12px;color:#e2e8f0;font-size:13px;width:100%}
|
||||
.modal-bg{position:fixed;inset:0;background:#00000088;z-index:50;display:flex;align-items:center;justify-content:center}
|
||||
.modal{background:#1e293b;border:1px solid #334155;border-radius:14px;padding:24px;width:560px;max-width:95vw}
|
||||
.badge{padding:2px 8px;border-radius:10px;font-size:11px}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id="login" style="display:none;min-height:100vh;align-items:center;justify-content:center" class="flex">
|
||||
<div style="background:#1e293b;border:1px solid #334155;border-radius:14px;padding:28px;width:320px">
|
||||
<div style="text-align:center;margin-bottom:20px"><div style="font-size:2.5rem">📧</div>
|
||||
<h1 style="font-weight:700;color:#fff">GoMail</h1></div>
|
||||
<input id="le" class="inp" placeholder="you@example.com" style="margin-bottom:10px">
|
||||
<input id="lp" type="password" class="inp" placeholder="Password" style="margin-bottom:10px" onkeydown="if(event.key==='Enter')login()">
|
||||
<button onclick="login()" class="btn btn-primary" style="width:100%">Sign in</button>
|
||||
<p id="lerr" style="display:none;color:#f87171;font-size:12px;text-align:center;margin-top:10px"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="app" style="display:none">
|
||||
<aside class="sidebar">
|
||||
<div style="padding:16px;border-bottom:1px solid #334155;font-weight:700;color:#fff">📧 GoMail</div>
|
||||
<div style="padding:12px 8px">
|
||||
<button onclick="openCompose()" class="btn btn-primary" style="width:100%;margin-bottom:12px">✎ Compose</button>
|
||||
<div id="folder-list"></div>
|
||||
<div class="nav-item" onclick="showQuarantine()" id="nav-quarantine" style="margin-top:8px">🔒 Quarantine</div>
|
||||
</div>
|
||||
<div style="position:absolute;bottom:0;padding:12px;border-top:1px solid #334155;width:100%;box-sizing:border-box">
|
||||
<span id="me-email" style="font-size:12px;color:#64748b"></span>
|
||||
<button onclick="logout()" style="float:right;font-size:11px;color:#475569;background:none;border:none;cursor:pointer">Logout</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<main class="main">
|
||||
<div id="view-mail" style="display:flex;flex:1">
|
||||
<div class="msg-list" id="msg-list"></div>
|
||||
<div class="msg-view" id="msg-view"><div style="color:#475569;text-align:center;margin-top:60px">Select a message</div></div>
|
||||
</div>
|
||||
<div id="view-quarantine" style="display:none;flex:1;padding:24px">
|
||||
<h2 style="color:#fff;font-weight:700;margin-bottom:16px">Quarantine</h2>
|
||||
<div id="quarantine-list"></div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<div id="compose-modal" class="modal-bg" style="display:none">
|
||||
<div class="modal">
|
||||
<h3 style="color:#fff;font-weight:700;margin-bottom:16px">New Message</h3>
|
||||
<input id="c-to" class="inp" placeholder="To" style="margin-bottom:8px">
|
||||
<input id="c-subject" class="inp" placeholder="Subject" style="margin-bottom:8px">
|
||||
<textarea id="c-body" class="inp" rows="8" placeholder="Message..." style="margin-bottom:12px"></textarea>
|
||||
<div style="display:flex;gap:8px;justify-content:flex-end">
|
||||
<button onclick="closeCompose()" class="btn btn-ghost">Cancel</button>
|
||||
<button onclick="sendMessage()" class="btn btn-primary">Send</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const API='/api';
|
||||
let token=localStorage.getItem('gomail_token')||'';
|
||||
let currentFolder='INBOX';
|
||||
|
||||
async function api(path,opts={}){
|
||||
const r=await fetch(API+path,{...opts,headers:{'Content-Type':'application/json','Authorization':'Bearer '+token,...(opts.headers||{})}});
|
||||
if(r.status===401){showLogin();return null;}
|
||||
return r.ok?r.json():Promise.reject(await r.json());
|
||||
}
|
||||
|
||||
async function login(){
|
||||
const email=document.getElementById('le').value,pwd=document.getElementById('lp').value;
|
||||
try{
|
||||
const d=await fetch(API+'/auth/login',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({Email:email,Password:pwd})}).then(r=>r.json());
|
||||
if(d.error)throw new Error(d.error);
|
||||
token=d.token;localStorage.setItem('gomail_token',token);
|
||||
showApp();
|
||||
}catch(e){const el=document.getElementById('lerr');el.textContent=e.message||'Login failed';el.style.display='';}
|
||||
}
|
||||
function logout(){localStorage.removeItem('gomail_token');token='';showLogin();}
|
||||
function showLogin(){document.getElementById('login').style.display='flex';document.getElementById('app').style.display='none';}
|
||||
async function showApp(){
|
||||
document.getElementById('login').style.display='none';document.getElementById('app').style.display='block';
|
||||
const me=await api('/me');if(!me)return;
|
||||
document.getElementById('me-email').textContent=me.email;
|
||||
loadFolders();
|
||||
}
|
||||
|
||||
async function loadFolders(){
|
||||
const folders=await api('/folders');if(!folders)return;
|
||||
document.getElementById('folder-list').innerHTML=folders.map(f=>
|
||||
`<div class="nav-item ${f.id===currentFolder?'active':''}" onclick="selectFolder('${f.id}')">
|
||||
${f.display_name} ${f.unread_count>0?`<span class="badge" style="background:#7c3aed;color:#fff">${f.unread_count}</span>`:''}
|
||||
</div>`).join('');
|
||||
loadMessages(currentFolder);
|
||||
}
|
||||
|
||||
function selectFolder(id){
|
||||
currentFolder=id;
|
||||
document.getElementById('view-mail').style.display='flex';
|
||||
document.getElementById('view-quarantine').style.display='none';
|
||||
loadFolders();
|
||||
}
|
||||
|
||||
async function loadMessages(folderID){
|
||||
const msgs=await api('/folders/'+folderID+'/messages');if(!msgs)return;
|
||||
document.getElementById('msg-list').innerHTML=msgs.length?msgs.map(m=>{
|
||||
const unread=!(m.Flags||[]).includes('\\Seen');
|
||||
return `<div class="msg-row ${unread?'unread':''}" onclick="viewMessage('${folderID}','${m.ID}')">
|
||||
<div style="color:#e2e8f0">${esc(m.From||'(unknown)')}</div>
|
||||
<div style="color:#94a3b8">${esc(m.Subject||'(no subject)')}</div>
|
||||
</div>`;
|
||||
}).join(''):'<div style="padding:20px;color:#475569;text-align:center">No messages</div>';
|
||||
}
|
||||
|
||||
async function viewMessage(folderID,id){
|
||||
const msg=await api('/messages/'+folderID+'/'+id);if(!msg)return;
|
||||
document.getElementById('msg-view').innerHTML=`
|
||||
<div style="border-bottom:1px solid #334155;padding-bottom:12px;margin-bottom:12px">
|
||||
<div style="font-size:18px;font-weight:700;color:#fff">${esc(msg.Subject||'(no subject)')}</div>
|
||||
<div style="color:#94a3b8;font-size:13px;margin-top:4px">From: ${esc(msg.From)}</div>
|
||||
<div style="color:#94a3b8;font-size:13px">To: ${esc(msg.To)}</div>
|
||||
</div>
|
||||
<pre style="white-space:pre-wrap;font-family:inherit;color:#cbd5e1;font-size:13px">${esc(bodyOf(msg.Raw))}</pre>
|
||||
<div style="margin-top:16px">
|
||||
<button onclick="deleteMessage('${folderID}','${id}')" class="btn btn-ghost">🗑 Delete</button>
|
||||
</div>`;
|
||||
api('/messages/'+folderID+'/'+id+'/flags',{method:'PUT',body:JSON.stringify({Flags:['\\Seen']})});
|
||||
}
|
||||
|
||||
function bodyOf(raw){
|
||||
if(!raw)return'';
|
||||
const decoded=atob(raw);
|
||||
const idx=decoded.indexOf('\r\n\r\n');
|
||||
return idx>=0?decoded.slice(idx+4):decoded;
|
||||
}
|
||||
|
||||
async function deleteMessage(folderID,id){
|
||||
await api('/messages/'+folderID+'/'+id,{method:'DELETE'});
|
||||
loadMessages(folderID);
|
||||
document.getElementById('msg-view').innerHTML='<div style="color:#475569;text-align:center;margin-top:60px">Select a message</div>';
|
||||
}
|
||||
|
||||
function openCompose(){document.getElementById('compose-modal').style.display='flex';}
|
||||
function closeCompose(){document.getElementById('compose-modal').style.display='none';}
|
||||
async function sendMessage(){
|
||||
const to=document.getElementById('c-to').value.split(',').map(s=>s.trim());
|
||||
const subject=document.getElementById('c-subject').value;
|
||||
const body=document.getElementById('c-body').value;
|
||||
try{
|
||||
await api('/messages',{method:'POST',body:JSON.stringify({to,subject,body})});
|
||||
closeCompose();
|
||||
document.getElementById('c-to').value='';document.getElementById('c-subject').value='';document.getElementById('c-body').value='';
|
||||
}catch(e){alert('Send failed: '+(e.error||e.message));}
|
||||
}
|
||||
|
||||
async function showQuarantine(){
|
||||
document.getElementById('view-mail').style.display='none';
|
||||
document.getElementById('view-quarantine').style.display='block';
|
||||
const entries=await api('/quarantine');if(!entries)return;
|
||||
document.getElementById('quarantine-list').innerHTML=entries.length?entries.map(e=>`
|
||||
<div style="background:#1e293b;border:1px solid #334155;border-radius:10px;padding:14px;margin-bottom:10px;display:flex;justify-content:space-between;align-items:center">
|
||||
<div><div style="color:#e2e8f0;font-size:13px">Reason: ${esc(e.Reason||'—')}</div>
|
||||
<div style="color:#64748b;font-size:12px">Held: ${e.CreatedAt}</div></div>
|
||||
<button onclick="releaseQ('${e.ID}')" class="btn btn-primary">Release</button>
|
||||
</div>`).join(''):'<div style="color:#475569;text-align:center;padding:40px">🎉 Nothing held</div>';
|
||||
}
|
||||
async function releaseQ(id){
|
||||
try{await api('/quarantine/'+id+'/release',{method:'POST'});showQuarantine();}
|
||||
catch(e){alert('Release failed: '+(e.error||e.message));}
|
||||
}
|
||||
|
||||
function esc(s){return String(s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');}
|
||||
|
||||
async function boot(){
|
||||
if(!token){showLogin();return;}
|
||||
try{const me=await api('/me');if(me)showApp();else showLogin();}catch{showLogin();}
|
||||
}
|
||||
boot();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user