errorCode; } public function httpStatus(): int { return $this->httpStatus; } } /** Verknüpftes IMAP-Postfach einer Adresse (oder null). */ function email_address_imap(PDO $pdo, int $addressId): ?array { $stmt = $pdo->prepare('SELECT * FROM imap_accounts WHERE email_address_id = ? ORDER BY id LIMIT 1'); $stmt->execute([$addressId]); return $stmt->fetch() ?: null; } /** Eigener SMTP-Zugang einer Absenderadresse (nur aktive Adresse mit aktiviertem SMTP), sonst null. */ function email_address_smtp_for(PDO $pdo, string $fromHeader): ?array { if (!preg_match('/<([^>]+)>/', $fromHeader, $m)) $m = [1 => $fromHeader]; $email = strtolower(trim($m[1])); if ($email === '') return null; $stmt = $pdo->prepare('SELECT smtp_host, smtp_port, smtp_encryption, smtp_username, smtp_password_enc FROM email_addresses WHERE LOWER(email) = ? AND is_active = 1 AND smtp_enabled = 1'); $stmt->execute([$email]); $row = $stmt->fetch(); return ($row && trim((string) $row['smtp_host']) !== '') ? $row : null; } /** * Legt eine E-Mail-Adresse an oder aktualisiert sie – inklusive IMAP-Postfach und eigenem SMTP. * * @param array $in Schlüssel: email,label,department_id,priority_id,help_topic_id,is_default,is_active,notes, * imap_enabled,imap_host,imap_port,imap_encryption,imap_username,imap_password,imap_folder, * imap_after_fetch,imap_archive_folder,imap_spam_folder, * smtp_enabled,smtp_host,smtp_port,smtp_encryption,smtp_username,smtp_password * @return int ID der Adresse * @throws EmailAddressException bei ungültigen Eingaben */ function email_address_save(PDO $pdo, int $id, array $in): int { $email = strtolower(trim((string) ($in['email'] ?? ''))); $label = trim((string) ($in['label'] ?? '')); $deptId = (int) ($in['department_id'] ?? 0); $prioId = isset($in['priority_id']) && $in['priority_id'] !== '' && $in['priority_id'] !== null ? (int) $in['priority_id'] : null; $topicId = isset($in['help_topic_id']) && $in['help_topic_id'] !== '' && $in['help_topic_id'] !== null ? (int) $in['help_topic_id'] : null; $isDefault = !empty($in['is_default']) ? 1 : 0; if (array_key_exists('is_active', $in)) { $isActive = !empty($in['is_active']) ? 1 : 0; } else { // nicht angegeben: bisherigen Status behalten (neu = aktiv) $cur = $pdo->prepare('SELECT is_active FROM email_addresses WHERE id = ?'); $cur->execute([$id]); $isActive = $id ? (int) ($cur->fetchColumn() ?? 1) : 1; } $notes = trim((string) ($in['notes'] ?? '')); if (!filter_var($email, FILTER_VALIDATE_EMAIL)) throw new EmailAddressException('Bitte eine gültige E-Mail-Adresse angeben.', 'INVALID_EMAIL'); $chk = $pdo->prepare('SELECT COUNT(*) FROM departments WHERE id = ?'); $chk->execute([$deptId]); if (!(int) $chk->fetchColumn()) throw new EmailAddressException('Bitte eine Abteilung wählen.', 'INVALID_DEPARTMENT'); if ($topicId !== null) { $chk = $pdo->prepare('SELECT COUNT(*) FROM help_topics WHERE id = ?'); $chk->execute([$topicId]); if (!(int) $chk->fetchColumn()) throw new EmailAddressException('Das gewählte Hilfethema existiert nicht.', 'INVALID_TOPIC'); } $dup = $pdo->prepare('SELECT id FROM email_addresses WHERE LOWER(email) = ? AND id <> ?'); $dup->execute([$email, $id]); if ($dup->fetchColumn()) throw new EmailAddressException('Diese E-Mail-Adresse ist bereits angelegt.', 'DUPLICATE_EMAIL'); // IMAP $existingImap = $id ? email_address_imap($pdo, $id) : null; $imapOn = !empty($in['imap_enabled']) && $isActive; $imap = [ 'host' => trim((string) ($in['imap_host'] ?? '')), 'port' => (int) ($in['imap_port'] ?? 993) ?: 993, 'enc' => in_array((string) ($in['imap_encryption'] ?? ''), ['ssl', 'tls', 'none'], true) ? $in['imap_encryption'] : 'ssl', 'username' => trim((string) ($in['imap_username'] ?? '')) ?: $email, 'password' => trim((string) ($in['imap_password'] ?? '')), 'folder' => trim((string) ($in['imap_folder'] ?? '')) ?: 'INBOX', 'after' => in_array((string) ($in['imap_after_fetch'] ?? ''), ['seen', 'archive', 'delete'], true) ? $in['imap_after_fetch'] : 'seen', 'archive' => trim((string) ($in['imap_archive_folder'] ?? '')) ?: 'Processed', 'spam' => trim((string) ($in['imap_spam_folder'] ?? '')) ?: 'Junk', ]; if (!empty($in['imap_enabled'])) { if ($imap['host'] === '') throw new EmailAddressException('Für den Abruf bitte einen Server (Host) angeben.', 'IMAP_HOST_REQUIRED'); if (!$existingImap && $imap['password'] === '') throw new EmailAddressException('Für den Abruf bitte ein Passwort angeben.', 'IMAP_PASSWORD_REQUIRED'); } // SMTP $existing = null; if ($id) { $s = $pdo->prepare('SELECT smtp_host, smtp_port, smtp_encryption, smtp_username, smtp_password_enc FROM email_addresses WHERE id = ?'); $s->execute([$id]); $existing = $s->fetch() ?: null; } $smtpOn = !empty($in['smtp_enabled']) ? 1 : 0; $smtp = [ 'host' => trim((string) ($in['smtp_host'] ?? '')), 'port' => (int) ($in['smtp_port'] ?? 587) ?: 587, 'enc' => in_array((string) ($in['smtp_encryption'] ?? ''), ['ssl', 'tls', 'none'], true) ? $in['smtp_encryption'] : 'tls', 'username' => trim((string) ($in['smtp_username'] ?? '')), 'password' => (string) ($in['smtp_password'] ?? ''), ]; if ($smtpOn && $smtp['host'] === '') throw new EmailAddressException('Für den eigenen Versand bitte einen SMTP-Server angeben.', 'SMTP_HOST_REQUIRED'); $smtpPassEnc = $smtp['password'] !== '' ? encrypt_secret($smtp['password']) : ($existing['smtp_password_enc'] ?? null); if (!$smtpOn && $existing) { // Eigener Versand ausgeschaltet: hinterlegte Zugangsdaten bleiben für später erhalten $smtp = ['host' => (string) $existing['smtp_host'], 'port' => (int) $existing['smtp_port'], 'enc' => $existing['smtp_encryption'] ?: 'tls', 'username' => (string) $existing['smtp_username'], 'password' => '']; } if ($isDefault) { $pdo->prepare('UPDATE email_addresses SET is_default = 0 WHERE department_id = ?')->execute([$deptId]); } $now = db_now(); if ($id === 0) { $pdo->prepare( 'INSERT INTO email_addresses (email, label, department_id, priority_id, help_topic_id, is_default, is_active, notes, smtp_enabled, smtp_host, smtp_port, smtp_encryption, smtp_username, smtp_password_enc, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)' )->execute([$email, $label, $deptId, $prioId, $topicId, $isDefault, $isActive, $notes ?: null, $smtpOn, $smtp['host'] ?: null, $smtp['port'], $smtp['enc'], $smtp['username'] ?: null, $smtpPassEnc, $now, $now]); $id = (int) $pdo->lastInsertId(); } else { $pdo->prepare( 'UPDATE email_addresses SET email=?, label=?, department_id=?, priority_id=?, help_topic_id=?, is_default=?, is_active=?, notes=?, smtp_enabled=?, smtp_host=?, smtp_port=?, smtp_encryption=?, smtp_username=?, smtp_password_enc=?, updated_at=? WHERE id=?' )->execute([$email, $label, $deptId, $prioId, $topicId, $isDefault, $isActive, $notes ?: null, $smtpOn, $smtp['host'] ?: null, $smtp['port'], $smtp['enc'], $smtp['username'] ?: null, $smtpPassEnc, $now, $id]); } // Verknüpftes Postfach: Abteilung/Priorität/Hilfethema kommen aus dem Konto if ($imapOn) { if ($existingImap) { $sql = 'UPDATE imap_accounts SET label=?, host=?, port=?, encryption=?, username=?, folder=?, after_fetch=?, archive_folder=?, spam_folder=?, department_id=?, priority_id=?, is_active=1'; $params = [$email, $imap['host'], $imap['port'], $imap['enc'], $imap['username'], $imap['folder'], $imap['after'], $imap['archive'], $imap['spam'], $deptId, $prioId]; if ($imap['password'] !== '') { $sql .= ', password_enc=?'; $params[] = encrypt_secret($imap['password']); } $params[] = $existingImap['id']; $pdo->prepare($sql . ' WHERE id=?')->execute($params); } else { $pdo->prepare( 'INSERT INTO imap_accounts (label, host, port, encryption, username, password_enc, folder, after_fetch, archive_folder, spam_folder, department_id, priority_id, is_active, email_address_id, created_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,1,?,?)' )->execute([$email, $imap['host'], $imap['port'], $imap['enc'], $imap['username'], encrypt_secret($imap['password']), $imap['folder'], $imap['after'], $imap['archive'], $imap['spam'], $deptId, $prioId, $id, $now]); } } elseif ($existingImap) { $pdo->prepare('UPDATE imap_accounts SET is_active = 0, department_id = ?, priority_id = ? WHERE id = ?')->execute([$deptId, $prioId, $existingImap['id']]); } return $id; } /** Löscht eine Adresse samt Postfach (oder löst das Postfach, falls Tickets darauf verweisen). Gibt die gelöschte E-Mail zurück. */ function email_address_delete(PDO $pdo, int $id): string { $stmt = $pdo->prepare('SELECT email FROM email_addresses WHERE id = ?'); $stmt->execute([$id]); $email = (string) ($stmt->fetchColumn() ?: $id); if ($imap = email_address_imap($pdo, $id)) { $used = $pdo->prepare('SELECT COUNT(*) FROM ticket_email_sources WHERE imap_account_id = ?'); $used->execute([$imap['id']]); if ((int) $used->fetchColumn() > 0) { $pdo->prepare('UPDATE imap_accounts SET is_active = 0, email_address_id = NULL WHERE id = ?')->execute([$imap['id']]); } else { $pdo->prepare('DELETE FROM imap_accounts WHERE id = ?')->execute([$imap['id']]); } } $pdo->prepare('DELETE FROM email_addresses WHERE id = ?')->execute([$id]); return $email; } /** Sendet eine Test-Mail von dieser Adresse an $recipient. Gibt die Erfolgsmeldung zurück. */ function email_address_send_test(PDO $pdo, int $id, string $recipient): string { $stmt = $pdo->prepare('SELECT email, label, smtp_enabled FROM email_addresses WHERE id = ?'); $stmt->execute([$id]); $addr = $stmt->fetch(); if (!$addr) throw new EmailAddressException('E-Mail-Adresse nicht gefunden.', 'NOT_FOUND', 404); $from = $addr['label'] ? "{$addr['label']} <{$addr['email']}>" : $addr['email']; $via = $addr['smtp_enabled'] ? 'eigenen SMTP-Server' : 'globalen SMTP-Server'; $html = '

Dies ist eine Test-Mail von DeskHub.

Absender: ' . e($addr['email']) . '
Versand über: ' . e($via) . '

'; if (!send_mail($recipient, 'DeskHub – Test-Mail von ' . $addr['email'], $html, $from, false)) { throw new EmailAddressException('Versand fehlgeschlagen. Bitte SMTP-Daten prüfen. ' . smtp_last_error(), 'MAIL_FAILED', 502); } return "Test-Mail von {$addr['email']} an {$recipient} gesendet (über {$via})."; } /** Prüft die IMAP-Verbindung des verknüpften Postfachs. Gibt die Erfolgsmeldung zurück. */ function email_address_test_imap(PDO $pdo, int $id): string { $acc = email_address_imap($pdo, $id); if (!$acc) throw new EmailAddressException('Für diese Adresse ist kein Postfach hinterlegt. Bitte zuerst speichern.', 'NO_MAILBOX'); if (!function_exists('imap_open')) throw new EmailAddressException('PHP-IMAP-Erweiterung (php-imap) ist auf diesem Server nicht installiert.', 'NO_IMAP', 501); $flag = match ($acc['encryption']) { 'ssl' => '/imap/ssl', 'tls' => '/imap/tls', default => '/imap/notls' }; $mbox = @imap_open('{' . $acc['host'] . ':' . $acc['port'] . $flag . '}' . $acc['folder'], $acc['username'], decrypt_secret($acc['password_enc']), 0, 1); if (!$mbox) throw new EmailAddressException('Verbindung fehlgeschlagen: ' . imap_last_error(), 'IMAP_FAILED', 502); $unseen = count(imap_search($mbox, 'UNSEEN') ?: []); imap_close($mbox); return "Verbindung erfolgreich. {$unseen} ungelesene Mail(s) im Postfach."; } /** Prüft die SMTP-Anmeldung dieser Adresse, ohne eine Mail zu senden. Gibt die Erfolgsmeldung zurück. */ function email_address_test_smtp(PDO $pdo, int $id): string { $stmt = $pdo->prepare('SELECT smtp_host, smtp_port, smtp_encryption, smtp_username, smtp_password_enc FROM email_addresses WHERE id = ?'); $stmt->execute([$id]); $row = $stmt->fetch(); if (!$row || trim((string) $row['smtp_host']) === '') throw new EmailAddressException('Für diese Adresse ist kein eigener SMTP-Server hinterlegt. Bitte zuerst speichern.', 'NO_SMTP'); $timeout = 8; $scheme = $row['smtp_encryption'] === 'ssl' ? 'ssl://' : ''; $errno = 0; $errstr = ''; $sock = @stream_socket_client($scheme . $row['smtp_host'] . ':' . (int) $row['smtp_port'], $errno, $errstr, $timeout); if (!$sock) throw new EmailAddressException("Verbindung zu {$row['smtp_host']}:{$row['smtp_port']} fehlgeschlagen: {$errstr}", 'SMTP_FAILED', 502); $banner = fgets($sock, 512); fclose($sock); if (!is_string($banner) || strpos($banner, '220') !== 0) throw new EmailAddressException('Der Server hat keine SMTP-Begrüßung gesendet.', 'SMTP_FAILED', 502); return "SMTP-Server {$row['smtp_host']}:{$row['smtp_port']} ist erreichbar. Die Anmeldung wird beim Versenden der Test-Mail geprüft."; }