Mailables
A mailable is one class per kind of email: make:mail generates it, and envelope, content and attachments configure it.
php artisan make:mail OrderShipped --markdown=mail.orders.shippedA mailable is a class representing one kind of email your application sends: one subject, one template, one set of attachments. Mailables live in app/Mail and Artisan writes the first one:
php artisan make:mail OrderShipped
The generated class extends Illuminate\Mail\Mailable and uses the Queueable and SerializesModels traits. Three methods configure it.
Envelope, content, attachments
envelope() returns an Illuminate\Mail\Mailables\Envelope: subject, from, replyTo, cc, bcc, tags, metadata. The stub fills the subject with Str::headline() of the class name, so OrderShipped ships as "Order Shipped". With no from, the sender comes from the from block in config/mail.php, which reads MAIL_FROM_ADDRESS.
content() returns a Content naming the Blade view. view: and html: are the same parameter under two names; text: adds a plain-text part, and markdown: renders through the framework's pre-styled components instead.
attachments() returns an array of Illuminate\Mail\Mailables\Attachment, built with fromPath(), fromStorage(), fromStorageDisk() or fromData(), then renamed with ->as() and ->withMime().
What hands data to the view
Two routes, easily confused. Every public property on the mailable is copied into the template automatically, so a promoted public Order $order gives Blade an $order. A protected or private property is not, and has to be listed in the Content object's with: array. Changing one property's visibility makes the variable vanish from the view, and the class itself still looks fine.
Example
<?php
// app/Mail/OrderShipped.php
declare(strict_types=1);
namespace App\Mail;
use App\Models\Order;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Address;
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;
/**
* $order is public, so the view receives $order.
* $invoicePdf is protected, so it does not reach the view.
*/
public function __construct(
public Order $order,
protected string $invoicePdf,
) {}
public function envelope(): Envelope
{
return new Envelope(
from: new Address('[email protected]', 'Example Orders'),
replyTo: [new Address('[email protected]', 'Example Support')],
subject: 'Order '.$this->order->reference.' has shipped',
tags: ['shipment'],
metadata: ['order_id' => $this->order->id],
);
}
public function content(): Content
{
return new Content(
view: 'mail.orders.shipped',
text: 'mail.orders.shipped-text',
with: [
'trackingUrl' => $this->order->trackingUrl(),
],
);
}
/**
* @return array<int, Attachment>
*/
public function attachments(): array
{
return [
Attachment::fromData(fn (): string => $this->invoicePdf, 'invoice.pdf')
->withMime('application/pdf'),
];
}
}When to use it
- Send an order confirmation with the invoice attached.
- Give a transactional email both an HTML and a plain-text part so text-only clients read it.
- Preview an email in the browser by returning the mailable from a route.
- Tag outgoing mail so the provider can report delivery per feature.
More examples
The view the mailable names
Both ways of passing data arrive as ordinary Blade variables, which is why the distinction is invisible in the template and only visible in the class. The $message variable is injected into every mail view by the framework, plain-text views included; embed() attaches the file and returns its cid: reference. In the plain-text half of a Markdown mailable it is wrapped in Illuminate\Mail\TextMessage, whose embed() and embedData() return an empty string, because plain text has no inline attachments.
{{-- resources/views/mail/orders/shipped.blade.php --}}
{{-- 'mail.orders.shipped' maps dots to directories under resources/views --}}
<h1>Order {{ $order->reference }}</h1>
<p>Shipped on {{ $order->shipped_at->toFormattedDateString() }}.</p>
{{-- $order comes from the public property, $trackingUrl from with: --}}
<p><a href="{{ $trackingUrl }}">Track this parcel</a></p>
<p>
Here is the carrier logo as an inline attachment:
<img src="{{ $message->embed(storage_path('app/carrier.png')) }}" alt="">
</p>The Markdown variant
Reach for Markdown when you want a responsive, inlined-CSS email without writing table-based HTML. The button component takes a url and an optional color of primary, success or error. Run php artisan vendor:publish --tag=laravel-mail to copy the components and resources/views/vendor/mail/html/themes/default.css into your application when the default styling is wrong for your brand.
{{-- resources/views/mail/orders/shipped.blade.php, generated by
php artisan make:mail OrderShipped --markdown=mail.orders.shipped
with content() returning: new Content(markdown: 'mail.orders.shipped') --}}
<x-mail::message>
# Order Shipped
Your order has been shipped.
<x-mail::button :url="$trackingUrl" color="success">
Track this parcel
</x-mail::button>
<x-mail::panel>
Delivery usually takes two working days.
</x-mail::panel>
<x-mail::table>
| Item | Qty |
| ------------- | --: |
| Keyboard | 1 |
</x-mail::table>
Thanks,<br>
{{ config('app.name') }}
</x-mail::message>Attachments from a disk, and from a model
fromPath reads the local filesystem, fromStorage and fromStorageDisk read a configured disk, and fromData takes a closure returning bytes for something generated in memory. Implementing Illuminate\Contracts\Mail\Attachable on a model moves the naming and disk choice into the model, so every mailable that attaches it agrees on the filename.
// In a mailable: three sources, same Attachment object.
public function attachments(): array
{
return [
Attachment::fromPath(storage_path('app/terms.pdf'))
->as('terms.pdf')
->withMime('application/pdf'),
Attachment::fromStorageDisk('s3', $this->order->invoice_path)
->as('invoice-'.$this->order->reference.'.pdf'),
// An Attachable model may be returned directly.
$this->order->shippingLabel,
];
}
// app/Models/ShippingLabel.php
class ShippingLabel extends Model implements Attachable
{
public function toMailAttachment(): Attachment
{
return Attachment::fromStorage($this->path)->as($this->filename);
}
}
Discussion