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';
Keine individuellen Absender gefunden. Lege sie unter „Mein Konto“ an.
+
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 [