stampie/stampie-bundle
Symfony bundle that integrates the Stampie email library, enabling transactional email delivery through multiple API-based mail providers. It provides framework-native configuration and services for quickly connecting your app to providers such as Mailgun, SendGrid, and Postmark.
Stampie is an API-first mailer library: it talks to transactional email providers (SendGrid, Mailgun, Postmark, Mandrill, SparkPost) over HTTP instead of SMTP. The v2.1.1 release only touches stampie/stampie-bundle, which is a Symfony bundle — in a Laravel app you almost never install the bundle. Install the library instead:
composer require stampie/stampie
You also need an HTTP adapter dependency. Stampie ships with a Guzzle adapter, so:
composer require guzzlehttp/guzzle
First working use case — send one message from a queued job or a service class:
use Stampie\Adapter\Guzzle;
use Stampie\Mailer\Postmark;
use Stampie\Message;
$mailer = new Postmark(new Guzzle(new \GuzzleHttp\Client()), config('services.postmark.token'));
$message = new Message();
$message->setFrom('[email protected]', 'Example');
$message->setTo('[email protected]');
$message->setSubject('Welcome');
$message->setText('Plain text body');
$message->setHtml('<p>HTML body</p>');
$ok = $mailer->send($message); // bool; failures throw, they don't return false silently
Where to look first:
Stampie\Mailer\* — pick the class matching your provider; each has a getEndpoint() constant you can override for EU/sandbox regions.Stampie\MessageInterface / Stampie\Message — the payload object. Subclass or implement the interface if you need custom headers or per-provider fields.Stampie\Adapter\* — the transport. Guzzle adapter is the default; swap it for a fake adapter in tests.Stampie\Exception\* — HttpException (transport-level) vs ApiException (provider rejected the payload, with the decoded error body).If you are on an older Laravel (5.8–6.x) that pins Symfony 4 components, v2.1.1 is the release that widens the bundle's Symfony constraint to include 5.x — but again, only relevant if you actually pull in the bundle.
Bind the mailer in a service provider, not inline. Resolve it from the container so tests can swap it:
// AppServiceProvider::register()
$this->app->singleton(\Stampie\Mailer::class, function ($app) {
$adapter = new \Stampie\Adapter\Guzzle(new \GuzzleHttp\Client([
'timeout' => 10,
'connect_timeout' => 5,
]));
return new \Stampie\Mailer\SendGrid($adapter, config('services.sendgrid.token'));
});
Then type-hint Stampie\Mailer $mailer in jobs, listeners, and controllers. Because Mailer is an abstract base, the container binding lets you flip providers per environment (Mailgun in staging, Postmark in production) without touching call sites.
Wrap it in a Laravel Mailable-shaped class. Stampie's Message is a mutable value object with a different API than Laravel's Mailable; don't leak it into your domain code. Create an adapter:
class StampieTransport
{
public function __construct(private \Stampie\Mailer $mailer) {}
public function send(Mailable $mailable): void
{
$message = new \Stampie\Message();
$message->setFrom(config('mail.from.address'), config('mail.from.name'));
$message->setTo($mailable->to[0]['address']);
$message->setSubject($mailable->subject);
$message->setHtml($mailable->render());
$message->setText(/* strip tags fallback */);
$this->mailer->send($message);
}
}
This keeps the eventual "go back to SMTP" migration cheap.
Always send from a queue job. API mailers are fast but not free-form-safe — a provider 429 or 500 should be retried, not surfaced to the request. Implement ShouldQueue, set $tries = 3 and $backoff = [10, 60, 300], and let Stampie\Exception\HttpException bubble so the queue marks it failed. Catch ApiException separately: a 422 (bad address, unverified sender) will never succeed on retry, so $this->fail($e) immediately rather than burning attempts.
Use tags/metadata for observability. Most Stampie mailer classes accept provider-specific headers via $message->addHeader(). SendGrid X-SMTPAPI, Mailgun o:tag, Postmark Tag — map these from a single addTag() helper on your wrapper so provider webhooks can be correlated back to a NotificationLog model.
Test with a fake adapter. Implement Stampie\Adapter\AdapterInterface (just send()), assert on the raw payload your mailer produced, and bind it in a testing service provider. This is faster and more deterministic than hitting a sandbox endpoint, and it lets you assert on the exact JSON shape per provider.
Multi-provider fallback. Because the mailer is just an object, you can build a small FailoverMailer implementing Stampie\Mailer that loops a list of inner mailers and re-throws only if all fail. Useful when one provider has a regional outage. Keep the list short — a two-provider fallback covers most incidents.
stampie/stampie-bundle in Laravel. v2.1.1 is exclusively a bundle release (Symfony 5 constraint fix). Installing it drags in Symfony bundle/framework expectations, registers nothing useful for Laravel, and its symfony/* constraints can conflict with Laravel's pinned components during composer update. Use stampie/stampie only.send() throws, it does not return false on failure. Every call needs a try/catch or a queue. HttpException means the HTTP layer failed (timeout, DNS, 5xx); ApiException means the provider answered with an error — its getResponse() carries the decoded body, which is where the real reason lives (unverified sender, suppressed recipient, bad template variables).mail.from.address aligned with each provider's verified domain, and expect this to bite when you switch providers in staging.$mailer->setEndpoint(...) or a subclass). Sandbox endpoints likewise. There is no Laravel config for this — put it in config/services.php yourself.Message object is mutable and reused state is a classic bug. Construct a fresh Message per send; don't cache one on a singleton and mutate setTo() between calls, or you'll leak recipients between jobs under a long-running worker (Octane, Horizon).Message covers from/to/cc/bcc/subject/text/html/headers. Anything beyond that (attachments, templates, scheduled send, dynamic templates) needs either a custom MessageInterface implementation per provider or a drop to the provider SDK. Check the specific mailer class before promising a feature to a product owner.timeout and connect_timeout; the default has no timeout, which is how you get a wedged queue worker.Mailer binding rather than editing call sites — implement Stampie\Mailer, wrap a real one, and swap the container binding. That's also the cleanest place to plug in Laravel's Log and Event facades.stampie/stampie 2.x is stable and quiet; there is no Laravel-specific release cadence to track. If you need the Symfony bundle for a legacy Symfony service in the same monorepo, 2.1.1 is the minimum for Symfony 5 compat.How can I help you explore Laravel packages today?