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:
ghassanandClaude Opus 4.7 committed 2026-10-21 00:35:41 +03:00
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\Rules\NotDisposableEmail;
use App\Rules\ReservedIdentifier;
use App\Support\SafeNotifier;
use Illuminate\Auth\Events\Registered;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
@@ -49,7 +50,7 @@ class RegisteredUserController extends Controller
// Notify all super admins of the new registration
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);
+5 -3
View File
@@ -9,6 +9,7 @@ use App\Notifications\NewCommentLikeNotification;
use App\Notifications\NewCommentNotification;
use App\Notifications\NewReplyNotification;
use App\Services\LlmLyricsService;
use App\Support\SafeNotifier;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
@@ -71,12 +72,12 @@ class CommentController extends Controller
// Reply — notify the parent comment's author
$parent = Comment::find($request->parent_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 {
// Top-level comment — notify the video owner
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) {
$video = $comment->video;
if ($video) {
$comment->user->notify(
SafeNotifier::send(
$comment->user,
new NewCommentLikeNotification($video, $comment, Auth::user())
);
}
+13 -1
View File
@@ -281,7 +281,19 @@ class User extends Authenticatable implements MustVerifyEmail
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
+43
View File
@@ -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(),
]);
}
}
}