Verificación de email en Laravel
Este tutorial integra Mailbeam en una aplicación Laravel como una regla de validación propia. La regla se engancha al sistema de validación que ya tiene Laravel, así que puedes usarla en cualquier sitio donde usarías required o email.
Qué vas a construir
- Una regla de validación propia
ValidMailbeamEmail(Laravel 10+) - Una capa de caché opcional con la fachada Cache de Laravel
- Un Form Request
SignupRequestque usa la regla - Tests con PHPUnit
Requisitos previos
- PHP 8.1+, Laravel 10+
- Composer
- Una clave de API de Mailbeam (regístrate gratis)
Paso 1 — Configura la clave de API
Añade esto a config/services.php:
'mailbeam' => [
'key' => env('MAILBEAM_KEY'),
'base_url' => 'https://api.mailbeam.dev',
],Y esto a .env:
MAILBEAM_KEY=mb_live_xxxxxxxxxxxxxxxxxxxx
Paso 2 — Crea la regla de validación
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);
// Mira primero en la caché
$result = Cache::remember($cacheKey, now()->addHour(), function () use ($email) {
return $this->callMailbeam($email);
});
if ($result === null) {
// Error de la API: falla en abierto (no rechaces el 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('Error de la API de Mailbeam', [
'status' => $response->status(),
'body' => $response->body(),
]);
return $this->failOpen ? null : ['valid' => false, 'score' => 0, 'reason' => 'api_error'];
} catch (\Throwable $e) {
Log::error('La petición a Mailbeam ha fallado', ['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 una dirección de email permanente, no una temporal.',
'no_mx_records' => 'Este dominio no parece aceptar correo.',
'smtp_rejected' => 'Esta dirección de email no parece existir.',
'role_address' => 'Usa una dirección de email personal.',
default => 'Escribe una dirección de email válida y alcanzable.',
};
}
}Paso 3 — Crea un 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' => 'Escribe un email con un formato válido.',
];
}
}Paso 4 — Úsalo en tu controlador
<?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() solo se ejecuta cuando pasan todas las reglas
$validated = $request->validated();
$user = User::create([
'email' => $validated['email'],
'password' => Hash::make($validated['password']),
]);
auth()->login($user);
return redirect('/dashboard');
}
}Ruta:
// routes/web.php
Route::post('/signup', [SignupController::class, 'store'])->name('signup');Pruebas
<?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 (por defecto), un 500 de Mailbeam no debe bloquear el registro
$response = $this->post('/signup', [
'email' => 'user@example.com',
'password' => 'secret1234',
'password_confirmation' => 'secret1234',
]);
$response->assertRedirect('/dashboard');
}
}Checklist de producción
-
MAILBEAM_KEYen.envy definida en el servidor de producción -
php artisan config:cacheejecutado tras añadirla aconfig/services.php - Driver de caché configurado (
CACHE_DRIVER=redisrecomendado en producción) -
failOpen=true(por defecto) revisado: entiende el compromiso - Mensajes de error traducidos si tu aplicación es multilingüe