Laravel Email Validation

Complete guide to email validation in Laravel

What You'll Learn

  • Integrate VerifyForge API in Laravel using HTTP Client
  • Create custom validation rules
  • Validate emails in Form Requests
  • Build a reusable validation service
  • Handle validation errors
  • Best practices for production

Prerequisites

  • Laravel 10+ installed
  • PHP 8+ installed
  • Basic knowledge of Laravel
  • VerifyForge API key (get free key)

Installation

No package installation needed - we'll use Laravel's built-in HTTP client.

Quick Start: Validate Single Email

<?php

use Illuminate\Support\Facades\Http;

$response = Http::withHeaders([
    'X-API-Key' => config('services.verifyforge.api_key')
])->get('https://verifyforge.com/api/validate', [
    'email' => 'user@example.com'
]);

$result = $response->json();

if ($result['data']['isValid']) {
    echo "✓ Email is valid!";
    echo "Reachability: " . $result['data']['reachability'];
} else {
    echo "✗ Email is invalid";
}

Tutorial 1: Create Validation Service

Step 1: Create VerifyForge Service

<?php
// app/Services/VerifyForgeService.php

namespace App\Services;

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

class VerifyForgeService
{
    protected string $apiKey;
    protected string $baseUrl;

    public function __construct()
    {
        $this->apiKey = config('services.verifyforge.api_key');
        $this->baseUrl = config('services.verifyforge.base_url', 'https://verifyforge.com');
    }

    /**
     * Validate a single email address
     */
    public function validate(string $email): array
    {
        try {
            $response = Http::withHeaders([
                'X-API-Key' => $this->apiKey
            ])->get("{$this->baseUrl}/api/validate", [
                'email' => $email
            ]);

            if ($response->failed()) {
                throw new Exception('Email validation failed');
            }

            return $response->json();
        } catch (Exception $e) {
            throw new Exception('Email validation service unavailable: ' . $e->getMessage());
        }
    }

    /**
     * Validate with caching
     */
    public function validateCached(string $email): array
    {
        $cacheKey = "email_validation:{$email}";

        return Cache::remember($cacheKey, 3600, function () use ($email) {
            return $this->validate($email);
        });
    }

    /**
     * Bulk validate emails
     */
    public function validateBulk(array $emails): array
    {
        try {
            $response = Http::withHeaders([
                'X-API-Key' => $this->apiKey,
                'Content-Type' => 'application/json'
            ])->post("{$this->baseUrl}/api/validate/bulk", [
                'emails' => $emails
            ]);

            if ($response->failed()) {
                throw new Exception('Bulk validation failed');
            }

            return $response->json();
        } catch (Exception $e) {
            throw new Exception('Bulk validation service unavailable: ' . $e->getMessage());
        }
    }

    /**
     * Check if email is valid
     */
    public function isValid(string $email): bool
    {
        $result = $this->validate($email);
        return $result['data']['isValid'] ?? false;
    }
}

Step 2: Add Configuration

<?php
// config/services.php

return [
    // ... other services

    'verifyforge' => [
        'api_key' => env('VERIFYFORGE_API_KEY'),
        'base_url' => env('VERIFYFORGE_BASE_URL', 'https://verifyforge.com'),
    ],
];
# .env
VERIFYFORGE_API_KEY=your_api_key_here

Step 3: Register Service Provider (Optional)

<?php
// app/Providers/AppServiceProvider.php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use App\Services\VerifyForgeService;

class AppServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        $this->app->singleton(VerifyForgeService::class, function ($app) {
            return new VerifyForgeService();
        });
    }
}

Tutorial 2: Custom Validation Rule

Step 1: Create Custom Rule

<?php
// app/Rules/ValidVerifyForgeEmail.php

namespace App\Rules;

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

class ValidVerifyForgeEmail implements ValidationRule
{
    protected bool $allowDisposable;
    protected bool $allowRisky;

    public function __construct(
        bool $allowDisposable = false,
        bool $allowRisky = false
    ) {
        $this->allowDisposable = $allowDisposable;
        $this->allowRisky = $allowRisky;
    }

    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        $service = app(VerifyForgeService::class);

        try {
            $result = $service->validateCached($value);
            $data = $result['data'];

            if (!$data['isValid']) {
                $fail('The :attribute is not a valid email address.');
                return;
            }

            if (!$this->allowDisposable && $data['disposable']) {
                $fail('Temporary email addresses are not allowed.');
                return;
            }

            if (!$this->allowRisky && $data['reachability'] === 'risky') {
                $fail('This email address appears risky. Please use another email.');
                return;
            }

            if ($data['reachability'] === 'invalid') {
                $fail('This email address cannot receive mail.');
                return;
            }

        } catch (\Exception $e) {
            // Log error but don't block validation
            logger()->error('VerifyForge validation error: ' . $e->getMessage());
        }
    }
}

Step 2: Use in Form Request

<?php
// app/Http/Requests/RegisterRequest.php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;
use App\Rules\ValidVerifyForgeEmail;

class RegisterRequest extends FormRequest
{
    public function authorize(): bool
    {
        return true;
    }

    public function rules(): array
    {
        return [
            'name' => ['required', 'string', 'max:255'],
            'email' => [
                'required',
                'email',
                'unique:users',
                new ValidVerifyForgeEmail(
                    allowDisposable: false,
                    allowRisky: false
                )
            ],
            'password' => ['required', 'string', 'min:8', 'confirmed'],
        ];
    }

    public function messages(): array
    {
        return [
            'email.required' => 'Please enter your email address',
            'email.email' => 'Please enter a valid email format',
            'email.unique' => 'This email is already registered',
        ];
    }
}

Step 3: Use in Controller

<?php
// app/Http/Controllers/AuthController.php

namespace App\Http\Controllers;

use App\Http\Requests\RegisterRequest;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;

class AuthController extends Controller
{
    public function register(RegisterRequest $request)
    {
        // Email is already validated by ValidVerifyForgeEmail rule
        $user = User::create([
            'name' => $request->name,
            'email' => $request->email,
            'password' => Hash::make($request->password),
        ]);

        auth()->login($user);

        return redirect()->route('dashboard')
            ->with('success', 'Registration successful!');
    }
}

Tutorial 3: API Controller for Email Validation

<?php
// app/Http/Controllers/Api/EmailValidationController.php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Services\VerifyForgeService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;

class EmailValidationController extends Controller
{
    public function __construct(
        protected VerifyForgeService $verifyForge
    ) {}

    /**
     * Validate single email
     */
    public function validate(Request $request): JsonResponse
    {
        $validator = Validator::make($request->all(), [
            'email' => 'required|email'
        ]);

        if ($validator->fails()) {
            return response()->json([
                'error' => 'Invalid email format'
            ], 400);
        }

        try {
            $result = $this->verifyForge->validate($request->email);

            return response()->json([
                'success' => true,
                'data' => [
                    'email' => $result['data']['email'],
                    'isValid' => $result['data']['isValid'],
                    'reachability' => $result['data']['reachability'],
                    'disposable' => $result['data']['disposable'],
                    'roleAccount' => $result['data']['roleAccount'],
                    'freeProvider' => $result['data']['freeProvider'],
                    'suggestion' => $result['data']['suggestion'] ?? null,
                ]
            ]);

        } catch (\Exception $e) {
            return response()->json([
                'error' => 'Validation service unavailable'
            ], 503);
        }
    }

    /**
     * Bulk validate emails
     */
    public function validateBulk(Request $request): JsonResponse
    {
        $validator = Validator::make($request->all(), [
            'emails' => 'required|array|min:1|max:100',
            'emails.*' => 'required|email'
        ]);

        if ($validator->fails()) {
            return response()->json([
                'error' => 'Invalid request',
                'details' => $validator->errors()
            ], 400);
        }

        try {
            $result = $this->verifyForge->validateBulk($request->emails);

            return response()->json([
                'success' => true,
                'data' => $result['data'],
                'summary' => $result['data']['summary'] ?? null,
            ]);

        } catch (\Exception $e) {
            return response()->json([
                'error' => 'Bulk validation service unavailable'
            ], 503);
        }
    }
}

Add Routes

<?php
// routes/api.php

use App\Http\Controllers\Api\EmailValidationController;

Route::post('/validate-email', [EmailValidationController::class, 'validate']);
Route::post('/validate-bulk', [EmailValidationController::class, 'validateBulk']);

Tutorial 4: Middleware for Email Validation

<?php
// app/Http/Middleware/ValidateEmailMiddleware.php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use App\Services\VerifyForgeService;
use Symfony\Component\HttpFoundation\Response;

class ValidateEmailMiddleware
{
    public function __construct(
        protected VerifyForgeService $verifyForge
    ) {}

    public function handle(Request $request, Closure $next): Response
    {
        $email = $request->input('email');

        if (!$email) {
            return response()->json([
                'error' => 'Email is required'
            ], 400);
        }

        try {
            $result = $this->verifyForge->validateCached($email);

            if (!$result['data']['isValid']) {
                return response()->json([
                    'error' => 'Invalid email address'
                ], 422);
            }

            // Add validation result to request
            $request->merge([
                'email_validation' => $result['data']
            ]);

        } catch (\Exception $e) {
            // Log but allow request to continue
            logger()->error('Email validation middleware error: ' . $e->getMessage());
        }

        return $next($request);
    }
}

Register in app/Http/Kernel.php:

protected $middlewareAliases = [
    // ... other middleware
    'validate.email' => \App\Http\Middleware\ValidateEmailMiddleware::class,
];

Use in routes:

Route::post('/subscribe', [NewsletterController::class, 'subscribe'])
    ->middleware('validate.email');

Tutorial 5: Artisan Command for Bulk Validation

<?php
// app/Console/Commands/ValidateSubscribers.php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use App\Models\Subscriber;
use App\Services\VerifyForgeService;

class ValidateSubscribers extends Command
{
    protected $signature = 'subscribers:validate {--batch-size=100}';
    protected $description = 'Validate all subscriber emails';

    public function handle(VerifyForgeService $verifyForge): int
    {
        $batchSize = (int) $this->option('batch-size');

        $subscribers = Subscriber::whereNull('validated_at')->get();
        $total = $subscribers->count();

        if ($total === 0) {
            $this->info('No subscribers to validate');
            return 0;
        }

        $this->info("Validating {$total} subscribers...");

        $bar = $this->output->createProgressBar($total);
        $bar->start();

        // Process in batches
        $subscribers->chunk($batchSize)->each(function ($chunk) use ($verifyForge, $bar) {
            $emails = $chunk->pluck('email')->toArray();

            try {
                $result = $verifyForge->validateBulk($emails);

                // Update each subscriber
                foreach ($result['data']['results'] as $validation) {
                    $subscriber = Subscriber::where('email', $validation['email'])->first();

                    if ($subscriber) {
                        $subscriber->update([
                            'is_valid' => $validation['isValid'],
                            'reachability' => $validation['reachable'],
                            'validated_at' => now(),
                        ]);
                    }

                    $bar->advance();
                }

            } catch (\Exception $e) {
                $this->error("Batch failed: {$e->getMessage()}");
            }
        });

        $bar->finish();
        $this->newLine();
        $this->info('Validation complete!');

        return 0;
    }
}

Run with:

php artisan subscribers:validate --batch-size=100

Best Practices for Production

1. Queue Validation for Background Processing

<?php
// app/Jobs/ValidateEmailJob.php

namespace App\Jobs;

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

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

    public function __construct(
        public string $email,
        public ?int $subscriberId = null
    ) {}

    public function handle(VerifyForgeService $verifyForge): void
    {
        try {
            $result = $verifyForge->validate($this->email);

            if ($this->subscriberId) {
                $subscriber = Subscriber::find($this->subscriberId);
                $subscriber?->update([
                    'is_valid' => $result['data']['isValid'],
                    'reachability' => $result['data']['reachability'],
                    'validated_at' => now(),
                ]);
            }

        } catch (\Exception $e) {
            logger()->error("Email validation job failed: {$e->getMessage()}");
        }
    }
}

Dispatch the job:

ValidateEmailJob::dispatch($email, $subscriber->id);

2. Rate Limiting

<?php
// app/Http/Controllers/NewsletterController.php

use Illuminate\Support\Facades\RateLimiter;

public function subscribe(Request $request)
{
    $key = 'subscribe:' . $request->ip();

    if (RateLimiter::tooManyAttempts($key, 5)) {
        $seconds = RateLimiter::availableIn($key);
        return response()->json([
            'error' => "Too many requests. Try again in {$seconds} seconds."
        ], 429);
    }

    RateLimiter::hit($key, 60); // 60 seconds

    // Proceed with subscription...
}

3. Graceful Error Handling

<?php
// app/Exceptions/Handler.php

use App\Services\VerifyForgeService;

public function register(): void
{
    $this->reportable(function (\Exception $e) {
        if (str_contains($e->getMessage(), 'VerifyForge')) {
            logger()->error('VerifyForge service error', [
                'message' => $e->getMessage(),
                'trace' => $e->getTraceAsString()
            ]);
        }
    });
}

Complete Example: Newsletter Subscription

<?php
// app/Http/Controllers/NewsletterController.php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Models\Subscriber;
use App\Services\VerifyForgeService;
use Illuminate\Support\Facades\RateLimiter;

class NewsletterController extends Controller
{
    public function __construct(
        protected VerifyForgeService $verifyForge
    ) {}

    public function subscribe(Request $request)
    {
        // Rate limiting
        $key = 'newsletter:' . $request->ip();
        if (RateLimiter::tooManyAttempts($key, 3)) {
            return back()->withErrors([
                'email' => 'Too many subscription attempts. Please try again later.'
            ]);
        }

        RateLimiter::hit($key, 60);

        // Validate input
        $request->validate([
            'email' => 'required|email'
        ]);

        try {
            // Validate with VerifyForge
            $result = $this->verifyForge->validate($request->email);

            if (!$result['data']['isValid']) {
                return back()->withErrors([
                    'email' => 'Please enter a valid email address'
                ])->withInput();
            }

            if ($result['data']['disposable']) {
                return back()->withErrors([
                    'email' => 'Temporary email addresses are not allowed'
                ])->withInput();
            }

            // Create or update subscriber
            $subscriber = Subscriber::updateOrCreate(
                ['email' => $request->email],
                [
                    'is_valid' => $result['data']['isValid'],
                    'reachability' => $result['data']['reachability'],
                    'subscribed_at' => now(),
                ]
            );

            return redirect()->route('newsletter.success')
                ->with('success', 'Successfully subscribed to our newsletter!');

        } catch (\Exception $e) {
            logger()->error('Newsletter subscription error: ' . $e->getMessage());

            return back()->withErrors([
                'email' => 'Subscription failed. Please try again.'
            ])->withInput();
        }
    }
}
<?php
// app/Models/Subscriber.php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Subscriber extends Model
{
    protected $fillable = [
        'email',
        'is_valid',
        'reachability',
        'subscribed_at',
        'validated_at',
    ];

    protected $casts = [
        'is_valid' => 'boolean',
        'subscribed_at' => 'datetime',
        'validated_at' => 'datetime',
    ];
}

Troubleshooting

Problem: API key not found

Solution: Ensure VERIFYFORGE_API_KEY is set in .env file

Problem: HTTP client timeout

Solution: Increase timeout in service:

Http::timeout(30)->withHeaders([...])->get(...)

Problem: Too many API calls

Solution: Implement caching as shown in validateCached() method

Next Steps