<?php
/**
 * cron/bounce_handler.php
 * -----------------------------------------------------------------------------
 * Liest Bounce-E-Mails aus allen **aktiven SMTP-Accounts** und legt – sofern
 * möglich – einen Eintrag in `email_bounces` an.  Bevorzugt wird IMAP über
 * die Bibliothek *php-imap/php-imap*; bei Fehlern oder wenn die Bibliothek
 * fehlt, erfolgt ein automatischer Fallback auf POP3 mit nativen `imap_*`
 * Funktionen.
 *
 * Highlights
 * ----------
 * • Läuft **ohne Composer** (POP3-Mode) – Composer wird nur für PhpImap benötigt  
 * • Durchgängiges **SQL-Logging** via `ErrorLogger::logSqlSuccess|logSqlError`  
 * • Je Account **Statistiken** (`IMAP_STATS` / `POP3_STATS`)  
 * • **Robuste Fehlerbehandlung** mit fatalem Logging (`BOUNCE_HANDLER_FATAL`)  
 * • Verhindert Constraint-Fehler: schreibt nur in `email_bounces`, wenn die
 *   E-Mail-Adresse im System (`recipients`) vorhanden ist
 *
 * Voraussetzungen
 * ---------------
 * • PHP-IMAP-Extension („imap“) aktiviert  
 * • Optional: *php-imap/php-imap* (Autoload via vendor/autoload.php)
 */

declare(strict_types=1);

// ============================================================================
//  Optionales Composer-Autoload (PhpImap) ------------------------------------
// ============================================================================
$autoload = __DIR__ . '/../vendor/autoload.php';
if (is_readable($autoload)) {
    require_once $autoload;            // PhpImap verfügbar, wenn installiert
}

// ============================================================================
//  System-Bootstrap -----------------------------------------------------------
// ============================================================================
require_once __DIR__ . '/../includes/init.php';
require_once __DIR__ . '/../includes/core/Mailer.php';

// ============================================================================
//  Hilfsfunktion: PDO-Aufrufe ausführen & loggen -----------------------------
// ============================================================================
/**
 * Führt einen PDO-Aufruf aus und loggt Erfolg oder Fehler.
 *
 * @template T
 * @param  string        $sql     Nur fürs Logging – muss nicht der exakte Query sein
 * @param  callable():T  $action  Closure, die den eigentlichen PDO-Call enthält
 * @return T                       Rückgabe des PDO-Calls
 * @throws Throwable               Re-throw der eigentlichen Exception
 */
function executeLoggedSql(string $sql, callable $action)
{
    try {
        $result = $action();
        ErrorLogger::logSqlSuccess($sql);
        return $result;
    } catch (Throwable $e) {
        ErrorLogger::logSqlError('bounce_handler | SQL: ' . $sql . ' | error: ' . $e->getMessage());
        throw $e;
    }
}

const BOUNCE_GUARD_WINDOW_MINUTES = 15;
const BOUNCE_GUARD_ALERT_COOLDOWN_MINUTES = 60;
const BOUNCE_GUARD_LAST_ALERT_KEY = 'bounce_guard_last_alert_at';
const BOUNCE_GUARD_MODE_KEY = 'bounce_guard_mode';
const BOUNCE_GUARD_SUMMARY_KEY = 'bounce_guard_summary';
const BOUNCE_GUARD_UPDATED_AT_KEY = 'bounce_guard_updated_at';
const BOUNCE_GUARD_TOTAL_THRESHOLD_KEY = 'bounce_guard_total_threshold';
const BOUNCE_GUARD_HARD_THRESHOLD_KEY = 'bounce_guard_hard_threshold';
const BOUNCE_GUARD_SOFT_THRESHOLD_KEY = 'bounce_guard_soft_threshold';
const BOUNCE_GUARD_SPAM_THRESHOLD_KEY = 'bounce_guard_spam_threshold';

function fetchSettingValue(PDO $db, string $key): ?string
{
    $stmt = $db->prepare('SELECT setting_value FROM settings WHERE setting_key = ? LIMIT 1');
    $stmt->execute([$key]);
    $value = $stmt->fetchColumn();

    return $value === false ? null : (string) $value;
}

function upsertSettingValue(PDO $db, string $key, string $value): void
{
    $updateStmt = $db->prepare('UPDATE settings SET setting_value = ?, updated_at = NOW() WHERE setting_key = ?');
    $updateStmt->execute([$value, $key]);

    if ($updateStmt->rowCount() > 0) {
        return;
    }

    $insertStmt = $db->prepare(
        'INSERT INTO settings (setting_key, setting_value, created_at, updated_at)
         VALUES (?, ?, NOW(), NOW())'
    );
    $insertStmt->execute([$key, $value]);
}

function fetchBounceGuardStats(PDO $db): array
{
    $stmt = $db->query(
        'SELECT COUNT(*) AS total_count,
                SUM(CASE WHEN bounce_type = "hard" THEN 1 ELSE 0 END) AS hard_count,
                SUM(CASE WHEN bounce_type = "soft" THEN 1 ELSE 0 END) AS soft_count,
                SUM(CASE WHEN bounce_type = "spam" THEN 1 ELSE 0 END) AS spam_count,
                MIN(logged_at) AS first_logged_at,
                MAX(logged_at) AS last_logged_at
           FROM email_bounces
          WHERE logged_at >= DATE_SUB(NOW(), INTERVAL ' . BOUNCE_GUARD_WINDOW_MINUTES . ' MINUTE)'
    );
    $row = $stmt->fetch(PDO::FETCH_ASSOC) ?: [];

    return [
        'window_minutes' => BOUNCE_GUARD_WINDOW_MINUTES,
        'total_count' => (int) ($row['total_count'] ?? 0),
        'hard_count' => (int) ($row['hard_count'] ?? 0),
        'soft_count' => (int) ($row['soft_count'] ?? 0),
        'spam_count' => (int) ($row['spam_count'] ?? 0),
        'first_logged_at' => (string) ($row['first_logged_at'] ?? ''),
        'last_logged_at' => (string) ($row['last_logged_at'] ?? ''),
    ];
}

function fetchBounceGuardThresholds(PDO $db): array
{
    $defaults = [
        'total_threshold' => 20,
        'hard_threshold' => 10,
        'soft_threshold' => 15,
        'spam_threshold' => 1,
    ];

    $stmt = $db->prepare(
        'SELECT setting_key, setting_value
           FROM settings
          WHERE setting_key IN (?, ?, ?, ?)'
    );
    $stmt->execute([
        BOUNCE_GUARD_TOTAL_THRESHOLD_KEY,
        BOUNCE_GUARD_HARD_THRESHOLD_KEY,
        BOUNCE_GUARD_SOFT_THRESHOLD_KEY,
        BOUNCE_GUARD_SPAM_THRESHOLD_KEY,
    ]);
    $settings = $stmt->fetchAll(PDO::FETCH_KEY_PAIR);

    return [
        'total_threshold' => max(1, (int) ($settings[BOUNCE_GUARD_TOTAL_THRESHOLD_KEY] ?? $defaults['total_threshold'])),
        'hard_threshold' => max(1, (int) ($settings[BOUNCE_GUARD_HARD_THRESHOLD_KEY] ?? $defaults['hard_threshold'])),
        'soft_threshold' => max(1, (int) ($settings[BOUNCE_GUARD_SOFT_THRESHOLD_KEY] ?? $defaults['soft_threshold'])),
        'spam_threshold' => max(1, (int) ($settings[BOUNCE_GUARD_SPAM_THRESHOLD_KEY] ?? $defaults['spam_threshold'])),
    ];
}

function determineBounceGuardState(array $stats, array $thresholds): array
{
    $mode = 'normal';
    $reasons = [];

    if ($stats['spam_count'] >= $thresholds['spam_threshold']) {
        $mode = 'blocked';
        $reasons[] = 'Spam-/Blocked-Bounces im Schwellwert';
    } else {
        if ($stats['hard_count'] >= $thresholds['hard_threshold']) {
            $mode = 'throttled';
            $reasons[] = 'Hard-Bounces im Schwellwert';
        }
        if ($stats['soft_count'] >= $thresholds['soft_threshold']) {
            $mode = 'throttled';
            $reasons[] = 'Soft-Bounces im Schwellwert';
        }
        if ($stats['total_count'] >= $thresholds['total_threshold']) {
            $mode = 'throttled';
            $reasons[] = 'Gesamt-Bounces im Schwellwert';
        }
    }

    if ($reasons === []) {
        $reasons[] = 'Keine aktive Bounce-Schutzmassnahme';
    }

    return [
        'mode' => $mode,
        'summary' => 'Modus=' . $mode
            . ' | Fenster=' . (int) $stats['window_minutes'] . 'min'
            . ' | Total=' . (int) $stats['total_count']
            . ' | Hard=' . (int) $stats['hard_count']
            . ' | Soft=' . (int) $stats['soft_count']
            . ' | Spam=' . (int) $stats['spam_count']
            . ' | Limits(T/H/S/SP)=' . (int) $thresholds['total_threshold']
            . '/' . (int) $thresholds['hard_threshold']
            . '/' . (int) $thresholds['soft_threshold']
            . '/' . (int) $thresholds['spam_threshold']
            . ' | Grund=' . implode(', ', $reasons),
    ];
}

function shouldTriggerBounceGuardAlert(array $stats, array $thresholds): bool
{
    $state = determineBounceGuardState($stats, $thresholds);
    return $state['mode'] !== 'normal';
}

function isBounceGuardAlertDue(PDO $db): bool
{
    $lastAlertAt = fetchSettingValue($db, BOUNCE_GUARD_LAST_ALERT_KEY);
    if ($lastAlertAt === null || $lastAlertAt === '') {
        return true;
    }

    $lastTs = strtotime($lastAlertAt);
    if ($lastTs === false) {
        return true;
    }

    return $lastTs <= (time() - (BOUNCE_GUARD_ALERT_COOLDOWN_MINUTES * 60));
}

function fetchActiveAdminRecipients(PDO $db): array
{
    $stmt = $db->query(
        "SELECT email, COALESCE(NULLIF(name, ''), email) AS display_name
           FROM admin_users
          WHERE role = 'admin'
            AND is_active = 1
            AND email <> ''
          ORDER BY email ASC"
    );

    return $stmt->fetchAll(PDO::FETCH_ASSOC);
}

function sendBounceGuardAlert(PDO $db, array $stats, array $thresholds): bool
{
    $adminRecipients = fetchActiveAdminRecipients($db);
    if ($adminRecipients === []) {
        ErrorLogger::log('BOUNCE_GUARD_ALERT: keine aktiven Admin-Empfaenger vorhanden');
        return false;
    }

    $config = include __DIR__ . '/../includes/config.php';
    $baseUrl = rtrim((string) ($config['app']['base_url'] ?? ''), '/');
    $bounceLogUrl = $baseUrl !== '' ? $baseUrl . '/admin/bounce_log.php' : '';
    $subject = '[MR] Bounce-Warnung: auffaellige Zustellfehler';
    $state = determineBounceGuardState($stats, $thresholds);

    $htmlBody = '<html><head><meta charset="UTF-8"></head><body>'
        . '<p>Der Bounce-Schutz hat eine auffaellige Anzahl von Zustellfehlern erkannt.</p>'
        . '<ul>'
        . '<li>Schutzmodus: ' . htmlspecialchars($state['mode'], ENT_QUOTES, 'UTF-8') . '</li>'
        . '<li>Zeitfenster: letzte ' . (int) $stats['window_minutes'] . ' Minuten</li>'
        . '<li>Gesamt-Bounces: ' . (int) $stats['total_count'] . '</li>'
        . '<li>Hard-Bounces: ' . (int) $stats['hard_count'] . '</li>'
        . '<li>Soft-Bounces: ' . (int) $stats['soft_count'] . '</li>'
        . '<li>Spam-/Blocked-Bounces: ' . (int) $stats['spam_count'] . '</li>'
        . '<li>Erster Eintrag: ' . htmlspecialchars((string) ($stats['first_logged_at'] ?: '-'), ENT_QUOTES, 'UTF-8') . '</li>'
        . '<li>Letzter Eintrag: ' . htmlspecialchars((string) ($stats['last_logged_at'] ?: '-'), ENT_QUOTES, 'UTF-8') . '</li>'
        . '</ul>';

    if ($bounceLogUrl !== '') {
        $htmlBody .= '<p><a href="' . htmlspecialchars($bounceLogUrl, ENT_QUOTES, 'UTF-8') . '">Bounce-Log im Admin oeffnen</a></p>';
    }

    $htmlBody .= '<p>Empfehlung: Versandlage pruefen, betroffene SMTPs/Listen kontrollieren und bei Bedarf Versand drosseln.</p>'
        . '</body></html>';

    $altBody = "Der Bounce-Schutz hat eine auffaellige Anzahl von Zustellfehlern erkannt.\n"
        . 'Schutzmodus: ' . $state['mode'] . "\n"
        . 'Zeitfenster: letzte ' . (int) $stats['window_minutes'] . " Minuten\n"
        . 'Gesamt-Bounces: ' . (int) $stats['total_count'] . "\n"
        . 'Hard-Bounces: ' . (int) $stats['hard_count'] . "\n"
        . 'Soft-Bounces: ' . (int) $stats['soft_count'] . "\n"
        . 'Spam-/Blocked-Bounces: ' . (int) $stats['spam_count'] . "\n"
        . 'Erster Eintrag: ' . (string) ($stats['first_logged_at'] ?: '-') . "\n"
        . 'Letzter Eintrag: ' . (string) ($stats['last_logged_at'] ?: '-');

    if ($bounceLogUrl !== '') {
        $altBody .= "\nBounce-Log: " . $bounceLogUrl;
    }

    $mailer = new Mailer();
    $sentAny = false;

    foreach ($adminRecipients as $recipient) {
        $email = trim((string) ($recipient['email'] ?? ''));
        if ($email === '') {
            continue;
        }

        $mailSent = $mailer->send($email, $subject, $htmlBody, $altBody);
        if ($mailSent) {
            $sentAny = true;
            ErrorLogger::log('BOUNCE_GUARD_ALERT_SENT: ' . $email);
        } else {
            ErrorLogger::log('BOUNCE_GUARD_ALERT_FAILED: ' . $email);
        }
    }

    return $sentAny;
}

function persistBounceGuardState(PDO $db, array $stats, array $thresholds): array
{
    $state = determineBounceGuardState($stats, $thresholds);
    upsertSettingValue($db, BOUNCE_GUARD_MODE_KEY, $state['mode']);
    upsertSettingValue($db, BOUNCE_GUARD_SUMMARY_KEY, $state['summary']);
    upsertSettingValue($db, BOUNCE_GUARD_UPDATED_AT_KEY, date('Y-m-d H:i:s'));

    return $state;
}

// ============================================================================
//  Hauptlogik ----------------------------------------------------------------
// ============================================================================
$db         = Database::getInstance();
$hasPhpImap = class_exists('PhpImap\\Mailbox');

$bouncePatterns = executeLoggedSql(
    'SELECT pattern, bounce_type FROM bounce_patterns',
    static function () use ($db) {
        return $db->query('SELECT pattern, bounce_type FROM bounce_patterns')
                  ->fetchAll(PDO::FETCH_ASSOC);
    }
);
$recipientLookupStmt = $db->prepare('SELECT id, status FROM recipients WHERE email = ? LIMIT 1');
$insertBounceStmt = $db->prepare(
    'INSERT INTO email_bounces
     (queue_id, recipient_id, email, bounce_type, reason, raw_body)
     VALUES (0, ?, ?, ?, ?, ?)'
);
$updateRecipientStatusStmt = $db->prepare('UPDATE recipients SET status = "bounced" WHERE id = ?');

try {
    // ------------------------------------------------------------------------
    // (1) Aktive SMTP-Accounts abrufen
    // ------------------------------------------------------------------------
    $smtpAccounts = executeLoggedSql(
        'SELECT * FROM smtp_accounts WHERE is_active = 1',
        static function () use ($db) {
            return $db->query(
                'SELECT * FROM smtp_accounts
                 WHERE is_active = 1
                   AND smtp_user IS NOT NULL
                   AND smtp_pass IS NOT NULL
                   AND smtp_host IS NOT NULL'
            )->fetchAll(PDO::FETCH_ASSOC);
        }
    );

    // ------------------------------------------------------------------------
    // (2) Account-Schleife über alle aktiven Konten
    // ------------------------------------------------------------------------
    foreach ($smtpAccounts as $smtp) {
        $host   = $smtp['smtp_host'];
        $user   = $smtp['smtp_user'];
        $pass   = $smtp['smtp_pass'];
        $secure = $smtp['smtp_secure'] ?? 'tls';   // 'ssl' | 'tls' | (leer)

        // ==============================
        //  POP3-Parameter vorbereiten
        // ==============================
        $popPort = $secure === 'ssl' ? 995 : 110;
        $encFlag = $secure === 'ssl' ? '/ssl' : '/notls';
        $popPath = sprintf('{%s:%d/pop3%s/novalidate-cert}', $host, $popPort, $encFlag);

        // ==============================
        //  (2a) Versuchen: IMAP-Modus
        // ==============================
        if ($hasPhpImap) {
            $processed = 0;
            try {
                $imapPort = $secure === 'ssl' ? 993 : 143;
                $imapEnc  = $secure === 'ssl' ? '/ssl' : '/tls';
                $imapPath = sprintf('{%s:%d/imap%s}INBOX', $host, $imapPort, $imapEnc);

                $mailbox = new PhpImap\Mailbox($imapPath, $user, $pass, null);
                $ids     = $mailbox->searchMailbox('ALL') ?: [];   // alle Mails
                $total   = count($ids);

                foreach ($ids as $id) {
                    $mail = $mailbox->getMail($id);

                    // Bounce verarbeiten (Plain + HTML + From-Adresse als Fallback)
                    if (processBounce($db, $mail->textPlain . $mail->textHtml, $mail->fromAddress, $bouncePatterns, $recipientLookupStmt, $insertBounceStmt, $updateRecipientStatusStmt)) {
                        ++$processed;
                    }
                    $mailbox->deleteMail($id);                      // \Deleted setzen
                }
                $mailbox->expungeDeletedMails();                   // endgültig löschen

                ErrorLogger::log(
                    'IMAP_STATS: total=' . $total .
                    ', processed=' . $processed .
                    ', account=' . $user . '@' . $host
                );

                continue;  // Erfolgreich – POP3-Fallback nicht nötig
            } catch (Throwable $e) {
                // IMAP fehlgeschlagen → Fallback auf POP3
                ErrorLogger::log(
                    'IMAP_FALLBACK: ' . $e->getMessage() .
                    ' [account=' . $user . '@' . $host . ']'
                );
            }
        }

        // ==============================
        //  (2b) POP3-Fallback
        // ==============================
        // Verbindung herstellen ------------------------------------------------
        $inbox = @imap_open($popPath, $user, $pass);
        if (!$inbox) {
            ErrorLogger::log(
                'POP3_CONNECT: ' . (imap_last_error() ?: 'imap_open failed') .
                ' [account=' . $user . '@' . $host . ']'
            );
            continue;      // nächster Account
        }

        // Nur ungelesene Mails holen ------------------------------------------
        $emails    = imap_search($inbox, 'UNSEEN') ?: [];
        $total     = count($emails);
        $processed = 0;

        // ------------------ Mail-Schleife -------------------------------------
        foreach ($emails as $num) {
            $header = imap_headerinfo($inbox, $num);
            $body   = imap_body($inbox, $num);
            $from   = isset($header->from[0])
                ? $header->from[0]->mailbox . '@' . $header->from[0]->host
                : '';

            // Bounce verarbeiten
            if (processBounce($db, $body, $from, $bouncePatterns, $recipientLookupStmt, $insertBounceStmt, $updateRecipientStatusStmt)) {
                ++$processed;
            }

            // 1) Nachricht zum Löschen vormerken -------------------------------
            if (!imap_delete($inbox, (string)$num)) {
                ErrorLogger::log(
                    'POP3_DELETE_FAIL: Message ' . $num .
                    ' [account=' . $user . '@' . $host . ']'
                );
            }
        }

        // 2) Mailbox expungen & schließen --------------------------------------
        //    Je nach PHP-Version ist CL_EXPUNGE evtl. nicht definiert – daher
        //    ggf. Fallback auf imap_expunge().
        if (defined('CL_EXPUNGE')) {
            imap_close($inbox, CL_EXPUNGE);          // löscht \Deleted-Mails
        } else {
            imap_expunge($inbox);                    // zuerst expungen …
            imap_close($inbox);                      // … dann sauber schließen
        }

        ErrorLogger::log(
            'POP3_STATS: total=' . $total .
            ', processed=' . $processed .
            ', account=' . $user . '@' . $host
        );
    } // foreach SMTP-Account

} catch (Throwable $fatal) {
    // Gesamtes Skript gescheitert – fatales Logging + Non-Zero-Exitcode
    ErrorLogger::log('BOUNCE_HANDLER_FATAL: ' . $fatal->getMessage());
    exit(1);
}

try {
    $guardStats = fetchBounceGuardStats($db);
    $guardThresholds = fetchBounceGuardThresholds($db);
    $guardState = persistBounceGuardState($db, $guardStats, $guardThresholds);
    if (shouldTriggerBounceGuardAlert($guardStats, $guardThresholds)) {
        if (isBounceGuardAlertDue($db)) {
            if (sendBounceGuardAlert($db, $guardStats, $guardThresholds)) {
                upsertSettingValue($db, BOUNCE_GUARD_LAST_ALERT_KEY, date('Y-m-d H:i:s'));
            }
        } else {
            ErrorLogger::log(
                'BOUNCE_GUARD_ALERT_SKIPPED_COOLDOWN: mode=' . $guardState['mode']
                . ', total=' . $guardStats['total_count']
                . ', hard=' . $guardStats['hard_count']
                . ', soft=' . $guardStats['soft_count']
                . ', spam=' . $guardStats['spam_count']
            );
        }
    }
} catch (Throwable $e) {
    ErrorLogger::log('BOUNCE_GUARD_ALERT_ERROR: ' . $e->getMessage());
}

exit(0);   // Erfolg

// ============================================================================
//  Helper: einzelne Bounce-Mail verarbeiten ----------------------------------
// ============================================================================
/**
 * Analysiert eine Bounce-Mail, klassifiziert sie und legt – falls möglich –
 * einen Eintrag in `email_bounces` an.  Es wird **nur** geschrieben, wenn die
 * Adresse als Empfänger im System vorhanden ist.
 *
 * @param  PDO    $db            DB-Handle
 * @param  string $rawBody       Kompletter Body (Plain + HTML)
 * @param  string $fallbackEmail Absender-Adresse als Fallback
 * @return bool   TRUE, wenn Eintrag erfolgte
 */
function processBounce(
    PDO $db,
    string $rawBody,
    string $fallbackEmail,
    array $patterns,
    PDOStatement $recipientLookupStmt,
    PDOStatement $insertBounceStmt,
    PDOStatement $updateRecipientStatusStmt
): bool
{
    // ------------------ (a) E-Mail-Adresse extrahieren ------------------------
    preg_match('/[\\w._%+-]+@[\\w.-]+\\.[a-zA-Z]{2,}/', $rawBody, $mailMatch);
    $matchedEmail = $mailMatch[0] ?? $fallbackEmail;

    // ------------------ (b) Empfänger + Status bestimmen ----------------------
    $recipient = executeLoggedSql(
        'SELECT id, status FROM recipients WHERE email = ? LIMIT 1',
        static function () use ($recipientLookupStmt, $matchedEmail) {
            $recipientLookupStmt->execute([$matchedEmail]);
            return $recipientLookupStmt->fetch(PDO::FETCH_ASSOC) ?: null;
        }
    );

    // Adresse nicht im System -> nur loggen, kein DB-Eintrag
    if ($recipient === null) {
        ErrorLogger::log('BOUNCE_NO_RECIPIENT: E-Mail nicht im System [' . $matchedEmail . ']');
        return false;
    }

    $recipientId = (int) $recipient['id'];
    $status = (string) ($recipient['status'] ?? '');

    // ------------------ (c) Bounce-Typ anhand Muster ermitteln ---------------
    $bounceType = 'other';
    $reason     = null;

    foreach ($patterns as $p) {
        if (stripos($rawBody, trim($p['pattern'], '%')) !== false) {
            $bounceType = $p['bounce_type'];
            $reason     = $p['pattern'];
            break;
        }
    }

    // ------------------ (d) Eintrag in email_bounces schreiben --------------
    executeLoggedSql(
        'INSERT INTO email_bounces (queue_id, recipient_id, email, bounce_type, reason, raw_body)
         VALUES (0, ?, ?, ?, ?, ?)',
        static function () use ($insertBounceStmt, $recipientId, $matchedEmail, $bounceType, $reason, $rawBody) {
            // Body auf 1500 Zeichen kürzen, um DB-Größe gering zu halten
            $snippet = mb_substr($rawBody, 0, 1500);
            $insertBounceStmt->execute([
                $recipientId,
                $matchedEmail,
                $bounceType,
                $reason,
                $snippet,
            ]);
        }
    );

    // Geschützte Status NICHT überschreiben
    if (in_array($status, ['unsubscribed', 'deleted'], true)) {
        ErrorLogger::log(
            'BOUNCE_STATUS_PROTECTED: ' . $status .
            ' [recipient_id=' . $recipientId . ']'
        );
        return true;
    }

    // Status auf bounced setzen
    executeLoggedSql(
        'UPDATE recipients SET status = "bounced" WHERE id = ?',
        static function () use ($updateRecipientStatusStmt, $recipientId) {
            $updateRecipientStatusStmt->execute([$recipientId]);
        }
    );

    return true;
}

