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 {$html} Bewahren Sie diese Ticketnummer gut auf – damit können Sie den Status Ihrer Anfrage jederzeit auch ohne Kundenkonto unter {lookup_url} prüfen. Falls Sie dies nicht angefordert haben, ignorieren Sie diese E-Mail einfach – es wird nichts verändert. Hallo {customer_name}, wir haben Ihnen zu Ihrem Ticket #{ticket_number} („{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.
HTML;
}
/** Button-artiger Call-to-Action-Link. */
function email_button(string $label, string $url, string $color = '#0f6f6a'): string
{
return <<
HTML;
}
/** Wiederverwendeter Inline-Style für normale Absätze in Mail-Vorlagen. */
function email_p(string $html, string $marginBottom = '16px'): string
{
return "
{$label}
Brauchen Sie noch Hilfe?
'
. '
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(['
{$safeMessage}
', '
', '
', '