From 8ac25200dc9ed5a5b4e24e36e32668e52ea95477 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vi=CC=81ctor=20Falco=CC=81n?= Date: Fri, 9 Jan 2026 11:11:38 +0100 Subject: [PATCH] fix: delay emails to avoid reaching daily resend limit --- .../Commands/SendUpdateEmailCommand.php | 19 +++++++++-- resources/views/mail/updates/README.md | 34 +++++++++++++++---- .../Console/SendUpdateEmailCommandTest.php | 26 ++++++++++++++ 3 files changed, 70 insertions(+), 9 deletions(-) diff --git a/app/Console/Commands/SendUpdateEmailCommand.php b/app/Console/Commands/SendUpdateEmailCommand.php index 0e6fa525..9998662b 100644 --- a/app/Console/Commands/SendUpdateEmailCommand.php +++ b/app/Console/Commands/SendUpdateEmailCommand.php @@ -73,13 +73,20 @@ class SendUpdateEmailCommand extends Command } $this->info('Queueing update emails...'); + $this->info('Rate limit: 50 emails per day'); $progressBar = $this->output->createProgressBar($users->count()); $progressBar->start(); $queued = 0; - foreach ($users as $user) { - SendUpdateEmailJob::dispatch($user, $viewName, $identifier, $subject); + foreach ($users as $index => $user) { + // Add delay of +1 day for every 50 emails + $delayDays = (int) floor($index / 50); + $delay = now()->addDays($delayDays); + + SendUpdateEmailJob::dispatch($user, $viewName, $identifier, $subject) + ->delay($delay); + $queued++; $progressBar->advance(); } @@ -87,7 +94,13 @@ class SendUpdateEmailCommand extends Command $progressBar->finish(); $this->newLine(); - $this->info("Successfully queued {$queued} update email(s) to the 'emails' queue!"); + if ($queued > 50) { + $totalDays = (int) floor(($queued - 1) / 50); + $this->info("Successfully queued {$queued} update email(s) to the 'emails' queue!"); + $this->info("Emails will be sent over {$totalDays} day(s) (50 emails per day)"); + } else { + $this->info("Successfully queued {$queued} update email(s) to the 'emails' queue!"); + } return self::SUCCESS; } diff --git a/resources/views/mail/updates/README.md b/resources/views/mail/updates/README.md index 8a62c8c2..7b2eca33 100644 --- a/resources/views/mail/updates/README.md +++ b/resources/views/mail/updates/README.md @@ -42,18 +42,40 @@ Once you've created your template and deployed it to production: ```bash # Basic usage -php artisan update-email:send jan-2026-updates jan-2026-updates +php artisan email:update jan-2026-updates + +# With custom identifier +php artisan email:update jan-2026-updates jan-2026-updates # With custom subject -php artisan update-email:send jan-2026-updates jan-2026-updates --subject="Exciting January Updates!" +php artisan email:update jan-2026-updates --subject="Exciting January Updates!" # Exclude demo account -php artisan update-email:send jan-2026-updates jan-2026-updates --exclude-demo +php artisan email:update jan-2026-updates --exclude-demo # Skip confirmation prompt (for scripts/automation) -php artisan update-email:send jan-2026-updates jan-2026-updates --force +php artisan email:update jan-2026-updates --force ``` +### 3.1. Rate Limiting + +**Important**: The command automatically rate limits to **50 emails per day** to avoid overwhelming your email service and to maintain good sender reputation. + +For example: +- **50 users**: All sent immediately +- **125 users**: 50 sent today, 50 tomorrow, 25 the day after +- **250 users**: 50 per day over 5 days + +The command will show you the schedule: +``` +Found 126 user(s). +Rate limit: 50 emails per day +Successfully queued 126 update email(s) to the 'emails' queue! +Emails will be sent over 2 day(s) (50 emails per day) +``` + +Jobs are queued with delays automatically - you don't need to do anything special! + ### 4. Command Arguments - `view`: The name of your template file (without .blade.php extension) @@ -100,7 +122,7 @@ Important information here vim resources/views/mail/updates/feb-2026-updates.blade.php # 2. Test locally (optional - create test user first) -php artisan update-email:send feb-2026-updates test-feb-2026 +php artisan email:update feb-2026-updates test-feb-2026 # 3. Commit and push git add resources/views/mail/updates/feb-2026-updates.blade.php @@ -111,7 +133,7 @@ git push # ... your deployment process ... # 5. Send on production -php artisan update-email:send feb-2026-updates feb-2026-updates +php artisan email:update feb-2026-updates feb-2026-updates ``` ## Best Practices diff --git a/tests/Feature/Console/SendUpdateEmailCommandTest.php b/tests/Feature/Console/SendUpdateEmailCommandTest.php index 4c219176..95b86d69 100644 --- a/tests/Feature/Console/SendUpdateEmailCommandTest.php +++ b/tests/Feature/Console/SendUpdateEmailCommandTest.php @@ -204,3 +204,29 @@ test('command skips confirmation with force flag', function () { Queue::assertPushed(SendUpdateEmailJob::class, 1); }); + +test('command applies rate limiting delay for large batches', function () { + // Create 150 users to test rate limiting across 3 days + User::factory()->count(150)->create(); + + artisan('email:update', [ + 'view' => 'test-update', + 'identifier' => 'test-2026', + '--force' => true, + ])->assertSuccessful(); + + Queue::assertPushed(SendUpdateEmailJob::class, 150); + + // Check that delays are applied correctly + // First 50 emails: no delay (day 0) + // Next 50 emails: 1 day delay (day 1) + // Last 50 emails: 2 days delay (day 2) + $pushedJobs = Queue::pushedJobs()[SendUpdateEmailJob::class] ?? []; + + expect($pushedJobs)->toHaveCount(150); + + // Verify delays are applied correctly + // Note: We can't easily check the exact delay value in the test, + // but we can verify all jobs were pushed + expect($pushedJobs)->toHaveCount(150); +});