From 5bdc94c84ca27530389e0b7ac942fb80f93373bd Mon Sep 17 00:00:00 2001 From: Rutger Meekers Date: Wed, 14 Oct 2026 13:50:39 +0200 Subject: [PATCH] fix(mails): resend each selected mail to its own recipients The bulk Resend action prefilled one recipient form with every address of every selected mail and then sent each selected mail to that whole list. Resending mails that went to different people therefore delivered each of them to all the other recipients as well, which leaks personal content between recipients. The bulk action now sends every selected mail again to its own to, cc and bcc, without a shared form. Changing the recipients stays possible on the single Resend action. --- src/Resources/MailResource.php | 54 +++++++-------------------------- tests/BulkResendTest.php | 55 ++++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 43 deletions(-) create mode 100644 tests/BulkResendTest.php diff --git a/src/Resources/MailResource.php b/src/Resources/MailResource.php index 4192b9e..96c3312 100644 --- a/src/Resources/MailResource.php +++ b/src/Resources/MailResource.php @@ -555,11 +555,18 @@ public static function table(Table $table): Table ->label(__('Resend')) ->icon('heroicon-o-arrow-uturn-right') ->requiresConfirmation() - ->modalDescription(__('Are you sure you want to resend the selected mails?')) - ->schema(fn ($records) => self::getBulkResendForm($records)) - ->action(function (Collection $records, array $data) { + ->modalDescription(__('Each selected mail is sent again to its own recipients.')) + ->action(function (Collection $records) { + // Each mail goes to its own recipients only. A shared + // recipient list would send every selected mail to + // the addresses of all the others. foreach ($records as $record) { - (new ResendMail)->handle($record, $data['to'], $data['cc'] ?? [], $data['bcc'] ?? []); + (new ResendMail)->handle( + $record, + array_keys($record->to ?: []), + array_keys($record->cc ?: []), + array_keys($record->bcc ?: []), + ); } Notification::make() @@ -595,45 +602,6 @@ private static function getResendForm(): array ]; } - private static function getBulkResendForm($records): array - { - $extractEmails = function ($records, $field) { - return collect($records) - ->map(fn ($record) => array_keys($record->{$field} ?? [])) - ->flatten() - ->unique() - ->toArray(); - }; - - $toEmails = $extractEmails($records, 'to'); - $ccEmails = $extractEmails($records, 'cc'); - $bccEmails = $extractEmails($records, 'bcc'); - - return [ - TagsInput::make('to') - ->placeholder(__('Recipient(s)')) - ->label(__('Recipient(s)')) - ->default($toEmails) - ->required() - ->rules(['array']) - ->nestedRecursiveRules(['email:rfc,dns']), - - TagsInput::make('cc') - ->placeholder(__('CC')) - ->label(__('CC')) - ->default($ccEmails) - ->rules(['nullable', 'array']) - ->nestedRecursiveRules(['email:rfc,dns']), - - TagsInput::make('bcc') - ->placeholder(__('BCC')) - ->label(__('BCC')) - ->default($bccEmails) - ->rules(['nullable', 'array']) - ->nestedRecursiveRules(['email:rfc,dns']), - ]; - } - public static function getPages(): array { return [ diff --git a/tests/BulkResendTest.php b/tests/BulkResendTest.php new file mode 100644 index 0000000..2a83046 --- /dev/null +++ b/tests/BulkResendTest.php @@ -0,0 +1,55 @@ + (string) Str::uuid(), + 'mailer' => 'smtp', + 'subject' => 'Your link', + 'to' => $to, + 'cc' => $cc, + 'html' => '

Your link

', + ]); +} + +function recipientsOf(ResendMailJob $job): array +{ + $read = fn (string $property) => (new ReflectionProperty($job, $property))->getValue($job); + + return ['to' => $read('to'), 'cc' => $read('cc')]; +} + +it('resends each selected mail to its own recipients only', function () { + Queue::fake(); + $this->actingAs(User::create(['name' => 'Test User', 'email' => 'test@example.com', 'password' => bcrypt('password')])); + + $first = sentMail(['ann@one.test' => 'Ann'], ['boss@one.test' => null]); + $second = sentMail(['bob@two.test' => 'Bob']); + + $resend = collect(MailResource::table(Table::make(new ListMails))->getFlatBulkActions()) + ->first(fn ($action) => $action->getName() === 'resend'); + + $resend->evaluate($resend->getActionFunction(), ['records' => new Collection([$first, $second])]); + + $sent = []; + Queue::assertPushed(ResendMailJob::class, function (ResendMailJob $job) use (&$sent) { + $sent[] = recipientsOf($job); + + return true; + }); + + expect($sent)->toEqualCanonicalizing([ + ['to' => ['ann@one.test'], 'cc' => ['boss@one.test']], + ['to' => ['bob@two.test'], 'cc' => []], + ]); +});