henrikbjorn/stampie-bundle
Symfony bundle that integrates Stampie, providing a configured mailer service for sending transactional email through providers like SendGrid, Mailgun, Postmark, and Mandrill. Configure API credentials in Symfony and inject the mailer where needed.
The bundle is a thin Symfony DI wrapper around stampie/stampie. The first place to look is the bundle README for the stampie: config tree, then the Stampie\Mailer\* classes in the library for the provider-specific options each mailer accepts.
Version note (v2.1.1): this release fixes Symfony 5 compatibility. If you are on Symfony 5, pin at least this version — earlier 2.1.x releases will fail to compile the container:
composer require henrikbjorn/stampie-bundle:^2.1.1
The package is low-activity rather than dead: fixes still land, but slowly, and only for the versions someone has sponsored/PR'd. Treat the supported-version window as narrow and verify it against your own Symfony version before committing.
// config/bundles.php
return [
// ...
Stampie\StampieBundle::class => ['all' => true],
];
# config/packages/stampie.yaml
stampie:
mailers:
default:
type: mailgun
options:
api_key: '%env(MAILGUN_API_KEY)%'
domain: '%env(MAILGUN_DOMAIN)%'
stampie service (the default mailer alias):use Stampie\Message;
$message = new Message();
$message->setFrom('[email protected]');
$message->setTo($user->getEmail());
$message->setSubject('Reset your password');
$message->setText('Open this link to reset your password: ...');
$message->setHtml('<p>Open <a href="...">this link</a> to reset your password.</p>');
$this->get('stampie')->send($message);
$providerMessageId = $message->getMessageId(); // store for support/debugging
The canonical first use case is exactly this: password resets, receipts, and notifications where you want a provider HTTP API instead of SMTP.
Alias the mailer for autowiring. The bundle exposes services as stampie.mailer.<name>; stampie points at the default. Add an explicit alias so type-hinting works regardless of bundle version:
services:
Stampie\Mailer:
alias: stampie.mailer.default
final class PasswordResetNotifier
{
public function __construct(private \Stampie\Mailer $mailer) {}
public function notify(User $user, string $token): void
{
$message = new \Stampie\Message();
$message->setFrom('[email protected]');
$message->setTo($user->getEmail());
$message->setSubject('Reset your password');
$message->setText(sprintf('Reset link: https://example.com/reset/%s', $token));
$this->mailer->send($message);
}
}
Centralize message construction. Keep a small MessageFactory or application-level Mailer service that knows your domain objects and template rendering. Controllers and command handlers should not repeat setFrom/setHtml logic.
Use multiple mailers for routing. Define separate mailers for transactional vs. marketing traffic (different providers, API keys, or subaccounts):
stampie:
mailers:
default:
type: postmark
options: { api_key: '%env(POSTMARK_KEY)%' }
marketing:
type: sendgrid
options: { api_key: '%env(SENDGRID_KEY)%' }
Retrieve them with stampie.mailer.marketing, or build a small registry/locator in your app if you need runtime selection by tenant or feature.
Queue via Symfony Messenger. Stampie uses synchronous HTTP calls, so sending in a web request adds provider latency. Dispatch a Messenger message containing scalar data (recipient, template name, parameters), and let the handler build and send a fresh Stampie\Message. Do not serialize Stampie\Message itself — attachments and internal state do not queue cleanly.
Decorate/wrap for logging and metrics. A wrapper service around Stampie\Mailer is simpler and safer than decorating the abstract mailer service directly:
final class LoggingMailer
{
public function __construct(
private \Stampie\Mailer $inner,
private \Psr\Log\LoggerInterface $logger,
) {}
public function send(\Stampie\MessageInterface $message): void
{
try {
$this->inner->send($message);
$this->logger->info('Email sent', ['id' => $message->getMessageId()]);
} catch (\Stampie\Exception\ApiException $e) {
$this->logger->error('Email failed', ['exception' => $e]);
throw $e;
}
}
}
Retry carefully. Catch Stampie\Exception\ApiException, inspect the status code and provider response, and only retry transport/5xx-style failures. Retrying after a 2xx response is not possible to detect reliably, so avoid blind retries that can duplicate emails. Symfony's Retry component or Messenger's retry strategy both work; log the provider message ID when available.
Testing. In config/services_test.yaml, alias Stampie\Mailer to an in-memory fake (a subclass of Stampie\Mailer implementing doSend, or a test double) and assert on recorded messages. Alternatively, if your bundle version exposes Stampie's logger mailer (type: logger), use it in dev/test and assert on logs.
Pin and watch the version. Because fixes arrive as point releases rather than a maintained LTS line, pin an explicit constraint (^2.1.1) and read the changelog before jumping a minor. A container that compiles today can break on an unrelated composer update if you allow *.
bin/console cache:clear); DI extension changes are compiled into the container, so a stale cache can mask or mimic the very error the patch fixes. Also re-check any services.yaml overrides that touched the bundle's service definitions.Stampie\Mailer\<Provider> implementation still targets a live API endpoint; a successful container build does not mean the provider will accept the request. This is unrelated to the Symfony 5 fix and remains the most common silent failure.config/bundles.php registration fails, register manually; if DI compilation fails, check for deprecated service-definition APIs or PHP version mismatch.%env(...)%. Remember that Symfony compiles config: changing an env var in production generally requires a cache clear unless you use runtime env resolution.stampie points to. The default alias can be driven by a default_mailer-style option depending on version. When in doubt, inject the explicit stampie.mailer.<name> service or define your own alias.from must be verified. Most providers reject unverified senders/domains. The resulting ApiException usually contains the provider's explanation — log the exception message and response body, not just the status code.setTo() puts them all in the To header. Use Bcc or send individual messages when recipients should not see each other.setHeaders(), setTag(), and setMetadata() only after confirming the specific mailer supports them.Stampie\Mailer, implement doSend, register it as a service, and wire it through your own application mailer. The bundle's type switch will not know custom types unless you override the DI extension. If you find yourself patching the DI extension for Symfony compatibility, this is also the place to fork.How can I help you explore Laravel packages today?