Most Laravel apps end up sending marketing-style email sooner or later: a welcome message, a product update, an invoice that should look like it came from a company and not a terminal. The usual options are a Blade template you hand-write once and nobody dares touch, or a SaaS editor that owns your templates.
I wanted a third option, so Odden has a package for it, and it is open source. You can try the editor now without installing anything.
Why email HTML is hard
Email clients are not browsers. Outlook on Windows renders with Word's engine, Gmail strips what it dislikes, and a layout that collapses nicely on a phone needs tables, inlined styles and conditional comments. Writing that by hand is where the afternoon goes.
getodden/mail hides it behind blocks. A hero, a features row, a button, a footer: each is a slot with a few fields, and the compiler turns the list into the table-based, inlined, responsive HTML that survives the trip. There are 29 blocks, and the compiler is plain PHP, so there is no Node build step at send time.
The editor
The same package ships a drag-and-drop editor as a web component:
<odden-mail-editor
schema-url="/admin/mail-editor/schema"
preview-url="/admin/mail-editor/preview"></odden-mail-editor>
You drag blocks from a palette, edit them in a side panel, and the preview is not a lookalike: the server renders it with the same compiler that builds the email you send. The preview runs in a sandboxed iframe with scripts disabled. If you use Filament, there are EmailSlotBuilder and MailEditor fields so it drops into a resource.
The demo page is that editor, unchanged. It keeps nothing: no accounts, no saving, no sending. There is a button to download the finished HTML.
Or skip the editor and use PHP
Everything the editor does is available in code:
use Odden\MailBuilder\MailBuilder;
use Odden\MailBuilder\Enums\SlotType;
$doc = MailBuilder::document(subject: 'Welcome', previewText: 'Two minutes to get set up');
$doc->append(SlotType::Hero, [
'title' => 'Welcome aboard',
'button_text' => 'Open your dashboard',
'button_url' => 'https://example.com/dashboard',
]);
$html = MailBuilder::compile($doc);
$text = MailBuilder::plainText($doc);
The plain-text part matters more than it looks. Mail with no text alternative scores worse with spam filters, and it is the thing nobody remembers to write.
Audit before you send
The package also checks what it built:
$report = MailBuilder::audit($html);
$report['spam_score'];
$report['critical_issues'];
$report['warnings'];
MailBuilder::wcagContrast('#2563eb', '#ffffff');
MailBuilder::validateDns('yourdomain.com'); // SPF, DMARC, BIMI
None of this replaces sending a test to a few real inboxes. It catches the dull mistakes first: a button that fails contrast, an image-only email, a sending domain with no DMARC record.
Sending through your own mailer
The package does not send mail on its own terms. It gives you a Mailable:
Mail::to($user)->send(new TemplateMailable(
template: $doc,
data: ['name' => $user->name],
subjectLine: 'Welcome',
embedCidImages: true,
));
So it uses whatever mailer you already configured: SES, Postmark, SMTP. Merge tags with conditionals are filled from data, and the headers for RFC 8058 one-click unsubscribe are there when you need them.
What it is not
It is a builder and a compiler. It does not keep contacts, lists, consent records or suppression lists, and it does not run campaigns. That is deliberate: those are the parts where getting it wrong hurts people, and they belong in an application.
If you want them, that is what Odden CRM and Odden Cloud are for. The paid products use this package; the package does not check for a license and never will.
One thing I fixed on the way
Two of the blocks, the RSS and dynamic feed blocks, fetch a web address on the server when an email is compiled. In 2.5.0 that fetch did not check where the address pointed, so a template author could aim it at an internal address. I fixed that in 2.5.1: the fetch now accepts public addresses only, pins the address it resolved, follows no redirects, and caps the time and the size of what it reads. If you use the package, please update. The details are in the security advisory.
The demo hides those two blocks and the raw HTML block, because anyone on the internet can use it.
Try it
The quickest way is the live demo: drag a few blocks together and download the HTML. To use it in your own app:
composer require getodden/mail
It needs Laravel 12 or 13 and PHP 8.3 or newer. The README covers the editor, the blocks and sending, and the package is on Packagist.
I would like to hear which block you reached for and did not find.