*/ public static function all(): array { return [ new Check( key: 'delivery.mailer_not_log', group: self::GROUP, severity: Check::SEVERITY_BLOCKING, label: __('readiness.delivery.mailer_not_log'), breaks: __('readiness.delivery.mailer_not_log_breaks'), tab: 'mail', // Reads the transport's own definition of "does not really // send" rather than a second, narrower one. `!== 'log'` alone // called 'array' — the MAIL_MAILER this whole suite runs // under — delivered, which MailboxTransport::delegate() has // never agreed with. satisfied: MailDelivery::delivers(), ), new Check( key: 'delivery.mailbox', group: self::GROUP, severity: Check::SEVERITY_BLOCKING, label: __('readiness.delivery.mailbox'), breaks: __('readiness.delivery.mailbox_breaks'), tab: 'mail', // Nicht mehr `Mailbox::query()->exists()`. Dass IRGENDEIN // Postfach angelegt ist, entscheidet nichts: gesendet wird // ueber das Postfach, das dem Zweck `system` zugeordnet ist, // und das muss aktiv sein und Zugangsdaten haben. Die alte // Fassung stand gruen, waehrend jede Systemmail — Passwort // vergessen, Adresse bestaetigen, neues Geraet — in der // Warteschlange starb. `system` genuegt als Pruefgegenstand, // weil MailboxResolver jeden anderen Zweck darauf zurueckfallen // laesst. satisfied: app(MailboxResolver::class)->usableFor(MailPurpose::SYSTEM) !== null, ), new Check( key: 'delivery.mail_templates', group: self::GROUP, severity: Check::SEVERITY_WARNING, label: __('readiness.delivery.mail_templates'), breaks: __('readiness.delivery.mail_templates_breaks'), tab: 'templates', satisfied: MailTemplate::query()->exists(), ), new Check( // Named after the vault entry it checks (inbound_mail.password), // not a shorthand — see the identical reasoning on // onboarding.ssh_private_key in OnboardingChecks.php. key: 'delivery.inbound_mail_password', group: self::GROUP, severity: Check::SEVERITY_WARNING, label: __('readiness.delivery.inbound_password'), breaks: __('readiness.delivery.inbound_password_breaks'), // The inbound-mail card (host/port/user/folder + this // password) is on the 'services' tab — 'integrations' is not // a member of Integrations::TABS at all (Fix-Runde, Befund 1). tab: 'services', satisfied: filled(app(SecretVault::class)->get('inbound_mail.password')), ), new Check( key: 'delivery.mail_lanes_moving', group: self::GROUP, severity: Check::SEVERITY_BLOCKING, label: __('readiness.delivery.mail_lanes_moving'), breaks: __('readiness.delivery.mail_lanes_moving_breaks'), // Eigener Wert, keiner der bestehenden ('mail' zeigt auf // admin/mail — Server, Postfächer, Zwecke; die Spuren liegen // seit Task 5 auf der eigenen Seite admin/mail-pace). Der // ReadinessPageTest hält jeden tab-Wert gegen die Stelle, an // der sein Feld wirklich liegt — 'mail' wäre hier derselbe // falsche Verweis, den jene Prüfung an neun anderen Stellen // schon einmal gefangen hat. tab: 'mail-pace', // Eine Spur, die STEHT, sieht von außen aus wie eine, die // LEER ist — `Queue::size()` (Task 5, admin/mail-pace) meldet // in beiden Fällen dieselbe Art Zahl und unterscheidet sie // nicht. Das Alter des ältesten wartenden Auftrags trennt die // beiden: eine leere Spur hat keinen, eine verstopfte hat // einen, der immer älter wird. // // `connection('redis')` ausdrücklich, nicht die // Standardverbindung: dieselbe, mit der der Arbeiter in // docker-compose.yml tatsächlich gestartet wird // (`queue:work redis --queue=mail-direkt,mail-wichtig, // mail-ruhig,default`) — unabhängig davon, worauf // QUEUE_CONNECTION gerade zeigt. Unter der Testsuite zwingt // phpunit.xml die STANDARDverbindung auf `sync` // (SyncQueue::size() liefert dort immer 0, egal ob leer oder // verstopft — siehe die Anmerkung auf admin/mail-pace); die // NAMENTLICH angeforderte `redis`-Verbindung bleibt davon // unberührt und bleibt echtes Redis, in beiden Umgebungen // erreichbar (siehe tests/Feature/Admin/HostTerminalTest.php). // `creationTimeOfOldestPendingJob()` ist Teil des // Queue-Vertrags selbst (Illuminate\Contracts\Queue\Queue) — // jeder Treiber implementiert es, `sync` eingeschlossen, und // liefert dort ehrlich `null` statt einer erfundenen Zahl. satisfied: self::laneMoving(MailLane::DIRECT) && self::laneMoving(MailLane::URGENT) && self::laneMoving(MailLane::CALM), ), ]; } /** * Ob der älteste wartende Auftrag einer Spur jünger als die Grenze ist. * * Zählt nur die unverzögerte Liste (`creationTimeOfOldestPendingJob` * schließt `:delayed` ausdrücklich aus) — ein von der Drossel * zurückgelegter Auftrag dort ist die Drossel bei der Arbeit, kein * Klemmen. Was hier erkannt wird, ist der Fall, in dem niemand mehr * abholt: der Arbeiter tot, oder die Spur aus seiner --queue-Liste * gefallen — dann wächst die unverzögerte Liste, weil neue Mail dort * ungedrosselt eintrifft (`RidesALane::queue()` reiht ohne Verzögerung * ein) und nie wieder verschwindet. */ private static function laneMoving(string $lane): bool { try { $oldest = Queue::connection('redis')->creationTimeOfOldestPendingJob($lane); } catch (\Throwable) { // Wie OperationChecks::isFresh(): eine Seite, deren einziger // Zweck ist, ruhig zu melden, was fehlt, darf nicht selbst // ausfallen, weil Redis gerade nicht erreichbar ist. Nicht // erreichbar heißt nicht erfüllt — nicht "wirft". return false; } if ($oldest === null) { // Noch nie etwas eingereiht, oder gerade leer geräumt — beides // ist gesund. Kein Auftrag heißt: nichts, das stehen könnte. return true; } return $oldest > now()->subMinutes(self::STUCK_AFTER_MINUTES)->getTimestamp(); } }