Compare commits
10 Commits
e563617da4
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
| 409fd3ac36 | |||
| 1fc2bcbe47 | |||
| cdccc73aaa | |||
| 5882e9745e | |||
| e55c88ac98 | |||
| 8801a8ba32 | |||
| 6f7ac43a0b | |||
| 6a8d5720c5 | |||
| 66d29ff6c4 | |||
| da44c2c9a5 |
@@ -40,6 +40,68 @@ require dirname(__DIR__) . '/../structure/layout_start.php';
|
|||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section class="section-card" data-role="admin">
|
||||||
|
<div class="flex items-center justify-between mb-3">
|
||||||
|
<h4>Versandprofile (SMTP)</h4>
|
||||||
|
<button type="button" id="btn-smtp-profile-add" class="btn">+ Versandprofil</button>
|
||||||
|
</div>
|
||||||
|
<p class="text-sm text-slate-600 mb-3">Mehrere SMTP-Profile fuer unterschiedliche Absender oder Server.</p>
|
||||||
|
<div class="overflow-auto">
|
||||||
|
<table class="team-table" id="smtpProfileTable">
|
||||||
|
<thead>
|
||||||
|
<tr><th>Bezeichnung</th><th>Server</th><th>Benutzer</th><th>Absender</th><th class="text-right">Aktionen</th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<form id="smtpProfileForm" class="space-y-3 mt-4 hidden">
|
||||||
|
<input type="hidden" name="profile_id">
|
||||||
|
<label class="block text-sm text-slate-600">Bezeichnung
|
||||||
|
<input type="text" name="label" class="input mt-1" placeholder="Profil-Name">
|
||||||
|
</label>
|
||||||
|
<label class="inline-flex items-center gap-2 text-sm text-slate-600">
|
||||||
|
<input type="checkbox" name="is_default" value="1"> Als Standard-Testprofil verwenden
|
||||||
|
</label>
|
||||||
|
<div class="grid md:grid-cols-2 gap-3">
|
||||||
|
<label class="block text-sm text-slate-600">SMTP-Server
|
||||||
|
<input type="text" name="smtp_host" class="input mt-1" placeholder="smtp.example.com">
|
||||||
|
</label>
|
||||||
|
<label class="block text-sm text-slate-600">Port
|
||||||
|
<input type="number" name="smtp_port" class="input mt-1" placeholder="587">
|
||||||
|
</label>
|
||||||
|
<label class="block text-sm text-slate-600">Verschluesselung
|
||||||
|
<select name="smtp_secure" class="input mt-1">
|
||||||
|
<option value="">Keine</option>
|
||||||
|
<option value="tls">TLS</option>
|
||||||
|
<option value="ssl">SSL</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label class="block text-sm text-slate-600">Benutzername
|
||||||
|
<input type="text" name="smtp_user" class="input mt-1" placeholder="user@example.com">
|
||||||
|
</label>
|
||||||
|
<label class="block text-sm text-slate-600">Passwort
|
||||||
|
<input type="password" name="smtp_pass" class="input mt-1" placeholder="••••••••">
|
||||||
|
</label>
|
||||||
|
<label class="inline-flex items-center gap-2 text-xs text-slate-500 mt-6">
|
||||||
|
<input type="checkbox" name="smtp_pass_clear" value="1"> Passwort loeschen
|
||||||
|
</label>
|
||||||
|
<label class="block text-sm text-slate-600">Absender (E-Mail)
|
||||||
|
<input type="email" name="from_email" class="input mt-1" placeholder="no-reply@example.com">
|
||||||
|
</label>
|
||||||
|
<label class="block text-sm text-slate-600">Absender (Name)
|
||||||
|
<input type="text" name="from_name" class="input mt-1" placeholder="EmailTemplate">
|
||||||
|
</label>
|
||||||
|
<label class="block text-sm text-slate-600">Reply-To (optional)
|
||||||
|
<input type="email" name="reply_to" class="input mt-1" placeholder="support@example.com">
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-end gap-2">
|
||||||
|
<button type="button" id="smtpProfileFormCancel" class="btn">Abbrechen</button>
|
||||||
|
<button type="submit" class="btn">Speichern</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section class="section-card" data-role="admin">
|
<section class="section-card" data-role="admin">
|
||||||
<h4>Integrationen, Downloads & Tokens</h4>
|
<h4>Integrationen, Downloads & Tokens</h4>
|
||||||
<p class="text-sm text-slate-600 mb-4">Die Dateien enthalten automatisch deine aktuellen Tokens. Nach dem Speichern neuer Tokens bitte die Dateien erneut herunterladen.</p>
|
<p class="text-sm text-slate-600 mb-4">Die Dateien enthalten automatisch deine aktuellen Tokens. Nach dem Speichern neuer Tokens bitte die Dateien erneut herunterladen.</p>
|
||||||
@@ -68,49 +130,6 @@ require dirname(__DIR__) . '/../structure/layout_start.php';
|
|||||||
<button type="button" class="btn" data-rotate="external">Neu erstellen</button>
|
<button type="button" class="btn" data-rotate="external">Neu erstellen</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<fieldset class="border border-slate-200 rounded-xl p-4">
|
|
||||||
<legend class="px-2 text-sm font-semibold">SMTP-Versand</legend>
|
|
||||||
<p class="text-xs text-slate-500 mb-3">Diese Zugangsdaten werden fuer den Testversand genutzt, wenn SMTP aktiviert ist.</p>
|
|
||||||
<div class="flex items-center justify-between gap-3 mb-3">
|
|
||||||
<label class="inline-flex items-center gap-2 text-sm text-slate-600">
|
|
||||||
<input type="checkbox" name="smtp_enabled" value="1"> SMTP aktivieren
|
|
||||||
</label>
|
|
||||||
<button type="button" id="btn-smtp-test" class="btn">SMTP Test</button>
|
|
||||||
</div>
|
|
||||||
<div class="grid md:grid-cols-2 gap-3">
|
|
||||||
<label class="block text-sm text-slate-600">SMTP-Server
|
|
||||||
<input type="text" name="smtp_host" class="input mt-1" placeholder="smtp.example.com">
|
|
||||||
</label>
|
|
||||||
<label class="block text-sm text-slate-600">Port
|
|
||||||
<input type="number" name="smtp_port" class="input mt-1" placeholder="587">
|
|
||||||
</label>
|
|
||||||
<label class="block text-sm text-slate-600">Verschluesselung
|
|
||||||
<select name="smtp_secure" class="input mt-1">
|
|
||||||
<option value="">Keine</option>
|
|
||||||
<option value="tls">TLS</option>
|
|
||||||
<option value="ssl">SSL</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
<label class="block text-sm text-slate-600">Benutzername
|
|
||||||
<input type="text" name="smtp_user" class="input mt-1" placeholder="user@example.com">
|
|
||||||
</label>
|
|
||||||
<label class="block text-sm text-slate-600">Passwort
|
|
||||||
<input type="password" name="smtp_pass" class="input mt-1" placeholder="••••••••">
|
|
||||||
</label>
|
|
||||||
<label class="inline-flex items-center gap-2 text-xs text-slate-500 mt-6">
|
|
||||||
<input type="checkbox" name="smtp_pass_clear" value="1"> Passwort loeschen
|
|
||||||
</label>
|
|
||||||
<label class="block text-sm text-slate-600">SMTP-Absender (E-Mail)
|
|
||||||
<input type="email" name="smtp_from_email" class="input mt-1" placeholder="no-reply@example.com">
|
|
||||||
</label>
|
|
||||||
<label class="block text-sm text-slate-600">SMTP-Absender (Name)
|
|
||||||
<input type="text" name="smtp_from_name" class="input mt-1" placeholder="EmailTemplate">
|
|
||||||
</label>
|
|
||||||
<label class="block text-sm text-slate-600">Reply-To (optional)
|
|
||||||
<input type="email" name="smtp_reply_to" class="input mt-1" placeholder="support@example.com">
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</fieldset>
|
|
||||||
<div class="flex justify-between gap-2 flex-wrap pt-2">
|
<div class="flex justify-between gap-2 flex-wrap pt-2">
|
||||||
<div class="flex gap-2" data-role="admin">
|
<div class="flex gap-2" data-role="admin">
|
||||||
<button type="button" class="btn" data-download="bridge">Bridge-Datei</button>
|
<button type="button" class="btn" data-download="bridge">Bridge-Datei</button>
|
||||||
|
|||||||
@@ -194,7 +194,7 @@
|
|||||||
removable: true,
|
removable: true,
|
||||||
draggable: true,
|
draggable: true,
|
||||||
copyable: true,
|
copyable: true,
|
||||||
droppable: false,
|
droppable: true,
|
||||||
'lib-id': this.get('lib-id') || '',
|
'lib-id': this.get('lib-id') || '',
|
||||||
'lib-kind': this.get('lib-kind') || '',
|
'lib-kind': this.get('lib-kind') || '',
|
||||||
rawHtml: this.get('rawHtml') || '',
|
rawHtml: this.get('rawHtml') || '',
|
||||||
@@ -446,6 +446,26 @@
|
|||||||
}, 0);
|
}, 0);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const ensureReferenceDropHandling = (editor) => {
|
||||||
|
if (!editor || editor.__bridgeRefDropPatch) return;
|
||||||
|
editor.__bridgeRefDropPatch = true;
|
||||||
|
editor.on('component:add', (cmp) => {
|
||||||
|
try {
|
||||||
|
if (!cmp || typeof cmp.parent !== 'function') return;
|
||||||
|
const parent = cmp.parent();
|
||||||
|
if (!parent || typeof parent.get !== 'function') return;
|
||||||
|
if (parent.get('type') !== REFERENCE_COMPONENT_TYPE) return;
|
||||||
|
const grand = parent.parent && parent.parent();
|
||||||
|
if (!grand || typeof grand.components !== 'function') {
|
||||||
|
cmp.remove();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const at = grand.components().indexOf(parent) + 1;
|
||||||
|
grand.append(cmp, { at });
|
||||||
|
} catch {}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
// (3) HINZUGEFÜGT: Speichern-Befehl (Command)
|
// (3) HINZUGEFÜGT: Speichern-Befehl (Command)
|
||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
@@ -770,6 +790,7 @@
|
|||||||
const plugin = (editor) => {
|
const plugin = (editor) => {
|
||||||
preRegisterCategoriesAndPlaceholders(editor);
|
preRegisterCategoriesAndPlaceholders(editor);
|
||||||
registerReferenceComponent(editor);
|
registerReferenceComponent(editor);
|
||||||
|
ensureReferenceDropHandling(editor);
|
||||||
registerSaveCommand(editor); // HINZUGEFÜGT: Speichern-Logik
|
registerSaveCommand(editor); // HINZUGEFÜGT: Speichern-Logik
|
||||||
|
|
||||||
editor.on('load', () => {
|
editor.on('load', () => {
|
||||||
|
|||||||
@@ -24,9 +24,11 @@ export function initEditor() {
|
|||||||
const sendSubject = document.getElementById('send_subject');
|
const sendSubject = document.getElementById('send_subject');
|
||||||
const sendInfo = document.getElementById('send_template_info');
|
const sendInfo = document.getElementById('send_template_info');
|
||||||
const btnCancelSend= document.getElementById('btn-cancel-send');
|
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 sendSender = document.getElementById('send_sender');
|
||||||
const sendSenderHint = document.getElementById('send_sender_hint');
|
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 prevFrame = document.getElementById('previewFrame');
|
||||||
const btnPrevClose = document.getElementById('btn-close-preview');
|
const btnPrevClose = document.getElementById('btn-close-preview');
|
||||||
const unsavedDialog = document.getElementById('unsavedDialog');
|
const unsavedDialog = document.getElementById('unsavedDialog');
|
||||||
@@ -46,6 +48,8 @@ export function initEditor() {
|
|||||||
let reqToken = 0; // steigender Token pro Öffnen -> ignoriert verspätete Events
|
let reqToken = 0; // steigender Token pro Öffnen -> ignoriert verspätete Events
|
||||||
let senderOptions = [];
|
let senderOptions = [];
|
||||||
let senderLoadPromise = null;
|
let senderLoadPromise = null;
|
||||||
|
let smtpProfileOptions = [];
|
||||||
|
let smtpProfileLoadPromise = null;
|
||||||
let currentEditorType = 'grapesjs';
|
let currentEditorType = 'grapesjs';
|
||||||
let versionItems = [];
|
let versionItems = [];
|
||||||
let savedSnapshot = '';
|
let savedSnapshot = '';
|
||||||
@@ -902,6 +906,24 @@ export function initEditor() {
|
|||||||
return senderLoadPromise;
|
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() {
|
function renderSenderOptions() {
|
||||||
if (!sendSender) return;
|
if (!sendSender) return;
|
||||||
const previous = sendSender.value;
|
const previous = sendSender.value;
|
||||||
@@ -921,6 +943,27 @@ export function initEditor() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderSmtpProfileOptions() {
|
||||||
|
if (!sendSmtpProfile) return;
|
||||||
|
const previous = sendSmtpProfile.value;
|
||||||
|
let html = '<option value="">Standard (System)</option>';
|
||||||
|
smtpProfileOptions.forEach(opt => {
|
||||||
|
const label = opt.label || opt.smtp_host || 'Profil';
|
||||||
|
const suffix = opt.is_default ? ' (Standard)' : '';
|
||||||
|
html += `<option value="${opt.id}">${escapeHtml(label + suffix)}</option>`;
|
||||||
|
});
|
||||||
|
sendSmtpProfile.innerHTML = html;
|
||||||
|
if (previous && smtpProfileOptions.some(opt => String(opt.id) === previous)) {
|
||||||
|
sendSmtpProfile.value = previous;
|
||||||
|
} else {
|
||||||
|
const def = smtpProfileOptions.find(opt => opt.is_default);
|
||||||
|
sendSmtpProfile.value = def ? String(def.id) : '';
|
||||||
|
}
|
||||||
|
if (sendSmtpProfileHint) {
|
||||||
|
sendSmtpProfileHint.classList.toggle('hidden', smtpProfileOptions.length > 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---------- Initialen HTML-Inhalt in Editor pushen (mit Token/Race-Schutz) ----------
|
// ---------- Initialen HTML-Inhalt in Editor pushen (mit Token/Race-Schutz) ----------
|
||||||
async function pushInitialHtmlToEditor({ mode, html, snippets, ref, token, hasJson, json }) {
|
async function pushInitialHtmlToEditor({ mode, html, snippets, ref, token, hasJson, json }) {
|
||||||
if (token !== reqToken) return; // veraltete Anfrage ignorieren
|
if (token !== reqToken) return; // veraltete Anfrage ignorieren
|
||||||
@@ -1250,8 +1293,9 @@ export function initEditor() {
|
|||||||
prevDlg?.showModal?.();
|
prevDlg?.showModal?.();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function openSend(ctx = null) {
|
async function openSend(ctx = null, sectionOverride = null) {
|
||||||
if (!current?.section?.is_template) {
|
const sectionCtx = sectionOverride || current?.section || window.__activeSection || null;
|
||||||
|
if (!sectionCtx?.is_template) {
|
||||||
err('Kein Template geladen');
|
err('Kein Template geladen');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1266,11 +1310,13 @@ export function initEditor() {
|
|||||||
if (sendSubject) sendSubject.value = ctx?.subject || 'Testversand';
|
if (sendSubject) sendSubject.value = ctx?.subject || 'Testversand';
|
||||||
if (sendTo) sendTo.value = ctx?.to || '';
|
if (sendTo) sendTo.value = ctx?.to || '';
|
||||||
await loadSenderOptions(true);
|
await loadSenderOptions(true);
|
||||||
|
await loadSmtpProfileOptions(true);
|
||||||
sendDlg?.showModal?.();
|
sendDlg?.showModal?.();
|
||||||
}
|
}
|
||||||
function closeSend(){
|
function closeSend(){
|
||||||
sendDlg?.close?.();
|
sendDlg?.close?.();
|
||||||
if (sendSender) sendSender.value = '';
|
if (sendSender) sendSender.value = '';
|
||||||
|
if (sendSmtpProfile) sendSmtpProfile.value = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
async function doSend(ev){
|
async function doSend(ev){
|
||||||
@@ -1291,8 +1337,11 @@ export function initEditor() {
|
|||||||
if (sendSender && sendSender.value) {
|
if (sendSender && sendSender.value) {
|
||||||
payload.sender_id = Number(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 });
|
const r = await apiAction('templates.test_send', { method:'POST', data: payload });
|
||||||
if(r?.ok){ toast("Testversand ausgelöst"); closeSend(); } else { toast("Senden fehlgeschlagen", false); }
|
if(r?.ok){ toast("Testversand erfolgreich versendet", true); closeSend(); } else { toast("Senden fehlgeschlagen", false); }
|
||||||
}
|
}
|
||||||
function closePreview(){ prevDlg?.close?.(); }
|
function closePreview(){ prevDlg?.close?.(); }
|
||||||
|
|
||||||
@@ -1489,13 +1538,14 @@ export function initEditor() {
|
|||||||
err('Testversand: Keine ID vorhanden');
|
err('Testversand: Keine ID vorhanden');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!current?.section?.is_template) {
|
const sectionCtx = opts.section || window.__activeSection || current?.section || null;
|
||||||
|
if (!sectionCtx?.is_template) {
|
||||||
err('Kein Template geladen');
|
err('Kein Template geladen');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
window.__currentItemId = targetId;
|
window.__currentItemId = targetId;
|
||||||
setSendContext(targetId, opts.name || '');
|
setSendContext(targetId, opts.name || '');
|
||||||
openSend({ id: targetId, name: opts.name || '' });
|
openSend({ id: targetId, name: opts.name || '' }, sectionCtx);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Public API
|
// Public API
|
||||||
|
|||||||
@@ -643,13 +643,8 @@ export async function loadList(section) {
|
|||||||
toast('Testversand: Ungültige ID', false);
|
toast('Testversand: Ungültige ID', false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const activeCheck = await apiAction('content.get', { method: 'GET', data: { id, section_id: section.id, active_only: 1 } }).catch(() => ({}));
|
|
||||||
if (!activeCheck?.active_version_id && !activeCheck?.item?.active_version_id) {
|
|
||||||
toast('Testversand nur mit aktiver Version möglich.', false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (window.AdminTestSend && typeof window.AdminTestSend.open === 'function') {
|
if (window.AdminTestSend && typeof window.AdminTestSend.open === 'function') {
|
||||||
window.AdminTestSend.open({ id, name: nm });
|
window.AdminTestSend.open({ id, name: nm, section });
|
||||||
} else {
|
} else {
|
||||||
toast('Testversand ist aktuell nicht verfügbar.', false);
|
toast('Testversand ist aktuell nicht verfügbar.', false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ const state = {
|
|||||||
userMap: new Map(),
|
userMap: new Map(),
|
||||||
senders: [],
|
senders: [],
|
||||||
senderMap: new Map(),
|
senderMap: new Map(),
|
||||||
|
smtpProfiles: [],
|
||||||
|
smtpProfileMap: new Map(),
|
||||||
currentTab: 'profile',
|
currentTab: 'profile',
|
||||||
loading: false,
|
loading: false,
|
||||||
};
|
};
|
||||||
@@ -25,6 +27,8 @@ let teamTable;
|
|||||||
let userForm;
|
let userForm;
|
||||||
let senderTable;
|
let senderTable;
|
||||||
let senderForm;
|
let senderForm;
|
||||||
|
let smtpProfileTable;
|
||||||
|
let smtpProfileForm;
|
||||||
let sectionsList;
|
let sectionsList;
|
||||||
let sectionsCreateForm;
|
let sectionsCreateForm;
|
||||||
let sectionNameInput;
|
let sectionNameInput;
|
||||||
@@ -56,7 +60,6 @@ let adminTablesSelectedSelect;
|
|||||||
let adminTablesAddBtn;
|
let adminTablesAddBtn;
|
||||||
let adminTablesRemoveBtn;
|
let adminTablesRemoveBtn;
|
||||||
let adminLoadBridgeBtn;
|
let adminLoadBridgeBtn;
|
||||||
let smtpTestBtn;
|
|
||||||
|
|
||||||
ensureConsoleCapture();
|
ensureConsoleCapture();
|
||||||
|
|
||||||
@@ -82,12 +85,13 @@ export function initAccountPage() {
|
|||||||
userForm = document.getElementById('userForm');
|
userForm = document.getElementById('userForm');
|
||||||
senderTable = document.getElementById('senderTable');
|
senderTable = document.getElementById('senderTable');
|
||||||
senderForm = document.getElementById('senderForm');
|
senderForm = document.getElementById('senderForm');
|
||||||
|
smtpProfileTable = document.getElementById('smtpProfileTable');
|
||||||
|
smtpProfileForm = document.getElementById('smtpProfileForm');
|
||||||
adminTablesAllSelect = document.getElementById('adminBridgeTablesAll');
|
adminTablesAllSelect = document.getElementById('adminBridgeTablesAll');
|
||||||
adminTablesSelectedSelect = document.getElementById('adminBridgeTablesSelected');
|
adminTablesSelectedSelect = document.getElementById('adminBridgeTablesSelected');
|
||||||
adminTablesAddBtn = document.getElementById('adminBridgeTablesAdd');
|
adminTablesAddBtn = document.getElementById('adminBridgeTablesAdd');
|
||||||
adminTablesRemoveBtn = document.getElementById('adminBridgeTablesRemove');
|
adminTablesRemoveBtn = document.getElementById('adminBridgeTablesRemove');
|
||||||
adminLoadBridgeBtn = document.getElementById('btn-admin-load-bridge');
|
adminLoadBridgeBtn = document.getElementById('btn-admin-load-bridge');
|
||||||
smtpTestBtn = document.getElementById('btn-smtp-test');
|
|
||||||
sectionsList = document.getElementById('sectionsList');
|
sectionsList = document.getElementById('sectionsList');
|
||||||
sectionsCreateForm = document.getElementById('sectionsCreateForm');
|
sectionsCreateForm = document.getElementById('sectionsCreateForm');
|
||||||
sectionNameInput = document.getElementById('sectionNameInput');
|
sectionNameInput = document.getElementById('sectionNameInput');
|
||||||
@@ -105,11 +109,16 @@ export function initAccountPage() {
|
|||||||
document.getElementById('senderFormCancel')?.addEventListener('click', () => closeSenderForm());
|
document.getElementById('senderFormCancel')?.addEventListener('click', () => closeSenderForm());
|
||||||
senderForm?.addEventListener('submit', submitSenderForm);
|
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);
|
profileForm?.addEventListener('submit', submitProfileForm);
|
||||||
passwordForm?.addEventListener('submit', submitPasswordForm);
|
passwordForm?.addEventListener('submit', submitPasswordForm);
|
||||||
settingsForm?.addEventListener('submit', submitSettingsForm);
|
settingsForm?.addEventListener('submit', submitSettingsForm);
|
||||||
teamTable?.addEventListener('click', handleTeamTableClick);
|
teamTable?.addEventListener('click', handleTeamTableClick);
|
||||||
senderTable?.addEventListener('click', handleSenderTableClick);
|
senderTable?.addEventListener('click', handleSenderTableClick);
|
||||||
|
smtpProfileTable?.addEventListener('click', handleSmtpProfileTableClick);
|
||||||
document.querySelectorAll('[data-user-tab]').forEach(btn => {
|
document.querySelectorAll('[data-user-tab]').forEach(btn => {
|
||||||
btn.addEventListener('click', () => switchTab(btn.getAttribute('data-user-tab')));
|
btn.addEventListener('click', () => switchTab(btn.getAttribute('data-user-tab')));
|
||||||
});
|
});
|
||||||
@@ -142,9 +151,6 @@ export function initAccountPage() {
|
|||||||
adminLoadBridgeBtn?.addEventListener('click', () => {
|
adminLoadBridgeBtn?.addEventListener('click', () => {
|
||||||
refreshBridgeTablesFromEndpoint();
|
refreshBridgeTablesFromEndpoint();
|
||||||
});
|
});
|
||||||
smtpTestBtn?.addEventListener('click', () => {
|
|
||||||
runSmtpTest();
|
|
||||||
});
|
|
||||||
|
|
||||||
initSectionsManager();
|
initSectionsManager();
|
||||||
|
|
||||||
@@ -421,6 +427,15 @@ async function loadAccountData() {
|
|||||||
renderSenderList();
|
renderSenderList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (smtpProfileTable) {
|
||||||
|
if (isAdmin()) {
|
||||||
|
await loadSmtpProfiles();
|
||||||
|
} else {
|
||||||
|
state.smtpProfiles = [];
|
||||||
|
state.smtpProfileMap = new Map();
|
||||||
|
renderSmtpProfileList();
|
||||||
|
}
|
||||||
|
}
|
||||||
reportViewDebugInfo(res);
|
reportViewDebugInfo(res);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
@@ -453,19 +468,6 @@ function fillSettingsForm(settings) {
|
|||||||
: 0;
|
: 0;
|
||||||
settingsForm.versions_retention.value = String(Math.max(0, retention));
|
settingsForm.versions_retention.value = String(Math.max(0, retention));
|
||||||
}
|
}
|
||||||
if (settingsForm.smtp_enabled) settingsForm.smtp_enabled.checked = Number(settings.smtp_enabled) === 1;
|
|
||||||
if (settingsForm.smtp_host) settingsForm.smtp_host.value = settings.smtp_host || '';
|
|
||||||
if (settingsForm.smtp_port) settingsForm.smtp_port.value = settings.smtp_port ? String(settings.smtp_port) : '';
|
|
||||||
if (settingsForm.smtp_user) settingsForm.smtp_user.value = settings.smtp_user || '';
|
|
||||||
if (settingsForm.smtp_pass) settingsForm.smtp_pass.value = '';
|
|
||||||
if (settingsForm.smtp_pass) {
|
|
||||||
settingsForm.smtp_pass.placeholder = settings.smtp_pass_set ? 'Passwort gesetzt' : '••••••••';
|
|
||||||
}
|
|
||||||
if (settingsForm.smtp_pass_clear) settingsForm.smtp_pass_clear.checked = false;
|
|
||||||
if (settingsForm.smtp_secure) settingsForm.smtp_secure.value = settings.smtp_secure || '';
|
|
||||||
if (settingsForm.smtp_from_email) settingsForm.smtp_from_email.value = settings.smtp_from_email || '';
|
|
||||||
if (settingsForm.smtp_from_name) settingsForm.smtp_from_name.value = settings.smtp_from_name || '';
|
|
||||||
if (settingsForm.smtp_reply_to) settingsForm.smtp_reply_to.value = settings.smtp_reply_to || '';
|
|
||||||
refreshAdminTables(settings.bridge_setup?.tables || [], settings.bridge_tables || []);
|
refreshAdminTables(settings.bridge_setup?.tables || [], settings.bridge_tables || []);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -519,19 +521,6 @@ async function submitSettingsForm(ev) {
|
|||||||
const parsed = raw === '' ? 0 : Number(raw);
|
const parsed = raw === '' ? 0 : Number(raw);
|
||||||
data.versions_retention = Number.isFinite(parsed) ? Math.max(0, Math.floor(parsed)) : 0;
|
data.versions_retention = Number.isFinite(parsed) ? Math.max(0, Math.floor(parsed)) : 0;
|
||||||
}
|
}
|
||||||
if (settingsForm.smtp_enabled) data.smtp_enabled = settingsForm.smtp_enabled.checked ? 1 : 0;
|
|
||||||
if (settingsForm.smtp_host) data.smtp_host = settingsForm.smtp_host.value.trim();
|
|
||||||
if (settingsForm.smtp_port) {
|
|
||||||
const rawPort = settingsForm.smtp_port.value.trim();
|
|
||||||
data.smtp_port = rawPort === '' ? 0 : Number(rawPort);
|
|
||||||
}
|
|
||||||
if (settingsForm.smtp_user) data.smtp_user = settingsForm.smtp_user.value.trim();
|
|
||||||
if (settingsForm.smtp_pass) data.smtp_pass = settingsForm.smtp_pass.value;
|
|
||||||
if (settingsForm.smtp_pass_clear) data.smtp_pass_clear = settingsForm.smtp_pass_clear.checked ? 1 : 0;
|
|
||||||
if (settingsForm.smtp_secure) data.smtp_secure = settingsForm.smtp_secure.value;
|
|
||||||
if (settingsForm.smtp_from_email) data.smtp_from_email = settingsForm.smtp_from_email.value.trim();
|
|
||||||
if (settingsForm.smtp_from_name) data.smtp_from_name = settingsForm.smtp_from_name.value.trim();
|
|
||||||
if (settingsForm.smtp_reply_to) data.smtp_reply_to = settingsForm.smtp_reply_to.value.trim();
|
|
||||||
if (adminTablesAllSelect && adminTablesSelectedSelect) {
|
if (adminTablesAllSelect && adminTablesSelectedSelect) {
|
||||||
const bridgeTables = normalizeTableList(state.settings.bridge_tables || []);
|
const bridgeTables = normalizeTableList(state.settings.bridge_tables || []);
|
||||||
data.bridge_tables = bridgeTables;
|
data.bridge_tables = bridgeTables;
|
||||||
@@ -568,30 +557,6 @@ async function downloadFile(type) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function runSmtpTest() {
|
|
||||||
if (!settingsForm) return;
|
|
||||||
const recipient = prompt('Test-E-Mail an welche Adresse senden?', window.__currentUser?.email || '');
|
|
||||||
if (!recipient) return;
|
|
||||||
const data = {
|
|
||||||
to: recipient.trim(),
|
|
||||||
smtp_enabled: settingsForm.smtp_enabled?.checked ? 1 : 0,
|
|
||||||
smtp_host: settingsForm.smtp_host?.value.trim() || '',
|
|
||||||
smtp_port: settingsForm.smtp_port?.value.trim() || '',
|
|
||||||
smtp_user: settingsForm.smtp_user?.value.trim() || '',
|
|
||||||
smtp_pass: settingsForm.smtp_pass?.value || '',
|
|
||||||
smtp_secure: settingsForm.smtp_secure?.value || '',
|
|
||||||
smtp_from_email: settingsForm.smtp_from_email?.value.trim() || '',
|
|
||||||
smtp_from_name: settingsForm.smtp_from_name?.value.trim() || '',
|
|
||||||
smtp_reply_to: settingsForm.smtp_reply_to?.value.trim() || '',
|
|
||||||
};
|
|
||||||
try {
|
|
||||||
const res = await apiAction('account.smtp.test', { method: 'POST', data });
|
|
||||||
if (!res?.ok) throw new Error(res?.error || 'SMTP Test fehlgeschlagen');
|
|
||||||
toast('SMTP Test gesendet', true);
|
|
||||||
} catch (err) {
|
|
||||||
toast(err.message || 'SMTP Test fehlgeschlagen', false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeTableList(input) {
|
function normalizeTableList(input) {
|
||||||
const items = Array.isArray(input) ? input : (typeof input === 'string' ? input.split(/[\s,]+/) : []);
|
const items = Array.isArray(input) ? input : (typeof input === 'string' ? input.split(/[\s,]+/) : []);
|
||||||
@@ -928,6 +893,175 @@ 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 = '<tr><td colspan="5" class="text-sm text-slate-500">Keine Versandprofile vorhanden.</td></tr>';
|
||||||
|
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 `
|
||||||
|
<tr>
|
||||||
|
<td>${escapeHtml(label)}${profile.is_default ? ' <span class="text-xs text-slate-500">(Standard)</span>' : ''}</td>
|
||||||
|
<td>${escapeHtml(host)}</td>
|
||||||
|
<td>${escapeHtml(user)}</td>
|
||||||
|
<td>${escapeHtml(sender)}</td>
|
||||||
|
<td class="text-right">
|
||||||
|
<button class="btn" data-smtp-action="test" data-smtp-id="${profile.id}">Test</button>
|
||||||
|
<button class="btn" data-smtp-action="edit" data-smtp-id="${profile.id}">Bearbeiten</button>
|
||||||
|
<button class="btn" data-smtp-action="copy" data-smtp-id="${profile.id}">Kopieren</button>
|
||||||
|
<button class="btn btn-danger" data-smtp-action="delete" data-smtp-id="${profile.id}">Löschen</button>
|
||||||
|
${profile.is_default ? '' : `<button class="btn" data-smtp-action="set-default" data-smtp-id="${profile.id}">Standard</button>`}
|
||||||
|
</td>
|
||||||
|
</tr>`;
|
||||||
|
}).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 === 'test') {
|
||||||
|
runSmtpProfileTest(profile.id);
|
||||||
|
} else if (action === 'set-default') {
|
||||||
|
setDefaultSmtpProfile(profile.id);
|
||||||
|
} 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 || '';
|
||||||
|
smtpProfileForm.is_default.checked = !!profile?.is_default;
|
||||||
|
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,
|
||||||
|
is_default: smtpProfileForm.is_default?.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runSmtpProfileTest(profileId) {
|
||||||
|
const recipient = prompt('Test-E-Mail an welche Adresse senden?', window.__currentUser?.email || '');
|
||||||
|
if (!recipient) return;
|
||||||
|
try {
|
||||||
|
const res = await apiAction('account.smtp.test', { method: 'POST', data: { to: recipient.trim(), smtp_profile_id: profileId } });
|
||||||
|
if (!res?.ok) throw new Error(res?.error || 'SMTP Test fehlgeschlagen');
|
||||||
|
toast('SMTP Test gesendet', true);
|
||||||
|
} catch (err) {
|
||||||
|
toast(err.message || 'SMTP Test fehlgeschlagen', false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setDefaultSmtpProfile(profileId) {
|
||||||
|
try {
|
||||||
|
const res = await apiAction('account.smtp_profiles.save', { method: 'POST', data: { profile_id: profileId, is_default: 1 } });
|
||||||
|
if (!res?.ok) throw new Error(res?.error || 'Standard konnte nicht gesetzt werden');
|
||||||
|
await loadSmtpProfiles();
|
||||||
|
toast('Standard-Testprofil gesetzt', true);
|
||||||
|
} catch (err) {
|
||||||
|
toast(err.message || 'Standard konnte nicht gesetzt werden', false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function escapeHtml(str) {
|
function escapeHtml(str) {
|
||||||
return String(str || '')
|
return String(str || '')
|
||||||
.replace(/&/g, '&')
|
.replace(/&/g, '&')
|
||||||
|
|||||||
@@ -130,6 +130,13 @@ require __DIR__ . '/../partials/structure/layout_start.php';
|
|||||||
</select>
|
</select>
|
||||||
<p id="send_sender_hint" class="text-xs text-slate-500 mt-1 hidden">Keine individuellen Absender gefunden. Lege sie unter „Mein Konto“ an.</p>
|
<p id="send_sender_hint" class="text-xs text-slate-500 mt-1 hidden">Keine individuellen Absender gefunden. Lege sie unter „Mein Konto“ an.</p>
|
||||||
</label>
|
</label>
|
||||||
|
<label class="block">
|
||||||
|
<span class="text-sm text-slate-600">Versandprofil (SMTP)</span>
|
||||||
|
<select id="send_smtp_profile" class="mt-1 w-full border rounded-lg px-3 py-2">
|
||||||
|
<option value="">Standard (System)</option>
|
||||||
|
</select>
|
||||||
|
<p id="send_smtp_profile_hint" class="text-xs text-slate-500 mt-1 hidden">Keine Versandprofile gefunden. Lege sie unter „Mein Konto“ an.</p>
|
||||||
|
</label>
|
||||||
<div class="flex justify-end gap-2">
|
<div class="flex justify-end gap-2">
|
||||||
<button type="button" id="btn-cancel-send" class="btn">Abbrechen</button>
|
<button type="button" id="btn-cancel-send" class="btn">Abbrechen</button>
|
||||||
<button type="submit" id="btn-send-now" class="btn">Senden</button>
|
<button type="submit" id="btn-send-now" class="btn">Senden</button>
|
||||||
|
|||||||
22
schema.sql
22
schema.sql
@@ -212,6 +212,28 @@ CREATE TABLE IF NOT EXISTS `emailtemplate_sender_identities` (
|
|||||||
CONSTRAINT `fk_sender_customer` FOREIGN KEY (`customer_id`) REFERENCES `emailtemplate_customers` (`id`) ON DELETE CASCADE
|
CONSTRAINT `fk_sender_customer` FOREIGN KEY (`customer_id`) REFERENCES `emailtemplate_customers` (`id`) ON DELETE CASCADE
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
) 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,
|
||||||
|
`is_default` tinyint(1) DEFAULT 0,
|
||||||
|
`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
|
-- Tabelle: emailtemplate_snippets
|
||||||
CREATE TABLE IF NOT EXISTS `emailtemplate_snippets` (
|
CREATE TABLE IF NOT EXISTS `emailtemplate_snippets` (
|
||||||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||||
|
|||||||
@@ -2247,6 +2247,10 @@ class ApiKernel
|
|||||||
$subject = 'Testversand';
|
$subject = 'Testversand';
|
||||||
}
|
}
|
||||||
$senderId = (int)$this->val($this->in, ['sender_id'], 0);
|
$senderId = (int)$this->val($this->in, ['sender_id'], 0);
|
||||||
|
$smtpProfileId = (int)$this->val($this->in, ['smtp_profile_id'], 0);
|
||||||
|
if ($smtpProfileId <= 0 && $customerId > 0) {
|
||||||
|
$smtpProfileId = $this->getDefaultSmtpProfileId($customerId);
|
||||||
|
}
|
||||||
|
|
||||||
$row = null;
|
$row = null;
|
||||||
$html = '';
|
$html = '';
|
||||||
@@ -2254,11 +2258,38 @@ class ApiKernel
|
|||||||
if ($customerId <= 0) $this->fail('Customer context missing', null, 500);
|
if ($customerId <= 0) $this->fail('Customer context missing', null, 500);
|
||||||
$section = $this->ensureEmailtemplateSection($customerId);
|
$section = $this->ensureEmailtemplateSection($customerId);
|
||||||
$itemsTable = $this->contentItemsTable();
|
$itemsTable = $this->contentItemsTable();
|
||||||
$sql = "SELECT * FROM `$itemsTable` WHERE `customer_id` = :cid AND `section_id` = :sid AND `id` = :id LIMIT 1";
|
$itemCols = $this->resolveContentItemColumns($itemsTable);
|
||||||
|
$versionsTable = $this->contentVersionsTable();
|
||||||
|
$versionCols = $this->tableExists($versionsTable) ? $this->resolveContentVersionColumns($versionsTable) : null;
|
||||||
|
$versionId = (int)$this->val($this->in, ['version_id', 'content_version_id'], 0);
|
||||||
|
$selectCols = [];
|
||||||
|
if (!empty($itemCols['html'])) $selectCols[] = "i.`{$itemCols['html']}` AS item_html";
|
||||||
|
if (!empty($itemCols['json'])) $selectCols[] = "i.`{$itemCols['json']}` AS item_json";
|
||||||
|
if ($versionCols && !empty($versionCols['html'])) $selectCols[] = "v.`{$versionCols['html']}` AS version_html";
|
||||||
|
$join = '';
|
||||||
|
if ($versionCols) {
|
||||||
|
if ($versionId > 0) {
|
||||||
|
$join = "LEFT JOIN `$versionsTable` v ON v.`content_id` = i.`id` AND v.`id` = :vid";
|
||||||
|
} else {
|
||||||
|
$join = "LEFT JOIN `$versionsTable` v ON v.`content_id` = i.`id` AND v.`is_active` = 1";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$sql = "SELECT " . implode(',', $selectCols) . " FROM `$itemsTable` i $join WHERE i.`customer_id` = :cid AND i.`section_id` = :sid AND i.`id` = :id LIMIT 1";
|
||||||
$stmt = $this->pdo->prepare($sql);
|
$stmt = $this->pdo->prepare($sql);
|
||||||
$stmt->execute([':cid' => $customerId, ':sid' => (int)$section['id'], ':id' => $templateId]);
|
$params = [':cid' => $customerId, ':sid' => (int)$section['id'], ':id' => $templateId];
|
||||||
|
if ($versionId > 0 && $versionCols) {
|
||||||
|
$params[':vid'] = $versionId;
|
||||||
|
}
|
||||||
|
$stmt->execute($params);
|
||||||
$row = $stmt->fetch();
|
$row = $stmt->fetch();
|
||||||
$html = $row ? (string)($row['html'] ?? '') : '';
|
$html = $row ? (string)($row['version_html'] ?? $row['item_html'] ?? '') : '';
|
||||||
|
if ($html === '' && $versionCols && $versionId <= 0) {
|
||||||
|
$sql = "SELECT " . implode(',', $selectCols) . " FROM `$itemsTable` i LEFT JOIN `$versionsTable` v ON v.`content_id` = i.`id` ORDER BY v.`created_at` DESC LIMIT 1";
|
||||||
|
$stmt = $this->pdo->prepare($sql);
|
||||||
|
$stmt->execute();
|
||||||
|
$row = $stmt->fetch();
|
||||||
|
$html = $row ? (string)($row['version_html'] ?? $row['item_html'] ?? '') : $html;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
$t = $this->tableMap['templates'];
|
$t = $this->tableMap['templates'];
|
||||||
[$idCol, $allCols] = $this->resolveIdCol('templates');
|
[$idCol, $allCols] = $this->resolveIdCol('templates');
|
||||||
@@ -2275,7 +2306,13 @@ class ApiKernel
|
|||||||
if (!$row) {
|
if (!$row) {
|
||||||
$this->fail('Template not found', ['id' => $templateId], 404);
|
$this->fail('Template not found', ['id' => $templateId], 404);
|
||||||
}
|
}
|
||||||
if ($html === '' && !empty($row['json_content'])) {
|
$jsonContent = '';
|
||||||
|
if ($row && array_key_exists('json_content', $row)) {
|
||||||
|
$jsonContent = (string)($row['json_content'] ?? '');
|
||||||
|
} elseif ($row && array_key_exists('item_json', $row)) {
|
||||||
|
$jsonContent = (string)($row['item_json'] ?? '');
|
||||||
|
}
|
||||||
|
if ($html === '' && $jsonContent !== '') {
|
||||||
$html = '<p>(Dieses Template enthält noch keine HTML-Inhalte.)</p>';
|
$html = '<p>(Dieses Template enthält noch keine HTML-Inhalte.)</p>';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2291,20 +2328,40 @@ class ApiKernel
|
|||||||
$sender = $this->fetchSenderRow($customerId, $senderId);
|
$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', [
|
$this->writeDebugLog('templates_test_send', [
|
||||||
'time' => date(DATE_ATOM),
|
'time' => date(DATE_ATOM),
|
||||||
'template_id' => $templateId,
|
'template_id' => $templateId,
|
||||||
'to' => $recipient,
|
'to' => $recipient,
|
||||||
'subject' => $subject,
|
'subject' => $subject,
|
||||||
'sender_id' => $senderId > 0 ? $senderId : null,
|
'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_email' => $sender['from_email'] ?? ($this->conf['smtp']['from_email'] ?? null),
|
||||||
'from_name' => $sender['from_name'] ?? ($this->conf['smtp']['from_name'] ?? null),
|
'from_name' => $sender['from_name'] ?? ($this->conf['smtp']['from_name'] ?? null),
|
||||||
'html_length' => strlen($html),
|
'html_length' => strlen($html),
|
||||||
'mail_error' => $this->lastMailError,
|
'mail_error' => $this->lastMailError,
|
||||||
]);
|
]);
|
||||||
$this->fail('Send failed', null, 500);
|
$detail = null;
|
||||||
|
if ($this->isStagingEnv()) {
|
||||||
|
$detail = ['mail_error' => $this->lastMailError];
|
||||||
|
}
|
||||||
|
$this->fail('Send failed', $detail, 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($customerId > 0) {
|
if ($customerId > 0) {
|
||||||
@@ -3054,6 +3111,18 @@ class ApiKernel
|
|||||||
case 'account.senders.delete':
|
case 'account.senders.delete':
|
||||||
$this->handleAccountSenderDelete();
|
$this->handleAccountSenderDelete();
|
||||||
break;
|
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':
|
case 'dashboard.metrics':
|
||||||
$this->handleDashboardMetrics();
|
$this->handleDashboardMetrics();
|
||||||
break;
|
break;
|
||||||
@@ -3351,6 +3420,21 @@ class ApiKernel
|
|||||||
$stmt->execute(array_merge([$customerId], $templateIds));
|
$stmt->execute(array_merge([$customerId], $templateIds));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function incrementTemplateUsage(int $customerId, int $templateId): void
|
||||||
|
{
|
||||||
|
if ($customerId <= 0 || $templateId <= 0) return;
|
||||||
|
$usageTable = $this->lookupTableName('template_usage', 'emailtemplate_template_usage');
|
||||||
|
if (!$this->tableExists($usageTable)) return;
|
||||||
|
try {
|
||||||
|
$sql = "INSERT INTO `$usageTable` (`template_id`,`customer_id`,`render_count`,`last_rendered_at`) VALUES (:tid,:cid,1,NOW())
|
||||||
|
ON DUPLICATE KEY UPDATE `render_count` = `render_count` + 1, `last_rendered_at` = NOW(), `customer_id` = VALUES(`customer_id`)";
|
||||||
|
$stmt = $this->pdo->prepare($sql);
|
||||||
|
$stmt->execute([':tid' => $templateId, ':cid' => $customerId]);
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
// Usage-Tracking darf keine Fehler auslösen.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private function extractIdList($raw): array
|
private function extractIdList($raw): array
|
||||||
{
|
{
|
||||||
if ($raw === null) return [];
|
if ($raw === null) return [];
|
||||||
@@ -3835,8 +3919,22 @@ class ApiKernel
|
|||||||
$this->fail('Valid recipient required', null, 422);
|
$this->fail('Valid recipient required', null, 422);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$smtpProfileId = (int)($this->in['smtp_profile_id'] ?? 0);
|
||||||
$smtpOverride = null;
|
$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 = [
|
$smtpOverride = [
|
||||||
'enabled' => !empty($this->in['smtp_enabled']),
|
'enabled' => !empty($this->in['smtp_enabled']),
|
||||||
'host' => trim((string)($this->in['smtp_host'] ?? '')),
|
'host' => trim((string)($this->in['smtp_host'] ?? '')),
|
||||||
@@ -3858,6 +3956,7 @@ class ApiKernel
|
|||||||
'time' => date(DATE_ATOM),
|
'time' => date(DATE_ATOM),
|
||||||
'customer_id' => $customerId,
|
'customer_id' => $customerId,
|
||||||
'to' => $recipient,
|
'to' => $recipient,
|
||||||
|
'smtp_profile_id' => $smtpProfileId > 0 ? $smtpProfileId : null,
|
||||||
'smtp_enabled' => $smtpOverride['enabled'] ?? null,
|
'smtp_enabled' => $smtpOverride['enabled'] ?? null,
|
||||||
'smtp_host' => $smtpOverride['host'] ?? null,
|
'smtp_host' => $smtpOverride['host'] ?? null,
|
||||||
'smtp_port' => $smtpOverride['port'] ?? null,
|
'smtp_port' => $smtpOverride['port'] ?? null,
|
||||||
@@ -4291,6 +4390,197 @@ class ApiKernel
|
|||||||
$this->respond(['ok' => true, 'deleted' => true]);
|
$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']);
|
||||||
|
$isDefault = !empty($this->in['is_default']);
|
||||||
|
|
||||||
|
$defaultOnly = $profileId > 0
|
||||||
|
&& $isDefault
|
||||||
|
&& $label === ''
|
||||||
|
&& $host === ''
|
||||||
|
&& $port === 0
|
||||||
|
&& $userName === ''
|
||||||
|
&& $pass === ''
|
||||||
|
&& $secure === ''
|
||||||
|
&& $fromEmail === ''
|
||||||
|
&& $fromName === ''
|
||||||
|
&& $replyTo === ''
|
||||||
|
&& !$passClear;
|
||||||
|
|
||||||
|
if ($defaultOnly) {
|
||||||
|
$this->ensureSmtpProfilesTableExists();
|
||||||
|
$table = $this->smtpProfilesTable();
|
||||||
|
$stmt = $this->pdo->prepare("UPDATE `$table` SET `is_default` = 0 WHERE `customer_id` = :cid AND `id` != :id");
|
||||||
|
$stmt->execute([':cid' => $customerId, ':id' => $profileId]);
|
||||||
|
$stmt = $this->pdo->prepare("UPDATE `$table` SET `is_default` = 1 WHERE `id` = :id AND `customer_id` = :cid");
|
||||||
|
$stmt->execute([':id' => $profileId, ':cid' => $customerId]);
|
||||||
|
$profile = $this->fetchSmtpProfileRow($customerId, $profileId);
|
||||||
|
$this->respond(['ok' => true, 'profile' => $profile]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
'is_default' => $isDefault ? 1 : 0,
|
||||||
|
];
|
||||||
|
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`,`is_default`,`created_at`,`updated_at`) VALUES (:cid,:label,:host,:port,:user,:pass,:secure,:fmail,:fname,:reply,:is_default,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,
|
||||||
|
':is_default' => $isDefault ? 1 : 0,
|
||||||
|
]);
|
||||||
|
$profileId = (int)$this->pdo->lastInsertId();
|
||||||
|
if ($isDefault) {
|
||||||
|
$stmt = $this->pdo->prepare("UPDATE `$table` SET `is_default` = 0 WHERE `customer_id` = :cid AND `id` != :id");
|
||||||
|
$stmt->execute([':cid' => $customerId, ':id' => $profileId]);
|
||||||
|
$stmt = $this->pdo->prepare("UPDATE `$table` SET `is_default` = 1 WHERE `id` = :id AND `customer_id` = :cid");
|
||||||
|
$stmt->execute([':id' => $profileId, ':cid' => $customerId]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($profileId > 0 && $isDefault) {
|
||||||
|
$stmt = $this->pdo->prepare("UPDATE `$table` SET `is_default` = 0 WHERE `customer_id` = :cid AND `id` != :id");
|
||||||
|
$stmt->execute([':cid' => $customerId, ':id' => $profileId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$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
|
private function handleDashboardMetrics(): void
|
||||||
{
|
{
|
||||||
$user = $this->requireAuth();
|
$user = $this->requireAuth();
|
||||||
@@ -5297,6 +5587,69 @@ SQL;
|
|||||||
return 'emailtemplate_sender_identities';
|
return 'emailtemplate_sender_identities';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function smtpProfilesTable(): string
|
||||||
|
{
|
||||||
|
return 'emailtemplate_smtp_profiles';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function ensureSmtpProfilesTableExists(): void
|
||||||
|
{
|
||||||
|
$table = $this->smtpProfilesTable();
|
||||||
|
if ($this->tableExists($table)) {
|
||||||
|
$this->ensureSmtpProfilesColumns($table);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
$sql = <<<SQL
|
||||||
|
CREATE TABLE IF NOT EXISTS `$table` (
|
||||||
|
`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,
|
||||||
|
`is_default` tinyint(1) DEFAULT 0,
|
||||||
|
`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
|
||||||
|
SQL;
|
||||||
|
$this->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 ensureSmtpProfilesColumns(string $table): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$columns = $this->tableColumns($table);
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
$this->fail('SMTP-Profil-Tabelle konnte nicht gelesen werden', $e->getMessage(), 500);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$missing = [];
|
||||||
|
if (!in_array('is_default', $columns, true)) {
|
||||||
|
$missing[] = 'ADD COLUMN `is_default` tinyint(1) DEFAULT 0';
|
||||||
|
}
|
||||||
|
if (!$missing) return;
|
||||||
|
try {
|
||||||
|
$sql = 'ALTER TABLE `' . $table . '` ' . implode(', ', $missing);
|
||||||
|
$this->pdo->exec($sql);
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
$this->fail('SMTP-Profil-Tabelle konnte nicht aktualisiert werden', $e->getMessage(), 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private function fetchSenderRow(int $customerId, int $senderId): array
|
private function fetchSenderRow(int $customerId, int $senderId): array
|
||||||
{
|
{
|
||||||
if ($customerId <= 0 || $senderId <= 0) {
|
if ($customerId <= 0 || $senderId <= 0) {
|
||||||
@@ -5323,6 +5676,63 @@ 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'] ?? '',
|
||||||
|
'is_default' => !empty($row['is_default']) ? 1 : 0,
|
||||||
|
'smtp_pass_set' => $pass !== '',
|
||||||
|
'created_at' => $row['created_at'] ?? null,
|
||||||
|
'updated_at' => $row['updated_at'] ?? null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getDefaultSmtpProfileId(int $customerId): int
|
||||||
|
{
|
||||||
|
if ($customerId <= 0) return 0;
|
||||||
|
$this->ensureSmtpProfilesTableExists();
|
||||||
|
$table = $this->smtpProfilesTable();
|
||||||
|
$stmt = $this->pdo->prepare("SELECT `id` FROM `$table` WHERE `customer_id` = :cid AND `is_default` = 1 ORDER BY `id` ASC LIMIT 1");
|
||||||
|
$stmt->execute([':cid' => $customerId]);
|
||||||
|
return (int)($stmt->fetchColumn() ?: 0);
|
||||||
|
}
|
||||||
|
|
||||||
private function formatUserOutput(array $row): array
|
private function formatUserOutput(array $row): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
@@ -5425,20 +5835,8 @@ SQL;
|
|||||||
|
|
||||||
private function writeDebugLog(string $name, array $payload): void
|
private function writeDebugLog(string $name, array $payload): void
|
||||||
{
|
{
|
||||||
$env = strtolower((string)($this->conf['env'] ?? ''));
|
if (!$this->isStagingEnv()) {
|
||||||
if ($env !== 'staging' && (!defined('APP_ENV') || strtolower((string)APP_ENV) !== 'staging')) {
|
return;
|
||||||
$host = '';
|
|
||||||
if (!empty($_SERVER['HTTP_X_FORWARDED_HOST'])) {
|
|
||||||
$host = strtolower(trim(explode(',', (string)$_SERVER['HTTP_X_FORWARDED_HOST'])[0]));
|
|
||||||
} elseif (!empty($_SERVER['HTTP_HOST'])) {
|
|
||||||
$host = strtolower((string)$_SERVER['HTTP_HOST']);
|
|
||||||
}
|
|
||||||
if ($host !== '') {
|
|
||||||
$host = preg_replace('/:\\d+$/', '', $host);
|
|
||||||
}
|
|
||||||
if ($host !== 'staging.emailtemplate.it') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
$dir = $this->debugDir();
|
$dir = $this->debugDir();
|
||||||
debug_log_write($name, $payload, [
|
debug_log_write($name, $payload, [
|
||||||
@@ -5449,6 +5847,23 @@ SQL;
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function isStagingEnv(): bool
|
||||||
|
{
|
||||||
|
$env = strtolower((string)($this->conf['env'] ?? ''));
|
||||||
|
if ($env === 'staging') return true;
|
||||||
|
if (defined('APP_ENV') && strtolower((string)APP_ENV) === 'staging') return true;
|
||||||
|
$host = '';
|
||||||
|
if (!empty($_SERVER['HTTP_X_FORWARDED_HOST'])) {
|
||||||
|
$host = strtolower(trim(explode(',', (string)$_SERVER['HTTP_X_FORWARDED_HOST'])[0]));
|
||||||
|
} elseif (!empty($_SERVER['HTTP_HOST'])) {
|
||||||
|
$host = strtolower((string)$_SERVER['HTTP_HOST']);
|
||||||
|
}
|
||||||
|
if ($host !== '') {
|
||||||
|
$host = preg_replace('/:\\d+$/', '', $host);
|
||||||
|
}
|
||||||
|
return $host === 'staging.emailtemplate.it';
|
||||||
|
}
|
||||||
|
|
||||||
private function defaultApiBase(): string
|
private function defaultApiBase(): string
|
||||||
{
|
{
|
||||||
$base = $this->conf['base_url'] ?? '';
|
$base = $this->conf['base_url'] ?? '';
|
||||||
|
|||||||
Reference in New Issue
Block a user