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);
|
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;
|
||||||
@@ -99,10 +99,60 @@ Live getestet: gültiges zweiseitiges PDF (58 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).
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
+115
-104
@@ -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,127 +18,135 @@ 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'];
|
||||||
|
$hasAccess = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// E-Mail-Versand für jeden Mitarbeiter
|
if (!$hasAccess) {
|
||||||
while ($row = sqlsrv_fetch_array($stmtMitarbeiter, SQLSRV_FETCH_ASSOC)) {
|
echo "<h2>Kein Zugriff</h2>";
|
||||||
$mitarbeiterID = $row['MitarbeiterID'];
|
include "footer.php";
|
||||||
$name = trim($row['Name']);
|
exit;
|
||||||
$email = $row['Email'];
|
}
|
||||||
// Gesamtstand berechnen
|
|
||||||
$gesamtstand = berechneGesamtstand($mitarbeiterID, $conn);
|
|
||||||
$stand = abs($gesamtstand);
|
|
||||||
$body = "Hallo $name,<br><br>";
|
|
||||||
|
|
||||||
if($gesamtstand > 0){
|
$ergebnisse = null;
|
||||||
$body .= "hier ist dein aktueller negativer Kaffeekassenstand. <br>
|
$dryRun = true;
|
||||||
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{
|
if ($_SERVER["REQUEST_METHOD"] === "POST") {
|
||||||
$body .= "hier ist dein aktuelles Guthaben in der Kaffeeliste: {$stand} Euro.<br>
|
$dryRun = !empty($_POST['dry_run']);
|
||||||
<br>";
|
$settings = saas_fetch_tenant_settings($pdo, $tenantId);
|
||||||
|
$dashboardUrl = saas_app_url('index.php');
|
||||||
|
$createdByUserId = $saasUser['user_id'] ?? null;
|
||||||
|
|
||||||
|
$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;
|
||||||
}
|
}
|
||||||
|
|
||||||
$body .= "
|
$subject = 'Kaffeeliste - Dein aktueller Stand';
|
||||||
Deinen aktuellen Stand findest du auch hier: <a href='https://kaffeeliste.arge.aok/'>https://kaffeeliste.arge.aok/</a><br>
|
$body = saas_render_balance_mail_body(
|
||||||
<br>
|
$person['display_name'],
|
||||||
<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>
|
(int)$person['balance_cents'],
|
||||||
<br>
|
$settings,
|
||||||
Vielen Dank!<br>
|
$dashboardUrl
|
||||||
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>
|
|
||||||
";
|
|
||||||
|
|
||||||
|
if ($dryRun) {
|
||||||
|
saas_log_outbound_email($pdo, $tenantId, $person['participant_id'], 'balance_notice', $subject, 'dry_run', null, $createdByUserId);
|
||||||
// PHPMailer initialisieren
|
$ergebnisse[] = ['name' => $person['display_name'], 'email' => $email, 'status' => 'dry_run'];
|
||||||
$mail = new PHPMailer(true);
|
continue;
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
//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>";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$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,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
?>
|
?>
|
||||||
|
|
||||||
|
<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>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
@@ -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.',
|
||||||
|
|||||||
Reference in New Issue
Block a user