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:
2026-07-15 16:13:38 +02:00
parent 81dfff51a0
commit d5195e7f9f
6 changed files with 286 additions and 117 deletions
+79
View File
@@ -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); 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 function saas_send_invite_mail(string $to, string $tenantName, string $role, string $token): array
{ {
$link = saas_app_url('passwort-zuruecksetzen.php?token=' . urlencode($token)); $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;
+52 -2
View File
@@ -99,10 +99,60 @@ Live getestet: gültiges zweiseitiges PDF (58&nbsp;KB) mit korrektem
stichprobenartig verifiziert (unkomprimierte Testausgabe), Vieltrinker-/ stichprobenartig verifiziert (unkomprimierte Testausgabe), Vieltrinker-/
Wenigtrinker-Aufteilung stimmt mit den Ledger-Daten überein. 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 ## Prüfstatus
- Golden Master: grün mit 104 Assertions. - Golden Master: grün mit 104 Assertions.
- M4 Ledger-Migration: grün mit 73 Assertions. - M4 Ledger-Migration: grün mit 73 Assertions.
- M4 Ledger-Service: grün mit 115 Assertions. - M4 Ledger-Service: grün mit 115 Assertions.
- HTTP-Smoke: grün mit 24 geprüften Seiten (PDF-Export jetzt regulär statt - HTTP-Smoke: grün mit 25 geprüften Seiten (PDF-Export und Mailversand
bekannter offener Punkt). jetzt regulär statt bekannter offener Punkte).
+7 -2
View File
@@ -306,7 +306,7 @@ Nicht tun:
| M3 | SaaS-Basis | Abgeschlossen: Tenants, User, Registrierung, Login, Rollen, Mail-Links und zentrale Mandantenauswahl funktionieren | | 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 | | 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 | | 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 | | 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 | | M8 | Härtung | Betrieb, Datenschutz, Monitoring und Isolation sind geprüft |
| M9 | Cutover | Produktivumstellung ist vorbereitet und Legacy ist read-only | | 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 Zugriffskontrolle; das ist behoben, die Seite liest jetzt tenant-sicher
aus dem Ledger. aus dem Ledger.
- Mailversand als nachvollziehbaren Versandjob mit Dry-Run und Versandlog - 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. - Jahresauswertung beziehungsweise Jahresbuchungen tenant-sicher abbilden.
Ergebnis: Ergebnis:
+114 -103
View File
@@ -1,6 +1,9 @@
<?php <?php
include "functions.php"; include "functions.php";
require_once __DIR__ . "/app/ledger.php";
require_once __DIR__ . "/app/saas-mail.php";
app_require_csrf();
include "header.php"; include "header.php";
include "headerline.php"; include "headerline.php";
include "nav.php"; include "nav.php";
@@ -15,128 +18,136 @@ include "nav.php";
<div class="content"> <div class="content">
<?php <?php
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\SMTP;
use PHPMailer\PHPMailer\Exception;
require 'PHPMailer/src/Exception.php'; $pdo = app_db_pdo();
require 'PHPMailer/src/PHPMailer.php'; $saasUser = saas_current_user($pdo);
require 'PHPMailer/src/SMTP.php'; $tenantId = 0;
$hasAccess = false;
if ($saasUser !== null && saas_user_has_role(['owner', 'admin', 'treasurer'], $saasUser)) {
// Funktion zum Berechnen des Gesamtstands $tenantId = (int)$saasUser['tenant_id'];
function berechneGesamtstand($mitarbeiterID, $conn) { $hasAccess = true;
// 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;
} }
// SQL-Abfrage für alle Mitarbeiter if (!$hasAccess && $saasUser === null && checkKaffeelisteAdmin($conn, $mailadress)) {
$sqlMitarbeiter = "SELECT MitarbeiterID, Name, Email FROM kl_Mitarbeiter WHERE aktiv='1'"; $tenant = ledger_fetch_default_tenant($pdo);
$stmtMitarbeiter = sqlsrv_query($conn, $sqlMitarbeiter); if ($tenant !== null) {
$tenantId = (int)$tenant['id'];
// E-Mail-Versand für jeden Mitarbeiter $hasAccess = true;
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>";
} }
}
$body .= " if (!$hasAccess) {
Deinen aktuellen Stand findest du auch hier: <a href='https://kaffeeliste.arge.aok/'>https://kaffeeliste.arge.aok/</a><br> echo "<h2>Kein Zugriff</h2>";
<br> include "footer.php";
<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> exit;
<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>
";
$ergebnisse = null;
$dryRun = true;
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;
// PHPMailer initialisieren $teilnehmer = ledger_fetch_participant_summaries($pdo, $tenantId, ['active_only' => true]);
$mail = new PHPMailer(true); $ergebnisse = [];
try { foreach ($teilnehmer as $person) {
//Server settings $email = trim((string)($person['email'] ?? ''));
$mail->SMTPDebug = SMTP::DEBUG_OFF; // Disable debugging if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
$mail->Timeout = 120; $ergebnisse[] = ['name' => $person['display_name'], 'email' => $email, 'status' => 'ohne_email'];
continue;
}
$mail->isSMTP(); $subject = 'Kaffeeliste - Dein aktueller Stand';
$mail->Host = 'smtpv.aoknds.aok'; // SMTP server $body = saas_render_balance_mail_body(
$person['display_name'],
(int)$person['balance_cents'],
$settings,
$dashboardUrl
);
## Hier wird absichtlich keine Authentifizierung genutzt. 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'];
}
}
}
#$mail->SMTPAuth = true; $log = saas_fetch_outbound_email_log($pdo, $tenantId, 20);
#$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
//Recipients
$mail->setFrom('kaffeelistesb3@nds.aok.de', 'Kaffeeliste ARGE');
$mail->addAddress($email, $name); // Add a recipient
// Content
$mail->isHTML(true); // Set email format to HTML
$mail->Subject = "Kaffeeliste ARGE - Dein Stand";
$mail->Body = utf8_decode($body);
$mail->send();
echo "E-Mail erfolgreich an $name gesendet.<br>";
} catch (Exception $e) {
echo "E-Mail konnte nicht gesendet werden. Fehler: {$mail->ErrorInfo}<br>";
}
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> </section>
<?php include "footer.php"; ?> <?php include "footer.php"; ?>
+5 -4
View File
@@ -137,16 +137,17 @@ $checks = [
'path' => 'exportKaffeeliste.php', 'path' => 'exportKaffeeliste.php',
'contains' => ['%PDF'], 'contains' => ['%PDF'],
], ],
[
'label' => 'Mailversand',
'path' => 'mailversenden.php',
'contains' => ['Info-Mail versenden', 'Dry-Run', 'Versandlog'],
],
]; ];
$knownIssueChecks = [ $knownIssueChecks = [
]; ];
$skippedUnsafe = [ $skippedUnsafe = [
[
'path' => 'mailversenden.php',
'reason' => 'GET kann bei vollständiger PHPMailer-Installation E-Mails versenden.',
],
[ [
'path' => 'jahresauswertung.php', 'path' => 'jahresauswertung.php',
'reason' => 'GET kann Jahresbuchungen schreiben und E-Mails versenden; PHPMailer-Abhängigkeit ist zudem unvollständig.', 'reason' => 'GET kann Jahresbuchungen schreiben und E-Mails versenden; PHPMailer-Abhängigkeit ist zudem unvollständig.',