mail send
This commit is contained in:
@@ -18,6 +18,7 @@ export function initEditor() {
|
||||
const sendForm = document.getElementById('sendTestForm');
|
||||
const sendTo = document.getElementById('send_to');
|
||||
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 prevFrame = document.getElementById('previewFrame');
|
||||
@@ -36,6 +37,27 @@ export function initEditor() {
|
||||
return (b?.dataset?.tab) || (current?.resource) || 'templates';
|
||||
}
|
||||
|
||||
function setSendContext(id, name = '') {
|
||||
if (sendDlg) {
|
||||
if (id) {
|
||||
sendDlg.dataset.templateId = String(id);
|
||||
} else {
|
||||
delete sendDlg.dataset.templateId;
|
||||
}
|
||||
if (name) sendDlg.dataset.templateName = name;
|
||||
}
|
||||
if (sendInfo) {
|
||||
if (!id) {
|
||||
sendInfo.textContent = 'Kein Template ausgewählt.';
|
||||
sendInfo.classList.add('text-rose-600');
|
||||
} else {
|
||||
const label = name ? `${name} – Template #${id}` : `Template #${id}`;
|
||||
sendInfo.textContent = label;
|
||||
sendInfo.classList.remove('text-rose-600');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function writeHtmlToFrame(html) {
|
||||
iframe.srcdoc = `<!doctype html><html>
|
||||
<head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"></head>
|
||||
@@ -246,6 +268,8 @@ export function initEditor() {
|
||||
// globaler Kontext
|
||||
window.__currentItemId = current.id;
|
||||
window.__currentEditorCtx = { id: current.id, mode: current.resource };
|
||||
setSendContext(current.id, current.name);
|
||||
if (btnTest) btnTest.classList.toggle('hidden', current.resource !== 'templates');
|
||||
|
||||
// Neuen Token erzeugen & alten Listener entfernen
|
||||
reqToken++;
|
||||
@@ -388,24 +412,32 @@ export function initEditor() {
|
||||
prevDlg?.showModal?.();
|
||||
}
|
||||
|
||||
async function openSend() {
|
||||
sendSubject.value = 'Testversand';
|
||||
sendTo.value = '';
|
||||
async function openSend(ctx = null) {
|
||||
const ctxId = ctx?.id ? Number(ctx.id) : (window.__currentItemId || current?.id || 0);
|
||||
if (!ctxId) {
|
||||
err('Kein Template geladen');
|
||||
return;
|
||||
}
|
||||
window.__currentItemId = ctxId;
|
||||
const ctxName = ctx?.name ?? sendDlg?.dataset?.templateName ?? current?.name ?? '';
|
||||
setSendContext(ctxId, ctxName);
|
||||
if (sendSubject) sendSubject.value = ctx?.subject || 'Testversand';
|
||||
if (sendTo) sendTo.value = ctx?.to || '';
|
||||
sendDlg?.showModal?.();
|
||||
}
|
||||
function closeSend(){ sendDlg?.close?.(); }
|
||||
|
||||
async function doSend(ev){
|
||||
ev?.preventDefault?.();
|
||||
const to = sendTo.value.trim();
|
||||
if(!to){ toast("Bitte Empfänger angeben", false); return; }
|
||||
const to = (sendTo?.value || '').trim();
|
||||
if(!to){ err('Bitte Empfänger angeben'); return; }
|
||||
const win = iframe?.contentWindow;
|
||||
const ctx = (win && win.__currentEditorCtx) || {};
|
||||
const id = (window.__currentItemId || ctx?.id || 0);
|
||||
if(!id){ toast("Kein Template geladen", false); return; }
|
||||
// Hier wird der gespeicherte HTML-Code verwendet, nicht der Live-HTML, da apiAction
|
||||
// keine Live-Daten erwartet. Es geht um template_id.
|
||||
const r = await apiAction('templates.test_send', { method:'POST', data:{ template_id: id, to, subject: sendSubject.value || 'Testversand' } });
|
||||
const r = await apiAction('templates.test_send', { method:'POST', data:{ template_id: id, to, subject: (sendSubject?.value || 'Testversand') } });
|
||||
if(r?.ok){ toast("Testversand ausgelöst"); closeSend(); } else { toast("Senden fehlgeschlagen", false); }
|
||||
}
|
||||
function closePreview(){ prevDlg?.close?.(); }
|
||||
@@ -440,6 +472,18 @@ export function initEditor() {
|
||||
btnCancelSend&& (btnCancelSend.onclick= closeSend);
|
||||
sendForm && (sendForm.onsubmit = doSend);
|
||||
|
||||
window.AdminTestSend = window.AdminTestSend || {};
|
||||
window.AdminTestSend.open = (opts = {}) => {
|
||||
const targetId = Number(opts.id || window.__currentItemId || 0);
|
||||
if (!targetId) {
|
||||
err('Testversand: Keine ID vorhanden');
|
||||
return;
|
||||
}
|
||||
window.__currentItemId = targetId;
|
||||
setSendContext(targetId, opts.name || '');
|
||||
openSend({ id: targetId, name: opts.name || '' });
|
||||
};
|
||||
|
||||
// Public API
|
||||
window.EditorUI = { open, save, close, clear: clearEditor, preview: openPreview };
|
||||
}
|
||||
|
||||
@@ -97,6 +97,9 @@ export async function loadList(resource){
|
||||
const editBtn = (resource==='snippets')
|
||||
? `<button class='btn' data-edit='snippets:${item.id}'>Bearbeiten</button>` : '';
|
||||
|
||||
const testBtn = resource==='templates'
|
||||
? `<button class='btn' data-test='${item.id}' data-name='${name}'>Testversand</button>` : '';
|
||||
|
||||
const prevBtn = `<button class='btn' data-preview='${resource}:${item.id}'>Vorschau</button>`;
|
||||
const delBtn = `<button class='btn btn-danger' data-del='${resource}:${item.id}' data-name='${name}'>Löschen</button>`;
|
||||
|
||||
@@ -104,7 +107,7 @@ export async function loadList(resource){
|
||||
<div class='min-w-48 font-medium truncate' title="${name}">${name || '(ohne Name)'}</div>
|
||||
<div class='text-xs text-gray-500'>#${item.id}</div>
|
||||
<div class='text-xs'>${parentBadge(resource,item)}</div>
|
||||
<div class='ms-auto flex gap-2'>${[openBtn, editBtn, prevBtn, delBtn].filter(Boolean).join('')}</div>
|
||||
<div class='ms-auto flex gap-2'>${[openBtn, editBtn, testBtn, prevBtn, delBtn].filter(Boolean).join('')}</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
|
||||
@@ -149,6 +152,21 @@ export async function loadList(resource){
|
||||
prevDlg.showModal();
|
||||
}));
|
||||
|
||||
// test send (templates only)
|
||||
list.querySelectorAll('[data-test]').forEach(b=>b.addEventListener('click', ()=>{
|
||||
const id = Number(b.dataset.test || '0');
|
||||
const nm = b.dataset.name || '';
|
||||
if (!id) {
|
||||
toast('Testversand: Ungültige ID', false);
|
||||
return;
|
||||
}
|
||||
if (window.AdminTestSend && typeof window.AdminTestSend.open === 'function') {
|
||||
window.AdminTestSend.open({ id, name: nm });
|
||||
} else {
|
||||
toast('Testversand ist aktuell nicht verfügbar.', false);
|
||||
}
|
||||
}));
|
||||
|
||||
// delete
|
||||
const delDlg=document.getElementById('deleteDialog'),
|
||||
delText=document.getElementById('deleteText'),
|
||||
|
||||
@@ -91,6 +91,8 @@ $assetVersion = defined('ASSET_VERSION') ? ASSET_VERSION : time();
|
||||
<div class="px-4 py-2 border-b flex items-center gap-2 bg-white/80 backdrop-blur">
|
||||
<strong class="me-auto">E-Mail Editor</strong>
|
||||
<button id="btn-clear-main" type="button" class="btn" title="Leeren">🧹</button>
|
||||
<button id="btn-preview" type="button" class="btn">Vorschau</button>
|
||||
<button id="btn-test" type="button" class="btn">Testversand</button>
|
||||
<button id="btn-save" type="button" class="btn">Speichern</button>
|
||||
<button id="btn-close" type="button" class="btn">Schließen</button>
|
||||
</div>
|
||||
@@ -109,6 +111,26 @@ $assetVersion = defined('ASSET_VERSION') ? ASSET_VERSION : time();
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<!-- Test Send Dialog -->
|
||||
<dialog id="sendTestDialog" class="rounded-2xl p-0 w-[480px]">
|
||||
<form id="sendTestForm" method="dialog" class="p-4 bg-white rounded-2xl space-y-4">
|
||||
<h3 class="text-lg font-semibold">Testversand</h3>
|
||||
<p id="send_template_info" class="text-sm text-slate-600">Kein Template ausgewählt.</p>
|
||||
<label class="block">
|
||||
<span class="text-sm text-slate-600">Empfänger (E-Mail)</span>
|
||||
<input id="send_to" type="email" required class="mt-1 w-full border rounded-lg px-3 py-2" placeholder="name@example.com" />
|
||||
</label>
|
||||
<label class="block">
|
||||
<span class="text-sm text-slate-600">Betreff</span>
|
||||
<input id="send_subject" type="text" class="mt-1 w-full border rounded-lg px-3 py-2" value="Testversand" />
|
||||
</label>
|
||||
<div class="flex justify-end gap-2">
|
||||
<button type="button" id="btn-cancel-send" class="btn">Abbrechen</button>
|
||||
<button type="submit" id="btn-send-now" class="btn">Senden</button>
|
||||
</div>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<!-- Edit Snippet Dialog -->
|
||||
<dialog id="editSnippetDialog" class="rounded-2xl p-0 w-[700px]">
|
||||
<form id="editSnippetForm" method="dialog" class="p-4 bg-white rounded-2xl">
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
use TijsVerkoyen\CssToInlineStyles\CssToInlineStyles;
|
||||
|
||||
// 💡 NEUE KORREKTUR: Starte Output Buffering so früh wie möglich, um Whitespace/Errors
|
||||
// von inkludierten Dateien (AuthService.php, config.php) abzufangen.
|
||||
ob_start();
|
||||
@@ -617,6 +619,118 @@ class ApiKernel
|
||||
$this->respond(['ok' => true, 'kind' => $kind, 'id' => $id, 'deleted' => true]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sendet einen Testversand für Templates.
|
||||
*/
|
||||
private function handleTemplateTestSend(): void
|
||||
{
|
||||
$auth = $this->requireAuth();
|
||||
$templateId = (int)$this->val($this->in, ['template_id', 'id', 'template'], 0);
|
||||
if ($templateId <= 0) {
|
||||
$this->fail('template_id required', null, 422);
|
||||
}
|
||||
|
||||
$recipient = trim((string)$this->val($this->in, ['to', 'email', 'recipient'], ''));
|
||||
if ($recipient === '' || !filter_var($recipient, FILTER_VALIDATE_EMAIL)) {
|
||||
$this->fail('Valid recipient required', null, 422);
|
||||
}
|
||||
|
||||
$subject = trim((string)$this->val($this->in, ['subject'], 'Testversand'));
|
||||
if ($subject === '') {
|
||||
$subject = 'Testversand';
|
||||
}
|
||||
|
||||
$t = $this->tableMap['templates'];
|
||||
[$idCol, $allCols] = $this->resolveIdCol('templates');
|
||||
[$tw, $tp] = $this->tenantWhere($auth);
|
||||
|
||||
$sql = "SELECT * FROM `$t` WHERE `$idCol` = :id" . $tw . " LIMIT 1";
|
||||
$stmt = $this->pdo->prepare($sql);
|
||||
$stmt->bindValue(':id', $templateId);
|
||||
foreach ($tp as $k => $v) $stmt->bindValue($k, $v);
|
||||
$stmt->execute();
|
||||
$row = $stmt->fetch();
|
||||
if (!$row) {
|
||||
$this->fail('Template not found', ['id' => $templateId], 404);
|
||||
}
|
||||
|
||||
$htmlCol = $this->firstExisting($allCols, ['html', 'body', 'markup', 'content']);
|
||||
$html = ($htmlCol && isset($row[$htmlCol])) ? (string)$row[$htmlCol] : '';
|
||||
if ($html === '' && !empty($row['json_content'])) {
|
||||
$html = '<p>(Dieses Template enthält noch keine HTML-Inhalte.)</p>';
|
||||
}
|
||||
$html = $this->prepareEmailHtml($html);
|
||||
|
||||
if (!$this->dispatchTestMail($recipient, $subject, $html)) {
|
||||
$this->fail('Send failed', null, 500);
|
||||
}
|
||||
|
||||
$this->respond([
|
||||
'ok' => true,
|
||||
'template_id' => $templateId,
|
||||
'to' => $recipient,
|
||||
'subject' => $subject,
|
||||
]);
|
||||
}
|
||||
|
||||
private function prepareEmailHtml(string $html): string
|
||||
{
|
||||
$html = trim($html);
|
||||
if ($html === '') {
|
||||
return '<p>(Kein Inhalt vorhanden)</p>';
|
||||
}
|
||||
|
||||
if (!class_exists(CssToInlineStyles::class)) {
|
||||
return $html;
|
||||
}
|
||||
|
||||
try {
|
||||
$inliner = new CssToInlineStyles();
|
||||
return $inliner->convert($html);
|
||||
} catch (Throwable $e) {
|
||||
return $html;
|
||||
}
|
||||
}
|
||||
|
||||
private function dispatchTestMail(string $to, string $subject, string $html): bool
|
||||
{
|
||||
if (!function_exists('mail')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$smtp = $this->conf['smtp'] ?? [];
|
||||
$fromEmail = $smtp['from_email'] ?? 'no-reply@example.com';
|
||||
$fromName = $smtp['from_name'] ?? 'EmailTemplate';
|
||||
$headers = [
|
||||
'MIME-Version: 1.0',
|
||||
'Content-Type: text/html; charset=UTF-8',
|
||||
'From: ' . $this->formatEmailAddress($fromEmail, $fromName),
|
||||
];
|
||||
|
||||
$encodedSubject = function_exists('mb_encode_mimeheader')
|
||||
? mb_encode_mimeheader($subject, 'UTF-8')
|
||||
: $subject;
|
||||
return @mail($to, $encodedSubject, $html, implode("\r\n", $headers));
|
||||
}
|
||||
|
||||
private function formatEmailAddress(string $email, string $name): string
|
||||
{
|
||||
$email = trim($email);
|
||||
if ($email === '') {
|
||||
return 'no-reply@example.com';
|
||||
}
|
||||
|
||||
$name = trim($name);
|
||||
if ($name === '') {
|
||||
return $email;
|
||||
}
|
||||
|
||||
$encoded = function_exists('mb_encode_mimeheader')
|
||||
? mb_encode_mimeheader($name, 'UTF-8')
|
||||
: $name;
|
||||
return sprintf('%s <%s>', $encoded, $email);
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
// 💡 Öffentliche run()-Methode (KORRIGIERT)
|
||||
// =================================================================
|
||||
@@ -649,6 +763,9 @@ class ApiKernel
|
||||
$this->authService->logout();
|
||||
$this->respond(['ok' => true]);
|
||||
break;
|
||||
case 'templates.test_send':
|
||||
$this->handleTemplateTestSend();
|
||||
break;
|
||||
|
||||
/* ---------- CRUD HANDLER ---------- */
|
||||
default:
|
||||
|
||||
Reference in New Issue
Block a user