Mailable classes — make:mail, build(), envelope(), content()
Concept
A Mailable class is Laravel's object-oriented abstraction over email composition. Instead of building emails imperatively, you define a class that encapsulates the message's data, view, subject, and recipients. Laravel introduced the modern Mailable API in version 5.3 and has refined it significantly since — the current API uses envelope() and content() methods alongside the older build() approach, with both supported in Laravel 10+.
When you run php artisan make:mail OrderShipped, Laravel generates a class extending Illuminate\Mail\Mailable. The framework's Mailer class (backed by Symfony Mailer since Laravel 9, which replaced SwiftMailer) calls build() or the envelope/content pair to construct a Symfony\Component\Mime\Email object before handing it to the configured transport.
The envelope() method returns an Illuminate\Mail\Mailables\Envelope instance that controls routing metadata: from, replyTo, cc, bcc, and subject. The content() method returns an Illuminate\Mail\Mailables\Content instance specifying which Blade view or Markdown template to render, along with any with data to pass. This separation of concerns — routing metadata vs. body content — is cleaner than the old build() approach where both concerns lived in one method.
Public properties on a Mailable are automatically available in Blade views without needing ->with(). This is a convenience powered by buildViewData() in Illuminate\Mail\Mailable, which reflects over the object and collects all public properties. If you need computed values or want to avoid exposing properties to the view namespace, use the with key in Content explicitly.
The attachments() method returns an array of Illuminate\Mail\Mailables\Attachment instances. You can attach from raw data (Attachment::fromData()), from a Storage disk path (Attachment::fromStorage()), or from a filesystem path. Always avoid attaching files by absolute server path in production — use Storage disk references so the attachment logic is portable across environments and drivers.
| Method | Purpose | Returns |
|---|---|---|
envelope() | Subject, from, cc, bcc | Envelope |
content() | View/markdown, with-data | Content |
attachments() | File attachments | Attachment[] |
build() (legacy) | All of the above combined | $this |
Code Example
<?php
namespace App\Mail;
use App\Models\Order;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Attachment;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
class OrderShipped extends Mailable
{
use Queueable, SerializesModels;
public function __construct(
public readonly Order $order,
) {}
public function envelope(): Envelope
{
return new Envelope(
subject: "Your Order #{$this->order->number} Has Shipped",
replyTo: [
new Address('support@example.com', 'Support Team'),
],
);
}
public function content(): Content
{
return new Content(
markdown: 'emails.orders.shipped',
with: [
'trackingUrl' => $this->order->trackingUrl(),
'estimatedDelivery' => $this->order->estimated_delivery->format('M j, Y'),
],
);
}
public function attachments(): array
{
return [
Attachment::fromStorage("invoices/order-{$this->order->id}.pdf")
->as('Invoice.pdf')
->withMime('application/pdf'),
];
}
}
// Sending:
use Illuminate\Support\Facades\Mail;
Mail::to($user->email)
->cc('ops@example.com')
->send(new OrderShipped($order));
// Queue it instead:
Mail::to($user)->queue(new OrderShipped($order));Interview Q&A
Q: What is the difference between envelope() and content() in a modern Laravel Mailable, and why was this separation introduced?
The envelope() method handles transport-level metadata — who the mail goes to/from, the subject, cc/bcc, and reply-to addresses. The content() method handles body rendering — which view or markdown template to use and what data to pass to it. The separation mirrors SMTP semantics more accurately than the old build() method, which conflated routing and rendering in a single method chain. It also makes it easier to swap content templates without touching addressing logic, and vice versa. The older build() approach still works but is considered legacy.
Q: How does Laravel pass public properties of a Mailable to its Blade view automatically?
Laravel's Mailable::buildViewData() uses PHP's ReflectionClass to inspect the mailable instance, collects all public properties, and merges them into the view data array alongside anything passed via with. This means any public property declared on the class is available in Blade templates by its property name, without any explicit passing. This is convenient but can be a footgun — public properties you add for other reasons (like controlling queueing behavior) will also leak into the view namespace. Prefer explicit with data for computed or derived values.
Q: What is SerializesModels and why is it important on a Mailable that may be queued?
SerializesModels is a trait that intercepts PHP's serialize() and unserialize() lifecycle to replace Eloquent model instances with a lightweight identifier (model class + primary key) during serialization, then re-fetch the model from the database during unserialization. Without it, the entire model — plus any loaded relationships — would be serialized into the queue payload, potentially creating multi-megabyte jobs and stale data problems if the model changes between dispatch and execution. With it, the queue stores just the ID, and the worker fetches a fresh copy when it runs.