Files
deskhub/DeskHub-Webseite/includes/mailer.php
T
2026-10-28 07:54:38 +00:00

505 lines
25 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* Mailer mit zwei Versandwegen:
* 1) Eigener SMTP-Client (includes/smtp_mailer.php), sobald unter Einstellungen
* → E-Mail-Versand ein SMTP-Server hinterlegt ist – funktioniert unabhängig
* von der Hosting-Konfiguration und ohne externe Bibliothek.
* 2) Fallback auf die einfache PHP-Funktion mail() (config.php → mail.enabled),
* falls kein SMTP-Server hinterlegt ist.
* Der Rest der Anwendung ruft ausschließlich send_mail() auf.
*/
function smtp_last_error(): string
{
static $lastError = '';
return func_num_args() > 0 ? ($lastError = func_get_arg(0)) : $lastError;
}
/**
* Sendet eine Mail. Scheitert der Versand, obwohl ein Versandweg eingerichtet ist, kommt die Mail in die Warteschlange
* (includes/mail_queue.php) und wird später erneut versucht; der Rückgabewert bleibt dann false, mail_last_queued() ist true.
* $queue = false für Test-Mails und für die Wiederholungsläufe der Warteschlange selbst.
*/
function send_mail(string $to, string $subject, string $htmlBody, ?string $fromOverride = null, bool $queue = true): bool
{
mail_attempted(false);
mail_last_queued(false);
$ok = send_mail_now($to, $subject, $htmlBody, $fromOverride);
if (!$ok && $queue && mail_attempted() && mail_queue_enabled()) {
try {
if (mail_queue_add(get_db(), $to, $subject, $htmlBody, $fromOverride, smtp_last_error()) !== null) mail_last_queued(true);
} catch (Throwable $e) {
error_log('send_mail queue: ' . $e->getMessage());
}
}
return $ok;
}
function send_mail_now(string $to, string $subject, string $htmlBody, ?string $fromOverride = null): bool
{
$cfg = [
'enabled' => get_setting('mail_enabled', config()['mail']['enabled'] ? '1' : '0') === '1',
'from_email' => get_setting('smtp_from_email', config()['mail']['from_email'] ?? ''),
'from_name' => get_setting('smtp_from_name', config()['mail']['from_name'] ?? ''),
];
// Adressen mit der reservierten Endung .invalid (z. B. von Discord- oder Telegram-Nutzern ohne E-Mail) sind nie zustellbar: nichts senden, kein Fehler
if (preg_match('/@[^@\\s]+\\.invalid$/i', trim($to))) return true;
$smtpHost = get_setting('smtp_host', '');
$fromHeader = $fromOverride ?: smtp_default_from();
if ($smtpHost === '' && !$cfg['enabled'] && !($fromOverride && email_address_smtp_for(get_db(), $fromHeader))) {
// Weder eigener SMTP-Server konfiguriert noch PHP mail() aktiviert.
// Wird still ignoriert, damit der Rest der Anwendung normal funktioniert.
return false;
}
// Eigener SMTP-Server der Absenderadresse (osTicket-Reiter "Abgehend") hat Vorrang vor dem globalen.
$own = $fromOverride ? email_address_smtp_for(get_db(), $fromHeader) : null;
if ($own !== null) {
$mailer = new SmtpMailer(
$own['smtp_host'],
(int) $own['smtp_port'],
$own['smtp_encryption'] ?: 'tls',
(string) $own['smtp_username'],
$own['smtp_password_enc'] ? decrypt_secret($own['smtp_password_enc']) : '',
);
mail_attempted(true);
$ok = $mailer->send($to, $fromHeader, $subject, $htmlBody);
if (!$ok) {
smtp_last_error($mailer->getLastError());
error_log('DeskHub SMTP-Versand (eigener Server) fehlgeschlagen: ' . $mailer->getLastError());
}
health_record_send_result($ok, $mailer->getLastError());
return $ok;
}
if ($smtpHost !== '') {
$mailer = new SmtpMailer(
$smtpHost,
(int) get_setting('smtp_port', '587'),
get_setting('smtp_encryption', 'tls'),
get_setting('smtp_username', ''),
decrypt_secret(get_setting('smtp_password', '')),
);
mail_attempted(true);
$ok = $mailer->send($to, $fromHeader, $subject, $htmlBody);
if (!$ok) {
smtp_last_error($mailer->getLastError());
error_log('DeskHub SMTP-Versand fehlgeschlagen: ' . $mailer->getLastError());
}
health_record_send_result($ok, $mailer->getLastError());
return $ok;
}
$headers = "MIME-Version: 1.0\r\n";
$headers .= "Content-type: text/html; charset=UTF-8\r\n";
$headers .= "From: {$fromHeader}\r\n";
mail_attempted(true);
return @mail($to, $subject, $htmlBody, $headers);
}
/**
* Wie send_mail(), protokolliert einen Fehlschlag aber zusätzlich sichtbar im
* Admin-Protokoll (staff/audit_log.php). System-Mails scheitern sonst komplett
* unbemerkt – der Kunde bekommt keine Fehlermeldung und niemand wird informiert.
*/
function email_send_and_log(string $to, string $subject, string $htmlBody, ?string $from, string $context, string $ticketNumber = ''): bool
{
$ok = send_mail($to, $subject, $htmlBody, $from);
if (!$ok) {
$ref = $ticketNumber !== '' ? " (Ticket #{$ticketNumber})" : '';
// admin_audit_log.action ist VARCHAR(255) NOT NULL – bei MySQL im Strict-Modus wirft ein
// zu langer Wert (z.B. eine ausführliche SMTP-Fehlermeldung) sonst eine harte Exception.
$message = mb_substr("{$context}{$ref} an {$to} fehlgeschlagen: " . smtp_last_error(), 0, 255);
audit_log(null, $message);
}
return $ok;
}
/** Standard-Absenderadresse: eigene SMTP-Absenderangabe, sonst die Werte aus config.php */
function smtp_default_from(): string
{
$cfg = [
'enabled' => get_setting('mail_enabled', config()['mail']['enabled'] ? '1' : '0') === '1',
'from_email' => get_setting('smtp_from_email', config()['mail']['from_email'] ?? ''),
'from_name' => get_setting('smtp_from_name', config()['mail']['from_name'] ?? ''),
];
$name = get_setting('smtp_from_name') ?: $cfg['from_name'];
$email = get_setting('smtp_from_email') ?: $cfg['from_email'];
return sprintf('%s <%s>', $name, $email);
}
/**
* Ermittelt die passende Absenderadresse für eine Abteilung (optional nach
* Priorität verfeinert) aus der E-Mail-Adressen-Verwaltung. Gibt null zurück,
* wenn keine passt – dann verwendet send_mail() die globale Standardadresse.
*/
function resolve_from_address(?int $departmentId, ?int $priorityId = null): ?string
{
if (!$departmentId) return null;
$pdo = get_db();
if ($priorityId) {
$stmt = $pdo->prepare('SELECT email, label FROM email_addresses WHERE department_id = ? AND priority_id = ? AND is_active = 1 LIMIT 1');
$stmt->execute([$departmentId, $priorityId]);
if ($row = $stmt->fetch()) {
return $row['label'] ? "{$row['label']} <{$row['email']}>" : $row['email'];
}
}
$stmt = $pdo->prepare('SELECT email, label FROM email_addresses WHERE department_id = ? AND is_default = 1 AND is_active = 1 LIMIT 1');
$stmt->execute([$departmentId]);
if ($row = $stmt->fetch()) {
return $row['label'] ? "{$row['label']} <{$row['email']}>" : $row['email'];
}
$stmt = $pdo->prepare('SELECT email, label FROM email_addresses WHERE department_id = ? AND is_active = 1 ORDER BY id LIMIT 1');
$stmt->execute([$departmentId]);
if ($row = $stmt->fetch()) {
return $row['label'] ? "{$row['label']} <{$row['email']}>" : $row['email'];
}
return null;
}
/**
* Liest eine anpassbare E-Mail-Vorlage aus den Einstellungen (Adminportal → E-Mail-Vorlagen)
* und ersetzt Platzhalter wie {ticket_number}. Fällt auf die mitgegebenen Standardtexte
* zurück, solange der Admin nichts Eigenes hinterlegt hat.
*
* @param array<string,string> $vars Platzhalter ohne geschweifte Klammern, z.B. ['ticket_number' => '...']
* @return array{0:string,1:string} [Betreff, HTML-Text]
*/
function render_email_template(string $key, array $vars, string $defaultSubject, string $defaultBody): array
{
$subject = get_setting("email_template_{$key}_subject") ?: $defaultSubject;
$body = get_setting("email_template_{$key}_body") ?: $defaultBody;
$search = [];
$replace = [];
foreach ($vars as $name => $value) {
$search[] = '{' . $name . '}';
$replace[] = $value;
}
return [str_replace($search, $replace, $subject), str_replace($search, $replace, $body)];
}
// ====================================================================
// Einheitliches, modernes E-Mail-Design
// ====================================================================
// email_wrap() umschließt JEDEN Vorlagentext (Standard oder vom Admin
// angepasst) mit demselben Branding-Rahmen, damit alle System-Mails
// gleich aussehen. email_badge()/email_button() sind wiederverwendbare
// Bausteine für die Standardtexte der einzelnen Vorlagen.
/** Branding-Rahmen (Header, weiße Karte, Footer) um einen HTML-Textkörper. */
function email_wrap(string $innerHtml): string
{
$siteName = e(get_setting('display_name', config()['site']['name'] ?? 'DeskHub Support'));
return <<<HTML
<div style="background:#f5f7f9;padding:32px 16px;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="max-width:520px;margin:0 auto;background:#ffffff;border-radius:12px;overflow:hidden;border:1px solid #e2e8f0;">
<tr>
<td style="background:#0f6f6a;padding:20px 28px;">
<span style="color:#ffffff;font-size:18px;font-weight:700;letter-spacing:-.01em;">{$siteName}</span>
</td>
</tr>
<tr>
<td style="padding:32px 28px;">
{$innerHtml}
</td>
</tr>
<tr>
<td style="padding:18px 28px;background:#f5f7f9;border-top:1px solid #e2e8f0;">
<p style="margin:0;color:#94a3b8;font-size:12px;">Diese E-Mail wurde automatisch von {$siteName} versendet.</p>
</td>
</tr>
</table>
</div>
HTML;
}
/** Farbige Hervorhebungs-Box, z.B. für die Ticketnummer. $label/$value dürfen bereits ersetzte {platzhalter} enthalten. */
function email_badge(string $label, string $value, string $color = '#0f6f6a', string $bg = '#e6f4f3'): string
{
return <<<HTML
<table role="presentation" cellpadding="0" cellspacing="0" style="margin:0 0 24px;">
<tr>
<td style="background:{$bg};border-radius:8px;padding:14px 18px;">
<div style="font-size:11px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:{$color};margin-bottom:2px;">{$label}</div>
<div style="font-size:20px;font-weight:700;color:{$color};font-family:'SFMono-Regular',Consolas,'Liberation Mono',monospace;">{$value}</div>
</td>
</tr>
</table>
HTML;
}
/** Button-artiger Call-to-Action-Link. */
function email_button(string $label, string $url, string $color = '#0f6f6a'): string
{
return <<<HTML
<table role="presentation" cellpadding="0" cellspacing="0">
<tr>
<td style="background:{$color};border-radius:8px;">
<a href="{$url}" style="display:inline-block;padding:12px 24px;color:#ffffff;font-size:14px;font-weight:600;text-decoration:none;">{$label}</a>
</td>
</tr>
</table>
HTML;
}
/** Wiederverwendeter Inline-Style für normale Absätze in Mail-Vorlagen. */
function email_p(string $html, string $marginBottom = '16px'): string
{
return "<p style=\"margin:0 0 {$marginBottom};color:#16202a;font-size:15px;line-height:1.5;\">{$html}</p>";
}
/**
* Standard-Textkörper der vier anpassbaren Vorlagen (Adminportal → E-Mail-Vorlagen).
* Enthalten nur den Innenbereich – der Branding-Rahmen kommt beim Versand
* automatisch über email_wrap() dazu, auch wenn ein Admin den Text anpasst.
*/
function email_ticket_created_default_body(): string
{
return email_p('Hallo {customer_name},')
. email_p('vielen Dank für Ihre Anfrage. Wir haben Ihr Ticket erhalten und kümmern uns so schnell wie möglich darum.', '20px')
. email_badge('Ihre Ticketnummer', '#{ticket_number}')
. email_p('<strong>Betreff:</strong> {subject}', '24px')
. email_button('Ticket ansehen', '{ticket_url}')
. '<p style="margin:24px 0 0;color:#64748b;font-size:13px;line-height:1.5;">Bewahren Sie diese Ticketnummer gut auf – damit können Sie den Status Ihrer Anfrage jederzeit auch ohne Kundenkonto unter <a href="{lookup_url}" style="color:#0f6f6a;">{lookup_url}</a> prüfen.</p>';
}
function email_new_reply_default_body(): string
{
return email_p('Hallo {customer_name},')
. email_p('es gibt eine neue Antwort zu Ihrem Ticket <strong>#{ticket_number}</strong>:')
. '<div style="background:#f8fafc;border-left:3px solid #0f6f6a;border-radius:0 6px 6px 0;padding:14px 18px;margin:0 0 24px;color:#16202a;font-size:14px;line-height:1.6;">{message}</div>'
. email_button('Ticket ansehen', '{ticket_url}');
}
function email_new_ticket_department_default_body(): string
{
return email_p('Ein neues Ticket wurde erstellt.')
. email_badge('Ticket', '#{ticket_number}')
. email_p('<strong>Von:</strong> {customer_name} ({customer_email})', '6px')
. email_p('<strong>Abteilung:</strong> {department_name}', '6px')
. email_p('<strong>Betreff:</strong> {subject}', '24px')
. email_button('Ticket ansehen', '{ticket_url}');
}
function email_ticket_overdue_default_body(): string
{
return email_p('Das folgende Ticket hat die SLA-Lösungsfrist überschritten und benötigt Aufmerksamkeit:')
. email_badge('Ticket', '#{ticket_number}', '#b91c1c', '#fee2e2')
. email_p('<strong>Betreff:</strong> {subject}', '6px')
. email_p('<strong>Abteilung:</strong> {department_name}', '24px')
. email_button('Ticket ansehen', '{ticket_url}', '#dc2626');
}
function email_live_chat_transcript_default_body(): string
{
return email_p('Hallo {customer_name},')
. email_p('vielen Dank für Ihren Chat mit uns. Hier der vollständige Verlauf zu Ihrer Unterlage:', '20px')
. '{transcript}'
. email_p('Bei weiteren Fragen antworten Sie einfach direkt auf diese E-Mail.', '20px')
. email_button('Ticket ansehen', '{ticket_url}');
}
/** Fertige, versandbereite HTML-Mail für Passwort-zurücksetzen-Links (Kunden- und Mitarbeiterbereich). */
function email_password_reset_body(string $link): string
{
$inner = email_p('Klicken Sie auf den folgenden Button, um Ihr Passwort zurückzusetzen. Der Link ist <strong>1 Stunde</strong> lang gültig.')
. email_button('Passwort zurücksetzen', e($link))
. '<p style="margin:20px 0 0;color:#64748b;font-size:13px;line-height:1.5;">Falls Sie dies nicht angefordert haben, ignorieren Sie diese E-Mail einfach – es wird nichts verändert.</p>';
return email_wrap($inner);
}
/** @param string[] $collaboratorEmails Zusätzliche Empfänger (Kollaboratoren), die dieselbe Antwort-Mail erhalten */
function notify_customer_new_reply(array $ticket, string $message, string $signature = '', array $collaboratorEmails = []): void
{
// $message ist bereits über sanitize_staff_html() bereinigtes, sicheres HTML – nicht erneut escapen.
// $signature stammt aus der Mitarbeiter- oder Abteilungspflege und wurde dort ebenfalls bereinigt.
$messageWithSignature = $message . ($signature !== '' ? "<div style=\"margin-top:18px;color:#64748b\">{$signature}</div>" : '');
[$subject, $body] = render_email_template('new_reply', [
'ticket_number' => $ticket['ticket_number'],
'subject' => e($ticket['subject']),
'customer_name' => e($ticket['requester_name'] ?? ''),
'message' => $messageWithSignature,
'ticket_url' => e(ticket_access_url($ticket)),
], "[#{$ticket['ticket_number']}] {$ticket['subject']}", email_new_reply_default_body());
$body = email_wrap($body);
$from = resolve_from_address($ticket['department_id'] ?? null, $ticket['priority_id'] ?? null);
email_send_and_log($ticket['requester_email'], $subject, $body, $from, "Neue-Antwort-Mail", $ticket['ticket_number']);
foreach ($collaboratorEmails as $ccEmail) {
if ($ccEmail === '' || strcasecmp($ccEmail, $ticket['requester_email']) === 0) continue;
email_send_and_log($ccEmail, $subject, $body, $from, "Neue-Antwort-Mail (Kollaborateur)", $ticket['ticket_number']);
}
}
/** Standardtext der Erinnerung an Kunden (siehe cron/remind_customers.php). */
function email_customer_reminder_default_body(): string
{
return '<h2 style="margin:0 0 12px;font-size:20px;color:#0f172a;">Brauchen Sie noch Hilfe?</h2>'
. '<p style="margin:0 0 12px;color:#334155;font-size:15px;line-height:1.6;">Hallo {customer_name}, wir haben Ihnen zu Ihrem Ticket <strong>#{ticket_number}</strong> („{subject}“) geantwortet und noch nichts von Ihnen gehört. Hat sich Ihr Anliegen erledigt, brauchen Sie nichts zu tun. Haben Sie noch Fragen, antworten Sie einfach.</p>'
. '{close_note}'
. '<p style="margin:20px 0 0;"><a href="{ticket_url}" style="background:#0f6f6a;color:#ffffff;text-decoration:none;padding:11px 20px;border-radius:8px;font-weight:600;display:inline-block;">Ticket ansehen</a></p>';
}
function notify_customer_ticket_created(array $ticket): void
{
[$subject, $body] = render_email_template('ticket_created', [
'ticket_number' => $ticket['ticket_number'],
'subject' => e($ticket['subject']),
'customer_name' => e($ticket['requester_name'] ?? ''),
'department_name' => e($ticket['department_name'] ?? ''),
'ticket_url' => e(ticket_access_url($ticket)),
'lookup_url' => base_url() . '/ticket_lookup.php',
], "Ihr Ticket #{ticket_number} wurde erstellt", email_ticket_created_default_body());
$from = resolve_from_address($ticket['department_id'] ?? null, null);
email_send_and_log($ticket['requester_email'], $subject, email_wrap($body), $from, 'Ticket-erstellt-Mail', $ticket['ticket_number']);
}
/** Rendert eine Chat-Nachricht als E-Mail-taugliche (Inline-Styles statt CSS-Klassen) Zeile. */
function email_chat_transcript_row(string $authorLabel, string $message, string $time, bool $isStaff): string
{
$align = $isStaff ? 'right' : 'left';
$bg = $isStaff ? '#0f6f6a' : '#f1f5f9';
$color = $isStaff ? '#ffffff' : '#16202a';
$safeMessage = nl2br(e($message));
return <<<HTML
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="margin:0 0 10px;">
<tr>
<td align="{$align}">
<div style="font-size:11px;color:#94a3b8;margin-bottom:3px;">{$authorLabel} · {$time}</div>
<table role="presentation" cellpadding="0" cellspacing="0" style="max-width:80%;display:inline-block;">
<tr>
<td style="background:{$bg};color:{$color};border-radius:10px;padding:8px 12px;font-size:14px;line-height:1.5;text-align:left;">{$safeMessage}</td>
</tr>
</table>
</td>
</tr>
</table>
HTML;
}
/**
* Schickt dem Kunden nach Ende eines Live-Chats (staff/live_chat.php, Aktion "end")
* den vollständigen Verlauf als E-Mail zu – abschaltbar über die Einstellung
* "live_chat_send_transcript" (Standard: an). $messages im Format wie von
* chat_messages() in live_chat.php: [['authorType'=>..,'authorName'=>..,'message'=>..,'createdAt'=>..], ...]
*/
function notify_customer_chat_transcript(array $ticket, array $messages): void
{
if (get_setting('live_chat_send_transcript', '1') !== '1') return;
if (!$messages) return;
$rows = '';
foreach ($messages as $m) {
$isStaff = $m['authorType'] === 'staff';
$label = $isStaff ? ($m['authorName'] ?: 'Support') : ($ticket['requester_name'] ?? 'Sie');
$time = substr((string) $m['createdAt'], 0, 16);
// Mitarbeiter-Nachrichten sind sanitiztes HTML (Rich-Text-Editor), Kunden-Nachrichten
// rohes Plain-Text (siehe live_chat.php-Konvention) – für die E-Mail beides einheitlich
// auf reinen Text reduzieren, sonst würden HTML-Tags als sichtbarer Text erscheinen.
$rawMessage = (string) $m['message'];
$plainMessage = preg_match('/<[a-zA-Z][^>]*>/i', $rawMessage)
? trim(html_entity_decode(strip_tags(str_replace(['<br>', '<br/>', '<br />', '</p>'], "\n", $rawMessage)), ENT_QUOTES | ENT_HTML5, 'UTF-8'))
: $rawMessage;
$rows .= email_chat_transcript_row(e($label), $plainMessage, e($time), $isStaff);
}
$transcriptBlock = '<div style="background:#ffffff;border:1px solid #e2e8f0;border-radius:8px;padding:16px;margin:0 0 20px;">' . $rows . '</div>';
[$subject, $body] = render_email_template('live_chat_transcript', [
'ticket_number' => $ticket['ticket_number'],
'customer_name' => e($ticket['requester_name'] ?? ''),
'transcript' => $transcriptBlock,
'ticket_url' => e(ticket_access_url($ticket)),
], 'Ihr Chatverlauf – Ticket #{ticket_number}', email_live_chat_transcript_default_body());
$from = resolve_from_address($ticket['department_id'] ?? null, null);
email_send_and_log($ticket['requester_email'], $subject, email_wrap($body), $from, 'Chatverlauf-Mail', $ticket['ticket_number']);
}
function notify_department_new_ticket(array $ticket, string $departmentEmail): void
{
[$subject, $body] = render_email_template('new_ticket_department', [
'ticket_number' => $ticket['ticket_number'],
'subject' => e($ticket['subject']),
'customer_name' => e($ticket['requester_name'] ?? ''),
'customer_email' => e($ticket['requester_email'] ?? ''),
'department_name' => e($ticket['department_name'] ?? ''),
'ticket_url' => base_url() . '/staff/ticket_view.php?n=' . urlencode($ticket['ticket_number']),
], "Neues Ticket [#{ticket_number}] {subject}", email_new_ticket_department_default_body());
email_send_and_log($departmentEmail, $subject, email_wrap($body), null, 'Neues-Ticket-Mail (Abteilung)', $ticket['ticket_number']);
}
/**
* Benachrichtigt per E-Mail: alle aktiven Admins (immer) sowie Mitarbeiter, deren
* Hauptabteilung der Ticket-Abteilung entspricht oder die für diese Abteilung als
* zusätzliche Abteilung mit aktivierter Benachrichtigung (staff_department.notify) eingetragen sind.
* Ergänzt die bestehende generische Abteilungs-E-Mail (notify_department_new_ticket), die weiterhin
* zusätzlich an die in der E-Mail-Adressen-Verwaltung hinterlegte Postfach-Adresse geht.
*/
function notify_staff_new_ticket(array $ticket): void
{
$pdo = get_db();
$deptId = $ticket['department_id'] ?? null;
if ($deptId) {
$stmt = $pdo->prepare(
"SELECT DISTINCT email FROM staff WHERE is_active = 1 AND (
role = 'admin'
OR department_id = ?
OR id IN (SELECT staff_id FROM staff_department WHERE department_id = ? AND notify = 1)
)"
);
$stmt->execute([$deptId, $deptId]);
} else {
$stmt = $pdo->query("SELECT email FROM staff WHERE is_active = 1 AND role = 'admin'");
}
$recipients = $stmt->fetchAll(PDO::FETCH_COLUMN);
if (!$recipients) return;
[$subject, $body] = render_email_template('new_ticket_department', [
'ticket_number' => $ticket['ticket_number'],
'subject' => e($ticket['subject']),
'customer_name' => e($ticket['requester_name'] ?? ''),
'customer_email' => e($ticket['requester_email'] ?? ''),
'department_name' => e($ticket['department_name'] ?? ''),
'ticket_url' => base_url() . '/staff/ticket_view.php?n=' . urlencode($ticket['ticket_number']),
], "Neues Ticket [#{ticket_number}] {subject}", email_new_ticket_department_default_body());
$body = email_wrap($body);
foreach ($recipients as $email) {
email_send_and_log($email, $subject, $body, null, 'Neues-Ticket-Mail (Mitarbeiter)', $ticket['ticket_number']);
}
}
/** Benachrichtigt alle aktiven Admins per E-Mail, wenn ein Ticket neu als überfällig erkannt wurde */
function notify_admins_overdue_ticket(array $ticket): void
{
$admins = get_db()->query("SELECT email FROM staff WHERE is_active = 1 AND role = 'admin'")->fetchAll(PDO::FETCH_COLUMN);
if (!$admins) return;
[$subject, $body] = render_email_template('ticket_overdue', [
'ticket_number' => $ticket['ticket_number'],
'subject' => e($ticket['subject']),
'department_name' => e($ticket['department_name'] ?? ''),
'ticket_url' => base_url() . '/staff/ticket_view.php?n=' . urlencode($ticket['ticket_number']),
], "SLA überschritten: Ticket #{ticket_number}", email_ticket_overdue_default_body());
$body = email_wrap($body);
foreach ($admins as $email) {
email_send_and_log($email, $subject, $body, null, 'Ticket-überfällig-Mail', $ticket['ticket_number']);
}
}