addMinutes($windowMinutes)); $count = (int) Cache::increment(self::CounterKey); if ($count < $threshold || $this->isOpen()) { return; } $pauseMinutes = max(1, (int) config('bounces.circuit_breaker.pause_minutes', 30)); $until = now()->addMinutes($pauseMinutes); Cache::put(self::OpenKey, $until->toIso8601String(), $until); Cache::forget(self::CounterKey); OperationalAlert::send( sprintf('Mailversand angehalten bis %s: %d Relay-Ablehnungen in %d Minuten', $until->format('H:i'), $count, $windowMinutes), [ 'last_rejection' => $detail, 'hint' => 'Absenderruf/Relay prüfen. Fehlgeschlagene Queue-Mails danach mit `php artisan queue:retry all` nachholen, laufende Newsletter-Kampagnen mit `php artisan newsletter:resume {kampagne} --failed`; `php artisan mail:circuit --reset` hebt die Pause vorzeitig auf.', ], ); } public function isOpen(): bool { return $this->openUntil() !== null; } public function openUntil(): ?Carbon { $until = Cache::get(self::OpenKey); if (! is_string($until)) { return null; } $carbon = Carbon::parse($until); return $carbon->isFuture() ? $carbon : null; } public function reset(): void { Cache::forget(self::OpenKey); Cache::forget(self::CounterKey); } public function pendingRejections(): int { return (int) Cache::get(self::CounterKey, 0); } }