diff --git a/src/Illuminate/Notifications/Channels/MailChannel.php b/src/Illuminate/Notifications/Channels/MailChannel.php index ffc23b9008ec..1a90e3d5ae24 100644 --- a/src/Illuminate/Notifications/Channels/MailChannel.php +++ b/src/Illuminate/Notifications/Channels/MailChannel.php @@ -60,7 +60,8 @@ public function send($notifiable, Notification $notification) } if ($message instanceof Mailable) { - return $message->send($this->mailer); + return $this->populateMailableRecipients($message, $notifiable, $notification) + ->send($this->mailer); } return $this->mailer->mailer($message->mailer ?? null)->send( @@ -213,7 +214,7 @@ protected function addressMessage($mailMessage, $notifiable, $notification, $mes { $this->addSender($mailMessage, $message); - $mailMessage->to($this->getRecipients($notifiable, $notification, $message)); + $mailMessage->to($this->getRecipients($notifiable, $notification)); if (! empty($message->cc)) { foreach ($message->cc as $cc) { @@ -248,15 +249,35 @@ protected function addSender($mailMessage, $message) } } + /** + * Populate the mailable recipients using the notifiable's mail route. + * + * @param \Illuminate\Contracts\Mail\Mailable $mailable + * @param mixed $notifiable + * @param \Illuminate\Notifications\Notification $notification + * @return \Illuminate\Contracts\Mail\Mailable + */ + protected function populateMailableRecipients($mailable, $notifiable, $notification) + { + if (! empty($mailable->to)) { + return $mailable; + } + + if (method_exists($mailable, 'envelope') && ! empty($mailable->envelope()->to)) { + return $mailable; + } + + return $mailable->to($this->getRecipients($notifiable, $notification)); + } + /** * Get the recipients of the given message. * * @param mixed $notifiable * @param \Illuminate\Notifications\Notification $notification - * @param \Illuminate\Notifications\Messages\MailMessage $message * @return mixed */ - protected function getRecipients($notifiable, $notification, $message) + protected function getRecipients($notifiable, $notification) { if (is_string($recipients = $notifiable->routeNotificationFor('mail', $notification))) { $recipients = [$recipients]; diff --git a/tests/Integration/Notifications/SendingNotificationsWithLocaleTest.php b/tests/Integration/Notifications/SendingNotificationsWithLocaleTest.php index fc5f4fea5dc3..87b28bd3b6a5 100644 --- a/tests/Integration/Notifications/SendingNotificationsWithLocaleTest.php +++ b/tests/Integration/Notifications/SendingNotificationsWithLocaleTest.php @@ -116,6 +116,38 @@ public function testMailableIsSentWithSelectedLocale() ); } + public function testMailableWithoutToIsAddressedToNotifiable() + { + $user = NotifiableLocalizedUser::forceCreate([ + 'email' => 'taylor@laravel.com', + ]); + + $user->notify(new GreetingMailNotificationWithMailableWithoutTo); + + $message = app('mailer')->getSymfonyTransport()->messages()[0]->getOriginalMessage(); + + $this->assertSame( + 'taylor@laravel.com', + $message->getTo()[0]->getAddress() + ); + } + + public function testMailableWithoutToIsAddressedToMultipleNotifiableRoutes() + { + $user = NotifiableLocalizedUserWithMultipleMailRoutes::forceCreate([ + 'email' => 'taylor@laravel.com', + ]); + + $user->notify(new GreetingMailNotificationWithMailableWithoutTo); + + $message = app('mailer')->getSymfonyTransport()->messages()[0]->getOriginalMessage(); + + $this->assertEqualsCanonicalizing( + ['foo_taylor@laravel.com', 'bar_taylor@laravel.com'], + array_map(fn ($address) => $address->getAddress(), $message->getTo()) + ); + } + public function testMailIsSentWithLocaleUpdatedListenersCalled() { Carbon::setTestNow('2018-07-25'); @@ -247,6 +279,17 @@ public function preferredLocale() } } +class NotifiableLocalizedUserWithMultipleMailRoutes extends NotifiableLocalizedUser +{ + public function routeNotificationForMail($notification) + { + return [ + 'foo_'.$this->email, + 'bar_'.$this->email, + ]; + } +} + class GreetingMailNotification extends Notification { public function via($notifiable) @@ -276,6 +319,19 @@ public function toMail($notifiable) } } +class GreetingMailNotificationWithMailableWithoutTo extends Notification +{ + public function via($notifiable) + { + return [MailChannel::class]; + } + + public function toMail($notifiable) + { + return new GreetingMailable; + } +} + class GreetingMailable extends Mailable { public function build() diff --git a/tests/Notifications/MailChannelTest.php b/tests/Notifications/MailChannelTest.php new file mode 100644 index 000000000000..8665d909bca7 --- /dev/null +++ b/tests/Notifications/MailChannelTest.php @@ -0,0 +1,153 @@ +channel()->populateMailableRecipients( + $mailable, + new MailChannelNotifiable('taylor@laravel.com'), + new MailChannelTestNotification, + ); + + $this->assertSame('taylor@laravel.com', $mailable->to[0]['address']); + } + + public function testItDoesNotOverrideExistingMailableRecipients() + { + $mailable = (new MailChannelTestMailable)->to('other@laravel.com'); + + $mailable = $this->channel()->populateMailableRecipients( + $mailable, + new MailChannelNotifiable('taylor@laravel.com'), + new MailChannelTestNotification, + ); + + $this->assertSame('other@laravel.com', $mailable->to[0]['address']); + } + + public function testItDoesNotOverrideMailableRecipientsDefinedOnEnvelope() + { + $mailable = new MailChannelTestMailableWithEnvelopeRecipient; + + $mailable = $this->channel()->populateMailableRecipients( + $mailable, + new MailChannelNotifiable('taylor@laravel.com'), + new MailChannelTestNotification, + ); + + $this->assertSame([], $mailable->to); + } + + public function testItPopulatesMultipleMailableRecipientsFromRoute() + { + $mailable = new MailChannelTestMailable; + + $mailable = $this->channel()->populateMailableRecipients( + $mailable, + new MailChannelNotifiableWithMultipleRoutes('taylor@laravel.com'), + new MailChannelTestNotification, + ); + + $this->assertEqualsCanonicalizing( + ['foo_taylor@laravel.com', 'bar_taylor@laravel.com'], + array_column($mailable->to, 'address') + ); + } + + protected function channel(): MailChannelForTesting + { + return new MailChannelForTesting( + m::mock(MailFactory::class), + m::mock(Markdown::class), + ); + } +} + +class MailChannelForTesting extends MailChannel +{ + public function populateMailableRecipients($mailable, $notifiable, $notification) + { + return parent::populateMailableRecipients($mailable, $notifiable, $notification); + } +} + +class MailChannelNotifiable +{ + public function __construct(public string $email) + { + } + + public function routeNotificationFor($driver, $notification = null) + { + return $driver === 'mail' + ? $this->routeNotificationForMail($notification) + : null; + } + + public function routeNotificationForMail($notification) + { + return $this->email; + } +} + +class MailChannelNotifiableWithMultipleRoutes extends MailChannelNotifiable +{ + public function routeNotificationForMail($notification) + { + return [ + 'foo_'.$this->email, + 'bar_'.$this->email, + ]; + } +} + +class MailChannelTestMailable extends Mailable +{ + public function build() + { + return $this->html('Test'); + } +} + +class MailChannelTestMailableWithEnvelopeRecipient extends Mailable +{ + public function envelope() + { + return new \Illuminate\Mail\Mailables\Envelope( + to: [new \Illuminate\Mail\Mailables\Address('envelope@laravel.com')], + subject: 'Test', + ); + } + + public function content() + { + return new \Illuminate\Mail\Mailables\Content( + htmlString: 'Test', + ); + } +} + +class MailChannelTestNotification extends Notification +{ + // +}