Appearance
Mail
Mail ships as laraspring/mail, a Composer package your application installs. It owns one thing: the template every transactional email in the kit is drawn inside, and the preview browser you look at it in.
It does not own delivery. Laravel already has mailers for SES, Postmark, Resend, Mailgun and SMTP, and wrapping them in a second abstraction would only add a layer between you and a driver that already works. See providers for the env keys each one wants.
What you get out of the box
| Feature | Where |
|---|---|
A branded template every MailMessage renders through | mail.markdown.theme |
| Logo, product name, colour and footer as configuration | config/laraspring-mail.php |
| Blade components: button, paragraph, code, divider, panel | the mail:: namespace |
| A preview browser over the kit's real emails | /laraspring/mail, never in production |
| Plain-text alternatives for every email | rendered automatically |
Nothing depends on this package
laraspring/auth sends a magic link. laraspring/organizations sends an invitation. Neither of them mentions laraspring/mail, and neither of them needs to.
They build a plain MailMessage, which Laravel renders through the markdown mail views in the mail view namespace and inlines with the theme named in mail.markdown.theme. Both are configuration, so this package brands every email in the kit by adding one view path and naming one theme while it boots:
php
// Laraspring\Mail\MailServiceProvider
$config->set('mail.markdown.paths', [...$paths, __DIR__.'/../resources/views/mail']);
$config->set('mail.markdown.theme', 'laraspring');That is the whole mechanism, and it is why the dependency graph stays a tree. Blade components the notifications had to @include would have put laraspring/mail in the composer.json of every package that sends anything, turned a leaf package into a dependency of three others, and left an installation that removed it with three broken emails rather than three plain ones.
The path is appended to whatever is already configured, so resources/views/vendor/mail still wins if you published the views, and Laravel's own copies stay last as the fallback for any component this package does not override.
Set laraspring-mail.theme to false and every email falls back to Laravel's default template, with nothing else in the kit noticing.
The preview browser
php artisan serve
open http://localhost:8000/laraspring/mailEvery email the installation can send, rendered against fixture data, with a toggle for the plain-text alternative. Nothing is sent and nothing is written.
It is never registered in production. The check is config('laraspring-mail.preview.enabled') && ! app()->isProduction(), and the second half is not negotiable by configuration on purpose: an operator who copies a .env between environments should still not be able to publish a page that shows anonymous visitors what your sign-in emails look like.
Because the routes may be absent, a link to them is guarded like any link behind a feature flag:
tsx
{route().has('laraspring-mail.preview') && <a href={route('laraspring-mail.preview')}>Mail previews</a>}Registering a preview
The registry lives in laraspring/mail; the registrations live in your edition, which is the one thing that knows which packages are installed. The reference edition does it in App\Mail\LaraspringMailPreviews:
php
$registry->register(
MailPreview::make('auth.magic-link', 'Magic link', fn () => (new MagicLinkNotification(
url: url('/magic-link/preview-token'),
expiresInMinutes: 15,
))->toMail($user))
->group('Authentication')
->description('Sent when somebody asks to sign in without a password.'),
);The closure is called when the preview is opened, never at registration time, so a fixture may use the router or the database without slowing every boot down. Return a MailMessage or a Mailable; a MailMessage also gets a text preview, a Mailable only renders HTML.
Fixtures should be unsaved models. A preview that needs a seeded database is a preview nobody opens.
What laraspring/billing sends
Nothing. Stripe owns receipts, dunning and card-expiry warnings, and sending our own would mean two systems disagreeing about what a customer was told. Configure that in your provider's dashboard.