diff --git a/VERSION b/VERSION
index f5301e2..b500665 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-1.3.47
+1.3.48
diff --git a/app/Livewire/Admin/Overview.php b/app/Livewire/Admin/Overview.php
index 87435ca..bc5e15a 100644
--- a/app/Livewire/Admin/Overview.php
+++ b/app/Livewire/Admin/Overview.php
@@ -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
// 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
diff --git a/lang/de/admin.php b/lang/de/admin.php
index 74e1715..4fb00d9 100644
--- a/lang/de/admin.php
+++ b/lang/de/admin.php
@@ -88,6 +88,7 @@ return [
],
'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.',
'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.',
diff --git a/lang/en/admin.php b/lang/en/admin.php
index a2098cd..b6fbe70 100644
--- a/lang/en/admin.php
+++ b/lang/en/admin.php
@@ -88,6 +88,7 @@ return [
],
'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.',
'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.',
diff --git a/resources/views/livewire/admin/mail.blade.php b/resources/views/livewire/admin/mail.blade.php
index 3b00c8e..e491f38 100644
--- a/resources/views/livewire/admin/mail.blade.php
+++ b/resources/views/livewire/admin/mail.blade.php
@@ -4,6 +4,19 @@
{{ __('mail_settings.subtitle') }}
+ {{-- 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))
+
+ {{ __('admin.notice.mail_not_delivering', ['mailer' => (string) (config('mail.default') ?? 'null')]) }}
+
+ @endif
+
+
@if (! $usable)
{{ __('mail_settings.no_key') }}
@endif
diff --git a/tests/Feature/Admin/ConsoleReportsRealDataTest.php b/tests/Feature/Admin/ConsoleReportsRealDataTest.php
index 1ae3419..8452747 100644
--- a/tests/Feature/Admin/ConsoleReportsRealDataTest.php
+++ b/tests/Feature/Admin/ConsoleReportsRealDataTest.php
@@ -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
// month. The figures are asserted directly rather than searched for in the
// 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')
->test(Overview::class)
->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 () {
+ // 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->assertSee(__('admin.systems_ok'));
diff --git a/tests/Feature/Admin/MailRegisterTest.php b/tests/Feature/Admin/MailRegisterTest.php
index 25b4572..1093418 100644
--- a/tests/Feature/Admin/MailRegisterTest.php
+++ b/tests/Feature/Admin/MailRegisterTest.php
@@ -300,3 +300,50 @@ it('says when the website has no hostname of its own', function () {
->test(Overview::class)
->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']));
+ }
+});