mail_javi/templates/index.html

653 lines
37 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>PSP Mail</title>
<link href="https://fonts.googleapis.com/css2?family=Google+Sans:wght@400;500;600&family=Roboto:wght@300;400;500&display=swap" rel="stylesheet"/>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"/>
<style>
*{box-sizing:border-box;margin:0;padding:0}
:root{
--bg:#f6f8fc;
--surface:#fff;
--accent:#1a73e8;
--accent-hover:#1557b0;
--accent-light:#e8f0fe;
--red:#d93025;
--yellow:#f4b400;
--green:#34a853;
--text:#202124;
--text2:#5f6368;
--text3:#444746;
--border:#e0e0e0;
--border2:#c4c7c5;
--hover:#f2f2f2;
--selected:#c2dbff;
--shadow-sm:0 1px 2px rgba(60,64,67,.3),0 1px 3px 1px rgba(60,64,67,.15);
--sidebar-w:256px;
--header-h:64px;
}
body{font-family:'Roboto',sans-serif;background:var(--bg);color:var(--text);height:100vh;display:flex;flex-direction:column;overflow:hidden;font-size:14px}
/* HEADER */
.header{display:flex;align-items:center;padding:8px 16px;background:var(--surface);height:var(--header-h);gap:8px;z-index:30;flex-shrink:0;border-bottom:1px solid var(--border)}
.menu-btn,.icon-btn{width:40px;height:40px;border-radius:50%;border:none;background:none;cursor:pointer;color:var(--text2);font-size:16px;display:flex;align-items:center;justify-content:center;transition:.15s;flex-shrink:0}
.menu-btn{font-size:18px}
.menu-btn:hover,.icon-btn:hover{background:var(--hover)}
.logo{display:flex;align-items:center;gap:4px;width:188px;flex-shrink:0;font-family:'Google Sans',sans-serif;font-size:22px;text-decoration:none;user-select:none;cursor:default}
.logo .l-p{color:#4285F4}.logo .l-s{color:#EA4335}.logo .l-pp{color:#FBBC05}
.logo-txt{font-weight:400;color:#5f6368;letter-spacing:-.5px}
.search-wrap{flex:1;max-width:720px;margin:0 auto;background:#eaf1fb;border-radius:24px;display:flex;align-items:center;padding:0 6px 0 16px;height:46px;transition:.2s}
.search-wrap:focus-within{background:#fff;box-shadow:var(--shadow-sm)}
.search-wrap input{border:none;background:transparent;outline:none;width:100%;font-size:16px;color:var(--text);font-family:'Google Sans',sans-serif}
.search-wrap input::placeholder{color:var(--text2)}
.s-btn{background:none;border:none;cursor:pointer;width:38px;height:38px;border-radius:50%;color:var(--text2);font-size:15px;display:flex;align-items:center;justify-content:center;transition:.15s;flex-shrink:0}
.s-btn:hover{background:rgba(60,64,67,.1)}
.header-right{display:flex;align-items:center;gap:2px;margin-left:4px}
.avatar{width:32px;height:32px;border-radius:50%;background:linear-gradient(135deg,#4285F4,#34A853);color:#fff;display:flex;align-items:center;justify-content:center;font-family:'Google Sans';font-weight:600;font-size:13px;cursor:pointer;margin-left:6px;flex-shrink:0}
/* LAYOUT */
.layout{display:flex;flex:1;overflow:hidden}
/* SIDEBAR */
.sidebar{width:var(--sidebar-w);flex-shrink:0;background:var(--bg);padding:8px 0;overflow-y:auto;overflow-x:hidden;transition:width .2s}
.compose-btn{display:flex;align-items:center;gap:12px;margin:4px 12px 12px;padding:17px 22px;background:var(--surface);border-radius:24px;box-shadow:0 1px 3px rgba(0,0,0,.2),0 1px 2px rgba(0,0,0,.1);cursor:pointer;font-family:'Google Sans';font-weight:500;font-size:14px;color:#444746;transition:.2s;border:none;width:calc(100% - 24px)}
.compose-btn:hover{box-shadow:0 2px 8px rgba(0,0,0,.25);background:#fafafa}
.compose-btn i{font-size:17px;color:var(--text2)}
.nav-item{display:flex;align-items:center;padding:0 16px 0 26px;height:32px;border-radius:0 16px 16px 0;margin-right:16px;cursor:pointer;font-size:14px;color:#444746;gap:14px;transition:background .1s;position:relative;user-select:none}
.nav-item:hover{background:#e8eaed}
.nav-item.active{background:#d3e3fd;font-weight:600;color:#041e49}
.nav-item .n-icon{font-size:17px;color:var(--text2);width:20px;text-align:center;flex-shrink:0}
.nav-item.active .n-icon{color:#041e49}
.nav-item .n-badge{margin-left:auto;font-size:12px;font-weight:600;color:#444746}
.nav-sep{height:1px;background:var(--border);margin:6px 0}
.nav-label{padding:8px 16px 4px 26px;font-size:11px;font-weight:600;color:var(--text2);text-transform:uppercase;letter-spacing:.5px}
/* MAIN */
.main{flex:1;display:flex;flex-direction:column;overflow:hidden;background:var(--bg)}
.toolbar{display:flex;align-items:center;padding:4px 8px;gap:2px;flex-shrink:0}
.t-btn{background:none;border:none;cursor:pointer;width:36px;height:36px;border-radius:50%;color:var(--text2);font-size:15px;display:flex;align-items:center;justify-content:center;transition:.15s;flex-shrink:0}
.t-btn:hover{background:var(--hover)}
.t-sep{width:1px;height:20px;background:var(--border);margin:0 4px;flex-shrink:0}
.t-label{font-size:13px;color:var(--text2);white-space:nowrap;padding:0 4px}
.t-spacer{flex:1}
/* MSG LIST BOX */
.list-box{flex:1;margin:0 16px 16px;overflow:hidden;border-radius:16px;background:var(--surface);box-shadow:0 1px 2px rgba(60,64,67,.15);display:flex;flex-direction:column}
.list-scroll{flex:1;overflow-y:auto}
/* MSG ROW */
.msg-row{display:flex;align-items:center;padding:0 12px;height:50px;cursor:pointer;gap:8px;transition:background .1s;border-bottom:1px solid #f0f0f0;flex-shrink:0}
.msg-row:last-child{border-bottom:none}
.msg-row:hover{background:#f5f5f5;box-shadow:inset 1px 0 0 #dadce0, inset -1px 0 0 #dadce0, 0 1px 3px 1px rgba(60,64,67,.12)}
.msg-row.unread{background:#fff}
.msg-row.read{background:#f2f6fc}
.msg-row.selected{background:#e8f0fe}
.msg-check{width:18px;height:18px;flex-shrink:0;display:flex;align-items:center;opacity:0;transition:opacity .15s}
.msg-row:hover .msg-check,.msg-row.selected .msg-check{opacity:1}
.msg-check input[type=checkbox]{cursor:pointer;width:16px;height:16px;accent-color:var(--accent)}
.msg-star{font-size:15px;cursor:pointer;color:transparent;flex-shrink:0;transition:.15s;-webkit-text-stroke:1.5px #bbb;line-height:1}
.msg-row:hover .msg-star{-webkit-text-stroke:1.5px #9aa0a6}
.msg-star.starred{color:var(--yellow);-webkit-text-stroke:1.5px var(--yellow)}
.msg-av{width:28px;height:28px;border-radius:50%;flex-shrink:0;display:flex;align-items:center;justify-content:center;font-family:'Google Sans';font-weight:500;font-size:12px;color:#fff}
.msg-from{width:155px;flex-shrink:0;font-size:14px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#444746}
.msg-row.unread .msg-from{font-weight:700;color:var(--text)}
.msg-body-col{flex:1;display:flex;align-items:baseline;gap:6px;overflow:hidden;min-width:0}
.msg-subject{font-size:14px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#444746;flex-shrink:0;max-width:50%}
.msg-row.unread .msg-subject{color:var(--text);font-weight:500}
.msg-snippet{font-size:13px;color:#9aa0a6;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1}
.msg-snippet::before{content:" — ";color:#c5c5c5}
.msg-date{font-size:12px;color:var(--text2);flex-shrink:0;min-width:52px;text-align:right}
.msg-row.unread .msg-date{font-weight:600;color:var(--text)}
/* EMPTY / LOADING */
.empty-state{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:16px;padding:80px 20px;color:var(--text2);text-align:center;flex:1}
.empty-state i{font-size:60px;opacity:.18;color:var(--accent)}
.empty-state h3{font-family:'Google Sans';font-size:20px;color:var(--text2);font-weight:400}
.spinner{width:28px;height:28px;border:3px solid #e0e0e0;border-top-color:var(--accent);border-radius:50%;animation:spin .8s linear infinite}
@keyframes spin{to{transform:rotate(360deg)}}
.loading-wrap{display:flex;align-items:center;justify-content:center;gap:14px;padding:60px;color:var(--text2);font-size:14px}
/* READ VIEW */
.read-panel{display:none;flex-direction:column;flex:1;overflow:hidden;background:var(--surface);border-radius:16px;margin:0 16px 16px;box-shadow:0 1px 2px rgba(60,64,67,.15)}
.read-panel.open{display:flex}
.read-toolbar{display:flex;align-items:center;padding:6px 12px;border-bottom:1px solid var(--border);gap:2px;flex-shrink:0}
.read-scroll{flex:1;overflow-y:auto;padding:0 24px 32px}
.read-subject-line{padding:20px 0 16px;font-family:'Google Sans';font-size:24px;font-weight:400;color:var(--text);line-height:1.3}
.msg-card{border:1px solid var(--border);border-radius:12px;margin-bottom:12px;overflow:hidden}
.card-head{display:flex;align-items:center;padding:14px 16px;gap:12px;cursor:pointer;background:var(--surface)}
.card-head:hover{background:#fafafa}
.card-sender-av{width:36px;height:36px;border-radius:50%;flex-shrink:0;display:flex;align-items:center;justify-content:center;font-family:'Google Sans';font-weight:500;font-size:15px;color:#fff}
.card-sender-info{flex:1;overflow:hidden}
.card-sender-name{font-weight:500;font-size:14px;color:var(--text)}
.card-sender-addr{font-size:12px;color:var(--text2);margin-top:2px}
.card-time{font-size:12px;color:var(--text2);flex-shrink:0;align-self:flex-start;margin-top:2px}
.card-acts{display:flex;gap:2px}
.card-body{padding:4px 16px 20px;font-size:14px;line-height:1.7;color:var(--text)}
.card-body iframe{width:100%;border:none;min-height:200px;display:block}
.card-body pre{white-space:pre-wrap;font-family:'Roboto',sans-serif;font-size:14px;line-height:1.6;color:var(--text)}
.card-atts{border-top:1px solid var(--border);padding:14px 16px;background:#fafafa}
.att-title{font-size:13px;color:var(--text2);font-weight:500;margin-bottom:10px}
.att-grid{display:flex;flex-wrap:wrap;gap:8px}
.att-chip{display:inline-flex;align-items:center;gap:10px;padding:8px 14px;border:1px solid var(--border);border-radius:8px;cursor:pointer;font-size:13px;transition:.15s;text-decoration:none;color:#444746;background:var(--surface)}
.att-chip:hover{background:var(--hover);border-color:var(--border2)}
.reply-row{display:flex;gap:10px;padding:16px 0 8px;flex-wrap:wrap}
.reply-btn{display:inline-flex;align-items:center;gap:8px;padding:8px 22px;border-radius:20px;font-size:14px;font-family:'Google Sans';cursor:pointer;transition:.15s;border:1px solid var(--border2);background:var(--surface);color:#444746}
.reply-btn:hover{background:var(--hover)}
/* BUTTONS */
.btn{display:inline-flex;align-items:center;gap:6px;padding:8px 20px;border-radius:4px;font-size:14px;font-family:'Google Sans';cursor:pointer;transition:.15s;border:1px solid var(--border)}
.btn-primary{background:var(--accent);color:#fff;border-color:var(--accent);border-radius:4px;box-shadow:0 1px 3px rgba(26,115,232,.3)}
.btn-primary:hover{background:var(--accent-hover);box-shadow:0 2px 6px rgba(26,115,232,.4)}
.btn-outline{background:var(--surface);color:var(--accent)}
.btn-outline:hover{background:var(--accent-light)}
/* COMPOSE */
.compose-modal{position:fixed;bottom:0;right:24px;width:560px;max-height:90vh;background:var(--surface);border-radius:8px 8px 0 0;box-shadow:0 8px 40px rgba(0,0,0,.3),0 2px 12px rgba(0,0,0,.2);display:flex;flex-direction:column;z-index:200;transform:translateY(100%);transition:transform .22s cubic-bezier(.4,0,.2,1)}
.compose-modal.open{transform:translateY(0)}
.compose-modal.minimized .compose-fields,.compose-modal.minimized .compose-body-wrap{display:none!important}
.compose-header{display:flex;align-items:center;padding:10px 16px;background:#404040;color:#fff;border-radius:8px 8px 0 0;cursor:pointer;user-select:none;gap:6px}
.compose-header h3{flex:1;font-size:14px;font-family:'Google Sans';font-weight:500}
.c-hbtn{background:none;border:none;color:rgba(255,255,255,.85);cursor:pointer;width:28px;height:28px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:14px;transition:.15s}
.c-hbtn:hover{background:rgba(255,255,255,.18)}
.compose-fields{display:flex;flex-direction:column}
.compose-field{display:flex;align-items:center;padding:6px 16px;border-bottom:1px solid var(--border);gap:8px}
.compose-field label{font-size:13px;color:var(--text2);min-width:44px;flex-shrink:0}
.compose-field input{flex:1;border:none;outline:none;font-size:14px;color:var(--text);font-family:'Roboto'}
.compose-body-wrap{flex:1;display:flex;flex-direction:column;min-height:0}
.att-preview{display:flex;flex-wrap:wrap;gap:6px;padding:6px 16px 0}
.att-pill{display:flex;align-items:center;gap:6px;padding:4px 10px;background:var(--accent-light);border-radius:12px;font-size:12px;color:var(--accent)}
.att-pill button{background:none;border:none;color:var(--accent);cursor:pointer;font-size:11px;padding:0 2px}
.compose-body{flex:1;padding:12px 16px;border:none;outline:none;font-size:14px;resize:none;font-family:'Roboto';min-height:220px;color:var(--text);line-height:1.6}
.compose-footer{display:flex;align-items:center;padding:10px 16px;border-top:1px solid var(--border);gap:6px}
.c-foot-btn{background:none;border:none;cursor:pointer;width:34px;height:34px;border-radius:50%;color:var(--text2);font-size:16px;display:flex;align-items:center;justify-content:center;transition:.15s}
.c-foot-btn:hover{background:var(--hover)}
#att-input{display:none}
/* TOAST */
.toast{position:fixed;bottom:24px;left:50%;transform:translateX(-50%) translateY(12px);background:#323232;color:#fff;padding:12px 20px;border-radius:6px;font-size:14px;opacity:0;transition:.3s cubic-bezier(.4,0,.2,1);z-index:400;pointer-events:none;box-shadow:0 4px 12px rgba(0,0,0,.3)}
.toast.show{opacity:1;transform:translateX(-50%) translateY(0)}
/* SCROLLBAR */
::-webkit-scrollbar{width:7px}::-webkit-scrollbar-track{background:transparent}
::-webkit-scrollbar-thumb{background:#dadce0;border-radius:4px}::-webkit-scrollbar-thumb:hover{background:#bbb}
/* AVATAR PALETTE */
.av-0{background:#DB4437}.av-1{background:#F4B400}.av-2{background:#0F9D58}
.av-3{background:#4285F4}.av-4{background:#AB47BC}.av-5{background:#00ACC1}
.av-6{background:#FF7043}.av-7{background:#5C6BC0}.av-8{background:#26A69A}.av-9{background:#8D6E63}
@media(max-width:900px){
:root{--sidebar-w:72px}
.nav-item span:not(.n-icon):not(.n-badge){display:none}
.nav-item{padding:0;justify-content:center}
.compose-btn span{display:none}
.compose-btn{padding:18px;justify-content:center}
.logo-txt{display:none}
}
@media(max-width:600px){
.sidebar{display:none}
.compose-modal{width:100%;right:0}
}
</style>
</head>
<body>
<!-- HEADER -->
<header class="header">
<button class="menu-btn" onclick="toggleSidebar()" title="Menú"><i class="fas fa-bars"></i></button>
<div class="logo">
<span class="l-p">P</span><span class="l-s">S</span><span class="l-pp">P</span>
<span class="logo-txt">&nbsp;Mail</span>
</div>
<div class="search-wrap">
<button class="s-btn" onclick="doSearch()" title="Buscar"><i class="fas fa-search"></i></button>
<input id="search-input" placeholder="Buscar en el correo" onkeydown="if(event.key==='Enter')doSearch()"/>
<button class="s-btn" onclick="clearSearch()" id="search-clear" style="display:none" title="Limpiar"><i class="fas fa-times"></i></button>
</div>
<div class="header-right">
<button class="icon-btn" onclick="refreshList()" id="refresh-btn" title="Actualizar"><i class="fas fa-sync-alt"></i></button>
<button class="icon-btn" title="Configuración"><i class="fas fa-cog"></i></button>
<button class="icon-btn" title="Aplicaciones"><i class="fas fa-th"></i></button>
<div class="avatar" title="javi@psp.es">J</div>
</div>
</header>
<div class="layout">
<!-- SIDEBAR -->
<nav class="sidebar" id="sidebar">
<button class="compose-btn" onclick="openCompose()">
<i class="fas fa-pen"></i><span>Redactar</span>
</button>
<div id="folder-list">
<div class="nav-item active" data-folder="INBOX" onclick="selectFolder(this,'INBOX')">
<span class="n-icon"><i class="fas fa-inbox"></i></span><span>Recibidos</span>
<span class="n-badge" id="badge-inbox"></span>
</div>
<div class="nav-item" data-folder="Sent" onclick="selectFolder(this,'Sent')">
<span class="n-icon"><i class="fas fa-paper-plane"></i></span><span>Enviados</span>
</div>
<div class="nav-item" data-folder="Drafts" onclick="selectFolder(this,'Drafts')">
<span class="n-icon"><i class="fas fa-file-alt"></i></span><span>Borradores</span>
</div>
<div class="nav-sep"></div>
<div class="nav-item" data-folder="Trash" onclick="selectFolder(this,'Trash')">
<span class="n-icon"><i class="fas fa-trash-alt"></i></span><span>Papelera</span>
</div>
<div class="nav-item" data-folder="Junk" onclick="selectFolder(this,'Junk')">
<span class="n-icon"><i class="fas fa-shield-alt"></i></span><span>Spam</span>
</div>
<div class="nav-sep"></div>
<div class="nav-label">Etiquetas</div>
<div class="nav-item" onclick="filterStarred()">
<span class="n-icon"><i class="fas fa-star" style="color:#f4b400"></i></span><span>Destacados</span>
</div>
</div>
</nav>
<!-- MAIN -->
<main class="main">
<!-- LIST VIEW -->
<div id="list-view" style="display:flex;flex-direction:column;flex:1;overflow:hidden">
<div class="toolbar">
<button class="t-btn" title="Seleccionar todo" onclick="toggleSelectAll()">
<i id="sel-icon" class="far fa-square"></i>
</button>
<button class="t-btn" title="Actualizar" onclick="refreshList()"><i class="fas fa-sync-alt"></i></button>
<div class="t-sep"></div>
<button class="t-btn" id="btn-delete" title="Eliminar" onclick="deleteSelected()" style="display:none"><i class="fas fa-trash-alt"></i></button>
<button class="t-btn" id="btn-mark-read" title="Marcar leído" onclick="markSelected('seen','add')" style="display:none"><i class="fas fa-envelope-open"></i></button>
<button class="t-btn" id="btn-mark-unread" title="Marcar no leído" onclick="markSelected('seen','remove')" style="display:none"><i class="fas fa-envelope"></i></button>
<div class="t-spacer"></div>
<span class="t-label" id="page-info"></span>
<button class="t-btn" title="Página anterior" onclick="prevPage()"><i class="fas fa-chevron-left"></i></button>
<button class="t-btn" title="Página siguiente" onclick="nextPage()"><i class="fas fa-chevron-right"></i></button>
</div>
<div class="list-box">
<div class="list-scroll" id="msg-list">
<div class="loading-wrap"><div class="spinner"></div><span>Cargando…</span></div>
</div>
</div>
</div>
<!-- READ VIEW -->
<div id="read-view" class="read-panel">
<div class="read-toolbar">
<button class="t-btn" title="Volver" onclick="backToList()"><i class="fas fa-arrow-left"></i></button>
<div class="t-sep"></div>
<button class="t-btn" title="Archivar" onclick="archiveMsg()"><i class="fas fa-archive"></i></button>
<button class="t-btn" title="Eliminar" onclick="deleteCurrentMsg()"><i class="fas fa-trash-alt"></i></button>
<button class="t-btn" title="Spam"><i class="fas fa-ban"></i></button>
<div class="t-sep"></div>
<button class="t-btn" title="Marcar no leído" onclick="markCurrentMsg('seen','remove')"><i class="fas fa-envelope"></i></button>
<button class="t-btn" title="Posponer"><i class="fas fa-clock"></i></button>
<div class="t-spacer"></div>
<button class="t-btn" title="Más opciones"><i class="fas fa-ellipsis-v"></i></button>
</div>
<div id="read-content" class="read-scroll"></div>
</div>
</main>
</div>
<!-- COMPOSE MODAL -->
<div class="compose-modal" id="compose-modal">
<div class="compose-header" onclick="toggleMinimize()">
<h3 id="compose-title">Nuevo mensaje</h3>
<button class="c-hbtn" onclick="event.stopPropagation();minimizeCompose()" title="Minimizar"><i class="fas fa-minus"></i></button>
<button class="c-hbtn" onclick="event.stopPropagation();maximizeCompose()" title="Maximizar"><i class="fas fa-expand-alt"></i></button>
<button class="c-hbtn" onclick="event.stopPropagation();closeCompose()" title="Cerrar"><i class="fas fa-times"></i></button>
</div>
<div class="compose-fields">
<div class="compose-field">
<label>Para</label>
<input id="c-to" type="email" placeholder="Destinatarios"/>
</div>
<div class="compose-field">
<label>Asunto</label>
<input id="c-subject" placeholder="Asunto"/>
</div>
</div>
<div class="compose-body-wrap">
<div class="att-preview" id="att-preview"></div>
<textarea class="compose-body" id="c-body" placeholder="Escribe tu mensaje aquí…"></textarea>
</div>
<div class="compose-footer">
<button class="btn btn-primary" onclick="sendEmail()">Enviar</button>
<label class="c-foot-btn" title="Adjuntar archivo">
<i class="fas fa-paperclip"></i>
<input type="file" id="att-input" multiple onchange="addAttachments(this)"/>
</label>
<label class="c-foot-btn" title="Emoji"><i class="far fa-smile"></i></label>
<div style="flex:1"></div>
<button class="t-btn" onclick="closeCompose()" title="Descartar borrador"><i class="fas fa-trash-alt"></i></button>
</div>
</div>
<!-- TOAST -->
<div class="toast" id="toast"></div>
<script>
'use strict';
let currentFolder='INBOX',currentPage=1,totalMessages=0;
const PER_PAGE=25;
let selectedUIDs=new Set(),currentMsg=null,attachFiles=[],isSearchMode=false,allMessages=[];
document.addEventListener('DOMContentLoaded',()=>loadMessages());
async function api(url,opts={}){
try{const r=await fetch(url,opts);return await r.json();}
catch(e){return{error:e.message};}
}
// ── Sidebar ───────────────────────────────────────────
function toggleSidebar(){
const s=document.getElementById('sidebar');
const open=s.style.width!=='0px'&&s.style.width!=='0';
s.style.width=open?'0':'';s.style.padding=open?'0':'';s.style.overflow=open?'hidden':'';
}
// ── Folders ───────────────────────────────────────────
function selectFolder(el,folder){
currentFolder=folder;currentPage=1;isSearchMode=false;
document.querySelectorAll('.nav-item').forEach(x=>x.classList.remove('active'));
el.classList.add('active');backToList();loadMessages();
}
function filterStarred(){
const s=allMessages.filter(m=>document.getElementById('star-'+m.uid)?.classList.contains('starred'));
if(!s.length){toast('No hay mensajes destacados');return;}
renderMessages(s);
}
// ── Message List ──────────────────────────────────────
async function loadMessages(){
const list=document.getElementById('msg-list');
list.innerHTML='<div class="loading-wrap"><div class="spinner"></div><span>Cargando mensajes…</span></div>';
selectedUIDs.clear();updateToolbar();
const data=await api(`/api/messages?folder=${encodeURIComponent(currentFolder)}&page=${currentPage}&per_page=${PER_PAGE}`);
allMessages=data.messages||[];totalMessages=data.total||0;
const pi=document.getElementById('page-info');
if(totalMessages>0){
const f=(currentPage-1)*PER_PAGE+1,t=Math.min(currentPage*PER_PAGE,totalMessages);
pi.textContent=`${f}${t} de ${totalMessages}`;
}else{pi.textContent='';}
renderMessages(allMessages);
}
function avClass(name){
let h=0;for(let i=0;i<name.length;i++)h=(h*31+name.charCodeAt(i))&0xFFFF;
return'av-'+(h%10);
}
function avLetter(name){return(name||'?').trim()[0].toUpperCase();}
function renderMessages(msgs){
const list=document.getElementById('msg-list');
if(!msgs||!msgs.length){
list.innerHTML='<div class="empty-state"><i class="fas fa-inbox"></i><h3>Bandeja vacía</h3><p>No hay mensajes aquí</p></div>';
return;
}
list.innerHTML=msgs.map(m=>{
const from=shortFrom(m.from);
return`<div class="msg-row ${m.seen?'read':'unread'}" id="row-${m.uid}" onclick="openMessage('${m.uid}')">
<div class="msg-check" onclick="event.stopPropagation()"><input type="checkbox" onchange="toggleSelect('${m.uid}',this)"/></div>
<span class="msg-star ${m.flagged?'starred':''}" id="star-${m.uid}" onclick="event.stopPropagation();toggleStar('${m.uid}')">★</span>
<div class="msg-av ${avClass(from)}">${avLetter(from)}</div>
<span class="msg-from">${escHtml(from)}</span>
<span class="msg-body-col">
<span class="msg-subject">${escHtml(m.subject)}</span>
</span>
<span class="msg-date">${formatDate(m.date)}</span>
</div>`;
}).join('');
}
function shortFrom(from){
if(!from)return'(desconocido)';
const m=from.match(/^"?([^"<]+)"?\s*</);if(m)return m[1].trim();
const a=from.match(/<([^>]+)>/);return a?a[1].split('@')[0]:from.split('@')[0];
}
function formatDate(ds){
if(!ds)return'';
try{
const d=new Date(ds),now=new Date();
if(d.toDateString()===now.toDateString())return d.toLocaleTimeString('es-ES',{hour:'2-digit',minute:'2-digit'});
if((now-d)/86400000<7)return d.toLocaleDateString('es-ES',{weekday:'short'});
if(d.getFullYear()===now.getFullYear())return d.toLocaleDateString('es-ES',{day:'numeric',month:'short'});
return d.toLocaleDateString('es-ES',{day:'numeric',month:'short',year:'2-digit'});
}catch{return'';}
}
function escHtml(s){
return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
// ── Pagination ────────────────────────────────────────
function prevPage(){if(currentPage>1){currentPage--;loadMessages();}}
function nextPage(){if(currentPage*PER_PAGE<totalMessages){currentPage++;loadMessages();}}
// ── Selection ─────────────────────────────────────────
function toggleSelect(uid,cb){
if(cb.checked)selectedUIDs.add(uid);else selectedUIDs.delete(uid);
document.getElementById('row-'+uid)?.classList.toggle('selected',cb.checked);
updateToolbar();
}
function toggleSelectAll(){
const cbs=document.querySelectorAll('.msg-check input');
const sel=selectedUIDs.size<cbs.length;
cbs.forEach(cb=>{
cb.checked=sel;
const uid=cb.closest('.msg-row').id.replace('row-','');
if(sel)selectedUIDs.add(uid);else selectedUIDs.delete(uid);
cb.closest('.msg-row').classList.toggle('selected',sel);
});
document.getElementById('sel-icon').className=sel?'fas fa-check-square':'far fa-square';
updateToolbar();
}
function updateToolbar(){
const has=selectedUIDs.size>0;
['btn-delete','btn-mark-read','btn-mark-unread'].forEach(id=>{
const el=document.getElementById(id);if(el)el.style.display=has?'':'none';
});
}
async function deleteSelected(){
if(!selectedUIDs.size)return;
toast('Eliminando…');
for(const uid of selectedUIDs)await api(`/api/delete/${uid}?folder=${encodeURIComponent(currentFolder)}`,{method:'DELETE'});
selectedUIDs.clear();toast('Mensajes eliminados');loadMessages();
}
async function markSelected(flag,action){
for(const uid of selectedUIDs)await api(`/api/mark/${uid}?folder=${encodeURIComponent(currentFolder)}`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({flag,action})});
toast('Actualizado');loadMessages();
}
// ── Open Message ──────────────────────────────────────
async function openMessage(uid){
document.getElementById('list-view').style.display='none';
const rv=document.getElementById('read-view');rv.classList.add('open');
document.getElementById('read-content').innerHTML='<div class="loading-wrap"><div class="spinner"></div></div>';
const data=await api(`/api/message/${uid}?folder=${encodeURIComponent(currentFolder)}`);
if(data.error){
document.getElementById('read-content').innerHTML=`<div class="loading-wrap" style="color:var(--red)"><i class="fas fa-exclamation-circle"></i>&nbsp;${escHtml(data.error)}</div>`;
return;
}
currentMsg=data;
document.getElementById('row-'+uid)?.classList.replace('unread','read');
const sn=shortFrom(data.from);
const hasAtt=data.attachments&&data.attachments.length>0;
const attHtml=hasAtt?`
<div class="card-atts">
<div class="att-title"><i class="fas fa-paperclip" style="margin-right:6px"></i>${data.attachments.length} adjunto${data.attachments.length>1?'s':''}</div>
<div class="att-grid">
${data.attachments.map((a,i)=>{
const isImg=a.mime&&a.mime.startsWith('image/');
const thumb=isImg?`<img src="data:${a.mime};base64,${a.data}" style="width:56px;height:44px;object-fit:cover;border-radius:4px;border:1px solid var(--border)"/>`
:`<i class="fas fa-file" style="font-size:20px;color:var(--text2)"></i>`;
return`<a class="att-chip" href="/api/attachment/${uid}/${i}?folder=${encodeURIComponent(currentFolder)}" download="${escHtml(a.filename)}">${thumb}<span><div style="font-weight:500;font-size:12px">${escHtml(a.filename)}</div><div style="font-size:11px;color:var(--text2)">${fmtSize(a.size)}</div></span></a>`;
}).join('')}
</div>
</div>`:'';
const bodyHtml=data.body_html
?`<iframe sandbox="allow-same-origin" srcdoc="${escHtml(data.body_html)}" style="width:100%;border:none;min-height:200px;display:block" onload="resizeIframe(this)"></iframe>`
:`<pre>${escHtml(data.body_text||'')}</pre>`;
document.getElementById('read-content').innerHTML=`
<div class="read-subject-line">${escHtml(data.subject)}</div>
<div class="msg-card">
<div class="card-head">
<div class="card-sender-av ${avClass(sn)}">${avLetter(sn)}</div>
<div class="card-sender-info">
<div class="card-sender-name">${escHtml(sn)}</div>
<div class="card-sender-addr"><span style="color:var(--text2)">De:</span> ${escHtml(data.from)} &nbsp;<span style="color:var(--text2)">Para:</span> ${escHtml(data.to)}</div>
</div>
<div class="card-time">${escHtml(data.date)}</div>
<div class="card-acts">
<button class="t-btn" title="Responder" onclick="replyMessage()"><i class="fas fa-reply"></i></button>
<button class="t-btn" title="Más"><i class="fas fa-ellipsis-v"></i></button>
</div>
</div>
<div class="card-body">${bodyHtml}</div>
${attHtml}
</div>
<div class="reply-row">
<button class="reply-btn" onclick="replyMessage()"><i class="fas fa-reply" style="color:var(--text2)"></i> Responder</button>
<button class="reply-btn" onclick="forwardMessage()"><i class="fas fa-share" style="color:var(--text2)"></i> Reenviar</button>
</div>`;
}
function resizeIframe(iframe){
try{iframe.style.height=(iframe.contentWindow.document.body.scrollHeight+24)+'px';}catch(e){}
}
function fmtSize(bytes){
if(!bytes)return'0 B';
if(bytes<1024)return bytes+' B';
if(bytes<1048576)return(bytes/1024).toFixed(1)+' KB';
return(bytes/1048576).toFixed(1)+' MB';
}
function backToList(){
document.getElementById('list-view').style.display='flex';
document.getElementById('read-view').classList.remove('open');
currentMsg=null;
}
async function deleteCurrentMsg(){
if(!currentMsg)return;
await api(`/api/delete/${currentMsg.uid}?folder=${encodeURIComponent(currentFolder)}`,{method:'DELETE'});
toast('Conversación eliminada');backToList();loadMessages();
}
function archiveMsg(){toast('Archivado');backToList();loadMessages();}
async function markCurrentMsg(flag,action){
if(!currentMsg)return;
await api(`/api/mark/${currentMsg.uid}?folder=${encodeURIComponent(currentFolder)}`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({flag,action})});
toast('Actualizado');
}
// ── Reply / Forward ───────────────────────────────────
function replyMessage(){
if(!currentMsg)return;openCompose();
document.getElementById('c-to').value=currentMsg.from;
document.getElementById('c-subject').value='Re: '+currentMsg.subject.replace(/^Re:\s*/i,'');
document.getElementById('c-body').value='\n\nEl '+currentMsg.date+', '+currentMsg.from+' escribió:\n\n'+(currentMsg.body_text||'').split('\n').map(l=>'> '+l).join('\n');
document.getElementById('compose-title').textContent='Responder';
document.getElementById('c-to').focus();
}
function forwardMessage(){
if(!currentMsg)return;openCompose();
document.getElementById('c-subject').value='Fwd: '+currentMsg.subject.replace(/^Fwd:\s*/i,'');
document.getElementById('c-body').value='\n\n---------- Mensaje reenviado ----------\nDe: '+currentMsg.from+'\nFecha: '+currentMsg.date+'\nAsunto: '+currentMsg.subject+'\nPara: '+currentMsg.to+'\n\n'+(currentMsg.body_text||'');
document.getElementById('compose-title').textContent='Reenviar';
document.getElementById('c-to').focus();
}
// ── Compose ───────────────────────────────────────────
function openCompose(){
const m=document.getElementById('compose-modal');
m.classList.add('open');m.classList.remove('minimized');
m.style.height='';m.style.maxWidth='';m.style.right='24px';m.style.left='';m.style.width='';m.style.borderRadius='';
document.getElementById('c-to').focus();
}
function closeCompose(){
document.getElementById('compose-modal').classList.remove('open','minimized');
['c-to','c-subject','c-body'].forEach(id=>document.getElementById(id).value='');
document.getElementById('compose-title').textContent='Nuevo mensaje';
attachFiles=[];document.getElementById('att-preview').innerHTML='';document.getElementById('att-input').value='';
}
let _minimized=false;
function minimizeCompose(){_minimized=!_minimized;document.getElementById('compose-modal').classList.toggle('minimized',_minimized);}
function toggleMinimize(){minimizeCompose();}
function maximizeCompose(){
const m=document.getElementById('compose-modal');
if(m.dataset.max==='1'){m.dataset.max='';m.style.maxWidth='';m.style.width='560px';m.style.height='';m.style.left='';m.style.right='24px';m.style.borderRadius='';}
else{m.dataset.max='1';m.style.maxWidth='100%';m.style.width='100%';m.style.height='100%';m.style.left='0';m.style.right='0';m.style.borderRadius='0';}
}
function addAttachments(input){Array.from(input.files).forEach(f=>attachFiles.push(f));renderAttPreviews();}
function renderAttPreviews(){
document.getElementById('att-preview').innerHTML=attachFiles.map((f,i)=>
`<div class="att-pill"><i class="fas fa-file" style="font-size:11px"></i>${escHtml(f.name)}<button onclick="removeAtt(${i})" title="Quitar"><i class="fas fa-times"></i></button></div>`
).join('');
}
function removeAtt(i){attachFiles.splice(i,1);renderAttPreviews();}
async function sendEmail(){
const to=document.getElementById('c-to').value.trim();
if(!to){toast('Indica el destinatario');return;}
const fd=new FormData();
fd.append('to',to);
fd.append('subject',document.getElementById('c-subject').value.trim()||'(Sin asunto)');
fd.append('body',document.getElementById('c-body').value);
fd.append('body_type','plain');
attachFiles.forEach(f=>fd.append('attachments',f));
const btn=document.querySelector('.compose-footer .btn-primary');
btn.innerHTML='<i class="fas fa-circle-notch fa-spin"></i> Enviando…';btn.disabled=true;
const data=await api('/api/send',{method:'POST',body:fd});
btn.innerHTML='Enviar';btn.disabled=false;
if(data.error){toast('Error: '+data.error);}else{toast('Mensaje enviado');closeCompose();}
}
// ── Search ─────────────────────────────────────────────
async function doSearch(){
const q=document.getElementById('search-input').value.trim();
document.getElementById('search-clear').style.display=q?'':'none';
if(!q){clearSearch();return;}
isSearchMode=true;backToList();
const list=document.getElementById('msg-list');
list.innerHTML='<div class="loading-wrap"><div class="spinner"></div><span>Buscando…</span></div>';
const data=await api(`/api/search?q=${encodeURIComponent(q)}&folder=${encodeURIComponent(currentFolder)}`);
allMessages=data.messages||[];
document.getElementById('page-info').textContent=allMessages.length?`${allMessages.length} resultado${allMessages.length!==1?'s':''}`:' Sin resultados';
renderMessages(allMessages);
}
function clearSearch(){
document.getElementById('search-input').value='';
document.getElementById('search-clear').style.display='none';
isSearchMode=false;loadMessages();
}
// ── Star ──────────────────────────────────────────────
async function toggleStar(uid){
const el=document.getElementById('star-'+uid);
const s=el.classList.toggle('starred');
await api(`/api/mark/${uid}?folder=${encodeURIComponent(currentFolder)}`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({flag:'flagged',action:s?'add':'remove'})});
}
// ── Refresh ───────────────────────────────────────────
function refreshList(){
const i=document.querySelector('#refresh-btn i');
if(i){i.classList.add('fa-spin');setTimeout(()=>i.classList.remove('fa-spin'),700);}
if(isSearchMode)doSearch();else loadMessages();
}
// ── Toast ─────────────────────────────────────────────
let _tt;
function toast(msg){
const t=document.getElementById('toast');t.textContent=msg;t.classList.add('show');
clearTimeout(_tt);_tt=setTimeout(()=>t.classList.remove('show'),3200);
}
</script>
</body>
</html>