Vérification d'e-mails dans Laravel
Ce tutoriel intègre Mailbeam à une application Laravel sous la forme d'une règle de validation maison. La règle s'insère dans le système de validation existant de Laravel : vous pouvez donc l'employer partout où vous écririez required ou email.
Ce que vous allez construire
- Une règle de validation
ValidMailbeamEmail(Laravel 10+) - Une couche de cache facultative, via la façade Cache de Laravel
- Une Form Request
SignupRequestqui utilise la règle - Des tests PHPUnit
Prérequis
- PHP 8.1+, Laravel 10+
- Composer
- Une clé d'API Mailbeam (inscription gratuite)
Étape 1 — Configurer la clé d'API
Ajoutez ceci à config/services.php :
'mailbeam' => [
'key' => env('MAILBEAM_KEY'),
'base_url' => 'https://api.mailbeam.dev',
],Et ceci à .env :
MAILBEAM_KEY=mb_live_xxxxxxxxxxxxxxxxxxxx
Étape 2 — Créer la règle de validation
php artisan make:rule ValidMailbeamEmail<?php
// app/Rules/ValidMailbeamEmail.php
namespace App\Rules;
use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
class ValidMailbeamEmail implements ValidationRule
{
public function __construct(
private int $minScore = 60,
private bool $failOpen = true
) {}
public function validate(string $attribute, mixed $value, Closure $fail): void
{
$email = strtolower(trim((string) $value));
$cacheKey = 'mailbeam_' . md5($email);
// On consulte d'abord le cache
$result = Cache::remember($cacheKey, now()->addHour(), function () use ($email) {
return $this->callMailbeam($email);
});
if ($result === null) {
// Erreur d'API — échec permissif (on ne rejette pas l'adresse)
return;
}
$valid = $result['valid'] ?? false;
$score = $result['score'] ?? 0;
$reason = $result['reason'] ?? null;
if (!$valid || $score < $this->minScore) {
$message = $this->getErrorMessage($reason);
$fail($message);
}
}
private function callMailbeam(string $email): ?array
{
try {
$response = Http::withToken(config('services.mailbeam.key'))
->timeout(5)
->post(config('services.mailbeam.base_url') . '/v1/verify', [
'email' => $email,
]);
if ($response->successful()) {
return $response->json();
}
Log::warning('Erreur de l\'API Mailbeam', [
'status' => $response->status(),
'body' => $response->body(),
]);
return $this->failOpen ? null : ['valid' => false, 'score' => 0, 'reason' => 'api_error'];
} catch (\Throwable $e) {
Log::error('Échec de la requête Mailbeam', ['error' => $e->getMessage()]);
return $this->failOpen ? null : ['valid' => false, 'score' => 0, 'reason' => 'api_error'];
}
}
private function getErrorMessage(?string $reason): string
{
return match ($reason) {
'disposable_domain' => 'Merci d\'utiliser une adresse e-mail permanente, pas une adresse temporaire.',
'no_mx_records' => 'Ce domaine ne semble pas accepter de courrier.',
'smtp_rejected' => 'Cette adresse e-mail ne semble pas exister.',
'role_address' => 'Merci d\'utiliser une adresse e-mail personnelle.',
default => 'Merci de fournir une adresse e-mail valide et joignable.',
};
}
}Étape 3 — Créer une Form Request
php artisan make:request SignupRequest<?php
// app/Http/Requests/SignupRequest.php
namespace App\Http\Requests;
use App\Rules\ValidMailbeamEmail;
use Illuminate\Foundation\Http\FormRequest;
class SignupRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'email' => ['required', 'email:rfc,dns', new ValidMailbeamEmail()],
'password' => ['required', 'string', 'min:8', 'confirmed'],
'password_confirmation' => ['required', 'string'],
];
}
public function messages(): array
{
return [
'email.email' => 'Merci de saisir une adresse e-mail au format valide.',
];
}
}Étape 4 — L'utiliser dans votre contrôleur
<?php
// app/Http/Controllers/Auth/SignupController.php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Http\Requests\SignupRequest;
use App\Models\User;
use Illuminate\Support\Facades\Hash;
class SignupController extends Controller
{
public function store(SignupRequest $request)
{
// $request->validated() ne s'exécute qu'une fois toutes les règles passées
$validated = $request->validated();
$user = User::create([
'email' => $validated['email'],
'password' => Hash::make($validated['password']),
]);
auth()->login($user);
return redirect('/dashboard');
}
}La route :
// routes/web.php
Route::post('/signup', [SignupController::class, 'store'])->name('signup');Tester
<?php
// tests/Feature/SignupTest.php
namespace Tests\Feature;
use Tests\TestCase;
use Illuminate\Support\Facades\Http;
use Illuminate\Foundation\Testing\RefreshDatabase;
class SignupTest extends TestCase
{
use RefreshDatabase;
public function test_valid_email_creates_account(): void
{
Http::fake([
'api.mailbeam.dev/*' => Http::response([
'valid' => true, 'score' => 94, 'reason' => null,
], 200),
]);
$response = $this->post('/signup', [
'email' => 'user@example.com',
'password' => 'secret1234',
'password_confirmation' => 'secret1234',
]);
$response->assertRedirect('/dashboard');
$this->assertDatabaseHas('users', ['email' => 'user@example.com']);
}
public function test_disposable_email_is_rejected(): void
{
Http::fake([
'api.mailbeam.dev/*' => Http::response([
'valid' => false, 'score' => 2, 'reason' => 'disposable_domain',
], 200),
]);
$response = $this->post('/signup', [
'email' => 'temp@mailinator.com',
'password' => 'secret1234',
'password_confirmation' => 'secret1234',
]);
$response->assertSessionHasErrors('email');
$this->assertDatabaseMissing('users', ['email' => 'temp@mailinator.com']);
}
public function test_api_failure_does_not_block_signup(): void
{
Http::fake([
'api.mailbeam.dev/*' => Http::response([], 500),
]);
// Avec failOpen=true (le défaut), un 500 de Mailbeam ne doit pas bloquer l'inscription
$response = $this->post('/signup', [
'email' => 'user@example.com',
'password' => 'secret1234',
'password_confirmation' => 'secret1234',
]);
$response->assertRedirect('/dashboard');
}
}Liste de contrôle avant la mise en production
-
MAILBEAM_KEYprésente dans.envet définie sur le serveur de production -
php artisan config:cacheexécuté après l'ajout dansconfig/services.php - Pilote de cache configuré (
CACHE_DRIVER=redisrecommandé en production) -
failOpen=true(le défaut) relu — le compromis est compris - Messages d'erreur traduits si votre application est multilingue