*/ 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 */ 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 */ 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 = '
' . nl2br(e($text)) . '
' . '

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.

'; 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(); }