Wegwahl je Mailart: welche Mail aus welchem Postfach geht
MailCatalogue haelt die eine Liste aller sechzehn Mailarten (Schluessel, Beschriftung, Vorgabe-Zweck), aus MailPreviews herausgezogen, damit es nur noch eine Stelle gibt, die beim naechsten Mailtyp vergessen werden kann. MailRoute sitzt darueber: ein Eintrag ist eine Ausnahme fuer GENAU diese eine Mailart, keine zweite Zuordnungsebene — ohne Eintrag oder bei abgeschaltetem Zielpostfach faellt sie unveraendert auf den Zweck zurueck, den MailboxResolver schon kennt. SendsFromMailbox bekommt dafuer einen optionalen $mailKey; alle bestehenden Aufrufer (inklusive ContactRequestMail, das mailboxAddresses selbst zusammensetzt) bleiben bei null und damit beim alten Verhalten. Jede Mailklasse und die CloudReady-Benachrichtigung nennen jetzt ihren Katalog-Schluessel. Die Konsole bekommt eine vierte Karte unter der Zweck-Zuordnung: eine Zeile je Mailart, ein <select> mit den aktiven Postfaechern und "wie der Zweck (...)" als Vorgabe. Der wichtigste Test schickt eine Mail ohne jeden Eintrag und prueft, dass sie exakt beim bisherigen Postfach landet. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>feat/versandtakt
parent
e3f2e9a88b
commit
5dcfee9957
|
|
@ -5,7 +5,9 @@ namespace App\Livewire\Admin;
|
|||
use App\Livewire\Concerns\ConfirmsPassword;
|
||||
use App\Models\Mailbox;
|
||||
use App\Services\Mail\MailboxTester;
|
||||
use App\Services\Mail\MailCatalogue;
|
||||
use App\Services\Mail\MailPurpose;
|
||||
use App\Services\Mail\MailRoute;
|
||||
use App\Services\Secrets\SecretCipher;
|
||||
use App\Support\MailDelivery;
|
||||
use App\Support\Settings;
|
||||
|
|
@ -43,6 +45,17 @@ class Mail extends Component
|
|||
/** @var array<string, string> purpose => mailbox key */
|
||||
public array $purposes = [];
|
||||
|
||||
/**
|
||||
* Die Wegwahl je Mailart — leer heißt „wie der Zweck".
|
||||
*
|
||||
* Eine Ebene unter $purposes, nicht daneben: dieselbe Seite, dieselbe
|
||||
* Berechtigung, aber eine eigene Tabelle, weil hier sechzehn Zeilen statt
|
||||
* fünf stehen.
|
||||
*
|
||||
* @var array<string, string> Katalog-Schlüssel => Postfach-Schlüssel
|
||||
*/
|
||||
public array $routes = [];
|
||||
|
||||
public string $testRecipient = '';
|
||||
|
||||
/** @var array{ok: bool, error: ?string}|null */
|
||||
|
|
@ -76,6 +89,10 @@ class Mail extends Component
|
|||
foreach (MailPurpose::ALL as $purpose) {
|
||||
$this->purposes[$purpose] = (string) Settings::get(MailPurpose::settingKey($purpose), '');
|
||||
}
|
||||
|
||||
foreach (MailCatalogue::all() as $key => $entry) {
|
||||
$this->routes[$key] = (string) Settings::get(MailRoute::settingKey($key), '');
|
||||
}
|
||||
}
|
||||
|
||||
public function saveServer(): void
|
||||
|
|
@ -190,6 +207,25 @@ class Mail extends Component
|
|||
$this->dispatch('notify', message: __('mail_settings.purposes_saved'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Die Wegwahl je Mailart. Nach dem Muster von savePurposes() — dieselbe
|
||||
* Berechtigung, dieselbe Meldung — aber ohne dessen Regelwerk: ein
|
||||
* Eintrag, der auf kein oder ein abgeschaltetes Postfach zeigt, ist hier
|
||||
* kein Fehler, weil MailRoute::purposeOrMailbox() genau diesen Fall schon
|
||||
* auf den Zweck zurückfallen lässt. Eine zweite Prüfung derselben
|
||||
* Sicherung wäre doppelte Arbeit ohne eigenen Wert.
|
||||
*/
|
||||
public function saveRoutes(): void
|
||||
{
|
||||
$this->authorize('mail.manage');
|
||||
|
||||
foreach (MailCatalogue::all() as $key => $entry) {
|
||||
Settings::set(MailRoute::settingKey($key), $this->routes[$key] ?? '');
|
||||
}
|
||||
|
||||
$this->dispatch('notify', message: __('mail_settings.purposes_saved'));
|
||||
}
|
||||
|
||||
public function test(string $uuid): void
|
||||
{
|
||||
$this->authorize('mail.manage');
|
||||
|
|
@ -212,6 +248,7 @@ class Mail extends Component
|
|||
return view('livewire.admin.mail', [
|
||||
'mailboxes' => Mailbox::query()->orderBy('key')->get(),
|
||||
'purposeList' => MailPurpose::ALL,
|
||||
'mailCatalogue' => MailCatalogue::all(),
|
||||
'passwordConfirmed' => $this->passwordRecentlyConfirmed(),
|
||||
]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ class CloudResumedMail extends Mailable implements ShouldQueue
|
|||
|
||||
public function envelope(): Envelope
|
||||
{
|
||||
return $this->mailboxEnvelope(MailPurpose::BILLING, __('dunning_mail.subject_resumed'));
|
||||
return $this->mailboxEnvelope(MailPurpose::BILLING, __('dunning_mail.subject_resumed'), 'cloud-resumed');
|
||||
}
|
||||
|
||||
public function content(): Content
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ class CloudSuspendedMail extends Mailable implements ShouldQueue
|
|||
|
||||
public function envelope(): Envelope
|
||||
{
|
||||
return $this->mailboxEnvelope(MailPurpose::BILLING, __('dunning_mail.subject_suspended'));
|
||||
return $this->mailboxEnvelope(MailPurpose::BILLING, __('dunning_mail.subject_suspended'), 'cloud-suspended');
|
||||
}
|
||||
|
||||
public function content(): Content
|
||||
|
|
|
|||
|
|
@ -3,11 +3,13 @@
|
|||
namespace App\Mail\Concerns;
|
||||
|
||||
use App\Services\Mail\MailboxResolver;
|
||||
use App\Services\Mail\MailRoute;
|
||||
use Illuminate\Mail\Mailables\Address;
|
||||
use Illuminate\Mail\Mailables\Envelope;
|
||||
|
||||
/**
|
||||
* From and Reply-To, taken from the mailbox behind a purpose.
|
||||
* From and Reply-To, taken from the mailbox behind a purpose — or, with a
|
||||
* $mailKey, from whatever MailRoute has that ONE mail type pointed at instead.
|
||||
*
|
||||
* Reply-To is the whole reason sending alone is enough: a customer answering a
|
||||
* support reply lands in the support mailbox, read in an ordinary mail client,
|
||||
|
|
@ -18,9 +20,9 @@ use Illuminate\Mail\Mailables\Envelope;
|
|||
*/
|
||||
trait SendsFromMailbox
|
||||
{
|
||||
protected function mailboxEnvelope(string $purpose, string $subject): Envelope
|
||||
protected function mailboxEnvelope(string $purpose, string $subject, ?string $mailKey = null): Envelope
|
||||
{
|
||||
[$from, $replyTo] = $this->mailboxAddresses($purpose);
|
||||
[$from, $replyTo] = $this->mailboxAddresses($purpose, $mailKey);
|
||||
|
||||
return new Envelope(
|
||||
from: $from,
|
||||
|
|
@ -39,11 +41,19 @@ trait SendsFromMailbox
|
|||
* Two independent copies of this same decision is exactly the shape of
|
||||
* gap Task 4 found and fixed in MailboxTransport::resolution().
|
||||
*
|
||||
* $mailKey names this ONE mail type in MailCatalogue and lets MailRoute
|
||||
* override the purpose's mailbox for it alone. Left null (every caller
|
||||
* before this parameter existed, and every one that still has no reason
|
||||
* to be overridden individually), behaviour is exactly what it was: the
|
||||
* purpose alone decides.
|
||||
*
|
||||
* @return array{0: ?Address, 1: ?Address} [from, replyTo]
|
||||
*/
|
||||
protected function mailboxAddresses(string $purpose): array
|
||||
protected function mailboxAddresses(string $purpose, ?string $mailKey = null): array
|
||||
{
|
||||
$box = app(MailboxResolver::class)->for($purpose);
|
||||
$box = $mailKey !== null
|
||||
? MailRoute::purposeOrMailbox($mailKey, $purpose)
|
||||
: app(MailboxResolver::class)->for($purpose);
|
||||
|
||||
if ($box === null) {
|
||||
// No mailbox configured yet — both null, so a caller falls back
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ class DormantAccountWarningMail extends Mailable implements ShouldQueue
|
|||
|
||||
public function envelope(): Envelope
|
||||
{
|
||||
return $this->mailboxEnvelope(MailPurpose::SYSTEM, __('dormant_mail.subject'));
|
||||
return $this->mailboxEnvelope(MailPurpose::SYSTEM, __('dormant_mail.subject'), 'dormant-warning');
|
||||
}
|
||||
|
||||
public function content(): Content
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ class DunningNoticeMail extends Mailable implements ShouldQueue
|
|||
return $this->mailboxEnvelope(
|
||||
MailPurpose::BILLING,
|
||||
__('dunning_mail.subject_'.$this->level),
|
||||
'dunning-'.$this->level,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ class InvoiceMail extends Mailable implements ShouldQueue
|
|||
return $this->mailboxEnvelope(
|
||||
MailPurpose::BILLING,
|
||||
__('invoice_mail.subject', ['number' => $this->invoice->number]),
|
||||
'invoice',
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ class MaintenanceAnnouncementMail extends Mailable implements ShouldQueue
|
|||
return $this->mailboxEnvelope(
|
||||
MailPurpose::MAINTENANCE,
|
||||
__('maintenance.mail_subject', ['title' => $this->window->title]),
|
||||
'maintenance-announcement',
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ class MaintenanceCancelledMail extends Mailable implements ShouldQueue
|
|||
return $this->mailboxEnvelope(
|
||||
MailPurpose::MAINTENANCE,
|
||||
__('maintenance.mail_cancel_subject', ['title' => $this->window->title]),
|
||||
'maintenance-cancelled',
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ class NewDeviceSignInMail extends Mailable implements ShouldQueue
|
|||
return $this->mailboxEnvelope(
|
||||
MailPurpose::SYSTEM,
|
||||
__('devices.mail_subject'),
|
||||
'new-device',
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ class OperatorMessageMail extends Mailable implements ShouldQueue
|
|||
|
||||
public function envelope(): Envelope
|
||||
{
|
||||
return $this->mailboxEnvelope(MailPurpose::SUPPORT, $this->subjectLine);
|
||||
return $this->mailboxEnvelope(MailPurpose::SUPPORT, $this->subjectLine, 'operator-message');
|
||||
}
|
||||
|
||||
public function content(): Content
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ class OrderConfirmationMail extends Mailable implements ShouldQueue
|
|||
return $this->mailboxEnvelope(
|
||||
MailPurpose::BILLING,
|
||||
__('orders.mail_subject', ['number' => $this->order->uuid ?? $this->order->id]),
|
||||
'order-confirmation',
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ class ResetPasswordMail extends Mailable implements ShouldQueue
|
|||
|
||||
public function envelope(): Envelope
|
||||
{
|
||||
return $this->mailboxEnvelope(MailPurpose::SYSTEM, __('reset_password.subject'));
|
||||
return $this->mailboxEnvelope(MailPurpose::SYSTEM, __('reset_password.subject'), 'reset-password');
|
||||
}
|
||||
|
||||
public function content(): Content
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ class VerifyEmailMail extends Mailable implements ShouldQueue
|
|||
|
||||
public function envelope(): Envelope
|
||||
{
|
||||
return $this->mailboxEnvelope(MailPurpose::SYSTEM, __('verify_email.subject'));
|
||||
return $this->mailboxEnvelope(MailPurpose::SYSTEM, __('verify_email.subject'), 'verify-email');
|
||||
}
|
||||
|
||||
public function content(): Content
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ class CloudReady extends Notification
|
|||
{
|
||||
$url = 'https://'.$this->instance->subdomain.'.'.ProvisioningSettings::dnsZone();
|
||||
|
||||
[$from, $replyTo] = $this->mailboxAddresses(MailPurpose::PROVISIONING);
|
||||
[$from, $replyTo] = $this->mailboxAddresses(MailPurpose::PROVISIONING, 'cloud-ready');
|
||||
|
||||
$message = (new MailMessage)->mailer('cp_'.MailPurpose::PROVISIONING);
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,58 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services\Mail;
|
||||
|
||||
/**
|
||||
* Die eine Liste aller Mailarten, die dieses Haus verschickt.
|
||||
*
|
||||
* Es gab sie schon — als Array in MailPreviews, nur für die Vorschau. Mit der
|
||||
* Wegwahl je Mailart bräuchte es eine zweite, und die zweite Liste ist die, die
|
||||
* beim siebzehnten Mail vergessen wird. Also eine, aus der beide lesen.
|
||||
*
|
||||
* `purpose` ist die VORGABE: das Postfach, aus dem diese Mailart geht, solange
|
||||
* niemand etwas anderes einstellt. Sie muss dem entsprechen, was die Mailklasse
|
||||
* heute an mailboxEnvelope() übergibt — sonst ändert dieser Umbau still das
|
||||
* Verhalten.
|
||||
*
|
||||
* Reihenfolge wie in der Vorschau: Anmeldung, Kauf, Betrieb, Geld, Sicherheit.
|
||||
*/
|
||||
final class MailCatalogue
|
||||
{
|
||||
/** @return array<string, array{label: string, purpose: string}> */
|
||||
public static function all(): array
|
||||
{
|
||||
return [
|
||||
'verify-email' => ['label' => 'E-Mail bestätigen (Registrierung)', 'purpose' => MailPurpose::SYSTEM],
|
||||
'reset-password' => ['label' => 'Passwort zurücksetzen', 'purpose' => MailPurpose::SYSTEM],
|
||||
'order-confirmation' => ['label' => 'Bestellbestätigung', 'purpose' => MailPurpose::BILLING],
|
||||
'cloud-ready' => ['label' => 'Cloud ist bereit (Zugangsdaten)', 'purpose' => MailPurpose::PROVISIONING],
|
||||
'maintenance-announcement' => ['label' => 'Wartungsfenster angekündigt', 'purpose' => MailPurpose::MAINTENANCE],
|
||||
'maintenance-cancelled' => ['label' => 'Wartungsfenster abgesagt', 'purpose' => MailPurpose::MAINTENANCE],
|
||||
'invoice' => ['label' => 'Rechnung', 'purpose' => MailPurpose::BILLING],
|
||||
'new-device' => ['label' => 'Anmeldung von einem neuen Gerät', 'purpose' => MailPurpose::SYSTEM],
|
||||
'dormant-warning' => ['label' => 'Konto ohne Paket wird gelöscht', 'purpose' => MailPurpose::SYSTEM],
|
||||
'operator-message' => ['label' => 'Nachricht aus der Konsole', 'purpose' => MailPurpose::SUPPORT],
|
||||
// Die sechs Texte des Mahnlaufs. Vorschaubar, weil sie die
|
||||
// unangenehmsten sind, die dieses Haus verschickt — und die
|
||||
// einzigen, die ein Kunde bekommt, wenn ohnehin gerade etwas
|
||||
// schiefläuft. Alle sechs aus dem BILLING-Postfach: eine Mahnung
|
||||
// ist immer eine Frage übers Geld.
|
||||
'dunning-0' => ['label' => 'Zahlung fehlgeschlagen (Hinweis)', 'purpose' => MailPurpose::BILLING],
|
||||
'dunning-1' => ['label' => 'Zahlungserinnerung (1. Mahnung)', 'purpose' => MailPurpose::BILLING],
|
||||
'dunning-2' => ['label' => '2. Mahnung (mit Mahnspesen)', 'purpose' => MailPurpose::BILLING],
|
||||
'dunning-3' => ['label' => '3. Mahnung (letzte vor Abschaltung)', 'purpose' => MailPurpose::BILLING],
|
||||
'cloud-suspended' => ['label' => 'Cloud abgeschaltet', 'purpose' => MailPurpose::BILLING],
|
||||
'cloud-resumed' => ['label' => 'Cloud läuft wieder', 'purpose' => MailPurpose::BILLING],
|
||||
];
|
||||
}
|
||||
|
||||
public static function label(string $key): string
|
||||
{
|
||||
return self::all()[$key]['label'] ?? $key;
|
||||
}
|
||||
|
||||
public static function purpose(string $key): string
|
||||
{
|
||||
return self::all()[$key]['purpose'] ?? MailPurpose::SYSTEM;
|
||||
}
|
||||
}
|
||||
|
|
@ -54,32 +54,15 @@ class MailPreviews
|
|||
* The list, in the order somebody would read them: sign-up, purchase,
|
||||
* operation, money, security.
|
||||
*
|
||||
* Read from MailCatalogue rather than kept as a second list — that WAS
|
||||
* this list, before MailRoute needed a default purpose per mail type
|
||||
* too. Two lists is the shape of bug that forgets the seventeenth mail.
|
||||
*
|
||||
* @return array<string, string> key => label
|
||||
*/
|
||||
public function all(): array
|
||||
{
|
||||
return [
|
||||
'verify-email' => 'E-Mail bestätigen (Registrierung)',
|
||||
'reset-password' => 'Passwort zurücksetzen',
|
||||
'order-confirmation' => 'Bestellbestätigung',
|
||||
'cloud-ready' => 'Cloud ist bereit (Zugangsdaten)',
|
||||
'maintenance-announcement' => 'Wartungsfenster angekündigt',
|
||||
'maintenance-cancelled' => 'Wartungsfenster abgesagt',
|
||||
'invoice' => 'Rechnung',
|
||||
'new-device' => 'Anmeldung von einem neuen Gerät',
|
||||
'dormant-warning' => 'Konto ohne Paket wird gelöscht',
|
||||
'operator-message' => 'Nachricht aus der Konsole',
|
||||
// Die sechs Texte des Mahnlaufs. Vorschaubar, weil sie die
|
||||
// unangenehmsten sind, die dieses Haus verschickt — und die
|
||||
// einzigen, die ein Kunde bekommt, wenn ohnehin gerade etwas
|
||||
// schiefläuft.
|
||||
'dunning-0' => 'Zahlung fehlgeschlagen (Hinweis)',
|
||||
'dunning-1' => 'Zahlungserinnerung (1. Mahnung)',
|
||||
'dunning-2' => '2. Mahnung (mit Mahnspesen)',
|
||||
'dunning-3' => '3. Mahnung (letzte vor Abschaltung)',
|
||||
'cloud-suspended' => 'Cloud abgeschaltet',
|
||||
'cloud-resumed' => 'Cloud läuft wieder',
|
||||
];
|
||||
return array_map(fn (array $entry) => $entry['label'], MailCatalogue::all());
|
||||
}
|
||||
|
||||
public function has(string $key): bool
|
||||
|
|
@ -232,7 +215,7 @@ class MailPreviews
|
|||
|
||||
public function envelope(): Envelope
|
||||
{
|
||||
return $this->mailboxEnvelope(MailPurpose::PROVISIONING, __('provisioning.mail.ready_subject'));
|
||||
return $this->mailboxEnvelope(MailPurpose::PROVISIONING, __('provisioning.mail.ready_subject'), 'cloud-ready');
|
||||
}
|
||||
|
||||
public function content(): Content
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services\Mail;
|
||||
|
||||
use App\Models\Mailbox;
|
||||
use App\Support\Settings;
|
||||
|
||||
/**
|
||||
* Welches Postfach trägt diese Mailart.
|
||||
*
|
||||
* Eine Ebene ÜBER den Zwecken, kein Ersatz für sie. Ohne Eintrag entscheidet
|
||||
* weiter der Zweck — der Umbau ändert also am heutigen Verhalten nichts, und
|
||||
* genau das ist die Bedingung dafür, dass man ihn gefahrlos ausrollen kann.
|
||||
*/
|
||||
final class MailRoute
|
||||
{
|
||||
public static function settingKey(string $mailKey): string
|
||||
{
|
||||
return 'mail.route.'.$mailKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Das Postfach für diese Mailart — der Eintrag, sonst der Zweck.
|
||||
*
|
||||
* Ein Eintrag, der auf ein gelöschtes oder abgeschaltetes Postfach zeigt,
|
||||
* fällt auf den Zweck zurück. Eine Mail, die nicht rausgeht, ist schlimmer
|
||||
* als eine aus der zweitbesten Adresse.
|
||||
*/
|
||||
public static function purposeOrMailbox(string $mailKey, string $purpose): ?Mailbox
|
||||
{
|
||||
$key = (string) Settings::get(self::settingKey($mailKey), '');
|
||||
|
||||
if ($key !== '') {
|
||||
$box = Mailbox::findByKey($key);
|
||||
|
||||
if ($box !== null && $box->active) {
|
||||
return $box;
|
||||
}
|
||||
}
|
||||
|
||||
return app(MailboxResolver::class)->for($purpose);
|
||||
}
|
||||
}
|
||||
|
|
@ -55,6 +55,10 @@ return [
|
|||
'system_inactive' => '„System" braucht ein aktives Postfach — es ist der Rückfall, auf den alles andere angewiesen ist.',
|
||||
'purpose_unknown_mailbox' => 'Dieses Postfach existiert nicht mehr. Bitte eines aus der Liste wählen.',
|
||||
|
||||
'routes_title' => 'Wegwahl je Mailart',
|
||||
'routes_hint' => 'Ohne Eintrag entscheidet der Zweck oben. Nur für die eine Mailart, die anders soll — der Nachbar bleibt, wo er war.',
|
||||
'route_default' => 'wie der Zweck (:purpose)',
|
||||
|
||||
'test' => 'Testmail senden',
|
||||
'testing' => 'Wird gesendet…',
|
||||
'test_recipient' => 'Testempfänger',
|
||||
|
|
|
|||
|
|
@ -55,6 +55,10 @@ return [
|
|||
'system_inactive' => '"System" needs a mailbox that is active — it is the fallback everything else depends on.',
|
||||
'purpose_unknown_mailbox' => 'That mailbox no longer exists. Please choose one from the list.',
|
||||
|
||||
'routes_title' => 'Routing by mail type',
|
||||
'routes_hint' => 'With no entry, the purpose above decides. Only for the one mail type that should differ — its neighbours stay where they were.',
|
||||
'route_default' => 'same as the purpose (:purpose)',
|
||||
|
||||
'test' => 'Send test mail',
|
||||
'testing' => 'Sending…',
|
||||
'test_recipient' => 'Test recipient',
|
||||
|
|
|
|||
|
|
@ -169,4 +169,31 @@
|
|||
{{ __('mail_settings.save') }}
|
||||
</x-ui.button>
|
||||
</x-ui.card>
|
||||
|
||||
{{-- 4. Die Wegwahl je Mailart. Eine Ebene ÜBER den Zwecken oben, kein
|
||||
Ersatz — leer heißt „wie der Zweck". Ein <select> pro Zeile, R20's
|
||||
stated exception, genau wie die Zuordnung darüber. --}}
|
||||
<x-ui.card>
|
||||
<h2 class="text-lg font-semibold text-ink">{{ __('mail_settings.routes_title') }}</h2>
|
||||
<p class="mt-1 text-sm text-muted">{{ __('mail_settings.routes_hint') }}</p>
|
||||
|
||||
<div class="mt-5 space-y-3">
|
||||
@foreach ($mailCatalogue as $key => $entry)
|
||||
<div class="flex items-center justify-between gap-6 border-b border-line py-2">
|
||||
<span class="text-sm text-ink">{{ $entry['label'] }}</span>
|
||||
<select wire:model="routes.{{ $key }}"
|
||||
class="w-64 rounded-md border border-line-strong bg-surface px-3 py-2 text-sm text-body">
|
||||
<option value="">{{ __('mail_settings.route_default', ['purpose' => __('mail_settings.purpose.'.$entry['purpose'])]) }}</option>
|
||||
@foreach ($mailboxes->where('active', true) as $box)
|
||||
<option value="{{ $box->key }}">{{ $box->address }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
<x-ui.button wire:click="saveRoutes" variant="primary" size="md" class="mt-5">
|
||||
{{ __('mail_settings.save') }}
|
||||
</x-ui.button>
|
||||
</x-ui.card>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,69 @@
|
|||
<?php // tests/Feature/Admin/MailRoutingTest.php
|
||||
|
||||
use App\Models\Mailbox;
|
||||
use App\Services\Mail\MailCatalogue;
|
||||
use App\Services\Mail\MailPurpose;
|
||||
use App\Services\Mail\MailRoute;
|
||||
use App\Support\Settings;
|
||||
|
||||
// Blank slate statt der migrationseigenen fuenf (no-reply, support, billing,
|
||||
// office, info): drei der Tests unten legen selbst ein Postfach mit dem
|
||||
// Schluessel 'no-reply' an, und das kollidiert sonst mit mailboxes.key's
|
||||
// Unique-Constraint gegen die von RefreshDatabase stehen gelassene Saat. Der
|
||||
// Helfer existiert schon in tests/Pest.php, genau fuer diesen Fall (siehe
|
||||
// tests/Feature/Mail/MailSettingsPageTest.php).
|
||||
beforeEach(fn () => clearMailboxSeed());
|
||||
|
||||
it('schickt ohne Eintrag genau dorthin, wo die Mail vorher hinging', function () {
|
||||
// Der wichtigste Test dieser Aufgabe: der Umbau darf am heutigen Verhalten
|
||||
// NICHTS ändern, solange niemand etwas einstellt. Sonst wandern beim
|
||||
// Ausrollen still die Rechnungen in ein anderes Postfach.
|
||||
$system = Mailbox::factory()->create(['key' => 'no-reply', 'active' => true]);
|
||||
Settings::set(MailPurpose::settingKey(MailPurpose::SYSTEM), 'no-reply');
|
||||
|
||||
expect(MailRoute::purposeOrMailbox('new-device', MailPurpose::SYSTEM)?->id)
|
||||
->toBe($system->id);
|
||||
});
|
||||
|
||||
it('legt eine einzelne Mailart auf ein anderes Postfach', function () {
|
||||
Mailbox::factory()->create(['key' => 'no-reply', 'active' => true]);
|
||||
$info = Mailbox::factory()->create(['key' => 'info', 'active' => true]);
|
||||
Settings::set(MailPurpose::settingKey(MailPurpose::SYSTEM), 'no-reply');
|
||||
Settings::set(MailRoute::settingKey('new-device'), 'info');
|
||||
|
||||
// Und nur diese eine: der Nachbar bleibt, wo er war. Mit einem EIGENEN
|
||||
// Postfach fuer Abrechnung — ohne das waere die Zusicherung leer, weil der
|
||||
// Resolver dann null liefert und null nun einmal nicht 'info' ist.
|
||||
$billing = Mailbox::factory()->create(['key' => 'billing', 'active' => true]);
|
||||
Settings::set(MailPurpose::settingKey(MailPurpose::BILLING), 'billing');
|
||||
|
||||
expect(MailRoute::purposeOrMailbox('new-device', MailPurpose::SYSTEM)?->id)->toBe($info->id)
|
||||
->and(MailRoute::purposeOrMailbox('invoice', MailPurpose::BILLING)?->id)->toBe($billing->id);
|
||||
});
|
||||
|
||||
it('faellt auf den Zweck zurueck, wenn das eingetragene Postfach abgeschaltet ist', function () {
|
||||
// Eine Mail, die nicht rausgeht, ist schlimmer als eine aus der zweitbesten
|
||||
// Adresse.
|
||||
Mailbox::factory()->create(['key' => 'no-reply', 'active' => true]);
|
||||
Mailbox::factory()->create(['key' => 'info', 'active' => false]);
|
||||
Settings::set(MailPurpose::settingKey(MailPurpose::SYSTEM), 'no-reply');
|
||||
Settings::set(MailRoute::settingKey('new-device'), 'info');
|
||||
|
||||
expect(MailRoute::purposeOrMailbox('new-device', MailPurpose::SYSTEM)?->key)->toBe('no-reply');
|
||||
});
|
||||
|
||||
it('kennt zu jeder Mailart einen Vorgabe-Zweck', function () {
|
||||
// Ohne Vorgabe stünde eine Mailart ohne Postfach da, sobald jemand die
|
||||
// Wegwahl leert.
|
||||
foreach (MailCatalogue::all() as $key => $eintrag) {
|
||||
expect($eintrag['purpose'])->toBeIn(MailPurpose::ALL, "[{$key}] hat keinen gueltigen Vorgabe-Zweck.");
|
||||
expect($eintrag['label'])->not->toBe('');
|
||||
}
|
||||
});
|
||||
|
||||
it('fuehrt die Liste der Mailarten nur an EINER Stelle', function () {
|
||||
// Zwei Listen bedeuten, dass die zweite beim siebzehnten Mail vergessen
|
||||
// wird. Die Vorschau muss aus dem Katalog lesen.
|
||||
expect(array_keys(app(\App\Services\Mail\MailPreviews::class)->all()))
|
||||
->toEqualCanonicalizing(array_keys(MailCatalogue::all()));
|
||||
});
|
||||
Loading…
Reference in New Issue