From 7925e2e42aba019993ecc4e737ed404dc896ad8c Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Sun, 17 Aug 2025 17:59:22 +0200 Subject: [PATCH] fix(email notifications): enhance EmailChannel to validate team membership for recipients and handle errors gracefully --- app/Notifications/Channels/EmailChannel.php | 148 +++++++++++++------- 1 file changed, 96 insertions(+), 52 deletions(-) diff --git a/app/Notifications/Channels/EmailChannel.php b/app/Notifications/Channels/EmailChannel.php index 8a9a95107..47994c690 100644 --- a/app/Notifications/Channels/EmailChannel.php +++ b/app/Notifications/Channels/EmailChannel.php @@ -2,6 +2,8 @@ namespace App\Notifications\Channels; +use App\Models\Team; +use Exception; use Illuminate\Notifications\Notification; use Resend; @@ -11,60 +13,102 @@ class EmailChannel public function send(SendsEmail $notifiable, Notification $notification): void { - $useInstanceEmailSettings = $notifiable->emailNotificationSettings->use_instance_email_settings; - $isTransactionalEmail = data_get($notification, 'isTransactionalEmail', false); - $customEmails = data_get($notification, 'emails', null); - if ($useInstanceEmailSettings || $isTransactionalEmail) { - $settings = instanceSettings(); - } else { - $settings = $notifiable->emailNotificationSettings; - } - $isResendEnabled = $settings->resend_enabled; - $isSmtpEnabled = $settings->smtp_enabled; - if ($customEmails) { - $recipients = [$customEmails]; - } else { - $recipients = $notifiable->getRecipients(); - } - $mailMessage = $notification->toMail($notifiable); + try { + // Get team and validate membership before proceeding + $team = data_get($notifiable, 'id'); + $members = Team::find($team)->members; - if ($isResendEnabled) { - $resend = Resend::client($settings->resend_api_key); - $from = "{$settings->smtp_from_name} <{$settings->smtp_from_address}>"; - $resend->emails->send([ - 'from' => $from, - 'to' => $recipients, - 'subject' => $mailMessage->subject, - 'html' => (string) $mailMessage->render(), + $useInstanceEmailSettings = $notifiable->emailNotificationSettings->use_instance_email_settings; + $isTransactionalEmail = data_get($notification, 'isTransactionalEmail', false); + $customEmails = data_get($notification, 'emails', null); + + if ($useInstanceEmailSettings || $isTransactionalEmail) { + $settings = instanceSettings(); + } else { + $settings = $notifiable->emailNotificationSettings; + } + + $isResendEnabled = $settings->resend_enabled; + $isSmtpEnabled = $settings->smtp_enabled; + + if ($customEmails) { + $recipients = [$customEmails]; + } else { + $recipients = $notifiable->getRecipients(); + } + + // Validate team membership for all recipients + if (count($recipients) === 0) { + throw new Exception('No email recipients found'); + } + + foreach ($recipients as $recipient) { + // Check if the recipient is part of the team + if (! $members->contains('email', $recipient)) { + $emailSettings = $notifiable->emailNotificationSettings; + data_set($emailSettings, 'smtp_password', '********'); + data_set($emailSettings, 'resend_api_key', '********'); + send_internal_notification(sprintf( + "Recipient is not part of the team: %s\nTeam: %s\nNotification: %s\nNotifiable: %s\nEmail Settings:\n%s", + $recipient, + $team, + get_class($notification), + get_class($notifiable), + json_encode($emailSettings, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) + )); + throw new Exception('Recipient is not part of the team'); + } + } + + $mailMessage = $notification->toMail($notifiable); + + if ($isResendEnabled) { + $resend = Resend::client($settings->resend_api_key); + $from = "{$settings->smtp_from_name} <{$settings->smtp_from_address}>"; + $resend->emails->send([ + 'from' => $from, + 'to' => $recipients, + 'subject' => $mailMessage->subject, + 'html' => (string) $mailMessage->render(), + ]); + } elseif ($isSmtpEnabled) { + $encryption = match (strtolower($settings->smtp_encryption)) { + 'starttls' => null, + 'tls' => 'tls', + 'none' => null, + default => null, + }; + + $transport = new \Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport( + $settings->smtp_host, + $settings->smtp_port, + $encryption + ); + $transport->setUsername($settings->smtp_username ?? ''); + $transport->setPassword($settings->smtp_password ?? ''); + + $mailer = new \Symfony\Component\Mailer\Mailer($transport); + + $fromEmail = $settings->smtp_from_address ?? 'noreply@localhost'; + $fromName = $settings->smtp_from_name ?? 'System'; + $from = new \Symfony\Component\Mime\Address($fromEmail, $fromName); + $email = (new \Symfony\Component\Mime\Email) + ->from($from) + ->to(...$recipients) + ->subject($mailMessage->subject) + ->html((string) $mailMessage->render()); + + $mailer->send($email); + } + } catch (\Throwable $e) { + \Illuminate\Support\Facades\Log::error('EmailChannel failed: '.$e->getMessage(), [ + 'notification' => get_class($notification), + 'notifiable' => get_class($notifiable), + 'team_id' => data_get($notifiable, 'id'), + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString(), ]); - } elseif ($isSmtpEnabled) { - $encryption = match (strtolower($settings->smtp_encryption)) { - 'starttls' => null, - 'tls' => 'tls', - 'none' => null, - default => null, - }; - - $transport = new \Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport( - $settings->smtp_host, - $settings->smtp_port, - $encryption - ); - $transport->setUsername($settings->smtp_username ?? ''); - $transport->setPassword($settings->smtp_password ?? ''); - - $mailer = new \Symfony\Component\Mailer\Mailer($transport); - - $fromEmail = $settings->smtp_from_address ?? 'noreply@localhost'; - $fromName = $settings->smtp_from_name ?? 'System'; - $from = new \Symfony\Component\Mime\Address($fromEmail, $fromName); - $email = (new \Symfony\Component\Mime\Email) - ->from($from) - ->to(...$recipients) - ->subject($mailMessage->subject) - ->html((string) $mailMessage->render()); - - $mailer->send($email); + throw $e; } } }