mirror of
https://github.com/ghostersk/gowebmail.git
synced 2026-06-19 08:40:41 +01:00
added calendar and contact - basic
This commit is contained in:
+120
-2
@@ -14,6 +14,7 @@
|
||||
</button>
|
||||
<span class="mob-title" id="mob-title">GoWebMail</span>
|
||||
<button class="compose-btn" onclick="openCompose()" style="margin-left:auto;padding:5px 10px;font-size:11px">+ New</button>
|
||||
<button class="compose-btn" onclick="window.open('/compose','_blank')" style="padding:5px 8px;font-size:11px" title="Compose in new tab">↗</button>
|
||||
</div>
|
||||
|
||||
<!-- Sidebar -->
|
||||
@@ -23,7 +24,16 @@
|
||||
<div class="logo-icon"><svg viewBox="0 0 24 24"><path d="M20 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 4l-8 5-8-5V6l8 5 8-5v2z"/></svg></div>
|
||||
<span class="logo-text"><a href="/">GoWebMail</a></span>
|
||||
</div>
|
||||
<button class="compose-btn" onclick="openCompose()">+ New</button>
|
||||
<div style="position:relative;display:inline-flex">
|
||||
<button class="compose-btn" onclick="openCompose()" style="border-radius:6px 0 0 6px">+ New</button>
|
||||
<button class="compose-btn" onclick="toggleComposeDropdown(event)" style="border-radius:0 6px 6px 0;border-left:1px solid rgba(255,255,255,.25);padding:6px 7px" title="More options">
|
||||
<svg viewBox="0 0 24 24" width="10" height="10" fill="white"><path d="M7 10l5 5 5-5z"/></svg>
|
||||
</button>
|
||||
<div id="compose-dropdown" style="display:none;position:absolute;top:100%;left:0;margin-top:4px;background:var(--surface);border:1px solid var(--border2);border-radius:7px;box-shadow:0 4px 16px rgba(0,0,0,.2);z-index:200;min-width:200px;overflow:hidden">
|
||||
<div class="ctx-item" onclick="openCompose();closeComposeDropdown()">✉ New message</div>
|
||||
<div class="ctx-item" onclick="window.open('/compose','_blank');closeComposeDropdown()">↗ New message in new tab</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="nav-section">
|
||||
@@ -36,6 +46,14 @@
|
||||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z"/></svg>
|
||||
Starred
|
||||
</div>
|
||||
<div class="nav-item" id="nav-contacts" onclick="showContacts()">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M20 0H4v2h16V0zM0 4v18h24V4H0zm22 16H2V6h20v14zM12 11c1.66 0 3-1.34 3-3s-1.34-3-3-3-3 1.34-3 3 1.34 3 3 3zm-6 6c0-2.21 2.69-4 6-4s6 1.79 6 4H6z"/></svg>
|
||||
Contacts
|
||||
</div>
|
||||
<div class="nav-item" id="nav-calendar" onclick="showCalendar()">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M20 3h-1V1h-2v2H7V1H5v2H4c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 18H4V8h16v13z"/></svg>
|
||||
Calendar
|
||||
</div>
|
||||
<div id="folders-by-account"></div>
|
||||
</div>
|
||||
|
||||
@@ -103,6 +121,105 @@
|
||||
<p>Choose a message from the list to read it</p>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- ── Contacts panel ──────────────────────────────────────────────────── -->
|
||||
<div id="contacts-panel" style="display:none;flex:1;flex-direction:column;overflow:hidden;background:var(--bg)">
|
||||
<div class="panel-header" style="padding:14px 18px 10px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:10px;flex-shrink:0">
|
||||
<span style="font-family:'DM Serif Display',serif;font-size:17px;flex:1">Contacts</span>
|
||||
<input id="contacts-search" type="search" placeholder="Search contacts…" oninput="filterContacts(this.value)"
|
||||
style="padding:5px 10px;border:1px solid var(--border2);border-radius:6px;background:var(--surface3);color:var(--text);font-size:13px;width:200px">
|
||||
<button class="btn-secondary" onclick="openContactForm()" style="font-size:12px">+ New Contact</button>
|
||||
</div>
|
||||
<div id="contacts-list" style="flex:1;overflow-y:auto;padding:12px"></div>
|
||||
</div>
|
||||
|
||||
<!-- ── Calendar panel ──────────────────────────────────────────────────── -->
|
||||
<div id="calendar-panel" style="display:none;flex:1;flex-direction:column;overflow:hidden;background:var(--bg)">
|
||||
<div style="padding:12px 18px 10px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:8px;flex-shrink:0">
|
||||
<button class="icon-btn" onclick="calNav(-1)" title="Previous">‹</button>
|
||||
<span id="cal-title" style="font-family:'DM Serif Display',serif;font-size:17px;min-width:200px;text-align:center"></span>
|
||||
<button class="icon-btn" onclick="calNav(1)" title="Next">›</button>
|
||||
<button class="btn-secondary" onclick="calGoToday()" style="font-size:12px;margin-left:4px">Today</button>
|
||||
<div style="margin-left:auto;display:flex;gap:4px">
|
||||
<button class="btn-secondary" id="cal-btn-month" onclick="calSetView('month')" style="font-size:12px">Month</button>
|
||||
<button class="btn-secondary" id="cal-btn-week" onclick="calSetView('week')" style="font-size:12px">Week</button>
|
||||
<button class="btn-secondary" onclick="openEventForm()" style="font-size:12px;background:var(--accent);color:white;border-color:var(--accent)">+ Event</button>
|
||||
<button class="icon-btn" onclick="showCalDAVSettings()" title="CalDAV / sharing">
|
||||
<svg viewBox="0 0 24 24" width="15" height="15" fill="currentColor"><path d="M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zm-6 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zm3.1-9H8.9V6c0-1.71 1.39-3.1 3.1-3.1 1.71 0 3.1 1.39 3.1 3.1v2z"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="cal-grid" style="flex:1;overflow-y:auto"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- ── Contact form modal ──────────────────────────────────────────────────── -->
|
||||
<div class="modal-overlay" id="contact-modal">
|
||||
<div class="modal" style="max-width:480px">
|
||||
<h2 id="contact-modal-title">New Contact</h2>
|
||||
<div class="modal-field"><label>Name</label><input id="cf-name" type="text" placeholder="Full name"></div>
|
||||
<div class="modal-field"><label>Email</label><input id="cf-email" type="email" placeholder="email@example.com"></div>
|
||||
<div class="modal-field"><label>Phone</label><input id="cf-phone" type="tel" placeholder="+1 555 000 0000"></div>
|
||||
<div class="modal-field"><label>Company</label><input id="cf-company" type="text" placeholder="Company name"></div>
|
||||
<div class="modal-field"><label>Notes</label><textarea id="cf-notes" rows="3" style="width:100%;resize:vertical;padding:8px;background:var(--surface3);border:1px solid var(--border2);border-radius:6px;color:var(--text);font-family:'DM Sans',sans-serif;font-size:13px"></textarea></div>
|
||||
<div class="modal-actions">
|
||||
<button class="modal-cancel" onclick="closeModal('contact-modal')">Cancel</button>
|
||||
<button id="cf-delete-btn" class="btn-secondary" style="color:var(--danger);display:none" onclick="deleteContact()">Delete</button>
|
||||
<button class="modal-submit" onclick="saveContact()">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Event form modal ──────────────────────────────────────────────────── -->
|
||||
<div class="modal-overlay" id="event-modal">
|
||||
<div class="modal" style="max-width:520px">
|
||||
<h2 id="event-modal-title">New Event</h2>
|
||||
<div class="modal-field"><label>Title</label><input id="ev-title" type="text" placeholder="Event title"></div>
|
||||
<div class="modal-row">
|
||||
<div class="modal-field"><label>Start</label><input id="ev-start" type="datetime-local"></div>
|
||||
<div class="modal-field"><label>End</label><input id="ev-end" type="datetime-local"></div>
|
||||
</div>
|
||||
<div class="modal-field" style="flex-direction:row;align-items:center;gap:8px">
|
||||
<input id="ev-allday" type="checkbox" style="width:auto">
|
||||
<label for="ev-allday" style="font-weight:normal;color:var(--text2)">All day</label>
|
||||
</div>
|
||||
<div class="modal-field"><label>Location</label><input id="ev-location" type="text" placeholder="Location or video link"></div>
|
||||
<div class="modal-field"><label>Description</label><textarea id="ev-desc" rows="3" style="width:100%;resize:vertical;padding:8px;background:var(--surface3);border:1px solid var(--border2);border-radius:6px;color:var(--text);font-family:'DM Sans',sans-serif;font-size:13px"></textarea></div>
|
||||
<div class="modal-field"><label>Color</label>
|
||||
<div style="display:flex;gap:6px" id="ev-colors">
|
||||
<span data-color="#0078D4" onclick="selectEvColor(this)" style="width:22px;height:22px;border-radius:50%;background:#0078D4;cursor:pointer;border:2px solid transparent"></span>
|
||||
<span data-color="#EA4335" onclick="selectEvColor(this)" style="width:22px;height:22px;border-radius:50%;background:#EA4335;cursor:pointer;border:2px solid transparent"></span>
|
||||
<span data-color="#34A853" onclick="selectEvColor(this)" style="width:22px;height:22px;border-radius:50%;background:#34A853;cursor:pointer;border:2px solid transparent"></span>
|
||||
<span data-color="#FBBC04" onclick="selectEvColor(this)" style="width:22px;height:22px;border-radius:50%;background:#FBBC04;cursor:pointer;border:2px solid transparent"></span>
|
||||
<span data-color="#9C27B0" onclick="selectEvColor(this)" style="width:22px;height:22px;border-radius:50%;background:#9C27B0;cursor:pointer;border:2px solid transparent"></span>
|
||||
<span data-color="#FF6D00" onclick="selectEvColor(this)" style="width:22px;height:22px;border-radius:50%;background:#FF6D00;cursor:pointer;border:2px solid transparent"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<button class="modal-cancel" onclick="closeModal('event-modal')">Cancel</button>
|
||||
<button id="ev-delete-btn" class="btn-secondary" style="color:var(--danger);display:none" onclick="deleteEvent()">Delete</button>
|
||||
<button class="modal-submit" onclick="saveEvent()">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── CalDAV settings modal ──────────────────────────────────────────────── -->
|
||||
<div class="modal-overlay" id="caldav-modal">
|
||||
<div class="modal" style="max-width:560px">
|
||||
<h2>CalDAV / Calendar Sharing</h2>
|
||||
<p style="font-size:13px;color:var(--text2);margin-bottom:14px">
|
||||
Subscribe to your GoWebMail calendar from any CalDAV client (Apple Calendar, Thunderbird, etc.) using a token URL. Tokens give read-only calendar access — no password needed.
|
||||
</p>
|
||||
<div id="caldav-tokens-list" style="margin-bottom:14px"></div>
|
||||
<div style="display:flex;gap:8px;align-items:center">
|
||||
<input id="caldav-label" type="text" placeholder="Token label (e.g. iPhone)" style="flex:1;padding:7px 10px;background:var(--surface3);border:1px solid var(--border2);border-radius:6px;color:var(--text);font-size:13px">
|
||||
<button class="btn-secondary" onclick="createCalDAVToken()" style="white-space:nowrap">Generate Token</button>
|
||||
</div>
|
||||
<div class="modal-actions" style="margin-top:16px">
|
||||
<button class="modal-cancel" onclick="closeModal('caldav-modal')">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Accounts submenu popup ──────────────────────────────────────────────── -->
|
||||
@@ -397,5 +514,6 @@
|
||||
{{end}}
|
||||
|
||||
{{define "scripts"}}
|
||||
<script src="/static/js/app.js?v=54"></script>
|
||||
<script src="/static/js/app.js?v=58"></script>
|
||||
<script src="/static/js/contacts_calendar.js?v=58"></script>
|
||||
{{end}}
|
||||
|
||||
@@ -5,12 +5,12 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{block "title" .}}GoWebMail{{end}}</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=DM+Serif+Display&family=DM+Sans:ital,wght@0,300;0,400;0,500;1,400&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="/static/css/gowebmail.css?v=54">
|
||||
<link rel="stylesheet" href="/static/css/gowebmail.css?v=58">
|
||||
{{block "head_extra" .}}{{end}}
|
||||
</head>
|
||||
<body class="{{block "body_class" .}}{{end}}">
|
||||
{{block "body" .}}{{end}}
|
||||
<script src="/static/js/gowebmail.js?v=54"></script>
|
||||
<script src="/static/js/gowebmail.js?v=58"></script>
|
||||
{{block "scripts" .}}{{end}}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,220 @@
|
||||
{{template "base" .}}
|
||||
{{define "title"}}Compose — GoWebMail{{end}}
|
||||
{{define "body_class"}}app-page{{end}}
|
||||
|
||||
{{define "body"}}
|
||||
<div id="compose-page" style="max-width:860px;margin:0 auto;padding:20px 16px;min-height:100vh">
|
||||
<div style="display:flex;align-items:center;gap:12px;margin-bottom:18px;padding-bottom:14px;border-bottom:1px solid var(--border)">
|
||||
<a href="/" style="color:var(--accent);text-decoration:none;font-size:13px;display:flex;align-items:center;gap:4px">
|
||||
<svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor"><path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z"/></svg>
|
||||
Back to GoWebMail
|
||||
</a>
|
||||
<span style="color:var(--border);font-size:16px">|</span>
|
||||
<span id="compose-page-title" style="font-size:14px;color:var(--text2)">New Message</span>
|
||||
<div style="margin-left:auto;display:flex;gap:6px">
|
||||
<button class="btn-secondary" id="save-draft-btn" onclick="saveDraft()" style="font-size:12px">Save Draft</button>
|
||||
<button class="modal-submit" id="send-page-btn" onclick="sendFromPage()" style="font-size:13px;padding:7px 18px">Send</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="compose-page-form">
|
||||
<!-- From -->
|
||||
<div style="display:flex;align-items:center;border-bottom:1px solid var(--border);padding:8px 0;gap:8px">
|
||||
<span style="font-size:12px;color:var(--muted);width:48px;flex-shrink:0">From</span>
|
||||
<select id="cp-from" style="flex:1;background:transparent;border:none;color:var(--text);font-size:13px;outline:none;cursor:pointer"></select>
|
||||
</div>
|
||||
<!-- To -->
|
||||
<div style="display:flex;align-items:flex-start;border-bottom:1px solid var(--border);padding:8px 0;gap:8px">
|
||||
<span style="font-size:12px;color:var(--muted);width:48px;flex-shrink:0;padding-top:6px">To</span>
|
||||
<div id="cp-to-tags" class="tag-field" style="flex:1;min-height:30px"></div>
|
||||
</div>
|
||||
<!-- CC -->
|
||||
<div style="display:flex;align-items:flex-start;border-bottom:1px solid var(--border);padding:8px 0;gap:8px">
|
||||
<span style="font-size:12px;color:var(--muted);width:48px;flex-shrink:0;padding-top:6px">CC</span>
|
||||
<div id="cp-cc-tags" class="tag-field" style="flex:1;min-height:30px"></div>
|
||||
</div>
|
||||
<!-- Subject -->
|
||||
<div style="display:flex;align-items:center;border-bottom:1px solid var(--border);padding:8px 0;gap:8px">
|
||||
<span style="font-size:12px;color:var(--muted);width:48px;flex-shrink:0">Subject</span>
|
||||
<input id="cp-subject" type="text" placeholder="Subject" style="flex:1;background:transparent;border:none;color:var(--text);font-size:14px;outline:none;font-family:'DM Sans',sans-serif">
|
||||
</div>
|
||||
<!-- Body -->
|
||||
<div id="cp-editor" contenteditable="true" style="min-height:400px;padding:16px 0;outline:none;font-size:14px;line-height:1.6;color:var(--text)" data-placeholder="Write your message…"></div>
|
||||
<!-- Attachments -->
|
||||
<div style="border-top:1px solid var(--border);padding:10px 0;display:flex;align-items:center;gap:8px;flex-wrap:wrap">
|
||||
<label style="cursor:pointer;font-size:12px;color:var(--muted);display:flex;align-items:center;gap:4px">
|
||||
<svg viewBox="0 0 24 24" width="15" height="15" fill="currentColor"><path d="M16.5 6v11.5c0 2.21-1.79 4-4 4s-4-1.79-4-4V5c0-1.38 1.12-2.5 2.5-2.5s2.5 1.12 2.5 2.5v10.5c0 .55-.45 1-1 1s-1-.45-1-1V6H10v9.5c0 1.38 1.12 2.5 2.5 2.5s2.5-1.12 2.5-2.5V5c0-2.21-1.79-4-4-4S7 2.79 7 5v12.5c0 3.04 2.46 5.5 5.5 5.5s5.5-2.46 5.5-5.5V6h-1.5z"/></svg>
|
||||
Attach file
|
||||
<input type="file" multiple style="display:none" onchange="addPageAttachments(this.files)">
|
||||
</label>
|
||||
<div id="cp-att-list" style="display:flex;flex-wrap:wrap;gap:6px"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="cp-status" style="font-size:13px;color:var(--muted);margin-top:8px"></div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{define "scripts"}}
|
||||
<script>
|
||||
// Parse URL params
|
||||
const params = new URLSearchParams(location.search);
|
||||
const replyId = parseInt(params.get('reply_id') || '0');
|
||||
const forwardId = parseInt(params.get('forward_id') || '0');
|
||||
const cpAttachments = [];
|
||||
|
||||
async function apiCall(method, path, body) {
|
||||
const opts = { method, headers: {} };
|
||||
if (body instanceof FormData) { opts.body = body; }
|
||||
else if (body) { opts.body = JSON.stringify(body); opts.headers['Content-Type'] = 'application/json'; }
|
||||
const r = await fetch('/api' + path, opts);
|
||||
return r.ok ? r.json() : null;
|
||||
}
|
||||
|
||||
function esc(s) { return (s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); }
|
||||
|
||||
// Tag field (simple comma/enter separated)
|
||||
function initTagField(id) {
|
||||
const el = document.getElementById(id);
|
||||
if (!el) return;
|
||||
el.innerHTML = '<input class="tag-input" type="email" multiple style="border:none;background:transparent;outline:none;color:var(--text);font-size:13px;min-width:180px;font-family:\'DM Sans\',sans-serif">';
|
||||
const inp = el.querySelector('input');
|
||||
inp.addEventListener('keydown', e => {
|
||||
if (e.key === 'Enter' || e.key === ',' || e.key === 'Tab') {
|
||||
e.preventDefault();
|
||||
const v = inp.value.trim().replace(/,$/, '');
|
||||
if (v) addTagTo(id, v);
|
||||
inp.value = '';
|
||||
} else if (e.key === 'Backspace' && !inp.value) {
|
||||
const tags = el.querySelectorAll('.tag-chip');
|
||||
if (tags.length) tags[tags.length-1].remove();
|
||||
}
|
||||
});
|
||||
inp.addEventListener('blur', () => {
|
||||
const v = inp.value.trim().replace(/,$/, '');
|
||||
if (v) { addTagTo(id, v); inp.value = ''; }
|
||||
});
|
||||
}
|
||||
|
||||
function addTagTo(fieldId, email) {
|
||||
const el = document.getElementById(fieldId);
|
||||
const inp = el.querySelector('input');
|
||||
const chip = document.createElement('span');
|
||||
chip.className = 'tag-chip';
|
||||
chip.style.cssText = 'display:inline-flex;align-items:center;gap:4px;padding:2px 8px;background:var(--accent-dim);color:var(--accent);border-radius:12px;font-size:12px;margin:2px';
|
||||
chip.innerHTML = `${esc(email)}<span style="cursor:pointer;margin-left:2px" onclick="this.parentNode.remove()">×</span>`;
|
||||
el.insertBefore(chip, inp);
|
||||
}
|
||||
|
||||
function getTagValues(fieldId) {
|
||||
const el = document.getElementById(fieldId);
|
||||
return Array.from(el.querySelectorAll('.tag-chip')).map(c => c.textContent.replace('×','').trim()).filter(Boolean);
|
||||
}
|
||||
|
||||
function addPageAttachments(files) {
|
||||
for (const f of files) {
|
||||
cpAttachments.push(f);
|
||||
const chip = document.createElement('span');
|
||||
chip.style.cssText = 'font-size:11px;padding:3px 8px;background:var(--surface3);border:1px solid var(--border2);border-radius:4px;color:var(--text2)';
|
||||
chip.textContent = f.name;
|
||||
document.getElementById('cp-att-list').appendChild(chip);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadAccounts() {
|
||||
const accounts = await apiCall('GET', '/accounts') || [];
|
||||
const sel = document.getElementById('cp-from');
|
||||
accounts.forEach(a => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = a.id;
|
||||
opt.textContent = `${a.display_name || a.email_address} <${a.email_address}>`;
|
||||
sel.appendChild(opt);
|
||||
});
|
||||
}
|
||||
|
||||
async function prefillReply() {
|
||||
if (!replyId) return;
|
||||
document.getElementById('compose-page-title').textContent = 'Reply';
|
||||
const msg = await apiCall('GET', '/messages/' + replyId);
|
||||
if (!msg) return;
|
||||
document.title = 'Reply: ' + (msg.subject || '') + ' — GoWebMail';
|
||||
document.getElementById('cp-subject').value = msg.subject?.startsWith('Re:') ? msg.subject : 'Re: ' + (msg.subject || '');
|
||||
addTagTo('cp-to-tags', msg.from_email || '');
|
||||
const editor = document.getElementById('cp-editor');
|
||||
editor.innerHTML = `<br><br><div style="border-left:3px solid #ccc;padding-left:12px;color:#666;margin-top:8px">
|
||||
<div style="font-size:12px;margin-bottom:4px">On ${msg.date ? new Date(msg.date).toLocaleString() : ''}, ${esc(msg.from_email)} wrote:</div>
|
||||
${msg.body_html || '<pre>' + (msg.body_text||'') + '</pre>'}
|
||||
</div>`;
|
||||
// Set from to same account
|
||||
if (msg.account_id) {
|
||||
const sel = document.getElementById('cp-from');
|
||||
for (const opt of sel.options) { if (parseInt(opt.value) === msg.account_id) { opt.selected = true; break; } }
|
||||
}
|
||||
}
|
||||
|
||||
async function prefillForward() {
|
||||
if (!forwardId) return;
|
||||
document.getElementById('compose-page-title').textContent = 'Forward';
|
||||
const msg = await apiCall('GET', '/messages/' + forwardId);
|
||||
if (!msg) return;
|
||||
document.title = 'Forward: ' + (msg.subject || '') + ' — GoWebMail';
|
||||
document.getElementById('cp-subject').value = 'Fwd: ' + (msg.subject || '');
|
||||
const editor = document.getElementById('cp-editor');
|
||||
editor.innerHTML = `<br><br><div style="border-left:3px solid #ccc;padding-left:12px;color:#666;margin-top:8px">
|
||||
<div style="font-size:12px;margin-bottom:4px">---------- Forwarded message ----------<br>From: ${esc(msg.from_email)}<br>Subject: ${esc(msg.subject)}</div>
|
||||
${msg.body_html || '<pre>' + (msg.body_text||'') + '</pre>'}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
async function sendFromPage() {
|
||||
const btn = document.getElementById('send-page-btn');
|
||||
const accountId = parseInt(document.getElementById('cp-from').value || '0');
|
||||
const to = getTagValues('cp-to-tags');
|
||||
if (!accountId || !to.length) { document.getElementById('cp-status').textContent = 'From account and To address required.'; return; }
|
||||
btn.disabled = true; btn.textContent = 'Sending…';
|
||||
|
||||
const meta = {
|
||||
account_id: accountId,
|
||||
to,
|
||||
cc: getTagValues('cp-cc-tags'),
|
||||
bcc: [],
|
||||
subject: document.getElementById('cp-subject').value,
|
||||
body_html: document.getElementById('cp-editor').innerHTML,
|
||||
body_text: document.getElementById('cp-editor').innerText,
|
||||
in_reply_to_id: replyId || 0,
|
||||
forward_from_id: forwardId || 0,
|
||||
};
|
||||
|
||||
let r;
|
||||
const endpoint = replyId ? '/reply' : forwardId ? '/forward' : '/send';
|
||||
if (cpAttachments.length) {
|
||||
const fd = new FormData();
|
||||
fd.append('meta', JSON.stringify(meta));
|
||||
cpAttachments.forEach(f => fd.append('file', f, f.name));
|
||||
const resp = await fetch('/api' + endpoint, { method: 'POST', body: fd });
|
||||
r = await resp.json().catch(() => null);
|
||||
} else {
|
||||
r = await apiCall('POST', endpoint, meta);
|
||||
}
|
||||
|
||||
btn.disabled = false; btn.textContent = 'Send';
|
||||
if (r?.ok) {
|
||||
document.getElementById('cp-status').innerHTML = '✓ Message sent! <a href="/" style="color:var(--accent)">Back to inbox</a>';
|
||||
document.getElementById('compose-page-form').style.opacity = '0.5';
|
||||
document.getElementById('compose-page-form').style.pointerEvents = 'none';
|
||||
} else {
|
||||
document.getElementById('cp-status').textContent = r?.error || 'Send failed.';
|
||||
}
|
||||
}
|
||||
|
||||
async function saveDraft() {
|
||||
document.getElementById('cp-status').textContent = 'Draft saving not yet supported in standalone view.';
|
||||
}
|
||||
|
||||
// Init
|
||||
initTagField('cp-to-tags');
|
||||
initTagField('cp-cc-tags');
|
||||
loadAccounts();
|
||||
if (replyId) prefillReply();
|
||||
else if (forwardId) prefillForward();
|
||||
</script>
|
||||
{{end}}
|
||||
@@ -0,0 +1,95 @@
|
||||
{{template "base" .}}
|
||||
{{define "title"}}Message — GoWebMail{{end}}
|
||||
{{define "body_class"}}app-page{{end}}
|
||||
|
||||
{{define "body"}}
|
||||
<div id="msg-page" style="max-width:860px;margin:0 auto;padding:20px 16px;min-height:100vh">
|
||||
<div style="display:flex;align-items:center;gap:12px;margin-bottom:18px;padding-bottom:14px;border-bottom:1px solid var(--border)">
|
||||
<a href="/" style="color:var(--accent);text-decoration:none;font-size:13px;display:flex;align-items:center;gap:4px">
|
||||
<svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor"><path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z"/></svg>
|
||||
Back to GoWebMail
|
||||
</a>
|
||||
<span style="color:var(--border);font-size:16px">|</span>
|
||||
<div id="msg-actions" style="display:flex;gap:8px"></div>
|
||||
<div style="margin-left:auto;display:flex;gap:6px">
|
||||
<button class="btn-secondary" id="btn-reply" style="font-size:12px" onclick="replyFromPage()">↩ Reply</button>
|
||||
<button class="btn-secondary" id="btn-forward" style="font-size:12px" onclick="forwardFromPage()">↪ Forward</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="msg-content">
|
||||
<div class="spinner" style="margin-top:80px"></div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{define "scripts"}}
|
||||
<script>
|
||||
const msgId = parseInt(location.pathname.split('/').pop());
|
||||
|
||||
async function api(method, path, body) {
|
||||
const opts = { method, headers: {} };
|
||||
if (body) { opts.body = JSON.stringify(body); opts.headers['Content-Type'] = 'application/json'; }
|
||||
const r = await fetch('/api' + path, opts);
|
||||
return r.ok ? r.json() : null;
|
||||
}
|
||||
|
||||
function esc(s) { return (s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); }
|
||||
|
||||
async function load() {
|
||||
const msg = await api('GET', '/messages/' + msgId);
|
||||
if (!msg) { document.getElementById('msg-content').innerHTML = '<p style="color:var(--danger)">Message not found or not accessible.</p>'; return; }
|
||||
|
||||
// Mark read
|
||||
await api('PUT', '/messages/' + msgId + '/read', { read: true });
|
||||
|
||||
document.title = (msg.subject || '(no subject)') + ' — GoWebMail';
|
||||
|
||||
const atts = msg.attachments || [];
|
||||
const attHtml = atts.length ? `
|
||||
<div style="padding:12px 0;border-top:1px solid var(--border);display:flex;flex-wrap:wrap;gap:8px">
|
||||
${atts.map(a => `<a href="/api/messages/${msgId}/attachments/${a.id}" download="${esc(a.filename)}"
|
||||
style="display:inline-flex;align-items:center;gap:6px;padding:5px 10px;background:var(--surface3);
|
||||
border:1px solid var(--border2);border-radius:6px;font-size:12px;color:var(--text);text-decoration:none">
|
||||
📎 ${esc(a.filename)} <span style="color:var(--muted)">(${(a.size/1024).toFixed(0)}KB)</span></a>`).join('')}
|
||||
</div>` : '';
|
||||
|
||||
document.getElementById('msg-content').innerHTML = `
|
||||
<h1 style="font-size:22px;font-weight:600;margin-bottom:16px;line-height:1.3">${esc(msg.subject || '(no subject)')}</h1>
|
||||
<div style="display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:16px;flex-wrap:wrap;gap:8px">
|
||||
<div>
|
||||
<span style="font-size:14px;font-weight:500">${esc(msg.from_name || msg.from_email)}</span>
|
||||
${msg.from_name ? `<span style="font-size:13px;color:var(--muted)"><${esc(msg.from_email)}></span>` : ''}
|
||||
<div style="font-size:12px;color:var(--muted);margin-top:2px">To: ${esc(msg.to_list || '')}</div>
|
||||
</div>
|
||||
<span style="font-size:12px;color:var(--muted);white-space:nowrap">${esc(msg.date ? new Date(msg.date).toLocaleString() : '')}</span>
|
||||
</div>
|
||||
<div style="border:1px solid var(--border);border-radius:8px;overflow:hidden;margin-bottom:12px">
|
||||
<iframe id="msg-iframe" sandbox="allow-same-origin" style="width:100%;border:none;min-height:400px;background:white"></iframe>
|
||||
</div>
|
||||
${attHtml}`;
|
||||
|
||||
// Write body into sandboxed iframe
|
||||
const iframe = document.getElementById('msg-iframe');
|
||||
const doc = iframe.contentDocument || iframe.contentWindow.document;
|
||||
doc.open();
|
||||
doc.write(`<!DOCTYPE html><html><head><style>
|
||||
body{font-family:sans-serif;font-size:14px;line-height:1.6;padding:16px;margin:0;color:#111;word-break:break-word}
|
||||
img{max-width:100%;height:auto}a{color:#0078D4}
|
||||
</style></head><body>${msg.body_html || '<pre style="white-space:pre-wrap">' + (msg.body_text||'') + '</pre>'}</body></html>`);
|
||||
doc.close();
|
||||
// Auto-resize iframe
|
||||
setTimeout(() => {
|
||||
try { iframe.style.height = (doc.documentElement.scrollHeight + 20) + 'px'; } catch(e) {}
|
||||
}, 200);
|
||||
}
|
||||
|
||||
function replyFromPage() {
|
||||
window.location = '/?action=reply&id=' + msgId;
|
||||
}
|
||||
function forwardFromPage() {
|
||||
window.location = '/?action=forward&id=' + msgId;
|
||||
}
|
||||
|
||||
load();
|
||||
</script>
|
||||
{{end}}
|
||||
Reference in New Issue
Block a user