Developer Guide 11 min read Markdown

Laravel Email Validation: From Built-In Rules to Blocking Disposables

Matt King
Matt King

October 1, 2025 · Updated August 30, 2026

Laravel Email Validation: From Built-In Rules to Blocking Disposables

This guide covers every layer of email validation in Laravel: the built-in email rule and what its modes actually check, unique validation, and then the part Laravel cannot do on its own, blocking disposable emails and scoring risk at signup with a custom rule.

Laravel's Built-In Email Validation

Before reaching for anything external, use what ships with the framework. The email rule validates format:

$request->validate([
    'email' => ['required', 'email'],
]);

By default this is RFC-style format checking only. You can make it stricter with modes:

// RFC compliance plus a DNS check that the domain has MX or A records
'email' => ['required', 'email:rfc,dns'],

// Add spoof detection (catches homograph tricks like Cyrillic lookalikes)
'email' => ['required', 'email:rfc,dns,spoof'],

email:rfc catches malformed addresses. dns catches domains that cannot receive mail at all, at the cost of a DNS lookup per validation. spoof needs the PHP intl extension.

To stop duplicate registrations, combine it with unique:

'email' => ['required', 'email:rfc,dns', 'unique:users,email'],

That is the ceiling of the built-in rules: format, resolvable domain, and uniqueness in your own database. What they cannot tell you is whether x7k2@10minutemail.com is a throwaway address that will bounce in an hour, whether the domain was registered yesterday, or whether this signup looks like free-tier abuse. For that you need an external validation API, and the rest of this guide wires one in properly: service class, custom validation rule, caching, and async fallback.

What You'll Build

  1. A FidroService class that wraps the API calls
  2. A custom NotDisposableEmail validation rule
  3. Redis caching to minimize API calls
  4. A queue job for async validation fallback

Step 1: Add Your API Key

# .env
FIDRO_API_KEY=your_api_key_here
// config/services.php
'fidro' => [
    'api_key' => env('FIDRO_API_KEY'),
    'base_url' => env('FIDRO_BASE_URL', 'https://fidro.io/api'),
],

Step 2: Create the Service Class

<?php
// app/Services/FidroService.php
namespace App\Services;

use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;

class FidroService
{
    public function validateEmail(string $email): ?array
    {
        $cacheKey = 'fidro:email:' . md5($email);

        return Cache::remember($cacheKey, now()->addHours(24), function () use ($email) {
            try {
                $response = Http::timeout(3)
                    ->withHeaders([
                        'Authorization' => 'Bearer ' . config('services.fidro.api_key'),
                    ])
                    ->post(config('services.fidro.base_url') . '/validate/email', [
                        'email' => $email,
                    ]);

                if ($response->successful()) {
                    return $response->json();
                }

                Log::warning('Fidro API error', [
                    'status' => $response->status(),
                    'email' => $email,
                ]);

                return null;
            } catch (\Exception $e) {
                Log::warning('Fidro API unreachable', [
                    'error' => $e->getMessage(),
                    'email' => $email,
                ]);

                return null; // Fail open
            }
        });
    }

    public function isDisposable(string $email): bool
    {
        $result = $this->validateEmail($email);
        return $result['disposable'] ?? false;
    }

    public function riskScore(string $email): float
    {
        $result = $this->validateEmail($email);
        return $result['risk_score'] ?? 0.0;
    }
}

Step 3: Create the Validation Rule

<?php
// app/Rules/NotDisposableEmail.php
namespace App\Rules;

use App\Services\FidroService;
use Closure;
use Illuminate\Contracts\Validation\ValidationRule;

class NotDisposableEmail implements ValidationRule
{
    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        $fidro = app(FidroService::class);
        $result = $fidro->validateEmail($value);

        // Fail open: if API is unreachable, allow the email
        if ($result === null) {
            return;
        }

        if ($result['disposable'] ?? false) {
            $fail('Please use a permanent email address. Temporary emails are not accepted.');
        }

        if (($result['risk_score'] ?? 0) > 0.7) {
            $fail('This email address could not be verified. Please try a different one.');
        }
    }
}

Step 4: Use It in Registration

// In your registration controller or form request
use App\Rules\NotDisposableEmail;

$request->validate([
    'name' => ['required', 'string', 'max:255'],
    'email' => ['required', 'string', 'email', 'max:255', 'unique:users', new NotDisposableEmail],
    'password' => ['required', 'confirmed', 'min:8'],
]);

This works identically with Laravel Breeze, Jetstream, and Fortify. Just add the rule to the existing validation array.

Step 5: Async Fallback with Queue Jobs

For cases where the API times out during signup, queue a background check:

<?php
// app/Jobs/ValidateUserEmail.php
namespace App\Jobs;

use App\Models\User;
use App\Services\FidroService;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;

class ValidateUserEmail implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public function __construct(public User $user) {}

    public function handle(FidroService $fidro): void
    {
        $result = $fidro->validateEmail($this->user->email);

        if ($result && ($result['disposable'] ?? false)) {
            $this->user->update([
                'email_flagged' => true,
                'email_flag_reason' => 'disposable',
            ]);
        }
    }
}

Dispatch it after registration:

ValidateUserEmail::dispatch($user)->delay(now()->addSeconds(5));

Testing the Integration

Mock the API in your tests so you never call the real endpoint:

public function test_disposable_emails_are_rejected(): void
{
    Http::fake([
        'fidro.io/api/*' => Http::response([
            'email' => 'test@mailinator.com',
            'disposable' => true,
            'risk_score' => 0.9,
        ]),
    ]);

    $response = $this->post('/register', [
        'name' => 'Test User',
        'email' => 'test@mailinator.com',
        'password' => 'password123',
        'password_confirmation' => 'password123',
    ]);

    $response->assertSessionHasErrors('email');
}

Next Steps

  1. Get your free API key, 200 validations/month included
  2. Check the API documentation for the complete response schema
  3. Try the free email checker to test addresses interactively

For the complete framework guide, see our Laravel integration documentation.

Email validation is one layer of securing your Laravel application. For a broader security audit covering headers, sessions, exposed debug tools, and dependency vulnerabilities, StackShield scans your production Laravel app from the outside and flags misconfigurations before attackers find them.

Frequently Asked Questions

Does Laravel have built-in disposable email detection?

No. Laravel's built-in email validation only checks format (RFC compliance) and optionally DNS records. It cannot detect disposable email providers, check domain reputation, or score risk. You need an external API like Fidro for that.

Should I validate emails synchronously or asynchronously in Laravel?

Use synchronous validation at signup to block disposable emails immediately. Use asynchronous validation (via Laravel queues) for bulk operations, re-checking existing users, or as a fallback when the API times out during synchronous checks.

How do I add Fidro to an existing Laravel project?

Three steps: add your API key to .env (FIDRO_API_KEY=your_key), create a service class that wraps the HTTP calls, and create a custom validation rule that uses the service. The entire integration takes under 30 minutes.

Will this work with Laravel Jetstream or Breeze?

Yes. Both Jetstream and Breeze use standard Laravel validation rules in their registration controllers. Add the custom Fidro validation rule to the existing rules array and it works immediately with no other changes needed.

How do I test email validation in my Laravel test suite?

Use Http::fake() to mock the Fidro API responses in your tests. Create factory methods that return disposable and non-disposable responses, then assert your application handles each case correctly. Never call the real API in automated tests.