diff --git a/app/Notifications/ApiTokenExpiringNotification.php b/app/Notifications/ApiTokenExpiringNotification.php index c00ac2d12..451dd312a 100644 --- a/app/Notifications/ApiTokenExpiringNotification.php +++ b/app/Notifications/ApiTokenExpiringNotification.php @@ -29,16 +29,6 @@ public function via(object $notifiable): array return $notifiable->getEnabledChannels('api_token_expiring'); } - public function deduplicationKey(object $notifiable, string $channel): ?string - { - return "api-token-expiring:{$this->token->id}"; - } - - public function deduplicateFor(): int - { - return 172800; - } - public function toMail(): MailMessage { $mail = new MailMessage; diff --git a/app/Notifications/Application/DeploymentFailed.php b/app/Notifications/Application/DeploymentFailed.php index 0ed705edd..8fff7f03b 100644 --- a/app/Notifications/Application/DeploymentFailed.php +++ b/app/Notifications/Application/DeploymentFailed.php @@ -52,16 +52,6 @@ public function via(object $notifiable): array return $notifiable->getEnabledChannels('deployment_failure'); } - public function deduplicationKey(object $notifiable, string $channel): ?string - { - return "deployment-failed:{$this->deployment_uuid}"; - } - - public function deduplicateFor(): int - { - return 86400; - } - public function toMail(): MailMessage { $mail = new MailMessage; diff --git a/app/Notifications/Application/DeploymentSuccess.php b/app/Notifications/Application/DeploymentSuccess.php index 56b692cda..415df5831 100644 --- a/app/Notifications/Application/DeploymentSuccess.php +++ b/app/Notifications/Application/DeploymentSuccess.php @@ -52,16 +52,6 @@ public function via(object $notifiable): array return $notifiable->getEnabledChannels('deployment_success'); } - public function deduplicationKey(object $notifiable, string $channel): ?string - { - return "deployment-success:{$this->deployment_uuid}"; - } - - public function deduplicateFor(): int - { - return 86400; - } - public function toMail(): MailMessage { $mail = new MailMessage; diff --git a/app/Notifications/Application/RestartLimitReached.php b/app/Notifications/Application/RestartLimitReached.php index 507bba28d..635dfdbdc 100644 --- a/app/Notifications/Application/RestartLimitReached.php +++ b/app/Notifications/Application/RestartLimitReached.php @@ -49,16 +49,6 @@ public function via(object $notifiable): array return $notifiable->getEnabledChannels('status_change'); } - public function deduplicationKey(object $notifiable, string $channel): ?string - { - return "restart-limit-reached:application:{$this->resource->uuid}:count:{$this->restart_count}"; - } - - public function deduplicateFor(): int - { - return 86400; - } - public function toMail(): MailMessage { $mail = new MailMessage; diff --git a/app/Notifications/Application/StatusChanged.php b/app/Notifications/Application/StatusChanged.php index 87986435d..ef61b7e6a 100644 --- a/app/Notifications/Application/StatusChanged.php +++ b/app/Notifications/Application/StatusChanged.php @@ -42,16 +42,6 @@ public function via(object $notifiable): array return $notifiable->getEnabledChannels('status_change'); } - public function deduplicationKey(object $notifiable, string $channel): ?string - { - return "application-status-changed:application:{$this->resource->uuid}:stopped"; - } - - public function deduplicateFor(): int - { - return 3600; - } - public function toMail(): MailMessage { $mail = new MailMessage; @@ -60,7 +50,6 @@ public function toMail(): MailMessage $mail->view('emails.application-status-changes', [ 'name' => $this->resource_name, 'fqdn' => $fqdn, - 'application_url' => $this->resource_url, 'resource_url' => $this->resource_url, ]); diff --git a/app/Notifications/Channels/EmailChannel.php b/app/Notifications/Channels/EmailChannel.php index 45c6cb2d6..abd115550 100644 --- a/app/Notifications/Channels/EmailChannel.php +++ b/app/Notifications/Channels/EmailChannel.php @@ -4,20 +4,13 @@ use App\Exceptions\NonReportableException; use App\Models\Team; -use App\Services\NotificationDeduplicator; use Exception; use Illuminate\Notifications\Notification; use Resend; -use Resend\Exceptions\ErrorException; -use Resend\Exceptions\TransporterException; -use Symfony\Component\Mailer\Mailer; -use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport; -use Symfony\Component\Mime\Address; -use Symfony\Component\Mime\Email; class EmailChannel { - public function __construct(private NotificationDeduplicator $deduplicator) {} + public function __construct() {} public function send(SendsEmail $notifiable, Notification $notification): void { @@ -74,11 +67,6 @@ public function send(SendsEmail $notifiable, Notification $notification): void } $mailMessage = $notification->toMail($notifiable); - $renderedMail = (string) $mailMessage->render(); - - if (! $this->deduplicator->shouldSend($notifiable, $notification, self::class, $recipients, $mailMessage->subject, $renderedMail)) { - return; - } if ($isResendEnabled) { $resend = Resend::client($settings->resend_api_key); @@ -87,17 +75,17 @@ public function send(SendsEmail $notifiable, Notification $notification): void 'from' => $from, 'to' => $recipients, 'subject' => $mailMessage->subject, - 'html' => $renderedMail, + 'html' => (string) $mailMessage->render(), ]); } elseif ($isSmtpEnabled) { - $encryption = match (strtolower($settings->smtp_encryption ?? '')) { + $encryption = match (strtolower($settings->smtp_encryption)) { 'starttls' => null, 'tls' => 'tls', 'none' => null, default => null, }; - $transport = new EsmtpTransport( + $transport = new \Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport( $settings->smtp_host, $settings->smtp_port, $encryption @@ -105,20 +93,20 @@ public function send(SendsEmail $notifiable, Notification $notification): void $transport->setUsername($settings->smtp_username ?? ''); $transport->setPassword($settings->smtp_password ?? ''); - $mailer = new Mailer($transport); + $mailer = new \Symfony\Component\Mailer\Mailer($transport); $fromEmail = $settings->smtp_from_address ?? 'noreply@localhost'; $fromName = $settings->smtp_from_name ?? 'System'; - $from = new Address($fromEmail, $fromName); - $email = (new Email) + $from = new \Symfony\Component\Mime\Address($fromEmail, $fromName); + $email = (new \Symfony\Component\Mime\Email) ->from($from) ->to(...$recipients) ->subject($mailMessage->subject) - ->html($renderedMail); + ->html((string) $mailMessage->render()); $mailer->send($email); } - } catch (ErrorException $e) { + } catch (\Resend\Exceptions\ErrorException $e) { // Map HTTP status codes to user-friendly messages $userMessage = match ($e->getErrorCode()) { 403 => 'Invalid Resend API key. Please verify your API key in the Resend dashboard and update it in settings.', @@ -143,13 +131,13 @@ public function send(SendsEmail $notifiable, Notification $notification): void // Don't report expected errors (invalid keys, validation) to Sentry if (in_array($e->getErrorCode(), [403, 401, 400])) { - throw NonReportableException::fromException(new Exception($userMessage, $e->getCode(), $e)); + throw NonReportableException::fromException(new \Exception($userMessage, $e->getCode(), $e)); } - throw new Exception($userMessage, $e->getCode(), $e); - } catch (TransporterException $e) { + throw new \Exception($userMessage, $e->getCode(), $e); + } catch (\Resend\Exceptions\TransporterException $e) { send_internal_notification("Resend Transport Error: {$e->getMessage()}"); - throw new Exception('Unable to connect to Resend API. Please check your internet connection and try again.'); + throw new \Exception('Unable to connect to Resend API. Please check your internet connection and try again.'); } catch (\Throwable $e) { // Check if this is a Resend domain verification error on cloud instances if (isCloud() && str_contains($e->getMessage(), 'domain is not verified')) { diff --git a/app/Notifications/Channels/TransactionalEmailChannel.php b/app/Notifications/Channels/TransactionalEmailChannel.php index 803db57f3..8ab74a60b 100644 --- a/app/Notifications/Channels/TransactionalEmailChannel.php +++ b/app/Notifications/Channels/TransactionalEmailChannel.php @@ -3,7 +3,6 @@ namespace App\Notifications\Channels; use App\Models\User; -use App\Services\NotificationDeduplicator; use Exception; use Illuminate\Mail\Message; use Illuminate\Notifications\Notification; @@ -11,8 +10,6 @@ class TransactionalEmailChannel { - public function __construct(private NotificationDeduplicator $deduplicator) {} - public function send(User $notifiable, Notification $notification): void { $settings = instanceSettings(); @@ -30,19 +27,13 @@ public function send(User $notifiable, Notification $notification): void } $this->bootConfigs(); $mailMessage = $notification->toMail($notifiable); - $renderedMail = (string) $mailMessage->render(); - - if (! $this->deduplicator->shouldSend($notifiable, $notification, self::class, [$email], $mailMessage->subject, $renderedMail)) { - return; - } - Mail::send( [], [], fn (Message $message) => $message ->to($email) ->subject($mailMessage->subject) - ->html($renderedMail) + ->html((string) $mailMessage->render()) ); } diff --git a/app/Notifications/Container/ContainerRestarted.php b/app/Notifications/Container/ContainerRestarted.php index d51c77cb3..2d7eb58b5 100644 --- a/app/Notifications/Container/ContainerRestarted.php +++ b/app/Notifications/Container/ContainerRestarted.php @@ -21,16 +21,6 @@ public function via(object $notifiable): array return $notifiable->getEnabledChannels('status_change'); } - public function deduplicationKey(object $notifiable, string $channel): ?string - { - return "container-restarted:server:{$this->server->uuid}:container:{$this->name}"; - } - - public function deduplicateFor(): int - { - return 3600; - } - public function toMail(): MailMessage { $mail = new MailMessage; diff --git a/app/Notifications/Container/ContainerStopped.php b/app/Notifications/Container/ContainerStopped.php index 7daba04ca..f518cd2fd 100644 --- a/app/Notifications/Container/ContainerStopped.php +++ b/app/Notifications/Container/ContainerStopped.php @@ -21,16 +21,6 @@ public function via(object $notifiable): array return $notifiable->getEnabledChannels('status_change'); } - public function deduplicationKey(object $notifiable, string $channel): ?string - { - return "container-stopped:server:{$this->server->uuid}:container:{$this->name}"; - } - - public function deduplicateFor(): int - { - return 3600; - } - public function toMail(): MailMessage { $mail = new MailMessage; diff --git a/app/Notifications/CustomEmailNotification.php b/app/Notifications/CustomEmailNotification.php index e3f62e22a..c3c89b30f 100644 --- a/app/Notifications/CustomEmailNotification.php +++ b/app/Notifications/CustomEmailNotification.php @@ -15,19 +15,4 @@ class CustomEmailNotification extends Notification implements ShouldQueue public $tries = 5; public $maxExceptions = 5; - - public function shouldDeduplicate(): bool - { - return true; - } - - public function deduplicateFor(): int - { - return 900; - } - - public function deduplicationKey(object $notifiable, string $channel): ?string - { - return null; - } } diff --git a/app/Notifications/Database/BackupFailed.php b/app/Notifications/Database/BackupFailed.php index 8d9c99603..c2b21b1d5 100644 --- a/app/Notifications/Database/BackupFailed.php +++ b/app/Notifications/Database/BackupFailed.php @@ -11,8 +11,6 @@ class BackupFailed extends CustomEmailNotification { - public int|string|null $backupId = null; - public string $name; public string $frequency; @@ -20,7 +18,6 @@ class BackupFailed extends CustomEmailNotification public function __construct(ScheduledDatabaseBackup $backup, public $database, public $output, public $database_name) { $this->onQueue('high'); - $this->backupId = data_get($backup, 'uuid') ?? data_get($backup, 'id'); $this->name = $database->name; $this->frequency = $backup->frequency; } @@ -30,16 +27,6 @@ public function via(object $notifiable): array return $notifiable->getEnabledChannels('backup_failure'); } - public function deduplicationKey(object $notifiable, string $channel): ?string - { - return "backup-failed:backup:{$this->backupId}:database:{$this->database->uuid}:output:".hash('sha256', (string) $this->output); - } - - public function deduplicateFor(): int - { - return 21600; - } - public function toMail(): MailMessage { $mail = new MailMessage; diff --git a/app/Notifications/Database/BackupSuccess.php b/app/Notifications/Database/BackupSuccess.php index 166a48496..3d2d8ece3 100644 --- a/app/Notifications/Database/BackupSuccess.php +++ b/app/Notifications/Database/BackupSuccess.php @@ -11,8 +11,6 @@ class BackupSuccess extends CustomEmailNotification { - public int|string|null $backupId = null; - public string $name; public string $frequency; @@ -20,7 +18,6 @@ class BackupSuccess extends CustomEmailNotification public function __construct(ScheduledDatabaseBackup $backup, public $database, public $database_name) { $this->onQueue('high'); - $this->backupId = data_get($backup, 'uuid') ?? data_get($backup, 'id'); $this->name = $database->name; $this->frequency = $backup->frequency; @@ -31,16 +28,6 @@ public function via(object $notifiable): array return $notifiable->getEnabledChannels('backup_success'); } - public function deduplicationKey(object $notifiable, string $channel): ?string - { - return "backup-success:backup:{$this->backupId}:database:{$this->database->uuid}:name:{$this->database_name}:frequency:{$this->frequency}"; - } - - public function deduplicateFor(): int - { - return 86400; - } - public function toMail(): MailMessage { $mail = new MailMessage; diff --git a/app/Notifications/Database/BackupSuccessWithS3Warning.php b/app/Notifications/Database/BackupSuccessWithS3Warning.php index 0da619448..ee24ef17d 100644 --- a/app/Notifications/Database/BackupSuccessWithS3Warning.php +++ b/app/Notifications/Database/BackupSuccessWithS3Warning.php @@ -11,8 +11,6 @@ class BackupSuccessWithS3Warning extends CustomEmailNotification { - public int|string|null $backupId = null; - public string $name; public string $frequency; @@ -22,7 +20,6 @@ class BackupSuccessWithS3Warning extends CustomEmailNotification public function __construct(ScheduledDatabaseBackup $backup, public $database, public $database_name, public $s3_error) { $this->onQueue('high'); - $this->backupId = data_get($backup, 'uuid') ?? data_get($backup, 'id'); $this->name = $database->name; $this->frequency = $backup->frequency; @@ -37,16 +34,6 @@ public function via(object $notifiable): array return $notifiable->getEnabledChannels('backup_failure'); } - public function deduplicationKey(object $notifiable, string $channel): ?string - { - return "backup-s3-warning:backup:{$this->backupId}:database:{$this->database->uuid}:error:".hash('sha256', (string) $this->s3_error); - } - - public function deduplicateFor(): int - { - return 21600; - } - public function toMail(): MailMessage { $mail = new MailMessage; diff --git a/app/Notifications/ScheduledTask/TaskFailed.php b/app/Notifications/ScheduledTask/TaskFailed.php index 5078ca8e9..bd060112a 100644 --- a/app/Notifications/ScheduledTask/TaskFailed.php +++ b/app/Notifications/ScheduledTask/TaskFailed.php @@ -28,16 +28,6 @@ public function via(object $notifiable): array return $notifiable->getEnabledChannels('scheduled_task_failure'); } - public function deduplicationKey(object $notifiable, string $channel): ?string - { - return "scheduled-task-failed:task:{$this->task->uuid}:output:".hash('sha256', $this->output); - } - - public function deduplicateFor(): int - { - return 3600; - } - public function toMail(): MailMessage { $mail = new MailMessage; diff --git a/app/Notifications/ScheduledTask/TaskSuccess.php b/app/Notifications/ScheduledTask/TaskSuccess.php index 0231ecf3d..58c959bd8 100644 --- a/app/Notifications/ScheduledTask/TaskSuccess.php +++ b/app/Notifications/ScheduledTask/TaskSuccess.php @@ -28,16 +28,6 @@ public function via(object $notifiable): array return $notifiable->getEnabledChannels('scheduled_task_success'); } - public function deduplicationKey(object $notifiable, string $channel): ?string - { - return "scheduled-task-success:task:{$this->task->uuid}:output:".hash('sha256', $this->output); - } - - public function deduplicateFor(): int - { - return 3600; - } - public function toMail(): MailMessage { $mail = new MailMessage; diff --git a/app/Notifications/Server/DockerCleanupFailed.php b/app/Notifications/Server/DockerCleanupFailed.php index ac0eea17d..9cbdeb488 100644 --- a/app/Notifications/Server/DockerCleanupFailed.php +++ b/app/Notifications/Server/DockerCleanupFailed.php @@ -21,16 +21,6 @@ public function via(object $notifiable): array return $notifiable->getEnabledChannels('docker_cleanup_failure'); } - public function deduplicationKey(object $notifiable, string $channel): ?string - { - return "docker-cleanup-failed:server:{$this->server->uuid}:message:".hash('sha256', $this->message); - } - - public function deduplicateFor(): int - { - return 21600; - } - public function toMail(): MailMessage { $mail = new MailMessage; diff --git a/app/Notifications/Server/DockerCleanupSuccess.php b/app/Notifications/Server/DockerCleanupSuccess.php index 7e5ec0bcf..d28f25c6c 100644 --- a/app/Notifications/Server/DockerCleanupSuccess.php +++ b/app/Notifications/Server/DockerCleanupSuccess.php @@ -21,16 +21,6 @@ public function via(object $notifiable): array return $notifiable->getEnabledChannels('docker_cleanup_success'); } - public function deduplicationKey(object $notifiable, string $channel): ?string - { - return "docker-cleanup-success:server:{$this->server->uuid}:message:".hash('sha256', $this->message); - } - - public function deduplicateFor(): int - { - return 21600; - } - public function toMail(): MailMessage { $mail = new MailMessage; diff --git a/app/Notifications/Server/ForceDisabled.php b/app/Notifications/Server/ForceDisabled.php index 8d1817026..4b56f5860 100644 --- a/app/Notifications/Server/ForceDisabled.php +++ b/app/Notifications/Server/ForceDisabled.php @@ -21,16 +21,6 @@ public function via(object $notifiable): array return $notifiable->getEnabledChannels('server_force_disabled'); } - public function deduplicationKey(object $notifiable, string $channel): ?string - { - return "server-force-disabled:{$this->server->uuid}"; - } - - public function deduplicateFor(): int - { - return 86400; - } - public function toMail(): MailMessage { $mail = new MailMessage; diff --git a/app/Notifications/Server/ForceEnabled.php b/app/Notifications/Server/ForceEnabled.php index 3db96f995..36dad3c60 100644 --- a/app/Notifications/Server/ForceEnabled.php +++ b/app/Notifications/Server/ForceEnabled.php @@ -21,16 +21,6 @@ public function via(object $notifiable): array return $notifiable->getEnabledChannels('server_force_enabled'); } - public function deduplicationKey(object $notifiable, string $channel): ?string - { - return "server-force-enabled:{$this->server->uuid}"; - } - - public function deduplicateFor(): int - { - return 86400; - } - public function toMail(): MailMessage { $mail = new MailMessage; diff --git a/app/Notifications/Server/HetznerDeletionFailed.php b/app/Notifications/Server/HetznerDeletionFailed.php index 866d2eb07..bb452b054 100644 --- a/app/Notifications/Server/HetznerDeletionFailed.php +++ b/app/Notifications/Server/HetznerDeletionFailed.php @@ -21,16 +21,6 @@ public function via(object $notifiable): array return $notifiable->getEnabledChannels('hetzner_deletion_failed'); } - public function deduplicationKey(object $notifiable, string $channel): ?string - { - return "hetzner-deletion-failed:{$this->hetznerServerId}:error:".hash('sha256', $this->errorMessage); - } - - public function deduplicateFor(): int - { - return 86400; - } - public function toMail(): MailMessage { $mail = new MailMessage; diff --git a/app/Notifications/Server/HighDiskUsage.php b/app/Notifications/Server/HighDiskUsage.php index 4007ca805..149d1bbc8 100644 --- a/app/Notifications/Server/HighDiskUsage.php +++ b/app/Notifications/Server/HighDiskUsage.php @@ -21,16 +21,6 @@ public function via(object $notifiable): array return $notifiable->getEnabledChannels('server_disk_usage'); } - public function deduplicationKey(object $notifiable, string $channel): ?string - { - return "high-disk-usage:server:{$this->server->uuid}:threshold:{$this->server_disk_usage_notification_threshold}"; - } - - public function deduplicateFor(): int - { - return 21600; - } - public function toMail(): MailMessage { $mail = new MailMessage; diff --git a/app/Notifications/Server/Reachable.php b/app/Notifications/Server/Reachable.php index b297b7d3d..e64b0af2a 100644 --- a/app/Notifications/Server/Reachable.php +++ b/app/Notifications/Server/Reachable.php @@ -30,16 +30,6 @@ public function via(object $notifiable): array return $notifiable->getEnabledChannels('server_reachable'); } - public function deduplicationKey(object $notifiable, string $channel): ?string - { - return "server-reachable:{$this->server->uuid}"; - } - - public function deduplicateFor(): int - { - return 1800; - } - public function toMail(): MailMessage { $mail = new MailMessage; diff --git a/app/Notifications/Server/ServerPatchCheck.php b/app/Notifications/Server/ServerPatchCheck.php index d0d5f4875..ba6cd4982 100644 --- a/app/Notifications/Server/ServerPatchCheck.php +++ b/app/Notifications/Server/ServerPatchCheck.php @@ -24,16 +24,6 @@ public function via(object $notifiable): array return $notifiable->getEnabledChannels('server_patch'); } - public function deduplicationKey(object $notifiable, string $channel): ?string - { - return "server-patch-check:server:{$this->server->uuid}:state:".hash('sha256', json_encode($this->patchData)); - } - - public function deduplicateFor(): int - { - return 86400; - } - public function toMail($notifiable = null): MailMessage { $mail = new MailMessage; diff --git a/app/Notifications/Server/TraefikVersionOutdated.php b/app/Notifications/Server/TraefikVersionOutdated.php index d6e5ae8aa..c94cc1732 100644 --- a/app/Notifications/Server/TraefikVersionOutdated.php +++ b/app/Notifications/Server/TraefikVersionOutdated.php @@ -38,18 +38,6 @@ private function getUpgradeTarget(array $info): string return $this->formatVersion($info['latest'] ?? 'unknown'); } - public function deduplicationKey(object $notifiable, string $channel): ?string - { - $serverUuids = $this->servers->pluck('uuid')->sort()->values()->join('|'); - - return 'traefik-version-outdated:servers:'.hash('sha256', $serverUuids); - } - - public function deduplicateFor(): int - { - return 86400; - } - public function toMail($notifiable = null): MailMessage { $mail = new MailMessage; diff --git a/app/Notifications/Server/Unreachable.php b/app/Notifications/Server/Unreachable.php index cd6fd63b6..99742f3b7 100644 --- a/app/Notifications/Server/Unreachable.php +++ b/app/Notifications/Server/Unreachable.php @@ -30,16 +30,6 @@ public function via(object $notifiable): array return $notifiable->getEnabledChannels('server_unreachable'); } - public function deduplicationKey(object $notifiable, string $channel): ?string - { - return "server-unreachable:{$this->server->uuid}"; - } - - public function deduplicateFor(): int - { - return 3600; - } - public function toMail(): ?MailMessage { $mail = new MailMessage; diff --git a/app/Notifications/SslExpirationNotification.php b/app/Notifications/SslExpirationNotification.php index 72ce136bd..78e1e8be9 100644 --- a/app/Notifications/SslExpirationNotification.php +++ b/app/Notifications/SslExpirationNotification.php @@ -59,22 +59,6 @@ public function via(object $notifiable): array return $notifiable->getEnabledChannels('ssl_certificate_renewal'); } - public function deduplicationKey(object $notifiable, string $channel): ?string - { - $resourceKeys = $this->resources - ->map(fn ($resource) => data_get($resource, 'uuid') ?? data_get($resource, 'name')) - ->sort() - ->values() - ->join('|'); - - return 'ssl-certificate-renewed:resources:'.hash('sha256', $resourceKeys); - } - - public function deduplicateFor(): int - { - return 86400; - } - public function toMail(): MailMessage { $mail = new MailMessage; diff --git a/app/Notifications/Test.php b/app/Notifications/Test.php index ea3dfe9c1..bbed22777 100644 --- a/app/Notifications/Test.php +++ b/app/Notifications/Test.php @@ -30,11 +30,6 @@ public function __construct(public ?string $emails = null, public ?string $chann $this->onQueue('high'); } - public function shouldDeduplicate(): bool - { - return false; - } - public function via(object $notifiable): array { if ($this->channel) { diff --git a/app/Notifications/TransactionalEmails/EmailChangeVerification.php b/app/Notifications/TransactionalEmails/EmailChangeVerification.php index bb5e7f870..ea8462366 100644 --- a/app/Notifications/TransactionalEmails/EmailChangeVerification.php +++ b/app/Notifications/TransactionalEmails/EmailChangeVerification.php @@ -25,16 +25,6 @@ public function __construct( $this->onQueue('high'); } - public function deduplicationKey(object $notifiable, string $channel): ?string - { - return "email-change-verification:user:{$this->user->id}:email:{$this->newEmail}:code:{$this->verificationCode}"; - } - - public function deduplicateFor(): int - { - return (int) max(1, now()->diffInSeconds($this->expiresAt, false)); - } - public function toMail(): MailMessage { // Use the configured expiry minutes value diff --git a/app/Notifications/TransactionalEmails/InvitationLink.php b/app/Notifications/TransactionalEmails/InvitationLink.php index f3b1e6d67..9bfb54798 100644 --- a/app/Notifications/TransactionalEmails/InvitationLink.php +++ b/app/Notifications/TransactionalEmails/InvitationLink.php @@ -21,16 +21,6 @@ public function __construct(public User $user, public bool $isTransactionalEmail $this->onQueue('high'); } - public function deduplicationKey(object $notifiable, string $channel): ?string - { - return "invitation-link:user:{$this->user->id}:email:{$this->user->email}"; - } - - public function deduplicateFor(): int - { - return 3600; - } - public function toMail(): MailMessage { $invitation = TeamInvitation::whereEmail($this->user->email)->first(); diff --git a/app/Notifications/TransactionalEmails/Test.php b/app/Notifications/TransactionalEmails/Test.php index dc8c0dac7..2f7d70bbf 100644 --- a/app/Notifications/TransactionalEmails/Test.php +++ b/app/Notifications/TransactionalEmails/Test.php @@ -15,11 +15,6 @@ public function __construct(public string $emails, public bool $isTransactionalE $this->onQueue('high'); } - public function shouldDeduplicate(): bool - { - return false; - } - public function via(): array { return [EmailChannel::class]; diff --git a/app/Services/NotificationDeduplicator.php b/app/Services/NotificationDeduplicator.php deleted file mode 100644 index d018dd0a5..000000000 --- a/app/Services/NotificationDeduplicator.php +++ /dev/null @@ -1,95 +0,0 @@ - $recipients - */ - public function shouldSend(object $notifiable, Notification $notification, string $channel, array $recipients, ?string $subject = null, ?string $body = null): bool - { - if (method_exists($notification, 'shouldDeduplicate') && ! $notification->shouldDeduplicate()) { - return true; - } - - $ttl = method_exists($notification, 'deduplicateFor') - ? $notification->deduplicateFor() - : self::DEFAULT_TTL; - - if ($ttl <= 0) { - return true; - } - - return Cache::add( - $this->cacheKey($notifiable, $notification, $channel, $recipients, $subject, $body), - true, - $ttl, - ); - } - - /** - * @param array $recipients - */ - private function cacheKey(object $notifiable, Notification $notification, string $channel, array $recipients, ?string $subject, ?string $body): string - { - $semanticKey = method_exists($notification, 'deduplicationKey') - ? $notification->deduplicationKey($notifiable, $channel) - : null; - - $payload = $semanticKey - ? $this->semanticFingerprint($notifiable, $notification, $channel, $recipients, $semanticKey) - : $this->defaultFingerprint($notifiable, $notification, $channel, $recipients, $subject, $body); - - return 'notification-dedupe:'.hash('sha256', $payload); - } - - /** - * @param array $recipients - */ - private function semanticFingerprint(object $notifiable, Notification $notification, string $channel, array $recipients, string $semanticKey): string - { - return json_encode([ - 'notification' => $notification::class, - 'notifiable' => $notifiable::class, - 'notifiable_id' => data_get($notifiable, 'id'), - 'channel' => $channel, - 'recipients' => $this->normalizeRecipients($recipients), - 'semantic_key' => $semanticKey, - ], JSON_THROW_ON_ERROR); - } - - /** - * @param array $recipients - */ - private function defaultFingerprint(object $notifiable, Notification $notification, string $channel, array $recipients, ?string $subject, ?string $body): string - { - return json_encode([ - 'notification' => $notification::class, - 'notifiable' => $notifiable::class, - 'notifiable_id' => data_get($notifiable, 'id'), - 'channel' => $channel, - 'recipients' => $this->normalizeRecipients($recipients), - 'subject' => $subject, - 'body_hash' => hash('sha256', (string) $body), - ], JSON_THROW_ON_ERROR); - } - - /** - * @param array $recipients - * @return array - */ - private function normalizeRecipients(array $recipients): array - { - return collect($recipients) - ->map(fn (string $recipient) => mb_strtolower(trim($recipient))) - ->sort() - ->values() - ->all(); - } -} diff --git a/tests/Feature/ApplicationStoppedAfterRestartLimitTest.php b/tests/Feature/ApplicationStoppedAfterRestartLimitTest.php index 482a3e16e..6b82d5568 100644 --- a/tests/Feature/ApplicationStoppedAfterRestartLimitTest.php +++ b/tests/Feature/ApplicationStoppedAfterRestartLimitTest.php @@ -48,24 +48,6 @@ function applicationWithRestartState(array $attributes = []): Application expect($html)->not->toContain('Stopped after reaching restart limit'); }); -it('uses a semantic dedupe key for restart limit notifications', function () { - $application = applicationWithRestartState(); - $application->forceFill([ - 'name' => 'crashy-app', - 'uuid' => 'application-uuid', - ]); - $application->setRelation('environment', (object) [ - 'uuid' => 'environment-uuid', - 'name' => 'production', - 'project' => (object) ['uuid' => 'project-uuid'], - ]); - - $notification = new RestartLimitReached($application); - - expect($notification->deduplicationKey((object) ['id' => 1], 'mail'))->toBe('restart-limit-reached:application:application-uuid:count:2') - ->and($notification->deduplicateFor())->toBe(86400); -}); - it('keeps restart tracking configurable when stopping an application', function () { $method = new ReflectionMethod(StopApplication::class, 'handle'); $resetRestartCount = collect($method->getParameters())->firstWhere('name', 'resetRestartCount'); diff --git a/tests/Feature/NotificationDeduplicationTest.php b/tests/Feature/NotificationDeduplicationTest.php deleted file mode 100644 index 39fc70433..000000000 --- a/tests/Feature/NotificationDeduplicationTest.php +++ /dev/null @@ -1,75 +0,0 @@ -subject('Test'); - } - - public function shouldDeduplicate(): bool - { - return $this->deduplicate; - } - - public function deduplicateFor(): int - { - return $this->ttl; - } - - public function deduplicationKey(object $notifiable, string $channel): ?string - { - return $this->semanticKey; - } -} - -beforeEach(function () { - Cache::flush(); - $this->deduplicator = app(NotificationDeduplicator::class); - $this->notifiable = new class - { - public int $id = 123; - }; -}); - -it('allows only the first identical notification fingerprint during the ttl', function () { - $notification = new DedupeTestNotification; - - expect($this->deduplicator->shouldSend($this->notifiable, $notification, 'mail', ['first@example.com'], 'Subject', '

Body

'))->toBeTrue() - ->and($this->deduplicator->shouldSend($this->notifiable, $notification, 'mail', ['first@example.com'], 'Subject', '

Body

'))->toBeFalse(); -}); - -it('allows different recipients and content through the default fingerprint', function () { - $notification = new DedupeTestNotification; - - expect($this->deduplicator->shouldSend($this->notifiable, $notification, 'mail', ['first@example.com'], 'Subject', '

Body

'))->toBeTrue() - ->and($this->deduplicator->shouldSend($this->notifiable, $notification, 'mail', ['second@example.com'], 'Subject', '

Body

'))->toBeTrue() - ->and($this->deduplicator->shouldSend($this->notifiable, $notification, 'mail', ['first@example.com'], 'Other subject', '

Body

'))->toBeTrue() - ->and($this->deduplicator->shouldSend($this->notifiable, $notification, 'mail', ['first@example.com'], 'Subject', '

Other body

'))->toBeTrue(); -}); - -it('uses semantic keys instead of rendered content when provided', function () { - $notification = new DedupeTestNotification(semanticKey: 'event:123'); - - expect($this->deduplicator->shouldSend($this->notifiable, $notification, 'mail', ['first@example.com'], 'Subject', '

Body

'))->toBeTrue() - ->and($this->deduplicator->shouldSend($this->notifiable, $notification, 'mail', ['first@example.com'], 'Other subject', '

Other body

'))->toBeFalse() - ->and($this->deduplicator->shouldSend($this->notifiable, $notification, 'mail', ['second@example.com'], 'Other subject', '

Other body

'))->toBeTrue(); -}); - -it('allows notifications to opt out of deduplication', function () { - $notification = new DedupeTestNotification(deduplicate: false); - - expect($this->deduplicator->shouldSend($this->notifiable, $notification, 'mail', ['first@example.com'], 'Subject', '

Body

'))->toBeTrue() - ->and($this->deduplicator->shouldSend($this->notifiable, $notification, 'mail', ['first@example.com'], 'Subject', '

Body

'))->toBeTrue(); -});