Say when no mail is being delivered at all
tests / pest (push) Failing after 8m40s Details
tests / assets (push) Successful in 28s Details
tests / release (push) Has been skipped Details

"Ich versuche mich zu registrieren, erhalte aber keine Verify-Mail, auch
nicht beim erneuten Senden." Traced rather than guessed, and the evidence is
in storage/logs: the address appears three times, so the mails were built and
handed to a transport that writes them to a file.

MAIL_MAILER=log. MailboxTransport::resolution() short-circuits on it —
NON_DELIVERING is ['log', 'array', null] — so every purpose mailer becomes
the log transport however well the five mailboxes are configured, which is
correct for a development machine and exactly what was happening here.

The reason it deserves a notice is that the failure is silent in every
direction an operator would look. Mail::queue() succeeds. No job fails. The
queue empties. And the console's own mailbox test reports SUCCESS, because
MailboxTester builds its own transport on purpose — a check that honoured
MAIL_MAILER=log would report success while writing to a file. So every
instrument on the page said fine.

Now the front page says it, linked to the mail page, and the mail page says
it above the mailboxes it makes decorative. All three swallowing values are
named, not only 'log': array and an unset default do the same thing, and a
notice that only knew about one would be silent in the two cases nobody
thinks to check.

Two existing tests asserted that a clean installation raises no notice, and
went red — correctly, because phpunit.xml forces MAIL_MAILER=array so a suite
cannot send mail. They pin a delivering mailer now: the warning is about the
environment, those tests are about the estate.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
feature/betriebsmodus^2 v1.3.48
nexxo 2026-07-30 13:55:55 +02:00
parent 70fa136e22
commit f49f1dca09
7 changed files with 90 additions and 1 deletions

View File

@ -1 +1 @@
1.3.47 1.3.48

View File

@ -382,6 +382,23 @@ class Overview extends Component
]; ];
} }
// MAIL_MAILER=log. Every purpose mailer short-circuits to the log
// transport then — see MailboxTransport::NON_DELIVERING — however well
// the mailboxes are configured, and nothing is delivered to anybody.
//
// Worth its own notice because the failure is SILENT in every direction:
// the queue reports success, no job fails, the console's own mailbox test
// reports success (MailboxTester builds its own transport on purpose), and
// the mail sits in storage/logs. Somebody registering and waiting for a
// verification mail has no way to find that out. Asked exactly that way.
if (in_array(config('mail.default'), ['log', 'array', null], true)) {
$notices[] = [
'level' => 'warning',
'text' => __('admin.notice.mail_not_delivering', ['mailer' => (string) (config('mail.default') ?? 'null')]),
'route' => 'admin.mail',
];
}
// No SITE_HOST. The marketing site is then registered without a // No SITE_HOST. The marketing site is then registered without a
// hostname, so it answers on the PORTAL's host as well — and every // hostname, so it answers on the PORTAL's host as well — and every
// route() it generates points at APP_URL, which is why a link on the // route() it generates points at APP_URL, which is why a link on the

View File

@ -88,6 +88,7 @@ return [
], ],
'notice' => [ 'notice' => [
'mail_not_delivering' => 'MAIL_MAILER steht auf „:mailer“ — es wird keine E-Mail zugestellt, alles landet in storage/logs. Auch der Postfach-Test meldet trotzdem Erfolg, weil er seinen eigenen Weg nimmt.',
'site_hidden' => 'Website und Kundenbereich sind ausgeblendet — Besucher außerhalb des VPN bekommen die Platzhalterseite (503). Umschalten unter Einstellungen → Installation.', 'site_hidden' => 'Website und Kundenbereich sind ausgeblendet — Besucher außerhalb des VPN bekommen die Platzhalterseite (503). Umschalten unter Einstellungen → Installation.',
'no_site_host' => 'SITE_HOST ist nicht gesetzt. Die Website antwortet dadurch auch auf :app, und Links auf ihr zeigen dorthin statt auf den eigenen Namen.', 'no_site_host' => 'SITE_HOST ist nicht gesetzt. Die Website antwortet dadurch auch auf :app, und Links auf ihr zeigen dorthin statt auf den eigenen Namen.',
'no_capacity' => 'Kein Host hat noch Platz für „:plan“ — das Paket braucht :needs GB, der freieste Host hat :largest GB. Eine bezahlte Bestellung dafür würde bei der Reservierung scheitern.', 'no_capacity' => 'Kein Host hat noch Platz für „:plan“ — das Paket braucht :needs GB, der freieste Host hat :largest GB. Eine bezahlte Bestellung dafür würde bei der Reservierung scheitern.',

View File

@ -88,6 +88,7 @@ return [
], ],
'notice' => [ 'notice' => [
'mail_not_delivering' => 'MAIL_MAILER is set to ":mailer" — no mail is delivered to anybody, it all goes to storage/logs. The mailbox test still reports success, because it builds its own transport.',
'site_hidden' => 'The website and the customer portal are hidden — a visitor outside the VPN gets the placeholder page (503). The switch is under Settings → Installation.', 'site_hidden' => 'The website and the customer portal are hidden — a visitor outside the VPN gets the placeholder page (503). The switch is under Settings → Installation.',
'no_site_host' => 'SITE_HOST is not set. The website therefore also answers on :app, and links on it point there instead of at its own name.', 'no_site_host' => 'SITE_HOST is not set. The website therefore also answers on :app, and links on it point there instead of at its own name.',
'no_capacity' => 'No host has room for “:plan” any more — the package needs :needs GB and the roomiest host has :largest GB. A paid order for it would fail at reservation.', 'no_capacity' => 'No host has room for “:plan” any more — the package needs :needs GB and the roomiest host has :largest GB. A paid order for it would fail at reservation.',

View File

@ -4,6 +4,19 @@
<p class="mt-1 text-sm text-muted">{{ __('mail_settings.subtitle') }}</p> <p class="mt-1 text-sm text-muted">{{ __('mail_settings.subtitle') }}</p>
</div> </div>
{{-- The one thing that makes every mailbox on this page decorative. Said
here as well as on the front page, because this is where somebody comes
to find out why no mail arrived and the test button below reports
success regardless, since MailboxTester builds its own transport on
purpose (a check that honoured MAIL_MAILER=log would report success
while writing to a file). --}}
@if (in_array(config('mail.default'), ['log', 'array', null], true))
<x-ui.alert variant="warning">
{{ __('admin.notice.mail_not_delivering', ['mailer' => (string) (config('mail.default') ?? 'null')]) }}
</x-ui.alert>
@endif
@if (! $usable) @if (! $usable)
<x-ui.alert variant="warning">{{ __('mail_settings.no_key') }}</x-ui.alert> <x-ui.alert variant="warning">{{ __('mail_settings.no_key') }}</x-ui.alert>
@endif @endif

View File

@ -25,6 +25,12 @@ it('reports an empty estate as empty, not as a going concern', function () {
// console still claimed 42 customers, 39 instances, 4 hosts and €7,842 a // console still claimed 42 customers, 39 instances, 4 hosts and €7,842 a
// month. The figures are asserted directly rather than searched for in the // month. The figures are asserted directly rather than searched for in the
// markup — "42" also occurs inside Livewire's own component ids. // markup — "42" also occurs inside Livewire's own component ids.
//
// The mailer is pinned to a delivering one: phpunit.xml forces
// MAIL_MAILER=array so a test suite cannot send mail, and the console
// rightly warns that nothing is being delivered. That warning is about the
// ENVIRONMENT, not about the estate, and this test is about an empty estate.
config()->set('mail.default', 'smtp');
Livewire::actingAs(operator('Owner'), 'operator') Livewire::actingAs(operator('Owner'), 'operator')
->test(Overview::class) ->test(Overview::class)
->assertViewHas('kpis', function (array $kpis) { ->assertViewHas('kpis', function (array $kpis) {
@ -107,6 +113,10 @@ it('states monthly revenue off the contracts, with a yearly term divided', funct
}); });
it('raises a notice only when something is actually wrong', function () { it('raises a notice only when something is actually wrong', function () {
// See above: the test suite deliberately delivers no mail, and the console
// says so. Pinned here so "clean" means an estate with nothing wrong in it.
config()->set('mail.default', 'smtp');
$clean = Livewire::actingAs(operator('Owner'), 'operator')->test(Overview::class); $clean = Livewire::actingAs(operator('Owner'), 'operator')->test(Overview::class);
$clean->assertSee(__('admin.systems_ok')); $clean->assertSee(__('admin.systems_ok'));

View File

@ -300,3 +300,50 @@ it('says when the website has no hostname of its own', function () {
->test(Overview::class) ->test(Overview::class)
->assertDontSee(__('admin.notice.no_site_host', ['app' => 'app.dev.example.test'])); ->assertDontSee(__('admin.notice.no_site_host', ['app' => 'app.dev.example.test']));
}); });
it('says when no mail is being delivered at all', function () {
// The report: "ich versuche mich zu registrieren, erhalte aber keine Verify-
// Mail, auch nicht beim erneuten Senden". MAIL_MAILER=log short-circuits
// every purpose mailer to the log transport (MailboxTransport::
// NON_DELIVERING) however well the mailboxes are configured — and the
// failure is silent in every direction: the queue reports success, no job
// fails, and the console's own mailbox test reports success too because it
// builds its own transport on purpose. Nothing on any page said so.
config()->set('mail.default', 'log');
Livewire::actingAs(operator('Owner'), 'operator')
->test(Overview::class)
->assertSee(__('admin.notice.mail_not_delivering', ['mailer' => 'log']))
->assertSee(route('admin.mail'), false);
// And on the page somebody actually opens to find out why.
Livewire::actingAs(operator('Owner'), 'operator')
->test(App\Livewire\Admin\Mail::class)
->assertSee(__('admin.notice.mail_not_delivering', ['mailer' => 'log']));
});
it('stays quiet once mail really goes out', function () {
config()->set('mail.default', 'smtp');
Livewire::actingAs(operator('Owner'), 'operator')
->test(Overview::class)
->assertDontSee(__('admin.notice.mail_not_delivering', ['mailer' => 'log']));
});
it('names every mailer that swallows mail, not only the log one', function () {
// array and an unset default do the same thing, and the transport already
// treats all three alike. A notice that only knew about 'log' would be
// silent in the two cases nobody thinks to check.
$swallowing = (new ReflectionClass(App\Mail\Transport\MailboxTransport::class))
->getConstant('NON_DELIVERING');
expect($swallowing)->toBe(['log', 'array', null]);
foreach (['array', null] as $mailer) {
config()->set('mail.default', $mailer);
Livewire::actingAs(operator('Owner'), 'operator')
->test(Overview::class)
->assertSee(__('admin.notice.mail_not_delivering', ['mailer' => $mailer ?? 'null']));
}
});