sohaibilyas/plunk-laravel
A Laravel integration for Plunk, the open-source email platform, making it easy to send transactional and marketing email through Plunk's API from your application. Includes a simple mail transport, configuration, and Laravel-friendly helpers for quick setup.
Minimal path from install to first send:
composer require <vendor>/<package> — pin to ^3.0.2 if you use standard Laravel password reset notifications (see Gotchas).php artisan vendor:publish --tag=<package>-config and set the API key/endpoint in .env.config/mail.php under mailers without any edit on your side.MAIL_MAILER=<package> in .env, or per-send:Mail::mailer('<package>')->to($user)->send(new WelcomeMail($user));
Where to look first: the published config file (endpoint, key, timeout, debug/serialized-request logging), then the Mail facade's mailer() calls already in your app to decide global vs. per-send selection.
First use case: a queued transactional mailable with one or two custom headers. It exercises the whole surface — transport selection, header serialization, and queue behavior — in a few lines.
Per-notification mailer selection. Keep the default SMTP mailer for internal mail and opt into the package only for customer-facing transactional sends:
public function via($notifiable): array
{
return [Channels\MailChannel::class];
}
public function toMail($notifiable): Mailable
{
return (new OrderShipped($this->order))
->mailer('<package>');
}
Custom headers. Set them on the mailable (via Symfony message headers or the package's header API, depending on your version) and treat the value as a flat string map:
$mailable->withSymfonyMessage(function (Email $message) {
$message->getHeaders()->addTextHeader('X-Order-Id', (string) $this->order->id);
});
Only add a header when you actually have a value — don't pass [], null, or an empty collection through.
Queued sends. Because queue retry idempotency is handled by the package, failed jobs can be retried (php artisan queue:retry) without producing duplicate deliveries. Let the package own the retry semantics; don't wrap sends in your own dedupe layer.
Standard Laravel flows stay untouched. Password resets, email verification, and other framework notifications use the default MailMessage path with no custom headers. On ^3.0.2 these serialize correctly. If you have a custom ResetPassword notification that subclasses the framework one, verify it still sends after upgrading.
Testing. Use Mail::fake() for assertions in feature tests, and reserve real-HTTP tests for a small suite that hits a sandbox endpoint with serialized-request logging enabled — that's where header-shape regressions actually surface.
Upgrade to 3.0.2 before shipping password resets. Prior versions serialized an absent custom-header set as a JSON array ([]) instead of omitting it, producing an invalid API payload. The symptom is a rejected/422 request for mail you never customized — most visibly the stock Laravel password reset notification. Populated headers were always sent as objects; only the empty case was broken. Pin ^3.0.2 and add one regression test that sends a header-less notification.
Don't confuse empty with absent. The fix is "omit when empty," not "send {}." If you build headers dynamically, filter the map before handing it over; a header with an empty-string value is still a header and may be rejected by the API.
Don't double-register the mailer. Auto-registration happens on boot. Adding your own Mail::extend or a mailers entry for the same name causes confusing precedence bugs where config changes appear to be ignored.
Retry semantics depend on package-internal state. Idempotency is unchanged in this release, so retries are safe — but if you clone or re-dispatch a mailable manually, you're outside that guarantee. Prefer queue:retry over re-sending.
Debugging serialization. Turn on the serialized-request logging in the config (never in production) to see exactly what's posted. That log is the fastest way to tell a header-shape problem from an auth or endpoint problem.
Extension points. Custom header classes and a swapped transport binding are the two supported layers; keep business logic out of both. Compatibility is validated on Laravel 12 and 13 — if you're on an older major, treat this release as untested and check the CI matrix before upgrading.
How can I help you explore Laravel packages today?