", "Reply-To: {$from}", 'Content-Type: text/plain; charset=UTF-8', ]); } function saas_log_mail(string $to, string $subject, string $body): array { $logDir = (string)app_env('APP_MAIL_LOG_DIR', APP_ROOT . '/var/mail'); if (!is_dir($logDir)) { @mkdir($logDir, 0700, true); } if (!is_dir($logDir) || !is_writable($logDir)) { return [ 'ok' => false, 'transport' => 'log', 'error' => 'mail log directory is not writable', ]; } $file = rtrim($logDir, '/') . '/mail_' . date('Ymd_His') . '_' . bin2hex(random_bytes(6)) . '.txt'; $content = "To: {$to}\nSubject: {$subject}\n\n{$body}\n"; if (file_put_contents($file, $content) === false) { return [ 'ok' => false, 'transport' => 'log', 'error' => 'mail log write failed', ]; } return [ 'ok' => true, 'transport' => 'log', 'path' => $file, ]; } function saas_send_mail(string $to, string $subject, string $body): array { $to = trim($to); if (!filter_var($to, FILTER_VALIDATE_EMAIL)) { return [ 'ok' => false, 'transport' => saas_mail_transport(), 'error' => 'invalid recipient', ]; } if (saas_mail_transport() === 'log') { return saas_log_mail($to, $subject, $body); } $sent = mail($to, $subject, $body, saas_mail_headers()); return [ 'ok' => $sent, 'transport' => 'mail', 'error' => $sent ? null : 'mail transport failed', ]; } function saas_send_password_reset_mail(string $to, string $token): array { $link = saas_app_url('passwort-zuruecksetzen.php?token=' . urlencode($token)); $body = implode("\n", [ 'Hallo,', '', 'für dein Kaffeeliste-Konto wurde ein Passwort-Reset angefordert.', 'Öffne diesen Link, um ein neues Passwort zu setzen:', '', $link, '', 'Der Link ist zeitlich begrenzt und kann nur einmal verwendet werden.', 'Falls du den Reset nicht angefordert hast, kannst du diese Nachricht ignorieren.', '', 'Deine Kaffeeliste', ]); 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 */ 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_render_year_end_bonus_mail_body(string $displayName, int $yearMarks, int $bonusCents, string $dashboardUrl): string { $bonus = saas_format_money_cents($bonusCents) . ' €'; $lines = [ "Hallo {$displayName},", '', "vielen Dank für deine Nutzung der Kaffeeliste in diesem Jahr!", "Du hast dieses Jahr {$yearMarks} Striche gemacht.", "Dafür wurden dir {$bonus} auf deinem Kaffeelisten-Konto gutgeschrieben.", '', "Deinen aktuellen Stand findest du hier: {$dashboardUrl}", '', 'Deine Kaffeeliste', ]; return implode("\n", $lines); } function saas_send_invite_mail(string $to, string $tenantName, string $role, string $token): array { $link = saas_app_url('passwort-zuruecksetzen.php?token=' . urlencode($token)); $body = implode("\n", [ 'Hallo,', '', "du wurdest für \"{$tenantName}\" in der Kaffeeliste als {$role} eingeladen.", 'Öffne diesen Link, um ein Passwort zu setzen und dich anzumelden:', '', $link, '', 'Der Link ist zeitlich begrenzt und kann nur einmal verwendet werden.', 'Falls du diese Einladung nicht erwartet hast, kannst du diese Nachricht ignorieren.', '', 'Deine Kaffeeliste', ]); return saas_send_mail($to, 'Einladung zur Kaffeeliste', $body); } function saas_send_email_verification_mail(string $to, string $token): array { $link = saas_app_url('email-verifizieren.php?token=' . urlencode($token)); $body = implode("\n", [ 'Hallo,', '', 'bitte bestätige deine E-Mail-Adresse für Kaffeeliste.', 'Öffne dazu diesen Link:', '', $link, '', 'Der Link ist zeitlich begrenzt und kann nur einmal verwendet werden.', '', 'Deine Kaffeeliste', ]); return saas_send_mail($to, 'Kaffeeliste E-Mail bestätigen', $body); }