/**
* 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 `
`;
}
/* ── Mail list ────────────────────────────────────────── */
function renderMailList(mails, isSent) {
if (!mails.length)
return ``;
const fid = currentFolderId();
const folderOptions = currentFolders().map(f =>
`${esc(f.name)} `).join('');
const moveSelect = (fid == null && !isSent) ? `
📁 Verschieben…
${folderOptions}
` : '';
const toInboxBtn = (fid != null) ? `
↩ Posteingang ` : '';
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 ``;
const fid = currentFolderId();
const folderOptions = currentFolders().map(f =>
`${esc(f.name)} `).join('');
const moveRow = (!isSent) ? `
📁 Verschieben…
${folderOptions}
${fid!=null ? `↩ Posteingang ` : ''}
` : '';
const replyBtn = !isSent ? `↩ Antworten ` : '';
const contactMatch = _contacts.find(c => c.email === (isSent ? mail.to_identifier : mail.from_identifier));
const contactBtn = !contactMatch ? `
👤 Als Kontakt speichern
` : '';
// Zeige Postfach-Badge wenn Shared-Mail
const mbBadge = mail.to_address && mail.to_address !== _personalAddr ? `
📮 ${esc(mail.to_address)}
` : '';
return `
`;
}
/* ── 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 =>
`${esc(b.label)} `
).join('');
return `
✒ Signatur
Postfach
${opts}
Die Signatur gehört zum Postfach: Wer es bedient, schreibt mit derselben
Fußzeile.
Speichern
Zurück
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 =>
`${esc(o.label)} `
).join('');
fromField = `
Von
${opts}
`;
} else if (fromAddresses.length === 1) {
fromField = ` `;
}
const contactOptions = _contacts.map(c =>
`${esc(c.name)}${c.email ? ' <' + esc(c.email) + '>' : ''} `
).join('');
const datalist = _contacts.length ? `
${contactOptions} ` : '';
return `
✏ Neue Nachricht
${datalist}
${fromField}
An (Adresse oder Kontakt)
Betreff
Nachricht
📤 Senden
Abbrechen
`;
}
/* ── 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 =>
``).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 += `
`;
}
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 ? `
` : `
+ Neuer Eintrag
`;
return `
${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}
Adresse erstellen
`;
}
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,
};
})();