diff --git a/composer.json b/composer.json index 82f3e80..60cee43 100755 --- a/composer.json +++ b/composer.json @@ -1,6 +1,7 @@ { "name": "ssh-w020abd8/staging", "require": { + "phpmailer/phpmailer": "^6.9", "tijsverkoyen/css-to-inline-styles": "^2.3" } } diff --git a/partials/landingpage/accountsetup/settings.php b/partials/landingpage/accountsetup/settings.php index e661357..bd8d6ba 100755 --- a/partials/landingpage/accountsetup/settings.php +++ b/partials/landingpage/accountsetup/settings.php @@ -68,6 +68,43 @@ require dirname(__DIR__) . '/../structure/layout_start.php'; +
+ SMTP-Versand +

Diese Zugangsdaten werden fuer den Testversand genutzt, wenn SMTP aktiviert ist.

+ +
+ + + + + + + + +
+
diff --git a/public/assets/js/ui-user.js b/public/assets/js/ui-user.js index 9d790b1..63d9b62 100755 --- a/public/assets/js/ui-user.js +++ b/public/assets/js/ui-user.js @@ -448,6 +448,15 @@ function fillSettingsForm(settings) { : 0; 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 = settings.smtp_pass || ''; + 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 || []); } @@ -501,6 +510,18 @@ async function submitSettingsForm(ev) { const parsed = raw === '' ? 0 : Number(raw); 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_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) { const bridgeTables = normalizeTableList(state.settings.bridge_tables || []); data.bridge_tables = bridgeTables; diff --git a/schema.sql b/schema.sql index f54d64d..8dc95a0 100755 --- a/schema.sql +++ b/schema.sql @@ -110,6 +110,15 @@ CREATE TABLE IF NOT EXISTS `emailtemplate_customer_settings` ( `sender_token` varchar(255) DEFAULT NULL, `external_api_token` varchar(255) DEFAULT NULL, `editor_default` varchar(32) DEFAULT NULL, + `smtp_enabled` tinyint(1) DEFAULT 0, + `smtp_host` varchar(255) DEFAULT 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, + `smtp_from_email` varchar(255) DEFAULT NULL, + `smtp_from_name` varchar(255) DEFAULT NULL, + `smtp_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 (`customer_id`) diff --git a/src/ApiKernel.php b/src/ApiKernel.php index 32a716f..ba8844f 100755 --- a/src/ApiKernel.php +++ b/src/ApiKernel.php @@ -2,6 +2,8 @@ declare(strict_types=1); use TijsVerkoyen\CssToInlineStyles\CssToInlineStyles; +use PHPMailer\PHPMailer\PHPMailer; +use PHPMailer\PHPMailer\Exception as PHPMailerException; // 💡 NEUE KORREKTUR: Starte Output Buffering so früh wie möglich, um Whitespace/Errors // von inkludierten Dateien (AuthService.php, config.php) abzufangen. @@ -25,6 +27,7 @@ class ApiKernel private array $tableMap; private AuthService $authService; private array $tableExistsCache = []; + private ?string $lastMailError = null; // --- Initialisierung & Konstruktor (Optimiert) --- @@ -2289,7 +2292,7 @@ class ApiKernel } } - if (!$this->dispatchTestMail($recipient, $subject, $html, $sender)) { + if (!$this->dispatchTestMail($recipient, $subject, $html, $sender, $customerId)) { $this->writeDebugLog('templates_test_send', [ 'time' => date(DATE_ATOM), 'template_id' => $templateId, @@ -2299,6 +2302,7 @@ class ApiKernel '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), + 'mail_error' => $this->lastMailError, ]); $this->fail('Send failed', null, 500); } @@ -2867,16 +2871,80 @@ class ApiKernel return $map ? strtr($html, $map) : $html; } - private function dispatchTestMail(string $to, string $subject, string $html, ?array $sender = null): bool + private function dispatchTestMail(string $to, string $subject, string $html, ?array $sender = null, ?int $customerId = null): bool { + $this->lastMailError = null; + $smtpConf = $this->conf['smtp'] ?? []; + $settings = ($customerId && $customerId > 0) ? $this->getCustomerSettings($customerId) : []; + $smtp = array_merge($smtpConf, array_filter([ + 'host' => $settings['smtp_host'] ?? null, + 'port' => $settings['smtp_port'] ?? null, + 'user' => $settings['smtp_user'] ?? null, + 'pass' => $settings['smtp_pass'] ?? null, + 'secure' => $settings['smtp_secure'] ?? null, + 'from_email' => $settings['smtp_from_email'] ?? null, + 'from_name' => $settings['smtp_from_name'] ?? null, + 'reply_to' => $settings['smtp_reply_to'] ?? null, + 'enabled' => $settings['smtp_enabled'] ?? null, + ], static fn($v) => $v !== null && $v !== '')); + + $smtpEnabled = !empty($smtp['enabled']); + $smtpHost = trim((string)($smtp['host'] ?? '')); + $smtpUser = trim((string)($smtp['user'] ?? '')); + $smtpPass = (string)($smtp['pass'] ?? ''); + $smtpSecure = strtolower(trim((string)($smtp['secure'] ?? ''))); + $smtpPort = (int)($smtp['port'] ?? 0); + + $fromEmail = $sender['from_email'] ?? ($smtp['from_email'] ?? ($smtpConf['from_email'] ?? 'no-reply@example.com')); + $fromName = $sender['from_name'] ?? ($sender['label'] ?? ($smtp['from_name'] ?? ($smtpConf['from_name'] ?? 'EmailTemplate'))); + $replyTo = $sender['reply_to'] ?? ($smtp['reply_to'] ?? ''); + + if ($smtpEnabled && $smtpHost !== '' && class_exists(PHPMailer::class)) { + try { + $mailer = new PHPMailer(true); + $mailer->CharSet = 'UTF-8'; + $mailer->isSMTP(); + $mailer->Host = $smtpHost; + $mailer->SMTPAuth = ($smtpUser !== '' || $smtpPass !== ''); + if ($smtpUser !== '') $mailer->Username = $smtpUser; + if ($smtpPass !== '') $mailer->Password = $smtpPass; + if ($smtpSecure === 'ssl') { + $mailer->SMTPSecure = PHPMailer::ENCRYPTION_SMTPS; + if ($smtpPort <= 0) $smtpPort = 465; + } elseif ($smtpSecure === 'tls') { + $mailer->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS; + if ($smtpPort <= 0) $smtpPort = 587; + } else { + $mailer->SMTPSecure = ''; + $mailer->SMTPAutoTLS = false; + if ($smtpPort <= 0) $smtpPort = 25; + } + if ($smtpPort > 0) $mailer->Port = $smtpPort; + + $mailer->setFrom($fromEmail, $fromName); + $mailer->addAddress($to); + if ($replyTo !== '') { + $mailer->addReplyTo($replyTo, $fromName ?: $fromEmail); + } + $mailer->isHTML(true); + $mailer->Subject = $subject; + $mailer->Body = $html; + $mailer->AltBody = trim(strip_tags($html)); + return $mailer->send(); + } catch (PHPMailerException $e) { + $this->lastMailError = $e->getMessage(); + return false; + } catch (Throwable $e) { + $this->lastMailError = $e->getMessage(); + return false; + } + } + if (!function_exists('mail')) { + $this->lastMailError = 'PHP mail() not available'; return false; } - $smtp = $this->conf['smtp'] ?? []; - $fromEmail = $sender['from_email'] ?? ($smtp['from_email'] ?? 'no-reply@example.com'); - $fromName = $sender['from_name'] ?? ($sender['label'] ?? ($smtp['from_name'] ?? 'EmailTemplate')); - $replyTo = $sender['reply_to'] ?? ''; $headers = [ 'MIME-Version: 1.0', 'Content-Type: text/html; charset=UTF-8', @@ -2889,7 +2957,11 @@ class ApiKernel $encodedSubject = function_exists('mb_encode_mimeheader') ? mb_encode_mimeheader($subject, 'UTF-8') : $subject; - return @mail($to, $encodedSubject, $html, implode("\r\n", $headers)); + $sent = @mail($to, $encodedSubject, $html, implode("\r\n", $headers)); + if (!$sent) { + $this->lastMailError = 'mail() returned false'; + } + return $sent; } private function formatEmailAddress(string $email, string $name): string @@ -3757,11 +3829,22 @@ class ApiKernel $hasVersionsRetention = array_key_exists('versions_retention', $this->in); $hasListSort = array_key_exists('list_sort', $this->in); $hasBridgeTables = array_key_exists('bridge_tables', $this->in); + $hasSmtpEnabled = array_key_exists('smtp_enabled', $this->in); + $hasSmtpHost = array_key_exists('smtp_host', $this->in); + $hasSmtpPort = array_key_exists('smtp_port', $this->in); + $hasSmtpUser = array_key_exists('smtp_user', $this->in); + $hasSmtpPass = array_key_exists('smtp_pass', $this->in); + $hasSmtpSecure = array_key_exists('smtp_secure', $this->in); + $hasSmtpFromEmail = array_key_exists('smtp_from_email', $this->in); + $hasSmtpFromName = array_key_exists('smtp_from_name', $this->in); + $hasSmtpReplyTo = array_key_exists('smtp_reply_to', $this->in); $rotateBridge = !empty($this->in['rotate_bridge_token']); $rotateSender = !empty($this->in['rotate_sender_token']); $rotateExternal = !empty($this->in['rotate_external_token']); $onlyListSort = $hasListSort && !$hasBridgeUrl && !$hasBridgeToken && !$hasSenderToken && !$hasExternalToken - && !$hasEditorDefault && !$hasBridgeTables && !$hasVersionsRetention && !$rotateBridge && !$rotateSender && !$rotateExternal; + && !$hasEditorDefault && !$hasBridgeTables && !$hasVersionsRetention && !$rotateBridge && !$rotateSender && !$rotateExternal + && !$hasSmtpEnabled && !$hasSmtpHost && !$hasSmtpPort && !$hasSmtpUser && !$hasSmtpPass && !$hasSmtpSecure + && !$hasSmtpFromEmail && !$hasSmtpFromName && !$hasSmtpReplyTo; if (!$onlyListSort) { $this->ensureRole($user, ['owner', 'admin']); @@ -3777,6 +3860,15 @@ class ApiKernel $versionsRetention = $hasVersionsRetention ? (int)($this->in['versions_retention'] ?? 0) : (int)($settings['versions_retention'] ?? 0); $listSort = $hasListSort ? strtolower(trim((string)($this->in['list_sort'] ?? ''))) : ''; $bridgeTables = $hasBridgeTables ? $this->normalizeBridgeTables($this->in['bridge_tables'] ?? []) : ($settings['bridge_tables'] ?? []); + $smtpEnabled = $hasSmtpEnabled ? (int)($this->in['smtp_enabled'] ?? 0) : (int)($settings['smtp_enabled'] ?? 0); + $smtpHost = $hasSmtpHost ? trim((string)($this->in['smtp_host'] ?? '')) : (string)($settings['smtp_host'] ?? ''); + $smtpPort = $hasSmtpPort ? (int)($this->in['smtp_port'] ?? 0) : (int)($settings['smtp_port'] ?? 0); + $smtpUser = $hasSmtpUser ? trim((string)($this->in['smtp_user'] ?? '')) : (string)($settings['smtp_user'] ?? ''); + $smtpPass = $hasSmtpPass ? (string)($this->in['smtp_pass'] ?? '') : (string)($settings['smtp_pass'] ?? ''); + $smtpSecure = $hasSmtpSecure ? strtolower(trim((string)($this->in['smtp_secure'] ?? ''))) : strtolower((string)($settings['smtp_secure'] ?? '')); + $smtpFromEmail = $hasSmtpFromEmail ? trim((string)($this->in['smtp_from_email'] ?? '')) : (string)($settings['smtp_from_email'] ?? ''); + $smtpFromName = $hasSmtpFromName ? trim((string)($this->in['smtp_from_name'] ?? '')) : (string)($settings['smtp_from_name'] ?? ''); + $smtpReplyTo = $hasSmtpReplyTo ? trim((string)($this->in['smtp_reply_to'] ?? '')) : (string)($settings['smtp_reply_to'] ?? ''); if ($bridgeUrl && !filter_var($bridgeUrl, FILTER_VALIDATE_URL)) { $this->fail('Ungültige Bridge-URL', null, 422); @@ -3786,6 +3878,24 @@ class ApiKernel $this->fail('Ungültige Sortierung', null, 422); } + if ($smtpEnabled) { + if ($smtpHost === '') { + $this->fail('SMTP-Host erforderlich', null, 422); + } + if ($smtpPort < 0 || $smtpPort > 65535) { + $this->fail('Ungültiger SMTP-Port', null, 422); + } + if ($smtpSecure !== '' && !in_array($smtpSecure, ['tls', 'ssl', 'none'], true)) { + $this->fail('Ungültige SMTP-Sicherheit', null, 422); + } + } + if ($smtpFromEmail !== '' && !filter_var($smtpFromEmail, FILTER_VALIDATE_EMAIL)) { + $this->fail('Ungültige SMTP-Absenderadresse', null, 422); + } + if ($smtpReplyTo !== '' && !filter_var($smtpReplyTo, FILTER_VALIDATE_EMAIL)) { + $this->fail('Ungültige SMTP-Reply-To-Adresse', null, 422); + } + if (!$onlyListSort) { if ($rotateBridge || $bridgeToken === '') $bridgeToken = $this->generateToken(); if ($rotateSender || $senderToken === '') $senderToken = $this->generateToken(); @@ -3797,6 +3907,9 @@ class ApiKernel if ($versionsRetention < 0) { $this->fail('Ungültiger Aufbewahrungswert', null, 422); } + if ($smtpSecure === 'none') { + $smtpSecure = ''; + } $settings = $this->saveCustomerSettings($customerId, [ 'bridge_url' => $bridgeUrl, @@ -3806,6 +3919,15 @@ class ApiKernel 'editor_default' => $editorDefault ?: null, 'bridge_tables' => $bridgeTables, 'versions_retention' => $versionsRetention, + 'smtp_enabled' => $smtpEnabled ? 1 : 0, + 'smtp_host' => $smtpHost ?: null, + 'smtp_port' => $smtpPort > 0 ? $smtpPort : null, + 'smtp_user' => $smtpUser ?: null, + 'smtp_pass' => $smtpPass !== '' ? $smtpPass : null, + 'smtp_secure' => $smtpSecure ?: null, + 'smtp_from_email' => $smtpFromEmail ?: null, + 'smtp_from_name' => $smtpFromName ?: null, + 'smtp_reply_to' => $smtpReplyTo ?: null, ]); } else { $settings = $customerId ? $this->ensureSettingsTokens($customerId, $settings) : $settings; @@ -4367,7 +4489,25 @@ class ApiKernel { if ($customerId <= 0) return []; $this->ensureCustomerSettingsTableExists(); - $allowed = ['bridge_url', 'bridge_token', 'sender_token', 'external_api_token', 'editor_default', 'bridge_tables', 'bridge_setup', 'versions_retention']; + $allowed = [ + 'bridge_url', + 'bridge_token', + 'sender_token', + 'external_api_token', + 'editor_default', + 'bridge_tables', + 'bridge_setup', + 'versions_retention', + 'smtp_enabled', + 'smtp_host', + 'smtp_port', + 'smtp_user', + 'smtp_pass', + 'smtp_secure', + 'smtp_from_email', + 'smtp_from_name', + 'smtp_reply_to', + ]; $fields = array_intersect_key($data, array_flip($allowed)); if (!$fields) return $this->getCustomerSettings($customerId); if (array_key_exists('versions_retention', $fields)) { @@ -4450,6 +4590,16 @@ class ApiKernel } else { $row['versions_retention'] = max(0, (int)$row['versions_retention']); } + if (!isset($row['smtp_enabled'])) { + $row['smtp_enabled'] = 0; + } else { + $row['smtp_enabled'] = (int)$row['smtp_enabled'] ? 1 : 0; + } + if (isset($row['smtp_port'])) { + $row['smtp_port'] = (int)$row['smtp_port']; + } else { + $row['smtp_port'] = 0; + } return $row; } @@ -4727,6 +4877,15 @@ CREATE TABLE IF NOT EXISTS `$table` ( `editor_default` varchar(32) DEFAULT NULL, `bridge_tables` text DEFAULT NULL, `versions_retention` int(10) unsigned DEFAULT 0, + `smtp_enabled` tinyint(1) DEFAULT 0, + `smtp_host` varchar(255) DEFAULT 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, + `smtp_from_email` varchar(255) DEFAULT NULL, + `smtp_from_name` varchar(255) DEFAULT NULL, + `smtp_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 (`customer_id`) @@ -4770,6 +4929,33 @@ SQL; if (!in_array('versions_retention', $columns, true)) { $missing[] = 'ADD COLUMN `versions_retention` int(10) unsigned DEFAULT 0'; } + if (!in_array('smtp_enabled', $columns, true)) { + $missing[] = 'ADD COLUMN `smtp_enabled` tinyint(1) DEFAULT 0'; + } + if (!in_array('smtp_host', $columns, true)) { + $missing[] = 'ADD COLUMN `smtp_host` varchar(255) DEFAULT NULL'; + } + if (!in_array('smtp_port', $columns, true)) { + $missing[] = 'ADD COLUMN `smtp_port` int(10) unsigned DEFAULT NULL'; + } + if (!in_array('smtp_user', $columns, true)) { + $missing[] = 'ADD COLUMN `smtp_user` varchar(255) DEFAULT NULL'; + } + if (!in_array('smtp_pass', $columns, true)) { + $missing[] = 'ADD COLUMN `smtp_pass` varchar(255) DEFAULT NULL'; + } + if (!in_array('smtp_secure', $columns, true)) { + $missing[] = 'ADD COLUMN `smtp_secure` varchar(16) DEFAULT NULL'; + } + if (!in_array('smtp_from_email', $columns, true)) { + $missing[] = 'ADD COLUMN `smtp_from_email` varchar(255) DEFAULT NULL'; + } + if (!in_array('smtp_from_name', $columns, true)) { + $missing[] = 'ADD COLUMN `smtp_from_name` varchar(255) DEFAULT NULL'; + } + if (!in_array('smtp_reply_to', $columns, true)) { + $missing[] = 'ADD COLUMN `smtp_reply_to` varchar(255) DEFAULT NULL'; + } if (!$missing) { return;