From 56863f488fc2a4010964d3aec85ca524705f402b Mon Sep 17 00:00:00 2001 From: Simon Hamp Date: Mon, 13 Apr 2026 17:54:17 +0100 Subject: [PATCH] Skip sending emails to users with unverified email addresses Co-Authored-By: Claude Opus 4.6 --- .../Commands/GrantPluginToBundleOwners.php | 2 +- .../Commands/ResendNewPluginNotifications.php | 1 + .../Commands/SendLegacyLicenseThankYou.php | 4 +-- .../Commands/SendLicenseExpiryWarnings.php | 2 +- .../Commands/SendMaxToUltraAnnouncement.php | 1 + .../SendPluginSubmissionReminders.php | 1 + .../Commands/SendUltraFreeUserPromotion.php | 1 + .../SendUltraLicenseHolderPromotion.php | 2 +- .../Commands/SendUltraUpgradePromotion.php | 1 + app/Jobs/SendNewPluginNotifications.php | 1 + .../SuppressMailNotificationListener.php | 4 +++ package-lock.json | 2 +- .../Jobs/SendNewPluginNotificationsTest.php | 13 +++++++ .../SendPluginSubmissionRemindersTest.php | 13 +++++++ .../SendUltraFreeUserPromotionTest.php | 13 +++++++ .../SuppressMailNotificationListenerTest.php | 34 +++++++++++++++++++ 16 files changed, 89 insertions(+), 6 deletions(-) diff --git a/app/Console/Commands/GrantPluginToBundleOwners.php b/app/Console/Commands/GrantPluginToBundleOwners.php index 07acb0d6..898c29a2 100644 --- a/app/Console/Commands/GrantPluginToBundleOwners.php +++ b/app/Console/Commands/GrantPluginToBundleOwners.php @@ -47,7 +47,7 @@ public function handle(): int ->distinct() ->pluck('user_id'); - $users = User::whereIn('id', $userIds)->get(); + $users = User::whereIn('id', $userIds)->whereNotNull('email_verified_at')->get(); if ($users->isEmpty()) { $this->warn('No users found who have purchased this bundle.'); diff --git a/app/Console/Commands/ResendNewPluginNotifications.php b/app/Console/Commands/ResendNewPluginNotifications.php index 14d0c941..d87a063e 100644 --- a/app/Console/Commands/ResendNewPluginNotifications.php +++ b/app/Console/Commands/ResendNewPluginNotifications.php @@ -37,6 +37,7 @@ public function handle(): int } $recipients = User::query() + ->whereNotNull('email_verified_at') ->where('receives_new_plugin_notifications', true) ->whereNotIn('id', $plugins->pluck('user_id')) ->get(); diff --git a/app/Console/Commands/SendLegacyLicenseThankYou.php b/app/Console/Commands/SendLegacyLicenseThankYou.php index 74c7b81b..d082742b 100644 --- a/app/Console/Commands/SendLegacyLicenseThankYou.php +++ b/app/Console/Commands/SendLegacyLicenseThankYou.php @@ -42,8 +42,8 @@ public function handle(): int foreach ($userLicenses as $userId => $licenses) { $user = $licenses->first()->user; - if (! $user) { - $this->warn("Skipping license(s) for user ID {$userId} - user not found"); + if (! $user || ! $user->email_verified_at) { + $this->warn("Skipping license(s) for user ID {$userId} - user not found or email not verified"); $skipped++; continue; diff --git a/app/Console/Commands/SendLicenseExpiryWarnings.php b/app/Console/Commands/SendLicenseExpiryWarnings.php index e9241933..f128f430 100644 --- a/app/Console/Commands/SendLicenseExpiryWarnings.php +++ b/app/Console/Commands/SendLicenseExpiryWarnings.php @@ -68,7 +68,7 @@ private function sendWarningsForDays(int $days, bool $catchUp = false): int $licenses = $query->get(); foreach ($licenses as $license) { - if ($license->user) { + if ($license->user && $license->user->email_verified_at) { $license->user->notify(new LicenseExpiryWarning($license, $days)); // Track that we sent this warning diff --git a/app/Console/Commands/SendMaxToUltraAnnouncement.php b/app/Console/Commands/SendMaxToUltraAnnouncement.php index 1515b5a9..d3e43348 100644 --- a/app/Console/Commands/SendMaxToUltraAnnouncement.php +++ b/app/Console/Commands/SendMaxToUltraAnnouncement.php @@ -29,6 +29,7 @@ public function handle(): int ]); $users = User::query() + ->whereNotNull('email_verified_at') ->whereHas('subscriptions', function ($query) use ($maxPriceIds) { $query->where('stripe_status', 'active') ->where('is_comped', false) diff --git a/app/Console/Commands/SendPluginSubmissionReminders.php b/app/Console/Commands/SendPluginSubmissionReminders.php index 1c80d3ee..3d7f3989 100644 --- a/app/Console/Commands/SendPluginSubmissionReminders.php +++ b/app/Console/Commands/SendPluginSubmissionReminders.php @@ -23,6 +23,7 @@ public function handle(): int } $users = User::query() + ->whereNotNull('email_verified_at') ->whereHas('plugins', function ($query) { $query->whereIn('status', [PluginStatus::Draft, PluginStatus::Pending, PluginStatus::Rejected]); }) diff --git a/app/Console/Commands/SendUltraFreeUserPromotion.php b/app/Console/Commands/SendUltraFreeUserPromotion.php index e6708723..56133c8c 100644 --- a/app/Console/Commands/SendUltraFreeUserPromotion.php +++ b/app/Console/Commands/SendUltraFreeUserPromotion.php @@ -22,6 +22,7 @@ public function handle(): int } $users = User::query() + ->whereNotNull('email_verified_at') ->whereDoesntHave('licenses') ->whereDoesntHave('subscriptions') ->get(); diff --git a/app/Console/Commands/SendUltraLicenseHolderPromotion.php b/app/Console/Commands/SendUltraLicenseHolderPromotion.php index 29a03243..8dd8347f 100644 --- a/app/Console/Commands/SendUltraLicenseHolderPromotion.php +++ b/app/Console/Commands/SendUltraLicenseHolderPromotion.php @@ -37,7 +37,7 @@ public function handle(): int foreach ($userLicenses as $userId => $licenses) { $user = $licenses->first()->user; - if (! $user) { + if (! $user || ! $user->email_verified_at) { $skipped++; continue; diff --git a/app/Console/Commands/SendUltraUpgradePromotion.php b/app/Console/Commands/SendUltraUpgradePromotion.php index 4b1fcbfb..f1a03fcd 100644 --- a/app/Console/Commands/SendUltraUpgradePromotion.php +++ b/app/Console/Commands/SendUltraUpgradePromotion.php @@ -36,6 +36,7 @@ public function handle(): int $eligiblePriceIds = array_merge($miniPriceIds, $proPriceIds); $users = User::query() + ->whereNotNull('email_verified_at') ->whereHas('subscriptions', function ($query) use ($eligiblePriceIds) { $query->where('stripe_status', 'active') ->where('is_comped', false) diff --git a/app/Jobs/SendNewPluginNotifications.php b/app/Jobs/SendNewPluginNotifications.php index a0877a18..2e5446f3 100644 --- a/app/Jobs/SendNewPluginNotifications.php +++ b/app/Jobs/SendNewPluginNotifications.php @@ -18,6 +18,7 @@ public function __construct(public Plugin $plugin) {} public function handle(): void { $recipients = User::query() + ->whereNotNull('email_verified_at') ->where('receives_new_plugin_notifications', true) ->where('id', '!=', $this->plugin->user_id) ->get(); diff --git a/app/Listeners/SuppressMailNotificationListener.php b/app/Listeners/SuppressMailNotificationListener.php index 234eb0d1..b84c3808 100644 --- a/app/Listeners/SuppressMailNotificationListener.php +++ b/app/Listeners/SuppressMailNotificationListener.php @@ -17,6 +17,10 @@ public function handle(NotificationSending $event): bool return true; } + if (! $event->notifiable->email_verified_at) { + return false; + } + return $event->notifiable->receives_notification_emails; } } diff --git a/package-lock.json b/package-lock.json index 1205e7c4..c61ba396 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,5 +1,5 @@ { - "name": "eager-ferret", + "name": "tender-husky", "lockfileVersion": 3, "requires": true, "packages": { diff --git a/tests/Feature/Jobs/SendNewPluginNotificationsTest.php b/tests/Feature/Jobs/SendNewPluginNotificationsTest.php index 5f656b38..5eea9b3d 100644 --- a/tests/Feature/Jobs/SendNewPluginNotificationsTest.php +++ b/tests/Feature/Jobs/SendNewPluginNotificationsTest.php @@ -41,4 +41,17 @@ public function test_job_does_not_notify_plugin_author(): void Notification::assertNotSentTo($author, NewPluginAvailable::class); } + + public function test_job_does_not_notify_unverified_users(): void + { + Notification::fake(); + + $author = User::factory()->create(); + $unverified = User::factory()->unverified()->create(['receives_new_plugin_notifications' => true]); + $plugin = Plugin::factory()->approved()->for($author)->create(); + + (new SendNewPluginNotifications($plugin))->handle(); + + Notification::assertNotSentTo($unverified, NewPluginAvailable::class); + } } diff --git a/tests/Feature/SendPluginSubmissionRemindersTest.php b/tests/Feature/SendPluginSubmissionRemindersTest.php index 5ad1ed19..048f894b 100644 --- a/tests/Feature/SendPluginSubmissionRemindersTest.php +++ b/tests/Feature/SendPluginSubmissionRemindersTest.php @@ -151,4 +151,17 @@ public function test_notification_database_array_contains_plugin_names(): void $this->assertArrayHasKey('plugin_names', $data); $this->assertContains('acme/test-plugin', $data['plugin_names']); } + + public function test_does_not_send_to_unverified_users(): void + { + Notification::fake(); + + $user = User::factory()->unverified()->create(); + Plugin::factory()->draft()->for($user)->create(); + + $this->artisan('plugins:send-submission-reminders') + ->assertExitCode(0); + + Notification::assertNothingSent(); + } } diff --git a/tests/Feature/SendUltraFreeUserPromotionTest.php b/tests/Feature/SendUltraFreeUserPromotionTest.php index d3b79c9f..a9338e0f 100644 --- a/tests/Feature/SendUltraFreeUserPromotionTest.php +++ b/tests/Feature/SendUltraFreeUserPromotionTest.php @@ -186,4 +186,17 @@ public function test_notification_mentions_nativephp_is_free(): void $this->assertStringContainsString('free and open source', $rendered); $this->assertStringContainsString('no license required', $rendered); } + + public function test_skips_unverified_users(): void + { + Notification::fake(); + + $unverified = User::factory()->unverified()->create(); + + $this->artisan('ultra:send-free-user-promo') + ->expectsOutputToContain('Found 0 eligible user(s)') + ->assertSuccessful(); + + Notification::assertNotSentTo($unverified, UltraFreeUserPromotion::class); + } } diff --git a/tests/Feature/SuppressMailNotificationListenerTest.php b/tests/Feature/SuppressMailNotificationListenerTest.php index 62716b40..6c9b4b98 100644 --- a/tests/Feature/SuppressMailNotificationListenerTest.php +++ b/tests/Feature/SuppressMailNotificationListenerTest.php @@ -71,4 +71,38 @@ public function test_new_users_receive_notifications_by_default(): void $this->assertTrue($user->receives_notification_emails); } + + public function test_suppresses_mail_when_user_email_is_not_verified(): void + { + $user = User::factory()->unverified()->create(['receives_notification_emails' => true]); + + $event = new NotificationSending( + $user, + new PluginApproved( + plugin: Plugin::factory()->for($user)->create(), + ), + 'mail', + ); + + $listener = new SuppressMailNotificationListener; + + $this->assertFalse($listener->handle($event)); + } + + public function test_allows_non_mail_channels_for_unverified_users(): void + { + $user = User::factory()->unverified()->create(['receives_notification_emails' => true]); + + $event = new NotificationSending( + $user, + new PluginApproved( + plugin: Plugin::factory()->for($user)->create(), + ), + 'database', + ); + + $listener = new SuppressMailNotificationListener; + + $this->assertTrue($listener->handle($event)); + } }