How to send emails in Laravel: SMTP, Mailable, and bulk BCC

- Andrés Cruz - ES En español

Video thumbnail

limiting the number of client requests, it is natural to take the next step: learning how to send emails to notify those very same users. In Laravel, configuring email sending is surprisingly simple. If you already have hosting with a mail server, all you need is to create an address with its corresponding password.

From there, you just need to adjust a few parameters in your project. If you don't know them because the mail server is provided by a third party, you will have to consult them with your provider. In the case of Hostinger, the configuration in config/mail.php would look like this:

'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,
],

If you do not have access to a real mail server during development, I recommend using a testing service like Mailtrap. It is completely free: you just need to create an account, create an inbox, and copy the credentials into your project. This way you can test sending emails without the risk of real emails reaching anyone.

The configuration in your .env file with Mailtrap would be as follows:

MAIL_MAILER=smtp
MAIL_HOST=sandbox.smtp.mailtrap.io
MAIL_PORT=2525
MAIL_USERNAME=ec5cbede982042
MAIL_PASSWORD=********0be7

Mailable Class

To send any email in Laravel, you must create a Mailable class. Just like models, controllers, or form requests, this class follows a specific structure that the framework understands and manages. You can generate it automatically with the following Artisan command:

$ php artisan make:mail OrderShipped

The resulting file, app/Mail/OrderShipped.php, will have this base structure:

<?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 [];
   }
}

Each method serves a specific function. The envelope() method defines the email subject; the content() method specifies the Blade view that will be used as the body of the message; and the attachments() method allows attaching files. The constructor, as usual in PHP, is responsible for initializing the class properties when creating the object —in this case, the recipient's email, the title, and the message content.

The public properties you define in the class ($email, $title, $content) will automatically be available in the corresponding Blade view, without needing to pass them explicitly as additional data. You can add or modify these properties according to your project's needs.

There is also a more compact alternative approach: the build() method, which allows defining the subject and the view in a single place by chaining calls. This pattern was very common before Laravel 9 and you will still find it in legacy projects or older tutorials:

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');
   }
}

From the class you can customize all the arguments that the email will receive —subject, recipient, content— and return a Blade view representing the body of the email. For example, a minimal view in resources/views/emails/subscribe.blade.php could be:

<p>Hi<br>
{!! $content !!}

The {!! !!} operator renders HTML without escaping, which is useful when the email content comes from a rich text editor like CKEditor. Use it with caution and always ensure that the content source is trusted to prevent XSS injections. If the content is generated by the end user themselves, sanitize the HTML before storing it.

Sending individual emails

To send an email, you use Laravel's Mail facade, which exposes a fluent and highly readable API. Simply instantiate your Mailable class and pass it to the send() method:

Mail::to('no-reply@example.net.com')->send(new SubscribeEmail('contact@gmail.com', $title, $content));

Parameters: to, cc, and bcc

You are not limited to a single recipient. Laravel allows you to chain the to(), cc(), and bcc() methods to precisely configure who receives the email:

Mail::to($request->user())
   ->cc($moreUsers)
   ->bcc($evenMoreUsers)
   ->send(new SubscribeEmail('contact@gmail.com', $title, $content));

CC and BCC/CCO

cc (carbon copy) allows sending a copy of the email to other people, and all recipients can see who else received it. bcc (blind carbon copy) works in a similar way, but the list of recipients remains invisible to the rest: no one knows who else the message was sent to. This distinction is key when conducting bulk mailings, as it protects the privacy of your contact list.

Sending bulk emails

A common way to send emails to multiple recipients simultaneously is to pass an array of emails to the cc() method. However, this approach has a major issue: it exposes all addresses to all recipients, which breeds mistrust and can violate your users' privacy:

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));

The solution is simple: use bcc() instead of cc(). This way the recipients are hidden and no one can see the other addresses on the list:

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));

As seen in the image, the emails defined in bcc do not appear visible in the message header:

 

Email headers

 

In contrast to the example using cc, when using bcc the recipients simply do not appear listed in the email header. An essential best practice for any bulk mailing.

Sending user email verification messages in Laravel

Confirmation emails are a fundamental pillar in any web system with user registration: they are sent after completing an action to confirm that the operation was successful —password change, product purchase, subscription signup, etc.—. Laravel greatly facilitates this implementation natively, without the need for external libraries.

To enable email verification, the first step is to implement the MustVerifyEmail contract in your user model:

use Illuminate\Contracts\Auth\MustVerifyEmail;
***
class User extends Authenticatable implements MustVerifyEmail

With that in place, you already have access to all the verification mechanisms built into the framework, including the necessary routes and notifications.

Programmatically sending confirmation messages

From any authenticated user instance, you can trigger the sending of the verification email with a single method:

$user->sendEmailVerificationNotification();

For this to work, you must have your SMTP server correctly configured in the .env file. Upon executing this method, the user will receive an email similar to the following:

Confirmation email
Confirm email

Extra: REST API to register and verify users

Here I leave you a real and very common use case: a REST API to register and verify users. We have two distinct functions: one for registration, which upon completion automatically triggers the verification email; and another to programmatically resend verification when requested by the user:

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");
   }

As you can see, in the register() function we obtain the user data, create them, log them in, and call sendEmailVerificationNotification() to automatically trigger the verification email. Note that calling Auth::attempt() before sending is important: some verification mechanisms require the user to be authenticated to correctly generate the signed link.

The verifie() function is an independent endpoint that you can call programmatically whenever you need to resend the verification email without going through the registration flow again. In this example we use Laravel Sanctum with bearer tokens, but the same approach works with any authentication guard you have configured.

Video thumbnail

GREAT TRICK to send automated and bulk emails in Laravel with Livewire

I am going to show you how to send emails in an automated way every 7, 5, or 4 seconds —or the interval you determine— using Laravel Livewire. Every time the counter advances, an email is sent to a different subscriber. It is a simple but surprisingly efficient system to manage newsletters or notification campaigns without the need for complex external queues.

I have used it myself to send around 1,000 emails in less than two hours. If you have a database of, say, 9,000 users, you can calculate the approximate time or even parallelize the process by opening multiple browser tabs at the same time. If you don't use Livewire, don't worry: at the end of the article I explain how to replicate this exact scheme with a bit of JavaScript.

The panel I use to manage shipments drafts the email content in HTML thanks to the CKEditor plugin. If you are interested in integrating it with Laravel, I have several videos and articles about it on the blog.

Implementation

The core of this solution is a Livewire component that leverages its reactive nature to execute actions on the server periodically. I explain the key points:

1. Counter management (subPage)

I use a property called $subPage that starts at zero. This property is essential for the resilience of the process: if an error occurs —the internet drops or the server fails—, I can see in which block the last shipment stayed and resume from that exact point. This allows me, for example, to send blocks of 300 emails, let the server rest for a few minutes, and continue afterwards without losing track.

<?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; // current sub
    ***
    // send email to each sub
    function sendEmailSubscriptions()
    {
        // SEND TO ALL
        $subscribe = Subscribe::select('name', 'email', 'id')
            ->where('active', $this->typeSubscriptors) // Selected Subs group
            ->offset($this->subPage) // page that varies in the poll
            ->limit(1) // only one
            ->orderBy('id')->first();
    }
}

2. The sending function (sendEmailSubscriptions)

The query logic is very deliberate: I look for a single subscriber using limit(1)->first() with an offset() that increases in each cycle. As long as there are records, the process continues; as soon as the query returns null, it stops automatically and the process ends without manual intervention:

class SubscribeSendEmail extends Component
{
    ***
    // send email to each sub
    function sendEmailSubscriptions()
    {
        // SEND TO ALL
        $subscribe = Subscribe::select('name', 'email', 'id')
            ->where('active', $this->typeSubscriptors) // Selected Subs group
            ->offset($this->subPage) // page that varies in the poll
            ->limit(1) // only one
            ->orderBy('id')->first();

        ***
}

3. The secret: wire:poll and the .keep-alive modifier

The true "magic" of this scheme is Livewire's wire:poll directive. This instruction tells the component how often it should refresh automatically, invoking a server function periodically without the user having to do anything.

  • Interval: I use 7 seconds (7000ms). I have tested with 3 or 4 seconds and it works well, but 7 seconds is a safe margin not to saturate third-party services like Hostinger or Gmail and avoid timeout errors (timeout).
  • Keep-alive mode: The .keep-alive modifier (formerly called .always in older versions of Livewire) ensures that the process continues even if you switch browser tabs. Without it, the poll would stop upon losing window focus.

The component's blade with the poll active:

resources/views/livewire/dashboard/subscribe-send-email.blade.php

@if ($active)
    <div wire:poll.7000ms.keep-alive="startSendEmailOne">
        <h3>Current Value: {{ $subPage }}</h3>
        <p class="m-0">This value updates automatically every 7 seconds.</p>
    </div>
    <label for="">SubPage</label>
@endif

The complete code of the Livewire component for automated sending:

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
{
    // object
    public SubscriptionContent $subscriptionContent;

    // info
    public $subscribes = [];
    public $countSubscribes = 0;

    // to send email, poll is activated and if active
    public $subPage = 0; // current sub
    public $active = false; // sending emails

    ***  

    // to move to the next sub to send the email
    public function startSendEmailOne()
    {
        $this->submit();
        $this->subPage++;
    }

    // this function starts the individual email sending process
    public function submit()
    {
        // activates poll
        $this->active = true;
        // sends the first email
        $this->sendEmailSubscriptions();
    }

    // send email to each sub
    function sendEmailSubscriptions()
    {
        // SEND TO ALL
        $subscribe = Subscribe::select('name', 'email', 'id')
            ->where('active', $this->typeSubscriptors) // Selected Subs group
            ->offset($this->subPage) // page that varies in the poll
            ->limit(1) // only one
            ->orderBy('id')->first();

        // if no sub, end
        if (!$subscribe) {
            $this->active = false;
            return;
        }

        // *** Testing
        Log::info("$this->subPage - $subscribe->id $subscribe->email");

        // app email
        $smtpUser = config('mail')['mailers']['smtp']['username'];

        // prepares mail to send email
        $mailer = Mail::build([
            'transport' => 'smtp',
            //'transport' => 'log', // *** FOR LOCAL TESTING
            'host' => 'smtp.domain.com',
            'port' => 465,
            'encryption' => 'ssl',
            'username' => $smtpUser,
            'password' => config('mail')['mailers']['smtp']['password'],
        ]);

        // Send email
        $mailer
            ->to($subscribe->email)
            ->send(
                (new SubscribeEmail($subscribe->email, $subscribe->name ?? $subscribe->email, $this->subscriptionContent->title, $this->subscriptionContent->content))
                    ->from($smtpUser, 'Andrés Cruz')
            );
    }
}

And the complete component blade:

resources/views/livewire/dashboard/subscribe-send-email.blade.php

@if ($active)
    <div wire:poll.7000ms.keep-alive="startSendEmailOne">
        <h3>Current Value: {{ $subPage }}</h3>
        <p class="m-0">This value updates automatically every 7 seconds.</p>
    </div>
    <label for="">SubPage</label>
@endif
<x-button class="mt-2 mb-3" wire:click="startSendEmailOne">
     {{ __('Start Send Email') }}
</x-button>

Optimization and Concurrency

If you need to speed up the process, you can open multiple instances of the panel in different tabs and assign a different range of subscribers to each using $subPage. For example:

  • Tab 1: Sends from subscriber 0 to 4,500.
  • Tab 2: Sends from subscriber 4,501 to 9,000.

This allows concurrent sending and reduces the total time practically by half. An important note: the 7-second interval is not arbitrary. If you reduce the interval too much —for example, to half a second— and the mail server takes 2 seconds to respond, you will accumulate requests faster than they are processed and end up saturating the queue, which will cause 500 errors and loss of mailings without simple recovery.

JavaScript Alternative

If you prefer not to depend on Livewire, you can replicate this exact behavior with setInterval() or setTimeout() in JavaScript. The idea is the same: every X seconds you make a fetch() or AJAX request to a Laravel endpoint that processes and sends the next email on the list. It is slightly more verbose than the Livewire approach, but just as effective and perfectly valid if your stack does not include this package.

Extra: Logging into a Hostinger email from Gmail

Configuring email reception in Gmail (Check mail from other accounts)

  1. Open your Gmail account on your computer.
  2. Click on the Settings icon (the gear in the top right corner) and select See all settings.
  3. Go to the Accounts and Import (or Accounts) tab.
  4. In the Check mail from other accounts section, click on Add a mail account.
  5. Type your Hostinger email address and click Next.
  6. Choose Import emails from my other account (POP3). Although Hostinger recommends IMAP for complete synchronization across devices, Gmail uses POP3 in this specific "Check mail" section.

Incoming configuration

Enter your incoming mail server details:

  1. Username: Your full Hostinger email address.
  2. Password: Your Hostinger email password.
  3. POP/IMAP server: pop.hostinger.com / smtp.hostinger.com
  4. Port: 995 (POP3) / 993 (IMAP)
  5. Check the recommended boxes —such as leaving a copy on the server or using a secure SSL/TLS connection— and click Add account.

Outgoing configuration

Enter your outgoing mail server details:

  1. Username: Your full Hostinger email address.
  2. Password: Your Hostinger email password.
  3. SMTP server: smtp.hostinger.com — Port 465 with SSL encryption.

The next step is for you to learn how you can extend Laravel with key features like detecting whether navigation is mobile or PC.

A practical guide to sending emails in Laravel: configure SMTP with Hostinger or Mailtrap, create a Mailable, verify users, and automate bulk mailings using Livewire wire:poll.


Únete a la comunidad de desarrolladores que han decidido dejar de picar código y empezar a construir productos reales. Recibe mis mejores trucos de arquitectura cada semana:

I agree to receive announcements of interest about this Blog.