Files
deskhub/Plugins/plugins/rundmail/lib.php
T
2026-10-24 20:15:15 +00:00

221 lines
13 KiB
PHP

<?php
// Störungsmitteilung und Rundmail. Alle Funktionen beginnen mit rundmail_.
const RUNDMAIL = 'rundmail';
const RUNDMAIL_BATCH = 25;
/** Vorlagen für typische Anlässe. @return array<string,array{0:string,1:string}> */
function rundmail_presets(): array
{
return [
'wartung' => ['Geplante Wartung', "Guten Tag {name},\n\nwir führen am [Datum] von [Uhrzeit] bis [Uhrzeit] eine Wartung durch. In dieser Zeit ist unser Dienst möglicherweise nicht erreichbar.\n\nIhr offenes Ticket bleibt davon unberührt, wir melden uns danach bei Ihnen.\n\nVielen Dank für Ihr Verständnis."],
'stoerung' => ['Aktuelle Störung', "Guten Tag {name},\n\nzurzeit gibt es eine Störung: [Beschreibung]. Wir arbeiten an der Behebung und halten Sie auf dem Laufenden.\n\nIhr offenes Ticket ist dadurch möglicherweise ebenfalls betroffen. Sie müssen nichts weiter tun.\n\nVielen Dank für Ihre Geduld."],
'behoben' => ['Störung behoben', "Guten Tag {name},\n\ndie Störung ist behoben. Alles läuft wieder wie gewohnt.\n\nSollte bei Ihnen noch etwas nicht funktionieren, antworten Sie einfach auf Ihr Ticket.\n\nVielen Dank für Ihre Geduld."],
];
}
/** Gültige Adresse eines echten Kunden (keine Platzhalter, keine anonymisierten oder gesperrten Adressen). */
function rundmail_usable_email(string $email): bool
{
$email = strtolower(trim($email));
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) return false;
if (str_ends_with($email, '.invalid') || str_ends_with($email, '@deleted.local') || str_starts_with($email, 'anon_')) return false;
if (function_exists('is_banned') && is_banned($email)) return false;
return true;
}
/** Abteilungen, an deren Kunden der Mitarbeiter schreiben darf (null = alle). */
function rundmail_allowed_departments(array $staff): ?array
{
$ids = staff_department_ids($staff);
return $ids === null ? null : array_map('intval', $ids);
}
/** Auswahl für das Formular. @return list<array{key:string,label:string}> */
function rundmail_groups(PDO $pdo, array $staff): array
{
$allowed = rundmail_allowed_departments($staff);
$out = [['key' => 'open', 'label' => 'Alle Kunden mit offenen Tickets' . ($allowed === null ? '' : ' (meiner Abteilungen)')]];
foreach ($pdo->query('SELECT id, name FROM departments ORDER BY name')->fetchAll() as $d) {
if ($allowed !== null && !in_array((int) $d['id'], $allowed, true)) continue;
$out[] = ['key' => 'department:' . $d['id'], 'label' => 'Kunden mit offenen Tickets in Abteilung „' . $d['name'] . '“'];
}
foreach ($pdo->query('SELECT id, name FROM organizations ORDER BY name')->fetchAll() as $o) {
$out[] = ['key' => 'organization:' . $o['id'], 'label' => 'Organisation „' . $o['name'] . '“' . ($allowed === null ? ' (alle Mitglieder)' : ' (Mitglieder mit offenem Ticket)')];
}
return $out;
}
/** @return array{type:string,id:?int,label:string}|null */
function rundmail_parse_group(PDO $pdo, string $key): ?array
{
if ($key === 'open') return ['type' => 'open', 'id' => null, 'label' => 'Kunden mit offenen Tickets'];
if (!preg_match('/^(department|organization):(\d+)$/', $key, $m)) return null;
$table = $m[1] === 'department' ? 'departments' : 'organizations';
$s = $pdo->prepare("SELECT name FROM {$table} WHERE id = ?");
$s->execute([(int) $m[2]]);
$name = $s->fetchColumn();
if ($name === false) return null;
return ['type' => $m[1], 'id' => (int) $m[2], 'label' => ($m[1] === 'department' ? 'Abteilung ' : 'Organisation ') . $name];
}
/** Empfänger der Gruppe, einmal je Adresse. @return list<array{email:string,name:string}> */
function rundmail_recipients(PDO $pdo, array $staff, string $type, ?int $id): array
{
$allowed = rundmail_allowed_departments($staff);
if ($type === 'department' && $allowed !== null && !in_array((int) $id, $allowed, true)) return [];
$open = "t.status NOT IN ('resolved','closed') AND t.is_spam = 0 AND t.merged_into IS NULL";
$params = [];
$deptSql = '';
if ($type === 'department') { $deptSql = ' AND t.department_id = ?'; $params[] = (int) $id; }
elseif ($allowed !== null) {
if (!$allowed) return [];
$deptSql = ' AND t.department_id IN (' . implode(',', array_fill(0, count($allowed), '?')) . ')';
$params = array_merge($params, $allowed);
}
if ($type === 'organization') {
if ($allowed === null) {
$stmt = $pdo->prepare('SELECT email, name FROM users WHERE organization_id = ? ORDER BY id');
$stmt->execute([(int) $id]);
} else {
$stmt = $pdo->prepare("SELECT DISTINCT u.email, u.name FROM users u JOIN tickets t ON t.user_id = u.id WHERE u.organization_id = ? AND {$open}{$deptSql} ORDER BY u.id");
$stmt->execute(array_merge([(int) $id], $params));
}
} else {
$stmt = $pdo->prepare("SELECT t.requester_email AS email, t.requester_name AS name FROM tickets t WHERE {$open}{$deptSql} ORDER BY t.id");
$stmt->execute($params);
}
$seen = [];
$out = [];
foreach ($stmt->fetchAll() as $r) {
$mail = strtolower(trim((string) $r['email']));
if (isset($seen[$mail]) || !rundmail_usable_email($mail)) continue;
$seen[$mail] = true;
$out[] = ['email' => $mail, 'name' => trim((string) $r['name'])];
}
return $out;
}
/** Betreff und Mail-Text für einen Empfänger. @return array{0:string,1:string} */
function rundmail_render(array $msg, string $name): array
{
$name = $name !== '' ? $name : 'Kunde';
$text = str_replace('{name}', $name, (string) $msg['body']);
$html = '<div>' . nl2br(e($text)) . '</div>'
. '<p style="color:#6b7280;font-size:12px;margin-top:20px">Sie erhalten diese Nachricht, weil Sie bei uns ein offenes Ticket haben bzw. zur genannten Kundengruppe gehören. Bitte antworten Sie bei Fragen auf Ihr Ticket.</p>';
if (function_exists('email_wrap')) $html = email_wrap($html);
return [(string) $msg['subject'], $html];
}
/** Versand einer einzelnen Mail (für Tests austauschbar über $GLOBALS['rundmail_send']). */
function rundmail_send(string $to, string $subject, string $html): bool
{
if (isset($GLOBALS['rundmail_send']) && is_callable($GLOBALS['rundmail_send'])) return (bool) ($GLOBALS['rundmail_send'])($to, $subject, $html);
return (bool) send_mail($to, $subject, $html);
}
/** Prüft Betreff und Text. */
function rundmail_validate(string $subject, string $body): ?string
{
if (trim($subject) === '' || mb_strlen($subject) > 150) return 'Bitte einen Betreff angeben (höchstens 150 Zeichen).';
if (trim($body) === '' || mb_strlen($body) > 5000) return 'Bitte einen Text angeben (höchstens 5000 Zeichen).';
if (preg_match('/[\r\n]/', $subject)) return 'Der Betreff darf nur eine Zeile sein.';
return null;
}
/** Legt die Nachricht an und startet den Versand. @return array{ok:bool,error?:string,id?:int,total?:int} */
function rundmail_create(PDO $pdo, array $staff, string $groupKey, string $subject, string $body): array
{
if ($err = rundmail_validate($subject, $body)) return ['ok' => false, 'error' => $err];
$group = rundmail_parse_group($pdo, $groupKey);
if (!$group) return ['ok' => false, 'error' => 'Bitte eine Empfängergruppe wählen.'];
$isAdmin = ($staff['role'] ?? '') === 'admin';
$recent = $pdo->prepare('SELECT COUNT(*) FROM plugin_rundmail_messages WHERE sender_id = ? AND created_at >= ?');
$recent->execute([(int) $staff['id'], date('Y-m-d H:i:s', time() - 3600)]);
if ((int) $recent->fetchColumn() >= ($isAdmin ? 20 : 5)) return ['ok' => false, 'error' => 'Sie haben in der letzten Stunde schon viele Rundmails verschickt. Bitte später noch einmal.'];
$rcpt = rundmail_recipients($pdo, $staff, $group['type'], $group['id']);
if (!$rcpt) return ['ok' => false, 'error' => 'In dieser Gruppe gibt es keine Empfänger (oder Sie dürfen ihr nicht schreiben).'];
$max = $isAdmin ? 2000 : 200;
if (count($rcpt) > $max) return ['ok' => false, 'error' => 'Zu viele Empfänger (' . count($rcpt) . '). Erlaubt sind höchstens ' . $max . '.' . ($isAdmin ? '' : ' Größere Verteiler darf nur ein Administrator senden.')];
$pdo->prepare("INSERT INTO plugin_rundmail_messages (subject, body, group_type, group_id, group_label, sender_id, sender_name, status, total, created_at) VALUES (?,?,?,?,?,?,?,'sending',?,?)")
->execute([trim($subject), trim($body), $group['type'], $group['id'], mb_substr($group['label'], 0, 190), (int) $staff['id'], mb_substr((string) $staff['name'], 0, 120), count($rcpt), db_now()]);
$id = (int) $pdo->lastInsertId();
$ins = $pdo->prepare('INSERT INTO plugin_rundmail_queue (message_id, email, name) VALUES (?,?,?)');
foreach ($rcpt as $r) $ins->execute([$id, $r['email'], mb_substr($r['name'], 0, 120)]);
rundmail_process($pdo, $id, RUNDMAIL_BATCH);
return ['ok' => true, 'id' => $id, 'total' => count($rcpt)];
}
/** Verschickt die nächsten Mails (einer Nachricht oder der ältesten offenen). @return int Zahl der bearbeiteten Empfänger */
function rundmail_process(PDO $pdo, ?int $messageId = null, int $batch = RUNDMAIL_BATCH): int
{
if ($messageId === null) {
$messageId = (int) $pdo->query("SELECT id FROM plugin_rundmail_messages WHERE status = 'sending' ORDER BY id LIMIT 1")->fetchColumn();
if (!$messageId) return 0;
}
$m = $pdo->prepare("SELECT * FROM plugin_rundmail_messages WHERE id = ? AND status = 'sending'");
$m->execute([$messageId]);
$msg = $m->fetch();
if (!$msg) return 0;
$q = $pdo->prepare("SELECT * FROM plugin_rundmail_queue WHERE message_id = ? AND status = 'pending' ORDER BY id LIMIT " . max(1, $batch));
$q->execute([$messageId]);
$n = 0;
foreach ($q->fetchAll() as $row) {
[$subject, $html] = rundmail_render($msg, (string) $row['name']);
try {
$ok = rundmail_send((string) $row['email'], $subject, $html);
$err = $ok ? null : 'Versand fehlgeschlagen (E-Mail-Versand eingerichtet?)';
} catch (Throwable $e) {
$ok = false;
$err = mb_substr($e->getMessage(), 0, 240);
}
$pdo->prepare('UPDATE plugin_rundmail_queue SET status = ?, error = ?, sent_at = ? WHERE id = ?')->execute([$ok ? 'sent' : 'failed', $err, db_now(), $row['id']]);
$n++;
}
$c = $pdo->prepare("SELECT SUM(CASE WHEN status = 'sent' THEN 1 ELSE 0 END), SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END), SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END) FROM plugin_rundmail_queue WHERE message_id = ?");
$c->execute([$messageId]);
[$sent, $failed, $pending] = array_map('intval', array_values($c->fetch(PDO::FETCH_NUM)));
if ($pending === 0) $pdo->prepare("UPDATE plugin_rundmail_messages SET sent = ?, failed = ?, status = 'done', finished_at = ? WHERE id = ?")->execute([$sent, $failed, db_now(), $messageId]);
else $pdo->prepare('UPDATE plugin_rundmail_messages SET sent = ?, failed = ? WHERE id = ?')->execute([$sent, $failed, $messageId]);
return $n;
}
/** Hintergrundlauf: nächste Mails der laufenden Nachrichten versenden. */
function rundmail_tick(PDO $pdo): int
{
return rundmail_process($pdo, null, RUNDMAIL_BATCH);
}
/** Bricht den Versand ab (noch nicht gesendete Mails entfallen). Verfasser oder Administrator. */
function rundmail_cancel(PDO $pdo, array $staff, int $id): bool
{
$s = $pdo->prepare("SELECT sender_id FROM plugin_rundmail_messages WHERE id = ? AND status = 'sending'");
$s->execute([$id]);
$owner = $s->fetchColumn();
if ($owner === false || (($staff['role'] ?? '') !== 'admin' && (int) $owner !== (int) $staff['id'])) return false;
$pdo->prepare("UPDATE plugin_rundmail_queue SET status = 'cancelled' WHERE message_id = ? AND status = 'pending'")->execute([$id]);
$c = $pdo->prepare("SELECT SUM(CASE WHEN status = 'sent' THEN 1 ELSE 0 END), SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) FROM plugin_rundmail_queue WHERE message_id = ?");
$c->execute([$id]);
[$sent, $failed] = array_map('intval', array_values($c->fetch(PDO::FETCH_NUM)));
$pdo->prepare("UPDATE plugin_rundmail_messages SET status = 'cancelled', sent = ?, failed = ?, finished_at = ? WHERE id = ?")->execute([$sent, $failed, db_now(), $id]);
return true;
}
/** Probemail an den angemeldeten Mitarbeiter. */
function rundmail_test(array $staff, string $subject, string $body): array
{
if ($err = rundmail_validate($subject, $body)) return ['ok' => false, 'error' => $err];
[$s, $html] = rundmail_render(['subject' => '[TEST] ' . trim($subject), 'body' => trim($body)], (string) $staff['name']);
$ok = rundmail_send((string) $staff['email'], $s, $html);
return $ok ? ['ok' => true] : ['ok' => false, 'error' => 'Die Probemail konnte nicht gesendet werden (E-Mail-Versand eingerichtet?).'];
}
/** Verlauf: Administratoren sehen alle, Mitarbeiter ihre eigenen. */
function rundmail_history(PDO $pdo, array $staff, int $limit = 40): array
{
if (($staff['role'] ?? '') === 'admin') return $pdo->query('SELECT * FROM plugin_rundmail_messages ORDER BY id DESC LIMIT ' . max(1, $limit))->fetchAll();
$s = $pdo->prepare('SELECT * FROM plugin_rundmail_messages WHERE sender_id = ? ORDER BY id DESC LIMIT ' . max(1, $limit));
$s->execute([(int) $staff['id']]);
return $s->fetchAll();
}