A sign-up form, an order confirmation, a password-reset link… almost every flow in a modern application involves email. The Laravel mail layer makes this far less daunting than you might expect: Mailable classes represent a single email, while Notification classes let you deliver the same message across several channels at once, such as mail, the database and Slack. In this guide we'll walk through when to use each, how to design your templates, and how to make delivery reliable.
Configuring SMTP and choosing a driver
Everything starts in config/mail.php and .env. Laravel supports drivers like SMTP, Amazon SES, Postmark, Mailgun and log. During development the log driver writes messages to storage/logs/laravel.log, so you can inspect output without sending real mail.
MAIL_MAILER=smtp
MAIL_HOST=smtp.mailgun.org
MAIL_PORT=587
MAIL_USERNAME=postmaster@example.com
MAIL_PASSWORD=secret
MAIL_ENCRYPTION=tls
MAIL_FROM_ADDRESS="hello@example.com"
MAIL_FROM_NAME="${APP_NAME}"
For local testing, a tool like Mailpit or Mailtrap is invaluable: every outgoing email is captured in a web inbox and nothing ever reaches real users.
Your first Mailable
Generate a class that represents a single email with Artisan. The --markdown flag scaffolds a ready-made template:
php artisan make:mail OrderShipped --markdown=mail.orders.shipped
Since Laravel 9, the Mailable is split into three methods: envelope() (subject, sender), content() (view and data) and attachments(). The result is clean and easy to read:
class OrderShipped extends Mailable
{
use Queueable, SerializesModels;
public function __construct(public Order $order) {}
public function envelope(): Envelope
{
return new Envelope(
subject: 'Your order is on its way',
);
}
public function content(): Content
{
return new Content(
markdown: 'mail.orders.shipped',
with: ['order' => $this->order],
);
}
}
Sending it takes only the Mail facade:
use Illuminate\Support\Facades\Mail;
Mail::to($order->user)->send(new OrderShipped($order));
Designing with Markdown templates
Markdown mailables use Laravel's prebuilt components and automatically render both an HTML and a plain-text version. The template ships with a clean, responsive theme:
<x-mail::message>
# Hello {{ $order->user->name }}
Your order ({{ $order->number }}) has shipped.
<x-mail::button :url="$url">
View Order
</x-mail::button>
Thanks,<br>
{{ config('app.name') }}
</x-mail::message>
To match the theme to your brand, run php artisan vendor:publish --tag=laravel-mail to publish the components and the resources/css/mail.css file. You edit colours, the logo and the footer right there.
Multi-channel delivery with Notifications
When you want to announce the same event over more than one channel, Notification takes over. For example, when an invoice is paid you may want to send an email and also store a record in the database.
php artisan make:notification InvoicePaid
class InvoicePaid extends Notification
{
use Queueable;
public function __construct(public Invoice $invoice) {}
public function via(object $notifiable): array
{
return ['mail', 'database'];
}
public function toMail(object $notifiable): MailMessage
{
return (new MailMessage)
->subject('Your invoice was paid')
->greeting('Hello!')
->line('Your invoice for '.$this->invoice->amount.' has been paid.')
->action('View Invoice', url('/invoices/'.$this->invoice->id))
->line('Thank you.');
}
public function toArray(object $notifiable): array
{
return ['invoice_id' => $this->invoice->id];
}
}
If your model uses the Notifiable trait, sending is a single line:
$user->notify(new InvoicePaid($invoice));
If you use the database channel in via(), don't forget to create the table with php artisan make:notifications-table. Alongside official channels such as Slack, SMS (Vonage) and broadcast, community packages add Telegram or WhatsApp.
Queueing and error handling
Sending email is slow work; never make the user wait for an SMTP response. Implementing the ShouldQueue interface pushes the mail or notification onto the queue automatically:
class OrderShipped extends Mailable implements ShouldQueue
{
use Queueable, SerializesModels;
// ...
}
A worker is required for the queue to run: php artisan queue:work. In production keep it alive with Supervisor. Failed jobs land in the failed_jobs table and can be re-run with php artisan queue:retry. For transient SMTP errors you can define $tries and a backoff() on the Mailable to control automatic retry intervals.
Testing
You don't need real SMTP to test sending. Mail::fake() intercepts delivery so you can assert against it:
Mail::fake();
// ... process the order ...
Mail::assertSent(OrderShipped::class, function ($mail) use ($order) {
return $mail->order->id === $order->id;
});
Likewise, Notification::fake() and Notification::assertSentTo() let you test notifications. This approach keeps your tests fast and independent of external services.
Frequently Asked Questions
Should I use a Mailable or a Notification?
If you're sending a rich, custom-designed message through a single channel (email only), a Mailable is enough. If you want to deliver the same event across several channels (mail + database + Slack) or let the user choose their channels, a Notification is the better fit.
Why are my emails sent late or not at all?
The most common cause is using ShouldQueue without running a worker. Check that queue:work is running, inspect the failed_jobs table and storage/logs, and verify your SMTP credentials and port settings.
How do I keep HTML emails out of spam?
A verified sending domain, SPF/DKIM/DMARC records and a plain-text alternative give the most reliable results. Markdown mailables already generate the plain-text version; also prefer a reputable delivery provider such as SES, Postmark or Mailgun.
Want to build your email flow from scratch or tidy up an existing notification system? Let's design the Laravel mail and notification stack around your needs. Get in touch with me.