Verifica delle email in Laravel
Questo tutorial integra Mailbeam in un'applicazione Laravel come una regola di validazione tua. La regola si aggancia al sistema di validazione che Laravel ha già, quindi puoi usarla ovunque useresti required o email.
Che cosa costruirai
- Una regola di validazione
ValidMailbeamEmail(Laravel 10+) - Uno strato di cache facoltativo con la facade Cache di Laravel
- Una Form Request
SignupRequestche usa la regola - Test con PHPUnit
Prerequisiti
- PHP 8.1+, Laravel 10+
- Composer
- Una chiave API di Mailbeam (iscriviti gratis)
Passo 1 — Configura la chiave API
Aggiungi questo a config/services.php:
'mailbeam' => [
'key' => env('MAILBEAM_KEY'),
'base_url' => 'https://api.mailbeam.dev',
],E questo a .env:
MAILBEAM_KEY=mb_live_xxxxxxxxxxxxxxxxxxxx
Passo 2 — Crea la regola di validazione
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);
// Guarda prima nella cache
$result = Cache::remember($cacheKey, now()->addHour(), function () use ($email) {
return $this->callMailbeam($email);
});
if ($result === null) {
// Errore dell'API: lascia passare (non rifiutare l'email)
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('Errore dell\'API di Mailbeam', [
'status' => $response->status(),
'body' => $response->body(),
]);
return $this->failOpen ? null : ['valid' => false, 'score' => 0, 'reason' => 'api_error'];
} catch (\Throwable $e) {
Log::error('La richiesta a Mailbeam è fallita', ['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' => 'Usa un indirizzo email permanente, non uno temporaneo.',
'no_mx_records' => 'Questo dominio non sembra accettare posta.',
'smtp_rejected' => 'Questo indirizzo email non sembra esistere.',
'role_address' => 'Usa un indirizzo email personale.',
default => 'Inserisci un indirizzo email valido e raggiungibile.',
};
}
}Passo 3 — Crea una 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' => 'Inserisci un\'email in un formato valido.',
];
}
}Passo 4 — Usala nel tuo controller
<?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() viene eseguito solo quando tutte le regole passano
$validated = $request->validated();
$user = User::create([
'email' => $validated['email'],
'password' => Hash::make($validated['password']),
]);
auth()->login($user);
return redirect('/dashboard');
}
}Rotta:
// routes/web.php
Route::post('/signup', [SignupController::class, 'store'])->name('signup');Prove
<?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),
]);
// Con failOpen=true (predefinito), un 500 di Mailbeam non deve bloccare la registrazione
$response = $this->post('/signup', [
'email' => 'user@example.com',
'password' => 'secret1234',
'password_confirmation' => 'secret1234',
]);
$response->assertRedirect('/dashboard');
}
}Lista di controllo per la produzione
-
MAILBEAM_KEYin.enve definita sul server di produzione -
php artisan config:cacheeseguito dopo averla aggiunta aconfig/services.php - Driver di cache configurato (
CACHE_DRIVER=redisconsigliato in produzione) -
failOpen=true(predefinito) rivisto: capisci il compromesso - Messaggi di errore tradotti se la tua applicazione è multilingue