1154 lines
48 KiB
JavaScript
1154 lines
48 KiB
JavaScript
|
|
/**
|
|||
|
|
* pc-live | Mail App
|
|||
|
|
* Multi-mailbox: persönliches Postfach + geteilte Postfächer (ic-mail)
|
|||
|
|
*/
|
|||
|
|
const MailApp = (() => {
|
|||
|
|
const WIN_ID = 'app-mail';
|
|||
|
|
|
|||
|
|
// ── Mail state ──────────────────────────────────────────
|
|||
|
|
let _inbox = [];
|
|||
|
|
let _sent = [];
|
|||
|
|
// Ordner gehören zum Postfach, nicht zur Person: ein geteiltes Postfach
|
|||
|
|
// wird von mehreren bedient und hat für alle dieselbe Struktur.
|
|||
|
|
// Schlüssel: Adresse, '' = persönliches Postfach.
|
|||
|
|
let _foldersByBox = {};
|
|||
|
|
let _folderMails = {};
|
|||
|
|
let _calendar = [];
|
|||
|
|
let _active = null;
|
|||
|
|
let _view = 'inbox';
|
|||
|
|
let _editEvent = null;
|
|||
|
|
let _contacts = [];
|
|||
|
|
|
|||
|
|
// ── Mailbox state (ic-mail) ─────────────────────────────
|
|||
|
|
let _mailboxes = []; // [{address, display_name, type, realm_type}]
|
|||
|
|
let _personalAddr = ''; // primäre persönliche Adresse
|
|||
|
|
let _activeAddr = null; // null = persönlich, sonst Adress-String
|
|||
|
|
let _personalSig = ''; // Signatur des persönlichen Postfachs
|
|||
|
|
let _sigAddr = ''; // Postfach, dessen Signatur gerade bearbeitet wird
|
|||
|
|
let _openBoxes = []; // per Passwort freigeschaltete Postfächer (ic-web)
|
|||
|
|
|
|||
|
|
/* ── Helpers ──────────────────────────────────────────── */
|
|||
|
|
function fmtDate(d) {
|
|||
|
|
if (!d) return '';
|
|||
|
|
const dt = new Date(d);
|
|||
|
|
return isNaN(dt) ? d : dt.toLocaleString('de-DE', {
|
|||
|
|
day:'2-digit', month:'2-digit', year:'2-digit', hour:'2-digit', minute:'2-digit'
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
function fmtDateInput(d) {
|
|||
|
|
if (!d) return '';
|
|||
|
|
const dt = new Date(d);
|
|||
|
|
if (isNaN(dt)) return '';
|
|||
|
|
return dt.toISOString().slice(0, 16);
|
|||
|
|
}
|
|||
|
|
function esc(s) {
|
|||
|
|
return String(s || '').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* Adresse des gerade gewählten Postfachs, '' = persönlich. */
|
|||
|
|
function boxKey() { return _activeAddr || ''; }
|
|||
|
|
|
|||
|
|
/* Ordner des gewählten Postfachs. */
|
|||
|
|
function currentFolders() { return _foldersByBox[boxKey()] || []; }
|
|||
|
|
|
|||
|
|
/* Signatur eines Postfachs. '' wenn keine gepflegt ist. */
|
|||
|
|
function signatureFor(address) {
|
|||
|
|
if (!address || address === _personalAddr) return _personalSig || '';
|
|||
|
|
const mb = _mailboxes.find(m => m.address === address);
|
|||
|
|
return (mb && mb.signature) || '';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* Postfächer, die über "Postfach hinzufügen" freigeschaltet wurden und sich
|
|||
|
|
deshalb auch wieder entfernen lassen. Das eigene und dienstlich
|
|||
|
|
zugewiesene gehören einem nicht. */
|
|||
|
|
function isRemovable(address) {
|
|||
|
|
return _openBoxes.includes(address);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* Trennzeile vor der Signatur – wie in jedem Mailprogramm. */
|
|||
|
|
const SIG_SEP = '\n\n-- \n';
|
|||
|
|
|
|||
|
|
/* Alten Signaturblock abschneiden und den neuen anhängen. So bleibt der
|
|||
|
|
geschriebene Text erhalten, wenn man den Absender wechselt. */
|
|||
|
|
function applySignature(text, address) {
|
|||
|
|
const idx = text.lastIndexOf(SIG_SEP);
|
|||
|
|
const body = idx >= 0 ? text.slice(0, idx) : text;
|
|||
|
|
const sig = signatureFor(address);
|
|||
|
|
return sig ? body + SIG_SEP + sig : body;
|
|||
|
|
}
|
|||
|
|
function currentFolderId() {
|
|||
|
|
if (_view.startsWith('folder:')) return parseInt(_view.split(':')[1]);
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Mails des aktiven Postfachs aus dem gesamten Inbox-Array filtern
|
|||
|
|
function _mailboxFilter(mails) {
|
|||
|
|
const sharedSet = new Set(_mailboxes.filter(m => m.type !== 'personal').map(m => m.address));
|
|||
|
|
if (!_activeAddr) {
|
|||
|
|
// Persönlich: alles was NICHT zu einem geteilten Postfach gehört
|
|||
|
|
return mails.filter(m => !m.to_address || !sharedSet.has(m.to_address));
|
|||
|
|
}
|
|||
|
|
return mails.filter(m => m.to_address === _activeAddr);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function currentMails() {
|
|||
|
|
if (_view === 'inbox') return _mailboxFilter(_inbox);
|
|||
|
|
if (_view === 'sent') {
|
|||
|
|
if (!_activeAddr) {
|
|||
|
|
const sharedSet = new Set(_mailboxes.filter(m => m.type !== 'personal').map(m => m.address));
|
|||
|
|
return _sent.filter(m => !sharedSet.has(m.from_identifier));
|
|||
|
|
}
|
|||
|
|
return _sent.filter(m => m.from_identifier === _activeAddr);
|
|||
|
|
}
|
|||
|
|
const fid = currentFolderId();
|
|||
|
|
if (fid != null) return _mailboxFilter(_folderMails[fid] || []);
|
|||
|
|
return [];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* ── Mailbox icon ─────────────────────────────────────── */
|
|||
|
|
function _mbIcon(mb) {
|
|||
|
|
if (mb.type === 'personal') return '✉';
|
|||
|
|
if (mb.realm_type === 'faction') return '🏛';
|
|||
|
|
if (mb.realm_type === 'company') return '🏢';
|
|||
|
|
return '📮';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* ── Sidebar ──────────────────────────────────────────── */
|
|||
|
|
function renderSidebar() {
|
|||
|
|
const fid = currentFolderId();
|
|||
|
|
|
|||
|
|
// Unread pro Postfach berechnen
|
|||
|
|
const sharedSet = new Set(_mailboxes.filter(m => m.type !== 'personal').map(m => m.address));
|
|||
|
|
const personalUnread = _inbox.filter(m => !m.is_read && (!m.to_address || !sharedSet.has(m.to_address))).length;
|
|||
|
|
|
|||
|
|
// Persönliches Postfach
|
|||
|
|
const personalLabel = _personalAddr || 'Mein Postfach';
|
|||
|
|
const personalActive = !_activeAddr;
|
|||
|
|
let mailboxItems = `
|
|||
|
|
<div class="sidebar-item ${personalActive ? 'active' : ''}" onclick="MailApp._setMailbox(null)">
|
|||
|
|
<span>✉ ${esc(personalLabel)}</span>
|
|||
|
|
${personalUnread > 0 ? `<span class="badge" style="margin-left:auto">${personalUnread}</span>` : ''}
|
|||
|
|
</div>`;
|
|||
|
|
|
|||
|
|
// Geteilte Postfächer
|
|||
|
|
for (const mb of _mailboxes) {
|
|||
|
|
if (mb.type === 'personal') continue;
|
|||
|
|
const mbUnread = _inbox.filter(m => m.to_address === mb.address && !m.is_read).length;
|
|||
|
|
const isActive = _activeAddr === mb.address;
|
|||
|
|
const label = mb.display_name || mb.address;
|
|||
|
|
const removeBtn = isRemovable(mb.address) ? `
|
|||
|
|
<button class="sidebar-folder-del" title="Postfach entfernen"
|
|||
|
|
onclick="event.stopPropagation();MailApp._removeMailbox('${esc(mb.address)}')">✕</button>` : '';
|
|||
|
|
mailboxItems += `
|
|||
|
|
<div class="sidebar-item ${isActive ? 'active' : ''}" onclick="MailApp._setMailbox('${esc(mb.address)}')">
|
|||
|
|
<span>${_mbIcon(mb)} ${esc(label)}</span>
|
|||
|
|
${mbUnread > 0 ? `<span class="badge" style="margin-left:auto">${mbUnread}</span>` : ''}
|
|||
|
|
${removeBtn}
|
|||
|
|
</div>`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
mailboxItems += `
|
|||
|
|
<div class="sidebar-item" style="color:var(--text-dim)" onclick="MailApp._addMailbox()">
|
|||
|
|
<span>+ Postfach hinzufügen</span>
|
|||
|
|
</div>`;
|
|||
|
|
|
|||
|
|
// Navigation für aktives Postfach
|
|||
|
|
const viewMails = currentMails();
|
|||
|
|
const viewUnread = _view === 'inbox' ? viewMails.filter(m => !m.is_read).length : 0;
|
|||
|
|
|
|||
|
|
// Ordner
|
|||
|
|
let folderItems = currentFolders().map(f => `
|
|||
|
|
<div class="sidebar-item sidebar-folder-item ${_view === 'folder:' + f.id ? 'active' : ''}"
|
|||
|
|
onclick="MailApp._setView('folder:${f.id}')">
|
|||
|
|
<span>📁 ${esc(f.name)}</span>
|
|||
|
|
<button class="sidebar-folder-del" title="Ordner löschen"
|
|||
|
|
onclick="event.stopPropagation();MailApp._deleteFolder(${f.id})">✕</button>
|
|||
|
|
</div>`).join('');
|
|||
|
|
|
|||
|
|
return `
|
|||
|
|
<div class="app-sidebar" style="min-width:175px;max-width:215px">
|
|||
|
|
<div class="sidebar-section" style="display:flex;align-items:center;justify-content:space-between">
|
|||
|
|
<span>Postfächer</span>
|
|||
|
|
<button class="btn-ghost" style="padding:1px 6px;font-size:.65rem" title="Postfächer aktualisieren"
|
|||
|
|
onclick="fetchNui('getSharedMailboxes',{})">↻</button>
|
|||
|
|
</div>
|
|||
|
|
${mailboxItems}
|
|||
|
|
|
|||
|
|
<div class="divider" style="margin:8px 12px"></div>
|
|||
|
|
|
|||
|
|
<div class="sidebar-section">Navigation</div>
|
|||
|
|
<div class="sidebar-item ${_view==='inbox' ? 'active':''}" onclick="MailApp._setView('inbox')">
|
|||
|
|
📥 Posteingang ${viewUnread > 0 ? `<span class="badge" style="margin-left:auto">${viewUnread}</span>` : ''}
|
|||
|
|
</div>
|
|||
|
|
<div class="sidebar-item ${_view==='sent' ? 'active':''}" onclick="MailApp._setView('sent')">
|
|||
|
|
📤 Gesendet
|
|||
|
|
</div>
|
|||
|
|
<div class="sidebar-item ${_view==='compose' ? 'active':''}" onclick="MailApp._setView('compose')">
|
|||
|
|
✏ Schreiben
|
|||
|
|
</div>
|
|||
|
|
<div class="sidebar-item ${_view==='signature' ? 'active':''}" onclick="MailApp._setView('signature')">
|
|||
|
|
✒ Signatur
|
|||
|
|
</div>
|
|||
|
|
<div class="sidebar-section" style="margin-top:8px;display:flex;align-items:center;justify-content:space-between">
|
|||
|
|
<span>Ordner</span>
|
|||
|
|
<button class="btn-ghost" style="padding:1px 6px;font-size:.7rem" onclick="MailApp._promptNewFolder()">+</button>
|
|||
|
|
</div>
|
|||
|
|
${folderItems}
|
|||
|
|
|
|||
|
|
<div class="divider" style="margin:8px 12px"></div>
|
|||
|
|
<div class="sidebar-section">Kalender</div>
|
|||
|
|
<div class="sidebar-item ${_view==='calendar'? 'active':''}" onclick="MailApp._setView('calendar')">
|
|||
|
|
📅 Kalender
|
|||
|
|
</div>
|
|||
|
|
</div>`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* ── Mail list ────────────────────────────────────────── */
|
|||
|
|
function renderMailList(mails, isSent) {
|
|||
|
|
if (!mails.length)
|
|||
|
|
return `<div class="empty-state"><div class="empty-icon">📭</div><p>Keine Nachrichten</p></div>`;
|
|||
|
|
|
|||
|
|
const fid = currentFolderId();
|
|||
|
|
const folderOptions = currentFolders().map(f =>
|
|||
|
|
`<option value="${f.id}">${esc(f.name)}</option>`).join('');
|
|||
|
|
const moveSelect = (fid == null && !isSent) ? `
|
|||
|
|
<select class="mail-move-select" title="In Ordner verschieben"
|
|||
|
|
onchange="MailApp._moveMail(${'{id}'}, this.value); this.selectedIndex=0">
|
|||
|
|
<option value="">📁 Verschieben…</option>
|
|||
|
|
${folderOptions}
|
|||
|
|
</select>` : '';
|
|||
|
|
const toInboxBtn = (fid != null) ? `
|
|||
|
|
<button class="btn-ghost btn-sm" onclick="MailApp._moveMail(${'{id}'}, null)" title="Zurück in Posteingang">↩ Posteingang</button>` : '';
|
|||
|
|
|
|||
|
|
return `<div class="mail-list">` + mails.map(m => {
|
|||
|
|
const addr = isSent
|
|||
|
|
? esc(m.to_address || m.to_identifier)
|
|||
|
|
: esc(m.from_identifier);
|
|||
|
|
const label = isSent ? `An: ${addr}` : addr;
|
|||
|
|
const moveBtn = moveSelect.replaceAll('{id}', m.id);
|
|||
|
|
const inboxBtn = toInboxBtn.replaceAll('{id}', m.id);
|
|||
|
|
return `
|
|||
|
|
<div class="mail-item ${m.is_read || isSent ? '' : 'unread'} ${_active===m.id ? 'active' : ''}"
|
|||
|
|
onclick="MailApp._openMail(${m.id})">
|
|||
|
|
<div class="mail-from">${label}</div>
|
|||
|
|
<div class="mail-subject">${esc(m.subject)}</div>
|
|||
|
|
<div style="display:flex;align-items:center;gap:4px;margin-top:2px">
|
|||
|
|
<div class="mail-date" style="flex:1">${fmtDate(m.sent_at)}</div>
|
|||
|
|
${moveBtn}${inboxBtn}
|
|||
|
|
</div>
|
|||
|
|
</div>`;
|
|||
|
|
}).join('') + `</div>`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* ── Mail detail ──────────────────────────────────────── */
|
|||
|
|
function renderMailDetail(mail, isSent) {
|
|||
|
|
if (!mail)
|
|||
|
|
return `<div class="empty-state"><div class="empty-icon">📧</div><p>Nachricht auswählen</p></div>`;
|
|||
|
|
|
|||
|
|
const fid = currentFolderId();
|
|||
|
|
const folderOptions = currentFolders().map(f =>
|
|||
|
|
`<option value="${f.id}">${esc(f.name)}</option>`).join('');
|
|||
|
|
const moveRow = (!isSent) ? `
|
|||
|
|
<select class="btn-ghost btn-sm" style="cursor:pointer"
|
|||
|
|
onchange="MailApp._moveMail(${mail.id}, this.value||null); this.selectedIndex=0">
|
|||
|
|
<option value="">📁 Verschieben…</option>
|
|||
|
|
${folderOptions}
|
|||
|
|
${fid!=null ? `<option value="">↩ Posteingang</option>` : ''}
|
|||
|
|
</select>` : '';
|
|||
|
|
|
|||
|
|
const replyBtn = !isSent ? `<button class="btn-ghost btn-sm" onclick="MailApp._reply()">↩ Antworten</button>` : '';
|
|||
|
|
const contactMatch = _contacts.find(c => c.email === (isSent ? mail.to_identifier : mail.from_identifier));
|
|||
|
|
const contactBtn = !contactMatch ? `
|
|||
|
|
<button class="btn-ghost btn-sm" onclick="MailApp._saveAsContact('${esc(isSent ? mail.to_identifier : mail.from_identifier)}')">
|
|||
|
|
👤 Als Kontakt speichern
|
|||
|
|
</button>` : '';
|
|||
|
|
|
|||
|
|
// Zeige Postfach-Badge wenn Shared-Mail
|
|||
|
|
const mbBadge = mail.to_address && mail.to_address !== _personalAddr ? `
|
|||
|
|
<span style="font-size:.7rem;background:#1a2a1a;border:1px solid #2a4a2a;color:#4caf50;
|
|||
|
|
padding:2px 8px;border-radius:4px;margin-left:4px">
|
|||
|
|
📮 ${esc(mail.to_address)}
|
|||
|
|
</span>` : '';
|
|||
|
|
|
|||
|
|
return `
|
|||
|
|
<div class="mail-detail fade-in">
|
|||
|
|
<div class="mail-detail-header">
|
|||
|
|
<div class="mail-detail-subject">${esc(mail.subject)}${mbBadge}</div>
|
|||
|
|
<div class="mail-detail-meta">
|
|||
|
|
<span>${isSent ? 'An' : 'Von'}: ${esc(isSent ? (mail.to_address || mail.to_identifier) : mail.from_identifier)}</span>
|
|||
|
|
<span>Datum: ${fmtDate(mail.sent_at)}</span>
|
|||
|
|
</div>
|
|||
|
|
<div style="margin-top:8px;display:flex;gap:6px;flex-wrap:wrap">
|
|||
|
|
${replyBtn}
|
|||
|
|
${moveRow}
|
|||
|
|
${contactBtn}
|
|||
|
|
${!isSent ? `<button class="btn-danger btn-sm" onclick="MailApp._deleteMail(${mail.id})">🗑 Löschen</button>` : ''}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="mail-detail-body">${esc(mail.body)}</div>
|
|||
|
|
</div>`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* ── Signatur ─────────────────────────────────────────── */
|
|||
|
|
function renderSignature() {
|
|||
|
|
// Alle Postfächer, für die eine Signatur gepflegt werden kann.
|
|||
|
|
const boxes = [];
|
|||
|
|
if (_personalAddr) boxes.push({ address: _personalAddr, label: _personalAddr });
|
|||
|
|
for (const mb of _mailboxes) {
|
|||
|
|
if (mb.address === _personalAddr) continue;
|
|||
|
|
boxes.push({ address: mb.address, label: mb.display_name
|
|||
|
|
? `${mb.display_name} (${mb.address})` : mb.address });
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (!boxes.length) {
|
|||
|
|
return `<div class="empty-state"><div class="empty-icon">✒</div>
|
|||
|
|
<p>Für dieses Postfach gibt es keine Adresse.</p></div>`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Vorauswahl: das gewählte Postfach, sonst das zuletzt bearbeitete.
|
|||
|
|
if (!boxes.some(b => b.address === _sigAddr)) {
|
|||
|
|
_sigAddr = _activeAddr || _personalAddr || boxes[0].address;
|
|||
|
|
}
|
|||
|
|
const addr = _sigAddr;
|
|||
|
|
|
|||
|
|
const opts = boxes.map(b =>
|
|||
|
|
`<option value="${esc(b.address)}" ${b.address === addr ? 'selected' : ''}>${esc(b.label)}</option>`
|
|||
|
|
).join('');
|
|||
|
|
|
|||
|
|
return `
|
|||
|
|
<div class="app-content fade-in" style="max-width:640px;padding:16px">
|
|||
|
|
<h3 style="color:#fff;font-size:.9rem;margin-bottom:4px">✒ Signatur</h3>
|
|||
|
|
<div class="form-group">
|
|||
|
|
<label>Postfach</label>
|
|||
|
|
<select id="mail-sig-box" onchange="MailApp._sigBoxChanged()">${opts}</select>
|
|||
|
|
</div>
|
|||
|
|
<div style="font-size:.72rem;color:var(--text-muted);margin-bottom:12px">
|
|||
|
|
Die Signatur gehört zum Postfach: Wer es bedient, schreibt mit derselben
|
|||
|
|
Fußzeile.
|
|||
|
|
</div>
|
|||
|
|
<div class="form-group">
|
|||
|
|
<textarea id="mail-signature" rows="7" maxlength="1000"
|
|||
|
|
placeholder="z. B. Mit freundlichen Grüßen Redaktion Weazel News presse@weazel-news.ls"
|
|||
|
|
>${esc(signatureFor(addr))}</textarea>
|
|||
|
|
</div>
|
|||
|
|
<div style="display:flex;gap:8px">
|
|||
|
|
<button class="btn-primary" onclick="MailApp._saveSignature()">Speichern</button>
|
|||
|
|
<button class="btn-ghost" onclick="MailApp._setView('inbox')">Zurück</button>
|
|||
|
|
</div>
|
|||
|
|
<div id="mail-sig-status" style="font-size:.72rem;margin-top:6px;color:var(--text-muted)"></div>
|
|||
|
|
<div style="font-size:.68rem;color:var(--text-dim);margin-top:14px;line-height:1.5">
|
|||
|
|
Beim Schreiben wird die Signatur unter den Text gesetzt, getrennt durch eine
|
|||
|
|
Zeile mit <code>--</code>. Wechselst du den Absender, tauscht sie sich mit.
|
|||
|
|
</div>
|
|||
|
|
</div>`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function _sigBoxChanged() {
|
|||
|
|
const sel = document.getElementById('mail-sig-box');
|
|||
|
|
if (!sel) return;
|
|||
|
|
_sigAddr = sel.value;
|
|||
|
|
_refresh();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function _saveSignature() {
|
|||
|
|
const addr = _sigAddr || _activeAddr || _personalAddr;
|
|||
|
|
const el = document.getElementById('mail-signature');
|
|||
|
|
const st = document.getElementById('mail-sig-status');
|
|||
|
|
if (!addr || !el) return;
|
|||
|
|
|
|||
|
|
if (st) st.textContent = 'Wird gespeichert…';
|
|||
|
|
fetchNui('setSignature', { address: addr, signature: el.value });
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function onSignatureSaved(data) {
|
|||
|
|
const st = document.getElementById('mail-sig-status');
|
|||
|
|
if (!st) return;
|
|||
|
|
st.textContent = (data && data.ok)
|
|||
|
|
? 'Gespeichert.'
|
|||
|
|
: '⚠ ' + ((data && data.error) || 'Speichern fehlgeschlagen.');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* ── Postfächer freischalten (ic-web) ─────────────────── */
|
|||
|
|
/* Ein Postfach ist ein Zugang mit Passwort. Wer die Daten kennt, bekommt es
|
|||
|
|
hier dazu – dieselbe Prüfung wie bei der Anmeldung am Webhosting. */
|
|||
|
|
function _addMailbox() {
|
|||
|
|
if (typeof ICWebRender === 'undefined') {
|
|||
|
|
return Desktop.showNotification('⚠ Postfachverwaltung nicht verfügbar.');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
ICWebRender.modal('Postfach hinzufügen', (box, close) => {
|
|||
|
|
const el = ICWebRender.el;
|
|||
|
|
box.appendChild(el('div', 'icweb-modal-sub',
|
|||
|
|
'Adresse und Passwort des Postfachs. Es bleibt geöffnet, bis du es '
|
|||
|
|
+ 'entfernst oder den Server verlässt.'));
|
|||
|
|
|
|||
|
|
const mkField = (label, type, placeholder) => {
|
|||
|
|
const row = el('div', 'wh-field');
|
|||
|
|
row.appendChild(el('label', 'wh-label', label));
|
|||
|
|
const input = el('input', 'icweb-input');
|
|||
|
|
input.setAttribute('type', type);
|
|||
|
|
if (placeholder) input.setAttribute('placeholder', placeholder);
|
|||
|
|
row.appendChild(input);
|
|||
|
|
box.appendChild(row);
|
|||
|
|
return input;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const addr = mkField('Adresse', 'text', 'presse@weazel-news.ls');
|
|||
|
|
const pass = mkField('Passwort', 'password', '');
|
|||
|
|
const err = el('div', 'wh-login-error');
|
|||
|
|
box.appendChild(err);
|
|||
|
|
|
|||
|
|
const row = el('div', 'icweb-modal-actions');
|
|||
|
|
const cancel = el('button', 'icweb-btn-ghost', 'Abbrechen');
|
|||
|
|
cancel.addEventListener('click', close);
|
|||
|
|
|
|||
|
|
const go = el('button', 'icweb-btn-primary', 'Hinzufügen');
|
|||
|
|
const submit = async () => {
|
|||
|
|
err.textContent = '';
|
|||
|
|
if (!addr.value || !pass.value) {
|
|||
|
|
err.textContent = 'Bitte Adresse und Passwort eingeben.';
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
go.disabled = true;
|
|||
|
|
const res = await ICWebNet.call('openMailbox', addr.value, pass.value);
|
|||
|
|
go.disabled = false;
|
|||
|
|
pass.value = '';
|
|||
|
|
|
|||
|
|
if (!res.ok) { err.textContent = res.error || 'Fehlgeschlagen.'; return; }
|
|||
|
|
close();
|
|||
|
|
refreshMailboxes();
|
|||
|
|
};
|
|||
|
|
go.addEventListener('click', submit);
|
|||
|
|
[addr, pass].forEach(i =>
|
|||
|
|
i.addEventListener('keydown', e => { if (e.key === 'Enter') submit(); }));
|
|||
|
|
|
|||
|
|
row.append(cancel, go);
|
|||
|
|
box.appendChild(row);
|
|||
|
|
setTimeout(() => addr.focus(), 0);
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function _removeMailbox(address) {
|
|||
|
|
ICWebRender.confirmBox('Postfach entfernen',
|
|||
|
|
'Postfach ' + address + ' entfernen?\n'
|
|||
|
|
+ 'Die Nachrichten bleiben erhalten, du siehst sie nur nicht mehr.', async () => {
|
|||
|
|
const res = await ICWebNet.call('closeMailbox', address);
|
|||
|
|
if (!res.ok) return Desktop.showNotification('⚠ ' + (res.error || 'Fehlgeschlagen.'));
|
|||
|
|
|
|||
|
|
if (_activeAddr === address) _activeAddr = null;
|
|||
|
|
refreshMailboxes();
|
|||
|
|
}, { danger: true, okLabel: 'Entfernen' });
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* Liste der freigeschalteten Postfächer holen und die Mailboxliste neu
|
|||
|
|
laden. Beides zusammen, damit die ✕-Knöpfe zur Anzeige passen. */
|
|||
|
|
async function refreshMailboxes() {
|
|||
|
|
if (typeof ICWebNet !== 'undefined') {
|
|||
|
|
const res = await ICWebNet.call('listMailboxes');
|
|||
|
|
_openBoxes = (res.ok && Array.isArray(res.data)) ? res.data : [];
|
|||
|
|
}
|
|||
|
|
fetchNui('getSharedMailboxes', {});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* ── Compose ──────────────────────────────────────────── */
|
|||
|
|
function renderCompose(prefillTo = '', prefillSubject = '') {
|
|||
|
|
// Von-Dropdown: alle Postfächer aus denen der Spieler senden darf
|
|||
|
|
const fromAddresses = [];
|
|||
|
|
if (_personalAddr) fromAddresses.push({ address: _personalAddr, label: _personalAddr });
|
|||
|
|
for (const mb of _mailboxes) {
|
|||
|
|
if (mb.type !== 'personal') {
|
|||
|
|
const lbl = mb.display_name ? `${mb.display_name} (${mb.address})` : mb.address;
|
|||
|
|
fromAddresses.push({ address: mb.address, label: lbl });
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
const defaultFrom = _activeAddr || _personalAddr || '';
|
|||
|
|
|
|||
|
|
let fromField = '';
|
|||
|
|
if (fromAddresses.length > 1) {
|
|||
|
|
const opts = fromAddresses.map(o =>
|
|||
|
|
`<option value="${esc(o.address)}" ${o.address === defaultFrom ? 'selected' : ''}>${esc(o.label)}</option>`
|
|||
|
|
).join('');
|
|||
|
|
fromField = `
|
|||
|
|
<div class="form-group">
|
|||
|
|
<label>Von</label>
|
|||
|
|
<select id="mail-from" onchange="MailApp._fromChanged()">${opts}</select>
|
|||
|
|
</div>`;
|
|||
|
|
} else if (fromAddresses.length === 1) {
|
|||
|
|
fromField = `<input type="hidden" id="mail-from" value="${esc(fromAddresses[0].address)}"/>`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const contactOptions = _contacts.map(c =>
|
|||
|
|
`<option value="${esc(c.email || c.name)}">${esc(c.name)}${c.email ? ' <' + esc(c.email) + '>' : ''}</option>`
|
|||
|
|
).join('');
|
|||
|
|
const datalist = _contacts.length ? `
|
|||
|
|
<datalist id="mail-contacts-list">${contactOptions}</datalist>` : '';
|
|||
|
|
|
|||
|
|
return `
|
|||
|
|
<div class="app-content fade-in" style="max-width:640px;padding:16px">
|
|||
|
|
<h3 style="color:#fff;font-size:.9rem;margin-bottom:12px">✏ Neue Nachricht</h3>
|
|||
|
|
${datalist}
|
|||
|
|
${fromField}
|
|||
|
|
<div class="form-group">
|
|||
|
|
<label>An (Adresse oder Kontakt)</label>
|
|||
|
|
<input id="mail-to" type="text" list="mail-contacts-list"
|
|||
|
|
placeholder="name@domain… oder Kontakt wählen"
|
|||
|
|
value="${esc(prefillTo)}"/>
|
|||
|
|
</div>
|
|||
|
|
<div class="form-group">
|
|||
|
|
<label>Betreff</label>
|
|||
|
|
<input id="mail-subject" type="text" placeholder="Betreff…" maxlength="80"
|
|||
|
|
value="${esc(prefillSubject)}"/>
|
|||
|
|
</div>
|
|||
|
|
<div class="form-group">
|
|||
|
|
<label>Nachricht</label>
|
|||
|
|
<textarea id="mail-body" rows="9" placeholder="Nachricht schreiben…" maxlength="2000"
|
|||
|
|
>${esc(applySignature('', defaultFrom))}</textarea>
|
|||
|
|
</div>
|
|||
|
|
<div style="display:flex;gap:8px">
|
|||
|
|
<button class="btn-primary" onclick="MailApp._sendMail()">📤 Senden</button>
|
|||
|
|
<button class="btn-ghost" onclick="MailApp._setView('inbox')">Abbrechen</button>
|
|||
|
|
</div>
|
|||
|
|
<div id="mail-send-status" style="font-size:.72rem;margin-top:6px;color:var(--text-muted)"></div>
|
|||
|
|
</div>`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* ── Calendar ─────────────────────────────────────────── */
|
|||
|
|
/* Kalender, in die diese Person eintragen darf: privat und jedes Postfach. */
|
|||
|
|
function writableCalendars() {
|
|||
|
|
const out = [{ address: '', label: '🔒 Privat' }];
|
|||
|
|
for (const mb of _mailboxes) {
|
|||
|
|
out.push({
|
|||
|
|
address: mb.address,
|
|||
|
|
label: '👥 ' + (mb.display_name || mb.address),
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
return out;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* Kurzes Schild am Termin, damit man sieht, wessen Kalender er gehört. */
|
|||
|
|
function calBadge(e) {
|
|||
|
|
if (e.visibility === 'public') return { text: '🌐 Öffentlich', title: e.address || '' };
|
|||
|
|
if (e.address) {
|
|||
|
|
const mb = _mailboxes.find(m => m.address === e.address);
|
|||
|
|
return { text: '👥 ' + ((mb && mb.display_name) || e.address), title: e.address };
|
|||
|
|
}
|
|||
|
|
return { text: '🔒 Privat', title: '' };
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function renderCalendar() {
|
|||
|
|
const now = new Date();
|
|||
|
|
const year = now.getFullYear();
|
|||
|
|
const month = now.getMonth();
|
|||
|
|
|
|||
|
|
const firstDay = new Date(year, month, 1).getDay();
|
|||
|
|
const daysInMonth = new Date(year, month + 1, 0).getDate();
|
|||
|
|
const monthNames = ['Januar','Februar','März','April','Mai','Juni',
|
|||
|
|
'Juli','August','September','Oktober','November','Dezember'];
|
|||
|
|
|
|||
|
|
let cells = '';
|
|||
|
|
const dayHeaders = ['Mo','Di','Mi','Do','Fr','Sa','So'].map(d =>
|
|||
|
|
`<div class="cal-header-cell">${d}</div>`).join('');
|
|||
|
|
|
|||
|
|
const offset = (firstDay + 6) % 7;
|
|||
|
|
for (let i = 0; i < offset; i++) cells += `<div class="cal-cell cal-cell-empty"></div>`;
|
|||
|
|
|
|||
|
|
for (let d = 1; d <= daysInMonth; d++) {
|
|||
|
|
const dateStr = `${year}-${String(month+1).padStart(2,'0')}-${String(d).padStart(2,'0')}`;
|
|||
|
|
const dayEvents = _calendar.filter(e => e.start_at && e.start_at.startsWith(dateStr));
|
|||
|
|
const dots = dayEvents.map(e =>
|
|||
|
|
`<div class="cal-dot" style="background:${esc(e.color||'#00aaff')}" title="${esc(e.title)}"></div>`
|
|||
|
|
).join('');
|
|||
|
|
const isToday = (d === now.getDate());
|
|||
|
|
cells += `
|
|||
|
|
<div class="cal-cell ${isToday ? 'cal-today' : ''}" onclick="MailApp._calDayClick('${dateStr}')">
|
|||
|
|
<span class="cal-day-num">${d}</span>
|
|||
|
|
<div class="cal-dots">${dots}</div>
|
|||
|
|
</div>`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const upcomingItems = _calendar
|
|||
|
|
.filter(e => new Date(e.start_at) >= new Date(year, month, 1))
|
|||
|
|
.slice(0, 8)
|
|||
|
|
.map(e => {
|
|||
|
|
const badge = calBadge(e);
|
|||
|
|
// Fremde öffentliche Termine sieht man, ändern darf sie nur, wer das
|
|||
|
|
// zugehörige Postfach bedient.
|
|||
|
|
const actions = e.can_edit ? `
|
|||
|
|
<div style="margin-top:4px;display:flex;gap:4px">
|
|||
|
|
<button class="btn-ghost btn-sm" onclick="MailApp._openCalEdit(${e.id})">✏</button>
|
|||
|
|
<button class="btn-danger btn-sm" onclick="MailApp._deleteCalEvent(${e.id})">🗑</button>
|
|||
|
|
</div>` : '';
|
|||
|
|
return `
|
|||
|
|
<div class="cal-event-item" style="border-left:3px solid ${esc(e.color||'#00aaff')}">
|
|||
|
|
<div style="display:flex;align-items:center;gap:6px">
|
|||
|
|
<div style="font-weight:600;font-size:.78rem;flex:1">${esc(e.title)}</div>
|
|||
|
|
<span class="cal-badge" title="${esc(badge.title)}">${esc(badge.text)}</span>
|
|||
|
|
</div>
|
|||
|
|
<div style="font-size:.7rem;color:var(--text-muted)">${fmtDate(e.start_at)}${e.end_at ? ' – ' + fmtDate(e.end_at) : ''}</div>
|
|||
|
|
${e.description ? `<div style="font-size:.7rem;margin-top:2px;color:var(--text-muted)">${esc(e.description)}</div>` : ''}
|
|||
|
|
${actions}
|
|||
|
|
</div>`;
|
|||
|
|
}).join('');
|
|||
|
|
|
|||
|
|
const editForm = _editEvent ? `
|
|||
|
|
<div class="cal-edit-form fade-in">
|
|||
|
|
<h4 style="font-size:.82rem;margin-bottom:8px;color:#fff">
|
|||
|
|
${_editEvent.id ? 'Eintrag bearbeiten' : 'Neuer Eintrag'}
|
|||
|
|
</h4>
|
|||
|
|
<div class="form-group">
|
|||
|
|
<label>Titel</label>
|
|||
|
|
<input id="cal-title" type="text" value="${esc(_editEvent.title||'')}" maxlength="120"/>
|
|||
|
|
</div>
|
|||
|
|
<div class="form-group">
|
|||
|
|
<label>Beschreibung</label>
|
|||
|
|
<input id="cal-desc" type="text" value="${esc(_editEvent.description||'')}" maxlength="500"/>
|
|||
|
|
</div>
|
|||
|
|
<div class="form-group" style="display:flex;gap:8px">
|
|||
|
|
<div style="flex:1">
|
|||
|
|
<label>Von</label>
|
|||
|
|
<input id="cal-start" type="datetime-local" value="${fmtDateInput(_editEvent.start_at)}"/>
|
|||
|
|
</div>
|
|||
|
|
<div style="flex:1">
|
|||
|
|
<label>Bis (optional)</label>
|
|||
|
|
<input id="cal-end" type="datetime-local" value="${fmtDateInput(_editEvent.end_at)}"/>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="form-group">
|
|||
|
|
<label>Kalender</label>
|
|||
|
|
<select id="cal-address" onchange="MailApp._calAddressChanged()">
|
|||
|
|
${writableCalendars().map(c =>
|
|||
|
|
`<option value="${esc(c.address)}" ${c.address === (_editEvent.address||'') ? 'selected' : ''}>${esc(c.label)}</option>`
|
|||
|
|
).join('')}
|
|||
|
|
</select>
|
|||
|
|
</div>
|
|||
|
|
${(_editEvent.address || '') !== '' ? `
|
|||
|
|
<div class="form-group">
|
|||
|
|
<label>Sichtbar für</label>
|
|||
|
|
<select id="cal-visibility">
|
|||
|
|
<option value="shared" ${_editEvent.visibility !== 'public' ? 'selected' : ''}>Nur wer dieses Postfach bedient</option>
|
|||
|
|
<option value="public" ${_editEvent.visibility === 'public' ? 'selected' : ''}>Alle (öffentlicher Termin)</option>
|
|||
|
|
</select>
|
|||
|
|
</div>` : `
|
|||
|
|
<div style="font-size:.68rem;color:var(--text-dim);margin:-4px 0 8px">
|
|||
|
|
Ein privater Termin ist nur für dich sichtbar.
|
|||
|
|
</div>`}
|
|||
|
|
<div class="form-group">
|
|||
|
|
<label>Farbe</label>
|
|||
|
|
<input id="cal-color" type="color" value="${_editEvent.color||'#00aaff'}" style="height:28px;cursor:pointer"/>
|
|||
|
|
</div>
|
|||
|
|
<div style="display:flex;gap:6px">
|
|||
|
|
<button class="btn-primary" onclick="MailApp._saveCalEvent()">💾 Speichern</button>
|
|||
|
|
<button class="btn-ghost" onclick="MailApp._cancelCalEdit()">Abbrechen</button>
|
|||
|
|
</div>
|
|||
|
|
</div>` : `
|
|||
|
|
<button class="btn-primary" style="margin-top:8px;width:100%" onclick="MailApp._newCalEvent()">
|
|||
|
|
+ Neuer Eintrag
|
|||
|
|
</button>`;
|
|||
|
|
|
|||
|
|
return `
|
|||
|
|
<div style="display:flex;height:100%;gap:0;overflow:hidden">
|
|||
|
|
<div style="flex:1;display:flex;flex-direction:column;overflow:hidden;border-right:1px solid var(--border)">
|
|||
|
|
<div class="cal-month-header">📅 ${monthNames[month]} ${year}</div>
|
|||
|
|
<div class="cal-grid">
|
|||
|
|
${dayHeaders}
|
|||
|
|
${cells}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div style="width:280px;display:flex;flex-direction:column;overflow-y:auto;padding:10px;gap:8px">
|
|||
|
|
${editForm}
|
|||
|
|
<div class="sidebar-section" style="margin-top:8px">Kommende Termine</div>
|
|||
|
|
${upcomingItems || '<div style="color:var(--text-dim);font-size:.75rem;padding:4px">Keine Termine</div>'}
|
|||
|
|
</div>
|
|||
|
|
</div>`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* ── Full layout refresh ──────────────────────────────── */
|
|||
|
|
function _refresh() {
|
|||
|
|
const body = WindowManager.getBody(WIN_ID);
|
|||
|
|
if (!body) return;
|
|||
|
|
|
|||
|
|
const isSent = _view === 'sent';
|
|||
|
|
const mails = currentMails();
|
|||
|
|
const activeMail = mails.find(m => m.id === _active) || null;
|
|||
|
|
|
|||
|
|
let rightContent;
|
|||
|
|
if (_view === 'compose') {
|
|||
|
|
rightContent = renderCompose();
|
|||
|
|
} else if (_view === 'signature') {
|
|||
|
|
rightContent = renderSignature();
|
|||
|
|
} else if (_view === 'calendar') {
|
|||
|
|
rightContent = renderCalendar();
|
|||
|
|
} else {
|
|||
|
|
rightContent = `
|
|||
|
|
<div style="display:flex;height:100%;overflow:hidden">
|
|||
|
|
<div style="width:280px;border-right:1px solid var(--border);overflow-y:auto;flex-shrink:0">
|
|||
|
|
${renderMailList(mails, isSent)}
|
|||
|
|
</div>
|
|||
|
|
<div style="flex:1;overflow-y:auto">
|
|||
|
|
${renderMailDetail(activeMail, isSent)}
|
|||
|
|
</div>
|
|||
|
|
</div>`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
body.innerHTML = `
|
|||
|
|
<div class="app-layout" style="height:100%">
|
|||
|
|
${renderSidebar()}
|
|||
|
|
<div style="flex:1;overflow:hidden;display:flex;flex-direction:column">
|
|||
|
|
${rightContent}
|
|||
|
|
</div>
|
|||
|
|
</div>`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* ── Mailbox actions ──────────────────────────────────── */
|
|||
|
|
function _setMailbox(address) {
|
|||
|
|
_activeAddr = address || null;
|
|||
|
|
_view = 'inbox';
|
|||
|
|
_active = null;
|
|||
|
|
_editEvent = null;
|
|||
|
|
|
|||
|
|
// Jedes Postfach hat eigene Ordner – die des vorherigen passen nicht.
|
|||
|
|
if (_foldersByBox[boxKey()] === undefined) {
|
|||
|
|
fetchNui('getFolders', { address: boxKey() });
|
|||
|
|
}
|
|||
|
|
_refresh();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function initMailboxes(mailboxes, personalAddress, personalSignature) {
|
|||
|
|
_mailboxes = mailboxes || [];
|
|||
|
|
_personalAddr = personalAddress || '';
|
|||
|
|
_personalSig = personalSignature || '';
|
|||
|
|
const body = WindowManager.getBody(WIN_ID);
|
|||
|
|
if (body) _refresh();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function onSharedMailboxes(mailboxes, personalAddress, personalSignature) {
|
|||
|
|
_mailboxes = mailboxes || [];
|
|||
|
|
_personalAddr = personalAddress || '';
|
|||
|
|
_personalSig = personalSignature || '';
|
|||
|
|
|
|||
|
|
// Ein neu geöffnetes Postfach bringt seinen Kalender mit.
|
|||
|
|
fetchNui('getCalendar', {});
|
|||
|
|
|
|||
|
|
const body = WindowManager.getBody(WIN_ID);
|
|||
|
|
if (body) _refresh();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* ── View actions ─────────────────────────────────────── */
|
|||
|
|
function _setView(v) {
|
|||
|
|
_view = v;
|
|||
|
|
_active = null;
|
|||
|
|
_editEvent = null;
|
|||
|
|
if (v === 'sent' && !_sent.length) fetchNui('getSent', {});
|
|||
|
|
if (v === 'calendar' && !_calendar.length) fetchNui('getCalendar', {});
|
|||
|
|
if (v.startsWith('folder:')) {
|
|||
|
|
const fid = parseInt(v.split(':')[1]);
|
|||
|
|
if (!_folderMails[fid]) fetchNui('getFolderMails', { folderId: fid, address: boxKey() });
|
|||
|
|
}
|
|||
|
|
_refresh();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function _openMail(id) {
|
|||
|
|
_active = id;
|
|||
|
|
const isSent = _view === 'sent';
|
|||
|
|
if (!isSent) {
|
|||
|
|
fetchNui('readMail', { id });
|
|||
|
|
const m = currentMails().find(m => m.id === id);
|
|||
|
|
if (m) m.is_read = 1;
|
|||
|
|
}
|
|||
|
|
_refresh();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* Absender gewechselt: alten Signaturblock ersetzen, Text behalten. */
|
|||
|
|
function _fromChanged() {
|
|||
|
|
const from = document.getElementById('mail-from')?.value || '';
|
|||
|
|
const body = document.getElementById('mail-body');
|
|||
|
|
if (!body) return;
|
|||
|
|
|
|||
|
|
const pos = body.selectionStart;
|
|||
|
|
body.value = applySignature(body.value, from);
|
|||
|
|
if (pos != null) { try { body.setSelectionRange(pos, pos); } catch (e) {} }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function _sendMail() {
|
|||
|
|
const to = (document.getElementById('mail-to')?.value || '').trim();
|
|||
|
|
const subject = (document.getElementById('mail-subject')?.value || '').trim();
|
|||
|
|
const body = (document.getElementById('mail-body')?.value || '').trim();
|
|||
|
|
const from = document.getElementById('mail-from')?.value || '';
|
|||
|
|
const status = document.getElementById('mail-send-status');
|
|||
|
|
if (!to || !subject || !body) {
|
|||
|
|
if (status) status.textContent = 'Bitte alle Felder ausfüllen.';
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
if (status) status.textContent = 'Wird gesendet…';
|
|||
|
|
fetchNui('sendMail', { to, subject, body, fromMailbox: from });
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function _reply() {
|
|||
|
|
const mail = currentMails().find(m => m.id === _active);
|
|||
|
|
if (!mail) return;
|
|||
|
|
_view = 'compose';
|
|||
|
|
_refresh();
|
|||
|
|
setTimeout(() => {
|
|||
|
|
const t = document.getElementById('mail-to');
|
|||
|
|
const s = document.getElementById('mail-subject');
|
|||
|
|
if (t) t.value = mail.from_identifier || '';
|
|||
|
|
if (s) s.value = 'RE: ' + (mail.subject || '');
|
|||
|
|
}, 0);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function _deleteMail(id) { fetchNui('deleteMail', { id }); }
|
|||
|
|
|
|||
|
|
function _moveMail(id, folderId) {
|
|||
|
|
fetchNui('moveMail', {
|
|||
|
|
id,
|
|||
|
|
folderId: folderId === '' || folderId === 'null' ? null : folderId,
|
|||
|
|
address: boxKey(),
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function _promptNewFolder() {
|
|||
|
|
// Kein prompt(): FiveMs CEF hat keinen Handler für die eingebauten
|
|||
|
|
// JS-Dialoge, das NUI bliebe stehen.
|
|||
|
|
const box = boxKey();
|
|||
|
|
ICWebRender.promptText('Neuer Ordner', {
|
|||
|
|
hint: box ? 'Der Ordner gehört zu ' + box + ' – alle, die dieses Postfach '
|
|||
|
|
+ 'bedienen, sehen ihn.'
|
|||
|
|
: 'Der Ordner gehört zu deinem persönlichen Postfach.',
|
|||
|
|
placeholder: 'z. B. Anfragen',
|
|||
|
|
okLabel: 'Anlegen',
|
|||
|
|
}, (name) => {
|
|||
|
|
fetchNui('createFolder', { name, address: box });
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function _deleteFolder(id) {
|
|||
|
|
const box = boxKey();
|
|||
|
|
ICWebRender.confirmBox('Ordner löschen',
|
|||
|
|
'E-Mails kommen zurück in den Posteingang.'
|
|||
|
|
+ (box ? '\nDer Ordner verschwindet für alle, die ' + box + ' bedienen.' : ''),
|
|||
|
|
() => fetchNui('deleteFolder', { id, address: box }),
|
|||
|
|
{ danger: true, okLabel: 'Löschen' });
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function _saveAsContact(emailOrId) {
|
|||
|
|
AddressBookApp.openAddNew({ email: emailOrId });
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* ── Calendar actions ─────────────────────────────────── */
|
|||
|
|
/* Vorbelegung: der Kalender des gerade gewählten Postfachs. Wer im
|
|||
|
|
Firmenpostfach arbeitet, trägt meist auch dort ein. */
|
|||
|
|
function newCalDraft(start) {
|
|||
|
|
return {
|
|||
|
|
title: '', description: '', start_at: start || '', end_at: '',
|
|||
|
|
color: '#00aaff', address: boxKey(),
|
|||
|
|
visibility: boxKey() ? 'shared' : 'private',
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function _newCalEvent(prefillDate) {
|
|||
|
|
_editEvent = newCalDraft(prefillDate);
|
|||
|
|
_refresh();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function _calDayClick(dateStr) {
|
|||
|
|
if (_view !== 'calendar') return;
|
|||
|
|
_editEvent = newCalDraft(dateStr + 'T08:00');
|
|||
|
|
_refresh();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* Kalender gewechselt: Formular neu zeichnen, damit die Sichtbarkeitsauswahl
|
|||
|
|
erscheint bzw. verschwindet. Eingaben bleiben erhalten. */
|
|||
|
|
function _calAddressChanged() {
|
|||
|
|
if (!_editEvent) return;
|
|||
|
|
readCalForm();
|
|||
|
|
const sel = document.getElementById('cal-address');
|
|||
|
|
_editEvent.address = sel ? sel.value : '';
|
|||
|
|
if (!_editEvent.address) _editEvent.visibility = 'private';
|
|||
|
|
else if (_editEvent.visibility === 'private') _editEvent.visibility = 'shared';
|
|||
|
|
_refresh();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* Aktuelle Formularwerte in den Entwurf übernehmen. */
|
|||
|
|
function readCalForm() {
|
|||
|
|
if (!_editEvent) return;
|
|||
|
|
const v = (id) => document.getElementById(id)?.value;
|
|||
|
|
_editEvent.title = v('cal-title') ?? _editEvent.title;
|
|||
|
|
_editEvent.description = v('cal-desc') ?? _editEvent.description;
|
|||
|
|
_editEvent.start_at = v('cal-start') ?? _editEvent.start_at;
|
|||
|
|
_editEvent.end_at = v('cal-end') ?? _editEvent.end_at;
|
|||
|
|
_editEvent.color = v('cal-color') ?? _editEvent.color;
|
|||
|
|
const vis = v('cal-visibility');
|
|||
|
|
if (vis) _editEvent.visibility = vis;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function _openCalEdit(id) {
|
|||
|
|
const ev = _calendar.find(e => e.id === id);
|
|||
|
|
if (!ev) return;
|
|||
|
|
if (!ev.can_edit) {
|
|||
|
|
return Desktop.showNotification('⚠ Diesen Termin darfst du nicht ändern.');
|
|||
|
|
}
|
|||
|
|
_editEvent = { ...ev, address: ev.address || '' };
|
|||
|
|
_refresh();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function _deleteCalEvent(id) {
|
|||
|
|
const ev = _calendar.find(e => e.id === id);
|
|||
|
|
if (!ev || !ev.can_edit) {
|
|||
|
|
return Desktop.showNotification('⚠ Diesen Termin darfst du nicht löschen.');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const badge = calBadge(ev);
|
|||
|
|
ICWebRender.confirmBox('Termin löschen',
|
|||
|
|
'„' + ev.title + '" wirklich löschen?'
|
|||
|
|
+ (ev.address ? '\nDer Termin verschwindet für alle in ' + badge.text + '.' : ''),
|
|||
|
|
() => fetchNui('deleteCalendarEvent', { id }),
|
|||
|
|
{ danger: true, okLabel: 'Löschen' });
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function _cancelCalEdit() {
|
|||
|
|
_editEvent = null;
|
|||
|
|
_refresh();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function _saveCalEvent() {
|
|||
|
|
const title = document.getElementById('cal-title')?.value.trim();
|
|||
|
|
const desc = document.getElementById('cal-desc')?.value.trim();
|
|||
|
|
const start = document.getElementById('cal-start')?.value;
|
|||
|
|
const end = document.getElementById('cal-end')?.value;
|
|||
|
|
const color = document.getElementById('cal-color')?.value || '#00aaff';
|
|||
|
|
const address = document.getElementById('cal-address')?.value || '';
|
|||
|
|
const visibility = address
|
|||
|
|
? (document.getElementById('cal-visibility')?.value || 'shared')
|
|||
|
|
: 'private';
|
|||
|
|
if (!title || !start) return;
|
|||
|
|
const payload = { title, description: desc, start_at: start, end_at: end || null,
|
|||
|
|
color, address, visibility };
|
|||
|
|
if (_editEvent && _editEvent.id) {
|
|||
|
|
fetchNui('updateCalendarEvent', { id: _editEvent.id, ...payload });
|
|||
|
|
} else {
|
|||
|
|
fetchNui('addCalendarEvent', payload);
|
|||
|
|
}
|
|||
|
|
_editEvent = null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* ── NUI events ───────────────────────────────────────── */
|
|||
|
|
function onInbox(payload) {
|
|||
|
|
_inbox = (payload && payload.mails) ? payload.mails : (Array.isArray(payload) ? payload : []);
|
|||
|
|
_foldersByBox[''] = (payload && payload.folders) ? payload.folders : [];
|
|||
|
|
if (_view === 'inbox' || _view.startsWith('folder:')) _refresh();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function onMailContent(mail) {
|
|||
|
|
const idx = _inbox.findIndex(m => m.id === mail.id);
|
|||
|
|
if (idx !== -1) _inbox[idx] = { ..._inbox[idx], ...mail };
|
|||
|
|
if (_active === mail.id && _view === 'inbox') _refresh();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function onMailSent() {
|
|||
|
|
const status = document.getElementById('mail-send-status');
|
|||
|
|
if (status) status.textContent = 'Gesendet!';
|
|||
|
|
_sent = [];
|
|||
|
|
setTimeout(() => _setView('inbox'), 800);
|
|||
|
|
fetchNui('getInbox', {});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function onMailDeleted(id) {
|
|||
|
|
_inbox = _inbox.filter(m => m.id !== id);
|
|||
|
|
for (const fid in _folderMails) _folderMails[fid] = _folderMails[fid].filter(m => m.id !== id);
|
|||
|
|
if (_active === id) _active = null;
|
|||
|
|
_refresh();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function onSetSent(rows) {
|
|||
|
|
// Duplikate entfernen (Rundmails an mehrere Empfänger)
|
|||
|
|
const seen = new Set();
|
|||
|
|
_sent = (rows || []).filter(m => {
|
|||
|
|
const key = `${m.from_identifier}|${m.subject}|${m.sent_at}`;
|
|||
|
|
if (seen.has(key)) return false;
|
|||
|
|
seen.add(key);
|
|||
|
|
return true;
|
|||
|
|
});
|
|||
|
|
if (_view === 'sent') _refresh();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function onFolderCreated(folder) {
|
|||
|
|
const key = (folder && folder.address) || '';
|
|||
|
|
_foldersByBox[key] = (_foldersByBox[key] || []).concat([folder]);
|
|||
|
|
_folderMails[folder.id] = [];
|
|||
|
|
_refresh();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function onFolderDeleted(folderId, address) {
|
|||
|
|
const key = address || '';
|
|||
|
|
_foldersByBox[key] = (_foldersByBox[key] || []).filter(f => f.id !== folderId);
|
|||
|
|
delete _folderMails[folderId];
|
|||
|
|
_inbox = [];
|
|||
|
|
fetchNui('getInbox', {});
|
|||
|
|
if (_view === 'folder:' + folderId) _view = 'inbox';
|
|||
|
|
_refresh();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* Ordnerliste eines Postfachs vom Server. Kommt beim Wechsel und wenn
|
|||
|
|
jemand anderes im geteilten Postfach einen Ordner anlegt oder löscht. */
|
|||
|
|
function onFoldersData(address, folders) {
|
|||
|
|
_foldersByBox[address || ''] = folders || [];
|
|||
|
|
_refresh();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function onFolderMailsData(folderId, rows) {
|
|||
|
|
_folderMails[folderId] = rows || [];
|
|||
|
|
if (_view === 'folder:' + folderId) _refresh();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function onMailMoved(id, folderId) {
|
|||
|
|
_inbox = _inbox.filter(m => m.id !== id);
|
|||
|
|
for (const fid in _folderMails) _folderMails[fid] = _folderMails[fid].filter(m => m.id !== id);
|
|||
|
|
if (folderId) fetchNui('getFolderMails', { folderId });
|
|||
|
|
if (_active === id) _active = null;
|
|||
|
|
_refresh();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function onCalendarData(rows) { _calendar = rows || []; if (_view === 'calendar') _refresh(); }
|
|||
|
|
function onCalendarEventAdded(ev) { _calendar.push(ev); _calendar.sort((a,b)=>new Date(a.start_at)-new Date(b.start_at)); if (_view==='calendar') _refresh(); }
|
|||
|
|
function onCalendarEventUpdated(ev) { const i=_calendar.findIndex(e=>e.id===ev.id); if(i!==-1)_calendar[i]=ev; else _calendar.push(ev); _calendar.sort((a,b)=>new Date(a.start_at)-new Date(b.start_at)); if(_view==='calendar') _refresh(); }
|
|||
|
|
function onCalendarEventDeleted(id) { _calendar=_calendar.filter(e=>e.id!==id); if(_view==='calendar') _refresh(); }
|
|||
|
|
|
|||
|
|
function setContacts(contacts) { _contacts = contacts || []; }
|
|||
|
|
|
|||
|
|
/* ── Setup wizard ─────────────────────────────────────── */
|
|||
|
|
function _showSetupWizard(realms) {
|
|||
|
|
const body = WindowManager.getBody(WIN_ID);
|
|||
|
|
if (!body) return;
|
|||
|
|
const defaultRealm = (realms && realms.length > 0) ? realms[0].realm : 'mail.ls';
|
|||
|
|
body.innerHTML = `
|
|||
|
|
<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;
|
|||
|
|
height:100%;gap:18px;padding:40px 32px;text-align:center;">
|
|||
|
|
<div style="font-size:3rem">📧</div>
|
|||
|
|
<div style="font-size:1.05rem;font-weight:700;color:#fff">E-Mail-Adresse einrichten</div>
|
|||
|
|
<div style="font-size:0.82rem;color:var(--text-muted);line-height:1.6;max-width:340px">
|
|||
|
|
Du hast noch keine persönliche E-Mail-Adresse.<br>
|
|||
|
|
Wähle einen Benutzernamen für <strong style="color:#ccc">@${defaultRealm}</strong>.
|
|||
|
|
</div>
|
|||
|
|
<div style="display:flex;align-items:center;background:#1a1a1a;border:1px solid var(--border);
|
|||
|
|
border-radius:8px;overflow:hidden;width:100%;max-width:360px">
|
|||
|
|
<input id="mail-setup-input" type="text" maxlength="40" placeholder="benutzername"
|
|||
|
|
style="flex:1;background:transparent;border:none;color:#fff;padding:11px 14px;
|
|||
|
|
font-size:0.9rem;outline:none;"
|
|||
|
|
oninput="MailApp._setupPreview('${defaultRealm}')">
|
|||
|
|
<span style="padding:0 14px;color:#555;font-size:0.85rem">@${defaultRealm}</span>
|
|||
|
|
</div>
|
|||
|
|
<div id="mail-setup-preview" style="font-size:0.75rem;color:#555;min-height:14px"></div>
|
|||
|
|
<button onclick="MailApp._submitSetup('${defaultRealm}')"
|
|||
|
|
style="background:var(--accent,#3b82f6);color:#fff;border:none;border-radius:8px;
|
|||
|
|
padding:11px 36px;font-size:0.88rem;font-weight:700;cursor:pointer">
|
|||
|
|
Adresse erstellen
|
|||
|
|
</button>
|
|||
|
|
<div id="mail-setup-error" style="color:#f44;font-size:0.78rem;min-height:14px"></div>
|
|||
|
|
</div>`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function _setupPreview(realm) {
|
|||
|
|
const input = document.getElementById('mail-setup-input');
|
|||
|
|
const preview = document.getElementById('mail-setup-preview');
|
|||
|
|
if (!input || !preview) return;
|
|||
|
|
const val = input.value.trim().toLowerCase().replace(/[^a-z0-9._-]/g, '');
|
|||
|
|
if (val.length >= 2) {
|
|||
|
|
preview.textContent = '✓ ' + val + '@' + realm;
|
|||
|
|
preview.style.color = '#4caf50';
|
|||
|
|
} else {
|
|||
|
|
preview.textContent = val.length ? 'Mindestens 2 Zeichen' : '';
|
|||
|
|
preview.style.color = '#888';
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function _submitSetup(realm) {
|
|||
|
|
const input = document.getElementById('mail-setup-input');
|
|||
|
|
const errEl = document.getElementById('mail-setup-error');
|
|||
|
|
if (!input) return;
|
|||
|
|
const username = input.value.trim();
|
|||
|
|
if (username.length < 2) {
|
|||
|
|
if (errEl) errEl.textContent = 'Bitte mindestens 2 Zeichen eingeben';
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
if (errEl) errEl.textContent = '';
|
|||
|
|
input.disabled = true;
|
|||
|
|
fetchNui('createMailAddress', { username });
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function onMailAddressStatus(data) {
|
|||
|
|
if (data.hasAddress) {
|
|||
|
|
// Adresse vorhanden → normal laden
|
|||
|
|
fetchNui('getInbox', {});
|
|||
|
|
fetchNui('getSharedMailboxes', {});
|
|||
|
|
_refresh();
|
|||
|
|
} else {
|
|||
|
|
_showSetupWizard(data.realms || []);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function onMailCreateResult(data) {
|
|||
|
|
if (data.success) {
|
|||
|
|
const body = WindowManager.getBody(WIN_ID);
|
|||
|
|
if (body) {
|
|||
|
|
body.innerHTML = `
|
|||
|
|
<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;
|
|||
|
|
height:100%;gap:14px;text-align:center;padding:40px">
|
|||
|
|
<div style="font-size:2.5rem">✅</div>
|
|||
|
|
<div style="font-size:1rem;font-weight:700;color:#fff">Adresse erstellt!</div>
|
|||
|
|
<div style="font-family:monospace;font-size:0.95rem;color:var(--accent,#3b82f6);
|
|||
|
|
background:#0a1020;border:1px solid #1a2a40;padding:8px 20px;border-radius:6px">
|
|||
|
|
${data.address}
|
|||
|
|
</div>
|
|||
|
|
<div style="font-size:0.78rem;color:var(--text-muted)">Postfach wird geladen…</div>
|
|||
|
|
</div>`;
|
|||
|
|
}
|
|||
|
|
_personalAddr = data.address;
|
|||
|
|
setTimeout(() => {
|
|||
|
|
fetchNui('getInbox', {});
|
|||
|
|
fetchNui('getSharedMailboxes', {});
|
|||
|
|
_refresh();
|
|||
|
|
}, 1500);
|
|||
|
|
} else {
|
|||
|
|
const errEl = document.getElementById('mail-setup-error');
|
|||
|
|
const input = document.getElementById('mail-setup-input');
|
|||
|
|
if (errEl) errEl.textContent = data.error || 'Fehler beim Erstellen';
|
|||
|
|
if (input) input.disabled = false;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* ── Open window ──────────────────────────────────────── */
|
|||
|
|
function open() {
|
|||
|
|
const created = WindowManager.create({
|
|||
|
|
id: WIN_ID,
|
|||
|
|
title: 'Mail',
|
|||
|
|
icon: '✉',
|
|||
|
|
width: 920,
|
|||
|
|
height: 580,
|
|||
|
|
content: '',
|
|||
|
|
});
|
|||
|
|
if (created) {
|
|||
|
|
_view = 'inbox';
|
|||
|
|
_active = null;
|
|||
|
|
_activeAddr = null;
|
|||
|
|
// Erst prüfen ob eine Mailadresse existiert
|
|||
|
|
fetchNui('checkMailAddress', {});
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
open, initMailboxes, onSharedMailboxes,
|
|||
|
|
onMailAddressStatus, onMailCreateResult,
|
|||
|
|
_setupPreview, _submitSetup,
|
|||
|
|
_setMailbox, _setView, _openMail, _sendMail, _reply, _deleteMail,
|
|||
|
|
_addMailbox, _removeMailbox, _saveSignature, _sigBoxChanged, _fromChanged,
|
|||
|
|
onSignatureSaved, refreshMailboxes,
|
|||
|
|
_moveMail, _promptNewFolder, _deleteFolder, _saveAsContact,
|
|||
|
|
_newCalEvent, _calDayClick, _openCalEdit, _deleteCalEvent, _cancelCalEdit, _saveCalEvent,
|
|||
|
|
_calAddressChanged,
|
|||
|
|
onInbox, onMailContent, onMailSent, onMailDeleted,
|
|||
|
|
onSetSent, onFolderCreated, onFolderDeleted, onFoldersData, onFolderMailsData, onMailMoved,
|
|||
|
|
onCalendarData, onCalendarEventAdded, onCalendarEventUpdated, onCalendarEventDeleted,
|
|||
|
|
setContacts,
|
|||
|
|
};
|
|||
|
|
})();
|