diff --git a/app/Models/User.php b/app/Models/User.php index f59d74c9..0dfbd18c 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -17,6 +17,7 @@ use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; use Illuminate\Notifications\Notification; use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Log; use Laravel\Cashier\Billable; use Laravel\Fortify\TwoFactorAuthenticatable; use Laravel\Pennant\Concerns\HasFeatures; @@ -216,7 +217,18 @@ class User extends Authenticatable implements HasLocalePreference, MustVerifyEma return null; } - return $this->email; + $email = trim((string) $this->email); + + if ($email === '' || filter_var($email, FILTER_VALIDATE_EMAIL) === false) { + Log::warning('Skipping mail notification: invalid recipient email', [ + 'user_id' => $this->getKey(), + 'notification' => $notification ? $notification::class : null, + ]); + + return null; + } + + return $email; } public function sendEmailVerificationNotification(): void diff --git a/app/Models/UserLead.php b/app/Models/UserLead.php index 14ca7406..4ccfd9f5 100644 --- a/app/Models/UserLead.php +++ b/app/Models/UserLead.php @@ -13,6 +13,8 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Notifications\Notifiable; +use Illuminate\Notifications\Notification; +use Illuminate\Support\Facades\Log; use Illuminate\Support\Str; /** @@ -124,6 +126,22 @@ class UserLead extends Model implements HasLocalePreference, MustVerifyEmail return $this->locale ?? 'en'; } + public function routeNotificationForMail(?Notification $notification = null): ?string + { + $email = trim((string) $this->email); + + if ($email === '' || filter_var($email, FILTER_VALIDATE_EMAIL) === false) { + Log::warning('Skipping mail notification: invalid recipient email', [ + 'user_lead_id' => $this->getKey(), + 'notification' => $notification ? $notification::class : null, + ]); + + return null; + } + + return $email; + } + public function getEmailForVerification(): string { return $this->email; diff --git a/tests/Feature/RouteNotificationForMailTest.php b/tests/Feature/RouteNotificationForMailTest.php new file mode 100644 index 00000000..b7dfa70d --- /dev/null +++ b/tests/Feature/RouteNotificationForMailTest.php @@ -0,0 +1,52 @@ +create(['email' => 'valid@example.com']); + + expect($user->routeNotificationForMail(new VerifyEmailNotification)) + ->toBe('valid@example.com'); +}); + +test('user skips mail routing when email is malformed', function () { + $user = User::factory()->create(); + $user->forceFill(['email' => 'not-an-email'])->saveQuietly(); + + expect($user->routeNotificationForMail(new VerifyEmailNotification)) + ->toBeNull(); +}); + +test('user skips mail routing when email has surrounding whitespace it cannot parse', function () { + $user = User::factory()->create(); + $user->forceFill(['email' => ' '])->saveQuietly(); + + expect($user->routeNotificationForMail(new VerifyEmailNotification)) + ->toBeNull(); +}); + +test('user trims valid email before routing', function () { + $user = User::factory()->create(); + $user->forceFill(['email' => " spaced@example.com\n"])->saveQuietly(); + + expect($user->routeNotificationForMail(new VerifyEmailNotification)) + ->toBe('spaced@example.com'); +}); + +test('user lead returns email for mail routing when valid', function () { + $lead = UserLead::factory()->create(['email' => 'lead@example.com']); + + expect($lead->routeNotificationForMail(new VerifyUserLeadEmailNotification('https://example.com'))) + ->toBe('lead@example.com'); +}); + +test('user lead skips mail routing when email is malformed', function () { + $lead = UserLead::factory()->create(); + $lead->forceFill(['email' => 'broken@@example'])->saveQuietly(); + + expect($lead->routeNotificationForMail(new VerifyUserLeadEmailNotification('https://example.com'))) + ->toBeNull(); +});