I’ve been using Unsend all over the place now that I’m self-hosting it.

In my Laravel apps I’d started out using a generic SMTP connection before realizing a simple custom transport could connect with the Unsend API directly.

The documentation was straightforward, and the bulk of the effort was mapping Laravel’s message details to a suitable API payload and testing it.

I started by creating app/Mail/Transports/UnsendTransport.php:

<?php

namespace App\Mail\Transports;

use Illuminate\Support\Facades\Http;
use Illuminate\Support\Uri;
use Symfony\Component\Mailer\SentMessage;
use Symfony\Component\Mailer\Transport\AbstractTransport;
use Symfony\Component\Mime\Address;
use Symfony\Component\Mime\MessageConverter;
use Symfony\Component\Mime\Part\DataPart;

class UnsendTransport extends AbstractTransport
{
    /**
     * {@inheritDoc}
     */
    protected function doSend(SentMessage $message): void
    {
        $email = MessageConverter::toEmail($message->getOriginalMessage());
        $apiKey = env('UNSEND_API_KEY');
        $baseUrl = Uri::to('/')
            ->withHost(env('UNSEND_DOMAIN'))
            ->withScheme('https')
            ->value();

        if ($from = collect($email->getFrom())->first()) {
            $fromValue = $from->toString();
        }

        $postBody = [
            'to' => collect($email->getTo())->map(function (Address $email) {
                return $email->toString();
            })->toArray(),
            'from' => $fromValue ?? null,
            'subject' => $email->getSubject(),
            'replyTo' => collect($email->getReplyTo())->map(function (Address $email) {
                return $email->getAddress();
            })->toArray(),
            'cc' => collect($email->getCc())->map(function (Address $email) {
                return $email->toString();
            })->toArray(),
            'bcc' => collect($email->getBcc())->map(function (Address $email) {
                return $email->toString();
            })->toArray(),
            'text' => $email->getTextBody(),
            'html' => $email->getHtmlBody(),
            'attachments' => collect($email->getAttachments())->map(function (DataPart $part) {
                return [
                    'filename' => $part->getFilename(),
                    'content' => base64_encode($part->getBody()),
                ];
            })->toArray(),
        ];

        Http::withHeaders([
            'Content-Type' => 'application/json',
            'Authorization' => 'Bearer '.$apiKey,
        ])
            ->baseUrl($baseUrl)
            ->post('/api/v1/emails', $postBody);
    }

    /**
     * Get the string representation of the transport.
     */
    public function __toString(): string
    {
        return 'unsend';
    }
}

This needs to be registered in app/providers/AppServiceProvider.php:

/**
 * Bootstrap any application services.
 */
public function boot(): void
{
    // ...

    Mail::extend('unsend', static function () {
        return new \App\Mail\Transports\UnsendTransport;
    });
}

You can then define a mailer in config/mail.php:

// ...
'mailers' => [
    'unsend' => [
        'transport' => 'unsend',
    ],
],

Finally, designate the mailer and add your API credentials:

MAIL_MAILER=unsend
UNSEND_API_KEY=us_•••••••••••••••••••••••••••••••••••••••••••
UNSEND_DOMAIN=unsend.example.com

You can see if it works by opening a Tinker shell with php artisan tinker, then using the Mail facade to send a quick test:

Mail::raw('Hi!', function($m) { $m->to('me@example.com')->subject('Test Email'); });