admin: Verlauf protokolliert jede versendete E-Mail

Neuer Listener RecordMailActivity am MessageSent-Event erfasst zentral
alle Versandwege (Mailables, Notifications, Fortify, Queue-Mails beim
realen Versand im Worker). Neuer ActivityType "E-Mail" mit Empfänger,
Betreff und Mailable-Klasse in den Metadaten; bekannte Empfänger werden
mit ihrem User verknüpft.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-07-21 09:13:43 +00:00
parent e3ce626b73
commit d411e6ce7f
6 changed files with 165 additions and 74 deletions

View file

@ -20,6 +20,7 @@ enum ActivityType: string
case TerminationRequest = 'termination_request';
case NewsletterSubscription = 'newsletter_subscription';
case SubscriptionChange = 'subscription_change';
case MailSent = 'mail_sent';
public function label(): string
{
@ -35,6 +36,7 @@ enum ActivityType: string
self::TerminationRequest => 'Kündigung',
self::NewsletterSubscription => 'Newsletter',
self::SubscriptionChange => 'Abo',
self::MailSent => 'E-Mail',
};
}
@ -50,6 +52,7 @@ enum ActivityType: string
self::BookingPaid, self::CreditTopup => 'amber',
self::ContactRequest, self::NewsletterSubscription => 'cyan',
self::LegalRequest, self::WithdrawalRequest, self::TerminationRequest, self::SubscriptionChange => 'red',
self::MailSent => 'purple',
};
}
}

View file

@ -0,0 +1,57 @@
<?php
namespace App\Listeners;
use App\Enums\ActivityType;
use App\Models\User;
use App\Services\ActivityLog;
use Illuminate\Mail\Events\MessageSent;
/**
* Protokolliert jede tatsächlich versendete E-Mail im Aktivitäts-Verlauf.
*
* Hängt zentral am MessageSent-Event des Mailers und erfasst damit alle
* Versandwege (Mailables, Notifications, Fortify). Bei Queue-Mails feuert
* das Event erst beim realen Versand im Worker — der Eintrag entsteht also
* zum echten Versandzeitpunkt.
*
* Die Methode heißt bewusst NICHT `handle`: Laravels Event-Discovery würde
* sie sonst zusätzlich zur manuellen Verdrahtung im AppServiceProvider
* registrieren (doppelte Einträge).
*/
class RecordMailActivity
{
public function __construct(private readonly ActivityLog $activityLog) {}
public function recordSent(MessageSent $event): void
{
$recipients = array_values(array_map(
fn ($address): string => $address->getAddress(),
$event->message->getTo() ?? [],
));
if ($recipients === []) {
return;
}
$subject = (string) $event->message->getSubject();
$user = User::query()->whereIn('email', $recipients)->first();
$this->activityLog->record(
ActivityType::MailSent,
sprintf(
'E-Mail „%s" an %s versendet.',
$subject !== '' ? $subject : __('(ohne Betreff)'),
implode(', ', $recipients),
),
user: $user,
actorLabel: $user?->name ?? $recipients[0],
meta: array_filter([
'to' => $recipients,
'subject' => $subject,
'mailable' => $event->data['__laravel_mailable'] ?? null,
'notification' => $event->data['__laravel_notification'] ?? null,
], fn ($value): bool => $value !== null && $value !== ''),
);
}
}

View file

@ -11,6 +11,7 @@ use App\Http\Middleware\LogSlowAdminRequests;
use App\Http\Middleware\SetEdition;
use App\Listeners\ActivateUserAfterVerification;
use App\Listeners\RecordAuthActivity;
use App\Listeners\RecordMailActivity;
use App\Listeners\SyncCompanyMembershipsOnLogin;
use App\Models\AdminPreset;
use App\Models\Category;
@ -38,6 +39,7 @@ use Illuminate\Auth\Notifications\ResetPassword;
use Illuminate\Auth\Notifications\VerifyEmail;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Events\QueryExecuted;
use Illuminate\Mail\Events\MessageSent;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Queue\Events\JobFailed;
use Illuminate\Support\Facades\DB;
@ -129,6 +131,9 @@ class AppServiceProvider extends ServiceProvider
Event::listen(Registered::class, [RecordAuthActivity::class, 'recordRegistered']);
Event::listen(Login::class, [RecordAuthActivity::class, 'recordLogin']);
// Jede tatsächlich versendete E-Mail landet ebenfalls im Verlauf.
Event::listen(MessageSent::class, [RecordMailActivity::class, 'recordSent']);
// Framework-Auth-Mails (Passwort-Reset, E-Mail-Verifizierung) im
// pressekonto-CI rendern statt im Laravel-Standard-Markdown-Theme —
// gleiches Layout wie alle übrigen System-Mails (emails/layout).