From da44c2c9a53a8e4b01d4ae3cfa37eeaa4f84dc1b Mon Sep 17 00:00:00 2001 From: Lars Gebhardt-Kusche Date: Tue, 24 Feb 2026 01:15:02 +0100 Subject: [PATCH] mail setup --- .../landingpage/accountsetup/settings.php | 59 ++++ public/assets/js/ui-editor.js | 48 ++- public/assets/js/ui-user.js | 158 ++++++++++ public/index.php | 7 + schema.sql | 21 ++ src/ApiKernel.php | 286 +++++++++++++++++- 6 files changed, 576 insertions(+), 3 deletions(-) diff --git a/partials/landingpage/accountsetup/settings.php b/partials/landingpage/accountsetup/settings.php index 75025e0..f3ad37c 100755 --- a/partials/landingpage/accountsetup/settings.php +++ b/partials/landingpage/accountsetup/settings.php @@ -40,6 +40,65 @@ require dirname(__DIR__) . '/../structure/layout_start.php'; +
+
+

Versandprofile (SMTP)

+ +
+

Mehrere SMTP-Profile fuer unterschiedliche Absender oder Server.

+
+ + + + + +
BezeichnungServerBenutzerAbsenderAktionen
+
+ +
+

Integrationen, Downloads & Tokens

Die Dateien enthalten automatisch deine aktuellen Tokens. Nach dem Speichern neuer Tokens bitte die Dateien erneut herunterladen.

diff --git a/public/assets/js/ui-editor.js b/public/assets/js/ui-editor.js index dcebec3..c46c478 100755 --- a/public/assets/js/ui-editor.js +++ b/public/assets/js/ui-editor.js @@ -24,9 +24,11 @@ export function initEditor() {   const sendSubject  = document.getElementById('send_subject');   const sendInfo     = document.getElementById('send_template_info');   const btnCancelSend= document.getElementById('btn-cancel-send'); -  const btnSendNow   = document.getElementById('btn-send-now'); + const btnSendNow   = document.getElementById('btn-send-now'); const sendSender   = document.getElementById('send_sender'); const sendSenderHint = document.getElementById('send_sender_hint'); + const sendSmtpProfile = document.getElementById('send_smtp_profile'); + const sendSmtpProfileHint = document.getElementById('send_smtp_profile_hint'); const prevFrame    = document.getElementById('previewFrame'); const btnPrevClose = document.getElementById('btn-close-preview'); const unsavedDialog = document.getElementById('unsavedDialog'); @@ -46,6 +48,8 @@ export function initEditor() { let reqToken = 0; // steigender Token pro Öffnen -> ignoriert verspätete Events let senderOptions = []; let senderLoadPromise = null; + let smtpProfileOptions = []; + let smtpProfileLoadPromise = null; let currentEditorType = 'grapesjs'; let versionItems = []; let savedSnapshot = ''; @@ -902,6 +906,24 @@ export function initEditor() {     return senderLoadPromise;   } +  async function loadSmtpProfileOptions(force = false) { +    if (!sendSmtpProfile) return; +    if (smtpProfileLoadPromise && !force) return smtpProfileLoadPromise; +    smtpProfileLoadPromise = apiAction('account.smtp_profiles.list', { method: 'GET' }) +      .then(res => { +        smtpProfileOptions = res?.items || []; +        renderSmtpProfileOptions(); +      }) +      .catch(() => { +        smtpProfileOptions = []; +        renderSmtpProfileOptions(); +      }) +      .finally(() => { +        smtpProfileLoadPromise = null; +      }); +    return smtpProfileLoadPromise; +  } +   function renderSenderOptions() {     if (!sendSender) return;     const previous = sendSender.value; @@ -921,6 +943,25 @@ export function initEditor() {     }   } +  function renderSmtpProfileOptions() { +    if (!sendSmtpProfile) return; +    const previous = sendSmtpProfile.value; +    let html = ''; +    smtpProfileOptions.forEach(opt => { +      const label = opt.label || opt.smtp_host || 'Profil'; +      html += ``; +    }); +    sendSmtpProfile.innerHTML = html; +    if (previous && smtpProfileOptions.some(opt => String(opt.id) === previous)) { +      sendSmtpProfile.value = previous; +    } else { +      sendSmtpProfile.value = ''; +    } +    if (sendSmtpProfileHint) { +      sendSmtpProfileHint.classList.toggle('hidden', smtpProfileOptions.length > 0); +    } +  } +   // ---------- Initialen HTML-Inhalt in Editor pushen (mit Token/Race-Schutz) ---------- async function pushInitialHtmlToEditor({ mode, html, snippets, ref, token, hasJson, json }) { if (token !== reqToken) return; // veraltete Anfrage ignorieren @@ -1266,11 +1307,13 @@ export function initEditor() {     if (sendSubject) sendSubject.value = ctx?.subject || 'Testversand';     if (sendTo) sendTo.value = ctx?.to || '';     await loadSenderOptions(true); +    await loadSmtpProfileOptions(true);     sendDlg?.showModal?.();   }   function closeSend(){     sendDlg?.close?.();     if (sendSender) sendSender.value = ''; +    if (sendSmtpProfile) sendSmtpProfile.value = '';   }   async function doSend(ev){ @@ -1291,6 +1334,9 @@ export function initEditor() {     if (sendSender && sendSender.value) {       payload.sender_id = Number(sendSender.value);     } +    if (sendSmtpProfile && sendSmtpProfile.value) { +      payload.smtp_profile_id = Number(sendSmtpProfile.value); +    }     const r = await apiAction('templates.test_send', { method:'POST', data: payload });     if(r?.ok){ toast("Testversand ausgelöst"); closeSend(); } else { toast("Senden fehlgeschlagen", false); }   } diff --git a/public/assets/js/ui-user.js b/public/assets/js/ui-user.js index 36338bd..02040f9 100755 --- a/public/assets/js/ui-user.js +++ b/public/assets/js/ui-user.js @@ -7,6 +7,8 @@ const state = { userMap: new Map(), senders: [], senderMap: new Map(), + smtpProfiles: [], + smtpProfileMap: new Map(), currentTab: 'profile', loading: false, }; @@ -25,6 +27,8 @@ let teamTable; let userForm; let senderTable; let senderForm; +let smtpProfileTable; +let smtpProfileForm; let sectionsList; let sectionsCreateForm; let sectionNameInput; @@ -82,6 +86,8 @@ export function initAccountPage() { userForm = document.getElementById('userForm'); senderTable = document.getElementById('senderTable'); senderForm = document.getElementById('senderForm'); + smtpProfileTable = document.getElementById('smtpProfileTable'); + smtpProfileForm = document.getElementById('smtpProfileForm'); adminTablesAllSelect = document.getElementById('adminBridgeTablesAll'); adminTablesSelectedSelect = document.getElementById('adminBridgeTablesSelected'); adminTablesAddBtn = document.getElementById('adminBridgeTablesAdd'); @@ -105,11 +111,16 @@ export function initAccountPage() { document.getElementById('senderFormCancel')?.addEventListener('click', () => closeSenderForm()); senderForm?.addEventListener('submit', submitSenderForm); + document.getElementById('btn-smtp-profile-add')?.addEventListener('click', () => openSmtpProfileForm()); + document.getElementById('smtpProfileFormCancel')?.addEventListener('click', () => closeSmtpProfileForm()); + smtpProfileForm?.addEventListener('submit', submitSmtpProfileForm); + profileForm?.addEventListener('submit', submitProfileForm); passwordForm?.addEventListener('submit', submitPasswordForm); settingsForm?.addEventListener('submit', submitSettingsForm); teamTable?.addEventListener('click', handleTeamTableClick); senderTable?.addEventListener('click', handleSenderTableClick); + smtpProfileTable?.addEventListener('click', handleSmtpProfileTableClick); document.querySelectorAll('[data-user-tab]').forEach(btn => { btn.addEventListener('click', () => switchTab(btn.getAttribute('data-user-tab'))); }); @@ -421,6 +432,15 @@ async function loadAccountData() { renderSenderList(); } } + if (smtpProfileTable) { + if (isAdmin()) { + await loadSmtpProfiles(); + } else { + state.smtpProfiles = []; + state.smtpProfileMap = new Map(); + renderSmtpProfileList(); + } + } reportViewDebugInfo(res); } catch (err) { console.error(err); @@ -928,6 +948,144 @@ async function deleteSender(senderId) { } } +async function loadSmtpProfiles() { + if (!smtpProfileTable) return; + try { + const res = await apiAction('account.smtp_profiles.list', { method: 'GET' }); + if (!res?.ok) throw new Error(res?.error || 'Versandprofile konnten nicht geladen werden'); + state.smtpProfiles = res.items || []; + state.smtpProfileMap = new Map(state.smtpProfiles.map(item => [item.id, item])); + renderSmtpProfileList(); + } catch (err) { + toast(err.message || 'Fehler beim Laden der Versandprofile', false); + } +} + +function renderSmtpProfileList() { + if (!smtpProfileTable) return; + const tbody = smtpProfileTable.querySelector('tbody'); + if (!tbody) return; + if (!state.smtpProfiles.length) { + tbody.innerHTML = 'Keine Versandprofile vorhanden.'; + return; + } + tbody.innerHTML = state.smtpProfiles.map(profile => { + const label = profile.label || profile.smtp_host || 'Profil'; + const host = profile.smtp_host || '—'; + const user = profile.smtp_user || '—'; + const sender = profile.from_email || '—'; + return ` + + ${escapeHtml(label)} + ${escapeHtml(host)} + ${escapeHtml(user)} + ${escapeHtml(sender)} + + + + + + `; + }).join(''); +} + +function handleSmtpProfileTableClick(ev) { + const btn = ev.target.closest('button[data-smtp-action]'); + if (!btn) return; + const id = Number(btn.getAttribute('data-smtp-id')); + const action = btn.getAttribute('data-smtp-action'); + const profile = state.smtpProfileMap.get(id); + if (!profile) return; + if (action === 'edit') { + openSmtpProfileForm(profile); + } else if (action === 'copy') { + if (confirm(`Versandprofil "${profile.label || profile.smtp_host}" kopieren?`)) { + copySmtpProfile(id); + } + } else if (action === 'delete') { + if (confirm(`Versandprofil "${profile.label || profile.smtp_host}" wirklich löschen?`)) { + deleteSmtpProfile(id); + } + } +} + +function openSmtpProfileForm(profile = null) { + if (!smtpProfileForm) return; + smtpProfileForm.classList.remove('hidden'); + smtpProfileForm.profile_id.value = profile?.id || ''; + smtpProfileForm.label.value = profile?.label || ''; + smtpProfileForm.smtp_host.value = profile?.smtp_host || ''; + smtpProfileForm.smtp_port.value = profile?.smtp_port ? String(profile.smtp_port) : ''; + smtpProfileForm.smtp_user.value = profile?.smtp_user || ''; + smtpProfileForm.smtp_pass.value = ''; + smtpProfileForm.smtp_pass.placeholder = profile?.smtp_pass_set ? 'Passwort gesetzt' : '••••••••'; + smtpProfileForm.smtp_secure.value = profile?.smtp_secure || ''; + smtpProfileForm.from_email.value = profile?.from_email || ''; + smtpProfileForm.from_name.value = profile?.from_name || ''; + smtpProfileForm.reply_to.value = profile?.reply_to || ''; + if (smtpProfileForm.smtp_pass_clear) smtpProfileForm.smtp_pass_clear.checked = false; +} + +function closeSmtpProfileForm() { + if (!smtpProfileForm) return; + smtpProfileForm.classList.add('hidden'); + smtpProfileForm.reset(); + smtpProfileForm.profile_id.value = ''; +} + +async function submitSmtpProfileForm(ev) { + ev.preventDefault(); + if (!smtpProfileForm) return; + const payload = { + profile_id: smtpProfileForm.profile_id.value ? Number(smtpProfileForm.profile_id.value) : undefined, + label: smtpProfileForm.label.value.trim(), + smtp_host: smtpProfileForm.smtp_host.value.trim(), + smtp_port: smtpProfileForm.smtp_port.value.trim(), + smtp_user: smtpProfileForm.smtp_user.value.trim(), + smtp_pass: smtpProfileForm.smtp_pass.value, + smtp_secure: smtpProfileForm.smtp_secure.value, + from_email: smtpProfileForm.from_email.value.trim(), + from_name: smtpProfileForm.from_name.value.trim(), + reply_to: smtpProfileForm.reply_to.value.trim(), + smtp_pass_clear: smtpProfileForm.smtp_pass_clear?.checked ? 1 : 0, + }; + if (!payload.smtp_host) { + toast('Bitte einen SMTP-Server angeben', false); + return; + } + try { + const res = await apiAction('account.smtp_profiles.save', { method: 'POST', data: payload }); + if (!res?.ok) throw new Error(res?.error || 'Speichern fehlgeschlagen'); + closeSmtpProfileForm(); + await loadSmtpProfiles(); + toast('Versandprofil gespeichert', true); + } catch (err) { + toast(err.message || 'Speichern fehlgeschlagen', false); + } +} + +async function deleteSmtpProfile(profileId) { + try { + const res = await apiAction('account.smtp_profiles.delete', { method: 'POST', data: { profile_id: profileId } }); + if (!res?.ok) throw new Error(res?.error || 'Löschen fehlgeschlagen'); + await loadSmtpProfiles(); + toast('Versandprofil gelöscht', true); + } catch (err) { + toast(err.message || 'Löschen fehlgeschlagen', false); + } +} + +async function copySmtpProfile(profileId) { + try { + const res = await apiAction('account.smtp_profiles.copy', { method: 'POST', data: { profile_id: profileId } }); + if (!res?.ok) throw new Error(res?.error || 'Kopieren fehlgeschlagen'); + await loadSmtpProfiles(); + toast('Versandprofil kopiert', true); + } catch (err) { + toast(err.message || 'Kopieren fehlgeschlagen', false); + } +} + function escapeHtml(str) { return String(str || '') .replace(/&/g, '&') diff --git a/public/index.php b/public/index.php index f2faa65..5b013eb 100755 --- a/public/index.php +++ b/public/index.php @@ -130,6 +130,13 @@ require __DIR__ . '/../partials/structure/layout_start.php'; +
diff --git a/schema.sql b/schema.sql index 8dc95a0..af0e902 100755 --- a/schema.sql +++ b/schema.sql @@ -212,6 +212,27 @@ CREATE TABLE IF NOT EXISTS `emailtemplate_sender_identities` ( CONSTRAINT `fk_sender_customer` FOREIGN KEY (`customer_id`) REFERENCES `emailtemplate_customers` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; +-- Tabelle: emailtemplate_smtp_profiles +CREATE TABLE IF NOT EXISTS `emailtemplate_smtp_profiles` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `customer_id` int(10) unsigned NOT NULL, + `label` varchar(255) NOT NULL, + `smtp_host` varchar(255) NOT NULL, + `smtp_port` int(10) unsigned DEFAULT NULL, + `smtp_user` varchar(255) DEFAULT NULL, + `smtp_pass` varchar(255) DEFAULT NULL, + `smtp_secure` varchar(16) DEFAULT NULL, + `from_email` varchar(255) DEFAULT NULL, + `from_name` varchar(255) DEFAULT NULL, + `reply_to` varchar(255) DEFAULT NULL, + `created_at` timestamp NOT NULL DEFAULT current_timestamp(), + `updated_at` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(), + PRIMARY KEY (`id`), + KEY `idx_smtp_customer` (`customer_id`), + KEY `idx_smtp_host` (`smtp_host`), + CONSTRAINT `fk_smtp_customer` FOREIGN KEY (`customer_id`) REFERENCES `emailtemplate_customers` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; + -- Tabelle: emailtemplate_snippets CREATE TABLE IF NOT EXISTS `emailtemplate_snippets` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, diff --git a/src/ApiKernel.php b/src/ApiKernel.php index 33fa278..89cd1f8 100755 --- a/src/ApiKernel.php +++ b/src/ApiKernel.php @@ -2247,6 +2247,7 @@ class ApiKernel $subject = 'Testversand'; } $senderId = (int)$this->val($this->in, ['sender_id'], 0); + $smtpProfileId = (int)$this->val($this->in, ['smtp_profile_id'], 0); $row = null; $html = ''; @@ -2291,14 +2292,30 @@ class ApiKernel $sender = $this->fetchSenderRow($customerId, $senderId); } } + $smtpOverride = null; + if ($smtpProfileId > 0 && $customerId > 0) { + $profile = $this->fetchSmtpProfileRow($customerId, $smtpProfileId); + $smtpOverride = [ + 'enabled' => true, + 'host' => $profile['smtp_host'] ?? '', + 'port' => $profile['smtp_port'] ?? 0, + 'user' => $profile['smtp_user'] ?? '', + 'pass' => $this->fetchSmtpProfilePassword($customerId, $smtpProfileId), + 'secure' => $profile['smtp_secure'] ?? '', + 'from_email' => $profile['from_email'] ?? '', + 'from_name' => $profile['from_name'] ?? '', + 'reply_to' => $profile['reply_to'] ?? '', + ]; + } - if (!$this->dispatchTestMail($recipient, $subject, $html, $sender, $customerId)) { + if (!$this->dispatchTestMail($recipient, $subject, $html, $sender, $customerId, $smtpOverride)) { $this->writeDebugLog('templates_test_send', [ 'time' => date(DATE_ATOM), 'template_id' => $templateId, 'to' => $recipient, 'subject' => $subject, 'sender_id' => $senderId > 0 ? $senderId : null, + 'smtp_profile_id' => $smtpProfileId > 0 ? $smtpProfileId : null, 'from_email' => $sender['from_email'] ?? ($this->conf['smtp']['from_email'] ?? null), 'from_name' => $sender['from_name'] ?? ($this->conf['smtp']['from_name'] ?? null), 'html_length' => strlen($html), @@ -3054,6 +3071,18 @@ class ApiKernel case 'account.senders.delete': $this->handleAccountSenderDelete(); break; + case 'account.smtp_profiles.list': + $this->handleAccountSmtpProfilesList(); + break; + case 'account.smtp_profiles.save': + $this->handleAccountSmtpProfileSave(); + break; + case 'account.smtp_profiles.delete': + $this->handleAccountSmtpProfileDelete(); + break; + case 'account.smtp_profiles.copy': + $this->handleAccountSmtpProfileCopy(); + break; case 'dashboard.metrics': $this->handleDashboardMetrics(); break; @@ -3835,8 +3864,22 @@ class ApiKernel $this->fail('Valid recipient required', null, 422); } + $smtpProfileId = (int)($this->in['smtp_profile_id'] ?? 0); $smtpOverride = null; - if (array_key_exists('smtp_host', $this->in) || array_key_exists('smtp_enabled', $this->in)) { + if ($smtpProfileId > 0) { + $profile = $this->fetchSmtpProfileRow($customerId, $smtpProfileId); + $smtpOverride = [ + 'enabled' => true, + 'host' => $profile['smtp_host'] ?? '', + 'port' => $profile['smtp_port'] ?? 0, + 'user' => $profile['smtp_user'] ?? '', + 'pass' => $this->fetchSmtpProfilePassword($customerId, $smtpProfileId), + 'secure' => $profile['smtp_secure'] ?? '', + 'from_email' => $profile['from_email'] ?? '', + 'from_name' => $profile['from_name'] ?? '', + 'reply_to' => $profile['reply_to'] ?? '', + ]; + } elseif (array_key_exists('smtp_host', $this->in) || array_key_exists('smtp_enabled', $this->in)) { $smtpOverride = [ 'enabled' => !empty($this->in['smtp_enabled']), 'host' => trim((string)($this->in['smtp_host'] ?? '')), @@ -3858,6 +3901,7 @@ class ApiKernel 'time' => date(DATE_ATOM), 'customer_id' => $customerId, 'to' => $recipient, + 'smtp_profile_id' => $smtpProfileId > 0 ? $smtpProfileId : null, 'smtp_enabled' => $smtpOverride['enabled'] ?? null, 'smtp_host' => $smtpOverride['host'] ?? null, 'smtp_port' => $smtpOverride['port'] ?? null, @@ -4291,6 +4335,158 @@ class ApiKernel $this->respond(['ok' => true, 'deleted' => true]); } + private function handleAccountSmtpProfilesList(): void + { + $user = $this->requireAuth(); + $customerId = (int)($user['customer_id'] ?? 0); + if ($customerId <= 0) $this->fail('Customer context missing', null, 500); + $this->ensureSmtpProfilesTableExists(); + $table = $this->smtpProfilesTable(); + $stmt = $this->pdo->prepare("SELECT * FROM `$table` WHERE `customer_id` = :cid ORDER BY `label` ASC"); + $stmt->execute([':cid' => $customerId]); + $items = []; + while ($row = $stmt->fetch()) { + $items[] = $this->formatSmtpProfileRow($row); + } + $this->respond(['ok' => true, 'items' => $items]); + } + + private function handleAccountSmtpProfileSave(): void + { + $user = $this->requireAuth(); + $this->ensureRole($user, ['owner', 'admin']); + $customerId = (int)($user['customer_id'] ?? 0); + if ($customerId <= 0) $this->fail('Customer context missing', null, 500); + + $profileId = (int)($this->in['profile_id'] ?? 0); + $label = trim((string)($this->in['label'] ?? '')); + $host = trim((string)($this->in['smtp_host'] ?? '')); + $port = (int)($this->in['smtp_port'] ?? 0); + $userName = trim((string)($this->in['smtp_user'] ?? '')); + $pass = (string)($this->in['smtp_pass'] ?? ''); + $secure = strtolower(trim((string)($this->in['smtp_secure'] ?? ''))); + $fromEmail = trim((string)($this->in['from_email'] ?? '')); + $fromName = trim((string)($this->in['from_name'] ?? '')); + $replyTo = trim((string)($this->in['reply_to'] ?? '')); + $passClear = !empty($this->in['smtp_pass_clear']); + + if ($label === '') $label = $fromEmail ?: $host; + if ($host === '') $this->fail('SMTP-Host erforderlich', null, 422); + if ($port < 0 || $port > 65535) $this->fail('Ungültiger SMTP-Port', null, 422); + if ($secure !== '' && !in_array($secure, ['tls', 'ssl', 'none'], true)) { + $this->fail('Ungültige SMTP-Sicherheit', null, 422); + } + if ($fromEmail !== '' && !filter_var($fromEmail, FILTER_VALIDATE_EMAIL)) { + $this->fail('Ungültige Absenderadresse', null, 422); + } + if ($replyTo !== '' && !filter_var($replyTo, FILTER_VALIDATE_EMAIL)) { + $this->fail('Ungültige Reply-To-Adresse', null, 422); + } + if ($secure === 'none') $secure = ''; + + $this->ensureSmtpProfilesTableExists(); + $table = $this->smtpProfilesTable(); + + if ($profileId > 0) { + $fields = [ + 'label' => $label, + 'smtp_host' => $host, + 'smtp_port' => $port > 0 ? $port : null, + 'smtp_user' => $userName ?: null, + 'smtp_secure' => $secure ?: null, + 'from_email' => $fromEmail ?: null, + 'from_name' => $fromName ?: null, + 'reply_to' => $replyTo ?: null, + ]; + if ($passClear) { + $fields['smtp_pass'] = null; + } elseif ($pass !== '') { + $fields['smtp_pass'] = $pass; + } + $set = []; + $params = [':id' => $profileId, ':cid' => $customerId]; + foreach ($fields as $k => $v) { + $set[] = "`$k` = :$k"; + $params[":$k"] = $v; + } + $set[] = "`updated_at` = NOW()"; + $sql = "UPDATE `$table` SET " . implode(',', $set) . " WHERE `id` = :id AND `customer_id` = :cid LIMIT 1"; + $stmt = $this->pdo->prepare($sql); + $stmt->execute($params); + if ($stmt->rowCount() === 0) $this->fail('Versandprofil nicht gefunden', null, 404); + } else { + $stmt = $this->pdo->prepare("INSERT INTO `$table` (`customer_id`,`label`,`smtp_host`,`smtp_port`,`smtp_user`,`smtp_pass`,`smtp_secure`,`from_email`,`from_name`,`reply_to`,`created_at`,`updated_at`) VALUES (:cid,:label,:host,:port,:user,:pass,:secure,:fmail,:fname,:reply,NOW(),NOW())"); + $stmt->execute([ + ':cid' => $customerId, + ':label' => $label, + ':host' => $host, + ':port' => $port > 0 ? $port : null, + ':user' => $userName ?: null, + ':pass' => $pass !== '' ? $pass : null, + ':secure' => $secure ?: null, + ':fmail' => $fromEmail ?: null, + ':fname' => $fromName ?: null, + ':reply' => $replyTo ?: null, + ]); + $profileId = (int)$this->pdo->lastInsertId(); + } + + $profile = $this->fetchSmtpProfileRow($customerId, $profileId); + $this->respond(['ok' => true, 'profile' => $profile]); + } + + private function handleAccountSmtpProfileDelete(): void + { + $user = $this->requireAuth(); + $this->ensureRole($user, ['owner', 'admin']); + $customerId = (int)($user['customer_id'] ?? 0); + $profileId = (int)($this->in['profile_id'] ?? 0); + if ($profileId <= 0) $this->fail('Ungültige Profil-ID', null, 422); + $this->ensureSmtpProfilesTableExists(); + $table = $this->smtpProfilesTable(); + $stmt = $this->pdo->prepare("DELETE FROM `$table` WHERE `id` = :id AND `customer_id` = :cid LIMIT 1"); + $stmt->execute([':id' => $profileId, ':cid' => $customerId]); + if ($stmt->rowCount() === 0) $this->fail('Versandprofil nicht gefunden', null, 404); + $this->respond(['ok' => true, 'deleted' => true]); + } + + private function handleAccountSmtpProfileCopy(): void + { + $user = $this->requireAuth(); + $this->ensureRole($user, ['owner', 'admin']); + $customerId = (int)($user['customer_id'] ?? 0); + $profileId = (int)($this->in['profile_id'] ?? 0); + if ($profileId <= 0) $this->fail('Ungültige Profil-ID', null, 422); + $this->ensureSmtpProfilesTableExists(); + $table = $this->smtpProfilesTable(); + $stmt = $this->pdo->prepare("SELECT * FROM `$table` WHERE `id` = :id AND `customer_id` = :cid LIMIT 1"); + $stmt->execute([':id' => $profileId, ':cid' => $customerId]); + $row = $stmt->fetch(); + if (!$row) $this->fail('Versandprofil nicht gefunden', null, 404); + + $newLabel = trim((string)($this->in['label'] ?? '')); + if ($newLabel === '') { + $newLabel = ($row['label'] ?? 'Profil') . ' (Kopie)'; + } + + $insert = $this->pdo->prepare("INSERT INTO `$table` (`customer_id`,`label`,`smtp_host`,`smtp_port`,`smtp_user`,`smtp_pass`,`smtp_secure`,`from_email`,`from_name`,`reply_to`,`created_at`,`updated_at`) VALUES (:cid,:label,:host,:port,:user,:pass,:secure,:fmail,:fname,:reply,NOW(),NOW())"); + $insert->execute([ + ':cid' => $customerId, + ':label' => $newLabel, + ':host' => $row['smtp_host'], + ':port' => $row['smtp_port'], + ':user' => $row['smtp_user'], + ':pass' => $row['smtp_pass'], + ':secure' => $row['smtp_secure'], + ':fmail' => $row['from_email'], + ':fname' => $row['from_name'], + ':reply' => $row['reply_to'], + ]); + $newId = (int)$this->pdo->lastInsertId(); + $profile = $this->fetchSmtpProfileRow($customerId, $newId); + $this->respond(['ok' => true, 'profile' => $profile]); + } + private function handleDashboardMetrics(): void { $user = $this->requireAuth(); @@ -5297,6 +5493,46 @@ SQL; return 'emailtemplate_sender_identities'; } + private function smtpProfilesTable(): string + { + return 'emailtemplate_smtp_profiles'; + } + + private function ensureSmtpProfilesTableExists(): void + { + $table = $this->smtpProfilesTable(); + if ($this->tableExists($table)) { + return; + } + try { + $sql = <<pdo->exec($sql); + $this->tableExistsCache[$table] = true; + } catch (Throwable $e) { + $this->fail('SMTP-Profil-Tabelle fehlt und konnte nicht erstellt werden', $e->getMessage(), 500); + } + } + private function fetchSenderRow(int $customerId, int $senderId): array { if ($customerId <= 0 || $senderId <= 0) { @@ -5323,6 +5559,52 @@ SQL; ]; } + private function fetchSmtpProfileRow(int $customerId, int $profileId): array + { + if ($customerId <= 0 || $profileId <= 0) { + $this->fail('Versandprofil nicht gefunden', null, 404); + } + $this->ensureSmtpProfilesTableExists(); + $table = $this->smtpProfilesTable(); + $stmt = $this->pdo->prepare("SELECT * FROM `$table` WHERE `id` = :id AND `customer_id` = :cid LIMIT 1"); + $stmt->execute([':id' => $profileId, ':cid' => $customerId]); + $row = $stmt->fetch(); + if (!$row) $this->fail('Versandprofil nicht gefunden', null, 404); + return $this->formatSmtpProfileRow($row); + } + + private function fetchSmtpProfilePassword(int $customerId, int $profileId): string + { + if ($customerId <= 0 || $profileId <= 0) { + return ''; + } + $this->ensureSmtpProfilesTableExists(); + $table = $this->smtpProfilesTable(); + $stmt = $this->pdo->prepare("SELECT `smtp_pass` FROM `$table` WHERE `id` = :id AND `customer_id` = :cid LIMIT 1"); + $stmt->execute([':id' => $profileId, ':cid' => $customerId]); + $row = $stmt->fetch(); + return (string)($row['smtp_pass'] ?? ''); + } + + private function formatSmtpProfileRow(array $row): array + { + $pass = (string)($row['smtp_pass'] ?? ''); + return [ + 'id' => (int)($row['id'] ?? 0), + 'label' => $row['label'] ?? '', + 'smtp_host' => $row['smtp_host'] ?? '', + 'smtp_port' => isset($row['smtp_port']) ? (int)$row['smtp_port'] : 0, + 'smtp_user' => $row['smtp_user'] ?? '', + 'smtp_secure' => $row['smtp_secure'] ?? '', + 'from_email' => $row['from_email'] ?? '', + 'from_name' => $row['from_name'] ?? '', + 'reply_to' => $row['reply_to'] ?? '', + 'smtp_pass_set' => $pass !== '', + 'created_at' => $row['created_at'] ?? null, + 'updated_at' => $row['updated_at'] ?? null, + ]; + } + private function formatUserOutput(array $row): array { return [