Laravel Mail & Notifications: Beyond echo 'Email sent'
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
-
Use Mailtrap in development. Never risk sending test emails to real users.
-
Notifications > Mailables for user communication. Notifications give you multi-channel support from day one.
-
Queue everything. Even if you only have one email, queue it. Users shouldn’t wait for SMTP.
-
Markdown emails are beautiful. The built-in components handle responsive design and dark mode.
-
Check
$notifiablepreferences. 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.
Saurav Sitaula
Software Architect • Nepal