/** * 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,'>'); } /* 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 = ` `; // 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) ? ` ` : ''; mailboxItems += ` `; } mailboxItems += ` `; // 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 => ` `).join(''); return `
${mailboxItems}
${folderItems}
`; } /* ── Mail list ────────────────────────────────────────── */ function renderMailList(mails, isSent) { if (!mails.length) return `
📭

Keine Nachrichten

`; const fid = currentFolderId(); const folderOptions = currentFolders().map(f => ``).join(''); const moveSelect = (fid == null && !isSent) ? ` ` : ''; const toInboxBtn = (fid != null) ? ` ` : ''; return `
` + 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 `
${label}
${esc(m.subject)}
${fmtDate(m.sent_at)}
${moveBtn}${inboxBtn}
`; }).join('') + `
`; } /* ── Mail detail ──────────────────────────────────────── */ function renderMailDetail(mail, isSent) { if (!mail) return `
📧

Nachricht auswählen

`; const fid = currentFolderId(); const folderOptions = currentFolders().map(f => ``).join(''); const moveRow = (!isSent) ? ` ` : ''; const replyBtn = !isSent ? `` : ''; const contactMatch = _contacts.find(c => c.email === (isSent ? mail.to_identifier : mail.from_identifier)); const contactBtn = !contactMatch ? ` ` : ''; // Zeige Postfach-Badge wenn Shared-Mail const mbBadge = mail.to_address && mail.to_address !== _personalAddr ? ` 📮 ${esc(mail.to_address)} ` : ''; return `
${esc(mail.subject)}${mbBadge}
${isSent ? 'An' : 'Von'}: ${esc(isSent ? (mail.to_address || mail.to_identifier) : mail.from_identifier)} Datum: ${fmtDate(mail.sent_at)}
${replyBtn} ${moveRow} ${contactBtn} ${!isSent ? `` : ''}
${esc(mail.body)}
`; } /* ── 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 `

Für dieses Postfach gibt es keine Adresse.

`; } // 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 => `` ).join(''); return `

✒ Signatur

Die Signatur gehört zum Postfach: Wer es bedient, schreibt mit derselben Fußzeile.
Beim Schreiben wird die Signatur unter den Text gesetzt, getrennt durch eine Zeile mit --. Wechselst du den Absender, tauscht sie sich mit.
`; } 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 => `` ).join(''); fromField = `
`; } else if (fromAddresses.length === 1) { fromField = ``; } const contactOptions = _contacts.map(c => `` ).join(''); const datalist = _contacts.length ? ` ${contactOptions}` : ''; return `

✏ Neue Nachricht

${datalist} ${fromField}
`; } /* ── 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 => `
${d}
`).join(''); const offset = (firstDay + 6) % 7; for (let i = 0; i < offset; i++) cells += `
`; 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 => `
` ).join(''); const isToday = (d === now.getDate()); cells += `
${d}
${dots}
`; } 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 ? `
` : ''; return `
${esc(e.title)}
${esc(badge.text)}
${fmtDate(e.start_at)}${e.end_at ? ' – ' + fmtDate(e.end_at) : ''}
${e.description ? `
${esc(e.description)}
` : ''} ${actions}
`; }).join(''); const editForm = _editEvent ? `

${_editEvent.id ? 'Eintrag bearbeiten' : 'Neuer Eintrag'}

${(_editEvent.address || '') !== '' ? `
` : `
Ein privater Termin ist nur für dich sichtbar.
`}
` : ` `; return `
📅 ${monthNames[month]} ${year}
${dayHeaders} ${cells}
${editForm} ${upcomingItems || '
Keine Termine
'}
`; } /* ── 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 = `
${renderMailList(mails, isSent)}
${renderMailDetail(activeMail, isSent)}
`; } body.innerHTML = `
${renderSidebar()}
${rightContent}
`; } /* ── 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 = `
📧
E-Mail-Adresse einrichten
Du hast noch keine persönliche E-Mail-Adresse.
Wähle einen Benutzernamen für @${defaultRealm}.
@${defaultRealm}
`; } 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 = `
Adresse erstellt!
${data.address}
Postfach wird geladen…
`; } _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, }; })();