228 lines
14 KiB
PHP
228 lines
14 KiB
PHP
<?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.";
|
||
}
|