
Mail tracking en Laravel: rastreo de correos enviador
¿Qué es el mail tracking?
Es el conjunto de técnicas para inferir si un correo fue abierto o interactuado. Lo más común:
Tracking de apertura
Inserta una imagen 1×1 (pixel transparente) cuyo src apunta a tu servidor con un token único. Cuando el cliente de correo descarga esa imagen, registras una “apertura”.Tracking de clics
Reescribes los enlaces del correo para que primero pasen por tu dominio (registras el clic) y luego redireccionas al destino final.
Importante: el tracking no es exacto; es una aproximación.
¿Cómo funciona técnicamente?
Pixel 1×1:
<img src="https://tu-dominio.com/m/open/{token}.png" width="1" height="1">
Tu backend busca el
, marcatoken
y devuelve un PNG de 1×1 con headers no-cache.opened_at
Enlaces con tracking:
<a href="https://tu-dominio.com/m/click/{token}?u=https://destino.com">
Tu backend graba el clic y hace un 302 a
.u
Ejemplo mínimo en Laravel 12
1) Migración para aperturas y clics
// database/migrations/2025_09_27_000000_create_mail_tracking_tables.php use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; return new class extends Migration { public function up(): void { Schema::create('mail_opens', function (Blueprint $table) { $table->id(); $table->uuid('token')->unique(); $table->string('recipient_email'); $table->string('subject')->nullable(); $table->timestamp('sent_at')->nullable(); $table->timestamp('opened_at')->nullable(); $table->string('ip')->nullable(); $table->text('user_agent')->nullable(); $table->timestamps(); $table->index(['recipient_email', 'sent_at']); }); Schema::create('mail_clicks', function (Blueprint $table) { $table->id(); $table->uuid('token')->index(); // mismo token que el envío $table->string('url'); // destino final $table->string('ip')->nullable(); $table->text('user_agent')->nullable(); $table->timestamps(); }); } public function down(): void { Schema::dropIfExists('mail_clicks'); Schema::dropIfExists('mail_opens'); } };
2) Modelos simples
// app/Models/MailOpen.php namespace App\Models; use Illuminate\Database\Eloquent\Model; class MailOpen extends Model { protected $fillable = ['token','recipient_email','subject','sent_at','opened_at','ip','user_agent']; protected $casts = ['sent_at'=>'datetime','opened_at'=>'datetime']; } // app/Models/MailClick.php namespace App\Models; use Illuminate\Database\Eloquent\Model; class MailClick extends Model { protected $fillable = ['token','url','ip','user_agent']; }
3) Controlador para pixel y clics
// app/Http/Controllers/MailTrackingController.php namespace App\Http\Controllers; use App\Models\MailClick; use App\Models\MailOpen; use Illuminate\Http\Request; use Illuminate\Support\Facades\Response; class MailTrackingController extends Controller { public function open(Request $request, string $token) { $open = MailOpen::where('token', $token)->first(); if ($open && is_null($open->opened_at)) { $open->update([ 'opened_at' => now(), 'ip' => $request->ip(), 'user_agent' => (string)$request->userAgent(), ]); } // PNG 1×1 base64 (transparente) $png = base64_decode('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMByc8p1t0AAAAASUVORK5CYII='); return Response::make($png, 200, [ 'Content-Type' => 'image/png', 'Cache-Control' => 'no-cache, no-store, must-revalidate, max-age=0', 'Pragma' => 'no-cache', 'Expires' => '0', ]); } public function click(Request $request, string $token) { $url = $request->query('u'); if (!$url) { abort(400, 'Missing destination'); } MailClick::create([ 'token' => $token, 'url' => $url, 'ip' => $request->ip(), 'user_agent' => (string)$request->userAgent(), ]); return redirect()->away($url, 302); } }
4) Rutas públicas (sin auth)
// routes/web.php use App\Http\Controllers\MailTrackingController; Route::get('/m/open/{token}.png', [MailTrackingController::class, 'open'])->name('mail.open'); Route::get('/m/click/{token}', [MailTrackingController::class, 'click'])->name('mail.click');
5) Inyectar el pixel y reescribir enlaces en tu Mailable
// app/Mail/ExampleCampaign.php namespace App\Mail; use App\Models\MailOpen; use Illuminate\Bus\Queueable; use Illuminate\Mail\Mailable; use Illuminate\Queue\SerializesModels; use Illuminate\Support\Str; class ExampleCampaign extends Mailable { use Queueable, SerializesModels; public string $pixelUrl; public function __construct(public string $toEmail) { $open = MailOpen::create([ 'token' => (string) Str::uuid(), 'recipient_email' => $toEmail, 'subject' => 'Novedades de la semana', 'sent_at' => now(), ]); $this->pixelUrl = route('mail.open', ['token' => $open->token]); } public function build() { // Link con tracking (ejemplo) $promo = 'https://tu-sitio.com/promo'; $tracked = route('mail.click', ['token' => request()->route()?->parameter('token') ?? '']) . '?u=' . urlencode($promo); // si prefieres, pasa el token a la vista y construye ahí los enlaces return $this->to($this->toEmail) ->subject('Novedades de la semana') ->view('emails.example', [ 'pixelUrl' => $this->pixelUrl, 'promoUrl' => $tracked, ]); } }
{{-- resources/views/emails/example.blade.php --}} <!doctype html> <html lang="es"><head><meta charset="utf-8"></head> <body style="margin:0;padding:0;font-family:system-ui,Arial,sans-serif;"> <h1 style="margin:0 0 16px;">Tus novedades</h1> <p>Conoce la promo aquí: <a href="{{ $promoUrl }}">Ver promoción</a></p> {{-- Pixel 1×1 al final del body --}} <img src="{{ $pixelUrl }}" width="1" height="1" alt="" style="display:block;border:0;outline:0;" /> </body></html>
Sugerencia: si generas muchos enlaces, crea un helper que envuelva
y recibaroute('mail.click')
+$token
.destino
Métricas y lectura
Tasa de apertura = abiertos / enviados
CTR (Click-Through Rate) = clics / enviados
CTOR (Click-To-Open Rate) = clics / abiertos
En la práctica, el CTR/CTOR suele ser más fiable que la apertura, porque el clic implica intención humana.
Limitaciones (lo que nadie te dice)
Apple Mail Privacy Protection (MPP)
En dispositivos Apple, MPP precarga imágenes a través de un proxy; verás aperturas “infladas” y IPs/UA genéricos.Bloqueo de imágenes
Algunos clientes no cargan imágenes por defecto; tendrás aperturas subestimadas.Proxies y antivirus
Gateways corporativos o antivirus pueden “tocar” los enlaces y disparar falsos clics.Texto plano
En emails solo-texto no hay pixel; solo podrás medir clics si reescribes URLs.Privacidad y cumplimiento
Informa en tu Política de privacidad el uso de píxeles y tracking de enlaces; respeta las normativas locales (consentimiento, opt-out).
Conclusión: usa aperturas como indicador débil y da más peso a clics y conversiones.