M6: Mailversand als nachvollziehbaren Versandjob mit Dry-Run/Log
mailversenden.php hatte keine Zugriffskontrolle, versendete bei jedem GET-Request sofort echte Mails und nutzte PHPMailer, dessen Quelldateien im Repo gar nicht vorhanden waren (nur composer.json/Lizenz) - der Aufruf waere also ohnehin mit Fatal Error abgebrochen. Zusaetzlich waren SMTP- Host, Absender, PayPal-Link und FAQ-URL fest auf einen Alt-Kunden (AOK) codiert. - Ersetzt PHPMailer durch die bestehende saas_send_mail()-Abstraktion aus M3 (Transport log/mail je nach APP_MAIL_TRANSPORT) statt eine fehlende Abhaengigkeit nachzuvendoren. - Neue Tabelle outbound_emails protokolliert jeden Versandversuch: Mandant, Mitglied, Vorlage, Betreff, Status, Fehler - das Versandlog. - Formular hat eine standardmaessig aktive Dry-Run-Checkbox; im Dry-Run wird nur geloggt, saas_send_mail() nicht aufgerufen. - Mailtext ist jetzt tenant-generisch (Saldo, optionaler PayPal-Link nur wenn der Mandant PayPal aktiviert hat, eigener Dashboard-Link) statt hartcodierter Alt-Kunden-Inhalte. - Zugriffskontrolle ergaenzt (owner/admin/treasurer + Legacy-Fallback). - http-smoke.php: mailversenden.php ist jetzt reguel</EOF>
This commit is contained in:
@@ -131,6 +131,85 @@ function saas_send_password_reset_mail(string $to, string $token): array
|
||||
return saas_send_mail($to, 'Kaffeeliste Passwort zurücksetzen', $body);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the tenant-generic balance notification body. Replaces the old
|
||||
* mailversenden.php text, which hardcoded one customer's SMTP host,
|
||||
* PayPal link and FAQ URL and therefore only ever worked for that one
|
||||
* tenant.
|
||||
*/
|
||||
function saas_render_balance_mail_body(string $displayName, int $balanceCents, array $tenantSettings, string $dashboardUrl): string
|
||||
{
|
||||
$amount = saas_format_money_cents(abs($balanceCents)) . ' €';
|
||||
$lines = ["Hallo {$displayName},", ''];
|
||||
|
||||
if ($balanceCents < 0) {
|
||||
$lines[] = "dein aktueller Kaffeelisten-Stand ist im Minus: {$amount}.";
|
||||
$lines[] = "Bitte gleiche den Betrag bei Gelegenheit aus.";
|
||||
if ((int)($tenantSettings['paypal_enabled'] ?? 0) === 1 && trim((string)($tenantSettings['paypal_url_template'] ?? '')) !== '') {
|
||||
$paypalLink = trim((string)$tenantSettings['paypal_url_template']) . number_format(abs($balanceCents) / 100, 2, '.', '');
|
||||
$lines[] = '';
|
||||
$lines[] = "Direkt bezahlen: {$paypalLink}";
|
||||
}
|
||||
} else {
|
||||
$lines[] = "dein aktuelles Guthaben in der Kaffeeliste beträgt {$amount}.";
|
||||
}
|
||||
|
||||
$lines[] = '';
|
||||
$lines[] = "Deinen aktuellen Stand findest du hier: {$dashboardUrl}";
|
||||
$lines[] = '';
|
||||
$lines[] = 'Deine Kaffeeliste';
|
||||
|
||||
return implode("\n", $lines);
|
||||
}
|
||||
|
||||
function saas_log_outbound_email(
|
||||
PDO $pdo,
|
||||
int $tenantId,
|
||||
?int $participantId,
|
||||
string $template,
|
||||
string $subject,
|
||||
string $status,
|
||||
?string $error,
|
||||
?int $createdByUserId
|
||||
): int {
|
||||
$stmt = $pdo->prepare(
|
||||
'INSERT INTO outbound_emails
|
||||
(tenant_id, participant_id, template, subject, status, sent_at, error, created_by_user_id)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)'
|
||||
);
|
||||
$stmt->execute([
|
||||
$tenantId,
|
||||
$participantId,
|
||||
$template,
|
||||
$subject,
|
||||
$status,
|
||||
in_array($status, ['sent', 'failed'], true) ? date('Y-m-d H:i:s') : null,
|
||||
$error,
|
||||
$createdByUserId,
|
||||
]);
|
||||
|
||||
return (int)$pdo->lastInsertId();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array{id: int, participant_id: ?int, display_name: ?string, template: string, subject: string, status: string, sent_at: ?string, error: ?string, created_at: string}>
|
||||
*/
|
||||
function saas_fetch_outbound_email_log(PDO $pdo, int $tenantId, int $limit = 50): array
|
||||
{
|
||||
$limit = max(1, min($limit, 200));
|
||||
$stmt = $pdo->prepare(
|
||||
"SELECT o.id, o.participant_id, p.display_name, o.template, o.subject, o.status, o.sent_at, o.error, o.created_at
|
||||
FROM outbound_emails o
|
||||
LEFT JOIN participants p ON p.id = o.participant_id
|
||||
WHERE o.tenant_id = ?
|
||||
ORDER BY o.created_at DESC, o.id DESC
|
||||
LIMIT {$limit}"
|
||||
);
|
||||
$stmt->execute([$tenantId]);
|
||||
|
||||
return $stmt->fetchAll();
|
||||
}
|
||||
|
||||
function saas_send_invite_mail(string $to, string $tenantName, string $role, string $token): array
|
||||
{
|
||||
$link = saas_app_url('passwort-zuruecksetzen.php?token=' . urlencode($token));
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
CREATE TABLE IF NOT EXISTS outbound_emails (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
tenant_id INT NOT NULL,
|
||||
participant_id INT NULL,
|
||||
template VARCHAR(60) NOT NULL,
|
||||
subject VARCHAR(255) NOT NULL,
|
||||
status VARCHAR(30) NOT NULL,
|
||||
sent_at DATETIME NULL,
|
||||
error VARCHAR(500) NULL,
|
||||
created_by_user_id INT NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
KEY idx_outbound_emails_tenant_created (tenant_id, created_at),
|
||||
KEY idx_outbound_emails_participant (participant_id),
|
||||
CONSTRAINT fk_outbound_emails_tenant
|
||||
FOREIGN KEY (tenant_id) REFERENCES tenants(id)
|
||||
ON DELETE CASCADE,
|
||||
CONSTRAINT fk_outbound_emails_participant
|
||||
FOREIGN KEY (participant_id) REFERENCES participants(id)
|
||||
ON DELETE SET NULL,
|
||||
CONSTRAINT fk_outbound_emails_created_by_user
|
||||
FOREIGN KEY (created_by_user_id) REFERENCES users(id)
|
||||
ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
@@ -99,10 +99,60 @@ Live getestet: gültiges zweiseitiges PDF (58 KB) mit korrektem
|
||||
stichprobenartig verifiziert (unkomprimierte Testausgabe), Vieltrinker-/
|
||||
Wenigtrinker-Aufteilung stimmt mit den Ledger-Daten überein.
|
||||
|
||||
## Mailversand
|
||||
|
||||
Umgesetzte Dateien:
|
||||
|
||||
```text
|
||||
database/migrations/0009_saas_outbound_emails.sql
|
||||
app/saas-mail.php (Vorlage + Versandlog)
|
||||
mailversenden.php
|
||||
scripts/http-smoke.php
|
||||
```
|
||||
|
||||
Ziel: Nachvollziehbarer Versandjob mit Dry-Run und Versandlog statt
|
||||
sofortigem, ungeprüftem Versand.
|
||||
|
||||
Umfang:
|
||||
|
||||
- `mailversenden.php` hatte zuvor keine Zugriffskontrolle, versendete auf
|
||||
jeden GET-Request sofort echte E-Mails und nutzte PHPMailer, dessen
|
||||
Quelldateien im Repo gar nicht vorhanden sind (nur `composer.json` und
|
||||
Lizenzdateien) – der Aufruf wäre also ohnehin mit einem Fatal Error
|
||||
abgebrochen. Zusätzlich waren SMTP-Host, Absender, PayPal-Link und
|
||||
FAQ-URL fest auf einen einzelnen Alt-Kunden (AOK) codiert.
|
||||
- Statt PHPMailer zu vendoren, nutzt der Versand jetzt die in M3 bereits
|
||||
gebaute Mail-Abstraktion (`saas_send_mail()`), die je nach
|
||||
`APP_MAIL_TRANSPORT` entweder wirklich per `mail()` verschickt oder
|
||||
(Standard im Dev-Modus) in `var/mail` protokolliert. Damit entfällt die
|
||||
fehlende Abhängigkeit vollständig, und "Dry-Run" beziehungsweise
|
||||
"Log" sind bereits strukturell dieselbe Mechanik.
|
||||
- Neue Tabelle `outbound_emails` protokolliert jeden Versandversuch:
|
||||
Mandant, Mitglied, Vorlage, Betreff, Status (`dry_run`/`sent`/`failed`),
|
||||
Zeitpunkt, Fehlermeldung. Das ist das im Plan geforderte Versandlog.
|
||||
- Echter zweistufiger Schutz: Das Formular hat eine standardmäßig aktive
|
||||
"Dry-Run"-Checkbox. Im Dry-Run wird für jeden Empfänger ein Log-Eintrag
|
||||
geschrieben, aber `saas_send_mail()` nicht aufgerufen. Erst mit
|
||||
deaktivierter Checkbox wird wirklich versendet.
|
||||
- Der Mailtext ist jetzt tenant-generisch (`saas_render_balance_mail_body()`):
|
||||
Saldo, optionaler PayPal-Link nur wenn der Mandant PayPal aktiviert hat,
|
||||
Link zum eigenen Dashboard über `saas_app_url()`. Keine hartcodierten
|
||||
Alt-Kunden-Inhalte mehr.
|
||||
- Zugriffskontrolle ergänzt (owner/admin/treasurer + Legacy-Fallback).
|
||||
- `scripts/http-smoke.php`: `mailversenden.php` ist jetzt ein regulärer
|
||||
Check statt eines übersprungenen unsicheren GET-Aufrufs, da GET keine
|
||||
Seiteneffekte mehr hat.
|
||||
|
||||
Live getestet: GET zeigt nur das Formular (kein Versand), Dry-Run
|
||||
protokolliert alle aktiven Mitglieder ohne `var/mail`-Dateien zu erzeugen,
|
||||
Live-Versand erzeugt für jeden Empfänger eine Log-Datei mit korrekt
|
||||
personalisiertem Inhalt (Guthaben- und Schuldenfall geprüft), Versandlog in
|
||||
der UI zeigt die Einträge. Testdaten anschließend entfernt.
|
||||
|
||||
## Prüfstatus
|
||||
|
||||
- Golden Master: grün mit 104 Assertions.
|
||||
- M4 Ledger-Migration: grün mit 73 Assertions.
|
||||
- M4 Ledger-Service: grün mit 115 Assertions.
|
||||
- HTTP-Smoke: grün mit 24 geprüften Seiten (PDF-Export jetzt regulär statt
|
||||
bekannter offener Punkt).
|
||||
- HTTP-Smoke: grün mit 25 geprüften Seiten (PDF-Export und Mailversand
|
||||
jetzt regulär statt bekannter offener Punkte).
|
||||
|
||||
@@ -306,7 +306,7 @@ Nicht tun:
|
||||
| M3 | SaaS-Basis | Abgeschlossen: Tenants, User, Registrierung, Login, Rollen, Mail-Links und zentrale Mandantenauswahl funktionieren |
|
||||
| M4 | Datenmigration | Gestartet: Ledger-Tabelle, Legacy-Backfill, Paritätscheck, Ledger-Service und Preview sind umgesetzt |
|
||||
| M5 | App-Kern | Weit fortgeschritten: Kernseiten lesen und schreiben tenant-sicher gegen das Ledger, inklusive Hinweise und rollenbasiertem Zugang; offen ist ein eigener Zahlungs-Screen |
|
||||
| M6 | Betriebsflows | Gestartet: CSV-Import und PDF-Export stehen; Mail und Jahresprozesse offen |
|
||||
| M6 | Betriebsflows | Weit fortgeschritten: CSV-Import, PDF-Export und Mailversand stehen; Jahresprozesse offen |
|
||||
| M7 | Landingpage | Public-Seite und Auth-Seiten sind im gemeinsamen Stil nutzbar; spätere Ausbaustufen folgen |
|
||||
| M8 | Härtung | Betrieb, Datenschutz, Monitoring und Isolation sind geprüft |
|
||||
| M9 | Cutover | Produktivumstellung ist vorbereitet und Legacy ist read-only |
|
||||
@@ -579,7 +579,12 @@ Schritte:
|
||||
Zugriffskontrolle; das ist behoben, die Seite liest jetzt tenant-sicher
|
||||
aus dem Ledger.
|
||||
- Mailversand als nachvollziehbaren Versandjob mit Dry-Run und Versandlog
|
||||
gestalten.
|
||||
gestalten: erledigt. `mailversenden.php` versendete zuvor ungeprüft bei
|
||||
jedem GET-Request echte Mails über PHPMailer (dessen Quelldateien im Repo
|
||||
gar nicht vorhanden waren) mit fest codierten Alt-Kunden-Inhalten (SMTP,
|
||||
PayPal-Link, FAQ-URL). Ersetzt durch die bestehende `saas_send_mail()`-
|
||||
Abstraktion aus M3, eine neue `outbound_emails`-Tabelle als Versandlog
|
||||
und eine standardmäßig aktive Dry-Run-Option.
|
||||
- Jahresauswertung beziehungsweise Jahresbuchungen tenant-sicher abbilden.
|
||||
|
||||
Ergebnis:
|
||||
|
||||
+120
-109
@@ -1,6 +1,9 @@
|
||||
<?php
|
||||
|
||||
include "functions.php";
|
||||
require_once __DIR__ . "/app/ledger.php";
|
||||
require_once __DIR__ . "/app/saas-mail.php";
|
||||
app_require_csrf();
|
||||
include "header.php";
|
||||
include "headerline.php";
|
||||
include "nav.php";
|
||||
@@ -15,128 +18,136 @@ include "nav.php";
|
||||
<div class="content">
|
||||
|
||||
<?php
|
||||
use PHPMailer\PHPMailer\PHPMailer;
|
||||
use PHPMailer\PHPMailer\SMTP;
|
||||
use PHPMailer\PHPMailer\Exception;
|
||||
|
||||
require 'PHPMailer/src/Exception.php';
|
||||
require 'PHPMailer/src/PHPMailer.php';
|
||||
require 'PHPMailer/src/SMTP.php';
|
||||
$pdo = app_db_pdo();
|
||||
$saasUser = saas_current_user($pdo);
|
||||
$tenantId = 0;
|
||||
$hasAccess = false;
|
||||
|
||||
|
||||
// Funktion zum Berechnen des Gesamtstands
|
||||
function berechneGesamtstand($mitarbeiterID, $conn) {
|
||||
// Kosten aus Kaffeeverbrauch abrufen
|
||||
$sqlKosten = "SELECT SUM(Kosten) AS GesamtKosten FROM kl_Kaffeeverbrauch WHERE MitarbeiterID = ?";
|
||||
$paramsKosten = array($mitarbeiterID);
|
||||
$stmtKosten = sqlsrv_query($conn, $sqlKosten, $paramsKosten);
|
||||
|
||||
if ($stmtKosten === false) {
|
||||
die(print_r(sqlsrv_errors(), true));
|
||||
}
|
||||
|
||||
$rowKosten = sqlsrv_fetch_array($stmtKosten, SQLSRV_FETCH_ASSOC);
|
||||
$gesamtKosten = $rowKosten['GesamtKosten'];
|
||||
|
||||
// Einzahlungen abrufen
|
||||
$sqlEinzahlungen = "SELECT SUM(Betrag) AS GesamtEinzahlungen FROM kl_Einzahlungen WHERE MitarbeiterID = ?";
|
||||
$paramsEinzahlungen = array($mitarbeiterID);
|
||||
$stmtEinzahlungen = sqlsrv_query($conn, $sqlEinzahlungen, $paramsEinzahlungen);
|
||||
|
||||
if ($stmtEinzahlungen === false) {
|
||||
die(print_r(sqlsrv_errors(), true));
|
||||
}
|
||||
|
||||
$rowEinzahlungen = sqlsrv_fetch_array($stmtEinzahlungen, SQLSRV_FETCH_ASSOC);
|
||||
$gesamtEinzahlungen = $rowEinzahlungen['GesamtEinzahlungen'];
|
||||
|
||||
// Gesamtstand berechnen (Kosten - Einzahlungen)
|
||||
$gesamtstand = $gesamtKosten - $gesamtEinzahlungen;
|
||||
|
||||
return $gesamtstand;
|
||||
if ($saasUser !== null && saas_user_has_role(['owner', 'admin', 'treasurer'], $saasUser)) {
|
||||
$tenantId = (int)$saasUser['tenant_id'];
|
||||
$hasAccess = true;
|
||||
}
|
||||
|
||||
// SQL-Abfrage für alle Mitarbeiter
|
||||
$sqlMitarbeiter = "SELECT MitarbeiterID, Name, Email FROM kl_Mitarbeiter WHERE aktiv='1'";
|
||||
$stmtMitarbeiter = sqlsrv_query($conn, $sqlMitarbeiter);
|
||||
|
||||
// E-Mail-Versand für jeden Mitarbeiter
|
||||
while ($row = sqlsrv_fetch_array($stmtMitarbeiter, SQLSRV_FETCH_ASSOC)) {
|
||||
$mitarbeiterID = $row['MitarbeiterID'];
|
||||
$name = trim($row['Name']);
|
||||
$email = $row['Email'];
|
||||
// Gesamtstand berechnen
|
||||
$gesamtstand = berechneGesamtstand($mitarbeiterID, $conn);
|
||||
$stand = abs($gesamtstand);
|
||||
$body = "Hallo $name,<br><br>";
|
||||
|
||||
if($gesamtstand > 0){
|
||||
$body .= "hier ist dein aktueller negativer Kaffeekassenstand. <br>
|
||||
Bitte zahle folgenden Betrag in die Kaffeekasse ein: {$stand} Euro.<br>
|
||||
<br>Es kann nur noch per Paypal bezahlt werden.<br>
|
||||
Dein Link: https://www.paypal.me/kaffeekassesb3/{$stand}<br>
|
||||
<b>Bezahle immer über die Freunde-Funktion von Paypal. Ansonsten stellen wir 20% des eingezahlten Betrags als Gebühr in Rechnung.</b><br>
|
||||
";
|
||||
|
||||
}else{
|
||||
$body .= "hier ist dein aktuelles Guthaben in der Kaffeeliste: {$stand} Euro.<br>
|
||||
<br>";
|
||||
if (!$hasAccess && $saasUser === null && checkKaffeelisteAdmin($conn, $mailadress)) {
|
||||
$tenant = ledger_fetch_default_tenant($pdo);
|
||||
if ($tenant !== null) {
|
||||
$tenantId = (int)$tenant['id'];
|
||||
$hasAccess = true;
|
||||
}
|
||||
|
||||
$body .= "
|
||||
Deinen aktuellen Stand findest du auch hier: <a href='https://kaffeeliste.arge.aok/'>https://kaffeeliste.arge.aok/</a><br>
|
||||
<br>
|
||||
<b>Hast du Fragen oder Probleme? Dann lies immer zu erst unsere <a href=https://kaffeeliste.arge.aok/faq.php>FAQs</a>. Danke! </b><br>
|
||||
<br>
|
||||
Vielen Dank!<br>
|
||||
Deine Kaffeekasse<br>
|
||||
<br>
|
||||
Dein Guthaben ist auf dem nächsten Ausdruck der Kaffeeliste einsehbar. Um die Umwelt zu schonen erfolgt ein neuer Ausdruck erst, wenn die Liste voll ist.<br>
|
||||
";
|
||||
|
||||
|
||||
|
||||
// PHPMailer initialisieren
|
||||
$mail = new PHPMailer(true);
|
||||
}
|
||||
|
||||
try {
|
||||
//Server settings
|
||||
$mail->SMTPDebug = SMTP::DEBUG_OFF; // Disable debugging
|
||||
$mail->Timeout = 120;
|
||||
|
||||
$mail->isSMTP();
|
||||
$mail->Host = 'smtpv.aoknds.aok'; // SMTP server
|
||||
|
||||
## Hier wird absichtlich keine Authentifizierung genutzt.
|
||||
|
||||
|
||||
#$mail->SMTPAuth = true;
|
||||
#$mail->Username = ''; // SMTP username
|
||||
#$mail->Password = ''; // SMTP password
|
||||
#$mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS; // Enable TLS encryption; `PHPMailer::ENCRYPTION_SMTPS` also accepted
|
||||
#$mail->Port = 587; // TCP port to connect to
|
||||
if (!$hasAccess) {
|
||||
echo "<h2>Kein Zugriff</h2>";
|
||||
include "footer.php";
|
||||
exit;
|
||||
}
|
||||
|
||||
//Recipients
|
||||
$mail->setFrom('kaffeelistesb3@nds.aok.de', 'Kaffeeliste ARGE');
|
||||
$mail->addAddress($email, $name); // Add a recipient
|
||||
$ergebnisse = null;
|
||||
$dryRun = true;
|
||||
|
||||
// Content
|
||||
$mail->isHTML(true); // Set email format to HTML
|
||||
$mail->Subject = "Kaffeeliste ARGE - Dein Stand";
|
||||
$mail->Body = utf8_decode($body);
|
||||
if ($_SERVER["REQUEST_METHOD"] === "POST") {
|
||||
$dryRun = !empty($_POST['dry_run']);
|
||||
$settings = saas_fetch_tenant_settings($pdo, $tenantId);
|
||||
$dashboardUrl = saas_app_url('index.php');
|
||||
$createdByUserId = $saasUser['user_id'] ?? null;
|
||||
|
||||
$mail->send();
|
||||
echo "E-Mail erfolgreich an $name gesendet.<br>";
|
||||
} catch (Exception $e) {
|
||||
echo "E-Mail konnte nicht gesendet werden. Fehler: {$mail->ErrorInfo}<br>";
|
||||
}
|
||||
$teilnehmer = ledger_fetch_participant_summaries($pdo, $tenantId, ['active_only' => true]);
|
||||
$ergebnisse = [];
|
||||
|
||||
|
||||
foreach ($teilnehmer as $person) {
|
||||
$email = trim((string)($person['email'] ?? ''));
|
||||
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||
$ergebnisse[] = ['name' => $person['display_name'], 'email' => $email, 'status' => 'ohne_email'];
|
||||
continue;
|
||||
}
|
||||
|
||||
$subject = 'Kaffeeliste - Dein aktueller Stand';
|
||||
$body = saas_render_balance_mail_body(
|
||||
$person['display_name'],
|
||||
(int)$person['balance_cents'],
|
||||
$settings,
|
||||
$dashboardUrl
|
||||
);
|
||||
|
||||
if ($dryRun) {
|
||||
saas_log_outbound_email($pdo, $tenantId, $person['participant_id'], 'balance_notice', $subject, 'dry_run', null, $createdByUserId);
|
||||
$ergebnisse[] = ['name' => $person['display_name'], 'email' => $email, 'status' => 'dry_run'];
|
||||
continue;
|
||||
}
|
||||
|
||||
$sendResult = saas_send_mail($email, $subject, $body);
|
||||
if ($sendResult['ok']) {
|
||||
saas_log_outbound_email($pdo, $tenantId, $person['participant_id'], 'balance_notice', $subject, 'sent', null, $createdByUserId);
|
||||
$ergebnisse[] = ['name' => $person['display_name'], 'email' => $email, 'status' => 'sent'];
|
||||
} else {
|
||||
saas_log_outbound_email($pdo, $tenantId, $person['participant_id'], 'balance_notice', $subject, 'failed', $sendResult['error'] ?? 'unbekannter Fehler', $createdByUserId);
|
||||
$ergebnisse[] = ['name' => $person['display_name'], 'email' => $email, 'status' => 'failed'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$log = saas_fetch_outbound_email_log($pdo, $tenantId, 20);
|
||||
|
||||
function mailversenden_status_label(string $status): string
|
||||
{
|
||||
return match ($status) {
|
||||
'sent' => 'Versendet',
|
||||
'failed' => 'Fehlgeschlagen',
|
||||
'dry_run' => 'Dry-Run (nicht versendet)',
|
||||
'ohne_email' => 'Keine E-Mail-Adresse',
|
||||
default => $status,
|
||||
};
|
||||
}
|
||||
|
||||
?>
|
||||
|
||||
</div>
|
||||
<h2>Info-Mail versenden</h2>
|
||||
<p>Verschickt an alle aktiven Mitglieder eine E-Mail mit dem aktuellen
|
||||
Kaffeelisten-Stand. Im Dry-Run wird nichts wirklich versendet, aber jeder
|
||||
Empfänger wird im Versandlog unten protokolliert.</p>
|
||||
|
||||
<?php if ($ergebnisse !== null): ?>
|
||||
<h3>Ergebnis dieses Laufs (<?php echo $dryRun ? 'Dry-Run' : 'Live-Versand'; ?>)</h3>
|
||||
<table>
|
||||
<tr><th>Name</th><th>E-Mail</th><th>Status</th></tr>
|
||||
<?php foreach ($ergebnisse as $eintrag): ?>
|
||||
<tr>
|
||||
<td><?php echo saas_html($eintrag['name']); ?></td>
|
||||
<td><?php echo saas_html($eintrag['email']); ?></td>
|
||||
<td><?php echo saas_html(mailversenden_status_label($eintrag['status'])); ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</table>
|
||||
<?php endif; ?>
|
||||
|
||||
<form method="post" action="<?php echo htmlspecialchars($_SERVER["PHP_SELF"]); ?>">
|
||||
<?php echo app_csrf_field(); ?>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" name="dry_run" id="dry_run" checked>
|
||||
<label class="form-check-label" for="dry_run">Dry-Run (nichts wirklich versenden, nur protokollieren)</label>
|
||||
</div>
|
||||
<button type="submit">Info-Mail versenden</button>
|
||||
</form>
|
||||
|
||||
<h3>Versandlog (letzte 20)</h3>
|
||||
<table>
|
||||
<tr><th>Datum</th><th>Mitglied</th><th>Betreff</th><th>Status</th><th>Fehler</th></tr>
|
||||
<?php if ($log === []): ?>
|
||||
<tr><td colspan="5">Noch kein Versand protokolliert.</td></tr>
|
||||
<?php endif; ?>
|
||||
<?php foreach ($log as $eintrag): ?>
|
||||
<tr>
|
||||
<td><?php echo saas_html($eintrag['created_at']); ?></td>
|
||||
<td><?php echo saas_html($eintrag['display_name'] ?? '—'); ?></td>
|
||||
<td><?php echo saas_html($eintrag['subject']); ?></td>
|
||||
<td><?php echo saas_html(mailversenden_status_label($eintrag['status'])); ?></td>
|
||||
<td><?php echo saas_html($eintrag['error'] ?? ''); ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<?php include "footer.php"; ?>
|
||||
<?php include "footer.php"; ?>
|
||||
|
||||
@@ -137,16 +137,17 @@ $checks = [
|
||||
'path' => 'exportKaffeeliste.php',
|
||||
'contains' => ['%PDF'],
|
||||
],
|
||||
[
|
||||
'label' => 'Mailversand',
|
||||
'path' => 'mailversenden.php',
|
||||
'contains' => ['Info-Mail versenden', 'Dry-Run', 'Versandlog'],
|
||||
],
|
||||
];
|
||||
|
||||
$knownIssueChecks = [
|
||||
];
|
||||
|
||||
$skippedUnsafe = [
|
||||
[
|
||||
'path' => 'mailversenden.php',
|
||||
'reason' => 'GET kann bei vollständiger PHPMailer-Installation E-Mails versenden.',
|
||||
],
|
||||
[
|
||||
'path' => 'jahresauswertung.php',
|
||||
'reason' => 'GET kann Jahresbuchungen schreiben und E-Mails versenden; PHPMailer-Abhängigkeit ist zudem unvollständig.',
|
||||
|
||||
Reference in New Issue
Block a user