Laravel Mail & Notifications: Beyond echo 'Email sent'

SS Saurav Sitaula

My PHP mail() days were over. Laravel's Mailable classes, notification system, and queue workers transformed how I thought about user communication. Beautiful emails, multiple channels, all from one notification class.

The mail() Days

In raw PHP, sending email looked like this:

$to = "user@example.com";
$subject = "Welcome!";
$message = "Thanks for signing up!";
$headers = "From: noreply@myapp.com";

mail($to, $subject, $message, $headers);

It worked sometimes. Other times emails vanished into the void. No HTML support. No attachments. No idea if it actually sent.

After setting up Laravel authentication, I needed to send welcome emails, password resets, and order confirmations. Time to learn Laravel Mail.

Configuring Mail

First, the .env setup:

MAIL_DRIVER=smtp
MAIL_HOST=smtp.mailtrap.io
MAIL_PORT=2525
MAIL_USERNAME=your_mailtrap_username
MAIL_PASSWORD=your_mailtrap_password
MAIL_FROM_ADDRESS=noreply@myapp.com
MAIL_FROM_NAME="My App"

I used Mailtrap for development — it catches all emails so you don’t accidentally spam real users while testing.

For production, switch to a real provider:

# SendGrid
MAIL_DRIVER=smtp
MAIL_HOST=smtp.sendgrid.net
MAIL_PORT=587
MAIL_USERNAME=apikey
MAIL_PASSWORD=your_sendgrid_api_key

# Or Mailgun, SES, etc.

Creating a Mailable

php artisan make:mail WelcomeEmail

This creates app/Mail/WelcomeEmail.php:

<?php

namespace App\Mail;

use App\User;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;

class WelcomeEmail extends Mailable
{
    use Queueable, SerializesModels;

    public $user;

    public function __construct(User $user)
    {
        $this->user = $user;
    }

    public function build()
    {
        return $this->subject('Welcome to My App!')
                    ->view('emails.welcome');
    }
}

The Blade template at resources/views/emails/welcome.blade.php:

<!DOCTYPE html>
<html>
<head>
    <style>
        body { font-family: Arial, sans-serif; line-height: 1.6; }
        .container { max-width: 600px; margin: 0 auto; padding: 20px; }
        .button { 
            background: #3490dc; 
            color: white; 
            padding: 12px 24px; 
            text-decoration: none; 
            border-radius: 4px;
            display: inline-block;
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>Welcome, {{ $user->name }}!</h1>
        
        <p>Thanks for joining My App. We're excited to have you!</p>
        
        <p>Here's what you can do next:</p>
        <ul>
            <li>Complete your profile</li>
            <li>Explore our features</li>
            <li>Connect with other users</li>
        </ul>
        
        <p>
            <a href="{{ url('/dashboard') }}" class="button">
                Go to Dashboard
            </a>
        </p>
        
        <p>
            Best regards,<br>
            The My App Team
        </p>
    </div>
</body>
</html>

Send it:

use App\Mail\WelcomeEmail;
use Illuminate\Support\Facades\Mail;

// After user registration
Mail::to($user->email)->send(new WelcomeEmail($user));

Beautiful HTML emails with one line of code.

Markdown Mailables

Writing HTML email templates is painful. Laravel has Markdown mailables:

php artisan make:mail OrderConfirmation --markdown=emails.orders.confirmation
public function build()
{
    return $this->markdown('emails.orders.confirmation')
                ->subject('Order Confirmation #' . $this->order->id);
}

The template uses Blade components:

@component('mail::message')
# Order Confirmed!

Thank you for your order, {{ $order->user->name }}.

**Order #{{ $order->id }}**

@component('mail::table')
| Item       | Quantity | Price    |
|:---------- |:--------:| --------:|
@foreach($order->items as $item)
| {{ $item->name }} | {{ $item->quantity }} | ${{ $item->price }} |
@endforeach
| **Total** | | **${{ $order->total }}** |
@endcomponent

@component('mail::button', ['url' => url('/orders/' . $order->id)])
View Order
@endcomponent

Thanks,<br>
{{ config('app.name') }}
@endcomponent

Laravel renders this as a beautiful, responsive HTML email. The same Markdown also works as plain text fallback.

Customize the design:

php artisan vendor:publish --tag=laravel-mail

Now you can edit the templates in resources/views/vendor/mail/.

Enter: Notifications

Emails are great. But what if users want notifications in multiple places?

  • Email for some things
  • SMS for urgent things
  • In-app notifications for everything
  • Slack for admin alerts

Laravel Notifications handle all of this with one class.

php artisan make:notification OrderShipped
<?php

namespace App\Notifications;

use App\Order;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Notification;
use Illuminate\Notifications\Messages\MailMessage;

class OrderShipped extends Notification
{
    use Queueable;

    protected $order;

    public function __construct(Order $order)
    {
        $this->order = $order;
    }

    // Which channels to use
    public function via($notifiable)
    {
        return ['mail', 'database'];
    }

    // Email version
    public function toMail($notifiable)
    {
        return (new MailMessage)
            ->subject('Your Order Has Shipped!')
            ->greeting('Hello ' . $notifiable->name . '!')
            ->line('Great news! Your order #' . $this->order->id . ' has shipped.')
            ->line('Tracking number: ' . $this->order->tracking_number)
            ->action('Track Order', url('/orders/' . $this->order->id))
            ->line('Thank you for shopping with us!');
    }

    // Database version (for in-app notifications)
    public function toDatabase($notifiable)
    {
        return [
            'order_id' => $this->order->id,
            'message' => 'Your order #' . $this->order->id . ' has shipped!',
            'tracking_number' => $this->order->tracking_number,
        ];
    }
}

Send it:

use App\Notifications\OrderShipped;

// Single user
$user->notify(new OrderShipped($order));

// Or via the Notification facade
Notification::send($users, new OrderShipped($order));

One notification, multiple channels. The user gets an email AND a database record for in-app display.

Database Notifications

For in-app notification bells, you need the notifications table:

php artisan notifications:table
php artisan migrate

This creates a notifications table with type, data, read_at, etc.

Display in Blade:

@foreach(auth()->user()->unreadNotifications as $notification)
    <div class="notification">
        <p>{{ $notification->data['message'] }}</p>
        <small>{{ $notification->created_at->diffForHumans() }}</small>
    </div>
@endforeach

{{-- Mark all as read --}}
@if(auth()->user()->unreadNotifications->count())
    <form action="/notifications/read" method="POST">
        @csrf
        <button type="submit">Mark all as read</button>
    </form>
@endif

Controller:

public function markAsRead()
{
    auth()->user()->unreadNotifications->markAsRead();
    return back();
}

Custom Notification Channels

Laravel supports email, database, broadcast, and Slack out of the box. But I needed SMS.

Using Nexmo (now Vonage):

composer require laravel/nexmo-notification-channel
NEXMO_KEY=your_key
NEXMO_SECRET=your_secret
NEXMO_SMS_FROM=15551234567

Add SMS to your notification:

public function via($notifiable)
{
    // Check user preferences
    $channels = ['database'];
    
    if ($notifiable->email_notifications) {
        $channels[] = 'mail';
    }
    
    if ($notifiable->sms_notifications && $notifiable->phone) {
        $channels[] = 'nexmo';
    }
    
    return $channels;
}

public function toNexmo($notifiable)
{
    return (new NexmoMessage)
        ->content('Your order #' . $this->order->id . ' has shipped! Track: ' . $this->order->tracking_number);
}

The User model needs a method to provide the phone number:

// app/User.php
public function routeNotificationForNexmo()
{
    return $this->phone;
}

Now users with SMS enabled get a text message too.

Queued Notifications

Sending emails is slow. If you send 100 welcome emails synchronously, your page hangs for 30 seconds.

Make notifications queueable:

class OrderShipped extends Notification implements ShouldQueue
{
    use Queueable;
    
    // ...
}

That’s it. Now notifications go to the queue.

Set up the queue worker:

QUEUE_CONNECTION=database
php artisan queue:table
php artisan migrate
php artisan queue:work

The queue:work command runs in the background, processing notifications one by one. Your user’s request completes instantly.

For production, use Supervisor to keep the worker running:

[program:laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/myapp/artisan queue:work --sleep=3 --tries=3
autostart=true
autorestart=true
user=www-data
numprocs=2
redirect_stderr=true
stdout_logfile=/var/www/myapp/storage/logs/worker.log

Notification Events

Track what happened:

// app/Providers/EventServiceProvider.php
protected $listen = [
    'Illuminate\Notifications\Events\NotificationSent' => [
        'App\Listeners\LogNotification',
    ],
];
// app/Listeners/LogNotification.php
public function handle(NotificationSent $event)
{
    Log::info('Notification sent', [
        'user' => $event->notifiable->id,
        'notification' => get_class($event->notification),
        'channel' => $event->channel,
    ]);
}

Email Previews in Browser

Testing emails by actually sending them is slow. Preview in browser:

// routes/web.php (development only!)
if (app()->environment('local')) {
    Route::get('/mail-preview', function () {
        $user = App\User::first();
        return new App\Mail\WelcomeEmail($user);
    });
}

Visit /mail-preview and see the rendered email instantly.

Real-World Notification Example

Here’s a complete notification from my project:

<?php

namespace App\Notifications;

use App\Order;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Notification;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Messages\NexmoMessage;

class PaymentReceived extends Notification implements ShouldQueue
{
    use Queueable;

    protected $order;

    public function __construct(Order $order)
    {
        $this->order = $order;
    }

    public function via($notifiable)
    {
        $channels = ['mail', 'database'];
        
        // SMS for high-value orders
        if ($this->order->total > 1000 && $notifiable->phone) {
            $channels[] = 'nexmo';
        }
        
        return $channels;
    }

    public function toMail($notifiable)
    {
        return (new MailMessage)
            ->subject('Payment Received - Order #' . $this->order->id)
            ->markdown('emails.payment-received', [
                'order' => $this->order,
                'user' => $notifiable,
            ]);
    }

    public function toDatabase($notifiable)
    {
        return [
            'type' => 'payment_received',
            'order_id' => $this->order->id,
            'amount' => $this->order->total,
            'message' => 'Payment of $' . number_format($this->order->total, 2) . ' received for order #' . $this->order->id,
        ];
    }

    public function toNexmo($notifiable)
    {
        return (new NexmoMessage)
            ->content('Payment of $' . number_format($this->order->total, 2) . ' received for your order. Thank you!');
    }
}

One class handles email, database, and SMS. Channel logic is centralized. Adding Slack later? Just add 'slack' to via() and implement toSlack().

What I Wish I’d Known Earlier

  1. Use Mailtrap in development. Never risk sending test emails to real users.

  2. Notifications > Mailables for user communication. Notifications give you multi-channel support from day one.

  3. Queue everything. Even if you only have one email, queue it. Users shouldn’t wait for SMTP.

  4. Markdown emails are beautiful. The built-in components handle responsive design and dark mode.

  5. Check $notifiable preferences. Let users choose how they want to be notified.

The Journey Continues

Email and notifications were handled. But my app also needed to send API responses — not just HTML pages.

React was becoming popular, and I needed to build a proper API alongside my Blade templates.

Laravel had me covered there too.


P.S. — The first time I saw a queued notification get processed, picked up by the worker, and delivered as both an email and an SMS within seconds — I felt like a real backend developer. Laravel’s notification system is genuinely one of the best I’ve used in any framework.

SS

Saurav Sitaula

Software Architect • Nepal

Back to all posts
Saurav.dev

© 2026 Saurav Sitaula.AstroNeoBrutalism