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
+120 -109
View File
@@ -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"; ?>