Files
2026-10-28 07:54:38 +00:00

228 lines
14 KiB
PHP
Raw Permalink 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
/**
* Gemeinsame Logik für E-Mail-Adressen (Konto, Abruf per IMAP, Versand per SMTP).
* Wird von staff/email_addresses.php (Weboberfläche) und api/staff/v1/email_addresses.php
* (Mobile-/Desktop-App) genutzt, damit beide exakt dasselbe tun.
*/
class EmailAddressException extends RuntimeException
{
public function __construct(string $message, private string $errorCode = 'INVALID', private int $httpStatus = 422)
{
parent::__construct($message);
}
public function errorCode(): string { return $this->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 = '<p>Dies ist eine Test-Mail von DeskHub.</p><p>Absender: ' . e($addr['email']) . '<br>Versand über: ' . e($via) . '</p>';
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.";
}