221 lines
13 KiB
PHP
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();
|
|
}
|