feat(queue): Implement queueable email jobs with rate limiting

This commit is contained in:
Víctor Falcón 2025-12-18 12:30:36 +01:00
parent 1e9566a289
commit 3d0d6c8bef
13 changed files with 124 additions and 30 deletions

View File

@ -4,16 +4,21 @@ namespace App\Mail\Drip;
use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\Middleware\RateLimited;
use Illuminate\Queue\SerializesModels;
class FeedbackEmail extends Mailable
class FeedbackEmail extends Mailable implements ShouldQueue
{
use Queueable, SerializesModels;
public function __construct(public User $user) {}
public function __construct(public User $user)
{
$this->onQueue('emails');
}
public function envelope(): Envelope
{
@ -31,4 +36,14 @@ class FeedbackEmail extends Mailable
],
);
}
/**
* Get the middleware the job should pass through.
*
* @return array<int, object>
*/
public function middleware(): array
{
return [new RateLimited('emails', releaseAfter: 1)];
}
}

View File

@ -4,16 +4,21 @@ namespace App\Mail\Drip;
use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\Middleware\RateLimited;
use Illuminate\Queue\SerializesModels;
class ImportHelpEmail extends Mailable
class ImportHelpEmail extends Mailable implements ShouldQueue
{
use Queueable, SerializesModels;
public function __construct(public User $user) {}
public function __construct(public User $user)
{
$this->onQueue('emails');
}
public function envelope(): Envelope
{
@ -31,4 +36,14 @@ class ImportHelpEmail extends Mailable
],
);
}
/**
* Get the middleware the job should pass through.
*
* @return array<int, object>
*/
public function middleware(): array
{
return [new RateLimited('emails', releaseAfter: 1)];
}
}

View File

@ -4,16 +4,21 @@ namespace App\Mail\Drip;
use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\Middleware\RateLimited;
use Illuminate\Queue\SerializesModels;
class OnboardingReminderEmail extends Mailable
class OnboardingReminderEmail extends Mailable implements ShouldQueue
{
use Queueable, SerializesModels;
public function __construct(public User $user) {}
public function __construct(public User $user)
{
$this->onQueue('emails');
}
public function envelope(): Envelope
{
@ -31,4 +36,14 @@ class OnboardingReminderEmail extends Mailable
],
);
}
/**
* Get the middleware the job should pass through.
*
* @return array<int, object>
*/
public function middleware(): array
{
return [new RateLimited('emails', releaseAfter: 1)];
}
}

View File

@ -4,16 +4,21 @@ namespace App\Mail\Drip;
use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\Middleware\RateLimited;
use Illuminate\Queue\SerializesModels;
class PromoCodeEmail extends Mailable
class PromoCodeEmail extends Mailable implements ShouldQueue
{
use Queueable, SerializesModels;
public function __construct(public User $user) {}
public function __construct(public User $user)
{
$this->onQueue('emails');
}
public function envelope(): Envelope
{
@ -32,4 +37,14 @@ class PromoCodeEmail extends Mailable
],
);
}
/**
* Get the middleware the job should pass through.
*
* @return array<int, object>
*/
public function middleware(): array
{
return [new RateLimited('emails', releaseAfter: 1)];
}
}

View File

@ -4,16 +4,21 @@ namespace App\Mail\Drip;
use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\Middleware\RateLimited;
use Illuminate\Queue\SerializesModels;
class WelcomeEmail extends Mailable
class WelcomeEmail extends Mailable implements ShouldQueue
{
use Queueable, SerializesModels;
public function __construct(public User $user) {}
public function __construct(public User $user)
{
$this->onQueue('emails');
}
public function envelope(): Envelope
{
@ -31,4 +36,14 @@ class WelcomeEmail extends Mailable
],
);
}
/**
* Get the middleware the job should pass through.
*
* @return array<int, object>
*/
public function middleware(): array
{
return [new RateLimited('emails', releaseAfter: 1)];
}
}

View File

@ -8,6 +8,7 @@ use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\Middleware\RateLimited;
use Illuminate\Queue\SerializesModels;
class UserLeadInvitation extends Mailable implements ShouldQueue
@ -19,7 +20,7 @@ class UserLeadInvitation extends Mailable implements ShouldQueue
*/
public function __construct(public UserLead $lead)
{
//
$this->onQueue('emails');
}
/**
@ -51,4 +52,14 @@ class UserLeadInvitation extends Mailable implements ShouldQueue
{
return [];
}
/**
* Get the middleware the job should pass through.
*
* @return array<int, object>
*/
public function middleware(): array
{
return [new RateLimited('emails', releaseAfter: 1)];
}
}

View File

@ -3,6 +3,8 @@
namespace App\Providers;
use App\Http\Responses\RegisterResponse;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\ServiceProvider;
use Laravel\Fortify\Contracts\RegisterResponse as RegisterResponseContract;
@ -21,6 +23,8 @@ class AppServiceProvider extends ServiceProvider
*/
public function boot(): void
{
//
RateLimiter::for('emails', function (object $job): Limit {
return Limit::perSecond(1);
});
}
}

View File

@ -1,5 +1,7 @@
<?php
use Illuminate\Support\Facades\Queue;
test('registration screen can be rendered', function () {
$response = $this->get(route('register'));
@ -7,6 +9,8 @@ test('registration screen can be rendered', function () {
});
test('new users can register', function () {
Queue::fake();
$response = $this->post(route('register.store'), [
'name' => 'Test User',
'email' => 'test@example.com',

View File

@ -17,7 +17,7 @@ test('feedback email is sent to non-subscribed users', function () {
SendFeedbackEmailJob::dispatchSync($user);
Mail::assertSent(FeedbackEmail::class, function ($mail) use ($user) {
Mail::assertQueued(FeedbackEmail::class, function ($mail) use ($user) {
return $mail->hasTo($user->email);
});
@ -39,7 +39,7 @@ test('feedback email is not sent to subscribed users', function () {
SendFeedbackEmailJob::dispatchSync($user);
Mail::assertNotSent(FeedbackEmail::class);
Mail::assertNotQueued(FeedbackEmail::class);
});
test('feedback email is not sent if already received', function () {
@ -49,5 +49,5 @@ test('feedback email is not sent if already received', function () {
SendFeedbackEmailJob::dispatchSync($user);
Mail::assertNotSent(FeedbackEmail::class);
Mail::assertNotQueued(FeedbackEmail::class);
});

View File

@ -18,7 +18,7 @@ test('import help email is sent to onboarded users without transactions who are
SendImportHelpEmailJob::dispatchSync($user);
Mail::assertSent(ImportHelpEmail::class, function ($mail) use ($user) {
Mail::assertQueued(ImportHelpEmail::class, function ($mail) use ($user) {
return $mail->hasTo($user->email);
});
@ -33,7 +33,7 @@ test('import help email is not sent to non-onboarded users', function () {
SendImportHelpEmailJob::dispatchSync($user);
Mail::assertNotSent(ImportHelpEmail::class);
Mail::assertNotQueued(ImportHelpEmail::class);
});
test('import help email is not sent to users with transactions', function () {
@ -42,7 +42,7 @@ test('import help email is not sent to users with transactions', function () {
SendImportHelpEmailJob::dispatchSync($user);
Mail::assertNotSent(ImportHelpEmail::class);
Mail::assertNotQueued(ImportHelpEmail::class);
});
test('import help email is not sent to subscribed users', function () {
@ -57,7 +57,7 @@ test('import help email is not sent to subscribed users', function () {
SendImportHelpEmailJob::dispatchSync($user);
Mail::assertNotSent(ImportHelpEmail::class);
Mail::assertNotQueued(ImportHelpEmail::class);
});
test('import help email is not sent if already received', function () {
@ -67,5 +67,5 @@ test('import help email is not sent if already received', function () {
SendImportHelpEmailJob::dispatchSync($user);
Mail::assertNotSent(ImportHelpEmail::class);
Mail::assertNotQueued(ImportHelpEmail::class);
});

View File

@ -16,7 +16,7 @@ test('onboarding reminder email is sent to non-onboarded users', function () {
SendOnboardingReminderEmailJob::dispatchSync($user);
Mail::assertSent(OnboardingReminderEmail::class, function ($mail) use ($user) {
Mail::assertQueued(OnboardingReminderEmail::class, function ($mail) use ($user) {
return $mail->hasTo($user->email);
});
@ -31,7 +31,7 @@ test('onboarding reminder email is not sent to onboarded users', function () {
SendOnboardingReminderEmailJob::dispatchSync($user);
Mail::assertNotSent(OnboardingReminderEmail::class);
Mail::assertNotQueued(OnboardingReminderEmail::class);
$this->assertDatabaseMissing('user_mail_logs', [
'user_id' => $user->id,
@ -46,5 +46,5 @@ test('onboarding reminder email is not sent if already received', function () {
SendOnboardingReminderEmailJob::dispatchSync($user);
Mail::assertNotSent(OnboardingReminderEmail::class);
Mail::assertNotQueued(OnboardingReminderEmail::class);
});

View File

@ -19,7 +19,7 @@ test('promo code email is sent to onboarded users with transactions who are not
SendPromoCodeEmailJob::dispatchSync($user);
Mail::assertSent(PromoCodeEmail::class, function ($mail) use ($user) {
Mail::assertQueued(PromoCodeEmail::class, function ($mail) use ($user) {
return $mail->hasTo($user->email);
});
@ -35,7 +35,7 @@ test('promo code email is not sent to non-onboarded users', function () {
SendPromoCodeEmailJob::dispatchSync($user);
Mail::assertNotSent(PromoCodeEmail::class);
Mail::assertNotQueued(PromoCodeEmail::class);
});
test('promo code email is not sent to users without transactions', function () {
@ -43,7 +43,7 @@ test('promo code email is not sent to users without transactions', function () {
SendPromoCodeEmailJob::dispatchSync($user);
Mail::assertNotSent(PromoCodeEmail::class);
Mail::assertNotQueued(PromoCodeEmail::class);
});
test('promo code email is not sent to subscribed users', function () {
@ -59,7 +59,7 @@ test('promo code email is not sent to subscribed users', function () {
SendPromoCodeEmailJob::dispatchSync($user);
Mail::assertNotSent(PromoCodeEmail::class);
Mail::assertNotQueued(PromoCodeEmail::class);
});
test('promo code email is not sent if already received', function () {
@ -70,5 +70,5 @@ test('promo code email is not sent if already received', function () {
SendPromoCodeEmailJob::dispatchSync($user);
Mail::assertNotSent(PromoCodeEmail::class);
Mail::assertNotQueued(PromoCodeEmail::class);
});

View File

@ -16,7 +16,7 @@ test('welcome email is sent and logged', function () {
SendWelcomeEmailJob::dispatchSync($user);
Mail::assertSent(WelcomeEmail::class, function ($mail) use ($user) {
Mail::assertQueued(WelcomeEmail::class, function ($mail) use ($user) {
return $mail->hasTo($user->email);
});
@ -33,5 +33,5 @@ test('welcome email is not sent if already received', function () {
SendWelcomeEmailJob::dispatchSync($user);
Mail::assertNotSent(WelcomeEmail::class);
Mail::assertNotQueued(WelcomeEmail::class);
});