feat(mail): make notification failures non-fatal
Add App\Support\SafeNotifier that wraps $notifiable->notify() in a Throwable guard with a logged warning, so SMTP outages (the current Gmail 535 errors) no longer propagate as a 500 to the user. Apply to the two hot paths that were still unprotected: CommentController (new comment, reply, like) and RegisteredUserController (super-admin registration alert). User::sendEmailVerificationNotification() is wrapped inline for the same reason. Also add docs/mail-setup.md covering the three delivery options: Gmail app password (quick), Mailgun, Postmark — with the exact .env keys for each. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
This commit is contained in:
1 parent
a1493462f7
commit
1cefc6ad2d
5 files changed
+125
-5
No files matched your search
@@ -7,6 +7,7 @@ use App\Models\User;
|
|||||||
use App\Notifications\NewUserRegistered;
|
use App\Notifications\NewUserRegistered;
|
||||||
use App\Rules\NotDisposableEmail;
|
use App\Rules\NotDisposableEmail;
|
||||||
use App\Rules\ReservedIdentifier;
|
use App\Rules\ReservedIdentifier;
|
||||||
|
use App\Support\SafeNotifier;
|
||||||
use Illuminate\Auth\Events\Registered;
|
use Illuminate\Auth\Events\Registered;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Hash;
|
use Illuminate\Support\Facades\Hash;
|
||||||
@@ -49,7 +50,7 @@ class RegisteredUserController extends Controller
|
|||||||
|
|
||||||
// Notify all super admins of the new registration
|
// Notify all super admins of the new registration
|
||||||
User::where('role', 'super_admin')->each(function (User $admin) use ($user) {
|
User::where('role', 'super_admin')->each(function (User $admin) use ($user) {
|
||||||
$admin->notify(new NewUserRegistered($user));
|
SafeNotifier::send($admin, new NewUserRegistered($user));
|
||||||
});
|
});
|
||||||
|
|
||||||
auth()->login($user);
|
auth()->login($user);
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ use App\Notifications\NewCommentLikeNotification;
|
|||||||
use App\Notifications\NewCommentNotification;
|
use App\Notifications\NewCommentNotification;
|
||||||
use App\Notifications\NewReplyNotification;
|
use App\Notifications\NewReplyNotification;
|
||||||
use App\Services\LlmLyricsService;
|
use App\Services\LlmLyricsService;
|
||||||
|
use App\Support\SafeNotifier;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
|
||||||
@@ -71,12 +72,12 @@ class CommentController extends Controller
|
|||||||
// Reply — notify the parent comment's author
|
// Reply — notify the parent comment's author
|
||||||
$parent = Comment::find($request->parent_id);
|
$parent = Comment::find($request->parent_id);
|
||||||
if ($parent && $parent->user_id !== $commenter->id) {
|
if ($parent && $parent->user_id !== $commenter->id) {
|
||||||
$parent->user->notify(new NewReplyNotification($video, $comment, $commenter));
|
SafeNotifier::send($parent->user, new NewReplyNotification($video, $comment, $commenter));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Top-level comment — notify the video owner
|
// Top-level comment — notify the video owner
|
||||||
if ($video->user_id !== $commenter->id) {
|
if ($video->user_id !== $commenter->id) {
|
||||||
$video->user->notify(new NewCommentNotification($video, $comment, $commenter));
|
SafeNotifier::send($video->user, new NewCommentNotification($video, $comment, $commenter));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,7 +163,8 @@ class CommentController extends Controller
|
|||||||
if ($comment->user_id !== $userId) {
|
if ($comment->user_id !== $userId) {
|
||||||
$video = $comment->video;
|
$video = $comment->video;
|
||||||
if ($video) {
|
if ($video) {
|
||||||
$comment->user->notify(
|
SafeNotifier::send(
|
||||||
|
$comment->user,
|
||||||
new NewCommentLikeNotification($video, $comment, Auth::user())
|
new NewCommentLikeNotification($video, $comment, Auth::user())
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-1
@@ -281,7 +281,19 @@ class User extends Authenticatable implements MustVerifyEmail
|
|||||||
|
|
||||||
public function sendEmailVerificationNotification(): void
|
public function sendEmailVerificationNotification(): void
|
||||||
{
|
{
|
||||||
$this->notify(new VerifyEmail);
|
// Mail failures must never bubble up as a 500 to the user — the account
|
||||||
|
// exists, the HTTP request was fine, only the delivery pipe is broken.
|
||||||
|
// We log the failure (ops picks it up) and move on. Re-sending via
|
||||||
|
// `php artisan app:resend-verification` fixes the user once SMTP recovers.
|
||||||
|
try {
|
||||||
|
$this->notify(new VerifyEmail);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
\Illuminate\Support\Facades\Log::warning('verify-email send failed', [
|
||||||
|
'user_id' => $this->id,
|
||||||
|
'email' => $this->email,
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Role helper methods
|
// Role helper methods
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Support;
|
||||||
|
|
||||||
|
use Illuminate\Notifications\Notification;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notification failures (SMTP down, Gmail 535, Mailgun 4xx, etc.) must never
|
||||||
|
* crash a user-facing request. Everywhere we previously did
|
||||||
|
*
|
||||||
|
* $user->notify(new SomeNotification(...));
|
||||||
|
*
|
||||||
|
* we now do
|
||||||
|
*
|
||||||
|
* SafeNotifier::send($user, new SomeNotification(...));
|
||||||
|
*
|
||||||
|
* which runs the same code, catches any Throwable, and writes a warning line
|
||||||
|
* so ops still has the signal.
|
||||||
|
*/
|
||||||
|
class SafeNotifier
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param mixed $notifiable Anything with ->notify(), usually a User.
|
||||||
|
*/
|
||||||
|
public static function send(mixed $notifiable, Notification $notification): void
|
||||||
|
{
|
||||||
|
if (! $notifiable || ! is_object($notifiable) || ! method_exists($notifiable, 'notify')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$notifiable->notify($notification);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
Log::warning('notification send failed', [
|
||||||
|
'notification' => $notification::class,
|
||||||
|
'notifiable' => $notifiable::class,
|
||||||
|
'notifiable_id'=> $notifiable->getKey() ?? null,
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
# Mail setup
|
||||||
|
|
||||||
|
Mail failures are now non-fatal — a down SMTP will log a warning in
|
||||||
|
`data/logs/laravel.log` but will NOT 500 the user. Still, delivery is a
|
||||||
|
feature; here's how to fix the current Gmail 535 auth errors.
|
||||||
|
|
||||||
|
## Option A — Gmail App Password (quickest, free, ~500 sends/day)
|
||||||
|
|
||||||
|
1. On `[email protected]`:
|
||||||
|
- Enable 2-Step Verification (Google Account → Security).
|
||||||
|
- Generate an **App Password** (Security → App passwords → "Mail", "Other: TAKEONE").
|
||||||
|
- Copy the 16-character password Google shows (no spaces).
|
||||||
|
|
||||||
|
2. `.env`:
|
||||||
|
```env
|
||||||
|
MAIL_MAILER=smtp
|
||||||
|
MAIL_HOST=smtp.gmail.com
|
||||||
|
MAIL_PORT=587
|
||||||
|
MAIL_USERNAME=platformtakeone@gmail.com
|
||||||
|
MAIL_PASSWORD=xxxx-xxxx-xxxx-xxxx # the app password, no quotes needed
|
||||||
|
MAIL_ENCRYPTION=tls
|
||||||
|
MAIL_FROM_ADDRESS=platformtakeone@gmail.com
|
||||||
|
MAIL_FROM_NAME="TAKEONE"
|
||||||
|
```
|
||||||
|
|
||||||
|
3. `php artisan config:clear && php artisan queue:restart`
|
||||||
|
|
||||||
|
4. Smoke test:
|
||||||
|
```bash
|
||||||
|
php artisan app:resend-verification --user=<id-of-an-unverified-user>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Option B — Mailgun (recommended for production)
|
||||||
|
|
||||||
|
```env
|
||||||
|
MAIL_MAILER=mailgun
|
||||||
|
MAILGUN_DOMAIN=mg.takeone.bh
|
||||||
|
MAILGUN_SECRET=key-...
|
||||||
|
MAILGUN_ENDPOINT=api.eu.mailgun.net # or api.mailgun.net for US region
|
||||||
|
MAIL_FROM_ADDRESS=no-reply@takeone.bh
|
||||||
|
MAIL_FROM_NAME="TAKEONE"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Option C — Postmark (also fine)
|
||||||
|
|
||||||
|
```env
|
||||||
|
MAIL_MAILER=postmark
|
||||||
|
POSTMARK_TOKEN=...
|
||||||
|
MAIL_FROM_ADDRESS=no-reply@takeone.bh
|
||||||
|
MAIL_FROM_NAME="TAKEONE"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Resend verification to users whose first email failed
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# One specific user
|
||||||
|
php artisan app:resend-verification --user=42
|
||||||
|
|
||||||
|
# Everyone still unverified (dry-run first)
|
||||||
|
php artisan app:resend-verification --all-unverified --dry-run
|
||||||
|
php artisan app:resend-verification --all-unverified
|
||||||
|
```
|
||||||
Reference in new issue
Block a user