653 lines
37 KiB
HTML
653 lines
37 KiB
HTML
<!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"> 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,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||
}
|
||
|
||
// ── 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> ${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)} <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>
|