Índice de contenido
- Clase Mailable
- Enviar correos de forma individual
- Parámetros: to, cc y bcc
- CC y BCC/CCO
- Enviar correos en masa
- Enviar mensajes de verificación de email del usuario en Laravel
- Mandar mensajes de confirmación de manera programática
- Extra: REST API para registrar y verificar usuarios
- TRUCAZO para enviar correos automatizados y masivos en Laravel con Livewire
- Implementación
- 1. Manejo del contador (subPage)
- 2. La función de envío (sendEmailSubscriptions)
- 3. El secreto: wire:poll y el modificador .keep-alive
- Optimización y concurrencia
- Alternativa con JavaScript
Una vez que conoces cómo limitar la cantidad de peticiones del usuario, resulta natural dar el siguiente paso: aprender a enviar correos electrónicos para notificar a esos mismos usuarios. En Laravel, configurar el envío de emails es sorprendentemente sencillo. Si ya dispones de un hosting con un servidor de correo, lo único que necesitas es crear una dirección con su contraseña correspondiente.
A partir de ahí, solo debes ajustar algunos parámetros en tu proyecto. Si no los conoces porque el servidor de correos lo provee un tercero, tendrás que consultarlos con tu proveedor. En el caso de Hostinger, la configuración en config/mail.php quedaría así:
'smtp' => [
'transport' => 'smtp',
'host' => env('MAIL_HOST', 'smtp.hostinger.com'),
'port' => env('MAIL_PORT', 465),
'encryption' => env('MAIL_ENCRYPTION', 'ssl'),
'username' => env('MAIL_USERNAME','<EMAIL>'),
'password' => env('MAIL_PASSWORD',"<PASSWORD>"),
'timeout' => null,
'auth_mode' => null,
],Si no tienes acceso a un servidor de correos real durante el desarrollo, te recomiendo utilizar un servicio de pruebas como Mailtrap. Es completamente gratuito: solo debes crearte una cuenta, crear un inbox y copiar las credenciales en tu proyecto. Así puedes probar el envío de correos sin riesgo de enviar emails reales a nadie.
La configuración en tu archivo .env con Mailtrap sería la siguiente:
MAIL_MAILER=smtp
MAIL_HOST=sandbox.smtp.mailtrap.io
MAIL_PORT=2525
MAIL_USERNAME=ec5cbede982042
MAIL_PASSWORD=********0be7Clase Mailable
Para enviar cualquier correo en Laravel, debes crear una clase Mailable. Al igual que ocurre con los modelos, controladores o form requests, esta clase sigue una estructura específica que el framework entiende y gestiona. Puedes generarla automáticamente con el siguiente comando Artisan:
$ php artisan make:mail OrderShippedEl archivo resultante, app/Mail/OrderShipped.php, tendrá esta estructura base:
<?php
namespace App\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
class OrderShipped extends Mailable
{
public $email;
public $title;
public $content;
use Queueable, SerializesModels;
public function __construct($email, $title, $content)
{
$this->email = $email;
$this->title = $title;
$this->content = $content;
}
/**
* Get the message envelope.
*/
public function envelope(): Envelope
{
return new Envelope(
subject: 'Order Shipped',
);
}
/**
* Get the message content definition.
*/
public function content(): Content
{
return new Content(
view: 'emails.subscribe',
);
}
/**
* Get the attachments for the message.
*
* @return array<int, \Illuminate\Mail\Mailables\Attachment>
*/
public function attachments(): array
{
return [];
}
}Cada método cumple una función concreta. El método envelope() define el asunto del correo; el método content() especifica la vista Blade que se usará como cuerpo del mensaje; y el método attachments() permite adjuntar archivos. El constructor, como de costumbre en PHP, se encarga de inicializar las propiedades de la clase al crear el objeto —en este caso, el email del destinatario, el título y el contenido.
Las propiedades públicas que defines en la clase ($email, $title, $content) estarán disponibles automáticamente en la vista Blade correspondiente, sin necesidad de pasarlas explícitamente. Puedes añadir o modificar estas propiedades según las necesidades de tu proyecto.
También existe un enfoque alternativo más compacto, mediante el método build(), que permite definir el asunto y la vista en un solo lugar. Este patrón era muy común antes de Laravel 9 y aún lo encontrarás en muchos proyectos:
app/Mail/SubscribeEmail.php
<?php
namespace App\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
class SubscribeEmail extends Mailable
{
use Queueable, SerializesModels;
public $email;
public $title;
public $content;
public function __construct($email, $title, $content)
{
$this->email = $email;
$this->title = $title;
$this->content = $content;
}
public function build()
{
return $this->subject($this->title)->view('subscribe');
}
}Desde la clase puedes personalizar todos los argumentos que recibirá el correo —asunto, destinatario, contenido— y retornar una vista Blade que representa el cuerpo del email. Por ejemplo, una vista mínima en resources/views/emails/subscribe.blade.php podría ser:
<p>Hi<br>
{!! $content !!}El operador {!! !!} renderiza HTML sin escapar, lo que es útil cuando el contenido del correo proviene de un editor enriquecido como CKEditor. Úsalo con precaución y asegúrate siempre de que el origen del contenido sea de confianza para evitar inyecciones XSS.
Enviar correos de forma individual
Para enviar un correo, utilizas el facade Mail de Laravel, que expone una API fluida y muy legible. Simplemente instancias tu clase Mailable y la pasas al método send():
Mail::to('no-reply@example.net.com')->send(new SubscribeEmail('contact@gmail.com', $title, $content));Parámetros: to, cc y bcc
No estás limitado a un único destinatario. Laravel te permite encadenar los métodos to(), cc() y bcc() para configurar con precisión a quién llega el correo:
Mail::to($request->user())
->cc($moreUsers)
->bcc($evenMoreUsers)
->send(new SubscribeEmail('contact@gmail.com', $title, $content));CC y BCC/CCO
El cc (carbon copy o copia de carbono) permite enviar una copia del correo a otras personas, y todos los destinatarios pueden ver quiénes más lo recibieron. El bcc (blind carbon copy o copia oculta) funciona igual, pero la lista de destinatarios permanece invisible para el resto: nadie sabe a quién más se le envió el mensaje. Esta distinción es clave al hacer envíos masivos.
Enviar correos en masa
Una forma habitual de enviar correos a múltiples destinatarios de forma simultánea es pasar un array de emails al método cc(). Sin embargo, este enfoque tiene un problema importante: expone todas las direcciones a todos los destinatarios, lo que genera desconfianza y puede violar la privacidad de tus usuarios:
Mail::to('no-reply@example.net.com')
->cc(['hideemail1@gmail.com','hideemail2@gmail.com','hideemail3@gmail.com'])->send(new SubscribeEmail('contact@gmail.com', $title, $content));La solución es sencilla: usa bcc() en lugar de cc(). Así los destinatarios quedan ocultos y nadie puede ver las demás direcciones de la lista:
Mail::to('no-reply@example.net.com')
->bcc(['hideemail1@gmail.com','hideemail2@gmail.com','hideemail3@gmail.com'])->send(new SubscribeEmail('contact@gmail.com', $title, $content));Como se aprecia en la imagen, los emails definidos en bcc no aparecen visibles en la cabecera del mensaje:
En vez de aparecer como en el ejemplo con cc, los destinatarios simplemente no se muestran. Una buena práctica para cualquier envío masivo.
Enviar mensajes de verificación de email del usuario en Laravel
Los correos de confirmación son un pilar fundamental en cualquier sistema web con registro de usuarios: se envían tras completar una acción para confirmar que la operación fue exitosa (cambio de contraseña, compra de un producto, alta en una suscripción, etc.). Laravel hace que esto sea muy fácil de implementar de forma nativa.
Para habilitar la verificación de email, lo primero es implementar el contrato MustVerifyEmail en tu modelo de usuario:
use Illuminate\Contracts\Auth\MustVerifyEmail;
***
class User extends Authenticatable implements MustVerifyEmailCon eso en su lugar, ya tienes acceso a los mecanismos de verificación integrados en el framework.
Mandar mensajes de confirmación de manera programática
Desde cualquier instancia de un usuario autenticado, puedes disparar el envío del email de verificación con un solo método:
$user->sendEmailVerificationNotification();Por supuesto, para que esto funcione debes tener correctamente configurado tu servidor SMTP en el archivo .env. Al ejecutar este método, el usuario recibirá un correo similar al siguiente:

Extra: REST API para registrar y verificar usuarios
Aquí te dejo un caso de uso real y muy común: una REST API para registrar y verificar usuarios. Tenemos dos funciones diferenciadas: una para el registro, que al finalizar dispara automáticamente el email de verificación; y otra para reenviar la verificación de forma programática:
class UserController extends Controller
{
/**
* Register
*/
public function register(Request $request)
{
$validator = Validator::make($request->all(), StoreUser::myRules());
if ($validator->fails())
return $this->errorResponse($validator->errors(), 422);
try {
$user = new User();
$user->name = $request->name;
$user->email = $request->email;
$user->password = Hash::make($request->password);
$user->save();
if ($request->subscribed) {
Subscribe::create(['email' => $request->email]);
}
$success = true;
$message = 'User register successfully';
} catch (\Illuminate\Database\QueryException $ex) {
$success = false;
$message = $ex->getMessage();
}
// response
$response = [
'success' => $success,
'message' => $message,
];
$credentials = [
'email' => $request->email,
'password' => $request->password,
];
Auth::attempt($credentials);
$user->sendEmailVerificationNotification();
return $this->successResponse($response);
}
public function verifie()
{
$user = Auth::user() ?? auth('sanctum')->user();
$userModel = User::find($user->id);
$userModel->sendEmailVerificationNotification();
return $this->successResponse("ok");
}Como ves, en la función register() obtenemos los datos del usuario, lo creamos, iniciamos sesión y llamamos a sendEmailVerificationNotification() para disparar el correo de verificación automáticamente. Fíjate que la llamada a Auth::attempt() antes del envío es importante: algunos mecanismos de verificación requieren que el usuario esté autenticado para generar el enlace firmado correctamente.
La función verifie() es un endpoint independiente que puedes llamar de forma programática cuando necesites reenviar el correo de verificación sin pasar por el registro de nuevo. En este ejemplo usamos Laravel Sanctum con tokens, pero el mismo enfoque funciona con cualquier guard de autenticación.
TRUCAZO para enviar correos automatizados y masivos en Laravel con Livewire
Te voy a mostrar cómo enviar correos electrónicos de forma automatizada cada 7, 5 o 4 segundos —o el intervalo que tú determines— utilizando Laravel Livewire. Cada vez que el contador avanza, se envía un email a un suscriptor distinto. Es un sistema simple pero sorprendentemente eficiente.
Yo mismo lo he utilizado para enviar alrededor de 1,000 correos en menos de dos horas. Si tienes una base de datos de, por ejemplo, 9,000 usuarios, puedes calcular el tiempo aproximado o incluso paralelizar el proceso abriendo varias pestañas del navegador al mismo tiempo. Si no usas Livewire, no te preocupes: al final del artículo te explico cómo replicar este mismo esquema con un poco de JavaScript.
El panel que utilizo para gestionar los envíos redacta el contenido del email en HTML gracias al plugin CKEditor. Si te interesa integrarlo con Laravel, tengo varios vídeos y artículos sobre ello en el blog.
Implementación
El núcleo de esta solución es un componente de Livewire que aprovecha su naturaleza reactiva. Te explico los puntos clave:
1. Manejo del contador (subPage)
Utilizo una propiedad llamada $subPage que comienza en cero. Esta propiedad es fundamental para la resiliencia del proceso: si ocurre un error (se cae internet o el servidor falla), puedo ver en qué bloque quedó el último envío y retomar desde ese punto exacto. Esto me permite, por ejemplo, enviar bloques de 300 correos, dejar descansar el servidor unos minutos y continuar después sin perder el hilo.
<?php
namespace App\Livewire\Dashboard\Blog;
use App\Mail\SubscribeEmail;
use App\Models\Subscribe;
use App\Models\SubscriptionContent;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Session;
use Livewire\Component;
use Livewire\WithFileUploads;
use Illuminate\Support\Facades\Config;
class SubscribeSendEmail extends Component
{
***
public $subPage = 0; // sub actual
***
// envia el correo a cada sub
function sendEmailSubscriptions()
{
// ENVIAR A TODOS
$subscribe = Subscribe::select('name', 'email', 'id')
->where('active', $this->typeSubscriptors) // Grupo de Subs seleccionado
->offset($this->subPage) // pagina que va variando en el poll
->limit(1) // solo uno
->orderBy('id')->first();
}
}2. La función de envío (sendEmailSubscriptions)
La lógica de consulta es muy deliberada: busco un único suscriptor usando limit(1)->first() con un offset() que va incrementando en cada ciclo. Mientras haya registros, el proceso continúa; en cuanto la consulta devuelve null, se detiene automáticamente:
class SubscribeSendEmail extends Component
{
***
// envia el correo a cada sub
function sendEmailSubscriptions()
{
// ENVIAR A TODOS
$subscribe = Subscribe::select('name', 'email', 'id')
->where('active', $this->typeSubscriptors) // Grupo de Subs seleccionado
->offset($this->subPage) // pagina que va variando en el poll
->limit(1) // solo uno
->orderBy('id')->first();
***
}3. El secreto: wire:poll y el modificador .keep-alive
La verdadera "magia" de este esquema es la directiva wire:poll de Livewire. Esta instrucción indica al componente cada cuánto tiempo debe refrescarse automáticamente, llamando a una función del servidor de forma periódica sin que el usuario tenga que hacer nada.
- Intervalo: Yo uso 7 segundos (
7000ms). He probado con 3 o 4 segundos y funciona bien, pero 7 segundos es un margen seguro para no saturar servicios de terceros como Hostinger o Gmail y evitar errores de tiempo de espera. - Modo keep-alive: El modificador
.keep-alive(antes llamado.alwaysen versiones anteriores de Livewire) garantiza que el proceso continúe aunque cambies de pestaña en el navegador. Sin él, el poll se detendría al perder el foco.
El blade del componente con el poll activo:
resources/views/livewire/dashboard/subscribe-send-email.blade.php
@if ($active)
<div wire:poll.7000ms.keep-alive="startSendEmailOne">
<h3>Valor Actual: {{ $subPage }}</h3>
<p class="m-0">Este valor se actualiza automáticamente cada 7 segundos.</p>
</div>
<label for="">SubPage</label>
@endifEl código completo del componente Livewire para el envío automatizado:
app/Livewire/Dashboard/Blog/SubscribeSendEmail.php
<?php
namespace App\Livewire\Dashboard\Blog;
use App\Mail\SubscribeEmail;
use App\Models\Subscribe;
use App\Models\SubscriptionContent;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Session;
use Livewire\Component;
use Livewire\WithFileUploads;
use Illuminate\Support\Facades\Config;
class SubscribeSendEmail extends Component
{
// objeto
public SubscriptionContent $subscriptionContent;
// info
public $subscribes = [];
public $countSubscribes = 0;
// para enviar el correo, se activa el poll y si esta activo
public $subPage = 0; // sub actual
public $active = false; // enviando correos
***
// para pasar al siguiente sub a enviar el correo
public function startSendEmailOne()
{
$this->submit();
$this->subPage++;
}
// esta funcion inicia el proceso de enviar el correo individual
public function submit()
{
// activa el poll
$this->active = true;
// envia el primer correo
$this->sendEmailSubscriptions();
}
// envia el correo a cada sub
function sendEmailSubscriptions()
{
// ENVIAR A TODOS
$subscribe = Subscribe::select('name', 'email', 'id')
->where('active', $this->typeSubscriptors) // Grupo de Subs seleccionado
->offset($this->subPage) // pagina que va variando en el poll
->limit(1) // solo uno
->orderBy('id')->first();
// si no hay sub termina
if (!$subscribe) {
$this->active = false;
return;
}
// *** Testing
Log::info("$this->subPage - $subscribe->id $subscribe->email");
// correo electronico de la app
$smtpUser = config('mail')['mailers']['smtp']['username'];
// prepara el mail para enviar el correo
$mailer = Mail::build([
'transport' => 'smtp',
//'transport' => 'log', // *** PARA TESTING EN LOCAL
'host' => 'smtp.domain.com',
'port' => 465,
'encryption' => 'ssl',
'username' => $smtpUser,
'password' => config('mail')['mailers']['smtp']['password'],
]);
// Envio el correo
$mailer
->to($subscribe->email)
->send(
(new SubscribeEmail($subscribe->email, $subscribe->name ?? $subscribe->email, $this->subscriptionContent->title, $this->subscriptionContent->content))
->from($smtpUser, 'Andrés Cruz')
);
}
}Y el blade completo del componente:
resources/views/livewire/dashboard/subscribe-send-email.blade.php
@if ($active)
<div wire:poll.7000ms.keep-alive="startSendEmailOne">
<h3>Valor Actual: {{ $subPage }}</h3>
<p class="m-0">Este valor se actualiza automáticamente cada 7 segundos.</p>
</div>
<label for="">SubPage</label>
@endif
<x-button class="mt-2 mb-3" wire:click="startSendEmailOne">
{{ __('Start Send Email') }}
</x-button>Optimización y concurrencia
Si necesitas acelerar el proceso, puedes abrir varias instancias del panel en pestañas distintas y asignar a cada una un rango diferente de suscriptores mediante $subPage. Por ejemplo:
- Pestaña 1: Envía desde el suscriptor 0 al 4,500.
- Pestaña 2: Envía desde el suscriptor 4,501 al 9,000.
Esto permite el envío concurrente y reduce el tiempo total a la mitad. Un apunte importante: el intervalo de 7 segundos no es arbitrario. Si reduces demasiado el intervalo —por ejemplo, a medio segundo— y el servidor de correo tarda 2 segundos en responder, irás acumulando peticiones más rápido de lo que se procesan y terminarás saturando la cola, lo que provocará errores 500 y pérdida de envíos.
Alternativa con JavaScript
Si prefieres no depender de Livewire, puedes replicar exactamente este comportamiento con setInterval() o setTimeout() en JavaScript. La idea es la misma: cada X segundos realizas una petición fetch() o AJAX a un endpoint de Laravel que procesa y envía el siguiente correo en la lista. Es ligeramente más verboso, pero igual de eficaz.
El siguiente paso es que conozcas cómo puedes extender Laravel con funcionalidades clave como detectar si la navegación es mobile o por PC.