Beta privadaMailbeam está en beta privada: la API pública todavía no está abierta.Únete a la lista de espera

Mailbeam
Laravel + PHPIniciación15 minutosActualizado en enero de 2025

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 SignupRequest que usa la regla
  • Tests con PHPUnit

Requisitos previos


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_KEY en .env y definida en el servidor de producción
  • php artisan config:cache ejecutado tras añadirla a config/services.php
  • Driver de caché configurado (CACHE_DRIVER=redis recomendado en producción)
  • failOpen=true (por defecto) revisado: entiende el compromiso
  • Mensajes de error traducidos si tu aplicación es multilingüe

Siguientes pasos