Skip to content

Mailables

A Mailable is a class that builds one email. It's an alternative to views and markdown files — reach for it when an email needs real logic: pulling in order data, formatting line items, branching on the contact.

Everything else about Courier stays the same. Tracking, suppression, and unsubscribe handling all work exactly as they do for view-based sends.

Writing a Mailable

Extend CourierMailable and put your composition in build():

<?php

namespace App\Mail;

use Myth\Courier\Mailables\CourierMailable;

class WelcomeMailable extends CourierMailable
{
    protected function build(): void
    {
        $this->from('hello@acme.com', 'Acme Team')
            ->to($this->contact->email)
            ->subject('Welcome aboard, ' . $this->contact->first_name . '!')
            ->html(view('App\Mail\Views\welcome', [
                'contact' => $this->contact,
            ]));
    }
}

build() runs once, lazily, at send time. Before it runs, Courier sets two properties for you:

Property Type What it is
$this->contact ContactDTO The recipient — always set
$this->campaign ?CampaignDTO The originating campaign, or null for a transactional send

The fluent helpers (from, to, subject, html, text) come straight from postal's Mailable, so anything postal supports is available here.

Tracking still works

After your Mailable renders, Courier post-processes the HTML just like a view: it rewrites links for click tracking and expands the same placeholders.

$this->html(
    '<p>Thanks for joining!</p>'
    . '<p><a href="{courier_unsubscribe_url}">Unsubscribe</a></p>'
    . '{courier_tracking_pixel}'
);

See Tracking placeholders for the full list.

Include the unsubscribe link

The default layout adds {courier_unsubscribe_url} for you, but a Mailable composes its own HTML — so it's on you to include an opt-out link. CAN-SPAM and GDPR both require one.

Using a Mailable in a campaign

Set the mailable field instead of view when you create the campaign:

<?php
$campaignService->create([
    'name'       => 'Welcome Blast',
    'subject'    => 'Welcome!',
    'from_name'  => 'Acme Team',
    'from_email' => 'hello@acme.com',
    'mailable'   => \App\Mail\WelcomeMailable::class,
    'type'       => 'blast',
]);

When both are present, the Mailable winsview is ignored. The Mailable also owns the subject and sender, since it sets those itself in build().

Using a Mailable in a drip step

Same idea on a step. A step always needs a view (it's required), so think of mailable as an override layered on top:

<?php
$campaignService->addDripStep($campaign->id, [
    'subject'     => 'Getting started',          // ignored when mailable is set
    'view'        => 'App\Views\emails\fallback', // required, but overridden
    'mailable'    => \App\Mail\GettingStartedMailable::class,
    'delay_hours' => 24,
]);

The step's mailable takes precedence over both the step's view and the campaign's. Leave mailable off to fall back to the view.

Transactional sends

Not every email belongs to a campaign. Receipts, password resets, and other one-offs go out through MailerService::sendMailable():

<?php
$contact = service('contactService')->getContact('buyer@example.com');

service('mailerService')->sendMailable(
    new \App\Mail\ReceiptMailable($order),
    $contact,
);

This records a send with no campaign attached (campaign_id is null), runs the same suppression check, applies tracking, and logs the result. A suppressed recipient returns false and is recorded with the suppressed status — it is never delivered.

Your Mailable can take constructor arguments

sendMailable() takes an already-built instance, so pass whatever the email needs (an order, an invoice) into the constructor. Courier sets $contact before calling build().

Next steps

  • Tracking — opens, clicks, suppression, and one-click unsubscribe
  • Email Templates — the view and markdown approach
  • Campaigns — blasts and audience targeting