From e4184d782c08731a64340a5457295297183bcd09 Mon Sep 17 00:00:00 2001 From: danilopolani Date: Thu, 26 Oct 2023 17:27:38 +0200 Subject: [PATCH 1/8] allow multiple cc and bcc --- .../6_transform_logs_cc_bcc_array.php | 41 +++++++ phpunit.xml.dist | 10 +- src/Dto/GenericMailDto.php | 6 +- src/Dto/RecipientDto.php | 10 +- src/Dto/SendMailDto.php | 10 +- src/Http/Requests/SendMailRequest.php | 37 ++++++- src/Mail/GenericMail.php | 4 +- src/Models/Casts/CollectionOfContacts.php | 37 +++++++ src/Models/Log.php | 10 +- .../Logs/CreateFromGenericMailTest.php | 104 +++++++++++------- 10 files changed, 207 insertions(+), 62 deletions(-) create mode 100644 database/migrations/6_transform_logs_cc_bcc_array.php create mode 100644 src/Models/Casts/CollectionOfContacts.php diff --git a/database/migrations/6_transform_logs_cc_bcc_array.php b/database/migrations/6_transform_logs_cc_bcc_array.php new file mode 100644 index 0000000..a68968c --- /dev/null +++ b/database/migrations/6_transform_logs_cc_bcc_array.php @@ -0,0 +1,41 @@ +getTable(); + + DB::table($tableName)->each(function (object $log) use ($tableName) { + $changes = []; + + if (!is_null($log->cc)) { + $changes['cc'] = [$log->cc]; + } + + if (!is_null($log->bcc)) { + $changes['bcc'] = [$log->bcc]; + } + + if (!empty($changes)) { + DB::table($tableName) + ->where('id', $log->id) + ->update($changes); + } + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + // + } +}; diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 7547831..3bb11a2 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,14 +1,11 @@ - + tests - - ./src - @@ -22,4 +19,9 @@ + + + ./src + + diff --git a/src/Dto/GenericMailDto.php b/src/Dto/GenericMailDto.php index ada6805..e5883e3 100644 --- a/src/Dto/GenericMailDto.php +++ b/src/Dto/GenericMailDto.php @@ -21,9 +21,11 @@ class GenericMailDto extends DataTransferObject public ?ContactDto $sender; - public ?ContactDto $cc; + /** @var \MailCarrier\Dto\ContactDto[]|null */ + public ?array $cc; - public ?ContactDto $bcc; + /** @var \MailCarrier\Dto\ContactDto[]|null */ + public ?array $bcc; public Template $template; diff --git a/src/Dto/RecipientDto.php b/src/Dto/RecipientDto.php index 743ce01..84013a7 100644 --- a/src/Dto/RecipientDto.php +++ b/src/Dto/RecipientDto.php @@ -16,11 +16,13 @@ class RecipientDto extends DataTransferObject /** @var array */ public array $variables = []; - #[CastWith(ContactStringCaster::class)] - public ?ContactDto $cc; + /** @var \MailCarrier\Dto\ContactDto[]|null */ + #[CastWith(ArrayCaster::class, itemType: ContactStringCaster::class)] + public ?array $cc; - #[CastWith(ContactStringCaster::class)] - public ?ContactDto $bcc; + /** @var \MailCarrier\Dto\ContactDto[]|null */ + #[CastWith(ArrayCaster::class, itemType: ContactStringCaster::class)] + public ?array $bcc; /** @var \Illuminate\Http\UploadedFile[] */ public array $attachments = []; diff --git a/src/Dto/SendMailDto.php b/src/Dto/SendMailDto.php index daa8eb2..1749536 100644 --- a/src/Dto/SendMailDto.php +++ b/src/Dto/SendMailDto.php @@ -20,11 +20,13 @@ class SendMailDto extends DataTransferObject public ?string $recipient; - #[CastWith(ContactStringCaster::class)] - public ?ContactDto $cc; + /** @var \MailCarrier\Dto\ContactDto[]|null */ + #[CastWith(ArrayCaster::class, itemType: ContactStringCaster::class)] + public ?array $cc; - #[CastWith(ContactStringCaster::class)] - public ?ContactDto $bcc; + /** @var \MailCarrier\Dto\ContactDto[]|null */ + #[CastWith(ArrayCaster::class, itemType: ContactStringCaster::class)] + public ?array $bcc; /** @var array */ public array $variables = []; diff --git a/src/Http/Requests/SendMailRequest.php b/src/Http/Requests/SendMailRequest.php index b81f28b..a12dacb 100644 --- a/src/Http/Requests/SendMailRequest.php +++ b/src/Http/Requests/SendMailRequest.php @@ -26,8 +26,10 @@ public function rules(): array 'trigger' => 'sometimes|string|max:255', 'subject' => 'required|string|max:255', 'sender' => ['sometimes', new ContactRule()], - 'cc' => ['sometimes', new ContactRule()], - 'bcc' => ['sometimes', new ContactRule()], + 'cc' => 'sometimes|array', + 'bcc' => 'sometimes|array', + 'cc.*' => [new ContactRule()], + 'bcc.*' => [new ContactRule()], 'variables' => 'sometimes|array', 'tags' => 'sometimes|array', 'metadata' => 'sometimes|array', @@ -65,8 +67,10 @@ public function rules(): array Rule::when($this->has('recipients') && is_array($this->json('recipients.0')), 'required|email'), ], 'recipients.*.variables' => 'sometimes|array', - 'recipients.*.cc' => ['sometimes', new ContactRule()], - 'recipients.*.bcc' => ['sometimes', new ContactRule()], + 'recipients.*.cc' => 'sometimes|array', + 'recipients.*.cc.*' => ['sometimes', new ContactRule()], + 'recipients.*.bcc' => 'sometimes|array', + 'recipients.*.bcc.*' => ['sometimes', new ContactRule()], // Recipients attachments 'recipients.*.attachments' => 'sometimes|array', @@ -106,6 +110,20 @@ protected function prepareForValidation(): void ]); } + // Wrap cc array list + if (!array_is_list($this->input('cc'))) { + $this->merge([ + 'cc' => [$this->input('cc')], + ]); + } + + // Wrap bcc array list + if (!array_is_list($this->input('bcc'))) { + $this->merge([ + 'bcc' => [$this->input('bcc')], + ]); + } + // Wrap recipient array list into a structured data if (is_array($this->input('recipients')) && !is_array($this->json('recipients.0'))) { $this->merge([ @@ -118,6 +136,17 @@ protected function prepareForValidation(): void ]); } + // Wrap recipient cc and bcc + foreach ($this->input('recipients') as $recipient) { + if (array_key_exists('cc', $recipient) && !array_is_list($recipient['cc'])) { + $recipient['cc'] = [$recipient['cc']]; + } + + if (array_key_exists('bcc', $recipient) && !array_is_list($recipient['bcc'])) { + $recipient['bcc'] = [$recipient['bcc']]; + } + } + // Wrap remote attachments array list into a structured data if (is_array($this->input('remoteAttachments')) && !is_array($this->json('remoteAttachments.0'))) { $this->merge([ diff --git a/src/Mail/GenericMail.php b/src/Mail/GenericMail.php index 0eedb7d..57859ef 100644 --- a/src/Mail/GenericMail.php +++ b/src/Mail/GenericMail.php @@ -65,11 +65,11 @@ public function build(): self ) ->when( $this->params->cc, - fn (GenericMail $mail) => $mail->cc($this->params->cc->email, $this->params->cc->name) + fn (GenericMail $mail) => $mail->cc($this->params->cc) ) ->when( $this->params->bcc, - fn (GenericMail $mail) => $mail->bcc($this->params->bcc->email, $this->params->bcc->name) + fn (GenericMail $mail) => $mail->bcc($this->params->bcc) ); } diff --git a/src/Models/Casts/CollectionOfContacts.php b/src/Models/Casts/CollectionOfContacts.php new file mode 100644 index 0000000..95669e1 --- /dev/null +++ b/src/Models/Casts/CollectionOfContacts.php @@ -0,0 +1,37 @@ + $attributes + * @return \Illuminate\Support\Collection|null + */ + public function get(Model $model, string $key, mixed $value, array $attributes): ?Collection + { + if (is_null($value)) { + return null; + } + + return Collection::make(json_decode($value, true)) + ->map(fn (array $value) => new ContactDto($value)); + } + + /** + * Prepare the given value for storage. + * + * @param array $attributes + */ + public function set(Model $model, string $key, mixed $value, array $attributes): string + { + return json_encode($value); + } +} diff --git a/src/Models/Log.php b/src/Models/Log.php index 1a78b34..ab76b3b 100644 --- a/src/Models/Log.php +++ b/src/Models/Log.php @@ -9,10 +9,12 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasMany; +use Illuminate\Support\Collection; use Illuminate\Support\Facades\Config; use MailCarrier\Dto\ContactDto; use MailCarrier\Dto\LogTemplateDto; use MailCarrier\Enums\LogStatus; +use MailCarrier\Models\Casts\CollectionOfContacts; use MailCarrier\Models\Concerns\IsUuid; /** @@ -21,8 +23,8 @@ * @property string|null $trigger * @property string|null $subject * @property \MailCarrier\Dto\ContactDto $sender - * @property \MailCarrier\Dto\ContactDto $cc - * @property \MailCarrier\Dto\ContactDto $bcc + * @property \Illuminate\Support\Collection|null $cc + * @property \Illuminate\Support\Collection|null $bcc * @property string $recipient * @property \MailCarrier\Dto\LogTemplateDto $template_frozen * @property array|null $variables @@ -82,8 +84,8 @@ class Log extends Model protected $casts = [ 'status' => LogStatus::class, 'sender' => ContactDto::class, - 'cc' => ContactDto::class, - 'bcc' => ContactDto::class, + 'cc' => CollectionOfContacts::class, + 'bcc' => CollectionOfContacts::class, 'template_frozen' => LogTemplateDto::class, 'variables' => 'array', ]; diff --git a/tests/Feature/Logs/CreateFromGenericMailTest.php b/tests/Feature/Logs/CreateFromGenericMailTest.php index 942c014..b26c5f2 100644 --- a/tests/Feature/Logs/CreateFromGenericMailTest.php +++ b/tests/Feature/Logs/CreateFromGenericMailTest.php @@ -29,12 +29,19 @@ sender: new ContactDto( email: 'sender@example.org', ), - cc: new ContactDto( - email: 'cc@example.org', - ), - bcc: new ContactDto( - email: 'bcc@example.org', - ), + cc: [ + new ContactDto( + email: 'cc@example.org', + ), + ], + bcc: [ + new ContactDto( + email: 'bcc@example.org', + ), + new ContactDto( + email: 'bcc2@example.org', + ), + ], template: $template, variables: ['name' => 'foo'], )); @@ -42,8 +49,9 @@ expect($log->trigger)->toBe('test'); expect($log->subject)->toBe('Welcome'); expect($log->sender->email)->toBe('sender@example.org'); - expect($log->cc->email)->toBe('cc@example.org'); - expect($log->bcc->email)->toBe('bcc@example.org'); + expect($log->cc[0]->email)->toBe('cc@example.org'); + expect($log->bcc[0]->email)->toBe('bcc@example.org'); + expect($log->bcc[1]->email)->toBe('bcc2@example.org'); expect($log->variables)->tobe(['name' => 'foo']); expect($log->template->id)->toBe($template->id); expect($log->status)->toBe($expected); @@ -68,12 +76,16 @@ subject: 'Welcome', error: null, recipient: 'foo@example.org', - cc: new ContactDto( - email: 'cc@example.org', - ), - bcc: new ContactDto( - email: 'bcc@example.org', - ), + cc: [ + new ContactDto( + email: 'cc@example.org', + ), + ], + bcc: [ + new ContactDto( + email: 'bcc@example.org', + ), + ], template: $template, variables: ['name' => 'foo'], ); @@ -110,12 +122,16 @@ sender: new ContactDto( email: 'sender@example.org', ), - cc: new ContactDto( - email: 'cc@example.org', - ), - bcc: new ContactDto( - email: 'bcc@example.org', - ), + cc: [ + new ContactDto( + email: 'cc@example.org', + ), + ], + bcc: [ + new ContactDto( + email: 'bcc@example.org', + ), + ], template: $template, variables: ['name' => 'foo'], )); @@ -148,12 +164,16 @@ sender: new ContactDto( email: 'sender@example.org', ), - cc: new ContactDto( - email: 'cc@example.org', - ), - bcc: new ContactDto( - email: 'bcc@example.org', - ), + cc: [ + new ContactDto( + email: 'cc@example.org', + ), + ], + bcc: [ + new ContactDto( + email: 'bcc@example.org', + ), + ], template: Template::factory()->create(), variables: ['name' => 'foo'], attachments: [ @@ -243,12 +263,16 @@ sender: new ContactDto( email: 'sender@example.org', ), - cc: new ContactDto( - email: 'cc@example.org', - ), - bcc: new ContactDto( - email: 'bcc@example.org', - ), + cc: [ + new ContactDto( + email: 'cc@example.org', + ), + ], + bcc: [ + new ContactDto( + email: 'bcc@example.org', + ), + ], template: Template::factory()->create(), variables: ['name' => 'foo'], attachments: [ @@ -348,12 +372,16 @@ sender: new ContactDto( email: 'sender@example.org', ), - cc: new ContactDto( - email: 'cc@example.org', - ), - bcc: new ContactDto( - email: 'bcc@example.org', - ), + cc: [ + new ContactDto( + email: 'cc@example.org', + ), + ], + bcc: [ + new ContactDto( + email: 'bcc@example.org', + ), + ], template: Template::factory()->create(), variables: ['name' => 'foo'], attachments: [ From 9e5c5f3f34f1760027f3978331f90d6fcf1f2fb0 Mon Sep 17 00:00:00 2001 From: danilopolani Date: Thu, 26 Oct 2023 17:30:23 +0200 Subject: [PATCH 2/8] fix tests about generic mail --- .../Logs/CreateFromGenericMailTest.php | 25 ++++++++++++++++--- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/tests/Feature/Logs/CreateFromGenericMailTest.php b/tests/Feature/Logs/CreateFromGenericMailTest.php index b26c5f2..e1f32cc 100644 --- a/tests/Feature/Logs/CreateFromGenericMailTest.php +++ b/tests/Feature/Logs/CreateFromGenericMailTest.php @@ -80,6 +80,9 @@ new ContactDto( email: 'cc@example.org', ), + new ContactDto( + email: 'cc2@example.org', + ), ], bcc: [ new ContactDto( @@ -98,8 +101,9 @@ expect($log->subject)->toBe('Welcome'); expect($log->sender->name)->toBe('MailCarrier'); expect($log->sender->email)->toBe('no-reply@mailcarrier.app'); - expect($log->cc->email)->toBe('cc@example.org'); - expect($log->bcc->email)->toBe('bcc@example.org'); + expect($log->cc[0]->email)->toBe('cc@example.org'); + expect($log->cc[1]->email)->toBe('cc2@example.org'); + expect($log->bcc[0]->email)->toBe('bcc@example.org'); expect($log->variables)->tobe(['name' => 'foo']); expect($log->template->id)->toBe($template->id); expect($log->status)->toBe(LogStatus::Pending); @@ -131,6 +135,9 @@ new ContactDto( email: 'bcc@example.org', ), + new ContactDto( + email: 'bcc2@example.org', + ), ], template: $template, variables: ['name' => 'foo'], @@ -139,8 +146,9 @@ expect($log->trigger)->toBe('test'); expect($log->subject)->toBe('Welcome'); expect($log->sender->email)->toBe('sender@example.org'); - expect($log->cc->email)->toBe('cc@example.org'); - expect($log->bcc->email)->toBe('bcc@example.org'); + expect($log->cc[0]->email)->toBe('cc@example.org'); + expect($log->bcc[0]->email)->toBe('bcc@example.org'); + expect($log->bcc[1]->email)->toBe('bcc2@example.org'); expect($log->variables)->tobe(['name' => 'foo']); expect($log->template->id)->toBe($template->id); expect($log->template_frozen->name)->toBe('template'); @@ -168,6 +176,9 @@ new ContactDto( email: 'cc@example.org', ), + new ContactDto( + email: 'cc2@example.org', + ), ], bcc: [ new ContactDto( @@ -272,6 +283,9 @@ new ContactDto( email: 'bcc@example.org', ), + new ContactDto( + email: 'bcc2@example.org', + ), ], template: Template::factory()->create(), variables: ['name' => 'foo'], @@ -376,6 +390,9 @@ new ContactDto( email: 'cc@example.org', ), + new ContactDto( + email: 'cc2@example.org', + ), ], bcc: [ new ContactDto( From 7a01d0f93f827bd6232ff2b183694d0ea2f6aff0 Mon Sep 17 00:00:00 2001 From: danilopolani Date: Fri, 27 Oct 2023 12:21:11 +0200 Subject: [PATCH 3/8] test that multiple cc and bcc are allowed --- src/Dto/RecipientDto.php | 6 +- src/Dto/SendMailDto.php | 5 +- src/Http/Requests/SendMailRequest.php | 30 +++++--- tests/Feature/SendMailTest.php | 100 ++++++++++++++++++++++++-- 4 files changed, 119 insertions(+), 22 deletions(-) diff --git a/src/Dto/RecipientDto.php b/src/Dto/RecipientDto.php index 84013a7..8121b97 100644 --- a/src/Dto/RecipientDto.php +++ b/src/Dto/RecipientDto.php @@ -2,7 +2,7 @@ namespace MailCarrier\Dto; -use MailCarrier\Dto\Casters\ContactStringCaster; +use MailCarrier\Dto\Casters\ContactArrayCaster; use MailCarrier\Dto\Validators\Email; use Spatie\DataTransferObject\Attributes\CastWith; use Spatie\DataTransferObject\Casters\ArrayCaster; @@ -17,11 +17,11 @@ class RecipientDto extends DataTransferObject public array $variables = []; /** @var \MailCarrier\Dto\ContactDto[]|null */ - #[CastWith(ArrayCaster::class, itemType: ContactStringCaster::class)] + #[CastWith(ContactArrayCaster::class)] public ?array $cc; /** @var \MailCarrier\Dto\ContactDto[]|null */ - #[CastWith(ArrayCaster::class, itemType: ContactStringCaster::class)] + #[CastWith(ContactArrayCaster::class)] public ?array $bcc; /** @var \Illuminate\Http\UploadedFile[] */ diff --git a/src/Dto/SendMailDto.php b/src/Dto/SendMailDto.php index 1749536..d4bc6f3 100644 --- a/src/Dto/SendMailDto.php +++ b/src/Dto/SendMailDto.php @@ -2,6 +2,7 @@ namespace MailCarrier\Dto; +use MailCarrier\Dto\Casters\ContactArrayCaster; use MailCarrier\Dto\Casters\ContactStringCaster; use Spatie\DataTransferObject\Attributes\CastWith; use Spatie\DataTransferObject\Casters\ArrayCaster; @@ -21,11 +22,11 @@ class SendMailDto extends DataTransferObject public ?string $recipient; /** @var \MailCarrier\Dto\ContactDto[]|null */ - #[CastWith(ArrayCaster::class, itemType: ContactStringCaster::class)] + #[CastWith(ContactArrayCaster::class)] public ?array $cc; /** @var \MailCarrier\Dto\ContactDto[]|null */ - #[CastWith(ArrayCaster::class, itemType: ContactStringCaster::class)] + #[CastWith(ContactArrayCaster::class)] public ?array $bcc; /** @var array */ diff --git a/src/Http/Requests/SendMailRequest.php b/src/Http/Requests/SendMailRequest.php index a12dacb..31bfc4a 100644 --- a/src/Http/Requests/SendMailRequest.php +++ b/src/Http/Requests/SendMailRequest.php @@ -68,9 +68,9 @@ public function rules(): array ], 'recipients.*.variables' => 'sometimes|array', 'recipients.*.cc' => 'sometimes|array', - 'recipients.*.cc.*' => ['sometimes', new ContactRule()], + 'recipients.*.cc.*' => [new ContactRule()], 'recipients.*.bcc' => 'sometimes|array', - 'recipients.*.bcc.*' => ['sometimes', new ContactRule()], + 'recipients.*.bcc.*' => [new ContactRule()], // Recipients attachments 'recipients.*.attachments' => 'sometimes|array', @@ -111,14 +111,20 @@ protected function prepareForValidation(): void } // Wrap cc array list - if (!array_is_list($this->input('cc'))) { + if ( + !is_null($this->input('cc')) + && (!is_array($this->input('cc')) || !array_is_list($this->input('cc'))) + ) { $this->merge([ 'cc' => [$this->input('cc')], ]); } // Wrap bcc array list - if (!array_is_list($this->input('bcc'))) { + if ( + !is_null($this->input('bcc')) + && (!is_array($this->input('bcc')) || !array_is_list($this->input('bcc'))) + ) { $this->merge([ 'bcc' => [$this->input('bcc')], ]); @@ -137,13 +143,15 @@ protected function prepareForValidation(): void } // Wrap recipient cc and bcc - foreach ($this->input('recipients') as $recipient) { - if (array_key_exists('cc', $recipient) && !array_is_list($recipient['cc'])) { - $recipient['cc'] = [$recipient['cc']]; - } - - if (array_key_exists('bcc', $recipient) && !array_is_list($recipient['bcc'])) { - $recipient['bcc'] = [$recipient['bcc']]; + if (is_array($this->input('recipients')) && array_is_list($this->input('recipients'))) { + foreach ($this->input('recipients') as $recipient) { + if (array_key_exists('cc', $recipient) && !array_is_list($recipient['cc'])) { + $recipient['cc'] = [$recipient['cc']]; + } + + if (array_key_exists('bcc', $recipient) && !array_is_list($recipient['bcc'])) { + $recipient['bcc'] = [$recipient['bcc']]; + } } } diff --git a/tests/Feature/SendMailTest.php b/tests/Feature/SendMailTest.php index e733dc0..337746b 100644 --- a/tests/Feature/SendMailTest.php +++ b/tests/Feature/SendMailTest.php @@ -121,8 +121,96 @@ $log = Log::first(); expect($log->status)->toBe(LogStatus::Sent); - expect($log->cc->email)->toBe('cc@example.org'); - expect($log->bcc->email)->toBe('bcc@example.org'); + expect($log->cc[0]->email)->toBe('cc@example.org'); + expect($log->bcc[0]->email)->toBe('bcc@example.org'); + expect($log->error)->toBeNull(); +}); + +it('allows multiple cc and bcc as email strings', function () { + Template::factory()->create([ + 'slug' => 'welcome', + ]); + + postJson(route('mailcarrier.send'), [ + 'enqueue' => false, + 'template' => 'welcome', + 'subject' => 'Welcome!', + 'recipient' => 'recipient@example.org', + 'cc' => ['cc@example.org', 'cc2@example.org'], + 'bcc' => ['bcc@example.org', 'bcc2@example.org'], + ])->assertOk(); + + Mail::assertSent(GenericMail::class, 1); + + Mail::assertSent(GenericMail::class, function (GenericMail $mail) { + $mail->build(); + + return $mail->hasTo('recipient@example.org') + && $mail->hasSubject('Welcome!') + && $mail->hasCc('cc@example.org') + && $mail->hasCc('cc2@example.org') + && $mail->hasBcc('bcc@example.org') + && $mail->hasBcc('bcc2@example.org'); + }); + + /** @var Log */ + $log = Log::first(); + + expect($log->status)->toBe(LogStatus::Sent); + expect($log->cc)->toHaveCount(2); + expect($log->cc[0]->email)->toBe('cc@example.org'); + expect($log->cc[1]->email)->toBe('cc2@example.org'); + expect($log->bcc)->toHaveCount(2); + expect($log->bcc[0]->email)->toBe('bcc@example.org'); + expect($log->bcc[1]->email)->toBe('bcc2@example.org'); + expect($log->error)->toBeNull(); +}); + +it('allows multiple cc and bcc as email contacts', function () { + Template::factory()->create([ + 'slug' => 'welcome', + ]); + + postJson(route('mailcarrier.send'), [ + 'enqueue' => false, + 'template' => 'welcome', + 'subject' => 'Welcome!', + 'recipient' => 'recipient@example.org', + 'cc' => [ + ['email' => 'cc@example.org', 'name' => 'CC'], + ['email' => 'cc2@example.org'], + ], + 'bcc' => [ + ['email' => 'bcc@example.org', 'name' => 'BCC'], + ['email' => 'bcc2@example.org'], + ], + ])->assertOk(); + + Mail::assertSent(GenericMail::class, 1); + + Mail::assertSent(GenericMail::class, function (GenericMail $mail) { + $mail->build(); + + return $mail->hasTo('recipient@example.org') + && $mail->hasSubject('Welcome!') + && $mail->hasCc('cc@example.org', 'CC') + && $mail->hasCc('cc2@example.org') + && $mail->hasBcc('bcc@example.org', 'BCC') + && $mail->hasBcc('bcc2@example.org'); + }); + + /** @var Log */ + $log = Log::first(); + + expect($log->status)->toBe(LogStatus::Sent); + expect($log->cc)->toHaveCount(2); + expect($log->cc[0]->email)->toBe('cc@example.org'); + expect($log->cc[0]->name)->toBe('CC'); + expect($log->cc[1]->email)->toBe('cc2@example.org'); + expect($log->bcc)->toHaveCount(2); + expect($log->bcc[0]->email)->toBe('bcc@example.org'); + expect($log->bcc[0]->name)->toBe('BCC'); + expect($log->bcc[1]->email)->toBe('bcc2@example.org'); expect($log->error)->toBeNull(); }); @@ -451,16 +539,16 @@ expect($log1->status)->toBe(LogStatus::Sent); expect($log1->error)->toBeNull(); - expect($log1->cc->email)->toBe('recipient1+cc@example.org'); - expect($log1->bcc->email)->toBe('recipient1+bcc@example.org'); + expect($log1->cc[0]->email)->toBe('recipient1+cc@example.org'); + expect($log1->bcc[0]->email)->toBe('recipient1+bcc@example.org'); expect($log1->variables)->toEqualCanonicalizing([ 'name' => 'foo', ]); expect($log2->status)->toBe(LogStatus::Sent); expect($log2->error)->toBeNull(); - expect($log2->cc->email)->toBe('recipient2+cc@example.org'); - expect($log2->bcc->email)->toBe('recipient2+bcc@example.org'); + expect($log2->cc[0]->email)->toBe('recipient2+cc@example.org'); + expect($log2->bcc[0]->email)->toBe('recipient2+bcc@example.org'); expect($log2->variables)->toEqualCanonicalizing([ 'name' => 'bar', ]); From 4f1e5762ff58aead2d4b54fa34a129d7350af4f8 Mon Sep 17 00:00:00 2001 From: danilopolani Date: Fri, 27 Oct 2023 12:21:51 +0200 Subject: [PATCH 4/8] add missing caster --- src/Dto/Casters/ContactArrayCaster.php | 49 ++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 src/Dto/Casters/ContactArrayCaster.php diff --git a/src/Dto/Casters/ContactArrayCaster.php b/src/Dto/Casters/ContactArrayCaster.php new file mode 100644 index 0000000..1cccf7a --- /dev/null +++ b/src/Dto/Casters/ContactArrayCaster.php @@ -0,0 +1,49 @@ + $value, + ]; + } + + if (Collection::make($value)->every(fn (mixed $value) => $value instanceof ContactDto)) { + return $value; + } + + if (is_array($value) && array_is_list($value) && is_string($value[0])) { + $value = array_map( + fn (string $item) => [ + 'email' => $item, + ], + $value + ); + } + + if (!array_is_list($value)) { + $value = [$value]; + } + + return array_map( + fn (array $item) => new ContactDto($item), + $value + ); + } +} From 16f5133020a33e67a1bf2f2b6cdb73d92088cd52 Mon Sep 17 00:00:00 2001 From: danilopolani Date: Fri, 27 Oct 2023 14:58:59 +0200 Subject: [PATCH 5/8] fix nested multiple cc and bcc inside recipients --- src/Http/Requests/SendMailRequest.php | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/src/Http/Requests/SendMailRequest.php b/src/Http/Requests/SendMailRequest.php index 31bfc4a..36ce4cf 100644 --- a/src/Http/Requests/SendMailRequest.php +++ b/src/Http/Requests/SendMailRequest.php @@ -143,16 +143,28 @@ protected function prepareForValidation(): void } // Wrap recipient cc and bcc - if (is_array($this->input('recipients')) && array_is_list($this->input('recipients'))) { - foreach ($this->input('recipients') as $recipient) { - if (array_key_exists('cc', $recipient) && !array_is_list($recipient['cc'])) { - $recipient['cc'] = [$recipient['cc']]; + $recipients = $this->input('recipients'); + + if (is_array($recipients)) { + foreach ($recipients as $i => $recipient) { + if ( + array_key_exists('cc', $recipient) + && (!is_array($this->input('cc')) || !array_is_list($this->input('cc'))) + ) { + $recipients[$i]['cc'] = [$recipient['cc']]; } - if (array_key_exists('bcc', $recipient) && !array_is_list($recipient['bcc'])) { - $recipient['bcc'] = [$recipient['bcc']]; + if ( + array_key_exists('bcc', $recipient) + && (!is_array($this->input('bcc')) || !array_is_list($this->input('bcc'))) + ) { + $recipients[$i]['bcc'] = [$recipient['bcc']]; } } + + $this->merge([ + 'recipients' => $recipients, + ]); } // Wrap remote attachments array list into a structured data From 3ba1799758f1b1fbbcd6e59a61ba4b7ad55102a2 Mon Sep 17 00:00:00 2001 From: danilopolani Date: Mon, 30 Oct 2023 12:17:07 +0100 Subject: [PATCH 6/8] fix migration to transform cc and bcc into arrays --- .../6_transform_logs_cc_bcc_array.php | 32 +++++++++++-------- src/MailCarrierServiceProvider.php | 1 + 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/database/migrations/6_transform_logs_cc_bcc_array.php b/database/migrations/6_transform_logs_cc_bcc_array.php index a68968c..644cd93 100644 --- a/database/migrations/6_transform_logs_cc_bcc_array.php +++ b/database/migrations/6_transform_logs_cc_bcc_array.php @@ -12,23 +12,27 @@ public function up(): void { $tableName = (new Log())->getTable(); - DB::table($tableName)->each(function (object $log) use ($tableName) { - $changes = []; + DB::table($tableName) + ->whereNotNull('cc') + ->orWhereNotNull('bcc') + ->orderBy('id') + ->each(function (object $log) use ($tableName) { + $changes = []; - if (!is_null($log->cc)) { - $changes['cc'] = [$log->cc]; - } + if (!is_null($log->cc)) { + $changes['cc'] = [json_decode($log->cc, true)]; + } - if (!is_null($log->bcc)) { - $changes['bcc'] = [$log->bcc]; - } + if (!is_null($log->bcc)) { + $changes['bcc'] = [json_decode($log->bcc, true)]; + } - if (!empty($changes)) { - DB::table($tableName) - ->where('id', $log->id) - ->update($changes); - } - }); + if (!empty($changes)) { + DB::table($tableName) + ->where('id', $log->id) + ->update($changes); + } + }); } /** diff --git a/src/MailCarrierServiceProvider.php b/src/MailCarrierServiceProvider.php index 81b3ebe..517608e 100644 --- a/src/MailCarrierServiceProvider.php +++ b/src/MailCarrierServiceProvider.php @@ -76,6 +76,7 @@ public function packageConfigured(Package $package): void '3_create_templates_table', '4_create_logs_table', '5_create_attachments_table', + '6_transform_logs_cc_bcc_array', ]) ->runsMigrations(); From 164755c8aee29cfeac4b8471f0e7c5a779d6eafe Mon Sep 17 00:00:00 2001 From: danilopolani Date: Mon, 30 Oct 2023 12:36:39 +0100 Subject: [PATCH 7/8] fix details modal with multiple cc and bcc --- resources/views/modals/details.blade.php | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/resources/views/modals/details.blade.php b/resources/views/modals/details.blade.php index 9c9419a..d496bb0 100644 --- a/resources/views/modals/details.blade.php +++ b/resources/views/modals/details.blade.php @@ -10,22 +10,26 @@ @if ($log->cc)

Cc

- @if ($log->cc->name) -

{{ $log->cc->name }} <{{ $log->cc->email }}>

- @else -

{{ $log->cc->email }}

- @endif + @foreach ($log->cc as $cc) + @if ($cc->name) +

{{ $cc->name }} <{{ $cc->email }}>

+ @else +

{{ $cc->email }}

+ @endif + @endforeach
@endif @if ($log->bcc)

Bcc

- @if ($log->bcc->name) -

{{ $log->bcc->name }} <{{ $log->bcc->email }}>

- @else -

{{ $log->bcc->email }}

- @endif + @foreach ($log->bcc as $bcc) + @if ($bcc->name) +

{{ $bcc->name }} <{{ $bcc->email }}>

+ @else +

{{ $bcc->email }}

+ @endif + @endforeach
@endif From cae7d44bbab5a4801d5cb29a3ef859118ff959d1 Mon Sep 17 00:00:00 2001 From: danilopolani Date: Mon, 30 Oct 2023 12:49:37 +0100 Subject: [PATCH 8/8] fix linting --- src/Models/Casts/CollectionOfContacts.php | 2 +- src/Models/Log.php | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Models/Casts/CollectionOfContacts.php b/src/Models/Casts/CollectionOfContacts.php index 95669e1..4fca1b1 100644 --- a/src/Models/Casts/CollectionOfContacts.php +++ b/src/Models/Casts/CollectionOfContacts.php @@ -13,7 +13,7 @@ class CollectionOfContacts implements CastsAttributes * Cast the given value. * * @param array $attributes - * @return \Illuminate\Support\Collection|null + * @return \Illuminate\Support\Collection<\MailCarrier\Dto\ContactDto>|null */ public function get(Model $model, string $key, mixed $value, array $attributes): ?Collection { diff --git a/src/Models/Log.php b/src/Models/Log.php index ab76b3b..588e602 100644 --- a/src/Models/Log.php +++ b/src/Models/Log.php @@ -9,7 +9,6 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasMany; -use Illuminate\Support\Collection; use Illuminate\Support\Facades\Config; use MailCarrier\Dto\ContactDto; use MailCarrier\Dto\LogTemplateDto;