diff --git a/config/mailator.php b/config/mailator.php index dab7be5440a5..d9282568f949 100644 --- a/config/mailator.php +++ b/config/mailator.php @@ -63,4 +63,14 @@ Binarcode\LaravelMailator\Replacers\SampleReplacer::class, ], ], + + 'serialization' => [ + /* + > Controls constructor property accessibility in mailable objects for PostgreSQL compatibility. + > When set to false, allows private/protected properties. When true, enforces + > public properties only to prevent PostgreSQL serialization errors caused by + > null bytes (\x00) in non-public properties. + */ + 'enforce_public_properties' => false, + ] ]; diff --git a/src/Models/MailatorSchedule.php b/src/Models/MailatorSchedule.php index 6fa7ac509496..b3a840d66cb8 100644 --- a/src/Models/MailatorSchedule.php +++ b/src/Models/MailatorSchedule.php @@ -28,6 +28,8 @@ use Illuminate\Support\Facades\Validator; use Illuminate\Support\Str; use Opis\Closure\SerializableClosure; +use ReflectionClass; +use RuntimeException; use Throwable; use TypeError; @@ -106,6 +108,10 @@ public static function init(string $name): self public function mailable(Mailable $mailable): self { + if (config('mailator.serialization.enforce_public_properties') && $this->hasNonPublicConstructorProps($mailable)) { + throw new RuntimeException('Mailable contains non-public constructor properties which cannot be safely serialized'); + } + if ($mailable instanceof Constraintable) { collect($mailable->constraints()) ->filter(fn ($constraint) => $constraint instanceof SendScheduleConstraint) @@ -598,4 +604,19 @@ public function save(array $options = []) return parent::save($options); } + + private function hasNonPublicConstructorProps(Mailable $mailable): bool + { + $reflection = new ReflectionClass($mailable); + $constructor = $reflection->getConstructor(); + + if (! $constructor) { + return false; + } + + return collect($constructor->getParameters()) + ->filter(fn ($param) => $reflection->getProperty($param->getName())->isPrivate() + || $reflection->getProperty($param->getName())->isProtected()) + ->isNotEmpty(); + } } diff --git a/tests/Feature/AllowNonPublicPropertiesTest.php b/tests/Feature/AllowNonPublicPropertiesTest.php new file mode 100644 index 000000000000..b964cf4e9c41 --- /dev/null +++ b/tests/Feature/AllowNonPublicPropertiesTest.php @@ -0,0 +1,84 @@ +set('mailator.serialization.enforce_public_properties', false); + + MailatorSchedule::init('private') + ->mailable(new PrivatePropertyMailable('test')) + ->execute(); + + Mail::assertSent(PrivatePropertyMailable::class); + } + + public function test_can_not_send_email_with_private_property(): void + { + config()->set('mailator.serialization.enforce_public_properties', true); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('Mailable contains non-public constructor properties which cannot be safely serialized'); + + MailatorSchedule::init('private') + ->mailable(new PrivatePropertyMailable('test')) + ->execute(); + } + + public function test_can_send_email_with_protected_property(): void + { + config()->set('mailator.serialization.enforce_public_properties', false); + + MailatorSchedule::init('protected') + ->mailable(new ProtectedPropertyMailable('test')) + ->execute(); + + Mail::assertSent(ProtectedPropertyMailable::class); + } + + public function test_can_not_send_email_with_protected_property(): void + { + config()->set('mailator.serialization.enforce_public_properties', true); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('Mailable contains non-public constructor properties which cannot be safely serialized'); + + MailatorSchedule::init('protected') + ->mailable(new ProtectedPropertyMailable('test')) + ->execute(); + } + + public function test_can_send_email_with_public_property(): void + { + config()->set('mailator.serialization.enforce_public_properties', true); + + MailatorSchedule::init('protected') + ->mailable(new PublicPropertyMailable('test')) + ->execute(); + + Mail::assertSent(PublicPropertyMailable::class); + + config()->set('mailator.serialization.enforce_public_properties', false); + + MailatorSchedule::init('protected') + ->mailable(new PublicPropertyMailable('test')) + ->execute(); + } +} diff --git a/tests/Fixtures/PrivatePropertyMailable.php b/tests/Fixtures/PrivatePropertyMailable.php new file mode 100644 index 000000000000..71e626c9c723 --- /dev/null +++ b/tests/Fixtures/PrivatePropertyMailable.php @@ -0,0 +1,24 @@ +view('laravel-mailator::mails.stub_invoice_reminder_view'); + } +} diff --git a/tests/Fixtures/ProtectedPropertyMailable.php b/tests/Fixtures/ProtectedPropertyMailable.php new file mode 100644 index 000000000000..18922eae6c49 --- /dev/null +++ b/tests/Fixtures/ProtectedPropertyMailable.php @@ -0,0 +1,24 @@ +view('laravel-mailator::mails.stub_invoice_reminder_view'); + } +} diff --git a/tests/Fixtures/PublicPropertyMailable.php b/tests/Fixtures/PublicPropertyMailable.php new file mode 100644 index 000000000000..024d260a72ce --- /dev/null +++ b/tests/Fixtures/PublicPropertyMailable.php @@ -0,0 +1,24 @@ +view('laravel-mailator::mails.stub_invoice_reminder_view'); + } +}